On Wed, Aug 12, 2026 at 12:56:28PM +0530, Anshu Kumari wrote:
Add fuzz-server that acts as passt's network peer during fuzzing. It connects to passt's UNIX socket and listens on 127.0.0.1:9999 for TCP connections.
As discussed in my other reply, I'm not really convinced a test server makes sense at all, at least in this form. So, I won't have much to say on the details here.
UNIX socket path: responds to ARP requests and TCP SYNs with stateless replies (swapped addresses, fixed ISN). Responses are XOR'd with AFL++ shared memory data so the fuzzer can mutate server behavior.
What's the reason for having a fixed response that's then mutated, rather than just generating the response directly from AFL's data?
TCP loopback path: accepts connections on port 9999, reads data, XOR-mutates with AFL++ shared memory, and echoes it back. Uses SO_LINGER(0) for immediate RST on close to avoid TIME_WAIT port exhaustion.
Turn-based synchronization via mmap'd flag in /dev/shm coordinates frame exchange between passt and the server.
Also adds: - fuzzing/testcase_dir/empty.bin - fuzzing/README.fuzzing.md
Signed-off-by: Anshu Kumari
--- Makefile | 8 +- fuzz-server.c | 490 +++++++++++++++++++++++++++++++++ fuzzing/README.fuzzing.md | 95 +++++++ fuzzing/testcase_dir/empty.bin | Bin 0 -> 12 bytes 4 files changed, 591 insertions(+), 2 deletions(-) create mode 100644 fuzz-server.c create mode 100644 fuzzing/README.fuzzing.md create mode 100644 fuzzing/testcase_dir/empty.bin diff --git a/Makefile b/Makefile index 8e4121e..4517bc7 100644 --- a/Makefile +++ b/Makefile @@ -125,17 +125,21 @@ valgrind: all
FUZZ_CC ?= afl-clang-fast
-.PHONY: fuzz +.PHONY: fuzz fuzz-server
fuzz: $(MAKE) clean $(MAKE) CC="$(FUZZ_CC)" CPPFLAGS="-DFUZZING -DNDEBUG" CFLAGS="-g -fsanitize=address" passt
Oh, sorry, forgot to mention this on the earlier patch: compiling with -DNDEBUG for fuzzing seems undesirable. Generally if fuzzed input can cause an assert() that's a bug, which we'd like to catch. -NDEBUG will suppress the assert()s, so we won't see it.
+fuzz-server: + $(CC) -D_GNU_SOURCE -O2 -o fuzz-server fuzz-server.c + .PHONY: clean clean: $(RM) $(BIN) *~ *.o seccomp.h seccomp_repair.h seccomp_pesto.h pasta.1 \ passt.tar passt.tar.gz *.deb *.rpm \ - passt.pid README.plain.md + passt.pid README.plain.md \ + fuzz-server
install: $(BIN) $(MANPAGES) docs mkdir -p $(DESTDIR)$(bindir) $(DESTDIR)$(man1dir) diff --git a/fuzz-server.c b/fuzz-server.c new file mode 100644 index 0000000..c9b4101 --- /dev/null +++ b/fuzz-server.c @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/* fuzz-server.c - Test server for bidirectional fuzz testing of passt + * + * Connects to passt's UNIX socket as a client. + * Reads outbound frames, generates protocol responses, XORs them + * with AFL++ shared memory data, and writes them back. + * + * Build: make fuzz-server + * Run: ./fuzz-server (after passt.fuzz is listening) + * + * Copyright Red Hat + * Author: Anshu Kumari
+ */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FUZZ_SOCK_PATH "/tmp/passt_fuzz_1.socket" +#define FUZZ_TURN_PATH "/dev/shm/passt_fuzz_turn" +#define FUZZ_XOR_OFF (12 + 64 * 1024) +#define MAX_FRAME 1600 +#define TCP_LISTEN_PORT 9999 +#define TCP_MAX_EVENTS 16 + +/** + * struct fuzz_turn - Turn-based synchronization flag (mmap'd shared memory) + * @turn: 0 = passt's turn, 1 = server's turn + */ +struct fuzz_turn { + uint32_t turn; +}; + +/** + * swap_eth_ip() - Swap Ethernet MACs and IPv4 addresses + * @pkt: Pointer to start of Ethernet frame + */ +static void swap_eth_ip(uint8_t *pkt) +{ + struct ethhdr *eth = (void *)pkt; + struct iphdr *iph = (void *)(eth + 1); + uint8_t tmp_mac[ETH_ALEN]; + uint8_t tmp_ip[4]; + + memcpy(tmp_mac, eth->h_source, ETH_ALEN); + memcpy(eth->h_source, eth->h_dest, ETH_ALEN); + memcpy(eth->h_dest, tmp_mac, ETH_ALEN); + + memcpy(tmp_ip, &iph->saddr, 4); + memcpy(&iph->saddr, &iph->daddr, 4); + memcpy(&iph->daddr, tmp_ip, 4); +} + +/** + * respond_arp() - Generate ARP reply for an ARP request + * @in: Incoming frame + * @in_len: Incoming frame length + * @out: Output buffer for response + * + * Swaps sender hardware and protocol addresses to form a + * valid ARP reply. Only responds to ARP requests (op=1). + * + * Return: response length, or 0 if not an ARP request + */ +static int respond_arp(const uint8_t *in, uint32_t in_len, uint8_t *out) +{ + /* Min ARP Frame size: eth(14) + ARP Header(8) + Payload(20) */ + if (in_len < 42) + return 0; + + const struct ethhdr *eth = (const void *)in; + const struct arphdr *arp = (const void *)(eth + 1); + + if (eth->h_proto != htons(ETH_P_ARP) || + arp->ar_op != htons(ARPOP_REQUEST)) + return 0; + + memcpy(out, in, in_len); + + struct ethhdr *out_eth = (void *)out; + struct arphdr *out_arp = (void *)(out_eth + 1); + + /* set MAC addresses for output buffer */ + memcpy(out_eth->h_dest, eth->h_source, ETH_ALEN); + memcpy(out_eth->h_source, eth->h_dest, ETH_ALEN); + + /* set ARP operation to REPLY */ + out_arp->ar_op = htons(ARPOP_REPLY); + + /* Swap ARP Sender and Target fields */ + uint8_t *sender = (uint8_t *)(out_arp + 1); + uint8_t *target = sender + 10; + uint8_t tmp[10]; + + memcpy(tmp, sender, 10); + memcpy(sender, target, 10); + memcpy(target, tmp, 10); + + return in_len; +} + +/** + * respond_tcp_syn() - Generate SYN-ACK for a TCP SYN + * @in: Incoming frame + * @in_len: Incoming frame length + * @out: Output buffer for response + * + * Swaps MAC/IP/port addresses and crafts a stateless SYN-ACK with + * a fixed ISN of 1000. Only responds to pure SYN (no ACK set). + * + * Return: response length, or 0 if not a TCP SYN + */ +static int respond_tcp_syn(const uint8_t *in, uint32_t in_len, uint8_t *out) +{ + const struct ethhdr *eth = (const void *)in; + const struct iphdr *iph; + const struct tcphdr *th; + struct tcphdr *out_th; + uint32_t ihl, hdr_len; + + /* Combined minimum length check for Eth + IP + TCP */ + if (in_len < sizeof(*eth) + sizeof(*iph) + sizeof(*th)) + return 0; + + if (eth->h_proto != htons(ETH_P_IP)) + return 0; + + iph = (const void *)(eth + 1); + ihl = iph->ihl * 4; + + if (iph->protocol != IPPROTO_TCP || + in_len < sizeof(*eth) + ihl + sizeof(*th)) + return 0; + + th = (const void *)((const uint8_t *)iph + ihl); + + /* Only respond to pure SYN packets (no ACK set) */ + if (!th->syn || th->ack) + return 0; + + hdr_len = sizeof(*eth) + ihl + sizeof(*th); + memcpy(out, in, hdr_len); + swap_eth_ip(out); + + /* Craft SYN-ACK response header */ + out_th = (struct tcphdr *)(out + sizeof(*eth) + ihl); + out_th->source = th->dest; + out_th->dest = th->source; + out_th->seq = htonl(1000); + out_th->ack_seq = htonl(ntohl(th->seq) + 1); + out_th->syn = 1; + out_th->ack = 1; + out_th->doff = 5; + out_th->check = 0; + + return hdr_len; +} + +/** + * generate_response() - Try all protocol responders on a frame + * @in: Incoming frame + * @in_len: Incoming frame length + * @out: Output buffer for response + * + * Tries ARP then TCP SYN responders in order. Returns the first + * successful response. + * + * Return: response length, or 0 if no responder matched + */ +static int generate_response(const uint8_t *in, uint32_t in_len, uint8_t *out) +{ + int len; + + if (in_len < sizeof(struct ethhdr)) + return 0; + + if ((len = respond_arp(in, in_len, out)) > 0) + return len; + if ((len = respond_tcp_syn(in, in_len, out)) > 0) + return len; + + return 0; +} + +/** + * map_afl_shm() - Map AFL++ shared memory for XOR mutation + * + * Reads __AFL_SHM_FUZZ_ID from the AFL++ environment + * variable and attaches the corresponding shared memory + * segment read-only. + * + * Return: pointer to mapped SHM, or NULL if not available + */ +static uint8_t *map_afl_shm(void) +{ + const char *id_str = getenv("__AFL_SHM_FUZZ_ID"); + int shm_id; + uint8_t *map; + + if (!id_str) + return NULL; + + shm_id = atoi(id_str); + map = shmat(shm_id, NULL, SHM_RDONLY); + if (map == (void *)-1) + return NULL; + + return map; +} + +/** + * apply_xor_mask() - XOR buffer with AFL++ shared memory data + * @buf: Buffer to mutate in place + * @len: Buffer length + * @afl_buf: AFL++ shared memory + * + * XORs each byte of @buf with the corresponding byte from the + * AFL++ buffer starting at offset FUZZ_XOR_OFF (65548). This + * lets AFL++ mutate server responses. + */ +static void apply_xor_mask(uint8_t *buf, int len, const uint8_t *afl_buf) +{ + int i; + + if (!afl_buf) + return; + + for (i = 0; i < len; i++) + buf[i] ^= afl_buf[FUZZ_XOR_OFF + i]; +} + +/* ---- TCP Host Listener (Loopback) ---- */ + +static int tcp_listen_fd = -1; +static int tcp_epfd = -1; + +/** + * tcp_listener_init() - Set up TCP listener on loopback port 9999 + * + * Creates an epoll instance and a non-blocking TCP listener on + * 127.0.0.1:9999 with SO_REUSEADDR. + * + * Return: 0 on success, -1 on failure + */ +static int tcp_listener_init(void) +{ + struct sockaddr_in sa = { + .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + .sin_port = htons(TCP_LISTEN_PORT), + }; + struct epoll_event ev; + int opt = 1; + + tcp_epfd = epoll_create1(EPOLL_CLOEXEC); + tcp_listen_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); + if (tcp_epfd < 0 || tcp_listen_fd < 0) + return -1; + + setsockopt(tcp_listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + if (bind(tcp_listen_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0 || + listen(tcp_listen_fd, 128) < 0) + return -1; + + ev.events = EPOLLIN; + ev.data.fd = tcp_listen_fd; + epoll_ctl(tcp_epfd, EPOLL_CTL_ADD, tcp_listen_fd, &ev); + + return 0; +} + +/** + * tcp_handle_accept() - Drain all pending TCP connections + * + * Sets SO_LINGER with l_linger=0 on every accepted connection, + * which does the immediate RST on connection close instead of + * waiting for connection to close on it's own. + */ +static void tcp_handle_accept(void) +{ + struct linger lg = { .l_onoff = 1, .l_linger = 0 }; + struct epoll_event ev; + int fd; + + /* Drain ALL pending connections in the backlog */ + while ((fd = accept4(tcp_listen_fd, NULL, NULL, + SOCK_NONBLOCK)) >= 0) { + setsockopt(fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)); + + ev.events = EPOLLIN | EPOLLRDHUP | EPOLLHUP | EPOLLERR; + ev.data.fd = fd; + epoll_ctl(tcp_epfd, EPOLL_CTL_ADD, fd, &ev); + } +} + +/** + * tcp_handle_data() - Read TCP data, XOR-mutate, echo back + * @fd: Connected TCP socket + * @afl_buf: AFL++ shared memory for XOR mask + */ +static void tcp_handle_data(int fd, const uint8_t *afl_buf) +{ + uint8_t buf[MAX_FRAME]; + ssize_t n, written, ret; + + n = read(fd, buf, sizeof(buf)); + if (n <= 0) { + epoll_ctl(tcp_epfd, EPOLL_CTL_DEL, fd, NULL); + close(fd); + return; + } + + apply_xor_mask(buf, (uint32_t)n, afl_buf); + + /* Ensure all n bytes are written, handling short writes */ + written = 0; + while (written < n) { + ret = write(fd, buf + written, (size_t)(n - written)); + if (ret <= 0) { + if (ret < 0 && (errno == EAGAIN || + errno == EWOULDBLOCK)) + continue; + + epoll_ctl(tcp_epfd, EPOLL_CTL_DEL, fd, NULL); + close(fd); + return; + } + written += ret; + } +} + +/** + * tcp_process_events() - Non-blocking poll for TCP events + * @afl_buf: AFL++ shared memory + */ +static void tcp_process_events(const uint8_t *afl_buf) +{ + struct epoll_event events[TCP_MAX_EVENTS]; + int nfds, i; + + nfds = epoll_wait(tcp_epfd, events, TCP_MAX_EVENTS, 0); + + for (i = 0; i < nfds; i++) { + int fd = events[i].data.fd; + + if (fd == tcp_listen_fd) { + tcp_handle_accept(); + } else if (events[i].events & (EPOLLIN | EPOLLRDHUP)) { + tcp_handle_data(fd, afl_buf); + } else if (events[i].events & (EPOLLHUP | EPOLLERR)) { + epoll_ctl(tcp_epfd, EPOLL_CTL_DEL, fd, NULL); + close(fd); + } + } +} + +/** + * create_turn_flag() - Create and mmap the turn synchronization flag + * + * Creates FUZZ_TURN_PATH in /dev/shm and maps it + * as shared memory. Both passt and fuzz-server map this file to + * coordinate turn-based frame exchange via atomic load/store. + * + * Return: pointer to mapped turn struct, or NULL on failure + */ +static struct fuzz_turn *create_turn_flag(void) +{ + struct fuzz_turn *t; + int fd; + + fd = open(FUZZ_TURN_PATH, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0) + return NULL; + + if (ftruncate(fd, sizeof(struct fuzz_turn)) < 0) { + close(fd); + return NULL; + } + + t = mmap(NULL, sizeof(*t), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + + if (t == MAP_FAILED) + return NULL; + + t->turn = 0; + return t; +} + +/** + * main() - Server entry point + * + * Outer loop reconnects to passt's UNIX socket on each AFL++ fork + * server restart. Inner loop handles turn-based TAP frame exchange + * and TCP events concurrently. + * + * Return: 0 on success, 1 on initialization failure + */ +int main(void) +{ + struct sockaddr_un addr = { .sun_family = AF_UNIX }; + uint8_t frame[MAX_FRAME], response[MAX_FRAME]; + struct fuzz_turn *turn; + uint8_t *afl_buf; + + strncpy(addr.sun_path, FUZZ_SOCK_PATH, sizeof(addr.sun_path) - 1); + + turn = create_turn_flag(); + if (!turn || tcp_listener_init() < 0) { + (void)fprintf(stderr, + "fuzz-server: tcp initialization failed\n"); + return 1; + } + + afl_buf = map_afl_shm(); + (void)fprintf(stderr, + "fuzz-server: ready, TCP listening on 127.0.0.1:%d\n", + TCP_LISTEN_PORT); + + /* + * Reconnect UNIX socket on passt restart — AFL++'s fork server + * restarts passt on each iteration. + */ + while (1) { + ssize_t n; + int sock; + + sock = socket(AF_UNIX, SOCK_SEQPACKET, 0); + if (sock < 0) + return 1; + + while (connect(sock, (struct sockaddr *)&addr, + sizeof(addr)) < 0) + usleep(10000); + + while (1) { + tcp_process_events(afl_buf); + + if (__atomic_load_n(&turn->turn, + __ATOMIC_ACQUIRE) == 1) { + n = recv(sock, frame, MAX_FRAME, + MSG_DONTWAIT); + if (n > 0) { + int resp_len; + + resp_len = generate_response( + frame, n, response); + if (resp_len > 0) { + apply_xor_mask(response, + resp_len, + afl_buf); + send(sock, response, + resp_len, MSG_NOSIGNAL); + } + } else if (n == 0 || + (n < 0 && errno != EAGAIN && + errno != EWOULDBLOCK)) { + __atomic_store_n(&turn->turn, 0, + __ATOMIC_RELEASE); + break; + } + + __atomic_store_n(&turn->turn, 0, + __ATOMIC_RELEASE); + } + } + + close(sock); + } + + return 0; +} diff --git a/fuzzing/README.fuzzing.md b/fuzzing/README.fuzzing.md new file mode 100644 index 0000000..f23e265 --- /dev/null +++ b/fuzzing/README.fuzzing.md @@ -0,0 +1,95 @@ +## Fuzzing passt with AFL++ + +### Prerequisites + +- AFL++ (afl-clang-fast, afl-fuzz) + +### Build + +``` +make fuzz +cp passt passt.fuzz +make fuzz-server +``` + +This produces: +- `passt.fuzz` -- instrumented with AFL++ and AddressSanitizer +- `fuzz-server` -- test server for bidirectional protocol fuzzing + +To use a specific AFL++ installation: + +``` +make FUZZ_CC=/path/to/afl-clang-fast fuzz +``` + +### Run + +Start the test server first, then the fuzzer: + +``` +# Terminal 1 -- test server: +./fuzz-server + +# Terminal 2 -- fuzzer: +afl-fuzz -i fuzzing/testcase_dir -o fuzzing/sync_dir \ + -- ./passt.fuzz --foreground +``` + +Multi-core (secondary instances share the corpus): + +``` +# Terminal 1 -- main instance: +afl-fuzz -M main -i fuzzing/testcase_dir -o fuzzing/sync_dir \ + -- ./passt.fuzz --foreground + +# Terminal 2 -- secondary with different power schedule: +afl-fuzz -S variant1 -p rare -i fuzzing/testcase_dir \ + -o fuzzing/sync_dir -- ./passt.fuzz --foreground +``` + +### Architecture + +The fuzzer uses AFL++ persistent mode with shared memory fuzzing. +Each iteration: + +1. Resets deterministic state (clock, flow table, epoll) +2. Reads an epoll event and packet data from AFL++ shared memory +3. For TAP events: injects a packet with fixed L2/L3/L4 headers + into the tap pipeline via tap_add_packet() + tap_handler(). + The TCP destination port is fixed to 9999 (the test server's + listening port). +4. Exchanges a turn flag with fuzz-server for bidirectional flow. +5. Calls passt_worker() to process the epoll event +6. Polls for host-side TCP events (connect completion, server data) + +The test server connects to passt's UNIX socket (SOCK_SEQPACKET) +and listens on 127.0.0.1:9999 for TCP connections. It responds to +ARP requests and TCP SYNs on the UNIX socket, and echoes TCP data +(XOR'd with AFL++ shared memory) on the loopback side. + +Deterministic wrappers in fuzz.c replace clock_gettime(), +getrandom(), getsockopt(TCP_INFO), and recv/recvmsg/recvfrom/ +recvmmsg to eliminate non-determinism from kernel state. The recv +wrappers return AFL++ buffer data for non-TAP file descriptors, +giving the fuzzer control over what passt "receives" from host +sockets. + +### Seed inputs + +`testcase_dir/empty.bin` is a 12-byte zero file +(sizeof(struct epoll_event)). AFL++ discovers packet formats +through mutation from this minimal seed. + +### Reproducing crashes + +``` +./passt.fuzz --foreground < \ + fuzzing/sync_dir/default/crashes/id:000000,... +``` + +Minimize a crash input: + +``` +afl-tmin -i fuzzing/sync_dir/default/crashes/id:000000,... \ + -o crash_minimized -- ./passt.fuzz --foreground +``` diff --git a/fuzzing/testcase_dir/empty.bin b/fuzzing/testcase_dir/empty.bin new file mode 100644 index 0000000000000000000000000000000000000000..ce58bc9f84b9623e708de4eb8427a57d9f9a160f GIT binary patch literal 12 KcmZQzKmY&$3;+QD literal 0 HcmV?d00001
-- 2.55.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson