* [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; 6+ 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] 6+ 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-12 7:26 ` [PATCH 2/5] fuzz: Add flow type guards for fuzzing stability Anshu Kumari
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ 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] 6+ 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-12 7:26 ` [PATCH 3/5] fuzz: Bypass isolation and adapt sockets for AFL++ Anshu Kumari
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ 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] 6+ 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-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, 0 replies; 6+ 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] 6+ 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-12 7:26 ` [PATCH 5/5] fuzz: Add test server for bidirectional protocol fuzzing Anshu Kumari
4 siblings, 0 replies; 6+ 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] 6+ 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
4 siblings, 0 replies; 6+ 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] 6+ messages in thread
end of thread, other threads:[~2026-08-12 7:27 UTC | newest]
Thread overview: 6+ 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-12 7:26 ` [PATCH 2/5] fuzz: Add flow type guards for fuzzing stability Anshu Kumari
2026-08-12 7:26 ` [PATCH 3/5] fuzz: Bypass isolation and adapt sockets for AFL++ Anshu Kumari
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
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).