etc.git


etc

~ scripts and random things
git clone https://github.com/djnnvx/etc.git
log | files | refs | archive

commit 60a3dc984176a3c4733634cd30fde5b7a990b6f9
parent 2d37317f18fc8049d0717550939dd808e632ac14
Author: djnn <[email protected]>
Date: Sun, 1 Mar 2026 20:34:28 +0900

feat(usbip): improving fuzzer harnesses

Diffstat:
Mfuzzing/usbip/build_fuzzers.sh | 60+++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mfuzzing/usbip/cleanup.sh | 10+++++++---
Mfuzzing/usbip/dictionaries/usbip.dict | 28++++++++++++++++++++++++++++
Mfuzzing/usbip/fuzz-include/usbip_fuzz.h | 70+++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mfuzzing/usbip/fuzz_devlist.c | 66+++++++++++++++++++++++++++++++++---------------------------------
Mfuzzing/usbip/fuzz_import.c | 61+++++++++++++++++++++++--------------------------------------
Mfuzzing/usbip/fuzz_protocol.c | 66+++++++++++++++++++++++-------------------------------------------
Afuzzing/usbip/fuzz_stub_client.c | 358+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfuzzing/usbip/fuzz_urb.c | 87+++++++++++++++++++++++++++++++++++--------------------------------------------
Afuzzing/usbip/fuzz_vhci_server.c | 680+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfuzzing/usbip/gen_corpus.py | 179++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Afuzzing/usbip/kcov_shim.c | 278+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfuzzing/usbip/mock_syscalls.c | 24++++++++++++++++++++----
Mfuzzing/usbip/net_send.c | 8++++++++
Mfuzzing/usbip/qemu/init | 8++++++++
Afuzzing/usbip/qemu/init-vhci | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfuzzing/usbip/qemu/kernel.config | 38++++++++++++++++++++++++++++++++++++--
Mfuzzing/usbip/run-fuzzers.sh | 190+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mfuzzing/usbip/setup.sh | 62++++++++++++++++++++++++++++++++++++++++++--------------------

19 files changed, 2066 insertions(+), 290 deletions(-)

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