// SPDX-License-Identifier: GPL-2.0-or-later /* fuzz-server.c - Test server for bidirectional fuzz testing of passt * * Connects to passt's UNIX socket as a client. * Reads outbound frames, generates protocol responses, XORs them * with AFL++ shared memory data, and writes them back. * * Build: make fuzz-server * Run: ./fuzz-server (after passt.fuzz is listening) * * Copyright Red Hat * Author: Anshu Kumari */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define FUZZ_SOCK_PATH "/tmp/passt_fuzz_1.socket" #define FUZZ_TURN_PATH "/dev/shm/passt_fuzz_turn" #define FUZZ_XOR_OFF (12 + 64 * 1024) #define MAX_FRAME 1600 #define TCP_LISTEN_PORT 9999 #define TCP_MAX_EVENTS 16 /** * struct fuzz_turn - Turn-based synchronization flag (mmap'd shared memory) * @turn: 0 = passt's turn, 1 = server's turn */ struct fuzz_turn { uint32_t turn; }; /** * swap_eth_ip() - Swap Ethernet MACs and IPv4 addresses * @pkt: Pointer to start of Ethernet frame */ static void swap_eth_ip(uint8_t *pkt) { struct ethhdr *eth = (void *)pkt; struct iphdr *iph = (void *)(eth + 1); uint8_t tmp_mac[ETH_ALEN]; uint8_t tmp_ip[4]; memcpy(tmp_mac, eth->h_source, ETH_ALEN); memcpy(eth->h_source, eth->h_dest, ETH_ALEN); memcpy(eth->h_dest, tmp_mac, ETH_ALEN); memcpy(tmp_ip, &iph->saddr, 4); memcpy(&iph->saddr, &iph->daddr, 4); memcpy(&iph->daddr, tmp_ip, 4); } /** * respond_arp() - Generate ARP reply for an ARP request * @in: Incoming frame * @in_len: Incoming frame length * @out: Output buffer for response * * Swaps sender hardware and protocol addresses to form a * valid ARP reply. Only responds to ARP requests (op=1). * * Return: response length, or 0 if not an ARP request */ static int respond_arp(const uint8_t *in, uint32_t in_len, uint8_t *out) { /* Min ARP Frame size: eth(14) + ARP Header(8) + Payload(20) */ if (in_len < 42) return 0; const struct ethhdr *eth = (const void *)in; const struct arphdr *arp = (const void *)(eth + 1); if (eth->h_proto != htons(ETH_P_ARP) || arp->ar_op != htons(ARPOP_REQUEST)) return 0; memcpy(out, in, in_len); struct ethhdr *out_eth = (void *)out; struct arphdr *out_arp = (void *)(out_eth + 1); /* set MAC addresses for output buffer */ memcpy(out_eth->h_dest, eth->h_source, ETH_ALEN); memcpy(out_eth->h_source, eth->h_dest, ETH_ALEN); /* set ARP operation to REPLY */ out_arp->ar_op = htons(ARPOP_REPLY); /* Swap ARP Sender and Target fields */ uint8_t *sender = (uint8_t *)(out_arp + 1); uint8_t *target = sender + 10; uint8_t tmp[10]; memcpy(tmp, sender, 10); memcpy(sender, target, 10); memcpy(target, tmp, 10); return in_len; } /** * respond_tcp_syn() - Generate SYN-ACK for a TCP SYN * @in: Incoming frame * @in_len: Incoming frame length * @out: Output buffer for response * * Swaps MAC/IP/port addresses and crafts a stateless SYN-ACK with * a fixed ISN of 1000. Only responds to pure SYN (no ACK set). * * Return: response length, or 0 if not a TCP SYN */ static int respond_tcp_syn(const uint8_t *in, uint32_t in_len, uint8_t *out) { const struct ethhdr *eth = (const void *)in; const struct iphdr *iph; const struct tcphdr *th; struct tcphdr *out_th; uint32_t ihl, hdr_len; /* Combined minimum length check for Eth + IP + TCP */ if (in_len < sizeof(*eth) + sizeof(*iph) + sizeof(*th)) return 0; if (eth->h_proto != htons(ETH_P_IP)) return 0; iph = (const void *)(eth + 1); ihl = iph->ihl * 4; if (iph->protocol != IPPROTO_TCP || in_len < sizeof(*eth) + ihl + sizeof(*th)) return 0; th = (const void *)((const uint8_t *)iph + ihl); /* Only respond to pure SYN packets (no ACK set) */ if (!th->syn || th->ack) return 0; hdr_len = sizeof(*eth) + ihl + sizeof(*th); memcpy(out, in, hdr_len); swap_eth_ip(out); /* Craft SYN-ACK response header */ out_th = (struct tcphdr *)(out + sizeof(*eth) + ihl); out_th->source = th->dest; out_th->dest = th->source; out_th->seq = htonl(1000); out_th->ack_seq = htonl(ntohl(th->seq) + 1); out_th->syn = 1; out_th->ack = 1; out_th->doff = 5; out_th->check = 0; return hdr_len; } /** * generate_response() - Try all protocol responders on a frame * @in: Incoming frame * @in_len: Incoming frame length * @out: Output buffer for response * * Tries ARP then TCP SYN responders in order. Returns the first * successful response. * * Return: response length, or 0 if no responder matched */ static int generate_response(const uint8_t *in, uint32_t in_len, uint8_t *out) { int len; if (in_len < sizeof(struct ethhdr)) return 0; if ((len = respond_arp(in, in_len, out)) > 0) return len; if ((len = respond_tcp_syn(in, in_len, out)) > 0) return len; return 0; } /** * map_afl_shm() - Map AFL++ shared memory for XOR mutation * * Reads __AFL_SHM_FUZZ_ID from the AFL++ environment * variable and attaches the corresponding shared memory * segment read-only. * * Return: pointer to mapped SHM, or NULL if not available */ static uint8_t *map_afl_shm(void) { const char *id_str = getenv("__AFL_SHM_FUZZ_ID"); int shm_id; uint8_t *map; if (!id_str) return NULL; shm_id = atoi(id_str); map = shmat(shm_id, NULL, SHM_RDONLY); if (map == (void *)-1) return NULL; return map; } /** * apply_xor_mask() - XOR buffer with AFL++ shared memory data * @buf: Buffer to mutate in place * @len: Buffer length * @afl_buf: AFL++ shared memory * * XORs each byte of @buf with the corresponding byte from the * AFL++ buffer starting at offset FUZZ_XOR_OFF (65548). This * lets AFL++ mutate server responses. */ static void apply_xor_mask(uint8_t *buf, int len, const uint8_t *afl_buf) { int i; if (!afl_buf) return; for (i = 0; i < len; i++) buf[i] ^= afl_buf[FUZZ_XOR_OFF + i]; } /* ---- TCP Host Listener (Loopback) ---- */ static int tcp_listen_fd = -1; static int tcp_epfd = -1; /** * tcp_listener_init() - Set up TCP listener on loopback port 9999 * * Creates an epoll instance and a non-blocking TCP listener on * 127.0.0.1:9999 with SO_REUSEADDR. * * Return: 0 on success, -1 on failure */ static int tcp_listener_init(void) { struct sockaddr_in sa = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(TCP_LISTEN_PORT), }; struct epoll_event ev; int opt = 1; tcp_epfd = epoll_create1(EPOLL_CLOEXEC); tcp_listen_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); if (tcp_epfd < 0 || tcp_listen_fd < 0) return -1; setsockopt(tcp_listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); if (bind(tcp_listen_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0 || listen(tcp_listen_fd, 128) < 0) return -1; ev.events = EPOLLIN; ev.data.fd = tcp_listen_fd; epoll_ctl(tcp_epfd, EPOLL_CTL_ADD, tcp_listen_fd, &ev); return 0; } /** * tcp_handle_accept() - Drain all pending TCP connections * * Sets SO_LINGER with l_linger=0 on every accepted connection, * which does the immediate RST on connection close instead of * waiting for connection to close on it's own. */ static void tcp_handle_accept(void) { struct linger lg = { .l_onoff = 1, .l_linger = 0 }; struct epoll_event ev; int fd; /* Drain ALL pending connections in the backlog */ while ((fd = accept4(tcp_listen_fd, NULL, NULL, SOCK_NONBLOCK)) >= 0) { setsockopt(fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)); ev.events = EPOLLIN | EPOLLRDHUP | EPOLLHUP | EPOLLERR; ev.data.fd = fd; epoll_ctl(tcp_epfd, EPOLL_CTL_ADD, fd, &ev); } } /** * tcp_handle_data() - Read TCP data, XOR-mutate, echo back * @fd: Connected TCP socket * @afl_buf: AFL++ shared memory for XOR mask */ static void tcp_handle_data(int fd, const uint8_t *afl_buf) { uint8_t buf[MAX_FRAME]; ssize_t n, written, ret; n = read(fd, buf, sizeof(buf)); if (n <= 0) { epoll_ctl(tcp_epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); return; } apply_xor_mask(buf, (uint32_t)n, afl_buf); /* Ensure all n bytes are written, handling short writes */ written = 0; while (written < n) { ret = write(fd, buf + written, (size_t)(n - written)); if (ret <= 0) { if (ret < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) continue; epoll_ctl(tcp_epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); return; } written += ret; } } /** * tcp_process_events() - Non-blocking poll for TCP events * @afl_buf: AFL++ shared memory */ static void tcp_process_events(const uint8_t *afl_buf) { struct epoll_event events[TCP_MAX_EVENTS]; int nfds, i; nfds = epoll_wait(tcp_epfd, events, TCP_MAX_EVENTS, 0); for (i = 0; i < nfds; i++) { int fd = events[i].data.fd; if (fd == tcp_listen_fd) { tcp_handle_accept(); } else if (events[i].events & (EPOLLIN | EPOLLRDHUP)) { tcp_handle_data(fd, afl_buf); } else if (events[i].events & (EPOLLHUP | EPOLLERR)) { epoll_ctl(tcp_epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); } } } /** * create_turn_flag() - Create and mmap the turn synchronization flag * * Creates FUZZ_TURN_PATH in /dev/shm and maps it * as shared memory. Both passt and fuzz-server map this file to * coordinate turn-based frame exchange via atomic load/store. * * Return: pointer to mapped turn struct, or NULL on failure */ static struct fuzz_turn *create_turn_flag(void) { struct fuzz_turn *t; int fd; fd = open(FUZZ_TURN_PATH, O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd < 0) return NULL; if (ftruncate(fd, sizeof(struct fuzz_turn)) < 0) { close(fd); return NULL; } t = mmap(NULL, sizeof(*t), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); if (t == MAP_FAILED) return NULL; t->turn = 0; return t; } /** * main() - Server entry point * * Outer loop reconnects to passt's UNIX socket on each AFL++ fork * server restart. Inner loop handles turn-based TAP frame exchange * and TCP events concurrently. * * Return: 0 on success, 1 on initialization failure */ int main(void) { struct sockaddr_un addr = { .sun_family = AF_UNIX }; uint8_t frame[MAX_FRAME], response[MAX_FRAME]; struct fuzz_turn *turn; uint8_t *afl_buf; strncpy(addr.sun_path, FUZZ_SOCK_PATH, sizeof(addr.sun_path) - 1); turn = create_turn_flag(); if (!turn || tcp_listener_init() < 0) { (void)fprintf(stderr, "fuzz-server: tcp initialization failed\n"); return 1; } afl_buf = map_afl_shm(); (void)fprintf(stderr, "fuzz-server: ready, TCP listening on 127.0.0.1:%d\n", TCP_LISTEN_PORT); /* * Reconnect UNIX socket on passt restart — AFL++'s fork server * restarts passt on each iteration. */ while (1) { ssize_t n; int sock; sock = socket(AF_UNIX, SOCK_SEQPACKET, 0); if (sock < 0) return 1; while (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) usleep(10000); while (1) { tcp_process_events(afl_buf); if (__atomic_load_n(&turn->turn, __ATOMIC_ACQUIRE) == 1) { n = recv(sock, frame, MAX_FRAME, MSG_DONTWAIT); if (n > 0) { int resp_len; resp_len = generate_response( frame, n, response); if (resp_len > 0) { apply_xor_mask(response, resp_len, afl_buf); send(sock, response, resp_len, MSG_NOSIGNAL); } } else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) { __atomic_store_n(&turn->turn, 0, __ATOMIC_RELEASE); break; } __atomic_store_n(&turn->turn, 0, __ATOMIC_RELEASE); } } close(sock); } return 0; }