diff --git a/fuzzing/usbip/build_fuzzers.sh b/fuzzing/usbip/build_fuzzers.sh
@@ -4,7 +4,7 @@
# Assumes setup.sh has already run and usbip-src/ exists.
# Can be run standalone after the initial setup to iterate on harnesses.
#
-# Outputs: fuzz_protocol, fuzz_devlist, fuzz_import, fuzz_urb
+# Outputs: fuzz_protocol, fuzz_devlist, fuzz_urb
# + *.cmplog variants for AFL++ CmpLog mode
#
# Usage:
@@ -30,9 +30,9 @@ die() { echo "[!] $*" >&2; exit 1; }
if [[ "${1:-}" == "--clean" ]]; then
log "Cleaning build artifacts..."
- rm -f fuzz_protocol fuzz_devlist fuzz_import fuzz_urb
- rm -f fuzz_protocol.cmplog fuzz_devlist.cmplog fuzz_import.cmplog fuzz_urb.cmplog
- rm -f usbip_network.o usbip_common.o mock_syscalls.o
+ rm -f fuzz_protocol fuzz_devlist fuzz_urb fuzz_stub_client
+ rm -f fuzz_protocol.cmplog fuzz_devlist.cmplog fuzz_urb.cmplog
+ rm -f usbip_network.o usbip_common.o mock_syscalls.o names.o
ok "Clean done."
fi
@@ -49,13 +49,14 @@ command -v "${CC}" > /dev/null 2>&1 \
# ── common compiler flags ─────────────────────────────────────────────────────
CFLAGS="-fsanitize=address,undefined -g -O1 -fno-omit-frame-pointer"
CFLAGS+=" -I fuzz-include"
+CFLAGS+=" -I ${SCRIPT_DIR}/usbip-src" # for config.h (HAVE_CONFIG_H / USBIP_VERSION)
CFLAGS+=" -I ${USBIP_SRC}"
CFLAGS+=" -I ${USBIP_LIB}"
CFLAGS+=" -Wno-implicit-function-declaration"
CFLAGS+=" -Wno-int-conversion"
CFLAGS+=" -Wno-pointer-to-int-cast"
CFLAGS+=" -Wno-incompatible-pointer-types"
-CFLAGS+=" -DFUZZ_BUILD"
+CFLAGS+=" -DFUZZ_BUILD -DHAVE_CONFIG_H"
# glib-2.0 is needed by usbip_common.c
GLIB_CFLAGS="$(pkg-config --cflags glib-2.0 2>/dev/null || echo '')"
@@ -63,6 +64,7 @@ GLIB_LIBS="$(pkg-config --libs glib-2.0 2>/dev/null || echo '-lglib-2.0')"
CFLAGS+=" ${GLIB_CFLAGS}"
LDFLAGS="-Wl,--wrap=recv -Wl,--wrap=send -Wl,--wrap=write"
+LDFLAGS+=" -ludev"
LDFLAGS+=" ${GLIB_LIBS}"
# ── Step 1: compile shared usbip objects ─────────────────────────────────────
@@ -76,9 +78,12 @@ ${CC} ${CFLAGS} -c "${USBIP_LIB}/usbip_common.c" -o usbip_common.o 2>/dev/null |
log "Compiling mock_syscalls.o..."
${CC} ${CFLAGS} -c mock_syscalls.c -o mock_syscalls.o
-COMMON_OBJS="usbip_network.o usbip_common.o mock_syscalls.o"
+log "Compiling names.o..."
+${CC} ${CFLAGS} -c "${USBIP_LIB}/names.c" -o names.o
-# ── Step 2: build each harness (standard + CmpLog) ────────────────────────────
+COMMON_OBJS="usbip_network.o usbip_common.o mock_syscalls.o names.o"
+
+# ── Step 2: build each in-process harness (standard + CmpLog) ────────────────
build_harness() {
local name="$1"
@@ -95,14 +100,51 @@ build_harness() {
build_harness fuzz_protocol fuzz_protocol.c
build_harness fuzz_devlist fuzz_devlist.c
-build_harness fuzz_import fuzz_import.c
build_harness fuzz_urb fuzz_urb.c
+# fuzz_stub_client: malicious USB-IP client targeting usbipd stub_rx.c
+# No ASAN / usbip library linkage — connects over TCP to the QEMU VM.
+if [[ ! -f "fuzz_stub_client" ]] || [[ "fuzz_stub_client.c" -nt "fuzz_stub_client" ]]; then
+ log "Building fuzz_stub_client (malicious client targeting stub_rx.c)..."
+ ${CC} -O2 -o fuzz_stub_client fuzz_stub_client.c
+ ok " fuzz_stub_client"
+fi
+
+# ── Step 3: build QEMU-mode network tools ─────────────────────────────────────
+# net_send: blind TCP relay (original QEMU mode — server side)
+if [[ ! -f "net_send" ]] || [[ "net_send.c" -nt "net_send" ]]; then
+ log "Building net_send (blind TCP relay for QEMU server-side mode)..."
+ ${CC} -O2 -o net_send net_send.c
+ ok " net_send"
+fi
+
+# fuzz_vhci_server: two-sided fuzzer targeting vhci-hcd in QEMU client mode
+if [[ ! -f "fuzz_vhci_server" ]] || [[ "fuzz_vhci_server.c" -nt "fuzz_vhci_server" ]]; then
+ log "Building fuzz_vhci_server (two-sided server targeting vhci-hcd)..."
+ ${CC} -O2 -o fuzz_vhci_server fuzz_vhci_server.c
+ ok " fuzz_vhci_server"
+fi
+
+# ── Step 4: build KCOV shim (for kernel coverage feedback) ───────────────────
+# kcov_shim runs INSIDE the QEMU VM. Cross-compile or build on the guest.
+# Here we build a host-native version for testing; deploy to VM initramfs manually.
+if [[ ! -f "kcov_shim" ]] || [[ "kcov_shim.c" -nt "kcov_shim" ]]; then
+ log "Building kcov_shim (KCOV coverage shim — deploy inside QEMU VM)..."
+ gcc -O2 -static -o kcov_shim kcov_shim.c 2>/dev/null || \
+ gcc -O2 -o kcov_shim kcov_shim.c && \
+ ok " kcov_shim (dynamic — static build failed, ensure libc is in VM)" || \
+ echo "[!] kcov_shim build failed (needs kernel KCOV headers) — skip for now"
+fi
+
# ── Done ─────────────────────────────────────────────────────────────────────
echo ""
ok "All harnesses built:"
-ls -lh fuzz_protocol fuzz_devlist fuzz_import fuzz_urb 2>/dev/null
+ls -lh fuzz_protocol fuzz_devlist fuzz_urb fuzz_vhci_server fuzz_stub_client 2>/dev/null
echo ""
echo " Quick smoke test:"
echo " ./fuzz_protocol corpus/op_common_req_devlist"
echo " ./fuzz_urb corpus/urb_submit_64b"
+echo ""
+echo " In-process (no VM): bash run-fuzzers.sh"
+echo " QEMU vhci (two-sided): afl-fuzz -S vhci -i corpus/vhci -o output -t 5000 -- ./fuzz_vhci_server @@ 0.0.0.0 13241"
+echo " QEMU stub (client): afl-fuzz -S stub -i corpus/stub -o output -t 5000 -- ./fuzz_stub_client @@ <vm-ip> 3240"
diff --git a/fuzzing/usbip/cleanup.sh b/fuzzing/usbip/cleanup.sh
@@ -61,11 +61,14 @@ pkill -u "$(id -un)" qemu-system-x86_64 2>/dev/null || true
# ── kill any background AFL++ processes ───────────────────────────────────────
pkill -u "$(id -un)" -f "afl-fuzz.*usbip" 2>/dev/null || true
-pkill -u "$(id -un)" -f "fuzz_protocol" 2>/dev/null || true
+pkill -u "$(id -un)" -f "fuzz_protocol" 2>/dev/null || true
pkill -u "$(id -un)" -f "fuzz_devlist" 2>/dev/null || true
pkill -u "$(id -un)" -f "fuzz_import" 2>/dev/null || true
pkill -u "$(id -un)" -f "fuzz_urb" 2>/dev/null || true
-pkill -u "$(id -un)" -f "net_send" 2>/dev/null || true
+pkill -u "$(id -un)" -f "fuzz_vhci_server" 2>/dev/null || true
+pkill -u "$(id -un)" -f "fuzz_stub_client" 2>/dev/null || true
+pkill -u "$(id -un)" -f "net_send" 2>/dev/null || true
+pkill -u "$(id -un)" -f "kcov_shim" 2>/dev/null || true
if [[ "${CRASHES_ONLY}" -eq 1 ]]; then
ok "Process cleanup done. (--crashes-only: no files removed)"
@@ -79,7 +82,8 @@ if [[ "${KEEP_BUILDS}" -eq 0 ]]; then
rm -f fuzz_devlist fuzz_devlist.cmplog
rm -f fuzz_import fuzz_import.cmplog
rm -f fuzz_urb fuzz_urb.cmplog
- rm -f usbip_network.o usbip_common.o mock_syscalls.o
+ rm -f fuzz_vhci_server fuzz_stub_client kcov_shim
+ rm -f usbip_network.o usbip_common.o mock_syscalls.o names.o
rm -f net_send usbipd-plain
log "Removing kernel build artifacts..."
diff --git a/fuzzing/usbip/dictionaries/usbip.dict b/fuzzing/usbip/dictionaries/usbip.dict
@@ -62,3 +62,31 @@ setup_all_ff="\xff\xff\xff\xff\xff\xff\xff\xff"
# Sysfs path prefix (used in usbip_usb_device.path)
sysfs_path="/sys/devices/pci0000:00/"
sysfs_usb_prefix="/sys/bus/usb/devices/"
+
+# USB setup packet bRequest values (single byte — control transfer discriminators)
+usb_req_get_status="\x00"
+usb_req_clear_feature="\x01"
+usb_req_set_feature="\x03"
+usb_req_set_address="\x05"
+usb_req_get_descriptor="\x06"
+usb_req_set_descriptor="\x07"
+usb_req_get_config="\x08"
+usb_req_set_config="\x09"
+usb_req_get_interface="\x0a"
+usb_req_set_interface="\x0b"
+
+# USB descriptor types (wValue high byte in GET_DESCRIPTOR)
+usb_dt_device="\x01"
+usb_dt_config="\x02"
+usb_dt_string="\x03"
+usb_dt_interface="\x04"
+usb_dt_endpoint="\x05"
+usb_dt_qualifier="\x06"
+
+# ISO overflow trigger values: number_of_packets that cause n*16 to wrap (4-byte BE)
+# 0x08000001: n*16 = 0x80000010 → overflows int32 → negative size → kmalloc underalloc
+# 0x10000001: n*16 = 0x100000010 → truncated uint32 = 0x10 → 16-byte alloc, n*16 bytes used
+# 0x40000000: n*16 = 0x400000000 → wraps u32 to 0 → kmalloc(0)
+iso_np_overflow_08="\x08\x00\x00\x01"
+iso_np_overflow_10="\x10\x00\x00\x01"
+iso_np_overflow_40="\x40\x00\x00\x00"
diff --git a/fuzzing/usbip/fuzz-include/usbip_fuzz.h b/fuzzing/usbip/fuzz-include/usbip_fuzz.h
@@ -5,7 +5,10 @@
* Provides:
* - global fuzz buffer (populated by harness main, consumed by __wrap_recv)
* - logging macro stubs so usbip source compiles without a real logging lib
- * - protocol constants / struct aliases for use in harnesses
+ * - usbip_header wire-format structs for fuzz_urb (not in userspace headers)
+ *
+ * Protocol constants (USBIP_VERSION, OP_*, SYSFS_BUS_ID_SIZE, etc.) are NOT
+ * defined here — they come from usbip_common.h / usbip_network.h / config.h.
*/
#include <stddef.h>
@@ -57,30 +60,59 @@ static inline void fuzz_reset(const uint8_t *data, size_t size) {
/*
* usbip_debug_flag — extern referenced by logging macros in usbip_common.h.
- * Set to 0 so all debug paths are no-ops.
*/
extern unsigned long usbip_debug_flag;
-/* ── USB/IP protocol constants ────────────────────────────────────── */
-#define USBIP_VERSION 0x0111
-
-#define OP_REQUEST (0x80 << 8)
-#define OP_REPLY (0x00 << 8)
-
-#define OP_DEVLIST 0x0005
-#define OP_IMPORT 0x0003
-
-#define OP_REQ_DEVLIST (OP_REQUEST | OP_DEVLIST) /* 0x8005 */
-#define OP_REP_DEVLIST (OP_REPLY | OP_DEVLIST) /* 0x0005 */
-#define OP_REQ_IMPORT (OP_REQUEST | OP_IMPORT) /* 0x8003 */
-#define OP_REP_IMPORT (OP_REPLY | OP_IMPORT) /* 0x0003 */
-
+/*
+ * ── USB/IP URB PDU wire-format constants and structs ──────────────────
+ * struct usbip_header lives only in the kernel's internal headers, not in
+ * any userspace UAPI. Define the wire layout here for fuzz_urb.c so it
+ * can call usbip_net_recv() and inspect the PDU fields.
+ *
+ * Matches drivers/usb/usbip/usbip_common.h — do not change field order.
+ */
#define USBIP_CMD_SUBMIT 0x00000001
#define USBIP_CMD_UNLINK 0x00000002
#define USBIP_RET_SUBMIT 0x00000003
#define USBIP_RET_UNLINK 0x00000004
-#define SYSFS_BUS_ID_SIZE 32
+/* PDU header is 48 bytes on the wire (20-byte basic + 28-byte union) */
+#define USBIP_PDU_HDR_SIZE 48
+
+struct usbip_header_basic {
+ uint32_t command;
+ uint32_t seqnum;
+ uint32_t devid;
+ uint32_t direction;
+ uint32_t ep;
+};
+
+struct usbip_header_cmd_submit {
+ uint32_t transfer_flags;
+ int32_t transfer_buffer_length;
+ int32_t start_frame;
+ int32_t number_of_packets;
+ int32_t interval;
+ uint8_t setup[8];
+};
+
+struct usbip_header_ret_submit {
+ int32_t status;
+ int32_t actual_length;
+ int32_t start_frame;
+ int32_t number_of_packets;
+ int32_t error_count;
+};
+
+struct usbip_header_cmd_unlink { uint32_t seqnum; };
+struct usbip_header_ret_unlink { int32_t status; };
-/* op_common is 8 bytes on the wire */
-#define OP_COMMON_SIZE 8
+struct usbip_header {
+ struct usbip_header_basic base;
+ union {
+ struct usbip_header_cmd_submit cmd_submit;
+ struct usbip_header_ret_submit ret_submit;
+ struct usbip_header_cmd_unlink cmd_unlink;
+ struct usbip_header_ret_unlink ret_unlink;
+ } u;
+};
diff --git a/fuzzing/usbip/fuzz_devlist.c b/fuzzing/usbip/fuzz_devlist.c
@@ -1,27 +1,15 @@
/*
* fuzz_devlist.c — USB/IP device-list reply parsing harness.
*
- * Directly exercises usbip_net_recv_op_devlist_reply() which parses:
- * [ndev:4 BE] followed by ndev × usbip_usb_device + usbip_usb_interface[]
+ * Exercises the devlist reply path:
+ * [ndev:4 BE] + ndev × (usbip_usb_device + bNumInterfaces × usbip_usb_interface)
*
- * Interesting bug classes:
- * - Large ndev values causing excessive allocations / OOM
- * - Malformed device structs (wrong size fields)
- * - Integer overflow in ndev × sizeof(usbip_usb_device)
- *
- * Input format:
- * [ndev:4 BE][device_0 struct bytes][...][device_N struct bytes]
- * Each device is followed by bNumInterfaces × usbip_usb_interface structs.
- *
- * Compile (via build_fuzzers.sh):
- * afl-clang-fast -fsanitize=address,undefined -g -O1 \
- * -I fuzz-include -I usbip-src/src -I usbip-src/libsrc \
- * fuzz_devlist.c usbip_network.o usbip_common.o mock_syscalls.o \
- * -Wl,--wrap=recv -Wl,--wrap=send -Wl,--wrap=write \
- * -o fuzz_devlist
+ * Bug classes: large ndev overflow, malformed device structs, integer overflow
+ * in ndev × sizeof(usbip_usb_device), bNumInterfaces out of bounds.
*/
#include "fuzz-include/usbip_fuzz.h"
+#include "usbip-src/libsrc/usbip_common.h"
#include "usbip-src/src/usbip_network.h"
#include <stdint.h>
@@ -29,27 +17,19 @@
#include <string.h>
#include <arpa/inet.h>
-#define FAKE_FD 42
-
-/*
- * Cap ndev at 64 so we don't spend all our time doing legal-but-slow
- * large allocations. AFL++ will explore values up to this cap quickly.
- */
+#define FAKE_FD 42
#define MAX_NDEV_FUZZ 64
static int fuzz_one(const uint8_t *data, size_t size)
{
- if (size < 4)
+ if (size < sizeof(struct op_devlist_reply))
return 0;
- /*
- * Clamp the ndev field in the copy we feed to the parser so we don't
- * OOM the fuzzer process with a 2^32-device list.
- */
uint8_t buf[FUZZ_BUF_MAX];
size_t n = size < FUZZ_BUF_MAX ? size : FUZZ_BUF_MAX;
memcpy(buf, data, n);
+ /* Clamp ndev so we don't OOM the fuzzer on a 2^32-device list */
uint32_t ndev_be;
memcpy(&ndev_be, buf, 4);
uint32_t ndev = ntohl(ndev_be);
@@ -61,11 +41,31 @@ static int fuzz_one(const uint8_t *data, size_t size)
fuzz_reset(buf, n);
- struct usbip_exported_devices edevs;
- memset(&edevs, 0, sizeof(edevs));
-
- usbip_net_recv_op_devlist_reply(FAKE_FD, &edevs);
- usbip_exported_devices_free(&edevs);
+ struct op_devlist_reply reply;
+ memset(&reply, 0, sizeof(reply));
+ if (usbip_net_recv(FAKE_FD, &reply, sizeof(reply)) < 0)
+ return 0;
+ PACK_OP_DEVLIST_REPLY(0, &reply);
+
+ uint32_t num = reply.ndev;
+ if (num > MAX_NDEV_FUZZ) num = MAX_NDEV_FUZZ;
+
+ for (uint32_t i = 0; i < num; i++) {
+ struct usbip_usb_device udev;
+ memset(&udev, 0, sizeof(udev));
+ if (usbip_net_recv(FAKE_FD, &udev, sizeof(udev)) < 0)
+ return 0;
+ usbip_net_pack_usb_device(0, &udev);
+
+ uint8_t nif = udev.bNumInterfaces;
+ for (uint8_t j = 0; j < nif; j++) {
+ struct usbip_usb_interface uinf;
+ memset(&uinf, 0, sizeof(uinf));
+ if (usbip_net_recv(FAKE_FD, &uinf, sizeof(uinf)) < 0)
+ return 0;
+ usbip_net_pack_usb_interface(0, &uinf);
+ }
+ }
return 0;
}
diff --git a/fuzzing/usbip/fuzz_import.c b/fuzzing/usbip/fuzz_import.c
@@ -1,67 +1,52 @@
/*
* fuzz_import.c — USB/IP device import reply parsing harness.
*
- * Exercises usbip_net_recv_op_import_reply() which parses:
- * [status:4 BE][usbip_usb_device struct (312 bytes)]
+ * Exercises op_import_reply parsing: struct op_import_reply contains a
+ * usbip_usb_device (busid, speed, descriptor fields).
*
- * Also exercises the 32-byte busid field inside op_import_request via a
- * direct server-side path: feed raw bytes that include the busid, then
- * call usbip_net_send_op_import_request() so the code validates/copies
- * the busid field.
+ * Also exercises the request-builder path: sends op_import_request with
+ * a fuzzer-supplied busid, exercising the busid field copy and send path.
*
- * Interesting bug classes:
- * - busid not NUL-terminated (missing null byte in 32-byte field)
- * - busid containing path-traversal chars (../../../etc/passwd style)
- * - busid containing format-string tokens (%s %n etc.)
- * - Malformed usbip_usb_device struct fields
+ * Bug classes: busid not NUL-terminated, busid with path-traversal chars,
+ * busid with format-string tokens, malformed usbip_usb_device struct fields.
*
* Input format:
- * [32-byte busid, possibly without NUL][status:4 BE][usbip_usb_device bytes]
- *
- * Compile (via build_fuzzers.sh):
- * afl-clang-fast -fsanitize=address,undefined -g -O1 \
- * -I fuzz-include -I usbip-src/src -I usbip-src/libsrc \
- * fuzz_import.c usbip_network.o usbip_common.o mock_syscalls.o \
- * -Wl,--wrap=recv -Wl,--wrap=send -Wl,--wrap=write \
- * -o fuzz_import
+ * [32-byte busid, possibly without NUL][op_import_reply bytes...]
*/
#include "fuzz-include/usbip_fuzz.h"
+#include "usbip-src/libsrc/usbip_common.h"
#include "usbip-src/src/usbip_network.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
-#define FAKE_FD 42
-#define MIN_INPUT (SYSFS_BUS_ID_SIZE + 4) /* busid + status */
+#define FAKE_FD 42
+#define MIN_INPUT (SYSFS_BUS_ID_SIZE + sizeof(struct op_import_reply))
static int fuzz_one(const uint8_t *data, size_t size)
{
if (size < MIN_INPUT)
return 0;
- /*
- * Path A: parse the import-reply (server → client direction).
- * The first 4 bytes become the status field; the rest is usbip_usb_device.
- * Feed everything starting at byte SYSFS_BUS_ID_SIZE as the "reply body".
- */
+ /* Path A: parse import reply (server → client direction) */
fuzz_reset(data + SYSFS_BUS_ID_SIZE, size - SYSFS_BUS_ID_SIZE);
- struct usbip_usb_device idev;
- memset(&idev, 0, sizeof(idev));
- usbip_net_recv_op_import_reply(FAKE_FD, &idev);
+ struct op_import_reply reply;
+ memset(&reply, 0, sizeof(reply));
+ if (usbip_net_recv(FAKE_FD, &reply, sizeof(reply)) == 0)
+ PACK_OP_IMPORT_REPLY(0, &reply);
- /*
- * Path B: exercise the request builder with the busid from input bytes.
- * usbip_net_send_op_import_request copies + validates the busid string.
- */
- char busid[SYSFS_BUS_ID_SIZE];
- memcpy(busid, data, SYSFS_BUS_ID_SIZE);
- /* Don't force NUL-termination — that's the point of this test */
+ /* Path B: build and send import request with fuzz-supplied busid */
+ struct op_import_request request;
+ memset(&request, 0, sizeof(request));
+ memcpy(request.busid, data, SYSFS_BUS_ID_SIZE);
+ /* Intentionally don't force NUL-termination — that's the test */
+ PACK_OP_IMPORT_REQUEST(1, &request);
- fuzz_reset(data, size); /* reset buffer so send mock is happy */
- usbip_net_send_op_import_request(FAKE_FD, busid);
+ fuzz_reset(data, size);
+ usbip_net_send(FAKE_FD, &request, sizeof(request));
return 0;
}
diff --git a/fuzzing/usbip/fuzz_protocol.c b/fuzzing/usbip/fuzz_protocol.c
@@ -1,24 +1,15 @@
/*
- * fuzz_protocol.c — USB/IP protocol header routing harness.
+ * fuzz_protocol.c — USB/IP op_common header parsing harness.
*
- * Exercises usbip_net_recv_op_common() (the first function called on every
- * new connection) which parses the 8-byte op_common header and validates
- * the protocol version + operation code.
+ * Exercises usbip_net_recv_op_common() which reads the 8-byte op_common
+ * header and validates protocol version + operation code.
+ * On success, reads the reply body depending on the opcode.
*
- * Input format: raw bytes starting at the op_common header.
- * [version:2 BE][op_code:2 BE][status:4 BE][... payload ...]
- *
- * Compile (via build_fuzzers.sh):
- * afl-clang-fast -fsanitize=address,undefined -g -O1 \
- * -I fuzz-include -I usbip-src/src -I usbip-src/libsrc \
- * fuzz_protocol.c usbip_network.o usbip_common.o mock_syscalls.o \
- * -Wl,--wrap=recv -Wl,--wrap=send -Wl,--wrap=write \
- * -o fuzz_protocol
+ * Input format: [version:2 BE][code:2 BE][status:4 BE][... payload ...]
*/
#include "fuzz-include/usbip_fuzz.h"
-
-/* usbip headers come from the kernel source tree (usbip-src/) */
+#include "usbip-src/libsrc/usbip_common.h"
#include "usbip-src/src/usbip_network.h"
#include <stdint.h>
@@ -26,58 +17,48 @@
#include <string.h>
#include <arpa/inet.h>
-/* Fake fd — value ignored by our recv() mock */
#define FAKE_FD 42
static int fuzz_one(const uint8_t *data, size_t size)
{
- if (size < OP_COMMON_SIZE)
+ if (size < sizeof(struct op_common))
return 0;
fuzz_reset(data, size);
/*
- * op_common {version:u16 BE, code:u16 BE, status:u32 BE}
- * usbip_net_recv_op_common reads exactly 8 bytes via recv().
+ * Pass OP_UNSPEC so recv_op_common accepts any opcode.
+ * The function reads sizeof(struct op_common) bytes via our mock recv.
*/
- struct op_common op;
- memset(&op, 0, sizeof(op));
+ uint16_t code = OP_UNSPEC;
+ int status = 0;
- int ret = usbip_net_recv_op_common(FAKE_FD, &op);
- if (ret < 0)
+ if (usbip_net_recv_op_common(FAKE_FD, &code, &status) < 0)
return 0;
- uint16_t code = ntohs(op.code);
- uint32_t status = ntohl(op.status);
- (void)status;
-
- /*
- * Route to the appropriate reply-parser based on op code.
- * These functions consume additional bytes from g_fuzz_buf via recv().
- */
+ /* Consume the reply body to exercise more of the parsing surface */
switch (code) {
case OP_REP_DEVLIST: {
- struct usbip_exported_devices edevs;
- memset(&edevs, 0, sizeof(edevs));
- usbip_net_recv_op_devlist_reply(FAKE_FD, &edevs);
- usbip_exported_devices_free(&edevs);
+ struct op_devlist_reply dr;
+ memset(&dr, 0, sizeof(dr));
+ if (usbip_net_recv(FAKE_FD, &dr, sizeof(dr)) == 0)
+ PACK_OP_DEVLIST_REPLY(0, &dr);
break;
}
case OP_REP_IMPORT: {
- struct usbip_usb_device idev;
- memset(&idev, 0, sizeof(idev));
- usbip_net_recv_op_import_reply(FAKE_FD, &idev);
+ struct op_import_reply ir;
+ memset(&ir, 0, sizeof(ir));
+ if (usbip_net_recv(FAKE_FD, &ir, sizeof(ir)) == 0)
+ PACK_OP_IMPORT_REPLY(0, &ir);
break;
}
default:
- /* Unknown op code: no further parsing — tests version/code validation */
break;
}
return 0;
}
-/* ── AFL++ persistent mode ──────────────────────────────────────── */
#ifdef __AFL_FUZZ_TESTCASE_LEN
__AFL_FUZZ_INIT();
@@ -86,13 +67,12 @@ int main(void)
__AFL_INIT();
uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF;
while (__AFL_LOOP(10000)) {
- size_t len = __AFL_FUZZ_TESTCASE_LEN;
- fuzz_one(buf, len);
+ fuzz_one(buf, __AFL_FUZZ_TESTCASE_LEN);
}
return 0;
}
-#else /* libfuzzer fallback */
+#else
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
diff --git a/fuzzing/usbip/fuzz_stub_client.c b/fuzzing/usbip/fuzz_stub_client.c
@@ -0,0 +1,358 @@
+/*
+ * fuzz_stub_client.c — Malicious USB-IP client targeting usbipd stub_rx.c.
+ *
+ * WHAT THIS TESTS
+ * ────────────────
+ * The stub side (usbipd running in the QEMU VM) receives CMD_SUBMIT packets
+ * from a USB-IP client and calls stub_recv_cmd_submit(). That function has:
+ *
+ * 1. No upper bound on transfer_buffer_length before sgl_alloc() —
+ * a huge value maps to a giant scatter-gather list → kernel OOM / KASAN.
+ *
+ * 2. Integer overflow in number_of_packets for ISO endpoints:
+ * kmalloc(number_of_packets * sizeof(struct usbip_iso_packet_descriptor))
+ * Same class as the vhci-hcd recv_iso bug but on the stub side.
+ *
+ * ARCHITECTURE
+ * ─────────────
+ * VM : usbipd running, a real USB device (or dummy_hcd) bound and exported
+ * Host : fuzz_stub_client <testcase> <vm-ip> <port> (AFL++ target)
+ *
+ * 1. Client TCP-connects to usbipd on <vm-ip>:<port> (default 3240).
+ * 2. Sends OP_REQ_IMPORT to claim the exported device (busid "1-1").
+ * 3. Reads OP_REP_IMPORT (import accepted → now in tunnel mode).
+ * 4. Sends one or more CMD_SUBMIT packets whose fields are taken from the
+ * AFL++ test-case bytes. The 48-byte CMD_SUBMIT header union is the
+ * primary mutation target.
+ * 5. Optionally reads and discards RET_SUBMIT from the stub.
+ * 6. Exits; AFL++ records a crash if the VM panics and usbipd drops the
+ * connection (ECONNRESET → send_all returns -1 → main returns 1).
+ *
+ * AFL++ INVOCATION
+ * ─────────────────
+ * afl-fuzz -S stub -i corpus/stub -o output -t 5000 \
+ * -- ./fuzz_stub_client @@ <vm-ip> 3240
+ *
+ * COMPILE
+ * ────────
+ * afl-clang-fast -O2 -o fuzz_stub_client fuzz_stub_client.c
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <unistd.h>
+
+/* ── USB/IP protocol constants ─────────────────────────────────────────────── */
+#define USBIP_VERSION 0x0111
+
+#define OP_REQ_IMPORT 0x8003
+#define OP_REP_IMPORT 0x0003
+
+#define USBIP_CMD_SUBMIT 0x00000001
+#define USBIP_CMD_UNLINK 0x00000002
+#define USBIP_RET_SUBMIT 0x00000003
+
+#define SYSFS_BUS_ID_SIZE 32
+#define SYSFS_PATH_MAX 256
+
+/* Maximum out-payload we'll actually send after a CMD_SUBMIT header */
+#define MAX_OUT_PAYLOAD (1 << 16) /* 64 KB */
+
+/* ── wire-format structures ─────────────────────────────────────────────────── */
+
+struct op_common {
+ uint16_t version;
+ uint16_t code;
+ uint32_t status;
+} __attribute__((packed));
+
+struct op_req_import {
+ struct op_common hdr;
+ char busid[SYSFS_BUS_ID_SIZE];
+} __attribute__((packed));
+
+struct usbip_usb_device {
+ char path[SYSFS_PATH_MAX];
+ char busid[SYSFS_BUS_ID_SIZE];
+ uint32_t busnum;
+ uint32_t devnum;
+ uint32_t speed;
+ uint16_t idVendor;
+ uint16_t idProduct;
+ uint16_t bcdDevice;
+ uint8_t bDeviceClass;
+ uint8_t bDeviceSubClass;
+ uint8_t bDeviceProtocol;
+ uint8_t bConfigurationValue;
+ uint8_t bNumConfigurations;
+ uint8_t bNumInterfaces;
+} __attribute__((packed));
+
+struct op_rep_import {
+ struct op_common hdr;
+ struct usbip_usb_device dev;
+} __attribute__((packed));
+
+/* 20-byte base header common to all URB PDUs */
+struct usbip_header_basic {
+ uint32_t command;
+ uint32_t seqnum;
+ uint32_t devid;
+ uint32_t direction;
+ uint32_t ep;
+} __attribute__((packed));
+
+/* CMD_SUBMIT union fields (28 bytes) */
+struct usbip_header_cmd_submit {
+ uint32_t transfer_flags;
+ int32_t transfer_buffer_length;
+ int32_t start_frame;
+ int32_t number_of_packets;
+ int32_t interval;
+ uint8_t setup[8];
+} __attribute__((packed));
+
+/* Full URB PDU header (48 bytes) */
+struct usbip_header {
+ struct usbip_header_basic base;
+ union {
+ struct usbip_header_cmd_submit cmd_submit;
+ uint8_t raw[28];
+ } u;
+} __attribute__((packed));
+
+/* ── helpers ────────────────────────────────────────────────────────────────── */
+
+static int recv_all(int fd, void *buf, size_t len)
+{
+ size_t done = 0;
+ while (done < len) {
+ ssize_t r = recv(fd, (char *)buf + done, len - done, 0);
+ if (r <= 0)
+ return -1;
+ done += (size_t)r;
+ }
+ return 0;
+}
+
+static int send_all(int fd, const void *buf, size_t len)
+{
+ size_t done = 0;
+ while (done < len) {
+ ssize_t r = send(fd, (const char *)buf + done, len - done, MSG_NOSIGNAL);
+ if (r <= 0)
+ return -1;
+ done += (size_t)r;
+ }
+ return 0;
+}
+
+/* ── import handshake ───────────────────────────────────────────────────────── */
+
+static int do_import(int sock, const char *busid)
+{
+ struct op_req_import req;
+ memset(&req, 0, sizeof(req));
+ req.hdr.version = htons(USBIP_VERSION);
+ req.hdr.code = htons(OP_REQ_IMPORT);
+ req.hdr.status = htonl(0);
+ strncpy(req.busid, busid, SYSFS_BUS_ID_SIZE - 1);
+
+ if (send_all(sock, &req, sizeof(req)) < 0)
+ return -1;
+
+ /* Read the server's reply */
+ struct op_rep_import rep;
+ memset(&rep, 0, sizeof(rep));
+ if (recv_all(sock, &rep, sizeof(rep)) < 0)
+ return -1;
+
+ uint16_t code = ntohs(rep.hdr.code);
+ uint32_t status = ntohl(rep.hdr.status);
+ if (code != OP_REP_IMPORT || status != 0)
+ return -1;
+
+ return 0;
+}
+
+/* ── fuzzing loop ───────────────────────────────────────────────────────────── *
+ *
+ * Interprets the fuzz input as a stream of CMD_SUBMIT packets:
+ * [48-byte header] [transfer_buffer_length bytes OUT payload]
+ * [48-byte header] ...
+ *
+ * The 48-byte header is taken verbatim from fuzz bytes (with command and seqnum
+ * fixed to valid values so usbipd doesn't drop the packet before parsing the
+ * fields we care about: transfer_buffer_length, number_of_packets).
+ *
+ * transfer_buffer_length is clamped for the actual payload we send, but the
+ * stub receives the UNCLAMPED value from the header, which is what triggers the
+ * sgl_alloc() bug in stub_recv_cmd_submit().
+ */
+
+static int fuzz_loop(int sock, const uint8_t *fuzz, size_t fuzz_len)
+{
+ size_t fuzz_off = 0;
+ uint32_t seqnum = 1;
+ uint32_t last_submit_seq = 0; /* last CMD_SUBMIT seqnum, for CMD_UNLINK */
+
+ /* OUT payload — static buffer of poison bytes */
+ static uint8_t out_payload[MAX_OUT_PAYLOAD];
+ memset(out_payload, 0xaa, sizeof(out_payload));
+
+ while (fuzz_off + sizeof(struct usbip_header) <= fuzz_len) {
+ struct usbip_header pdu;
+ memset(&pdu, 0, sizeof(pdu));
+
+ /* Overlay fuzz bytes onto the full 48-byte PDU header */
+ memcpy(&pdu, fuzz + fuzz_off, sizeof(pdu));
+ fuzz_off += sizeof(pdu);
+
+ /*
+ * Bit 0 of the fuzz command field selects the PDU type:
+ * 0 → CMD_SUBMIT (exercises sgl_alloc + ISO kmalloc overflow)
+ * 1 → CMD_UNLINK (exercises seqnum linked-list lookup + potential UAF)
+ * CMD_UNLINK requires at least one prior CMD_SUBMIT for a valid seqnum
+ * to reference; fall back to CMD_SUBMIT on the very first packet.
+ */
+ uint32_t fuzz_cmd = ntohl(pdu.base.command);
+ if ((fuzz_cmd & 1) && last_submit_seq > 0) {
+ /* ── CMD_UNLINK path ─────────────────────────────────────────── */
+ pdu.base.command = htonl(USBIP_CMD_UNLINK);
+ pdu.base.seqnum = htonl(seqnum++);
+ pdu.base.devid = htonl(0x00010001);
+ /* unlink_seqnum sits at the start of the 28-byte union (BE u32) */
+ memset(pdu.u.raw, 0, sizeof(pdu.u.raw));
+ *(uint32_t *)pdu.u.raw = htonl(last_submit_seq);
+ if (send_all(sock, &pdu, sizeof(pdu)) < 0)
+ return 1;
+ struct usbip_header ret_ul;
+ recv(sock, &ret_ul, sizeof(ret_ul), MSG_DONTWAIT);
+ } else {
+ /* ── CMD_SUBMIT path ─────────────────────────────────────────── */
+ pdu.base.command = htonl(USBIP_CMD_SUBMIT);
+ pdu.base.seqnum = htonl(seqnum);
+ last_submit_seq = seqnum++;
+ /* devid is ignored by the stub once in tunnel mode */
+ pdu.base.devid = htonl(0x00010001);
+
+ /* Send the CMD_SUBMIT header */
+ if (send_all(sock, &pdu, sizeof(pdu)) < 0)
+ return 1; /* ECONNRESET = stub panicked → AFL++ crash */
+
+ /*
+ * For OUT transfers (direction == 0) the stub expects
+ * transfer_buffer_length bytes of payload after the header.
+ * Send at most MAX_OUT_PAYLOAD bytes; the stub sees the full
+ * (unclamped) length field in the header regardless.
+ */
+ uint32_t dir = ntohl(pdu.base.direction);
+ if (dir == 0 /* USBIP_DIR_OUT */) {
+ int32_t tbl = ntohl(pdu.u.cmd_submit.transfer_buffer_length);
+ int32_t send_len = tbl;
+ if (send_len < 0) send_len = 0;
+ if (send_len > MAX_OUT_PAYLOAD) send_len = MAX_OUT_PAYLOAD;
+ if (send_len > 0) {
+ if (send_all(sock, out_payload, (size_t)send_len) < 0)
+ return 1;
+ }
+ }
+
+ /*
+ * Optionally drain a RET_SUBMIT from the stub (non-blocking).
+ * We don't need to read it for the attack — the stub will crash
+ * before replying if we've triggered the bug. We just discard
+ * whatever comes back to keep the pipe clear.
+ */
+ {
+ struct usbip_header ret;
+ recv(sock, &ret, sizeof(ret), MSG_DONTWAIT);
+ }
+ }
+ }
+
+ return 0;
+}
+
+/* ── entry point ─────────────────────────────────────────────────────────────── */
+
+static uint8_t fuzz_buf[1 << 16];
+
+int main(int argc, char *argv[])
+{
+ if (argc < 4) {
+ fprintf(stderr,
+ "usage: %s <testcase> <vm-ip> <port>\n"
+ " testcase — AFL++ @@ input file (stream of 48-byte CMD_SUBMIT headers)\n"
+ " vm-ip — IP of QEMU VM running usbipd (e.g. 127.0.0.1)\n"
+ " port — usbipd port (default 3240)\n",
+ argv[0]);
+ return 1;
+ }
+
+ const char *testcase = argv[1];
+ const char *vmip = argv[2];
+ int port = atoi(argv[3]);
+
+ int fd = open(testcase, O_RDONLY);
+ if (fd < 0) { perror("open"); return 1; }
+ ssize_t fuzz_len = read(fd, fuzz_buf, sizeof(fuzz_buf));
+ close(fd);
+ if (fuzz_len <= 0)
+ return 0;
+
+ signal(SIGPIPE, SIG_IGN);
+
+ int sock = socket(AF_INET, SOCK_STREAM, 0);
+ if (sock < 0) { perror("socket"); return 1; }
+
+ /* Short connect timeout — VM might not be ready */
+ struct timeval tv = { .tv_sec = 3, .tv_usec = 0 };
+ setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+ setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
+
+ int one = 1;
+ setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
+
+ struct sockaddr_in sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sin_family = AF_INET;
+ sa.sin_port = htons((uint16_t)port);
+ if (inet_pton(AF_INET, vmip, &sa.sin_addr) != 1) {
+ fprintf(stderr, "bad address: %s\n", vmip);
+ close(sock);
+ return 1;
+ }
+
+ if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
+ /* VM not reachable — not a crash */
+ close(sock);
+ return 0;
+ }
+
+ /* Widen I/O timeout for the fuzzing phase */
+ struct timeval iotv = { .tv_sec = 0, .tv_usec = 500000 };
+ setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &iotv, sizeof(iotv));
+ setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &iotv, sizeof(iotv));
+
+ if (do_import(sock, "1-1") < 0) {
+ /* Import failed — stub may not be running or device not exported */
+ close(sock);
+ return 0;
+ }
+
+ int rc = fuzz_loop(sock, fuzz_buf, (size_t)fuzz_len);
+ close(sock);
+ return rc; /* 1 = ECONNRESET = kernel panic → AFL++ crash */
+}
diff --git a/fuzzing/usbip/fuzz_urb.c b/fuzzing/usbip/fuzz_urb.c
@@ -1,38 +1,26 @@
/*
- * fuzz_urb.c — USB/IP URB header parsing harness (CVE-2016-3955 class).
+ * fuzz_urb.c — USB/IP URB PDU header parsing harness (CVE-2016-3955 class).
*
- * Exercises usbip_header parsing: the 48-byte compound header composed of
- * usbip_header_basic (20 bytes) + usbip_header_cmd_submit (28 bytes).
+ * struct usbip_header is a kernel-internal type (not in userspace UAPI), so
+ * its wire layout is defined in usbip_fuzz.h. We receive it via usbip_net_recv()
+ * and byte-swap fields manually to exercise the validation paths that the
+ * kernel's vhci_rx.c / stub_rx.c would perform.
*
- * Target: usbip_net_pack_pdu() and the length validation performed when
- * processing USBIP_CMD_SUBMIT / USBIP_RET_SUBMIT messages.
+ * CVE-2016-3955: USBIP_RET_SUBMIT with actual_length > transfer_buffer_length
+ * causes a heap overflow in usbip_recv_xbuff() (kernel module).
*
- * CVE-2016-3955 pattern: USBIP_RET_SUBMIT with actual_length > urb->transfer_buffer_length
- * causes a heap overflow in usbip_recv_xbuff() (kernel module side).
- * The userspace stub_rx.c also processes these fields.
- *
- * Interesting bug classes:
- * - transfer_buffer_length negative / INT_MIN
- * - number_of_packets overflow
- * - actual_length > transfer_buffer_length (CVE-2016-3955 repro class)
- * - start_frame out of range for ISO transfers
- * - ep > 15 (invalid endpoint)
+ * Bug classes: transfer_buffer_length negative / INT_MIN, number_of_packets
+ * overflow, actual_length > transfer_buffer_length, ep > 15, bad command type.
*
* Input format:
- * [command:4 BE][seqnum:4 BE][devid:4 BE][direction:4 BE][ep:4 BE]
- * [transfer_flags:4 BE][transfer_buffer_length:4 BE][start_frame:4 BE]
- * [number_of_packets:4 BE][interval:4 BE][setup[8]]
+ * [command:4 BE][seqnum:4 BE][devid:4 BE][direction:4 BE][ep:4 BE] = 20 bytes
+ * [transfer_flags:4][transfer_buffer_length:4][start_frame:4] = 12 bytes
+ * [number_of_packets:4][interval:4][setup:8] = 16 bytes
* [... optional data payload ...]
- *
- * Compile (via build_fuzzers.sh):
- * afl-clang-fast -fsanitize=address,undefined -g -O1 \
- * -I fuzz-include -I usbip-src/src -I usbip-src/libsrc \
- * fuzz_urb.c usbip_network.o usbip_common.o mock_syscalls.o \
- * -Wl,--wrap=recv -Wl,--wrap=send -Wl,--wrap=write \
- * -o fuzz_urb
*/
#include "fuzz-include/usbip_fuzz.h"
+#include "usbip-src/libsrc/usbip_common.h"
#include "usbip-src/src/usbip_network.h"
#include <stdint.h>
@@ -40,18 +28,12 @@
#include <string.h>
#include <arpa/inet.h>
-#define FAKE_FD 42
-#define HDR_SIZE 48 /* sizeof(usbip_header) */
-
-/*
- * Cap transfer_buffer_length to 64 KB to avoid spending time on legal
- * multi-MB allocations while still testing overflow conditions.
- */
-#define MAX_XFER_LEN (1 << 16)
+#define FAKE_FD 42
+#define MAX_XFER_LEN (1 << 16) /* 64 KB cap to avoid slow-but-legal allocs */
static int fuzz_one(const uint8_t *data, size_t size)
{
- if (size < HDR_SIZE)
+ if (size < USBIP_PDU_HDR_SIZE)
return 0;
uint8_t buf[FUZZ_BUF_MAX];
@@ -59,8 +41,9 @@ static int fuzz_one(const uint8_t *data, size_t size)
memcpy(buf, data, n);
/*
- * Read transfer_buffer_length from offset 24 (after the 20-byte basic hdr
- * + 4-byte transfer_flags) and clamp it so we don't OOM.
+ * Clamp transfer_buffer_length (bytes 24-27 in the PDU) so we don't
+ * spend time on legal multi-MB allocations. Still allows testing
+ * negative values and overflow conditions.
*/
int32_t tbl_be;
memcpy(&tbl_be, buf + 24, 4);
@@ -73,22 +56,27 @@ static int fuzz_one(const uint8_t *data, size_t size)
fuzz_reset(buf, n);
- /*
- * usbip_net_recv_pdu_header parses the full compound header.
- * This exercises all the byte-swap + validation logic.
- */
struct usbip_header hdr;
memset(&hdr, 0, sizeof(hdr));
- usbip_net_recv_pdu_header(FAKE_FD, &hdr);
+ if (usbip_net_recv(FAKE_FD, &hdr, sizeof(hdr)) < 0)
+ return 0;
+
+ /* Byte-swap base header fields from network order */
+ hdr.base.command = ntohl(hdr.base.command);
+ hdr.base.seqnum = ntohl(hdr.base.seqnum);
+ hdr.base.devid = ntohl(hdr.base.devid);
+ hdr.base.direction = ntohl(hdr.base.direction);
+ hdr.base.ep = ntohl(hdr.base.ep);
+
+ uint32_t cmd = hdr.base.command;
- /*
- * For submit commands, also exercise the data-payload receive path.
- * usbip_net_recv_xbuff reads transfer_buffer_length bytes — this is
- * where CVE-2016-3955 manifested (actual_length > transfer_buffer_length).
- */
- uint32_t cmd = ntohl(hdr.base.command);
if (cmd == USBIP_CMD_SUBMIT || cmd == USBIP_RET_SUBMIT) {
- int32_t xfer_len = ntohl((uint32_t)hdr.u.cmd_submit.transfer_buffer_length);
+ /*
+ * For submit/return, read the payload indicated by transfer_buffer_length.
+ * This exercises the CVE-2016-3955 overflow class.
+ */
+ int32_t xfer_len = (int32_t)ntohl(
+ (uint32_t)hdr.u.cmd_submit.transfer_buffer_length);
if (xfer_len > 0 && xfer_len <= MAX_XFER_LEN) {
void *payload = malloc((size_t)xfer_len);
if (payload) {
@@ -96,6 +84,9 @@ static int fuzz_one(const uint8_t *data, size_t size)
free(payload);
}
}
+ } else if (cmd == USBIP_CMD_UNLINK || cmd == USBIP_RET_UNLINK) {
+ /* Unlink just has a seqnum — already in hdr.u.cmd_unlink.seqnum */
+ hdr.u.cmd_unlink.seqnum = ntohl(hdr.u.cmd_unlink.seqnum);
}
return 0;
diff --git a/fuzzing/usbip/fuzz_vhci_server.c b/fuzzing/usbip/fuzz_vhci_server.c
@@ -0,0 +1,680 @@
+/*
+ * fuzz_vhci_server.c — Two-sided USB-IP fuzzer: malicious server targeting vhci-hcd.
+ *
+ * WHAT THIS TESTS (vs. fuzz_urb.c)
+ * ─────────────────────────────────
+ * fuzz_urb.c exercises the *userspace* copy of the URB parsing code (linked from
+ * tools/usb/usbip/). This binary exercises the *kernel* copy — specifically the
+ * vhci-hcd driver's receive path (vhci_rx.c → usbip_recv_xbuff / usbip_recv_iso)
+ * which runs without any sandbox and directly handles untrusted network data.
+ *
+ * CVE-2016-3955: a server sends USBIP_RET_SUBMIT with actual_length >
+ * transfer_buffer_length → heap overflow in usbip_recv_xbuff(). The server
+ * has full control over actual_length. fuzz_urb.c can't trigger this in kernel
+ * memory; this binary can (with KASAN enabled in the QEMU kernel).
+ *
+ * ISO OVERFLOW BUGS (usbip_recv_iso / usbip_pad_iso)
+ * ───────────────────────────────────────────────────
+ * The server controls urb->number_of_packets via the RET_SUBMIT response.
+ * In usbip_recv_iso():
+ * int np = urb->number_of_packets; // ← server sets this
+ * int size = np * sizeof(*iso); // integer overflow if np >= 0x08000001
+ * buff = kzalloc(size, GFP_KERNEL); // undersized allocation
+ * usbip_net_recv(sock, buff, size); // receives only `size` bytes
+ * for (i = 0; i < np; i++) // loops np times → OOB read from buff
+ *
+ * ARCHITECTURE
+ * ─────────────
+ * Host : fuzz_vhci_server <testcase> <host> <port> (AFL++ target)
+ * VM : vhci-hcd loaded, usbip-attach-watchdog running (see qemu/init-vhci)
+ *
+ * 1. Host server listens on <host>:<port>.
+ * 2. VM's vhci-hcd client connects (driven by usbip-attach-watchdog in /init-vhci).
+ * 3. Server completes USB-IP handshake: read OP_REQ_IMPORT → send OP_REP_IMPORT.
+ * 4. USB core enumerates device via EP0 control transfers (GET_DESCRIPTOR etc.).
+ * Server responds with a valid USB Audio Class descriptor chain so that the
+ * snd_usb_audio driver binds and submits ISO URBs.
+ * 5. For non-EP0 URBs, server responds with USBIP_RET_SUBMIT packets whose
+ * ret_submit fields (status, actual_length, number_of_packets, etc.) are
+ * overlaid from the AFL++ test case bytes. ISO descriptor data is appended.
+ * 6. If the kernel panics (KASAN report + panic_on_oops=1), the VM reboots,
+ * vhci-hcd drops the connection → ECONNRESET → server exits 1 → AFL++ crash.
+ * 7. Watchdog in VM immediately retries `usbip attach` for the next iteration.
+ *
+ * AFL++ INVOCATION (from run-fuzzers.sh or manually)
+ * ────────────────────────────────────────────────────
+ * # in one tmux pane: start QEMU VM in vhci-client mode
+ * qemu-system-x86_64 -kernel bzImage -initrd initramfs-vhci.cpio.gz \
+ * -nographic -append "console=ttyS0 quiet" -m 512M \
+ * -net nic,model=e1000 -net user
+ *
+ * # in another pane: run AFL++ against the server
+ * afl-fuzz -S vhci -i corpus/vhci -o output -t 3000 -x dictionaries/usbip.dict \
+ * -- ./fuzz_vhci_server @@ 0.0.0.0 13241
+ *
+ * COMPILE
+ * ────────
+ * afl-clang-fast -O2 -o fuzz_vhci_server fuzz_vhci_server.c
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <time.h>
+#include <unistd.h>
+
+/* ── USB/IP protocol constants ─────────────────────────────────────────────── */
+#define USBIP_VERSION 0x0111
+
+#define OP_REQ_IMPORT 0x8003
+#define OP_REP_IMPORT 0x0003
+
+#define USBIP_CMD_SUBMIT 0x00000001
+#define USBIP_CMD_UNLINK 0x00000002
+#define USBIP_RET_SUBMIT 0x00000003
+#define USBIP_RET_UNLINK 0x00000004
+
+#define SYSFS_BUS_ID_SIZE 32
+#define SYSFS_PATH_MAX 256
+
+/* Maximum actual_length / ISO descriptor data we'll allocate+send (to avoid OOM) */
+#define MAX_ACTUAL_LEN (1 << 16) /* 64 KB */
+#define MAX_ISO_DESC_LEN (1 << 16) /* 64 KB — kernel won't get more */
+
+/* sizeof(struct usbip_iso_packet_descriptor) in the kernel */
+#define ISO_DESC_SIZE 16
+
+/*
+ * ISO endpoint number on our virtual audio device.
+ * EP1 IN is declared as isochronous in the config descriptor below.
+ */
+#define ISO_EP_NUM 1
+
+/* ── wire-format structures (all fields in big-endian / network order) ──────── */
+
+struct op_common {
+ uint16_t version;
+ uint16_t code;
+ uint32_t status;
+} __attribute__((packed));
+
+struct op_req_import {
+ struct op_common hdr;
+ char busid[SYSFS_BUS_ID_SIZE];
+} __attribute__((packed));
+
+struct usbip_usb_device {
+ char path[SYSFS_PATH_MAX];
+ char busid[SYSFS_BUS_ID_SIZE];
+ uint32_t busnum;
+ uint32_t devnum;
+ uint32_t speed;
+ uint16_t idVendor;
+ uint16_t idProduct;
+ uint16_t bcdDevice;
+ uint8_t bDeviceClass;
+ uint8_t bDeviceSubClass;
+ uint8_t bDeviceProtocol;
+ uint8_t bConfigurationValue;
+ uint8_t bNumConfigurations;
+ uint8_t bNumInterfaces;
+} __attribute__((packed));
+
+struct op_rep_import {
+ struct op_common hdr;
+ struct usbip_usb_device dev;
+} __attribute__((packed));
+
+struct usbip_header_basic {
+ uint32_t command;
+ uint32_t seqnum;
+ uint32_t devid;
+ uint32_t direction;
+ uint32_t ep;
+} __attribute__((packed));
+
+struct usbip_header_cmd_submit {
+ uint32_t transfer_flags;
+ int32_t transfer_buffer_length;
+ int32_t start_frame;
+ int32_t number_of_packets;
+ int32_t interval;
+ uint8_t setup[8];
+} __attribute__((packed));
+
+struct usbip_header_ret_submit {
+ int32_t status;
+ int32_t actual_length;
+ int32_t start_frame;
+ int32_t number_of_packets;
+ int32_t error_count;
+ uint8_t pad[8];
+} __attribute__((packed));
+
+struct usbip_header {
+ struct usbip_header_basic base;
+ union {
+ struct usbip_header_cmd_submit cmd_submit;
+ struct usbip_header_ret_submit ret_submit;
+ uint8_t raw[28];
+ } u;
+} __attribute__((packed));
+
+/* ── USB Descriptors for virtual USB Audio Class device ────────────────────────
+ *
+ * We present a minimal USB Audio 1.0 device with:
+ * Interface 0: AudioControl
+ * Interface 1 alt-0: AudioStreaming (zero bandwidth, 0 endpoints)
+ * Interface 1 alt-1: AudioStreaming (active, 1 ISO IN endpoint → EP1)
+ *
+ * The snd_usb_audio driver (built into typical kernels) will bind to this device
+ * and start submitting isochronous URBs on EP1 for audio streaming.
+ * Those ISO URBs come back to us as CMD_SUBMIT on ep==1, and we respond with
+ * fuzz-controlled RET_SUBMIT.number_of_packets to trigger usbip_recv_iso() bugs.
+ *
+ * Descriptor chain layout (wTotalLength = 79):
+ * Config(9) + Iface0-AC(9) + AC-Header(9) +
+ * Iface1-alt0(9) + Iface1-alt1(9) + AS-General(7) + FormatType(11) +
+ * EP-ISO-IN(9) + CS-EP(7) = 79
+ */
+
+/* Device Descriptor (18 bytes, USB 2.0, per-interface class) */
+static const uint8_t usb_device_desc[] = {
+ 18, /* bLength */
+ 0x01, /* bDescriptorType: DEVICE */
+ 0x00, 0x02, /* bcdUSB: 2.0 (little-endian) */
+ 0x00, /* bDeviceClass: defined per-interface */
+ 0x00, /* bDeviceSubClass */
+ 0x00, /* bDeviceProtocol */
+ 64, /* bMaxPacketSize0: 64 bytes for HS control */
+ 0x34, 0x12, /* idVendor: 0x1234 */
+ 0x78, 0x56, /* idProduct: 0x5678 */
+ 0x00, 0x01, /* bcdDevice: 1.0 */
+ 0, /* iManufacturer */
+ 0, /* iProduct */
+ 0, /* iSerialNumber */
+ 1, /* bNumConfigurations */
+};
+
+/*
+ * Full Configuration Descriptor chain.
+ * wTotalLength = 79 (see layout above).
+ * bNumInterfaces = 2 (AudioControl + AudioStreaming).
+ */
+static const uint8_t usb_config_desc[] = {
+ /* ── Configuration Descriptor (9 bytes) ── */
+ 9, 0x02, /* bLength, bDescriptorType: CONFIGURATION */
+ 79, 0, /* wTotalLength: 79 (LE) */
+ 2, /* bNumInterfaces */
+ 1, /* bConfigurationValue */
+ 0, /* iConfiguration */
+ 0x80, /* bmAttributes: bus-powered, no remote wakeup */
+ 50, /* bMaxPower: 100 mA */
+
+ /* ── Interface 0: AudioControl, alt 0, 0 endpoints (9 bytes) ── */
+ 9, 0x04, /* bLength, bDescriptorType: INTERFACE */
+ 0, /* bInterfaceNumber: 0 */
+ 0, /* bAlternateSetting: 0 */
+ 0, /* bNumEndpoints: 0 (control-only AC interface) */
+ 0x01, /* bInterfaceClass: Audio */
+ 0x01, /* bInterfaceSubClass: AudioControl */
+ 0x00, /* bInterfaceProtocol */
+ 0, /* iInterface */
+
+ /* ── AudioControl Class-Specific Interface: HEADER (9 bytes) ── */
+ 9, 0x24, /* bLength, bDescriptorType: CS_INTERFACE */
+ 0x01, /* bDescriptorSubType: HEADER */
+ 0x00, 0x01, /* bcdADC: 1.0 (LE) */
+ 9, 0, /* wTotalLength: 9 bytes AC-only (LE) */
+ 1, /* bInCollection: 1 streaming interface */
+ 1, /* baInterfaceNr[0]: interface 1 is the streaming interface */
+
+ /* ── Interface 1: AudioStreaming, alt 0 — zero bandwidth (9 bytes) ── */
+ 9, 0x04, /* bLength, bDescriptorType: INTERFACE */
+ 1, /* bInterfaceNumber: 1 */
+ 0, /* bAlternateSetting: 0 */
+ 0, /* bNumEndpoints: 0 (idle setting) */
+ 0x01, /* bInterfaceClass: Audio */
+ 0x02, /* bInterfaceSubClass: AudioStreaming */
+ 0x00, /* bInterfaceProtocol */
+ 0, /* iInterface */
+
+ /* ── Interface 1: AudioStreaming, alt 1 — active ISO streaming (9 bytes) ── */
+ 9, 0x04, /* bLength, bDescriptorType: INTERFACE */
+ 1, /* bInterfaceNumber: 1 */
+ 1, /* bAlternateSetting: 1 */
+ 1, /* bNumEndpoints: 1 (the ISO IN endpoint) */
+ 0x01, /* bInterfaceClass: Audio */
+ 0x02, /* bInterfaceSubClass: AudioStreaming */
+ 0x00, /* bInterfaceProtocol */
+ 0, /* iInterface */
+
+ /* ── AudioStreaming Class-Specific Interface: AS_GENERAL (7 bytes) ── */
+ 7, 0x24, /* bLength, bDescriptorType: CS_INTERFACE */
+ 0x01, /* bDescriptorSubType: AS_GENERAL */
+ 1, /* bTerminalLink: Terminal ID 1 (placeholder) */
+ 1, /* bDelay: 1 frame pipeline delay */
+ 0x01, 0x00, /* wFormatTag: PCM (LE) */
+
+ /* ── AudioStreaming Class-Specific Interface: FORMAT_TYPE_I (11 bytes) ── */
+ 11, 0x24, /* bLength, bDescriptorType: CS_INTERFACE */
+ 0x02, /* bDescriptorSubType: FORMAT_TYPE */
+ 0x01, /* bFormatType: FORMAT_TYPE_I */
+ 2, /* bNrChannels: stereo */
+ 2, /* bSubframeSize: 2 bytes per sample */
+ 16, /* bBitResolution: 16 bits */
+ 1, /* bSamFreqType: 1 discrete frequency */
+ 0x44, 0xAC, 0x00, /* tSamFreq[0]: 44100 Hz (3-byte LE: 0x00AC44) */
+
+ /* ── Isochronous IN Endpoint (EP1 IN): bmAttributes=iso/async/data (9 bytes) ── */
+ 9, 0x05, /* bLength, bDescriptorType: ENDPOINT */
+ 0x81, /* bEndpointAddress: EP1 IN (0x80 | 1) */
+ 0x01, /* bmAttributes: isochronous, no sync, data */
+ 196, 0, /* wMaxPacketSize: 196 bytes (stereo 16-bit 44100Hz @ 1kHz frames, LE) */
+ 1, /* bInterval: every 1 ms (full-speed) or 1 microframe (high-speed) */
+ 0, /* bRefresh */
+ 0, /* bSynchAddress */
+
+ /* ── AudioStreaming Class-Specific Endpoint: EP_GENERAL (7 bytes) ── */
+ 7, 0x25, /* bLength, bDescriptorType: CS_ENDPOINT */
+ 0x01, /* bDescriptorSubType: EP_GENERAL */
+ 0x01, /* bmAttributes: sampling frequency control */
+ 0, /* bLockDelayUnits */
+ 0x00, 0x00, /* wLockDelay: 0 */
+};
+
+/* Device Qualifier Descriptor (10 bytes) — required for USB 2.0 high-speed devices */
+static const uint8_t usb_devqual_desc[] = {
+ 10, /* bLength */
+ 0x06, /* bDescriptorType: DEVICE_QUALIFIER */
+ 0x00, 0x02, /* bcdUSB: 2.0 */
+ 0x00, /* bDeviceClass */
+ 0x00, /* bDeviceSubClass */
+ 0x00, /* bDeviceProtocol */
+ 64, /* bMaxPacketSize0 */
+ 1, /* bNumConfigurations */
+ 0, /* bReserved */
+};
+
+/* ── helpers ────────────────────────────────────────────────────────────────── */
+
+static int recv_all(int fd, void *buf, size_t len)
+{
+ size_t done = 0;
+ while (done < len) {
+ ssize_t r = recv(fd, (char *)buf + done, len - done, 0);
+ if (r <= 0)
+ return -1;
+ done += (size_t)r;
+ }
+ return 0;
+}
+
+static int send_all(int fd, const void *buf, size_t len)
+{
+ size_t done = 0;
+ while (done < len) {
+ ssize_t r = send(fd, (const char *)buf + done, len - done, MSG_NOSIGNAL);
+ if (r <= 0)
+ return -1;
+ done += (size_t)r;
+ }
+ return 0;
+}
+
+/* ── handshake: read OP_REQ_IMPORT, send OP_REP_IMPORT ─────────────────────── */
+
+static int do_handshake(int sock)
+{
+ struct op_req_import req;
+ memset(&req, 0, sizeof(req));
+ if (recv_all(sock, &req, sizeof(req)) < 0)
+ return -1;
+
+ uint16_t code = ntohs(req.hdr.code);
+ if (code != OP_REQ_IMPORT)
+ return -1;
+
+ /*
+ * Report our virtual device as USB_SPEED_HIGH audio class device.
+ * bDeviceClass=0 (per-interface) with bNumInterfaces=2 matches the
+ * config descriptor we return for GET_DESCRIPTOR.
+ */
+ struct op_rep_import rep;
+ memset(&rep, 0, sizeof(rep));
+ rep.hdr.version = htons(USBIP_VERSION);
+ rep.hdr.code = htons(OP_REP_IMPORT);
+ rep.hdr.status = htonl(0);
+
+ strncpy(rep.dev.path, "/sys/devices/pci0000:00/0000:00:01.0/usb1/1-1", SYSFS_PATH_MAX - 1);
+ strncpy(rep.dev.busid, "1-1", SYSFS_BUS_ID_SIZE - 1);
+ rep.dev.busnum = htonl(1);
+ rep.dev.devnum = htonl(1);
+ rep.dev.speed = htonl(3); /* USB_SPEED_HIGH */
+ rep.dev.idVendor = htons(0x1234);
+ rep.dev.idProduct = htons(0x5678);
+ rep.dev.bcdDevice = htons(0x0100);
+ rep.dev.bDeviceClass = 0; /* per-interface */
+ rep.dev.bDeviceSubClass = 0;
+ rep.dev.bDeviceProtocol = 0;
+ rep.dev.bConfigurationValue = 1;
+ rep.dev.bNumConfigurations = 1;
+ rep.dev.bNumInterfaces = 2; /* AudioControl + AudioStreaming */
+
+ return send_all(sock, &rep, sizeof(rep));
+}
+
+/* ── EP0 control transfer handler ───────────────────────────────────────────── *
+ *
+ * Responds to USB control requests from the kernel USB core during enumeration.
+ * Without valid responses here, USB enumeration fails and no driver ever binds,
+ * so no bulk/ISO URBs are submitted and the interesting kernel paths are never hit.
+ *
+ * Setup packet fields:
+ * setup[0]: bmRequestType
+ * setup[1]: bRequest
+ * setup[2..3]: wValue (descriptor type in high byte for GET_DESCRIPTOR)
+ * setup[4..5]: wIndex
+ * setup[6..7]: wLength (max bytes host wants back)
+ */
+
+/* Send a RET_SUBMIT for a control transfer with an optional data payload */
+static int send_ctrl_response(int sock, struct usbip_header *hdr,
+ int32_t status,
+ const void *data, uint32_t data_len)
+{
+ struct usbip_header resp;
+ memset(&resp, 0, sizeof(resp));
+ resp.base.command = htonl(USBIP_RET_SUBMIT);
+ resp.base.seqnum = hdr->base.seqnum;
+ resp.base.devid = hdr->base.devid;
+ resp.base.direction = hdr->base.direction;
+ resp.base.ep = hdr->base.ep;
+ resp.u.ret_submit.status = htonl((uint32_t)status);
+ resp.u.ret_submit.actual_length = htonl(data_len);
+ resp.u.ret_submit.start_frame = 0;
+ resp.u.ret_submit.number_of_packets = 0;
+ resp.u.ret_submit.error_count = 0;
+
+ if (send_all(sock, &resp, sizeof(resp)) < 0)
+ return -1;
+ if (data && data_len > 0) {
+ if (send_all(sock, data, data_len) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int handle_ep0(int sock, struct usbip_header *hdr)
+{
+ uint8_t *setup = hdr->u.cmd_submit.setup;
+ uint8_t breq = setup[1]; /* bRequest */
+ uint8_t dtype = setup[3]; /* descriptor type (high byte of wValue) */
+ uint16_t wlen = (uint16_t)setup[6] | ((uint16_t)setup[7] << 8);
+
+ /* Drain any OUT data payload the host sent (unlikely for enumeration) */
+ int32_t tbl = ntohl(hdr->u.cmd_submit.transfer_buffer_length);
+ if (ntohl(hdr->base.direction) == 0 /* USBIP_DIR_OUT */ && tbl > 0) {
+ uint8_t drain[MAX_ACTUAL_LEN];
+ int dlen = tbl < MAX_ACTUAL_LEN ? tbl : MAX_ACTUAL_LEN;
+ recv_all(sock, drain, (size_t)dlen);
+ }
+
+ if (breq == 0x06) {
+ /* GET_DESCRIPTOR */
+ const uint8_t *desc = NULL;
+ uint32_t desc_len = 0;
+
+ switch (dtype) {
+ case 0x01: /* DEVICE */
+ desc = usb_device_desc;
+ desc_len = sizeof(usb_device_desc);
+ break;
+ case 0x02: /* CONFIGURATION */
+ desc = usb_config_desc;
+ desc_len = sizeof(usb_config_desc);
+ break;
+ case 0x06: /* DEVICE QUALIFIER */
+ desc = usb_devqual_desc;
+ desc_len = sizeof(usb_devqual_desc);
+ break;
+ default:
+ /* String, HID, etc. — STALL (no descriptor) */
+ return send_ctrl_response(sock, hdr, -32 /* -EPIPE */, NULL, 0);
+ }
+
+ /* Honour the host's wLength limit */
+ if (desc_len > wlen)
+ desc_len = wlen;
+
+ return send_ctrl_response(sock, hdr, 0, desc, desc_len);
+
+ } else if (breq == 0x09 || /* SET_CONFIGURATION */
+ breq == 0x0B || /* SET_INTERFACE */
+ breq == 0x01 || /* CLEAR_FEATURE */
+ breq == 0x03 || /* SET_FEATURE */
+ breq == 0x0A /* GET_INTERFACE */) {
+ /* Acknowledge with empty success */
+ return send_ctrl_response(sock, hdr, 0, NULL, 0);
+
+ } else {
+ /* Unknown/unsupported request — STALL */
+ return send_ctrl_response(sock, hdr, -32 /* -EPIPE */, NULL, 0);
+ }
+}
+
+/* ── main fuzzing loop ──────────────────────────────────────────────────────── *
+ *
+ * Dispatch:
+ * EP0 → handle_ep0() with valid descriptor responses (enables enumeration)
+ * EP1 → fuzz RET_SUBMIT; ISO descriptor data is also sent (enables iso path)
+ * EPx → fuzz RET_SUBMIT (bulk/interrupt — triggers xbuff path)
+ */
+
+static int fuzz_loop(int sock, const uint8_t *fuzz, size_t fuzz_len)
+{
+ size_t fuzz_off = 0;
+
+ static uint8_t payload[MAX_ACTUAL_LEN];
+ memset(payload, 0xcc, sizeof(payload));
+
+ static uint8_t iso_buf[MAX_ISO_DESC_LEN];
+ memset(iso_buf, 0x00, sizeof(iso_buf));
+
+ for (;;) {
+ struct usbip_header hdr;
+ memset(&hdr, 0, sizeof(hdr));
+ if (recv_all(sock, &hdr, sizeof(hdr)) < 0)
+ return 0;
+
+ uint32_t cmd = ntohl(hdr.base.command);
+ uint32_t ep = ntohl(hdr.base.ep);
+
+ if (cmd == USBIP_CMD_SUBMIT) {
+
+ /* ── EP0: control transfer — respond with valid USB descriptors ── */
+ if (ep == 0) {
+ if (handle_ep0(sock, &hdr) < 0)
+ return 1;
+ continue;
+ }
+
+ /* ── Non-EP0: drain OUT payload ── */
+ int32_t tbl = ntohl(hdr.u.cmd_submit.transfer_buffer_length);
+ if (ntohl(hdr.base.direction) == 0 /* USBIP_DIR_OUT */ && tbl > 0) {
+ int drain_len = tbl < MAX_ACTUAL_LEN ? tbl : MAX_ACTUAL_LEN;
+ uint8_t drain[MAX_ACTUAL_LEN];
+ recv_all(sock, drain, (size_t)drain_len);
+ }
+
+ /* ── Build RET_SUBMIT with fuzz bytes overlaid on the union ── */
+ struct usbip_header resp;
+ memset(&resp, 0, sizeof(resp));
+ resp.base.command = htonl(USBIP_RET_SUBMIT);
+ resp.base.seqnum = hdr.base.seqnum;
+ resp.base.devid = hdr.base.devid;
+ resp.base.direction = hdr.base.direction;
+ resp.base.ep = hdr.base.ep;
+
+ /* Overlay fuzz bytes onto the 28-byte ret_submit union */
+ size_t overlay = fuzz_len - fuzz_off;
+ if (overlay > sizeof(resp.u.raw))
+ overlay = sizeof(resp.u.raw);
+ memcpy(resp.u.raw, fuzz + fuzz_off, overlay);
+ fuzz_off += overlay;
+
+ /* Clamp actual_length for the host-side payload send */
+ int32_t actual_len = ntohl(resp.u.ret_submit.actual_length);
+ int32_t send_len = actual_len;
+ if (send_len < 0) send_len = 0;
+ if (send_len > MAX_ACTUAL_LEN) send_len = MAX_ACTUAL_LEN;
+
+ if (send_all(sock, &resp, sizeof(resp)) < 0)
+ return 1;
+
+ /* Send xbuff (actual_length bytes) */
+ if (send_len > 0) {
+ if (send_all(sock, payload, (size_t)send_len) < 0)
+ return 1;
+ }
+
+ /*
+ * ── ISO descriptor data (EP1 only) ──────────────────────────────
+ *
+ * For ISO URBs the kernel expects, AFTER the xbuff:
+ * number_of_packets × sizeof(struct usbip_iso_packet_descriptor)
+ * bytes of ISO frame descriptors.
+ *
+ * The overflow attack: if number_of_packets is large enough that
+ * np * ISO_DESC_SIZE overflows int32 (e.g. np=0x08000001 → wraps
+ * to a small value), the kernel allocates a small buffer but loops
+ * np times → KASAN heap OOB.
+ *
+ * We mirror the kernel's 32-bit truncated multiplication so we send
+ * exactly the number of bytes the (buggy) kernel will recv(), keeping
+ * the connection alive for the subsequent OOB loop to fire.
+ */
+ if (ep == ISO_EP_NUM) {
+ int32_t np = ntohl(resp.u.ret_submit.number_of_packets);
+ if (np > 0) {
+ /*
+ * Replicate the kernel's 32-bit overflow:
+ * uint32_t size = (uint32_t)np * ISO_DESC_SIZE;
+ * If np * 16 wraps to a small value, we send that many bytes
+ * and the kernel will allocate the same small buffer, then
+ * loop np times reading OOB → KASAN hit.
+ */
+ uint32_t iso_size = (uint32_t)((uint32_t)np * (uint32_t)ISO_DESC_SIZE);
+ uint32_t iso_send = iso_size;
+ if (iso_send > MAX_ISO_DESC_LEN)
+ iso_send = MAX_ISO_DESC_LEN;
+ if (iso_send > 0) {
+ if (send_all(sock, iso_buf, (size_t)iso_send) < 0)
+ return 1;
+ }
+ }
+ }
+
+ if (fuzz_off >= fuzz_len)
+ return 0;
+
+ } else if (cmd == USBIP_CMD_UNLINK) {
+ struct usbip_header resp;
+ memset(&resp, 0, sizeof(resp));
+ resp.base.command = htonl(USBIP_RET_UNLINK);
+ resp.base.seqnum = hdr.base.seqnum;
+ resp.base.devid = hdr.base.devid;
+ /* status = -ENOENT (-2), big-endian */
+ resp.u.raw[0] = 0xff; resp.u.raw[1] = 0xff;
+ resp.u.raw[2] = 0xff; resp.u.raw[3] = 0xfe;
+ if (send_all(sock, &resp, sizeof(resp)) < 0)
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+}
+
+/* ── entry point ─────────────────────────────────────────────────────────────── */
+
+static uint8_t fuzz_buf[1 << 16];
+
+int main(int argc, char *argv[])
+{
+ if (argc < 4) {
+ fprintf(stderr, "usage: %s <testcase> <bind-addr> <port>\n"
+ " testcase — AFL++ @@ input file\n"
+ " bind-addr — address to listen on (e.g. 0.0.0.0)\n"
+ " port — TCP port (e.g. 13241)\n",
+ argv[0]);
+ return 1;
+ }
+
+ const char *testcase = argv[1];
+ const char *bindaddr = argv[2];
+ int port = atoi(argv[3]);
+
+ int fd = open(testcase, O_RDONLY);
+ if (fd < 0) { perror("open"); return 1; }
+ ssize_t fuzz_len = read(fd, fuzz_buf, sizeof(fuzz_buf));
+ close(fd);
+ if (fuzz_len <= 0)
+ return 0;
+
+ signal(SIGPIPE, SIG_IGN);
+
+ int srv = socket(AF_INET, SOCK_STREAM, 0);
+ if (srv < 0) { perror("socket"); return 1; }
+
+ int yes = 1;
+ setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
+ setsockopt(srv, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));
+
+ struct sockaddr_in sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sin_family = AF_INET;
+ sa.sin_port = htons((uint16_t)port);
+ if (inet_pton(AF_INET, bindaddr, &sa.sin_addr) != 1) {
+ fprintf(stderr, "bad bind address: %s\n", bindaddr);
+ return 1;
+ }
+
+ if (bind(srv, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
+ perror("bind"); return 1;
+ }
+ if (listen(srv, 1) < 0) { perror("listen"); return 1; }
+
+ /* Wait up to 3 seconds for the VM to connect */
+ struct timeval tv = { .tv_sec = 3, .tv_usec = 0 };
+ setsockopt(srv, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
+ int cli = accept(srv, NULL, NULL);
+ close(srv);
+ if (cli < 0)
+ return 0; /* timeout — not a crash */
+
+ struct timeval iotv = { .tv_sec = 0, .tv_usec = 500000 };
+ setsockopt(cli, SOL_SOCKET, SO_RCVTIMEO, &iotv, sizeof(iotv));
+ setsockopt(cli, SOL_SOCKET, SO_SNDTIMEO, &iotv, sizeof(iotv));
+
+ int one = 1;
+ setsockopt(cli, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
+
+ if (do_handshake(cli) < 0) {
+ close(cli); return 0;
+ }
+
+ int rc = fuzz_loop(cli, fuzz_buf, (size_t)fuzz_len);
+ close(cli);
+ return rc; /* 1 = ECONNRESET = kernel panic → AFL++ crash */
+}
diff --git a/fuzzing/usbip/gen_corpus.py b/fuzzing/usbip/gen_corpus.py
@@ -202,9 +202,18 @@ seed("urb_ret_len_max", urb_header(RET_SUBMIT, transfer_buffer_length=65
seed("urb_ret_len_overflow", urb_header(RET_SUBMIT, transfer_buffer_length=0x7FFFFFFF))
# number_of_packets edge cases (ISO transfers)
-seed("urb_iso_packets_0", urb_header(CMD_SUBMIT, number_of_packets=0))
-seed("urb_iso_packets_max", urb_header(CMD_SUBMIT, number_of_packets=0x7FFFFFFF))
-seed("urb_iso_packets_neg", urb_header(CMD_SUBMIT, number_of_packets=-1))
+# stub_rx.c: kmalloc(number_of_packets * sizeof(struct usbip_iso_packet_descriptor))
+# iso_descriptor is 16 bytes; overflow threshold: n > 0x0fffffff
+seed("urb_iso_packets_0", urb_header(CMD_SUBMIT, number_of_packets=0))
+seed("urb_iso_packets_max", urb_header(CMD_SUBMIT, number_of_packets=0x7FFFFFFF))
+seed("urb_iso_packets_neg", urb_header(CMD_SUBMIT, number_of_packets=-1))
+seed("urb_iso_wrap_u32", urb_header(CMD_SUBMIT, number_of_packets=0x10000000)) # n*16 == 0
+seed("urb_iso_wrap_u32p1", urb_header(CMD_SUBMIT, number_of_packets=0x10000001)) # n*16 == 16
+seed("urb_iso_wrap_s32", urb_header(CMD_SUBMIT, number_of_packets=0x08000000)) # n*16 wraps s32
+
+# transfer_buffer_length near overflow boundaries
+seed("urb_tbl_uint_max", urb_header(CMD_SUBMIT, transfer_buffer_length=0x7FFFFFFF))
+seed("urb_tbl_wrap_s32", urb_header(CMD_SUBMIT, transfer_buffer_length=-2147483648))
# Invalid endpoint / direction
seed("urb_ep_15", urb_header(CMD_SUBMIT, ep=15))
@@ -238,7 +247,135 @@ seed("urb_truncated", b"\x00" * 20) # just basic header, no cmd_submi
# ══════════════════════════════════════════════════════════════════════════════
-# 7. Raw boundary seeds
+# 7. vhci-hcd server-side seeds (for fuzz_vhci_server — two-sided fuzzer)
+#
+# Each seed is the 28-byte ret_submit union that fuzz_vhci_server overlays onto
+# USBIP_RET_SUBMIT responses. Fields: status(4) actual_length(4) start_frame(4)
+# number_of_packets(4) error_count(4) + 8 bytes padding.
+# ══════════════════════════════════════════════════════════════════════════════
+
+def ret_submit_fields(status=0, actual_length=0, start_frame=0,
+ number_of_packets=0, error_count=0) -> bytes:
+ return struct.pack(">iiiii", status, actual_length, start_frame,
+ number_of_packets, error_count) + b"\x00" * 8
+
+INT_MIN = -0x80000000
+INT_MAX = 0x7fffffff
+
+# Normal / status variants
+seed("vhci_ret_ok", ret_submit_fields(status=0, actual_length=64))
+seed("vhci_ret_status_error", ret_submit_fields(status=-22)) # -EINVAL
+seed("vhci_ret_status_epipe", ret_submit_fields(status=-32)) # -EPIPE
+seed("vhci_ret_status_enodev", ret_submit_fields(status=-19)) # -ENODEV
+
+# CVE-2016-3955 class: actual_length > transfer_buffer_length
+# fuzz_vhci_server will send this as the server's response to any CMD_SUBMIT
+seed("vhci_actual_len_overflow", ret_submit_fields(actual_length=INT_MAX))
+seed("vhci_actual_len_intmin", ret_submit_fields(actual_length=INT_MIN))
+seed("vhci_actual_len_neg1", ret_submit_fields(actual_length=-1))
+seed("vhci_actual_len_64k", ret_submit_fields(actual_length=65536))
+seed("vhci_actual_len_64k_plus1", ret_submit_fields(actual_length=65537))
+seed("vhci_actual_len_zero", ret_submit_fields(actual_length=0))
+
+# ISO path: number_of_packets > 0 triggers usbip_recv_iso code path
+seed("vhci_iso_one_pkt", ret_submit_fields(actual_length=16, number_of_packets=1))
+seed("vhci_iso_many_pkts", ret_submit_fields(actual_length=0, number_of_packets=255))
+seed("vhci_iso_max_pkts", ret_submit_fields(number_of_packets=INT_MAX))
+seed("vhci_iso_neg_pkts", ret_submit_fields(number_of_packets=-1))
+seed("vhci_iso_overflow", ret_submit_fields(actual_length=INT_MAX, number_of_packets=INT_MAX))
+
+# error_count / start_frame edge cases
+seed("vhci_start_frame_intmin", ret_submit_fields(start_frame=INT_MIN))
+seed("vhci_error_count_max", ret_submit_fields(error_count=INT_MAX))
+seed("vhci_all_ff", b"\xff" * 28)
+seed("vhci_all_zeros", b"\x00" * 28)
+
+# Integer overflow in kmalloc size calculation:
+# usbip_recv_iso: kmalloc(number_of_packets * sizeof(struct usbip_iso_packet_descriptor))
+# sizeof(usbip_iso_packet_descriptor) == 16 bytes on kernel side
+# Overflow threshold: n * 16 wraps u32 when n > 0x0fffffff (268435455)
+# These values cause the allocated buffer to be smaller than the data being received.
+ISO_DESC_SIZE = 16
+WRAP_U32 = (1 << 32) // ISO_DESC_SIZE # 0x10000000 — exactly wraps to 0
+WRAP_U32_PLUS1 = WRAP_U32 + 1 # 0x10000001 — wraps to 16 bytes allocated
+WRAP_S32 = (1 << 31) // ISO_DESC_SIZE # 0x08000000 — wraps signed to negative
+# WRAP_U32=0x10000000: n*16 == 0x100000000 which wraps to 0 in u32 → kmalloc(0)
+# WRAP_U32_PLUS1: n*16 == 0x100000010 which wraps to 16 → 16-byte alloc but n*16 bytes used
+seed("vhci_iso_wrap_u32", ret_submit_fields(number_of_packets=WRAP_U32, actual_length=64))
+seed("vhci_iso_wrap_u32p1", ret_submit_fields(number_of_packets=WRAP_U32_PLUS1, actual_length=64))
+seed("vhci_iso_wrap_s32", ret_submit_fields(number_of_packets=WRAP_S32, actual_length=64))
+
+# Compound: both actual_length overflow AND iso multiplication overflow
+seed("vhci_double_overflow", ret_submit_fields(actual_length=INT_MAX, number_of_packets=WRAP_U32_PLUS1))
+
+# actual_length near UINT_MAX (unsigned interpretation — some code paths treat it unsigned)
+seed("vhci_actual_uint_max", struct.pack(">iIiii", 0, 0xFFFFFFFF, 0, 0, 0) + b"\x00" * 8)
+
+# Borderline: actual_length == transfer_buffer_length (exactly equal — off-by-one test)
+# fuzz_vhci_server always sends the response regardless of the original request's tbl,
+# but these seed values help AFL++ discover the boundary condition faster.
+for tbl in [0, 1, 63, 64, 127, 128, 255, 256, 4095, 4096]:
+ seed(f"vhci_actual_eq_tbl_{tbl}", ret_submit_fields(actual_length=tbl))
+
+# ── ISO overflow: targeted values matching the three unpatched kernel bugs ──────
+#
+# usbip_recv_iso() bug: int size = np * sizeof(*iso) where sizeof(*iso)==16
+# np=0x08000001 → 0x08000001*16 = 0x80000010 → overflows int32 → -2147483632
+# np=0x10000001 → 0x10000001*16 = 0x100000010 → truncated uint32 = 0x10 = 16
+# np=0x7FFFFFFF/16+1 = 0x08000000 → edge case at INT_MAX/16
+#
+# With fuzz_vhci_server's ISO descriptor mirroring, the server sends exactly
+# the overflowed number of bytes and the kernel's loop reads OOB → KASAN hit.
+#
+# Seeds named with explicit overflow semantics for clarity in AFL++ crash reports.
+seed("vhci_iso_np_overflow_08",
+ ret_submit_fields(actual_length=64, number_of_packets=0x08000001))
+seed("vhci_iso_np_overflow_10",
+ ret_submit_fields(actual_length=64, number_of_packets=0x10000001))
+seed("vhci_iso_np_overflow_7f",
+ ret_submit_fields(actual_length=64, number_of_packets=0x7FFFFFFF // 16 + 1))
+
+# usbip_pad_iso() bug: actualoffset underflow → memmove with negative offset
+# Trigger: ISO packet with actual_length > accumulated offset in the packet array.
+# We send actual_length=0 per ISO packet but a large total actual_length header,
+# forcing pad_iso to compute a negative offset for memmove().
+# Represent as: number_of_packets=8 (valid), actual_length=INT_MAX (large total)
+seed("vhci_iso_pad_underflow",
+ ret_submit_fields(actual_length=INT_MAX, number_of_packets=8))
+
+# Per-packet ISO descriptor corruption seeds → usbip_pad_iso() memmove underflow.
+#
+# struct usbip_iso_packet_descriptor layout (16 bytes each):
+# offset(4BE) actual_length(4BE) status(4BE) padding(4BE)
+#
+# When the per-packet actual_length inside the ISO descriptor array is corrupted
+# to a huge value, usbip_pad_iso() computes a negative actualoffset for its
+# memmove(), causing a kernel heap underwrite. These seeds exercise that path
+# directly — prior seeds only corrupted the header-level actual_length field.
+for _np in [1, 2, 4, 8]:
+ for _bad in [0x7FFFFFFF, 0x80000000, 0xFFFFFFFF]:
+ _iso_desc = struct.pack(">II", 0, _bad) + b"\x00" * 8 # 16 bytes
+ seed(f"vhci_iso_pkt_corrupt_np{_np}_{hex(_bad)}",
+ ret_submit_fields(actual_length=0, number_of_packets=_np) + _iso_desc * _np)
+
+# Stub-side sgl_alloc() boundary seeds (for fuzz_stub_client / corpus/stub/).
+#
+# stub_recv_cmd_submit() calls sgl_alloc(transfer_buffer_length) with no upper
+# bound check. Values at and above SG_MAX_SINGLE_ALLOC (128 pages = 512 KB on
+# x86) exercise the fallback scatter-gather allocation path. IN transfers
+# (direction=1) trigger the alloc without needing the client to send payload.
+for _tbl in [0x80000, 0x1000000, 0x40000000, 0x7FFFFFFF, -1]:
+ seed(f"stub_tbl_in_{hex(_tbl & 0xFFFFFFFF)}",
+ urb_header(CMD_SUBMIT, direction=1, transfer_buffer_length=_tbl))
+
+# OUT direction: stub also allocates for the receive path, so exercise that too.
+for _tbl in [0x80000, 0x1000000]:
+ seed(f"stub_tbl_out_{hex(_tbl)}",
+ urb_header(CMD_SUBMIT, direction=0, transfer_buffer_length=_tbl))
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# 8. Raw boundary seeds
# ══════════════════════════════════════════════════════════════════════════════
seed("raw_empty", b"")
@@ -249,7 +386,39 @@ seed("raw_max_size", b"\x01\x11\x80\x05\x00\x00\x00\x00" + b"\xde\xad
# ══════════════════════════════════════════════════════════════════════════════
-# Done
+# Per-harness subdirs
+#
+# run-fuzzers.sh points each AFL++ instance at a subdirectory that contains
+# only the seeds meaningful for that harness. This keeps calibration fast and
+# map density high from the start. Cross-pollination between instances still
+# happens through the shared output/ directory.
# ══════════════════════════════════════════════════════════════════════════════
+import shutil
+
+SUBDIRS = {
+ "protocol": lambda n, _: n.startswith("op_common") or n.startswith("raw_8bytes"),
+ "devlist": lambda n, _: "devlist" in n,
+ "import": lambda n, _: "import" in n,
+ "urb": lambda n, _: "urb" in n or "cmd_" in n or "ret_" in n,
+ "vhci": lambda n, _: n.startswith("vhci_"),
+ # stub seeds: CMD_SUBMIT headers for fuzz_stub_client
+ # Uses URB submit seeds as the base (same 48-byte CMD_SUBMIT format)
+ "stub": lambda n, _: "urb_submit" in n or "urb_iso" in n or "urb_tbl" in n
+ or "urb_ep" in n or "urb_dir" in n or "urb_setup" in n
+ or "urb_all" in n or "urb_ret_len" in n
+ or n.startswith("stub_tbl_"),
+}
+
+for subdir, predicate in SUBDIRS.items():
+ dest = os.path.join(OUT, subdir)
+ os.makedirs(dest, exist_ok=True)
+ count = 0
+ for fname in os.listdir(OUT):
+ src = os.path.join(OUT, fname)
+ if os.path.isfile(src) and predicate(fname, src):
+ shutil.copy2(src, os.path.join(dest, fname))
+ count += 1
+ print(f"[+] corpus/{subdir}/: {count} seeds")
+
print(f"[+] Generated {written} seeds in {OUT}/")
diff --git a/fuzzing/usbip/kcov_shim.c b/fuzzing/usbip/kcov_shim.c
@@ -0,0 +1,278 @@
+/*
+ * kcov_shim.c — KCOV-backed AFL++ coverage shim for QEMU-mode kernel fuzzing.
+ *
+ * WHAT THIS DOES
+ * ───────────────
+ * This binary runs INSIDE the QEMU VM. AFL++ runs on the HOST. The two
+ * communicate over a virtio-serial channel (or a pipe if using netns mode).
+ *
+ * Per iteration:
+ * 1. AFL++ writes the test case bytes over the serial link.
+ * 2. This shim reads them, enables KCOV, sends the bytes to usbipd via TCP.
+ * 3. usbipd processes them → kernel code executes → KCOV records PC trace.
+ * 4. This shim reads the KCOV buffer, hashes consecutive (PC[i], PC[i+1])
+ * pairs as coverage "edges", and writes the edge bitmap back to AFL++ over
+ * the serial link.
+ * 5. AFL++ uses the edge bitmap to guide mutation (same as in-process mode
+ * but the coverage comes from the kernel, not userspace).
+ *
+ * CHANNEL PROTOCOL (over virtio-serial)
+ * ───────────────────────────────────────
+ * HOST→GUEST: [len:4 LE][payload:len] (test case)
+ * GUEST→HOST: [bitmap:MAP_SIZE] (AFL++ shared map, 64 KB)
+ * GUEST→HOST: [crash:1] (0x00 = ok, 0x01 = crash detected)
+ *
+ * MAP_SIZE = 65536 (matches AFL++ default __AFL_SHM_FUZZ_LEN)
+ *
+ * QEMU INVOCATION (add to qemu-system-x86_64 command line)
+ * ─────────────────────────────────────────────────────────
+ * -chardev socket,id=kcov,path=/tmp/kcov.sock,server=on,wait=off \
+ * -device virtio-serial \
+ * -device virtserialport,chardev=kcov,name=kcov0
+ *
+ * # Inside VM /init: exec kcov_shim /dev/vport0p1 127.0.0.1 3240
+ *
+ * HOST SIDE
+ * ─────────
+ * afl-fuzz ... -- ./kcov_host_relay /tmp/kcov.sock @@
+ * (kcov_host_relay is a thin wrapper that reads @@ and writes to the socket)
+ *
+ * COMPILE (inside VM, or cross-compile)
+ * ───────────────────────────────────────
+ * gcc -O2 -static -o kcov_shim kcov_shim.c
+ *
+ * KERNEL CONFIG REQUIREMENTS
+ * ───────────────────────────
+ * CONFIG_KCOV=y
+ * CONFIG_KCOV_ENABLE_COMPARISONS=y
+ * CONFIG_DEBUG_FS=y (debugfs must be mounted at /sys/kernel/debug)
+ *
+ * NOTE: KCOV covers code running in task context for the current process.
+ * The TCP receive path and usbip parsing run in task context (kthread or
+ * workqueue bound to the usbipd process's fd) and WILL be covered.
+ * IRQ and softirq handlers will NOT be covered (KCOV limitation).
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <time.h>
+#include <unistd.h>
+
+/* ── KCOV kernel ABI ────────────────────────────────────────────────────────── */
+/* From linux/kcov.h (reproduced here to avoid kernel header dependency) */
+#define KCOV_INIT_TRACE _IOR('c', 1, unsigned long)
+#define KCOV_ENABLE _IO('c', 100)
+#define KCOV_DISABLE _IO('c', 101)
+#define KCOV_TRACE_PC 0
+
+#define KCOV_ENTRIES (64 * 1024) /* 64K entries = 512 KB mmap */
+#define KCOV_PATH "/sys/kernel/debug/kcov"
+
+/* ── AFL++ shared map ───────────────────────────────────────────────────────── */
+#define MAP_SIZE (1 << 16) /* 64 KB — AFL++ default */
+
+/* ── protocol sizes ─────────────────────────────────────────────────────────── */
+#define MAX_INPUT (1 << 16) /* 64 KB max test case */
+
+/* ── globals ─────────────────────────────────────────────────────────────────── */
+static uint8_t afl_map[MAP_SIZE]; /* coverage edge bitmap */
+static uint8_t ibuf[MAX_INPUT]; /* incoming test case */
+static uint64_t *kcov_buf; /* mmap of KCOV trace buffer */
+static int kcov_fd = -1;
+
+/* ── KCOV helpers ────────────────────────────────────────────────────────────── */
+
+static int kcov_init(void)
+{
+ kcov_fd = open(KCOV_PATH, O_RDWR);
+ if (kcov_fd < 0) {
+ perror("open " KCOV_PATH);
+ return -1;
+ }
+
+ if (ioctl(kcov_fd, KCOV_INIT_TRACE, KCOV_ENTRIES) < 0) {
+ perror("ioctl KCOV_INIT_TRACE");
+ return -1;
+ }
+
+ kcov_buf = mmap(NULL, KCOV_ENTRIES * sizeof(uint64_t),
+ PROT_READ | PROT_WRITE, MAP_SHARED, kcov_fd, 0);
+ if (kcov_buf == MAP_FAILED) {
+ perror("mmap kcov");
+ return -1;
+ }
+
+ return 0;
+}
+
+static void kcov_reset_and_enable(void)
+{
+ kcov_buf[0] = 0; /* reset count */
+ ioctl(kcov_fd, KCOV_ENABLE, KCOV_TRACE_PC);
+}
+
+static void kcov_disable_and_harvest(void)
+{
+ ioctl(kcov_fd, KCOV_DISABLE, 0);
+
+ uint64_t count = kcov_buf[0];
+ if (count > (uint64_t)(KCOV_ENTRIES - 1))
+ count = KCOV_ENTRIES - 1;
+
+ /* Hash consecutive PC pairs as edge coverage.
+ * AFL++ treats (src, dst) pairs as edges in the control-flow graph.
+ * We use a simple hash: edge = ((pc[i] >> 1) ^ pc[i+1]) % MAP_SIZE
+ * This is the same scheme used by honggfuzz and oracle/kernel-fuzzing. */
+ for (uint64_t i = 0; i + 1 < count; i++) {
+ uint64_t a = kcov_buf[1 + i];
+ uint64_t b = kcov_buf[1 + i + 1];
+ uint32_t edge = (uint32_t)(((a >> 1) ^ b) % MAP_SIZE);
+ if (afl_map[edge] < 255)
+ afl_map[edge]++;
+ }
+}
+
+/* ── TCP helpers ─────────────────────────────────────────────────────────────── */
+
+static int recv_all(int fd, void *buf, size_t len)
+{
+ size_t done = 0;
+ while (done < len) {
+ ssize_t r = recv(fd, (char *)buf + done, len - done, 0);
+ if (r <= 0) return -1;
+ done += (size_t)r;
+ }
+ return 0;
+}
+
+static int send_all_fd(int fd, const void *buf, size_t len)
+{
+ size_t done = 0;
+ while (done < len) {
+ ssize_t r = write(fd, (const char *)buf + done, len - done);
+ if (r <= 0) return -1;
+ done += (size_t)r;
+ }
+ return 0;
+}
+
+static int tcp_connect(const char *host, int port)
+{
+ int sock = socket(AF_INET, SOCK_STREAM, 0);
+ if (sock < 0) return -1;
+
+ struct sockaddr_in sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sin_family = AF_INET;
+ sa.sin_port = htons((uint16_t)port);
+ inet_pton(AF_INET, host, &sa.sin_addr);
+
+ int one = 1;
+ setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
+ struct timeval tv = { .tv_sec = 0, .tv_usec = 200000 }; /* 200 ms */
+ setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
+ if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
+ close(sock);
+ return -1;
+ }
+ return sock;
+}
+
+/* ── main loop ───────────────────────────────────────────────────────────────── */
+
+int main(int argc, char *argv[])
+{
+ if (argc < 4) {
+ fprintf(stderr,
+ "usage: %s <serial-dev> <usbipd-host> <usbipd-port>\n"
+ " serial-dev — virtio-serial device (e.g. /dev/vport0p1)\n"
+ " usbipd-host — usbipd address (usually 127.0.0.1)\n"
+ " usbipd-port — usbipd port (usually 3240)\n",
+ argv[0]);
+ return 1;
+ }
+
+ const char *serial_dev = argv[1];
+ const char *usbipd_host = argv[2];
+ int usbipd_port = atoi(argv[3]);
+
+ signal(SIGPIPE, SIG_IGN);
+
+ /* Open virtio-serial channel to host */
+ int serial = open(serial_dev, O_RDWR);
+ if (serial < 0) {
+ perror("open serial");
+ return 1;
+ }
+
+ /* Init KCOV */
+ if (kcov_init() < 0)
+ return 1;
+
+ fprintf(stderr, "[kcov_shim] ready on %s, targeting %s:%d\n",
+ serial_dev, usbipd_host, usbipd_port);
+
+ /* Main loop: one iteration per AFL++ test case */
+ for (;;) {
+ /* ── receive test case from host (via virtio-serial) ── */
+ uint32_t len_le;
+ if (recv_all(serial, &len_le, 4) < 0)
+ break;
+ uint32_t ilen = le32toh(len_le);
+ if (ilen == 0 || ilen > MAX_INPUT)
+ ilen = 0;
+
+ if (ilen > 0 && recv_all(serial, ibuf, ilen) < 0)
+ break;
+
+ /* ── reset edge map for this iteration ── */
+ memset(afl_map, 0, MAP_SIZE);
+
+ uint8_t crash = 0;
+
+ if (ilen > 0) {
+ /* ── enable KCOV ── */
+ kcov_reset_and_enable();
+
+ /* ── send payload to usbipd via TCP ── */
+ int sock = tcp_connect(usbipd_host, usbipd_port);
+ if (sock >= 0) {
+ /* Send; ignore errors — crash detection is via KCOV + exit */
+ send(sock, ibuf, ilen, MSG_NOSIGNAL);
+
+ /* Wait briefly for response (or connection close) */
+ uint8_t rbuf[256];
+ ssize_t r = recv(sock, rbuf, sizeof(rbuf), 0);
+ if (r < 0 && (errno == ECONNRESET || errno == EPIPE))
+ crash = 1;
+ close(sock);
+ }
+
+ /* ── disable KCOV and harvest coverage ── */
+ kcov_disable_and_harvest();
+ }
+
+ /* ── send bitmap + crash byte back to AFL++ ── */
+ if (send_all_fd(serial, afl_map, MAP_SIZE) < 0)
+ break;
+ if (send_all_fd(serial, &crash, 1) < 0)
+ break;
+ }
+
+ close(serial);
+ return 0;
+}
diff --git a/fuzzing/usbip/mock_syscalls.c b/fuzzing/usbip/mock_syscalls.c
@@ -25,6 +25,14 @@ size_t g_fuzz_offset;
unsigned long usbip_debug_flag = 0;
/*
+ * udev_context — global udev handle owned by usbip_host_common.c which we
+ * don't compile for the fuzz build. Provide a NULL stub; the network
+ * parsing paths we target never call udev_device_new_from_subsystem_sysname.
+ */
+struct udev;
+struct udev *udev_context = NULL;
+
+/*
* __wrap_recv — intercepts every recv() call from the usbip objects.
* Serves data from g_fuzz_buf in order; returns 0 (EOF) when exhausted.
*
@@ -62,11 +70,19 @@ ssize_t __wrap_send(int fd, const void *buf, size_t len, int flags)
/* Some usbip code paths use write() for logging; silence those too */
ssize_t __wrap_write(int fd, const void *buf, size_t len)
{
- /* Allow writes to stderr (fd 2) for ASAN/abort messages */
- if (fd == 2) {
- extern ssize_t __real_write(int, const void *, size_t);
+ extern ssize_t __real_write(int, const void *, size_t);
+ /*
+ * Pass through:
+ * fd 2 — stderr (ASAN/abort messages need to reach the terminal)
+ * fd 198 — AFL++ forkserver control pipe (FORKSRV_FD)
+ * fd 199 — AFL++ forkserver status pipe (FORKSRV_FD+1)
+ *
+ * AFL++ runtime calls write(199, hello, 4) to hand-shake with afl-fuzz.
+ * Without this pass-through the hello is silently discarded and AFL++
+ * deadlocks waiting for the forkserver to become ready.
+ */
+ if (fd == 2 || fd == 198 || fd == 199)
return __real_write(fd, buf, len);
- }
(void)buf;
return (ssize_t)len;
}
diff --git a/fuzzing/usbip/net_send.c b/fuzzing/usbip/net_send.c
@@ -31,6 +31,7 @@
#include <time.h>
#include <sys/socket.h>
#include <sys/stat.h>
+#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
@@ -94,6 +95,13 @@ int main(int argc, char *argv[])
int one = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
+ /* recv timeout: 2 s — prevents blocking forever when usbipd
+ * reads the fuzz bytes but never replies (which is normal behaviour
+ * for most malformed packets). Without this every calibration seed
+ * times out and AFL++ aborts with "All test cases time out". */
+ struct timeval tv = { .tv_sec = 2, .tv_usec = 0 }; /* 2 s */
+ setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0)
break;
diff --git a/fuzzing/usbip/qemu/init b/fuzzing/usbip/qemu/init
@@ -21,9 +21,17 @@ mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
mount -t tmpfs tmpfs /tmp
+# debugfs: required for KCOV (/sys/kernel/debug/kcov)
+mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null || true
+
# suppress most kernel messages on the console
echo 1 > /proc/sys/kernel/printk
+# KASAN: make KASAN reports immediately panic (and reboot) rather than
+# continuing with corrupted state. AFL++ detects the reboot as ECONNRESET.
+echo 1 > /proc/sys/kernel/panic_on_oops
+echo 1 > /proc/sys/kernel/panic
+
# ── networking ─────────────────────────────────────────────────────────────
# QEMU user-mode networking gives us 10.0.2.15/24, gateway 10.0.2.2
ip link set lo up
diff --git a/fuzzing/usbip/qemu/init-vhci b/fuzzing/usbip/qemu/init-vhci
@@ -0,0 +1,83 @@
+#!/bin/sh
+#
+# /init-vhci — initramfs init for vhci-hcd client-mode fuzzing VM.
+#
+# Unlike qemu/init (which runs usbipd as a server), this init loads vhci-hcd
+# as a CLIENT and continuously tries to attach to a remote device. The remote
+# "device" is fuzz_vhci_server running on the host, which sends malformed
+# USBIP_RET_SUBMIT responses to trigger bugs in vhci-hcd.
+#
+# This is the attack surface for CVE-2016-3955 class bugs:
+# - actual_length > transfer_buffer_length → heap overflow in usbip_recv_xbuff
+# - ISO descriptor mismatches → overflow in usbip_recv_iso
+# - status / number_of_packets edge cases
+#
+# QEMU invocation (from the host):
+# qemu-system-x86_64 \
+# -kernel bzImage \
+# -initrd initramfs-vhci.cpio.gz \
+# -nographic -append "console=ttyS0 quiet" \
+# -m 512M \
+# -net nic,model=e1000 -net user
+#
+# The host's fuzz_vhci_server listens on 0.0.0.0:13241.
+# QEMU user-mode networking: host is reachable at 10.0.2.2 from inside the VM.
+
+set -e
+
+# ── virtual filesystems ────────────────────────────────────────────────────────
+mount -t proc proc /proc
+mount -t sysfs sysfs /sys
+mount -t devtmpfs devtmpfs /dev
+mkdir -p /dev/pts
+mount -t devpts devpts /dev/pts
+mount -t tmpfs tmpfs /tmp
+
+# debugfs: required for KCOV (/sys/kernel/debug/kcov)
+mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null || true
+
+# suppress most kernel messages on the console
+echo 1 > /proc/sys/kernel/printk
+
+# KASAN: panic immediately on memory error so AFL++ detects it via ECONNRESET
+echo 1 > /proc/sys/kernel/panic_on_oops
+echo 1 > /proc/sys/kernel/panic
+
+# ── networking ─────────────────────────────────────────────────────────────────
+# QEMU user-mode networking gives us 10.0.2.15/24, gateway 10.0.2.2
+ip link set lo up
+ip link set eth0 up
+ip addr add 10.0.2.15/24 dev eth0
+ip route add default via 10.0.2.2 dev eth0
+
+# ── USB/IP client kernel modules ───────────────────────────────────────────────
+modprobe usbip-core 2>/dev/null || true
+modprobe vhci-hcd 2>/dev/null || true
+# Note: do NOT load usbip-host — we're only the client here
+
+# ── usbip attach watchdog ──────────────────────────────────────────────────────
+# Continuously tries to attach to the host's fuzz_vhci_server.
+# Host address: 10.0.2.2 (QEMU user-mode networking gateway)
+# Port: 13241 (fuzz_vhci_server's listening port)
+# Busid: 1-1 (matches the device in fuzz_vhci_server's OP_REP_IMPORT response)
+#
+# When fuzz_vhci_server closes the connection (normal iteration end), the attach
+# fails and the watchdog immediately retries — connecting to the next AFL++
+# iteration of fuzz_vhci_server.
+#
+# When vhci-hcd causes a KASAN report → kernel panic → VM reboots → fuzz_vhci_server
+# detects ECONNRESET → exits 1 → AFL++ records a crash.
+
+HOST="10.0.2.2"
+PORT="13241"
+BUSID="1-1"
+
+echo "usbip-fuzz: starting vhci attach watchdog (${HOST}:${PORT} busid=${BUSID})"
+
+while true; do
+ usbip attach -r "${HOST}" -p "${PORT}" -b "${BUSID}" 2>/dev/null || true
+ # Brief pause to avoid hammering if the host server isn't ready yet
+ sleep 0
+done
+
+# unreachable
diff --git a/fuzzing/usbip/qemu/kernel.config b/fuzzing/usbip/qemu/kernel.config
@@ -31,7 +31,9 @@ CONFIG_POSIX_TIMERS=y
# ── memory ───────────────────────────────────────────────────────────────
CONFIG_MEMORY_ISOLATION=n
CONFIG_SLUB=y
-CONFIG_SLAB_MERGE_DEFAULT=y
+# KASAN requires slab merging disabled — merged slabs collapse object types
+# together, destroying redzone separation and defeating heap corruption detection
+CONFIG_SLAB_MERGE_DEFAULT=n
# ── networking ───────────────────────────────────────────────────────────
CONFIG_NET=y
@@ -112,7 +114,39 @@ CONFIG_WIRELESS=n
CONFIG_RFKILL=n
CONFIG_CRYPTO_HW=n
+# ── sanitizers (KASAN, KCOV, UBSAN) ─────────────────────────────────────
+# KASAN: catches heap overflows, UAF, OOB reads in kernel code.
+# Without this, usbip_recv_xbuff / usbip_recv_iso heap overflows are invisible.
+# Overhead: ~2x slowdown, 8x extra RAM for shadow memory. Increase -m to 512M.
+CONFIG_KASAN=y
+CONFIG_KASAN_GENERIC=y
+CONFIG_KASAN_OUTLINE=y # outline = smaller code, faster compile, same detection
+CONFIG_KASAN_TAGS=n
+
+# UBSAN: catches undefined behaviour (signed overflow, out-of-bounds, etc.)
+CONFIG_UBSAN=y
+CONFIG_UBSAN_BOUNDS=y
+CONFIG_UBSAN_SANITIZE_ALL=y
+
+# DEBUG_LIST: catches list corruption (double-free via list_del poisoning)
+CONFIG_DEBUG_LIST=y
+
+# KCOV: exports kernel PC coverage to userspace via /sys/kernel/debug/kcov.
+# Needed for the KCOV shim that feeds kernel coverage into AFL++ bitmap.
+CONFIG_KCOV=y
+CONFIG_KCOV_ENABLE_COMPARISONS=y
+
+# DEBUGFS: required by KCOV (mounts at /sys/kernel/debug)
+CONFIG_DEBUG_FS=y
+
+# Panic on oops: KASAN reports are oopses. This makes them immediately reboot
+# the VM, which AFL++ detects as ECONNRESET → crash. Without this, a KASAN
+# report prints and execution continues with corrupted state.
+CONFIG_PANIC_ON_OOPS=y
+
# ── size optimization ────────────────────────────────────────────────────
-CONFIG_CC_OPTIMIZE_FOR_SIZE=y
+# NOTE: CC_OPTIMIZE_FOR_SIZE is intentionally disabled with KASAN.
+# KASAN adds heavy instrumentation that conflicts with aggressive size opts.
+CONFIG_CC_OPTIMIZE_FOR_SIZE=n
CONFIG_KERNEL_XZ=n
CONFIG_KERNEL_GZIP=y
diff --git a/fuzzing/usbip/run-fuzzers.sh b/fuzzing/usbip/run-fuzzers.sh
@@ -1,14 +1,25 @@
#!/bin/bash
# run-fuzzers.sh — launch the usbipd fuzzing environment in a tmux session.
#
-# Architecture:
-# Pane 1 (master) — AFL++ master, in-process fuzzing of fuzz_protocol.
-# High throughput (~50k exec/s), direct crash detection.
-# Pane 2 (worker) — AFL++ secondary, sends mutated packets to usbipd running
-# inside the QEMU VM via net_send. Catches kernel-level bugs.
+# Layout (2×2 tmux grid):
+# top-left (master) — AFL++ master, in-process fuzzing of fuzz_protocol.
+# Exercises the op_common header dispatcher + reply body
+# parsers. CmpLog enabled on master only.
+# top-right (urb) — AFL++ secondary, in-process fuzzing of fuzz_urb.
+# Exercises the URB submission / unlink path — the area
+# where CVE-2016-3955 class (transfer-length overflow) bugs
+# live.
+# bot-left (devlist) — AFL++ secondary, in-process fuzzing of fuzz_devlist.
+# Covers the ndev multiplication overflow and devlist body
+# parsing paths not reached by fuzz_protocol.
+# bot-right (status) — afl-whatsup live status (refreshes every 10 s).
#
-# Both instances share the same output/ directory for corpus cross-pollination.
-# The QEMU VM boots from bzImage + initramfs.cpio.gz before the worker starts.
+# CmpLog is enabled only on the master to reduce resource usage
+# (was 8 processes = 4 fuzzers × 2 CmpLog; now 4 = 3 fuzzers + 1 CmpLog).
+# All instances share output/ for corpus cross-pollination.
+#
+# QEMU vhci fuzzer (fuzz_vhci_server) must be started separately:
+# See the "QEMU fuzzer" section at the bottom of this script for exact commands.
#
# Prerequisites: setup.sh + build_fuzzers.sh must have completed.
#
@@ -26,14 +37,14 @@ AFL_BIN="${SCRIPT_DIR}/afl-build/usr/local/bin"
[[ -d "${AFL_BIN}" ]] && export PATH="${AFL_BIN}:${PATH}"
SESSION="usbip-fuzz"
-QEMU_HOST_PORT=13240 # host port forwarded to VM's port 3240
-QEMU_MEMORY=256 # MB
-QEMU_PID_FILE="${SCRIPT_DIR}/.qemu.pid"
OUTPUT_DIR="${SCRIPT_DIR}/output"
CORPUS_DIR="${SCRIPT_DIR}/corpus"
+CORPUS_PROTOCOL="${CORPUS_DIR}/protocol"
+CORPUS_URB="${CORPUS_DIR}/urb"
DICT="${SCRIPT_DIR}/dictionaries/usbip.dict"
-TIMEOUT_MASTER=1000 # ms per execution (in-process)
-TIMEOUT_WORKER=3000 # ms per execution (network round-trip)
+CORPUS_DEVLIST="${CORPUS_DIR}/devlist"
+TIMEOUT=1000 # ms per execution (in-process, all instances)
+NICE=10 # niceness for afl-fuzz processes (0=normal, 19=lowest priority)
log() { echo "[*] $*"; }
ok() { echo "[+] $*"; }
@@ -48,8 +59,6 @@ case "${1:-}" in
exit 0 ;;
--rerun)
tmux kill-session -t "${SESSION}" 2>/dev/null || true
- [[ -f "${QEMU_PID_FILE}" ]] && kill "$(cat "${QEMU_PID_FILE}")" 2>/dev/null || true
- rm -f "${QEMU_PID_FILE}"
;;
"") ;;
*) die "Unknown option: ${1}. Use --rerun or --status." ;;
@@ -57,12 +66,15 @@ esac
# ── sanity checks ─────────────────────────────────────────────────────────────
command -v afl-fuzz > /dev/null 2>&1 || die "afl-fuzz not found."
-[[ -f "fuzz_protocol" ]] || die "fuzz_protocol not found. Run build_fuzzers.sh first."
+[[ -f "fuzz_protocol" ]] || die "fuzz_protocol not found. Run build_fuzzers.sh first."
[[ -f "fuzz_protocol.cmplog" ]] || die "fuzz_protocol.cmplog not found."
-[[ -f "net_send" ]] || die "net_send not found. Run setup.sh first."
-[[ -f "bzImage" ]] || die "bzImage not found. Run setup.sh first."
-[[ -f "initramfs.cpio.gz" ]] || die "initramfs.cpio.gz not found."
-[[ -d "${CORPUS_DIR}" ]] || die "corpus/ not found. Run setup.sh first."
+[[ -f "fuzz_urb" ]] || die "fuzz_urb not found. Run build_fuzzers.sh first."
+[[ -f "fuzz_urb.cmplog" ]] || die "fuzz_urb.cmplog not found."
+[[ -f "fuzz_devlist" ]] || die "fuzz_devlist not found. Run build_fuzzers.sh first."
+[[ -f "fuzz_devlist.cmplog" ]] || die "fuzz_devlist.cmplog not found."
+[[ -d "${CORPUS_PROTOCOL}" ]] || die "corpus/protocol/ not found. Re-run gen_corpus.py first."
+[[ -d "${CORPUS_URB}" ]] || die "corpus/urb/ not found. Re-run gen_corpus.py first."
+[[ -d "${CORPUS_DEVLIST}" ]] || die "corpus/devlist/ not found. Re-run gen_corpus.py first."
tmux list-sessions 2>/dev/null | grep -q "${SESSION}" && \
die "Session '${SESSION}' already running. Use --rerun to restart."
@@ -72,74 +84,120 @@ echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governo
mkdir -p "${OUTPUT_DIR}"
-# ── launch QEMU VM for the worker pane ────────────────────────────────────────
-log "Starting QEMU VM (usbipd on guest port 3240 → host port ${QEMU_HOST_PORT})..."
-
-QEMU_KVM=""
-[[ -c /dev/kvm ]] && QEMU_KVM="-enable-kvm -cpu host" && log "KVM available — using hardware acceleration."
-
-qemu-system-x86_64 \
- ${QEMU_KVM} \
- -m ${QEMU_MEMORY}M \
- -smp 1 \
- -kernel bzImage \
- -initrd initramfs.cpio.gz \
- -nographic \
- -append "console=ttyS0 rdinit=/init quiet loglevel=0" \
- -netdev "user,id=n0,hostfwd=tcp::${QEMU_HOST_PORT}-:3240" \
- -device e1000,netdev=n0 \
- -no-reboot \
- -daemonize \
- -pidfile "${QEMU_PID_FILE}"
-
-ok "QEMU started (pid: $(cat "${QEMU_PID_FILE}"))"
-
-# Wait for usbipd to accept connections (max 10 s)
-log "Waiting for usbipd to come up..."
-for i in $(seq 1 20); do
- if nc -z 127.0.0.1 "${QEMU_HOST_PORT}" 2>/dev/null; then
- ok "usbipd is reachable on port ${QEMU_HOST_PORT}."
- break
- fi
- sleep 0.5
-done
-
-# ── create tmux session ───────────────────────────────────────────────────────
+# ── create tmux session (2×2 grid) ────────────────────────────────────────────
log "Creating tmux session '${SESSION}'..."
tmux new-session -d -s "${SESSION}" -x 220 -y 50
-# ── Pane 1: AFL++ master (in-process) ─────────────────────────────────────────
-log "Launching AFL++ master (in-process fuzz_protocol)..."
+# Build 2×2 layout:
+# split the window into left/right halves, then split each half top/bottom.
+# Result: 0.0 (top-left) │ 0.1 (top-right)
+# 0.2 (bot-left) │ 0.3 (bot-right)
+tmux split-window -h -t "${SESSION}:0"
+tmux split-window -v -t "${SESSION}:0.0"
+tmux split-window -v -t "${SESSION}:0.1"
+
+# ── Pane 0.0 (top-left): AFL++ master — fuzz_protocol ─────────────────────────
+log "Launching AFL++ master (fuzz_protocol)..."
tmux send-keys -t "${SESSION}:0.0" \
- "AFL_SKIP_CPUFREQ=1 afl-fuzz \
+ "AFL_SKIP_CPUFREQ=1 AFL_AUTORESUME=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 \
+ ASAN_OPTIONS=abort_on_error=1:symbolize=0:detect_leaks=0 \
+ nice -n ${NICE} afl-fuzz \
-M master \
- -i ${CORPUS_DIR} \
+ -i ${CORPUS_PROTOCOL} \
-o ${OUTPUT_DIR} \
-x ${DICT} \
-c ./fuzz_protocol.cmplog \
- -t ${TIMEOUT_MASTER} \
- -- ./fuzz_protocol @@" \
+ -t ${TIMEOUT} \
+ -p fast \
+ -m none \
+ -- ./fuzz_protocol" \
Enter
-# ── Pane 2: AFL++ worker (QEMU network mode) ──────────────────────────────────
-log "Launching AFL++ worker (QEMU network via net_send)..."
-tmux split-window -v -t "${SESSION}:0"
+# ── Pane 0.1 (top-right): AFL++ secondary — fuzz_urb ──────────────────────────
+log "Launching AFL++ secondary (fuzz_urb — URB submission path)..."
tmux send-keys -t "${SESSION}:0.1" \
- "AFL_SKIP_CPUFREQ=1 afl-fuzz \
- -S worker \
- -i ${CORPUS_DIR} \
+ "AFL_SKIP_CPUFREQ=1 AFL_AUTORESUME=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 \
+ ASAN_OPTIONS=abort_on_error=1:symbolize=0:detect_leaks=0 \
+ nice -n ${NICE} afl-fuzz \
+ -S urb \
+ -i ${CORPUS_URB} \
+ -o ${OUTPUT_DIR} \
+ -x ${DICT} \
+ -t ${TIMEOUT} \
+ -p fast \
+ -m none \
+ -- ./fuzz_urb" \
+ Enter
+
+# ── Pane 0.2 (bot-left): AFL++ secondary — fuzz_devlist ───────────────────────
+log "Launching AFL++ secondary (fuzz_devlist — devlist ndev overflow)..."
+tmux send-keys -t "${SESSION}:0.2" \
+ "AFL_SKIP_CPUFREQ=1 AFL_AUTORESUME=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 \
+ ASAN_OPTIONS=abort_on_error=1:symbolize=0:detect_leaks=0 \
+ nice -n ${NICE} afl-fuzz \
+ -S devlist \
+ -i ${CORPUS_DEVLIST} \
-o ${OUTPUT_DIR} \
-x ${DICT} \
- -t ${TIMEOUT_WORKER} \
- -- ./net_send @@ 127.0.0.1 ${QEMU_HOST_PORT}" \
+ -t ${TIMEOUT} \
+ -p fast \
+ -m none \
+ -- ./fuzz_devlist" \
+ Enter
+
+# ── Pane 0.3 (bot-right): afl-whatsup live status ────────────────────────────
+log "Adding status pane (afl-whatsup)..."
+tmux send-keys -t "${SESSION}:0.3" \
+ "watch -n 10 '$(command -v afl-whatsup) ${OUTPUT_DIR} 2>/dev/null || echo \"output dir: ${OUTPUT_DIR}\"'" \
Enter
+# ── Window 3: QEMU vhci fuzzer instructions ───────────────────────────────────
+#
+# fuzz_vhci_server is the ONLY harness that exercises kernel memory (vhci-hcd).
+# It requires a running QEMU VM. Launch it manually:
+#
+# STEP 1 — start the QEMU VM (in a separate terminal or tmux pane):
+# qemu-system-x86_64 \
+# -kernel qemu/bzImage \
+# -initrd qemu/initramfs-vhci.cpio.gz \
+# -nographic \
+# -append "console=ttyS0 quiet panic=-1 oops=panic kasan_multi_shot" \
+# -m 512M \
+# -net nic,model=e1000 -net user,hostfwd=tcp:127.0.0.1:13241-:3240
+#
+# STEP 2 — once the VM boots and the watchdog starts, launch AFL++:
+# afl-fuzz -S vhci \
+# -i corpus/vhci \
+# -o output \
+# -t 5000 \
+# -x dictionaries/usbip.dict \
+# -- ./fuzz_vhci_server @@ 0.0.0.0 13241
+#
+# Enumeration check: in the VM, `lsusb` should show the device and
+# `dmesg | grep snd_usb_audio` should confirm driver binding.
+# ISO URBs: `dmesg | grep usbip` should show EP1 submissions.
+# Crash detection: KASAN + panic_on_oops=1 → VM reboots → ECONNRESET →
+# fuzz_vhci_server exits 1 → AFL++ records crash in output/vhci/crashes/.
+#
+log "Adding QEMU vhci fuzzer instructions window..."
+tmux new-window -t "${SESSION}" -n vhci-howto
+tmux send-keys -t "${SESSION}:vhci-howto" \
+ "echo '=== QEMU vhci fuzzer ===' && \
+ echo 'Step 1: start QEMU VM (see run-fuzzers.sh comments for full command)' && \
+ echo 'Step 2: afl-fuzz -S vhci -i corpus/vhci -o output -t 5000 -- ./fuzz_vhci_server @@ 0.0.0.0 13241'" \
+ Enter
+
+tmux select-window -t "${SESSION}:0"
+
# ── attach ────────────────────────────────────────────────────────────────────
ok "Fuzzing session started."
echo ""
echo " Attach: tmux attach -t ${SESSION}"
echo " Status: ${0} --status"
echo " Crashes: ${OUTPUT_DIR}/master/crashes/"
+echo " ${OUTPUT_DIR}/urb/crashes/"
+echo " ${OUTPUT_DIR}/devlist/crashes/"
+echo " QEMU vhci: see window 'vhci-howto' in tmux session (kernel fuzzing)"
echo " Stop: bash cleanup.sh"
echo ""
diff --git a/fuzzing/usbip/setup.sh b/fuzzing/usbip/setup.sh
@@ -27,8 +27,10 @@ BUSYBOX_VER="${BUSYBOX_VER:-1.36.1}"
AFL_REPO="https://github.com/AFLplusplus/AFLplusplus.git"
SKIP_KERNEL=0
+SKIP_DEPS=0
for arg in "$@"; do
[[ "$arg" == "--no-kernel" ]] && SKIP_KERNEL=1
+ [[ "$arg" == "--skip-deps" ]] && SKIP_DEPS=1
done
log() { echo "[*] $*"; }
@@ -36,19 +38,23 @@ ok() { echo "[+] $*"; }
warn() { echo "[!] $*"; }
# ── 1. System dependencies ───────────────────────────────────────────────────
-log "Installing system dependencies..."
-sudo apt-get update -qq
-sudo apt-get install -y \
- build-essential git curl wget python3 python3-pip tmux \
- libelf-dev libssl-dev flex bison bc cpio \
- libncurses-dev libncurses5-dev \
- libusb-1.0-0-dev libudev-dev libglib2.0-dev \
- pkg-config autoconf automake libtool \
- qemu-system-x86 \
- gcc-multilib \
- llvm clang lld \
- gdb
-ok "Dependencies installed."
+if [[ "${SKIP_DEPS}" -eq 1 ]]; then
+ ok "Skipping dependency install (--skip-deps)."
+else
+ log "Installing system dependencies..."
+ sudo apt-get update -qq
+ sudo apt-get install -y \
+ build-essential git curl wget python3 python3-pip tmux \
+ libelf-dev libssl-dev flex meson bison bc cpio \
+ libncurses-dev libncurses5-dev \
+ libusb-1.0-0-dev libudev-dev libglib2.0-dev \
+ pkg-config autoconf automake libtool \
+ qemu-system-x86 \
+ gcc-multilib \
+ llvm clang lld \
+ gdb
+ ok "Dependencies installed."
+fi
# ── 2. AFL++ ─────────────────────────────────────────────────────────────────
if [[ ! -x "afl-build/usr/local/bin/afl-fuzz" ]]; then
@@ -59,7 +65,7 @@ if [[ ! -x "afl-build/usr/local/bin/afl-fuzz" ]]; then
cd AFLplusplus
# QEMU mode requires python3-dev
- sudo apt-get install -y python3-dev python3-setuptools
+ [[ "${SKIP_DEPS}" -eq 0 ]] && sudo apt-get install -y python3-dev python3-setuptools || true
# make distrib builds: core + LLVM instrumentation + QEMU mode
make distrib -j"$(nproc)"
@@ -96,6 +102,15 @@ else
log "Configuring kernel (allnoconfig + USB/IP fragment)..."
cd "${KERNEL_DIR}"
+ # GCC 15+ defaults to C23, where 'bool'/'false' are keywords — the kernel's
+ # arch/x86/boot/compressed/Makefile overrides KBUILD_CFLAGS entirely and
+ # doesn't inherit the top-level -std=gnu11, so patch it directly.
+ if gcc --version 2>&1 | grep -qE "gcc \(.*\) (1[5-9]|[2-9][0-9])\."; then
+ sed -i 's/^KBUILD_CFLAGS := /KBUILD_CFLAGS := -std=gnu11 /' \
+ arch/x86/boot/compressed/Makefile
+ warn "GCC 15+ detected: patched compressed/Makefile with -std=gnu11"
+ fi
+
make allnoconfig
# Merge our minimal USB/IP config on top of allnoconfig
@@ -105,7 +120,8 @@ else
make olddefconfig
log "Building kernel (this takes ~15-20 min on first run)..."
- make -j"$(nproc)" bzImage 2>&1 | tail -5
+ make -j"$(nproc)" bzImage 2>&1 | tee /tmp/kbuild.log | grep -E "^\s*(CC|LD|AR|OBJCOPY|HOSTCC|error:|warning: )" || \
+ { grep "error:" /tmp/kbuild.log | tail -20; exit 1; }
cp arch/x86/boot/bzImage "${SCRIPT_DIR}/bzImage"
cd "${SCRIPT_DIR}"
@@ -134,6 +150,9 @@ else
sed -i 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config
sed -i 's/CONFIG_STATIC=n/CONFIG_STATIC=y/' .config
echo "CONFIG_STATIC=y" >> .config
+ # Disable tc applet: uses CBQ structs removed from modern kernel UAPI headers
+ sed -i 's/CONFIG_TC=y/CONFIG_TC=n/' .config
+ echo "CONFIG_TC=n" >> .config
make oldconfig < /dev/null || true
make -j"$(nproc)"
make install CONFIG_PREFIX="${SCRIPT_DIR}/busybox-install"
@@ -158,7 +177,6 @@ if [[ -n "${KERNEL_USBIP}" ]]; then
cd "${KERNEL_USBIP}"
./autogen.sh
./configure --prefix="${SCRIPT_DIR}/usbipd-plain-install" \
- LDFLAGS="-static" \
CFLAGS="-O2 -g"
make -j"$(nproc)"
make install
@@ -234,14 +252,18 @@ exit 1' > initramfs/usr/sbin/usbipd
fi
# ── 7. net_send.c ─────────────────────────────────────────────────────────────
+# Compile with afl-clang-fast so AFL++ can spawn it via the forkserver.
+# The coverage of net_send.c itself is trivial (it's just a TCP relay), but
+# building with afl-clang-fast avoids the "fork server handshake failed" error
+# that occurs when AFL++ tries to start an uninstrumented binary as a worker.
if [[ ! -f "net_send" ]]; then
- log "Compiling net_send.c..."
- gcc -O2 -Wall -o net_send net_send.c
+ log "Compiling net_send.c (afl-clang-fast)..."
+ afl-clang-fast -O2 -Wall -o net_send net_send.c
ok "net_send compiled."
fi
-# ── 8. Generate corpus ────────────────────────────────────────────────────────
-if [[ ! -d "corpus" ]]; then
+# ── 8. Generate corpus (and per-harness subdirs) ─────────────────────────────
+if [[ ! -d "corpus" ]] || [[ ! -d "corpus/protocol" ]]; then
log "Generating seed corpus..."
python3 gen_corpus.py
fi
| | | | | | | | | | | | | | | | | | |