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 > --- > 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 > + */ > + > +#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 > +#include > +#include > +#include > +#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 > + */ > + > +#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 > +#include > +#include > +#include > +#include > + > +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