[PATCH v2 0/7] Add AFL++ fuzzing support for passt
This series adds integrated AFL++ fuzzing support for passt, extending the earlier work by AbdAlRahman Gad with persistent mode, bidirectional protocol fuzzing, and real TCP connection coverage via a companion test server. Each testcase is a flat buffer split into four regions: epoll events, raw L2 tap frames, test-server payloads, and getsockopt(TCP_INFO) overrides. AFL++ controls all four through mutation of a 10-byte header. Fuzz-injected epoll events are interleaved with real kernel events so protocol handshakes can complete. A standalone test server (fuzz-server) attaches to AFL++'s shared memory and sends fuzzer-controlled payload on every accepted TCP connection. AnyIP routing in a rootless user+network namespace makes every destination local, so the test server intercepts all outbound traffic from passt without mocking recv(). Deterministic wrappers replace clock_gettime(), getsockopt() and assert() to eliminate non-determinism from kernel state. Sandboxing (seccomp, namespaces, close_range, capabilities) is bypassed under FUZZING builds since AFL++ needs its own fds and syscalls. *** BLURB HERE *** Anshu Kumari (7): fuzz: Add AFL++ shared memory testcase buffer layout fuzz: Add deterministic wrappers for assert, clock and getsockopt fuzz: Guard protocol handlers against invalid fuzz-injected state fuzz: Bypass sandboxing for fuzzing builds fuzz: Add AFL++ persistent mode fuzz loop fuzz: Add host-side test server for bidirectional fuzzing fuzz: Add build targets, namespace setup and documentation Makefile | 35 +++- flow.c | 52 +++++ fuzz-server.c | 343 +++++++++++++++++++++++++++++++++ fuzz-testbuf.h | 135 +++++++++++++ fuzz.c | 102 ++++++++++ fuzz.h | 38 ++++ fuzzing/README.fuzzing.md | 129 +++++++++++++ fuzzing/fuzz-setup.sh | 24 +++ fuzzing/testcase_dir/empty.bin | Bin 0 -> 12 bytes icmp.c | 8 +- isolation.c | 23 +++ passt.c | 198 +++++++++++++++++++ tap.c | 13 ++ tcp.c | 18 +- tcp_buf.c | 1 + tcp_splice.c | 4 +- udp.c | 29 ++- udp_flow.c | 3 +- util.c | 1 + 19 files changed, 1131 insertions(+), 25 deletions(-) create mode 100644 fuzz-server.c create mode 100644 fuzz-testbuf.h create mode 100644 fuzz.c create mode 100644 fuzz.h create mode 100644 fuzzing/README.fuzzing.md create mode 100755 fuzzing/fuzz-setup.sh create mode 100644 fuzzing/testcase_dir/empty.bin -- 2.55.0
Define the shared memory layout that both passt and the test
server use to interpret AFL++ testcase data. Each testcase
starts with a 10-byte header carrying explicit lengths:
[0..3] u32 n_events epoll events to inject
[4..5] u16 tap_len L2 tap frame data
[6..7] u16 testbuf_len test-server payload
[8..9] u16 sockopt_len getsockopt() overrides
followed by four variable-length payload regions:
(a) events: simulated epoll events for passt's main loop
(b) tap: length-prefixed raw L2 frames
(c) testbuf: payload the test server sends on connections
(d) sockopt: fuzzer-controlled TCP_INFO data
fuzz_parse_layout() reads the header and computes each
region's offset and length.
Signed-off-by: Anshu Kumari
Add fuzz_assert() macro that calls _exit(0) instead of abort()
so AFL++ treats assertion failures as normal exits rather than
crashes.
Add deterministic replacements for clock_gettime() and
getsockopt() that eliminate non-determinism from kernel state:
- fuzz_clock_gettime(): returns a monotonically incrementing
timestamp from a fixed baseline, reset each AFL++ iteration
- fuzz_getsockopt(): returns fuzzer-controlled TCP_INFO from
AFL++ shared memory (region d), and fixed values for
SO_ERROR/SO_RCVBUF/SO_SNDBUF
Signed-off-by: Anshu Kumari
Use fuzz_assert() instead of assert() for flow lookups, add NULL
checks after conn_at_sidx()/udp_at_sidx(), and validate timer
references — all no-ops in non-fuzzing builds.
Signed-off-by: Anshu Kumari
Integrate AFL++ persistent mode into main(). Each iteration
resets clock, flow table and epoll state, then parses the
testcase buffer into epoll events, tap frames and TCP_INFO
data. Real and fuzz-injected events are interleaved so
protocol handshakes can complete. fuzz-server is fork+exec'd
once before the forkserver starts.
Add fuzz_flow_cleanup() in flow.c to close sockets and
timerfds leaked between iterations.
Signed-off-by: Anshu Kumari
Skip isolation steps that prevent AFL++ from operating:
- close_range(): AFL++ needs its shared memory fds open
- User namespace and setuid/setgid: the fuzzer runs in a
separate rootless namespace, not passt's own
- Capability dropping: not needed without real isolation
- Seccomp BPF filters: the fuzz loop uses syscalls
(epoll_create1, fork, execl).
Signed-off-by: Anshu Kumari
Add fuzz-server, a standalone program that acts as a host-side
peer for TCP connections from passt during fuzzing.
It attaches to AFL++'s shared memory segment (via the inherited
__AFL_SHM_FUZZ_ID env var) and sends region (c) payload on
every accepted connection and after each read, enabling
bidirectional protocol fuzzing without mocking recv().
Combined with AnyIP routing in the fuzzing namespace, fuzz-server
listens on TCP ports (1-55535) on 0.0.0.0 to intercept every
outbound connection from passt regardless of destination IP or
port.
Fork+exec'd by passt before __AFL_INIT(), so it starts once
in the forkserver parent and persists across iterations.
Signed-off-by: Anshu Kumari
Add Makefile targets for building and running AFL++ fuzzing:
- fuzz: AFL++-instrumented binary with AddressSanitizer
- fuzz-server: host-side test server
Add supporting scripts and data:
- fuzzing/fuzz-setup.sh: creates a rootless user+network namespace
with AnyIP routing so fuzz-server intercepts all outbound TCP
from passt, no root access needed
- fuzzing/testcase_dir/empty.bin: minimal seed input
- fuzzing/README.fuzzing.md: prerequisites, build instructions,
namespace setup, single and multi-core fuzzing invocations,
architecture overview, seed inputs, reproducing crashes
Signed-off-by: Anshu Kumari
participants (1)
-
Anshu Kumari