public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
* [PATCH 0/5] Add AFL++ fuzzing support for passt
@ 2026-08-12  7:26 Anshu Kumari
  2026-08-12  7:26 ` [PATCH 1/5] fuzz: Add deterministic wrappers for system calls Anshu Kumari
                   ` (4 more replies)
  0 siblings, 5 replies; 11+ messages in thread
From: Anshu Kumari @ 2026-08-12  7:26 UTC (permalink / raw)
  To: david, sbrivio, passt-dev; +Cc: aerosound161, abdobngad, anskuma, lvivier

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.

Architecture
------------
The fuzzer runs passt in AFL++ persistent mode (__AFL_LOOP)
with shared memory fuzzing. A separate test server process
connects to passt's UNIX socket and listens on 127.0.0.1:9999
for real TCP connections:

Deterministic wrappers replace clock_gettime, getrandom,
getsockopt, and recv-family calls to eliminate kernel-level
non-determinism. Per-iteration reset of the flow table,
epoll instance, and clock.

*** BLURB HERE ***

Anshu Kumari (5):
  fuzz: Add deterministic wrappers for system calls
  fuzz: Add flow type guards for fuzzing stability
  fuzz: Bypass isolation and adapt sockets for AFL++
  fuzz: Add AFL++ persistent mode fuzz loop
  fuzz: Add test server for bidirectional protocol fuzzing

 Makefile                       |  28 +-
 fuzz-server.c                  | 490 +++++++++++++++++++++++++++++++++
 fuzz.c                         | 275 ++++++++++++++++++
 fuzz.h                         |  62 +++++
 fuzzing/README.fuzzing.md      |  95 +++++++
 fuzzing/testcase_dir/empty.bin | Bin 0 -> 12 bytes
 icmp.c                         |  14 +-
 isolation.c                    |  11 +
 passt.c                        | 189 +++++++++++++
 passt.h                        |   4 +
 tap.c                          |  21 ++
 tcp.c                          |  19 +-
 tcp_buf.c                      |   1 +
 tcp_splice.c                   |  10 +
 udp.c                          |  29 +-
 udp_flow.c                     |   5 +
 util.c                         |  10 +
 17 files changed, 1251 insertions(+), 12 deletions(-)
 create mode 100644 fuzz-server.c
 create mode 100644 fuzz.c
 create mode 100644 fuzz.h
 create mode 100644 fuzzing/README.fuzzing.md
 create mode 100644 fuzzing/testcase_dir/empty.bin

-- 
2.55.0


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 1/5] fuzz: Add deterministic wrappers for system calls
  2026-08-12  7:26 [PATCH 0/5] Add AFL++ fuzzing support for passt Anshu Kumari
@ 2026-08-12  7:26 ` Anshu Kumari
  2026-08-13  3:46   ` David Gibson
  2026-08-12  7:26 ` [PATCH 2/5] fuzz: Add flow type guards for fuzzing stability Anshu Kumari
                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 11+ messages in thread
From: Anshu Kumari @ 2026-08-12  7:26 UTC (permalink / raw)
  To: david, sbrivio, passt-dev; +Cc: aerosound161, abdobngad, anskuma, lvivier

Add fuzz.h and fuzz.c with wrapper implementations for
clock_gettime(), getrandom(), getsockopt(), recv(),
recvmsg(), recvfrom() and recvmmsg().

Under -DFUZZING, these macros replace the real system
calls across the codebase:

- fuzz_clock_gettime(): returns a deterministic clock that
  advances by 1 microsecond per call.
- fuzz_getrandom(): fills buffers with a fixed 0x41 pattern.
- fuzz_getsockopt(): returns static values for TCP_INFO,
  SO_ERROR, SO_RCVBUF, SO_SNDBUF.
- fuzz_recv/recvmsg/recvfrom/recvmmsg(): for fd_tap, calls
  the real syscall; for all other fds, returns data from
  AFL++ shared memory buffer

These wrappers eliminate kernel-level non-determinism during
AFL++ fuzzing.

Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
 Makefile |  14 +--
 fuzz.c   | 275 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 fuzz.h   |  62 +++++++++++++
 3 files changed, 344 insertions(+), 7 deletions(-)
 create mode 100644 fuzz.c
 create mode 100644 fuzz.h

diff --git a/Makefile b/Makefile
index b315242..fe1df58 100644
--- a/Makefile
+++ b/Makefile
@@ -38,7 +38,7 @@ PASST_SRCS = arch.c arp.c bitmap.c checksum.c conf.c dhcp.c dhcpv6.c \
 	isolation.c lineread.c log.c mld.c ndp.c netlink.c migrate.c packet.c \
 	parse.c passt.c pasta.c pcap.c pif.c repair.c serialise.c tap.c tcp.c \
 	tcp_buf.c tcp_splice.c tcp_vu.c udp.c udp_flow.c udp_vu.c util.c \
-	vhost_user.c virtio.c vu_common.c
+	vhost_user.c virtio.c vu_common.c fuzz.c
 PASST_REPAIR_SRCS = passt-repair.c
 PESTO_SRCS = pesto.c bitmap.c fwd_rule.c inany.c ip.c lineread.c parse.c \
 	serialise.c
@@ -47,12 +47,12 @@ SRCS = $(PASST_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
 MANPAGES = passt.1 pasta.1 pesto.1 passt-repair.1
 
 PASST_HEADERS = arch.h arp.h bitmap.h checksum.h conf.h dhcp.h dhcpv6.h \
-	epoll_ctl.h flow.h fwd.h fwd_rule.h flow_table.h icmp.h icmp_flow.h \
-	inany.h iov.h ip.h isolation.h lineread.h linux_dep.h log.h migrate.h \
-	ndp.h netlink.h packet.h parse.h passt.h pasta.h pcap.h pif.h repair.h \
-	serialise.h siphash.h tap.h tcp.h tcp_buf.h tcp_conn.h tcp_internal.h \
-	tcp_splice.h tcp_vu.h udp.h udp_flow.h udp_internal.h udp_vu.h util.h \
-	vhost_user.h virtio.h vu_common.h
+	epoll_ctl.h flow.h fwd.h fwd_rule.h flow_table.h fuzz.h icmp.h \
+	icmp_flow.h inany.h iov.h ip.h isolation.h lineread.h linux_dep.h \
+	log.h migrate.h ndp.h netlink.h packet.h parse.h passt.h pasta.h \
+	pcap.h pif.h repair.h serialise.h siphash.h tap.h tcp.h tcp_buf.h \
+	tcp_conn.h tcp_internal.h tcp_splice.h tcp_vu.h udp.h udp_flow.h \
+	udp_internal.h udp_vu.h util.h vhost_user.h virtio.h vu_common.h
 PASST_REPAIR_HEADERS = linux_dep.h
 PESTO_HEADERS = bitmap.h common.h fwd_rule.h inany.h ip.h log.h parse.h \
 	pesto.h serialise.h
diff --git a/fuzz.c b/fuzz.c
new file mode 100644
index 0000000..a1f6c01
--- /dev/null
+++ b/fuzz.c
@@ -0,0 +1,275 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/* fuzz.c - AFL++ fuzzing support: deterministic wrappers for
+ *          clock_gettime(), getrandom(), getsockopt(), recv(),
+ *          recvmsg(), recvfrom() and recvmmsg()
+ *
+ * Copyright Red Hat
+ * Author: Anshu Kumari <anskuma@redhat.com>
+ */
+
+#ifdef FUZZING
+
+#include <string.h>
+#include <time.h>
+#include <errno.h>
+#include <netinet/tcp.h>
+#include "passt.h"
+#include "fuzz.h"
+
+/* Undo macros so definitions here call the real syscalls */
+#undef clock_gettime
+#undef getrandom
+#undef getsockopt
+#undef recv
+#undef recvmsg
+#undef recvfrom
+#undef recvmmsg
+
+const unsigned char *fuzz_recv_data;
+int fuzz_recv_data_len;
+
+#define FUZZ_CLOCK_BASE_SEC	10000
+
+static struct timespec fuzz_clock;
+
+/**
+ * fuzz_clock_reset() - Reset clock to fixed baseline
+ *
+ * Called at the start of every __AFL_LOOP iteration so
+ * the clock is identical regardless of iteration number.
+ */
+void fuzz_clock_reset(void)
+{
+	fuzz_clock.tv_sec = FUZZ_CLOCK_BASE_SEC;
+	fuzz_clock.tv_nsec = 0;
+}
+
+/**
+ * fuzz_clock_gettime() - Return deterministic time
+ * @clk:	Clock ID
+ * @tp:		Output timespec
+ *
+ * Return: 0 (always succeeds)
+ */
+int fuzz_clock_gettime(clockid_t clk, struct timespec *tp)
+{
+	(void)clk;
+	*tp = fuzz_clock;
+
+	/* increment the timestamp by 1 micro sec monotonically */
+	fuzz_clock.tv_nsec += 1000;
+	if (fuzz_clock.tv_nsec >= 1000000000) {
+		fuzz_clock.tv_sec++;
+		fuzz_clock.tv_nsec -= 1000000000;
+	}
+	return 0;
+}
+
+/**
+ * fuzz_getrandom() - Return static deterministic bytes
+ * @buf:	Output buffer
+ * @buflen:	Bytes to fill
+ * @flags:	Ignored
+ *
+ * Fills buffer with a repeating 0x41 pattern. Every call with the
+ * same length returns identical bytes, eliminating randomness
+ *
+ * Return: buflen (always succeeds)
+ */
+ssize_t fuzz_getrandom(void *buf, size_t buflen, unsigned int flags)
+{
+	(void)flags;
+	memset(buf, 0x41, buflen);
+	return buflen;
+}
+
+/**
+ * fuzz_getsockopt() - Deterministic getsockopt wrapper
+ * @fd:		Socket file descriptor
+ * @level:	Protocol level
+ * @optname:	Option name
+ * @optval:	Output buffer
+ * @optlen:	In/out option length
+ *
+ * For TCP_INFO, SO_ERROR, SO_RCVBUF, SO_SNDBUF returns determinstic
+ * values. For all other options: calls the real getsockopt.
+ *
+ * Return: 0 on success, -1 on error
+ */
+int fuzz_getsockopt(int fd, int level, int optname, void *optval,
+		    socklen_t *optlen)
+{
+	if (level == SOL_SOCKET) {
+		if (optname == SO_ERROR) {
+			*(int *)optval = 0;
+			return 0;
+		}
+		if (optname == SO_RCVBUF || optname == SO_SNDBUF) {
+			*(int *)optval = 212992; /* default linux buff size */
+			return 0;
+		}
+	}
+
+	/* intercept SOL_TCP option: TCP_INFO */
+	if (level == SOL_TCP && optname == TCP_INFO) {
+		size_t fill = *optlen;
+
+		memset(optval, 0, fill);
+
+		if (fill >= sizeof(struct tcp_info)) {
+			struct tcp_info *ti = optval;
+
+			ti->tcpi_state = 1;		/* TCP_ESTABLISHED */
+			ti->tcpi_rto = 200000;		/* 200ms */
+			ti->tcpi_rtt = 1000;		/* 1ms RTT */
+			ti->tcpi_rttvar = 500;
+			ti->tcpi_snd_mss = 1460;
+			ti->tcpi_rcv_mss = 1460;
+			ti->tcpi_snd_cwnd = 10;
+			ti->tcpi_advmss = 1460;
+			ti->tcpi_pmtu = 1500;
+
+			*optlen = sizeof(struct tcp_info);
+		}
+
+		return 0;
+	}
+
+	return getsockopt(fd, level, optname, optval, optlen);
+}
+
+/**
+ * fuzz_recv() - recv wrapper
+ * @fd:		File descriptor
+ * @buf:	Output buffer
+ * @len:	Max bytes
+ * @flags:	recv flags (passed through for fd_tap)
+ *
+ * real recv() for fd_tap, AFL++ data for everything else
+ *
+ * Return: bytes read, or -1
+ */
+ssize_t fuzz_recv(int fd, void *buf, size_t len, int flags)
+{
+	size_t n;
+
+	/* fd is TAP socket for UNIX connection */
+	if (fd == passt_ctx.fd_tap)
+		return recv(fd, buf, len, flags);
+
+	if (!fuzz_recv_data || fuzz_recv_data_len <= 0) {
+		errno = EAGAIN;
+		return -1;
+	}
+
+	n = (len < (size_t)fuzz_recv_data_len) ?
+		len : (size_t)fuzz_recv_data_len;
+	memcpy(buf, fuzz_recv_data, n);
+	fuzz_recv_data += n;
+	fuzz_recv_data_len -= n;
+	return n;
+}
+
+/**
+ * fuzz_recvmsg() - recvmsg wrapper
+ * @fd:		File descriptor
+ * @msg:	Message header
+ * @flags:	recvmsg flags
+ *
+ * For fd_tap: calls real recvmsg. For all other fds: fills each
+ * buffer sequentially from the AFL++ shared memory stream.
+ *
+ * Return: total bytes read across all iovecs, or -1
+ */
+ssize_t fuzz_recvmsg(int fd, struct msghdr *msg, int flags)
+{
+	size_t total = 0;
+	size_t i;
+
+	if (fd == passt_ctx.fd_tap)
+		return recvmsg(fd, msg, flags);
+
+	if (!fuzz_recv_data || fuzz_recv_data_len <= 0) {
+		errno = EAGAIN;
+		return -1;
+	}
+
+	for (i = 0; i < (size_t)msg->msg_iovlen &&
+	     fuzz_recv_data_len > 0; i++) {
+		size_t n = msg->msg_iov[i].iov_len;
+
+		if ((int)n > fuzz_recv_data_len)
+			n = fuzz_recv_data_len;
+		memcpy(msg->msg_iov[i].iov_base, fuzz_recv_data, n);
+		fuzz_recv_data += n;
+		fuzz_recv_data_len -= n;
+		total += n;
+	}
+
+	return total;
+}
+
+/**
+ * fuzz_recvfrom() - recvfrom wrapper
+ * @fd:		File descriptor
+ * @buf:	Output buffer
+ * @len:	Max bytes
+ * @flags:	recv flags
+ * @src:	Source address output
+ * @addrlen:	Source address length
+ *
+ * For fd_tap: calls real recvfrom(). For all other fds: zeroes the
+ * source address (so callers see a deterministic sender) and
+ * delegates to fuzz_recv() for the payload.
+ *
+ * Return: bytes read, or -1
+ */
+ssize_t fuzz_recvfrom(int fd, void *buf, size_t len, int flags,
+		      struct sockaddr *src, socklen_t *addrlen)
+{
+	if (fd == passt_ctx.fd_tap)
+		return recvfrom(fd, buf, len, flags, src, addrlen);
+
+	if (src && addrlen)
+		memset(src, 0, *addrlen);
+
+	return fuzz_recv(fd, buf, len, flags);
+}
+
+/**
+ * fuzz_recvmmsg() - recvmmsg wrapper
+ * @fd:		File descriptor
+ * @mmh:	Array of mmsghdr structures to fill
+ * @vlen:	Number of mmsghdr entries available
+ * @flags:	recv flags
+ * @timeout:	Timeout
+ *
+ * For fd_tap: calls real recvmmsg(). For all other fds: fills only
+ * the first message from the AFL++ buffer via fuzz_recvmsg() and
+ * returns 1.
+ *
+ * Return: number of messages received (0 or 1), or -1
+ */
+int fuzz_recvmmsg(int fd, struct mmsghdr *mmh, unsigned int vlen,
+		  int flags, struct timespec *timeout)
+{
+	ssize_t n;
+
+	if (fd == passt_ctx.fd_tap)
+		return recvmmsg(fd, mmh, vlen, flags, timeout);
+
+	if (!vlen || !fuzz_recv_data || fuzz_recv_data_len <= 0) {
+		errno = EAGAIN;
+		return -1;
+	}
+
+	n = fuzz_recvmsg(fd, &mmh[0].msg_hdr, flags);
+	if (n < 0)
+		return -1;
+
+	mmh[0].msg_len = n;
+	return 1;
+}
+
+#endif
diff --git a/fuzz.h b/fuzz.h
new file mode 100644
index 0000000..3f834a0
--- /dev/null
+++ b/fuzz.h
@@ -0,0 +1,62 @@
+//SPDX-License-Identifier: GPL-2.0-or-later
+
+/* fuzz.h - AFL++ fuzzing support for passt
+ *
+ * Copyright Red Hat
+ * Author: Anshu Kumari <anskuma@redhat.com>
+ */
+
+#ifndef FUZZ_H
+#define FUZZ_H
+
+#ifdef FUZZING
+
+#include <stdint.h>
+#include <stddef.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+int fuzz_clock_gettime(clockid_t clk, struct timespec *tp);
+void fuzz_clock_reset(void);
+ssize_t fuzz_getrandom(void *buf, size_t buflen, unsigned int flags);
+int fuzz_getsockopt(int fd, int level, int optname, void *optval,
+		    socklen_t *optlen);
+
+#define clock_gettime(clk, tp) 		fuzz_clock_gettime(clk, tp)
+#define getrandom(buf, len, flags) 	fuzz_getrandom(buf, len, flags)
+#define getsockopt(fd, level, name, val, len) \
+	fuzz_getsockopt(fd, level, name, val, len)
+
+/* AFL++ buf layout: [0..11] epoll_event, [12..65547] recv payload */
+#define FUZZ_RECV_OFF		12
+#define FUZZ_RECV_MAX		(64 * 1024)
+
+extern const unsigned char *fuzz_recv_data;
+extern int fuzz_recv_data_len;
+
+ssize_t fuzz_recv(int fd, void *buf, size_t len, int flags);
+ssize_t fuzz_recvmsg(int fd, struct msghdr *msg, int flags);
+ssize_t fuzz_recvfrom(int fd, void *buf, size_t len, int flags,
+		      struct sockaddr *src, socklen_t *addrlen);
+int fuzz_recvmmsg(int fd, struct mmsghdr *mmh, unsigned int vlen,
+		  int flags, struct timespec *timeout);
+
+/* Override existing wrappers for recvfrom() present inside util.h*/
+#undef recvfrom
+
+#define recv(fd, buf, len, flags)		fuzz_recv(fd, buf, len, flags)
+#define recvmsg(fd, msg, flags)			fuzz_recvmsg(fd, msg, flags)
+#define recvfrom(fd, buf, len, flags, src, sl)		\
+	fuzz_recvfrom(fd, buf, len, flags, src, sl)
+#define recvmmsg(fd, mmh, vlen, flags, timeout)		\
+	fuzz_recvmmsg(fd, mmh, vlen, flags, timeout)
+
+#define FUZZ_TURN_PATH	"/dev/shm/passt_fuzz_turn"
+
+struct fuzz_turn {
+	uint32_t turn;
+};
+
+#endif
+#endif
-- 
2.55.0


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 2/5] fuzz: Add flow type guards for fuzzing stability
  2026-08-12  7:26 [PATCH 0/5] Add AFL++ fuzzing support for passt Anshu Kumari
  2026-08-12  7:26 ` [PATCH 1/5] fuzz: Add deterministic wrappers for system calls Anshu Kumari
@ 2026-08-12  7:26 ` Anshu Kumari
  2026-08-13  4:45   ` David Gibson
  2026-08-12  7:26 ` [PATCH 3/5] fuzz: Bypass isolation and adapt sockets for AFL++ Anshu Kumari
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 11+ messages in thread
From: Anshu Kumari @ 2026-08-12  7:26 UTC (permalink / raw)
  To: david, sbrivio, passt-dev; +Cc: aerosound161, abdobngad, anskuma, lvivier

Under FUZZING, AFL++ can inject arbitrary epoll event types
from its shared memory buffer. When an event references a flow
table entry whose type doesn't match the handler, the existing
assert() crashes the process eventually masking the real bugs.
If there is no flow at the start of fuzzing then also we are
just returning early instead of hitting crashes. Allowing the
fuzzer to explore other code path.

Replace assert() with NULL returns in the flow-lookup functions
when compiled with -DFUZZING:

- tcp.c: conn_at_sidx(), tcp_timer_handler(), tcp_sock_handler()
- tcp_splice.c: conn_at_sidx(), tcp_splice_sock_handler()
- udp.c: udp_sock_handler(), udp_sock_to_sock(),
  udp_buf_sock_to_tap(), udp_sock_fwd() error path
- udp_flow.c: udp_at_sidx()
- icmp.c: ping_at_sidx(), icmp_sock_handler()

Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
 icmp.c       | 14 +++++++++++++-
 tcp.c        | 19 ++++++++++++++++++-
 tcp_splice.c | 10 ++++++++++
 udp.c        | 29 +++++++++++++++++++++++++++--
 udp_flow.c   |  5 +++++
 5 files changed, 73 insertions(+), 4 deletions(-)

diff --git a/icmp.c b/icmp.c
index 0fe2366..cdfa253 100644
--- a/icmp.c
+++ b/icmp.c
@@ -39,6 +39,7 @@
 #include "icmp.h"
 #include "flow_table.h"
 #include "epoll_ctl.h"
+#include "fuzz.h"
 
 #define ICMP_ECHO_TIMEOUT	60 /* s, timeout for ICMP socket activity */
 #define ICMP_NUM_IDS		(1U << 16)
@@ -58,7 +59,12 @@ static struct icmp_ping_flow *ping_at_sidx(flow_sidx_t sidx)
 	if (!flow)
 		return NULL;
 
+#ifdef FUZZING
+	if (flow->f.type != FLOW_PING4 && flow->f.type != FLOW_PING6)
+		return NULL;
+#else
 	assert(flow->f.type == FLOW_PING4 || flow->f.type == FLOW_PING6);
+#endif
 	return &flow->ping;
 }
 
@@ -72,7 +78,13 @@ void icmp_sock_handler(const struct ctx *c, union epoll_ref ref,
 		       const struct timespec *now)
 {
 	struct icmp_ping_flow *pingf = ping_at_sidx(ref.flowside);
-	const struct flowside *ini = &pingf->f.side[INISIDE];
+	const struct flowside *ini;
+
+#ifdef FUZZING
+	if (!pingf)
+		return;
+#endif
+	ini = &pingf->f.side[INISIDE];
 	union sockaddr_inany sr;
 	socklen_t sl = sizeof(sr);
 	char buf[USHRT_MAX];
diff --git a/tcp.c b/tcp.c
index 3b78d2e..612c884 100644
--- a/tcp.c
+++ b/tcp.c
@@ -316,6 +316,7 @@
 #include "tcp_buf.h"
 #include "tcp_vu.h"
 #include "epoll_ctl.h"
+#include "fuzz.h"
 
 /*
  * The size of TCP header (including options) is given by doff (Data Offset)
@@ -456,7 +457,12 @@ static struct tcp_tap_conn *conn_at_sidx(flow_sidx_t sidx)
 	if (!flow)
 		return NULL;
 
+#ifdef FUZZING
+	if (flow->f.type != FLOW_TCP)
+		return NULL;
+#else
 	assert(flow->f.type == FLOW_TCP);
+#endif
 	return &flow->tcp;
 }
 
@@ -2681,7 +2687,14 @@ void tcp_timer_handler(const struct ctx *c, union epoll_ref ref,
 		       const struct timespec *now)
 {
 	struct itimerspec check_armed = { { 0 }, { 0 } };
-	struct tcp_tap_conn *conn = &FLOW(ref.flow)->tcp;
+	struct tcp_tap_conn *conn;
+
+#ifdef FUZZING
+	if (ref.flow >= FLOW_MAX ||
+	    FLOW(ref.flow)->f.type != FLOW_TCP)
+		return;
+#endif
+	conn = &FLOW(ref.flow)->tcp;
 
 	assert(!c->no_tcp);
 	assert(conn->f.type == FLOW_TCP);
@@ -2752,6 +2765,10 @@ void tcp_sock_handler(const struct ctx *c, union epoll_ref ref,
 {
 	struct tcp_tap_conn *conn = conn_at_sidx(ref.flowside);
 
+#ifdef FUZZING
+	if (!conn)
+		return;
+#endif
 	assert(!c->no_tcp);
 	assert(pif_at_sidx(ref.flowside) != PIF_TAP);
 
diff --git a/tcp_splice.c b/tcp_splice.c
index 4b01f1a..005ecd1 100644
--- a/tcp_splice.c
+++ b/tcp_splice.c
@@ -105,7 +105,12 @@ static struct tcp_splice_conn *conn_at_sidx(flow_sidx_t sidx)
 	if (!flow)
 		return NULL;
 
+#ifdef FUZZING
+	if (flow->f.type != FLOW_TCP_SPLICE)
+		return NULL;
+#else
 	assert(flow->f.type == FLOW_TCP_SPLICE);
+#endif
 	return &flow->tcp_splice;
 }
 
@@ -594,6 +599,11 @@ void tcp_splice_sock_handler(struct ctx *c, union epoll_ref ref,
 	struct tcp_splice_conn *conn = conn_at_sidx(ref.flowside);
 	unsigned evsidei = ref.flowside.sidei;
 
+#ifdef FUZZING
+	if (!conn)
+		return;
+#endif
+
 	assert(conn->f.type == FLOW_TCP_SPLICE);
 
 	if (conn->events == SPLICE_CLOSED)
diff --git a/udp.c b/udp.c
index 505e554..9431353 100644
--- a/udp.c
+++ b/udp.c
@@ -118,6 +118,7 @@
 #include "udp_internal.h"
 #include "udp_vu.h"
 #include "epoll_ctl.h"
+#include "fuzz.h"
 
 #define UDP_MAX_FRAMES		32  /* max # of frames to receive at once */
 
@@ -807,9 +808,15 @@ static void udp_sock_to_sock(const struct ctx *c, int from_s, int n,
 	const struct flowside *toside = flowside_at_sidx(tosidx);
 	const struct udp_flow *uflow = udp_at_sidx(tosidx);
 	uint8_t topif = pif_at_sidx(tosidx);
-	int to_s = uflow->s[tosidx.sidei];
+	int to_s;
 	int i;
 
+#ifdef FUZZING
+	if (!uflow)
+		return;
+#endif
+	to_s = uflow->s[tosidx.sidei];
+
 	if ((n = udp_sock_recv(c, from_s, udp_mh_recv, n)) <= 0)
 		return;
 
@@ -836,9 +843,15 @@ static void udp_buf_sock_to_tap(const struct ctx *c, int s, int n,
 {
 	const struct flowside *toside = flowside_at_sidx(tosidx);
 	struct udp_flow *uflow = udp_at_sidx(tosidx);
-	uint8_t *omac = uflow->f.tap_omac;
+	uint8_t *omac;
 	int i;
 
+#ifdef FUZZING
+	if (!uflow)
+		return;
+#endif
+	omac = uflow->f.tap_omac;
+
 	if ((n = udp_sock_recv(c, s, udp_mh_recv, n)) <= 0)
 		return;
 
@@ -901,10 +914,18 @@ void udp_sock_fwd(const struct ctx *c, int s, int rule_hint,
 		} else if (flow_sidx_valid(tosidx)) {
 			struct udp_flow *uflow = udp_at_sidx(tosidx);
 
+#ifdef FUZZING
+			if (!uflow) {
+				discard = true;
+				continue;
+			}
+#endif
+
 			flow_err_ratelimit(
 				uflow, now,
 				"No support for forwarding UDP from %s to %s",
 				pif_name(frompif), pif_name(topif));
+
 			discard = true;
 		} else {
 			warn_ratelimit(now, "Discarding datagram without flow");
@@ -949,6 +970,10 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
 {
 	struct udp_flow *uflow = udp_at_sidx(ref.flowside);
 
+#ifdef FUZZING
+	if (!uflow)
+		return;
+#endif
 	assert(!c->no_udp && uflow);
 
 	if (events & EPOLLERR) {
diff --git a/udp_flow.c b/udp_flow.c
index f59649f..6c5b010 100644
--- a/udp_flow.c
+++ b/udp_flow.c
@@ -31,7 +31,12 @@ struct udp_flow *udp_at_sidx(flow_sidx_t sidx)
 	if (!flow)
 		return NULL;
 
+#ifdef FUZZING
+	if (flow->f.type != FLOW_UDP)
+		return NULL;
+#else
 	assert(flow->f.type == FLOW_UDP);
+#endif
 	return &flow->udp;
 }
 
-- 
2.55.0


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 3/5] fuzz: Bypass isolation and adapt sockets for AFL++
  2026-08-12  7:26 [PATCH 0/5] Add AFL++ fuzzing support for passt Anshu Kumari
  2026-08-12  7:26 ` [PATCH 1/5] fuzz: Add deterministic wrappers for system calls Anshu Kumari
  2026-08-12  7:26 ` [PATCH 2/5] fuzz: Add flow type guards for fuzzing stability Anshu Kumari
@ 2026-08-12  7:26 ` Anshu Kumari
  2026-08-13  5:04   ` David Gibson
  2026-08-12  7:26 ` [PATCH 4/5] fuzz: Add AFL++ persistent mode fuzz loop Anshu Kumari
  2026-08-12  7:26 ` [PATCH 5/5] fuzz: Add test server for bidirectional protocol fuzzing Anshu Kumari
  4 siblings, 1 reply; 11+ messages in thread
From: Anshu Kumari @ 2026-08-12  7:26 UTC (permalink / raw)
  To: david, sbrivio, passt-dev; +Cc: aerosound161, abdobngad, anskuma, lvivier

Few components which needs to be disabled to support AFL++
to work:

- isolation.c: Skip isolation and seccomp sandboxing that breaks AFL++
  pipes, namespaces, and ASan mmap/mprotect operations.
- util.c / tap.c: Switch UNIX socket to SOCK_SEQPACKET to preserve frame
  boundaries, simplifying tap_passt_input() to a single recv() and removing
  vnet_len framing.
- passt.h: Use /tmp/passt_fuzz_%i.socket to avoid path collisions with
  production instances.
- tcp_buf.c: Include fuzz.h to route recvmsg() through deterministic wrappers.

Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
 isolation.c | 11 +++++++++++
 passt.h     |  4 ++++
 tap.c       | 21 +++++++++++++++++++++
 tcp_buf.c   |  1 +
 util.c      | 10 ++++++++++
 5 files changed, 47 insertions(+)

diff --git a/isolation.c b/isolation.c
index a30b329..61fc76a 100644
--- a/isolation.c
+++ b/isolation.c
@@ -208,6 +208,9 @@ static int move_root(void)
  */
 void isolate_initial(void)
 {
+#ifdef FUZZING
+	return;
+#endif
 	uint64_t keep;
 
 	/* We want to keep CAP_NET_BIND_SERVICE in the initial
@@ -389,6 +392,10 @@ void isolate_user(const struct ctx *c, uid_t uid, gid_t gid, bool use_userns,
  */
 int isolate_prefork(const struct ctx *c)
 {
+#ifdef FUZZING
+	(void)c;
+	return 0;
+#endif
 	int flags = CLONE_NEWIPC | CLONE_NEWNS | CLONE_NEWUTS;
 	uint64_t ns_caps = 0;
 
@@ -466,6 +473,10 @@ int isolate_prefork(const struct ctx *c)
  */
 void isolate_postfork(const struct ctx *c)
 {
+#ifdef FUZZING
+	(void)c;
+	return;
+#endif
 	struct sock_fprog prog;
 
 	prctl(PR_SET_DUMPABLE, 0);
diff --git a/passt.h b/passt.h
index 51ccd4f..141c9f8 100644
--- a/passt.h
+++ b/passt.h
@@ -7,7 +7,11 @@
 #define PASST_H
 
 #define UNIX_SOCK_MAX		100
+#ifdef FUZZING
+#define UNIX_SOCK_PATH		"/tmp/passt_fuzz_%i.socket"
+#else
 #define UNIX_SOCK_PATH		"/tmp/passt_%i.socket"
+#endif
 
 union epoll_ref;
 
diff --git a/tap.c b/tap.c
index dfa66c7..f32c9ad 100644
--- a/tap.c
+++ b/tap.c
@@ -14,6 +14,7 @@
  */
 
 #include <sched.h>
+#include <time.h>
 #include <unistd.h>
 #include <signal.h>
 #include <stdio.h>
@@ -61,6 +62,7 @@
 #include "vhost_user.h"
 #include "vu_common.h"
 #include "epoll_ctl.h"
+#include "fuzz.h"
 
 /* Maximum allowed frame lengths (including L2 header) */
 
@@ -144,8 +146,10 @@ void tap_send_single(const struct ctx *c, const void *data, size_t l2len)
 
 	switch (c->mode) {
 	case MODE_PASST:
+#ifndef FUZZING
 		iov[iovcnt] = IOV_OF_LVALUE(vnet_len);
 		iovcnt++;
+#endif
 		/* fall through */
 	case MODE_PASTA:
 		iov[iovcnt].iov_base = (void *)data;
@@ -1231,6 +1235,22 @@ static void tap_passt_input(struct ctx *c, const struct timespec *now)
 
 	tap_flush_pools();
 
+#ifdef FUZZING
+	/* SOCK_SEQPACKET: each recv returns exactly one frame */
+	do {
+		n = recv(c->fd_tap, pkt_buf, sizeof(pkt_buf), MSG_DONTWAIT);
+	} while ((n < 0) && errno == EINTR);
+
+	if (n > 0 && n >= (ssize_t)sizeof(struct ethhdr)) {
+		struct iov_tail data;
+
+		data = IOV_TAIL_FROM_BUF(pkt_buf, n, 0);
+		tap_add_packet(c, &data, now);
+	} else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
+		tap_sock_reset(c);
+		return;
+	}
+#else
 	if (partial_len) {
 		/* We have a partial frame from an earlier pass.  Move it to the
 		 * start of the buffer, top up with new data, then process all
@@ -1281,6 +1301,7 @@ static void tap_passt_input(struct ctx *c, const struct timespec *now)
 
 	partial_len = n;
 	partial_frame = p;
+#endif
 
 	tap_handler(c, now);
 }
diff --git a/tcp_buf.c b/tcp_buf.c
index 72c4541..eb28abe 100644
--- a/tcp_buf.c
+++ b/tcp_buf.c
@@ -32,6 +32,7 @@
 #include "tcp_conn.h"
 #include "tcp_internal.h"
 #include "tcp_buf.h"
+#include "fuzz.h"
 
 #define TCP_FRAMES_MEM			128
 #define TCP_FRAMES							   \
diff --git a/util.c b/util.c
index 28c32e4..7f29c3b 100644
--- a/util.c
+++ b/util.c
@@ -36,6 +36,7 @@
 #include "epoll_ctl.h"
 #include "pasta.h"
 #include "serialise.h"
+#include "fuzz.h"
 #ifdef HAS_GETRANDOM
 #include <sys/random.h>
 #endif
@@ -229,7 +230,11 @@ int sock_l4_dualstack_any(const struct ctx *c, enum epoll_type type,
  */
 int sock_unix(char *sock_path)
 {
+#ifdef FUZZING
+	int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
+#else
 	int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+#endif
 	struct sockaddr_un addr = {
 		.sun_family = AF_UNIX,
 	};
@@ -248,8 +253,13 @@ int sock_unix(char *sock_path)
 					UNIX_SOCK_PATH, i))
 			die_perror("Can't build UNIX domain socket path");
 
+#ifdef FUZZING
+		ex = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC,
+			    0);
+#else
 		ex = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
 			    0);
+#endif
 		if (ex < 0)
 			die_perror("Failed to check for UNIX domain conflicts");
 
-- 
2.55.0


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 4/5] fuzz: Add AFL++ persistent mode fuzz loop
  2026-08-12  7:26 [PATCH 0/5] Add AFL++ fuzzing support for passt Anshu Kumari
                   ` (2 preceding siblings ...)
  2026-08-12  7:26 ` [PATCH 3/5] fuzz: Bypass isolation and adapt sockets for AFL++ Anshu Kumari
@ 2026-08-12  7:26 ` Anshu Kumari
  2026-08-13  6:34   ` Stefano Brivio
  2026-08-12  7:26 ` [PATCH 5/5] fuzz: Add test server for bidirectional protocol fuzzing Anshu Kumari
  4 siblings, 1 reply; 11+ messages in thread
From: Anshu Kumari @ 2026-08-12  7:26 UTC (permalink / raw)
  To: david, sbrivio, passt-dev; +Cc: aerosound161, abdobngad, anskuma, lvivier

Add the AFL++ persistent mode fuzz loop to passt.c main().
The loop uses __AFL_LOOP() for in-process iteration and
__AFL_FUZZ_TESTCASE_BUF for shared memory fuzzing.

Each iteration:
  - Resets deterministic clock, flow table, and epoll instance.
  - Drains stale data from the TAP socket.
  - Reads an epoll event from the AFL++ buffer.
  - For TAP events: constructs a packet with fixed L2/L3/L4
    headers and injects it via tap_add_packet() + tap_handler().
  - Exchanges a turn flag with the test server for
    bidirectional flow over the UNIX socket.
  - Calls passt_worker() to process the event.
  - Polls for host-side TCP events via epoll_wait().
  - Runs post_handler() for deferred work.

Added the 'make fuzz' target which builds passt with
afl-clang-fast, -DFUZZING, -DNDEBUG, and AddressSanitizer.

Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
 Makefile |   8 +++
 passt.c  | 189 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 197 insertions(+)

diff --git a/Makefile b/Makefile
index fe1df58..8e4121e 100644
--- a/Makefile
+++ b/Makefile
@@ -123,6 +123,14 @@ valgrind: BASE_CPPFLAGS += -DVALGRIND
 valgrind: BASE_CFLAGS += -g
 valgrind: all
 
+FUZZ_CC ?= afl-clang-fast
+
+.PHONY: fuzz
+
+fuzz:
+	$(MAKE) clean
+	$(MAKE) CC="$(FUZZ_CC)" CPPFLAGS="-DFUZZING -DNDEBUG" CFLAGS="-g -fsanitize=address" passt
+
 .PHONY: clean
 clean:
 	$(RM) $(BIN) *~ *.o seccomp.h seccomp_repair.h seccomp_pesto.h pasta.1 \
diff --git a/passt.c b/passt.c
index 5054551..e026eb2 100644
--- a/passt.c
+++ b/passt.c
@@ -35,6 +35,7 @@
 #include <sys/prctl.h>
 #include <netinet/if_ether.h>
 #include <libgen.h>
+#include <netinet/tcp.h>
 
 #include "util.h"
 #include "passt.h"
@@ -54,12 +55,56 @@
 #include "repair.h"
 #include "netlink.h"
 #include "epoll_ctl.h"
+#include "flow_table.h"
+#include "fuzz.h"
 
 #define NUM_EPOLL_EVENTS	8
 
 #define TIMER_INTERVAL_		MIN(TCP_TIMER_INTERVAL, FWD_PORT_SCAN_INTERVAL)
 #define TIMER_INTERVAL		MIN(TIMER_INTERVAL_, FLOW_TIMER_INTERVAL)
 
+#ifdef FUZZING
+
+/* AFL++ persistent mode / shared memory fuzzing compatibility macros. */
+#ifndef __AFL_FUZZ_TESTCASE_LEN
+  ssize_t		fuzz_len;
+  unsigned char		fuzz_buf[1024 * 1024];
+# define __AFL_FUZZ_TESTCASE_LEN	fuzz_len
+# define __AFL_FUZZ_TESTCASE_BUF	fuzz_buf
+# define __AFL_FUZZ_INIT()		void sync(void)
+# define __AFL_LOOP(x)		\
+	((fuzz_len = read(0, fuzz_buf, sizeof(fuzz_buf))) > 0 ? 1 : 0)
+# define __AFL_INIT()		sync()
+#endif
+
+#ifdef __AFL_HAVE_MANUAL_CONTROL
+  __AFL_FUZZ_INIT();
+#endif
+
+static struct fuzz_turn *fuzz_turn_ptr;
+
+/**
+ * fuzz_turn_connect() - Map the turn flag shared memory
+ *
+ * Return: pointer to mapped turn flag, or NULL on failure
+ */
+static struct fuzz_turn *fuzz_turn_connect(void)
+{
+	struct fuzz_turn *t;
+	int fd;
+
+	fd = open(FUZZ_TURN_PATH, O_RDWR);
+	if (fd < 0)
+		return NULL;
+
+	t = mmap(NULL, sizeof(*t), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+	close(fd);
+
+	return (t == MAP_FAILED) ? NULL : t;
+}
+
+#endif
+
 char pkt_buf[PKT_BUF_BYTES]	__attribute__ ((aligned(PAGE_SIZE)));
 
 struct ctx passt_ctx = {
@@ -282,9 +327,17 @@ static void passt_worker(void *opaque, int nfds, struct epoll_event *events)
 			icmp_sock_handler(c, ref, &now);
 			break;
 		case EPOLL_TYPE_VHOST_CMD:
+#ifdef FUZZING
+			if (!c->vdev)
+				break;
+#endif
 			vu_control_handler(c->vdev, c->fd_tap, eventmask);
 			break;
 		case EPOLL_TYPE_VHOST_KICK:
+#ifdef FUZZING
+			if (!c->vdev)
+				break;
+#endif
 			vu_kick_cb(c->vdev, ref, &now);
 			break;
 		case EPOLL_TYPE_REPAIR_LISTEN:
@@ -450,6 +503,141 @@ int main(int argc, char **argv)
 
 	timer_init(c, &now);
 
+#ifdef FUZZING
+	fuzz_turn_ptr = fuzz_turn_connect();
+
+#define FUZZ_LOOP_ITERATIONS	10000
+#define FUZZ_DRAIN_BUF_SIZE	1600
+
+#ifdef __AFL_HAVE_MANUAL_CONTROL
+	__AFL_INIT();
+#endif
+	{
+		unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
+
+		while (__AFL_LOOP(FUZZ_LOOP_ITERATIONS)) {
+			int len = __AFL_FUZZ_TESTCASE_LEN;
+			int injected = 0;
+			int pkt_len, round;
+			struct epoll_event ev;
+			union epoll_ref ref;
+			int min_pkt = sizeof(struct ethhdr) +
+				      sizeof(struct iphdr) +
+				      sizeof(struct tcphdr);
+
+			if (len < (int)sizeof(ev))
+				continue;
+
+			/* Reset clock, flow table and epoll for each
+			 * AFL++ iteration.
+			 */
+			fuzz_clock_reset();
+			clock_gettime(CLOCK_MONOTONIC, &now);
+			timer_init(c, &now);
+
+			flow_init();
+
+			/* Recreate epoll instance */
+			close(c->epollfd);
+			c->epollfd = epoll_create1(EPOLL_CLOEXEC);
+			flow_epollid_register(EPOLLFD_ID_DEFAULT, c->epollfd);
+
+			if (c->fd_tap >= 0) {
+				union epoll_ref tref = {
+					.type = EPOLL_TYPE_TAP_PASST,
+					.fd = c->fd_tap
+				};
+				epoll_add(c->epollfd,
+					  EPOLLIN | EPOLLRDHUP, tref);
+
+				/* Drain stale socket data */
+				char drain[FUZZ_DRAIN_BUF_SIZE];
+				while (recv(c->fd_tap, drain, sizeof(drain),
+					    MSG_DONTWAIT) > 0);
+			}
+
+			/* Read epoll event from AFL++ buffer */
+			memcpy(&ev, buf, sizeof(ev));
+			ref = *((union epoll_ref *)&ev.data.u64);
+
+			/* Set recv payload in AFL++ shared memory */
+			fuzz_recv_data = buf + FUZZ_RECV_OFF;
+			fuzz_recv_data_len =
+				(len > FUZZ_RECV_OFF + FUZZ_RECV_MAX)
+					     ? FUZZ_RECV_MAX
+					     : ((len > FUZZ_RECV_OFF)
+						? len - FUZZ_RECV_OFF : 0);
+
+			/* Inject fuzz packet for TAP events */
+			if (ref.type == EPOLL_TYPE_TAP_PASST ||
+			    ref.type == EPOLL_TYPE_TAP_PASTA) {
+				struct iov_tail data;
+				struct ethhdr *eh;
+				struct iphdr *iph;
+				struct tcphdr *th;
+
+				tap_flush_pools();
+				memset(pkt_buf, 0, min_pkt);
+
+				pkt_len = len - (int)sizeof(ev);
+				if (pkt_len > 0)
+					memcpy(pkt_buf, buf + sizeof(ev),
+					       pkt_len);
+				if (pkt_len < min_pkt)
+					pkt_len = min_pkt;
+
+				/* construct ethernet header */
+				eh = (struct ethhdr *)pkt_buf;
+				memcpy(eh->h_dest, c->our_tap_mac, ETH_ALEN);
+				memcpy(eh->h_source, c->guest_mac, ETH_ALEN);
+				eh->h_proto = htons(ETH_P_IP);
+
+				/* construct IPv4 header */
+				iph = (struct iphdr *)(pkt_buf + sizeof(*eh));
+				iph->version = 4;
+				iph->ihl = 5;
+				iph->protocol = IPPROTO_TCP;
+				iph->saddr = c->ip4.addr.s_addr;
+				iph->daddr = c->ip4.guest_gw.s_addr;
+				iph->tot_len = htons(pkt_len - sizeof(*eh));
+
+				/* Fix TCP Header */
+				th = (struct tcphdr *)(pkt_buf + sizeof(*eh) +
+								 sizeof(*iph));
+				th->dest = htons(9999);
+				if (th->doff < 5)
+					th->doff = 5;
+
+				data = IOV_TAIL_FROM_BUF(pkt_buf, pkt_len, 0);
+				tap_add_packet(c, &data, &now);
+				tap_handler(c, &now);
+				injected = 1;
+			}
+
+			/* Turn exchange -- only if data was sent */
+			if (injected && fuzz_turn_ptr) {
+				__atomic_store_n(&fuzz_turn_ptr->turn, 1,
+						 __ATOMIC_RELEASE);
+				while (__atomic_load_n(&fuzz_turn_ptr->turn,
+						      __ATOMIC_ACQUIRE) != 0);
+			}
+
+			passt_worker(c, 1, &ev);
+
+			/* Process host-side TCP events */
+			for (round = 0; round < 4; round++) {
+				nfds = epoll_wait(c->epollfd, events,
+						  NUM_EPOLL_EVENTS, 0);
+				if (nfds <= 0)
+					break;
+				passt_worker(c, nfds, events);
+			}
+
+			post_handler(c, &now);
+		}
+	}
+	return 0;
+#else
 loop:
 	/* NOLINTBEGIN(bugprone-branch-clone): intervals can be the same */
 	/* cppcheck-suppress [duplicateValueTernary, unmatchedSuppression] */
@@ -461,4 +649,5 @@ loop:
 	passt_worker(c, nfds, events);
 
 	goto loop;
+#endif /* FUZZING */
 }
-- 
2.55.0


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 5/5] fuzz: Add test server for bidirectional protocol fuzzing
  2026-08-12  7:26 [PATCH 0/5] Add AFL++ fuzzing support for passt Anshu Kumari
                   ` (3 preceding siblings ...)
  2026-08-12  7:26 ` [PATCH 4/5] fuzz: Add AFL++ persistent mode fuzz loop Anshu Kumari
@ 2026-08-12  7:26 ` Anshu Kumari
  2026-08-13  7:53   ` Stefano Brivio
  4 siblings, 1 reply; 11+ messages in thread
From: Anshu Kumari @ 2026-08-12  7:26 UTC (permalink / raw)
  To: david, sbrivio, passt-dev; +Cc: aerosound161, abdobngad, anskuma, lvivier

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.

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.

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 <anskuma@redhat.com>
---
 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
 
+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 <anskuma@redhat.com>
+ */
+
+#include <linux/if_ether.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/epoll.h>
+#include <sys/shm.h>
+#include <arpa/inet.h>
+#include <net/ethernet.h>
+#include <net/if_arp.h>
+#include <netinet/ip.h>
+#include <netinet/tcp.h>
+
+#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


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 1/5] fuzz: Add deterministic wrappers for system calls
  2026-08-12  7:26 ` [PATCH 1/5] fuzz: Add deterministic wrappers for system calls Anshu Kumari
@ 2026-08-13  3:46   ` David Gibson
  0 siblings, 0 replies; 11+ messages in thread
From: David Gibson @ 2026-08-13  3:46 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, aerosound161, abdobngad, lvivier

[-- Attachment #1: Type: text/plain, Size: 16529 bytes --]

On Wed, Aug 12, 2026 at 12:56:24PM +0530, Anshu Kumari wrote:
> Add fuzz.h and fuzz.c with wrapper implementations for
> clock_gettime(), getrandom(), getsockopt(), recv(),
> recvmsg(), recvfrom() and recvmmsg().
> 
> Under -DFUZZING, these macros replace the real system
> calls across the codebase:
> 
> - fuzz_clock_gettime(): returns a deterministic clock that
>   advances by 1 microsecond per call.
> - fuzz_getrandom(): fills buffers with a fixed 0x41 pattern.
> - fuzz_getsockopt(): returns static values for TCP_INFO,
>   SO_ERROR, SO_RCVBUF, SO_SNDBUF.
> - fuzz_recv/recvmsg/recvfrom/recvmmsg(): for fd_tap, calls
>   the real syscall; for all other fds, returns data from
>   AFL++ shared memory buffer
> 
> These wrappers eliminate kernel-level non-determinism during
> AFL++ fuzzing.

I certainly think the general approach of replacing the system call
interactions with deterministic version is a good one (btw, I believe
this approach is known as Deterministic Simulation Testing).  Ideally
I'd like to see more of these wrappers based on data from the AFL++
buffer - that way we can use AFL++'s coverage directed fuzzing to find
more possible bad paths.  Still, starting with a limited set and
expanding from there is reasonable.

> Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> ---
>  Makefile |  14 +--
>  fuzz.c   | 275 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
>  fuzz.h   |  62 +++++++++++++
>  3 files changed, 344 insertions(+), 7 deletions(-)
>  create mode 100644 fuzz.c
>  create mode 100644 fuzz.h
> 
> diff --git a/Makefile b/Makefile
> index b315242..fe1df58 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -38,7 +38,7 @@ PASST_SRCS = arch.c arp.c bitmap.c checksum.c conf.c dhcp.c dhcpv6.c \
>  	isolation.c lineread.c log.c mld.c ndp.c netlink.c migrate.c packet.c \
>  	parse.c passt.c pasta.c pcap.c pif.c repair.c serialise.c tap.c tcp.c \
>  	tcp_buf.c tcp_splice.c tcp_vu.c udp.c udp_flow.c udp_vu.c util.c \
> -	vhost_user.c virtio.c vu_common.c
> +	vhost_user.c virtio.c vu_common.c fuzz.c
>  PASST_REPAIR_SRCS = passt-repair.c
>  PESTO_SRCS = pesto.c bitmap.c fwd_rule.c inany.c ip.c lineread.c parse.c \
>  	serialise.c
> @@ -47,12 +47,12 @@ SRCS = $(PASST_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
>  MANPAGES = passt.1 pasta.1 pesto.1 passt-repair.1
>  
>  PASST_HEADERS = arch.h arp.h bitmap.h checksum.h conf.h dhcp.h dhcpv6.h \
> -	epoll_ctl.h flow.h fwd.h fwd_rule.h flow_table.h icmp.h icmp_flow.h \
> -	inany.h iov.h ip.h isolation.h lineread.h linux_dep.h log.h migrate.h \
> -	ndp.h netlink.h packet.h parse.h passt.h pasta.h pcap.h pif.h repair.h \
> -	serialise.h siphash.h tap.h tcp.h tcp_buf.h tcp_conn.h tcp_internal.h \
> -	tcp_splice.h tcp_vu.h udp.h udp_flow.h udp_internal.h udp_vu.h util.h \
> -	vhost_user.h virtio.h vu_common.h
> +	epoll_ctl.h flow.h fwd.h fwd_rule.h flow_table.h fuzz.h icmp.h \
> +	icmp_flow.h inany.h iov.h ip.h isolation.h lineread.h linux_dep.h \
> +	log.h migrate.h ndp.h netlink.h packet.h parse.h passt.h pasta.h \
> +	pcap.h pif.h repair.h serialise.h siphash.h tap.h tcp.h tcp_buf.h \
> +	tcp_conn.h tcp_internal.h tcp_splice.h tcp_vu.h udp.h udp_flow.h \
> +	udp_internal.h udp_vu.h util.h vhost_user.h virtio.h vu_common.h
>  PASST_REPAIR_HEADERS = linux_dep.h
>  PESTO_HEADERS = bitmap.h common.h fwd_rule.h inany.h ip.h log.h parse.h \
>  	pesto.h serialise.h
> diff --git a/fuzz.c b/fuzz.c
> new file mode 100644
> index 0000000..a1f6c01
> --- /dev/null
> +++ b/fuzz.c
> @@ -0,0 +1,275 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +
> +/* fuzz.c - AFL++ fuzzing support: deterministic wrappers for
> + *          clock_gettime(), getrandom(), getsockopt(), recv(),
> + *          recvmsg(), recvfrom() and recvmmsg()
> + *
> + * Copyright Red Hat
> + * Author: Anshu Kumari <anskuma@redhat.com>
> + */
> +
> +#ifdef FUZZING

A possible alternative to wrapping the entire .c file in a #ifdef
would be to exclude it from the build in the Makefile in the !FUZZING
case.

> +#include <string.h>
> +#include <time.h>
> +#include <errno.h>
> +#include <netinet/tcp.h>
> +#include "passt.h"
> +#include "fuzz.h"
> +
> +/* Undo macros so definitions here call the real syscalls */
> +#undef clock_gettime
> +#undef getrandom
> +#undef getsockopt
> +#undef recv
> +#undef recvmsg
> +#undef recvfrom
> +#undef recvmmsg
> +
> +const unsigned char *fuzz_recv_data;
> +int fuzz_recv_data_len;
> +
> +#define FUZZ_CLOCK_BASE_SEC	10000
> +
> +static struct timespec fuzz_clock;
> +
> +/**
> + * fuzz_clock_reset() - Reset clock to fixed baseline
> + *
> + * Called at the start of every __AFL_LOOP iteration so
> + * the clock is identical regardless of iteration number.
> + */
> +void fuzz_clock_reset(void)
> +{
> +	fuzz_clock.tv_sec = FUZZ_CLOCK_BASE_SEC;
> +	fuzz_clock.tv_nsec = 0;
> +}
> +
> +/**
> + * fuzz_clock_gettime() - Return deterministic time
> + * @clk:	Clock ID
> + * @tp:		Output timespec
> + *
> + * Return: 0 (always succeeds)
> + */
> +int fuzz_clock_gettime(clockid_t clk, struct timespec *tp)
> +{
> +	(void)clk;
> +	*tp = fuzz_clock;
> +
> +	/* increment the timestamp by 1 micro sec monotonically */
> +	fuzz_clock.tv_nsec += 1000;
> +	if (fuzz_clock.tv_nsec >= 1000000000) {
> +		fuzz_clock.tv_sec++;
> +		fuzz_clock.tv_nsec -= 1000000000;
> +	}
> +	return 0;
> +}

Since we have logic that depends on specific elapsed times, eventually
it would certainly be nice to have this influenced by the fuzzer as
well.  To avoid problems with time going backwards, the obvious way to
do that would be to read a a number of ns to advance by from the
shared memory.

But again, this 1µs per call approach is pretty good for a first cut.

> +
> +/**
> + * fuzz_getrandom() - Return static deterministic bytes
> + * @buf:	Output buffer
> + * @buflen:	Bytes to fill
> + * @flags:	Ignored
> + *
> + * Fills buffer with a repeating 0x41 pattern. Every call with the
> + * same length returns identical bytes, eliminating randomness
> + *
> + * Return: buflen (always succeeds)
> + */
> +ssize_t fuzz_getrandom(void *buf, size_t buflen, unsigned int flags)
> +{
> +	(void)flags;
> +	memset(buf, 0x41, buflen);
> +	return buflen;
> +}

It would be nice in principle to fuzz this, but we only use
getrandom() in a handful of places, so it's not a high priority.

> +
> +/**
> + * fuzz_getsockopt() - Deterministic getsockopt wrapper
> + * @fd:		Socket file descriptor
> + * @level:	Protocol level
> + * @optname:	Option name
> + * @optval:	Output buffer
> + * @optlen:	In/out option length
> + *
> + * For TCP_INFO, SO_ERROR, SO_RCVBUF, SO_SNDBUF returns determinstic
> + * values. For all other options: calls the real getsockopt.
> + *
> + * Return: 0 on success, -1 on error
> + */
> +int fuzz_getsockopt(int fd, int level, int optname, void *optval,
> +		    socklen_t *optlen)
> +{
> +	if (level == SOL_SOCKET) {
> +		if (optname == SO_ERROR) {
> +			*(int *)optval = 0;
> +			return 0;
> +		}
> +		if (optname == SO_RCVBUF || optname == SO_SNDBUF) {
> +			*(int *)optval = 212992; /* default linux buff size */
> +			return 0;
> +		}
> +	}
> +
> +	/* intercept SOL_TCP option: TCP_INFO */
> +	if (level == SOL_TCP && optname == TCP_INFO) {
> +		size_t fill = *optlen;
> +
> +		memset(optval, 0, fill);
> +
> +		if (fill >= sizeof(struct tcp_info)) {
> +			struct tcp_info *ti = optval;
> +
> +			ti->tcpi_state = 1;		/* TCP_ESTABLISHED */
> +			ti->tcpi_rto = 200000;		/* 200ms */
> +			ti->tcpi_rtt = 1000;		/* 1ms RTT */
> +			ti->tcpi_rttvar = 500;
> +			ti->tcpi_snd_mss = 1460;
> +			ti->tcpi_rcv_mss = 1460;
> +			ti->tcpi_snd_cwnd = 10;
> +			ti->tcpi_advmss = 1460;
> +			ti->tcpi_pmtu = 1500;
> +
> +			*optlen = sizeof(struct tcp_info);

Hm.  Unconditionally setting *optlen to sizeof(struct tcp_info) means
we'll never report the Linux extension fields for TCP_INFO.  I believe
we do have fall back logic to handle that, but that means the fuzzer
won't be exercising the same paths that we use most of the time.

Again, this is fine to get something going, but I think expanding the
fuzzing of TCP_INFO should be one of the highest priorities after the
basics are working: we make a *lot* of decisions which affect control
flow based on TCP_INFO (including many Linux extension fields).  We've
had a bunch of subtle TCP bugs caused here as well, so it's exactly
the sort of place that fuzzing would be beneficial.

> +		}
> +
> +		return 0;
> +	}
> +
> +	return getsockopt(fd, level, optname, optval, optlen);
> +}
> +
> +/**
> + * fuzz_recv() - recv wrapper
> + * @fd:		File descriptor
> + * @buf:	Output buffer
> + * @len:	Max bytes
> + * @flags:	recv flags (passed through for fd_tap)
> + *
> + * real recv() for fd_tap, AFL++ data for everything else
> + *
> + * Return: bytes read, or -1
> + */
> +ssize_t fuzz_recv(int fd, void *buf, size_t len, int flags)

You might be able to avoid some duplicated code by implementing
some of these wrappers in terms of each other: fuzz_recv() in terms of
fuzz_recvfrom() in terms of fuzz_recvmsg().

> +{
> +	size_t n;
> +
> +	/* fd is TAP socket for UNIX connection */
> +	if (fd == passt_ctx.fd_tap)
> +		return recv(fd, buf, len, flags);
> +
> +	if (!fuzz_recv_data || fuzz_recv_data_len <= 0) {
> +		errno = EAGAIN;
> +		return -1;

I haven't looked at the rest of the series yet, so I'm not sure how
fuzz_recv_data gets populated.  Because when fuzzing we're in a test
harness environment, it would also be acceptable to block waiting for
more data from the fuzzer here.

> +	}
> +
> +	n = (len < (size_t)fuzz_recv_data_len) ?
> +		len : (size_t)fuzz_recv_data_len;

We have an existing MIN macro.

> +	memcpy(buf, fuzz_recv_data, n);
> +	fuzz_recv_data += n;
> +	fuzz_recv_data_len -= n;

This suggests there's a fixed amount of data in the buffer.  So it
might also be ok to treat running out of fuzz data as an EOF.

I'd also consider allowing the fuzzer to generate errors or short
reads() from recv as a fairly high priority.  Those are reasonably
likely thing in real life, so it would be good to be able to exercise
those paths from the fuzzer.

> +	return n;
> +}
> +
> +/**
> + * fuzz_recvmsg() - recvmsg wrapper
> + * @fd:		File descriptor
> + * @msg:	Message header
> + * @flags:	recvmsg flags
> + *
> + * For fd_tap: calls real recvmsg. For all other fds: fills each
> + * buffer sequentially from the AFL++ shared memory stream.
> + *
> + * Return: total bytes read across all iovecs, or -1
> + */
> +ssize_t fuzz_recvmsg(int fd, struct msghdr *msg, int flags)
> +{
> +	size_t total = 0;
> +	size_t i;
> +
> +	if (fd == passt_ctx.fd_tap)
> +		return recvmsg(fd, msg, flags);
> +
> +	if (!fuzz_recv_data || fuzz_recv_data_len <= 0) {
> +		errno = EAGAIN;
> +		return -1;
> +	}
> +
> +	for (i = 0; i < (size_t)msg->msg_iovlen &&
> +	     fuzz_recv_data_len > 0; i++) {
> +		size_t n = msg->msg_iov[i].iov_len;
> +
> +		if ((int)n > fuzz_recv_data_len)
> +			n = fuzz_recv_data_len;
> +		memcpy(msg->msg_iov[i].iov_base, fuzz_recv_data, n);
> +		fuzz_recv_data += n;
> +		fuzz_recv_data_len -= n;
> +		total += n;

I think you can use iov_from_buf() to simplify this.

I think you also need to populate msg_name (if non-NULL).  Eventually
it would be good to fuzz that, but we can probably start off with just
a fixed value.

For UDP we do also use msg_control for IP_PKTINFO so that will need to
be populated too.  Like msg_name, you may be able to just use a fixed
value to start with, but it would be good to fuzz it eventually.

> +	}
> +
> +	return total;
> +}
> +
> +/**
> + * fuzz_recvfrom() - recvfrom wrapper
> + * @fd:		File descriptor
> + * @buf:	Output buffer
> + * @len:	Max bytes
> + * @flags:	recv flags
> + * @src:	Source address output
> + * @addrlen:	Source address length
> + *
> + * For fd_tap: calls real recvfrom(). For all other fds: zeroes the
> + * source address (so callers see a deterministic sender) and
> + * delegates to fuzz_recv() for the payload.
> + *
> + * Return: bytes read, or -1
> + */
> +ssize_t fuzz_recvfrom(int fd, void *buf, size_t len, int flags,
> +		      struct sockaddr *src, socklen_t *addrlen)
> +{
> +	if (fd == passt_ctx.fd_tap)
> +		return recvfrom(fd, buf, len, flags, src, addrlen);
> +
> +	if (src && addrlen)
> +		memset(src, 0, *addrlen);
> +
> +	return fuzz_recv(fd, buf, len, flags);
> +}
> +
> +/**
> + * fuzz_recvmmsg() - recvmmsg wrapper
> + * @fd:		File descriptor
> + * @mmh:	Array of mmsghdr structures to fill
> + * @vlen:	Number of mmsghdr entries available
> + * @flags:	recv flags
> + * @timeout:	Timeout
> + *
> + * For fd_tap: calls real recvmmsg(). For all other fds: fills only
> + * the first message from the AFL++ buffer via fuzz_recvmsg() and
> + * returns 1.
> + *
> + * Return: number of messages received (0 or 1), or -1
> + */
> +int fuzz_recvmmsg(int fd, struct mmsghdr *mmh, unsigned int vlen,
> +		  int flags, struct timespec *timeout)
> +{
> +	ssize_t n;
> +
> +	if (fd == passt_ctx.fd_tap)
> +		return recvmmsg(fd, mmh, vlen, flags, timeout);
> +
> +	if (!vlen || !fuzz_recv_data || fuzz_recv_data_len <= 0) {
> +		errno = EAGAIN;
> +		return -1;
> +	}
> +
> +	n = fuzz_recvmsg(fd, &mmh[0].msg_hdr, flags);
> +	if (n < 0)
> +		return -1;
> +
> +	mmh[0].msg_len = n;
> +	return 1;
> +}
> +
> +#endif

Comment to say what the #endif matches from way above would be helpful.

> diff --git a/fuzz.h b/fuzz.h
> new file mode 100644
> index 0000000..3f834a0
> --- /dev/null
> +++ b/fuzz.h
> @@ -0,0 +1,62 @@
> +//SPDX-License-Identifier: GPL-2.0-or-later
> +
> +/* fuzz.h - AFL++ fuzzing support for passt
> + *
> + * Copyright Red Hat
> + * Author: Anshu Kumari <anskuma@redhat.com>
> + */
> +
> +#ifndef FUZZ_H
> +#define FUZZ_H
> +
> +#ifdef FUZZING

I'd probably recommend against guarding the entire .h file with a
#ifdef.  Even if most of this is unnecessary when not fuzzing, it's
generally harmless declarations.  Obviously putting the actual macro
wrappers in place must still be #ifdef FUZZING.

> +
> +#include <stdint.h>
> +#include <stddef.h>
> +#include <time.h>
> +#include <sys/types.h>
> +#include <sys/socket.h>
> +
> +int fuzz_clock_gettime(clockid_t clk, struct timespec *tp);
> +void fuzz_clock_reset(void);
> +ssize_t fuzz_getrandom(void *buf, size_t buflen, unsigned int flags);
> +int fuzz_getsockopt(int fd, int level, int optname, void *optval,
> +		    socklen_t *optlen);
> +
> +#define clock_gettime(clk, tp) 		fuzz_clock_gettime(clk, tp)
> +#define getrandom(buf, len, flags) 	fuzz_getrandom(buf, len, flags)
> +#define getsockopt(fd, level, name, val, len) \
> +	fuzz_getsockopt(fd, level, name, val, len)
> +
> +/* AFL++ buf layout: [0..11] epoll_event, [12..65547] recv payload */
> +#define FUZZ_RECV_OFF		12
> +#define FUZZ_RECV_MAX		(64 * 1024)

Huh.. only 64kiB for all of our recv()s, that's not a lot.

> +
> +extern const unsigned char *fuzz_recv_data;
> +extern int fuzz_recv_data_len;
> +
> +ssize_t fuzz_recv(int fd, void *buf, size_t len, int flags);
> +ssize_t fuzz_recvmsg(int fd, struct msghdr *msg, int flags);
> +ssize_t fuzz_recvfrom(int fd, void *buf, size_t len, int flags,
> +		      struct sockaddr *src, socklen_t *addrlen);
> +int fuzz_recvmmsg(int fd, struct mmsghdr *mmh, unsigned int vlen,
> +		  int flags, struct timespec *timeout);
> +
> +/* Override existing wrappers for recvfrom() present inside util.h*/
> +#undef recvfrom
> +
> +#define recv(fd, buf, len, flags)		fuzz_recv(fd, buf, len, flags)
> +#define recvmsg(fd, msg, flags)			fuzz_recvmsg(fd, msg, flags)
> +#define recvfrom(fd, buf, len, flags, src, sl)		\
> +	fuzz_recvfrom(fd, buf, len, flags, src, sl)
> +#define recvmmsg(fd, mmh, vlen, flags, timeout)		\
> +	fuzz_recvmmsg(fd, mmh, vlen, flags, timeout)
> +
> +#define FUZZ_TURN_PATH	"/dev/shm/passt_fuzz_turn"
> +
> +struct fuzz_turn {

Explanatory comment would be helpful for this.

> +	uint32_t turn;
> +};
> +
> +#endif
> +#endif

Comments indicating what these match, please.

-- 
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

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 2/5] fuzz: Add flow type guards for fuzzing stability
  2026-08-12  7:26 ` [PATCH 2/5] fuzz: Add flow type guards for fuzzing stability Anshu Kumari
@ 2026-08-13  4:45   ` David Gibson
  0 siblings, 0 replies; 11+ messages in thread
From: David Gibson @ 2026-08-13  4:45 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, aerosound161, abdobngad, lvivier

[-- Attachment #1: Type: text/plain, Size: 10806 bytes --]

On Wed, Aug 12, 2026 at 12:56:25PM +0530, Anshu Kumari wrote:
> Under FUZZING, AFL++ can inject arbitrary epoll event types
> from its shared memory buffer. When an event references a flow
> table entry whose type doesn't match the handler, the existing
> assert() crashes the process eventually masking the real bugs.

This makes sense to me up until the "eventually masking the real
bugs".  IIUC the way AFL works is that it will use the coverage
feedback to adjust its input - so even if many inputs result in a
early crash due to mismatching types, it should be able to discover
inputs that get past that into more interesting territory.

Is it just a question of trying to filter our all the uninteresting
crashes from flow type mismatches?  Or to put it another way,
distinguishing between bugs and correct abnormal exits due to
something going wrong?

> If there is no flow at the start of fuzzing then also we are
> just returning early instead of hitting crashes.

I don't really understand what you mean by this.

> Allowing the
> fuzzer to explore other code path.
> 
> Replace assert() with NULL returns in the flow-lookup functions
> when compiled with -DFUZZING:
> 
> - tcp.c: conn_at_sidx(), tcp_timer_handler(), tcp_sock_handler()
> - tcp_splice.c: conn_at_sidx(), tcp_splice_sock_handler()
> - udp.c: udp_sock_handler(), udp_sock_to_sock(),
>   udp_buf_sock_to_tap(), udp_sock_fwd() error path
> - udp_flow.c: udp_at_sidx()
> - icmp.c: ping_at_sidx(), icmp_sock_handler()

So, in looking at this, I'm bearing in mind the distinction between
what should be a die() and what should be an assert().  Generally if
something external did something wrong we can't cope with, it's a
die() - and that's not a bug.  If we did something wrong it's an
assert() and that is a bug.

The tricky case is where the "external" thing is the kernel.  We
obviously have to trust it to some extent, so if we get something we
never expect to get from a kernel interace is that a die() or an
assert().

The reasoning for these being assert()s is that if we get something
unexpected here, by far the most likely cause is that we did something
wrong when we set the epoll reference, rather than the kernel
returning garbage.

Fuzzing, at least with the current draft setup is breaking that
assumption because the simulated kernel *is* returning garbage a lot
of the time.

So, we either need to constrain generation of epoll events to
"plausible" ones, or safely ignore bad events.  AIUI this patch is
taking the second approach.

That basic approach makes sense to me, but I'm not loving the way it
works out in practice.  It's fairly ugly and invasive, for starters.
But, hat concerns me more though, is that at the points you're
removing the assert()s it's not very obvious what new other paths the
early returns will trigger.  Because those are paths we'll never reach
in real workloads, they're also not that interesting to fuzz.

I can think of two possible approaches that might be this nicer.

1) IIUC the way the coverage driven fuzzing works, in the assert()
   cases we don't really need to carry on looking for other bugs.  It
   should be ok to exit immediately as long as that is flagged as "we
   exited because something went wrong externally" not "we exited
   because we hit a bug".  The coverage driven engine should then be
   able to go back and generate new inputs that explore other paths.

   I don't really know the interfaces used to communicate with AFL++,
   but could we replace these assert()s with say fuzz_assert(), which
   still exits, but signals to the fuzzer that this exit is not a bug?
   Is changing the abort() to a die() #ifdef FUZZING enough to do that?

2) We could move epoll event validation to immediately after the
   epoll_wait().  We esentially want to filter generated epoll events
   to plausible ones - at least meaning that the returned reference is
   one of the ones we epoll_add()ed.

   In one way, that's awkward, because we have to have a single point
   with all the validation logic, which might vary across various
   different branches.  On the other hand, it makes the invasiveness
   much more localised, and it's clearer what a validation failure
   will do, whether that's exit in a non-bug way, or just ignore this
   event and go on to the next one.

> Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> ---
>  icmp.c       | 14 +++++++++++++-
>  tcp.c        | 19 ++++++++++++++++++-
>  tcp_splice.c | 10 ++++++++++
>  udp.c        | 29 +++++++++++++++++++++++++++--
>  udp_flow.c   |  5 +++++
>  5 files changed, 73 insertions(+), 4 deletions(-)
> 
> diff --git a/icmp.c b/icmp.c
> index 0fe2366..cdfa253 100644
> --- a/icmp.c
> +++ b/icmp.c
> @@ -39,6 +39,7 @@
>  #include "icmp.h"
>  #include "flow_table.h"
>  #include "epoll_ctl.h"
> +#include "fuzz.h"
>  
>  #define ICMP_ECHO_TIMEOUT	60 /* s, timeout for ICMP socket activity */
>  #define ICMP_NUM_IDS		(1U << 16)
> @@ -58,7 +59,12 @@ static struct icmp_ping_flow *ping_at_sidx(flow_sidx_t sidx)
>  	if (!flow)
>  		return NULL;
>  
> +#ifdef FUZZING
> +	if (flow->f.type != FLOW_PING4 && flow->f.type != FLOW_PING6)
> +		return NULL;
> +#else
>  	assert(flow->f.type == FLOW_PING4 || flow->f.type == FLOW_PING6);
> +#endif
>  	return &flow->ping;
>  }
>  
> @@ -72,7 +78,13 @@ void icmp_sock_handler(const struct ctx *c, union epoll_ref ref,
>  		       const struct timespec *now)
>  {
>  	struct icmp_ping_flow *pingf = ping_at_sidx(ref.flowside);
> -	const struct flowside *ini = &pingf->f.side[INISIDE];
> +	const struct flowside *ini;
> +
> +#ifdef FUZZING
> +	if (!pingf)
> +		return;
> +#endif
> +	ini = &pingf->f.side[INISIDE];
>  	union sockaddr_inany sr;
>  	socklen_t sl = sizeof(sr);
>  	char buf[USHRT_MAX];
> diff --git a/tcp.c b/tcp.c
> index 3b78d2e..612c884 100644
> --- a/tcp.c
> +++ b/tcp.c
> @@ -316,6 +316,7 @@
>  #include "tcp_buf.h"
>  #include "tcp_vu.h"
>  #include "epoll_ctl.h"
> +#include "fuzz.h"
>  
>  /*
>   * The size of TCP header (including options) is given by doff (Data Offset)
> @@ -456,7 +457,12 @@ static struct tcp_tap_conn *conn_at_sidx(flow_sidx_t sidx)
>  	if (!flow)
>  		return NULL;
>  
> +#ifdef FUZZING
> +	if (flow->f.type != FLOW_TCP)
> +		return NULL;
> +#else
>  	assert(flow->f.type == FLOW_TCP);
> +#endif
>  	return &flow->tcp;
>  }
>  
> @@ -2681,7 +2687,14 @@ void tcp_timer_handler(const struct ctx *c, union epoll_ref ref,
>  		       const struct timespec *now)
>  {
>  	struct itimerspec check_armed = { { 0 }, { 0 } };
> -	struct tcp_tap_conn *conn = &FLOW(ref.flow)->tcp;
> +	struct tcp_tap_conn *conn;
> +
> +#ifdef FUZZING
> +	if (ref.flow >= FLOW_MAX ||
> +	    FLOW(ref.flow)->f.type != FLOW_TCP)
> +		return;
> +#endif
> +	conn = &FLOW(ref.flow)->tcp;
>  
>  	assert(!c->no_tcp);
>  	assert(conn->f.type == FLOW_TCP);
> @@ -2752,6 +2765,10 @@ void tcp_sock_handler(const struct ctx *c, union epoll_ref ref,
>  {
>  	struct tcp_tap_conn *conn = conn_at_sidx(ref.flowside);
>  
> +#ifdef FUZZING
> +	if (!conn)
> +		return;
> +#endif
>  	assert(!c->no_tcp);
>  	assert(pif_at_sidx(ref.flowside) != PIF_TAP);
>  
> diff --git a/tcp_splice.c b/tcp_splice.c
> index 4b01f1a..005ecd1 100644
> --- a/tcp_splice.c
> +++ b/tcp_splice.c
> @@ -105,7 +105,12 @@ static struct tcp_splice_conn *conn_at_sidx(flow_sidx_t sidx)
>  	if (!flow)
>  		return NULL;
>  
> +#ifdef FUZZING
> +	if (flow->f.type != FLOW_TCP_SPLICE)
> +		return NULL;
> +#else
>  	assert(flow->f.type == FLOW_TCP_SPLICE);
> +#endif
>  	return &flow->tcp_splice;
>  }
>  
> @@ -594,6 +599,11 @@ void tcp_splice_sock_handler(struct ctx *c, union epoll_ref ref,
>  	struct tcp_splice_conn *conn = conn_at_sidx(ref.flowside);
>  	unsigned evsidei = ref.flowside.sidei;
>  
> +#ifdef FUZZING
> +	if (!conn)
> +		return;
> +#endif
> +
>  	assert(conn->f.type == FLOW_TCP_SPLICE);
>  
>  	if (conn->events == SPLICE_CLOSED)
> diff --git a/udp.c b/udp.c
> index 505e554..9431353 100644
> --- a/udp.c
> +++ b/udp.c
> @@ -118,6 +118,7 @@
>  #include "udp_internal.h"
>  #include "udp_vu.h"
>  #include "epoll_ctl.h"
> +#include "fuzz.h"
>  
>  #define UDP_MAX_FRAMES		32  /* max # of frames to receive at once */
>  
> @@ -807,9 +808,15 @@ static void udp_sock_to_sock(const struct ctx *c, int from_s, int n,
>  	const struct flowside *toside = flowside_at_sidx(tosidx);
>  	const struct udp_flow *uflow = udp_at_sidx(tosidx);
>  	uint8_t topif = pif_at_sidx(tosidx);
> -	int to_s = uflow->s[tosidx.sidei];
> +	int to_s;
>  	int i;
>  
> +#ifdef FUZZING
> +	if (!uflow)
> +		return;
> +#endif
> +	to_s = uflow->s[tosidx.sidei];
> +
>  	if ((n = udp_sock_recv(c, from_s, udp_mh_recv, n)) <= 0)
>  		return;
>  
> @@ -836,9 +843,15 @@ static void udp_buf_sock_to_tap(const struct ctx *c, int s, int n,
>  {
>  	const struct flowside *toside = flowside_at_sidx(tosidx);
>  	struct udp_flow *uflow = udp_at_sidx(tosidx);
> -	uint8_t *omac = uflow->f.tap_omac;
> +	uint8_t *omac;
>  	int i;
>  
> +#ifdef FUZZING
> +	if (!uflow)
> +		return;
> +#endif
> +	omac = uflow->f.tap_omac;
> +
>  	if ((n = udp_sock_recv(c, s, udp_mh_recv, n)) <= 0)
>  		return;
>  
> @@ -901,10 +914,18 @@ void udp_sock_fwd(const struct ctx *c, int s, int rule_hint,
>  		} else if (flow_sidx_valid(tosidx)) {
>  			struct udp_flow *uflow = udp_at_sidx(tosidx);
>  
> +#ifdef FUZZING
> +			if (!uflow) {
> +				discard = true;
> +				continue;
> +			}
> +#endif
> +
>  			flow_err_ratelimit(
>  				uflow, now,
>  				"No support for forwarding UDP from %s to %s",
>  				pif_name(frompif), pif_name(topif));
> +
>  			discard = true;
>  		} else {
>  			warn_ratelimit(now, "Discarding datagram without flow");
> @@ -949,6 +970,10 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
>  {
>  	struct udp_flow *uflow = udp_at_sidx(ref.flowside);
>  
> +#ifdef FUZZING
> +	if (!uflow)
> +		return;
> +#endif
>  	assert(!c->no_udp && uflow);
>  
>  	if (events & EPOLLERR) {
> diff --git a/udp_flow.c b/udp_flow.c
> index f59649f..6c5b010 100644
> --- a/udp_flow.c
> +++ b/udp_flow.c
> @@ -31,7 +31,12 @@ struct udp_flow *udp_at_sidx(flow_sidx_t sidx)
>  	if (!flow)
>  		return NULL;
>  
> +#ifdef FUZZING
> +	if (flow->f.type != FLOW_UDP)
> +		return NULL;
> +#else
>  	assert(flow->f.type == FLOW_UDP);
> +#endif
>  	return &flow->udp;
>  }
>  
> -- 
> 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

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 3/5] fuzz: Bypass isolation and adapt sockets for AFL++
  2026-08-12  7:26 ` [PATCH 3/5] fuzz: Bypass isolation and adapt sockets for AFL++ Anshu Kumari
@ 2026-08-13  5:04   ` David Gibson
  0 siblings, 0 replies; 11+ messages in thread
From: David Gibson @ 2026-08-13  5:04 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, aerosound161, abdobngad, lvivier

[-- Attachment #1: Type: text/plain, Size: 7136 bytes --]

On Wed, Aug 12, 2026 at 12:56:26PM +0530, Anshu Kumari wrote:
> Few components which needs to be disabled to support AFL++
> to work:
> 
> - isolation.c: Skip isolation and seccomp sandboxing that breaks AFL++
>   pipes, namespaces, and ASan mmap/mprotect operations.
> - util.c / tap.c: Switch UNIX socket to SOCK_SEQPACKET to preserve frame
>   boundaries, simplifying tap_passt_input() to a single recv() and removing
>   vnet_len framing.

Most of these changes are pretty trivial, but this one is not.  I'd
suggest moving this into its own patch for clarity, and so it can get
a more detailed rationale / explanation in the commit message.

Because the test server is using its own SEQPACKET protocol, somewhat
similar to, but not identical with the qemu socket protocol, you're
essentially adding a new tap backend for fuzzing.  That's a reasonable
approach, but I think it would be clearer to treat it as that, rather
than as a weird special case of the normal passt tap backend.

> - passt.h: Use /tmp/passt_fuzz_%i.socket to avoid path collisions with
>   production instances.
> - tcp_buf.c: Include fuzz.h to route recvmsg() through deterministic wrappers.
> 
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> ---
>  isolation.c | 11 +++++++++++
>  passt.h     |  4 ++++
>  tap.c       | 21 +++++++++++++++++++++
>  tcp_buf.c   |  1 +
>  util.c      | 10 ++++++++++
>  5 files changed, 47 insertions(+)
> 
> diff --git a/isolation.c b/isolation.c
> index a30b329..61fc76a 100644
> --- a/isolation.c
> +++ b/isolation.c
> @@ -208,6 +208,9 @@ static int move_root(void)
>   */
>  void isolate_initial(void)
>  {
> +#ifdef FUZZING
> +	return;
> +#endif

Rather than just eliminating the isolate_*() routines entirely, I'd
prefer to selectively disable the specific parts that block fuzzing.

>  	uint64_t keep;
>  
>  	/* We want to keep CAP_NET_BIND_SERVICE in the initial
> @@ -389,6 +392,10 @@ void isolate_user(const struct ctx *c, uid_t uid, gid_t gid, bool use_userns,
>   */
>  int isolate_prefork(const struct ctx *c)
>  {
> +#ifdef FUZZING
> +	(void)c;
> +	return 0;
> +#endif
>  	int flags = CLONE_NEWIPC | CLONE_NEWNS | CLONE_NEWUTS;
>  	uint64_t ns_caps = 0;
>  
> @@ -466,6 +473,10 @@ int isolate_prefork(const struct ctx *c)
>   */
>  void isolate_postfork(const struct ctx *c)
>  {
> +#ifdef FUZZING
> +	(void)c;
> +	return;
> +#endif
>  	struct sock_fprog prog;
>  
>  	prctl(PR_SET_DUMPABLE, 0);
> diff --git a/passt.h b/passt.h
> index 51ccd4f..141c9f8 100644
> --- a/passt.h
> +++ b/passt.h
> @@ -7,7 +7,11 @@
>  #define PASST_H
>  
>  #define UNIX_SOCK_MAX		100
> +#ifdef FUZZING
> +#define UNIX_SOCK_PATH		"/tmp/passt_fuzz_%i.socket"
> +#else
>  #define UNIX_SOCK_PATH		"/tmp/passt_%i.socket"
> +#endif

Good idea.

>  union epoll_ref;
>  
> diff --git a/tap.c b/tap.c
> index dfa66c7..f32c9ad 100644
> --- a/tap.c
> +++ b/tap.c
> @@ -14,6 +14,7 @@
>   */
>  
>  #include <sched.h>
> +#include <time.h>
>  #include <unistd.h>
>  #include <signal.h>
>  #include <stdio.h>
> @@ -61,6 +62,7 @@
>  #include "vhost_user.h"
>  #include "vu_common.h"
>  #include "epoll_ctl.h"
> +#include "fuzz.h"
>  
>  /* Maximum allowed frame lengths (including L2 header) */
>  
> @@ -144,8 +146,10 @@ void tap_send_single(const struct ctx *c, const void *data, size_t l2len)
>  
>  	switch (c->mode) {
>  	case MODE_PASST:
> +#ifndef FUZZING
>  		iov[iovcnt] = IOV_OF_LVALUE(vnet_len);
>  		iovcnt++;
> +#endif

Right, I think this might be clearer as a new 'case MODE_FUZZ:'.

>  		/* fall through */
>  	case MODE_PASTA:
>  		iov[iovcnt].iov_base = (void *)data;
> @@ -1231,6 +1235,22 @@ static void tap_passt_input(struct ctx *c, const struct timespec *now)
>  
>  	tap_flush_pools();
>  
> +#ifdef FUZZING
> +	/* SOCK_SEQPACKET: each recv returns exactly one frame */

And I think this would be clearer as a new tap_fuzz_input().

> +	do {
> +		n = recv(c->fd_tap, pkt_buf, sizeof(pkt_buf), MSG_DONTWAIT);
> +	} while ((n < 0) && errno == EINTR);
> +
> +	if (n > 0 && n >= (ssize_t)sizeof(struct ethhdr)) {

I suggest removing the length check: that way the fuzzer can also look
for any bugs we might have if we ever get undersized frames from the
tap interface.

> +		struct iov_tail data;
> +
> +		data = IOV_TAIL_FROM_BUF(pkt_buf, n, 0);
> +		tap_add_packet(c, &data, now);
> +	} else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
> +		tap_sock_reset(c);

I don't think we really care about reset and recovery for the fuzzing
case, so a die() would probably suffice here.

> +		return;
> +	}


> +#else
>  	if (partial_len) {
>  		/* We have a partial frame from an earlier pass.  Move it to the
>  		 * start of the buffer, top up with new data, then process all
> @@ -1281,6 +1301,7 @@ static void tap_passt_input(struct ctx *c, const struct timespec *now)
>  
>  	partial_len = n;
>  	partial_frame = p;
> +#endif

Whenever a #if is more than a handful of lines, it's generally helpful
to put a comment on the #endif so you can tell what the #if was
conditional on without having to scroll up a bunch.

>  
>  	tap_handler(c, now);
>  }
> diff --git a/tcp_buf.c b/tcp_buf.c
> index 72c4541..eb28abe 100644
> --- a/tcp_buf.c
> +++ b/tcp_buf.c
> @@ -32,6 +32,7 @@
>  #include "tcp_conn.h"
>  #include "tcp_internal.h"
>  #include "tcp_buf.h"
> +#include "fuzz.h"
>  
>  #define TCP_FRAMES_MEM			128
>  #define TCP_FRAMES							   \
> diff --git a/util.c b/util.c
> index 28c32e4..7f29c3b 100644
> --- a/util.c
> +++ b/util.c
> @@ -36,6 +36,7 @@
>  #include "epoll_ctl.h"
>  #include "pasta.h"
>  #include "serialise.h"
> +#include "fuzz.h"
>  #ifdef HAS_GETRANDOM
>  #include <sys/random.h>
>  #endif
> @@ -229,7 +230,11 @@ int sock_l4_dualstack_any(const struct ctx *c, enum epoll_type type,
>   */
>  int sock_unix(char *sock_path)
>  {
> +#ifdef FUZZING
> +	int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
> +#else
>  	int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
> +#endif

Special casing what's ostensibly a general helper to open unix sockets
is a bit nasty - it's relying on the fact that the only Unix socket
that we're really using is the one for tap.  Treating fuzz as a
different tap backend would address this too.

>  	struct sockaddr_un addr = {
>  		.sun_family = AF_UNIX,
>  	};
> @@ -248,8 +253,13 @@ int sock_unix(char *sock_path)
>  					UNIX_SOCK_PATH, i))
>  			die_perror("Can't build UNIX domain socket path");
>  
> +#ifdef FUZZING
> +		ex = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC,
> +			    0);
> +#else
>  		ex = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
>  			    0);
> +#endif
>  		if (ex < 0)
>  			die_perror("Failed to check for UNIX domain conflicts");
>  
> -- 
> 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

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 4/5] fuzz: Add AFL++ persistent mode fuzz loop
  2026-08-12  7:26 ` [PATCH 4/5] fuzz: Add AFL++ persistent mode fuzz loop Anshu Kumari
@ 2026-08-13  6:34   ` Stefano Brivio
  0 siblings, 0 replies; 11+ messages in thread
From: Stefano Brivio @ 2026-08-13  6:34 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: david, passt-dev, aerosound161, abdobngad, lvivier

Not a complete review, just a few notes, mostly about the general
concept:

On Wed, 12 Aug 2026 12:56:27 +0530
Anshu Kumari <anskuma@redhat.com> wrote:

> Add the AFL++ persistent mode fuzz loop to passt.c main().
> The loop uses __AFL_LOOP() for in-process iteration and
> __AFL_FUZZ_TESTCASE_BUF for shared memory fuzzing.
> 
> Each iteration:
>   - Resets deterministic clock, flow table, and epoll instance.
>   - Drains stale data from the TAP socket.
>   - Reads an epoll event from the AFL++ buffer.
>   - For TAP events: constructs a packet with fixed L2/L3/L4
>     headers and injects it via tap_add_packet() + tap_handler().
>   - Exchanges a turn flag with the test server for
>     bidirectional flow over the UNIX socket.

This complexity could probably be avoided if you switch to a model where
the test server is just operating on the host side of things (accepting
TCP connections and replying). More on that in a bit as a comment to
5/5.

>   - Calls passt_worker() to process the event.
>   - Polls for host-side TCP events via epoll_wait().
>   - Runs post_handler() for deferred work.
> 
> Added the 'make fuzz' target which builds passt with
> afl-clang-fast, -DFUZZING, -DNDEBUG, and AddressSanitizer.
> 
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> ---
>  Makefile |   8 +++
>  passt.c  | 189 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
>  2 files changed, 197 insertions(+)
> 
> diff --git a/Makefile b/Makefile
> index fe1df58..8e4121e 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -123,6 +123,14 @@ valgrind: BASE_CPPFLAGS += -DVALGRIND
>  valgrind: BASE_CFLAGS += -g
>  valgrind: all
>  
> +FUZZ_CC ?= afl-clang-fast
> +
> +.PHONY: fuzz
> +
> +fuzz:
> +	$(MAKE) clean
> +	$(MAKE) CC="$(FUZZ_CC)" CPPFLAGS="-DFUZZING -DNDEBUG" CFLAGS="-g -fsanitize=address" passt
> +
>  .PHONY: clean
>  clean:
>  	$(RM) $(BIN) *~ *.o seccomp.h seccomp_repair.h seccomp_pesto.h pasta.1 \
> diff --git a/passt.c b/passt.c
> index 5054551..e026eb2 100644
> --- a/passt.c
> +++ b/passt.c
> @@ -35,6 +35,7 @@
>  #include <sys/prctl.h>
>  #include <netinet/if_ether.h>
>  #include <libgen.h>
> +#include <netinet/tcp.h>
>  
>  #include "util.h"
>  #include "passt.h"
> @@ -54,12 +55,56 @@
>  #include "repair.h"
>  #include "netlink.h"
>  #include "epoll_ctl.h"
> +#include "flow_table.h"
> +#include "fuzz.h"
>  
>  #define NUM_EPOLL_EVENTS	8
>  
>  #define TIMER_INTERVAL_		MIN(TCP_TIMER_INTERVAL, FWD_PORT_SCAN_INTERVAL)
>  #define TIMER_INTERVAL		MIN(TIMER_INTERVAL_, FLOW_TIMER_INTERVAL)
>  
> +#ifdef FUZZING
> +
> +/* AFL++ persistent mode / shared memory fuzzing compatibility macros. */
> +#ifndef __AFL_FUZZ_TESTCASE_LEN
> +  ssize_t		fuzz_len;
> +  unsigned char		fuzz_buf[1024 * 1024];
> +# define __AFL_FUZZ_TESTCASE_LEN	fuzz_len
> +# define __AFL_FUZZ_TESTCASE_BUF	fuzz_buf
> +# define __AFL_FUZZ_INIT()		void sync(void)
> +# define __AFL_LOOP(x)		\
> +	((fuzz_len = read(0, fuzz_buf, sizeof(fuzz_buf))) > 0 ? 1 : 0)
> +# define __AFL_INIT()		sync()
> +#endif
> +
> +#ifdef __AFL_HAVE_MANUAL_CONTROL
> +  __AFL_FUZZ_INIT();
> +#endif
> +
> +static struct fuzz_turn *fuzz_turn_ptr;
> +
> +/**
> + * fuzz_turn_connect() - Map the turn flag shared memory
> + *
> + * Return: pointer to mapped turn flag, or NULL on failure
> + */
> +static struct fuzz_turn *fuzz_turn_connect(void)
> +{
> +	struct fuzz_turn *t;
> +	int fd;
> +
> +	fd = open(FUZZ_TURN_PATH, O_RDWR);
> +	if (fd < 0)
> +		return NULL;
> +
> +	t = mmap(NULL, sizeof(*t), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
> +	close(fd);
> +
> +	return (t == MAP_FAILED) ? NULL : t;
> +}
> +
> +#endif
> +
>  char pkt_buf[PKT_BUF_BYTES]	__attribute__ ((aligned(PAGE_SIZE)));
>  
>  struct ctx passt_ctx = {
> @@ -282,9 +327,17 @@ static void passt_worker(void *opaque, int nfds, struct epoll_event *events)
>  			icmp_sock_handler(c, ref, &now);
>  			break;
>  		case EPOLL_TYPE_VHOST_CMD:
> +#ifdef FUZZING
> +			if (!c->vdev)
> +				break;
> +#endif
>  			vu_control_handler(c->vdev, c->fd_tap, eventmask);
>  			break;
>  		case EPOLL_TYPE_VHOST_KICK:
> +#ifdef FUZZING
> +			if (!c->vdev)
> +				break;
> +#endif
>  			vu_kick_cb(c->vdev, ref, &now);
>  			break;
>  		case EPOLL_TYPE_REPAIR_LISTEN:
> @@ -450,6 +503,141 @@ int main(int argc, char **argv)
>  
>  	timer_init(c, &now);
>  
> +#ifdef FUZZING
> +	fuzz_turn_ptr = fuzz_turn_connect();
> +
> +#define FUZZ_LOOP_ITERATIONS	10000
> +#define FUZZ_DRAIN_BUF_SIZE	1600
> +
> +#ifdef __AFL_HAVE_MANUAL_CONTROL
> +	__AFL_INIT();
> +#endif
> +	{
> +		unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
> +
> +		while (__AFL_LOOP(FUZZ_LOOP_ITERATIONS)) {

I think this loop is a useful implementation, as far as I understand
the purpose is to avoid that AFL++ needs to restarts us at every new
attempt. So I think it makes sense that you reset the state below.

But, inside this loop, we need to allow AFL++ to send us arbitrary
sequences of packets, not just inject a single one. Not much will
happen with a single packet.

> +			int len = __AFL_FUZZ_TESTCASE_LEN;
> +			int injected = 0;
> +			int pkt_len, round;
> +			struct epoll_event ev;
> +			union epoll_ref ref;
> +			int min_pkt = sizeof(struct ethhdr) +
> +				      sizeof(struct iphdr) +
> +				      sizeof(struct tcphdr);
> +
> +			if (len < (int)sizeof(ev))
> +				continue;
> +
> +			/* Reset clock, flow table and epoll for each
> +			 * AFL++ iteration.
> +			 */
> +			fuzz_clock_reset();
> +			clock_gettime(CLOCK_MONOTONIC, &now);
> +			timer_init(c, &now);
> +
> +			flow_init();
> +
> +			/* Recreate epoll instance */
> +			close(c->epollfd);
> +			c->epollfd = epoll_create1(EPOLL_CLOEXEC);
> +			flow_epollid_register(EPOLLFD_ID_DEFAULT, c->epollfd);
> +
> +			if (c->fd_tap >= 0) {
> +				union epoll_ref tref = {
> +					.type = EPOLL_TYPE_TAP_PASST,
> +					.fd = c->fd_tap
> +				};
> +				epoll_add(c->epollfd,
> +					  EPOLLIN | EPOLLRDHUP, tref);
> +
> +				/* Drain stale socket data */
> +				char drain[FUZZ_DRAIN_BUF_SIZE];
> +				while (recv(c->fd_tap, drain, sizeof(drain),
> +					    MSG_DONTWAIT) > 0);
> +			}
> +
> +			/* Read epoll event from AFL++ buffer */
> +			memcpy(&ev, buf, sizeof(ev));
> +			ref = *((union epoll_ref *)&ev.data.u64);

This is needed to let AFL++ generate events. But if you call
epoll_wait() below, with 'events' (which is not set from 'ev'), we
won't actually use those events generated by AFL++. I guess you're only
getting events from the test server. But I don't think that hardcoding
a sequence of:

- single packet from AFL++ (tap side)

- four packets from the test server (all host side I guess?)

will actually result in any meaningful exchange (including a TCP
connection).

By the way, in the approach I was suggesting, where AFL++ would act as
guest and feeding data to us directly, while the test server would act
as host / internet side (with data fed from AFL++), AFL++ would only
generate tap-side events, so we would probably need to *add* those to
'ev' while also reacting to host-side events (for example the test
server accepting a connection, or sending data over an accepted
connection).

> +
> +			/* Set recv payload in AFL++ shared memory */
> +			fuzz_recv_data = buf + FUZZ_RECV_OFF;
> +			fuzz_recv_data_len =
> +				(len > FUZZ_RECV_OFF + FUZZ_RECV_MAX)
> +					     ? FUZZ_RECV_MAX
> +					     : ((len > FUZZ_RECV_OFF)
> +						? len - FUZZ_RECV_OFF : 0);
> +
> +			/* Inject fuzz packet for TAP events */
> +			if (ref.type == EPOLL_TYPE_TAP_PASST ||
> +			    ref.type == EPOLL_TYPE_TAP_PASTA) {
> +				struct iov_tail data;
> +				struct ethhdr *eh;
> +				struct iphdr *iph;
> +				struct tcphdr *th;
> +
> +				tap_flush_pools();
> +				memset(pkt_buf, 0, min_pkt);
> +
> +				pkt_len = len - (int)sizeof(ev);
> +				if (pkt_len > 0)
> +					memcpy(pkt_buf, buf + sizeof(ev),
> +					       pkt_len);
> +				if (pkt_len < min_pkt)
> +					pkt_len = min_pkt;
> +
> +				/* construct ethernet header */

I guess this whole path is needed to quickly get something working,
but, eventually, we shouldn't need this. We need to give the
possibility to AFL++ to give us multiple packets, and possibly (or
especially) malformed ones.

If it just generates payload, that looks relatively "safe" and is
relatively unlikely to discover issues.

> +				eh = (struct ethhdr *)pkt_buf;
> +				memcpy(eh->h_dest, c->our_tap_mac, ETH_ALEN);
> +				memcpy(eh->h_source, c->guest_mac, ETH_ALEN);
> +				eh->h_proto = htons(ETH_P_IP);
> +
> +				/* construct IPv4 header */
> +				iph = (struct iphdr *)(pkt_buf + sizeof(*eh));
> +				iph->version = 4;
> +				iph->ihl = 5;
> +				iph->protocol = IPPROTO_TCP;
> +				iph->saddr = c->ip4.addr.s_addr;
> +				iph->daddr = c->ip4.guest_gw.s_addr;
> +				iph->tot_len = htons(pkt_len - sizeof(*eh));
> +
> +				/* Fix TCP Header */
> +				th = (struct tcphdr *)(pkt_buf + sizeof(*eh) +
> +								 sizeof(*iph));
> +				th->dest = htons(9999);
> +				if (th->doff < 5)
> +					th->doff = 5;
> +
> +				data = IOV_TAIL_FROM_BUF(pkt_buf, pkt_len, 0);
> +				tap_add_packet(c, &data, &now);
> +				tap_handler(c, &now);
> +				injected = 1;
> +			}
> +
> +			/* Turn exchange -- only if data was sent */
> +			if (injected && fuzz_turn_ptr) {
> +				__atomic_store_n(&fuzz_turn_ptr->turn, 1,
> +						 __ATOMIC_RELEASE);
> +				while (__atomic_load_n(&fuzz_turn_ptr->turn,
> +						      __ATOMIC_ACQUIRE) != 0);
> +			}
> +
> +			passt_worker(c, 1, &ev);
> +
> +			/* Process host-side TCP events */
> +			for (round = 0; round < 4; round++) {
> +				nfds = epoll_wait(c->epollfd, events,
> +						  NUM_EPOLL_EVENTS, 0);
> +				if (nfds <= 0)
> +					break;
> +				passt_worker(c, nfds, events);
> +			}
> +
> +			post_handler(c, &now);
> +		}
> +	}
> +	return 0;
> +#else
>  loop:
>  	/* NOLINTBEGIN(bugprone-branch-clone): intervals can be the same */
>  	/* cppcheck-suppress [duplicateValueTernary, unmatchedSuppression] */
> @@ -461,4 +649,5 @@ loop:
>  	passt_worker(c, nfds, events);
>  
>  	goto loop;
> +#endif /* FUZZING */
>  }

-- 
Stefano


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 5/5] fuzz: Add test server for bidirectional protocol fuzzing
  2026-08-12  7:26 ` [PATCH 5/5] fuzz: Add test server for bidirectional protocol fuzzing Anshu Kumari
@ 2026-08-13  7:53   ` Stefano Brivio
  0 siblings, 0 replies; 11+ messages in thread
From: Stefano Brivio @ 2026-08-13  7:53 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: david, passt-dev, aerosound161, abdobngad, lvivier

On Wed, 12 Aug 2026 12:56:28 +0530
Anshu Kumari <anskuma@redhat.com> wrote:

> Add fuzz-server that acts as passt's network peer
> during fuzzing.

To me, this part makes sense. But this one:

> It connects to passt's UNIX socket

much less, while:

> and listens on 127.0.0.1:9999 for TCP connections.

this is the part that I expected instead. Otherwise it's not just
passt's network peer, it's the guest as well.

> 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.

This looks rather complicated to me.

The approach I was suggesting with a test server is the following:


    ,- exchanges guest-side data with ------------.
    |                                   ,---------|---------.
    |                                ,--|      passt        |
    |                               /   '-.---------------^-'
,---|---.                          /      | connect(),    | accept(),
| AFL++ |-- shares memory with ---|       | send data,    | reply with
'---|---'                          \      | etc.          | data, etc.
    |                               \   ,-v---------------'-.
    |                                '--|    test server    |
    |                                   '---------|---------'
    '- exchanges host-side data with -------------'

...at least in its basic form. Eventually, the test server should be
able to connect to passt itself (and we could call it "test peer" at
that point).

As far as I understood, it's not trivial to make the same instance
of AFL++ share memory with two processes at the same time, so the
memory-sharing path might need to take a more complicated turn, for
example there could be a wrapper starting both passt and the test
server and sharing memory with them, or passt could _additionally_
(using a special out-of-band fuzzing channel) share data from AFL++
with the test server.

An example of communication below (but events don't necessarily need
to be in this order, this is just an example). For simplicity, let's
ignore the fact that AFL++ might not directly share memory with passt
and test server, and assume there are three areas of memory that
AFL++ directly controls:

a. shared with passt: an array of struct epoll_event, 'ev'

b. shared with passt: the kind of tap-side buffer you implemented in
   4/5, 'buf'

c. shared with the test server: a separate buffer, 'test_buf'

Example:

1. AFL++ writes an EPOLLIN event in 'ev' with type
   EPOLL_TYPE_TAP_PASST, of some data in 'buf', and some data in
   'test_buf'

2. AFL++ starts passt and the test server

3. passt reads the EPOLL_TYPE_TAP_PASST event from 'ev', reads data
   from 'buf' and hands it to passt_tap_handler()

4. this happens to be have Ethernet, IP, and TCP headers, with the
   SYN flag set, and destination address set to the address of the
   test server (we might want to force all this, at least initially,
   or give it as a hint to AFL++ somehow), so passt connects to
   the test server

5. the test server accepts the connection, and sends the contents
   of 'test_buf' on it (for the test server, this is directly
   payload, without headers, as they don't make sense there). I'm
   not sure if we should have a different set of events (maybe we
   need a "play script" for the server, in case?)

6. this generates an EPOLLOUT event for passt. It's not in 'ev',
   it's a regular epoll_wait() (I think we could have an
   epoll_wait() loop where we additionally read one event from
   'ev' for every iteration, or something like that)

7. passt marks the connection as established and inserts it in the
   flow table

8. passt reads the data sent from the test server and generates
   whatever TCP data packet to the "guest" (it might simply be
   a sink)

...and this attempt ends here because AFL++ generated a single
event for passt, but there could be more (this should also be
decided by AFL++).

Would something like this make sense?

-- 
Stefano


^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-08-13  7:53 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-12  7:26 [PATCH 0/5] Add AFL++ fuzzing support for passt Anshu Kumari
2026-08-12  7:26 ` [PATCH 1/5] fuzz: Add deterministic wrappers for system calls Anshu Kumari
2026-08-13  3:46   ` David Gibson
2026-08-12  7:26 ` [PATCH 2/5] fuzz: Add flow type guards for fuzzing stability Anshu Kumari
2026-08-13  4:45   ` David Gibson
2026-08-12  7:26 ` [PATCH 3/5] fuzz: Bypass isolation and adapt sockets for AFL++ Anshu Kumari
2026-08-13  5:04   ` David Gibson
2026-08-12  7:26 ` [PATCH 4/5] fuzz: Add AFL++ persistent mode fuzz loop Anshu Kumari
2026-08-13  6:34   ` Stefano Brivio
2026-08-12  7:26 ` [PATCH 5/5] fuzz: Add test server for bidirectional protocol fuzzing Anshu Kumari
2026-08-13  7:53   ` Stefano Brivio

Code repositories for project(s) associated with this public inbox

	https://passt.top/passt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).