From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=pass (p=quarantine dis=none) header.from=redhat.com Authentication-Results: passt.top; dkim=pass (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=e0o4HRYk; dkim-atps=neutral Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by passt.top (Postfix) with ESMTPS id 250615A026E for ; Mon, 28 Sep 2026 07:17:55 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1790572674; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=Q28lUHU8654HZiwqpWNYOe1fwGiszu8XlpUsVrzPQYU=; b=e0o4HRYk06guWbRxSxidDlzoVqTiYA9TS0KS2/SqlAJwFFkuc8s+8bv0/EdWhUnpJ9Xr8d l2FKRBiDT/mih6UpLsSiZ+Z88jr7WdlTIe4Sj5nIsm6HPJKXpO4OI/TrfOhkjduGdpd4qb 0vHZeO//v3cPLrmxo4EhsxCQi3U1agI= Received: from mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-618-Hl_x7nxXOsuRW2lBSvFeCg-1; Mon, 28 Sep 2026 01:17:50 -0400 X-MC-Unique: Hl_x7nxXOsuRW2lBSvFeCg-1 X-Mimecast-MFC-AGG-ID: Hl_x7nxXOsuRW2lBSvFeCg_1790572670 Received: from mx-prod-int-05.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-05.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.17]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id B8B791828B17; Mon, 28 Sep 2026 05:17:49 +0000 (UTC) Received: from anskuma-thinkpadp1gen7.bengluru.csb (unknown [10.74.80.83]) by mx-prod-int-05.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 93BA219560AB; Mon, 28 Sep 2026 05:17:47 +0000 (UTC) From: Anshu Kumari To: sbrivio@redhat.com, passt-dev@passt.top Subject: [PATCH v2 6/7] fuzz: Add host-side test server for bidirectional fuzzing Date: Mon, 28 Sep 2026 10:47:26 +0530 Message-ID: <20260928051727.2251281-7-anskuma@redhat.com> In-Reply-To: <20260928051727.2251281-1-anskuma@redhat.com> References: <20260928051727.2251281-1-anskuma@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.0 on 10.30.177.17 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: Y0n4EuGXosQeYPFXbH62g3pYlgaZrOo5KyF7Obi3--Q_1790572670 X-Mimecast-Originator: redhat.com Content-Transfer-Encoding: 8bit content-type: text/plain; charset="US-ASCII"; x-default=true Message-ID-Hash: 37VBPWD7EILLBIX7BGKNCRID7LUO3RFO X-Message-ID-Hash: 37VBPWD7EILLBIX7BGKNCRID7LUO3RFO X-MailFrom: anskuma@redhat.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: lvivier@redhat.com, anskuma@redhat.com, abdobngad@gmail.com X-Mailman-Version: 3.3.8 Precedence: list List-Id: Development discussion and patches for passt Archived-At: Archived-At: List-Archive: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Add fuzz-server, a standalone program that acts as a host-side peer for TCP connections from passt during fuzzing. It attaches to AFL++'s shared memory segment (via the inherited __AFL_SHM_FUZZ_ID env var) and sends region (c) payload on every accepted connection and after each read, enabling bidirectional protocol fuzzing without mocking recv(). Combined with AnyIP routing in the fuzzing namespace, fuzz-server listens on TCP ports (1-55535) on 0.0.0.0 to intercept every outbound connection from passt regardless of destination IP or port. Fork+exec'd by passt before __AFL_INIT(), so it starts once in the forkserver parent and persists across iterations. Signed-off-by: Anshu Kumari --- fuzz-server.c | 343 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 fuzz-server.c diff --git a/fuzz-server.c b/fuzz-server.c new file mode 100644 index 00000000..15f4348d --- /dev/null +++ b/fuzz-server.c @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/* fuzz-server.c - Host-side test peer for AFL++ fuzzing of passt + * + * Accepts TCP connections from passt and sends AFL++-controlled + * payload read directly from AFL++'s shared memory (region c of + * the testcase buffer). + * + * Started by passt (fork+exec before __AFL_INIT), inherits the + * __AFL_SHM_FUZZ_ID env var and attaches directly. + * + * Runs in a network namespace with AnyIP routing, listening on + * TCP ports 1 through FUZZ_LISTEN_MAX on both IPv4 and IPv6. + * The top FUZZ_RESERVED_PORTS ports are left free for passt's + * own connect() and bind() calls. + * + * Build: make fuzz-server + * Run: started automatically by passt when built with FUZZING + * + * Copyright Red Hat + * Author: Anshu Kumari + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fuzz-testbuf.h" + +#define FUZZ_MAX_PORT 65535 +#define FUZZ_RESERVED_PORTS 10000 +#define FUZZ_LISTEN_MAX (FUZZ_MAX_PORT - FUZZ_RESERVED_PORTS) +#define MAX_EVENTS 64 + +#define TYPE_LISTENER 1 +#define TYPE_CONNECTION 2 + +static uint8_t *afl_shmem; +static int epfd = -1; + +/** + * map_afl_shmem() - Attach to AFL++'s shared memory segment + * + * Reads __AFL_SHM_FUZZ_ID env var (inherited when passt fork+execs). + * + * Return: 0 on success, -1 on failure + */ +static int map_afl_shmem(void) +{ + const char *env; + char *endptr; + void *ptr; + long id; + + env = getenv("__AFL_SHM_FUZZ_ID"); + if (!env) + return -1; + + errno = 0; + id = strtol(env, &endptr, 10); + if (errno || *endptr || endptr == env) + return -1; + + ptr = shmat((int)id, NULL, SHM_RDONLY); + if (ptr == (void *)-1) + return -1; + + afl_shmem = ptr; + return 0; +} + +/** + * listen_on() - Create one non-blocking listening socket + * @af: Address family, AF_INET or AF_INET6 + * @port: TCP port to bind + * + * Return: listening socket, or -1 if it can't be created or bound + */ +static int listen_on(int af, int port) +{ + struct sockaddr_in6 sa6 = { + .sin6_family = AF_INET6, + .sin6_addr = in6addr_any, + .sin6_port = htons(port), + }; + struct sockaddr_in sa4 = { + .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_ANY), + .sin_port = htons(port), + }; + const struct sockaddr *sa; + int fd, opt = 1; + socklen_t sl; + + fd = socket(af, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (fd < 0) + return -1; + + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); + + if (af == AF_INET6) { + setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)); + sa = (const struct sockaddr *)&sa6; + sl = sizeof(sa6); + } else { + sa = (const struct sockaddr *)&sa4; + sl = sizeof(sa4); + } + + if (bind(fd, sa, sl) || listen(fd, 128)) { + close(fd); + return -1; + } + + return fd; +} + +/** + * epoll_add() - Register @fd in the event loop, tagged with @type + * @fd: File descriptor to watch + * @type: TYPE_LISTENER or TYPE_CONNECTION + * @events: epoll event mask + */ +static void epoll_add(int fd, uint32_t type, uint32_t events) +{ + struct epoll_event ev = { + .events = events, + .data.u64 = ((uint64_t)type << 32) | (uint32_t)fd, + }; + + epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); +} + +/** + * server_init() - Create TCP listeners on every port, IPv4 and IPv6 + * + * Binds to 0.0.0.0 and [::] on ports 1 through FUZZ_LISTEN_MAX. + * Ports that fail to bind are skipped. + * + * Return: 0 on success, -1 on failure + */ +static int server_init(void) +{ + struct rlimit rl; + int port; + + if (getrlimit(RLIMIT_NOFILE, &rl)) + return -1; + + rl.rlim_cur = rl.rlim_max; + if (setrlimit(RLIMIT_NOFILE, &rl)) + return -1; + + epfd = epoll_create1(EPOLL_CLOEXEC); + if (epfd < 0) + return -1; + + for (port = 1; port <= FUZZ_LISTEN_MAX; port++) { + int fd; + + if ((fd = listen_on(AF_INET, port)) >= 0) + epoll_add(fd, TYPE_LISTENER, EPOLLIN); + + if ((fd = listen_on(AF_INET6, port)) >= 0) + epoll_add(fd, TYPE_LISTENER, EPOLLIN); + } + + return 0; +} + +/** + * send_fuzz_payload() - Send region (c) from AFL++ shared memory + * @fd: Connected non-blocking socket + * + * Reads the testcase length and event count from AFL++'s shared + * memory, computes the region (c) offset and length, and sends + * the payload to @fd, tolerating short writes. + */ +static void send_fuzz_payload(int fd) +{ + const uint8_t *testcase, *data; + struct fuzz_layout fl; + uint32_t total_len; + size_t off = 0; + size_t len; + + if (!afl_shmem) + return; + + memcpy(&total_len, afl_shmem, sizeof(total_len)); + testcase = afl_shmem + sizeof(uint32_t); + fl = fuzz_parse_layout(testcase, total_len); + + if (!fl.testbuf_len) + return; + + data = testcase + fl.testbuf_off; + len = (size_t)fl.testbuf_len; + + while (off < len) { + ssize_t n = send(fd, data + off, len - off, MSG_NOSIGNAL); + + if (n > 0) { + off += (size_t)n; + continue; + } + + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + struct pollfd pfd = { .fd = fd, .events = POLLOUT }; + + if (poll(&pfd, 1, 50) <= 0) + return; + continue; + } + + if (n < 0 && errno == EINTR) + continue; + + return; + } +} + +/** + * handle_accept() - Accept connections and send test payload + * @lfd: Listening socket file descriptor + * + * For each accepted connection, sends the current region (c) data + * from AFL++'s shared memory and registers the connection for + * further I/O events. + */ +static void handle_accept(int lfd) +{ + int fd; + + while ((fd = accept4(lfd, NULL, NULL, + SOCK_NONBLOCK | SOCK_CLOEXEC)) >= 0) { + send_fuzz_payload(fd); + epoll_add(fd, TYPE_CONNECTION, + EPOLLIN | EPOLLRDHUP | EPOLLHUP | EPOLLERR); + } +} + +/** + * handle_data() - Read data from passt and reply with fuzz payload + * @fd: Connected TCP socket + */ +static void handle_data(int fd) +{ + uint8_t buf[4096]; + ssize_t n; + + n = read(fd, buf, sizeof(buf)); + if (n <= 0) { + epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); + close(fd); + return; + } + + send_fuzz_payload(fd); +} + +/** + * main() - Server entry point + * + * Attaches to AFL++ shared memory via env var, + * binds all ports, then enters the epoll loop. Dies automatically + * when the parent (passt forkserver) exits. + * + * Return: 0 on success, 1 on initialization failure + */ +int main(void) +{ + struct epoll_event events[MAX_EVENTS]; + int nfds, i; + + signal(SIGPIPE, SIG_IGN); + prctl(PR_SET_PDEATHSIG, SIGTERM); + + if (map_afl_shmem() < 0) { + (void)fprintf(stderr, + "fuzz-server: __AFL_SHM_FUZZ_ID not set, " + "running without AFL++ shared memory\n"); + } else { + (void)fprintf(stderr, + "fuzz-server: attached to AFL++ shared memory\n"); + } + + if (server_init() < 0) { + perror("fuzz-server: server_init"); + return 1; + } + + (void)fprintf(stderr, + "fuzz-server: listening on ports 1-%d, " + "%d-%d reserved for passt, IPv4+IPv6\n", + FUZZ_LISTEN_MAX, FUZZ_LISTEN_MAX + 1, FUZZ_MAX_PORT); + + while (1) { + nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); + if (nfds < 0) { + if (errno == EINTR) + continue; + perror("fuzz-server: epoll_wait"); + return 1; + } + + for (i = 0; i < nfds; i++) { + uint32_t type = events[i].data.u64 >> 32; + int fd = (int)(events[i].data.u64 & 0xFFFFFFFF); + + if (type == TYPE_LISTENER) { + handle_accept(fd); + } else if (type == TYPE_CONNECTION) { + if (events[i].events & + (EPOLLHUP | EPOLLERR | EPOLLRDHUP)) { + epoll_ctl(epfd, EPOLL_CTL_DEL, + fd, NULL); + close(fd); + } else if (events[i].events & EPOLLIN) { + handle_data(fd); + } + } + } + } + + return 0; +} -- 2.55.0