etc.git


etc

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

commit 2d37317f18fc8049d0717550939dd808e632ac14
parent 968a6bf9fe5ec8985f40ce04f537a4bc24410918
Author: djnn <[email protected]>
Date: Tue, 24 Feb 2026 23:57:23 +0900

feat(fuzzing): usbip fuzzing attempt

Diffstat:
Afuzzing/usbip/README.md | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/backup-crashes.sh | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/build_fuzzers.sh | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/cleanup.sh | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/dictionaries/usbip.dict | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/fuzz-include/usbip_fuzz.h | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/fuzz_devlist.c | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/fuzz_import.c | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/fuzz_protocol.c | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/fuzz_urb.c | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/gen_corpus.py | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/mock_syscalls.c | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/net_send.c | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/qemu/init | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/qemu/kernel.config | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/run-fuzzers.sh | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/usbip/setup.sh | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

17 files changed, 1971 insertions(+), 0 deletions(-)

diff --git a/fuzzing/usbip/README.md b/fuzzing/usbip/README.md
@@ -0,0 +1,71 @@
+# usbipd fuzzer
+
+AFL++ fuzzing environment for the USB/IP daemon (`usbipd`) and protocol stack.
+
+Two complementary modes run in parallel:
+- **In-process** (fast, ~50k exec/s) — AFL++ calls usbip parsing functions directly with `recv()` intercepted via `--wrap`
+- **QEMU network** (deep) — AFL++ sends mutated packets to a live `usbipd` inside a minimal VM; catches kernel-level bugs in the USB/IP module
+
+## Quick start
+
+```bash
+bash setup.sh          # install deps, build AFL++, compile kernel, create initramfs (~20 min)
+bash build_fuzzers.sh  # compile harnesses (~30 sec)
+bash run-fuzzers.sh    # launch tmux session with both fuzzer instances
+```
+
+Check status / stop:
+```bash
+bash run-fuzzers.sh --status   # afl-whatsup summary
+bash cleanup.sh                # kill everything
+bash cleanup.sh --keep-corpus  # kill procs + rm builds, keep output/
+```
+
+Crash backup (run as cron or loop):
+```bash
+bash backup-crashes.sh         # archives new unique crashes to ./backups/
+# crontab: */15 * * * * /path/to/usbip/backup-crashes.sh
+```
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `setup.sh` | Full bootstrap: deps, AFL++, Linux 6.12, busybox, initramfs |
+| `build_fuzzers.sh` | Compile harnesses (fast re-build without re-running setup) |
+| `run-fuzzers.sh` | Tmux session: AFL++ master + QEMU worker |
+| `backup-crashes.sh` | Timestamped, deduplicated crash archival |
+| `cleanup.sh` | Kill processes, remove artifacts |
+| `fuzz_protocol.c` | op_common header routing harness |
+| `fuzz_devlist.c` | Device-list reply parsing (ndev overflow class) |
+| `fuzz_import.c` | Device import + busid string edge cases |
+| `fuzz_urb.c` | URB header parsing (CVE-2016-3955 class) |
+| `mock_syscalls.c` | `__wrap_recv` / `__wrap_send` — feeds AFL input into usbip source |
+| `net_send.c` | TCP client that sends AFL input to usbipd in the QEMU VM |
+| `fuzz-include/usbip_fuzz.h` | Fuzz buffer, logging stubs, protocol constants |
+| `gen_corpus.py` | Generates 87 structured seeds across 7 categories |
+| `dictionaries/usbip.dict` | AFL++ dictionary (op codes, URB flags, busid strings) |
+| `qemu/init` | Initramfs `/init`: mounts, loads USB/IP modules, watchdog loops usbipd |
+| `qemu/kernel.config` | Minimal kernel config (USB/IP + e1000 + serial, no distro cruft) |
+
+## Architecture
+
+```
+AFL++ master ──────────────────► fuzz_protocol (in-process)
+                                  recv() → g_fuzz_buf (--wrap)
+                                  ASAN crash → AFL++ detects instantly
+
+AFL++ worker ──► net_send ──TCP──► usbipd (QEMU VM, port 13240→3240)
+                                  ECONNRESET → exit(1) → AFL++ marks crash
+         shared output/ dir
+         (corpus cross-pollination)
+```
+
+The QEMU VM boots from `bzImage` + `initramfs.cpio.gz` in under 2 seconds. `usbipd` is restarted immediately on crash by the `/init` watchdog, keeping round-trip latency low.
+
+## Notable targets
+
+- `usbip_net_recv_op_common()` — parses the 8-byte op_common header on every connection
+- `usbip_net_recv_op_devlist_reply()` — `ndev` integer overflow / large allocation
+- `usbip_net_recv_op_import_reply()` — busid string handling (path traversal, no-NUL)
+- URB `transfer_buffer_length` validation — CVE-2016-3955 class (heap overflow via unchecked length field in `USBIP_RET_SUBMIT`)
diff --git a/fuzzing/usbip/backup-crashes.sh b/fuzzing/usbip/backup-crashes.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+# backup-crashes.sh — archive unique AFL++ crashes with deduplication.
+#
+# Intended to run as a cron job or in a background loop:
+#   crontab -e
+#   */15 * * * *  /path/to/usbip/backup-crashes.sh >> /tmp/crash-backup.log 2>&1
+#
+# Or in a loop from another terminal:
+#   while sleep 900; do bash backup-crashes.sh; done
+#
+# What it does:
+#   1. Scans output/*/crashes/ for crash files newer than .last_backup stamp
+#   2. Deduplicates by sha256 (avoids archiving the same crash twice)
+#   3. Compresses unique new crashes into backups/crashes_YYYYMMDD_HHMMSS.tar.gz
+#   4. Updates .last_backup stamp
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+OUTPUT_DIR="${SCRIPT_DIR}/output"
+BACKUP_DIR="${SCRIPT_DIR}/backups"
+STAMP_FILE="${SCRIPT_DIR}/.last_backup"
+SEEN_HASHES="${SCRIPT_DIR}/.seen_crash_hashes"
+
+log()  { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
+
+mkdir -p "${BACKUP_DIR}"
+touch "${STAMP_FILE}" "${SEEN_HASHES}"
+
+# ── find new crash files ──────────────────────────────────────────────────────
+TMPLIST="$(mktemp)"
+trap "rm -f ${TMPLIST}" EXIT
+
+# Collect crash files from all AFL++ fuzzer subdirs
+find "${OUTPUT_DIR}" -path "*/crashes/id:*" -newer "${STAMP_FILE}" \
+    -not -name "README.txt" \
+    2>/dev/null > "${TMPLIST}" || true
+
+TOTAL=$(wc -l < "${TMPLIST}")
+if [[ "${TOTAL}" -eq 0 ]]; then
+    log "No new crashes since last backup."
+    exit 0
+fi
+
+log "Found ${TOTAL} new crash file(s) since last backup."
+
+# ── deduplicate by sha256 ─────────────────────────────────────────────────────
+TMPSTAGE="$(mktemp -d)"
+trap "rm -rf ${TMPSTAGE} ${TMPLIST}" EXIT
+
+NEW=0
+while IFS= read -r crash; do
+    hash="$(sha256sum "${crash}" | cut -d' ' -f1)"
+    if ! grep -qF "${hash}" "${SEEN_HASHES}" 2>/dev/null; then
+        cp "${crash}" "${TMPSTAGE}/"
+        echo "${hash}" >> "${SEEN_HASHES}"
+        NEW=$((NEW + 1))
+    fi
+done < "${TMPLIST}"
+
+if [[ "${NEW}" -eq 0 ]]; then
+    log "All ${TOTAL} new files were duplicates. Nothing to archive."
+    touch "${STAMP_FILE}"
+    exit 0
+fi
+
+# ── create archive ────────────────────────────────────────────────────────────
+ARCHIVE="${BACKUP_DIR}/crashes_$(date +%Y%m%d_%H%M%S).tar.gz"
+tar -czf "${ARCHIVE}" -C "${TMPSTAGE}" .
+
+SIZE="$(du -sh "${ARCHIVE}" | cut -f1)"
+log "Archived ${NEW} unique crash(es) → ${ARCHIVE} (${SIZE})"
+
+# Update stamp AFTER successful archive
+touch "${STAMP_FILE}"
+
+# ── prune old backups (keep last 50) ─────────────────────────────────────────
+ls -t "${BACKUP_DIR}"/crashes_*.tar.gz 2>/dev/null | tail -n +51 | while read old; do
+    rm -f "${old}"
+    log "Pruned old backup: $(basename "${old}")"
+done
+
+exit 0
diff --git a/fuzzing/usbip/build_fuzzers.sh b/fuzzing/usbip/build_fuzzers.sh
@@ -0,0 +1,108 @@
+#!/bin/bash
+# build_fuzzers.sh — compile AFL++ in-process fuzzing harnesses.
+#
+# 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
+#          + *.cmplog variants for AFL++ CmpLog mode
+#
+# Usage:
+#   bash build_fuzzers.sh
+#   bash build_fuzzers.sh --clean   # remove all binaries before rebuilding
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+# Add AFL++ to PATH if installed locally by setup.sh
+AFL_BIN="${SCRIPT_DIR}/afl-build/usr/local/bin"
+[[ -d "${AFL_BIN}" ]] && export PATH="${AFL_BIN}:${PATH}"
+
+CC="afl-clang-fast"
+USBIP_SRC="${SCRIPT_DIR}/usbip-src/src"
+USBIP_LIB="${SCRIPT_DIR}/usbip-src/libsrc"
+
+log()  { echo "[*] $*"; }
+ok()   { echo "[+] $*"; }
+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
+    ok "Clean done."
+fi
+
+# ── sanity checks ─────────────────────────────────────────────────────────────
+command -v "${CC}" > /dev/null 2>&1 \
+    || die "afl-clang-fast not found. Run setup.sh first (or add AFL++ to PATH)."
+
+[[ -d "${USBIP_SRC}" ]] \
+    || die "usbip-src/src not found. Run setup.sh first."
+
+# ── ensure corpus and dict exist ─────────────────────────────────────────────
+[[ -d "corpus" ]] || python3 gen_corpus.py
+
+# ── common compiler flags ─────────────────────────────────────────────────────
+CFLAGS="-fsanitize=address,undefined -g -O1 -fno-omit-frame-pointer"
+CFLAGS+=" -I fuzz-include"
+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"
+
+# glib-2.0 is needed by usbip_common.c
+GLIB_CFLAGS="$(pkg-config --cflags glib-2.0 2>/dev/null || echo '')"
+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+=" ${GLIB_LIBS}"
+
+# ── Step 1: compile shared usbip objects ─────────────────────────────────────
+log "Compiling usbip_network.o..."
+${CC} ${CFLAGS} -c "${USBIP_SRC}/usbip_network.c" -o usbip_network.o
+
+log "Compiling usbip_common.o..."
+${CC} ${CFLAGS} -c "${USBIP_LIB}/usbip_common.c" -o usbip_common.o 2>/dev/null || \
+    ${CC} ${CFLAGS} -c "${USBIP_SRC}/usbip_common.c" -o usbip_common.o
+
+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"
+
+# ── Step 2: build each harness (standard + CmpLog) ────────────────────────────
+
+build_harness() {
+    local name="$1"
+    local src="$2"
+
+    log "Building ${name}..."
+    ${CC} ${CFLAGS} ${src} ${COMMON_OBJS} ${LDFLAGS} -o "${name}"
+
+    log "Building ${name}.cmplog (CmpLog variant)..."
+    AFL_LLVM_CMPLOG=1 ${CC} ${CFLAGS} ${src} ${COMMON_OBJS} ${LDFLAGS} -o "${name}.cmplog"
+
+    ok "  ${name}  ${name}.cmplog"
+}
+
+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
+
+# ── Done ─────────────────────────────────────────────────────────────────────
+echo ""
+ok "All harnesses built:"
+ls -lh fuzz_protocol fuzz_devlist fuzz_import fuzz_urb 2>/dev/null
+echo ""
+echo "  Quick smoke test:"
+echo "    ./fuzz_protocol corpus/op_common_req_devlist"
+echo "    ./fuzz_urb      corpus/urb_submit_64b"
diff --git a/fuzzing/usbip/cleanup.sh b/fuzzing/usbip/cleanup.sh
@@ -0,0 +1,108 @@
+#!/bin/bash
+# cleanup.sh — terminate all fuzzing processes and remove build artifacts.
+#
+# Options:
+#   --keep-corpus    preserve corpus/ and output/ directories
+#   --keep-builds    preserve compiled binaries + kernel artifacts
+#   --crashes-only   only kill processes, don't remove anything
+#
+# Usage:
+#   bash cleanup.sh                # full cleanup
+#   bash cleanup.sh --keep-corpus  # kill procs + rm artifacts, keep fuzzing data
+
+set -uo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+SESSION="usbip-fuzz"
+QEMU_PID_FILE="${SCRIPT_DIR}/.qemu.pid"
+
+KEEP_CORPUS=0
+KEEP_BUILDS=0
+CRASHES_ONLY=0
+
+for arg in "$@"; do
+    case "${arg}" in
+        --keep-corpus)  KEEP_CORPUS=1  ;;
+        --keep-builds)  KEEP_BUILDS=1  ;;
+        --crashes-only) CRASHES_ONLY=1 ;;
+        *) echo "[!] Unknown option: ${arg}" >&2 ;;
+    esac
+done
+
+log()  { echo "[*] $*"; }
+ok()   { echo "[+] $*"; }
+
+# ── kill tmux session ─────────────────────────────────────────────────────────
+if tmux list-sessions 2>/dev/null | grep -q "${SESSION}"; then
+    log "Killing tmux session '${SESSION}'..."
+    tmux kill-session -t "${SESSION}" 2>/dev/null || true
+    ok "tmux session killed."
+else
+    log "No tmux session '${SESSION}' found."
+fi
+
+# ── kill QEMU ─────────────────────────────────────────────────────────────────
+if [[ -f "${QEMU_PID_FILE}" ]]; then
+    QPID="$(cat "${QEMU_PID_FILE}")"
+    if kill -0 "${QPID}" 2>/dev/null; then
+        log "Killing QEMU (pid ${QPID})..."
+        kill "${QPID}" 2>/dev/null || true
+        sleep 0.5
+        kill -9 "${QPID}" 2>/dev/null || true
+        ok "QEMU killed."
+    fi
+    rm -f "${QEMU_PID_FILE}"
+fi
+
+# Kill any stray qemu-system-x86_64 processes belonging to this user
+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_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
+
+if [[ "${CRASHES_ONLY}" -eq 1 ]]; then
+    ok "Process cleanup done. (--crashes-only: no files removed)"
+    exit 0
+fi
+
+# ── remove build artifacts ────────────────────────────────────────────────────
+if [[ "${KEEP_BUILDS}" -eq 0 ]]; then
+    log "Removing compiled binaries..."
+    rm -f fuzz_protocol fuzz_protocol.cmplog
+    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 net_send usbipd-plain
+
+    log "Removing kernel build artifacts..."
+    rm -f bzImage initramfs.cpio.gz
+    rm -rf initramfs/
+
+    log "Removing source trees (AFL++, kernel, busybox, usbip)..."
+    rm -rf AFLplusplus/ afl-build/
+    rm -rf "linux-6.12/"          # adjust if KERNEL_VER differs
+    rm -rf "busybox-1.36.1/" busybox-install/ busybox/
+    rm -rf usbip-src/ usbipd-plain-install/
+
+    ok "Build artifacts removed."
+fi
+
+# ── remove fuzzing data ───────────────────────────────────────────────────────
+if [[ "${KEEP_CORPUS}" -eq 0 ]]; then
+    log "Removing corpus and fuzzing output..."
+    rm -rf corpus/ output/
+    rm -f .last_backup .seen_crash_hashes .qemu.pid
+    ok "Corpus and output removed."
+else
+    log "Keeping corpus/ and output/ (--keep-corpus)."
+fi
+
+ok "Cleanup complete."
diff --git a/fuzzing/usbip/dictionaries/usbip.dict b/fuzzing/usbip/dictionaries/usbip.dict
@@ -0,0 +1,64 @@
+# USB/IP AFL++ dictionary
+# Protocol tokens as binary keywords for smarter mutations.
+#
+# Usage: afl-fuzz -x dictionaries/usbip.dict ...
+
+# Protocol version
+version_0x0111="\x01\x11"
+
+# Operation codes (big-endian, 2 bytes)
+op_req_devlist="\x80\x05"
+op_rep_devlist="\x00\x05"
+op_req_import="\x80\x03"
+op_rep_import="\x00\x03"
+
+# op_common header prefix: version + OP_REQ_DEVLIST
+op_hdr_devlist="\x01\x11\x80\x05"
+op_hdr_import="\x01\x11\x80\x03"
+
+# Status codes (4 bytes, big-endian)
+status_ok="\x00\x00\x00\x00"
+status_error="\x00\x00\x00\x01"
+
+# URB command types (4 bytes, big-endian)
+cmd_submit="\x00\x00\x00\x01"
+cmd_unlink="\x00\x00\x00\x02"
+ret_submit="\x00\x00\x00\x03"
+ret_unlink="\x00\x00\x00\x04"
+
+# Common busid strings
+busid_1_1="1-1"
+busid_2_1="2-1"
+busid_1_2="1-2"
+
+# Interesting 32-bit values (length / count fields)
+u32_zero="\x00\x00\x00\x00"
+u32_one="\x00\x00\x00\x01"
+u32_max="\xff\xff\xff\xff"
+u32_intmin="\x80\x00\x00\x00"
+u32_intmax="\x7f\xff\xff\xff"
+u32_65536="\x00\x01\x00\x00"
+u32_256="\x00\x00\x01\x00"
+
+# USB speed values (used in usbip_usb_device.speed)
+speed_low="\x00\x00\x00\x01"
+speed_full="\x00\x00\x00\x02"
+speed_high="\x00\x00\x00\x03"
+speed_super="\x00\x00\x00\x05"
+
+# Transfer flags (URB submit)
+urb_short_not_ok="\x00\x00\x00\x01"
+urb_iso_asap="\x00\x00\x00\x02"
+urb_no_transfer_dma="\x00\x00\x00\x04"
+urb_zero_packet="\x00\x00\x00\x40"
+
+# USB setup packet templates (8 bytes)
+setup_get_descriptor="\x80\x06\x00\x01\x00\x00\xff\x00"
+setup_set_address="\x00\x05\x01\x00\x00\x00\x00\x00"
+setup_set_config="\x00\x09\x01\x00\x00\x00\x00\x00"
+setup_clear_feature="\x02\x01\x00\x00\x00\x00\x00\x00"
+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/"
diff --git a/fuzzing/usbip/fuzz-include/usbip_fuzz.h b/fuzzing/usbip/fuzz-include/usbip_fuzz.h
@@ -0,0 +1,86 @@
+#pragma once
+/*
+ * usbip_fuzz.h — fuzz control header, included before any usbip source.
+ *
+ * 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
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdio.h>
+
+/* ── fuzz buffer (defined in mock_syscalls.c, declared here) ──────── */
+#define FUZZ_BUF_MAX (1 << 16)  /* 64 KB */
+
+extern uint8_t  g_fuzz_buf[FUZZ_BUF_MAX];
+extern size_t   g_fuzz_len;
+extern size_t   g_fuzz_offset;
+
+static inline void fuzz_reset(const uint8_t *data, size_t size) {
+    size_t n = size < FUZZ_BUF_MAX ? size : FUZZ_BUF_MAX;
+    memcpy(g_fuzz_buf, data, n);
+    g_fuzz_len    = n;
+    g_fuzz_offset = 0;
+}
+
+/* ── silence usbip logging ────────────────────────────────────────── */
+#ifndef pr_err
+# define pr_err(fmt, ...)   do { } while (0)
+#endif
+#ifndef pr_warn
+# define pr_warn(fmt, ...)  do { } while (0)
+#endif
+#ifndef pr_info
+# define pr_info(fmt, ...)  do { } while (0)
+#endif
+#ifndef pr_debug
+# define pr_debug(fmt, ...) do { } while (0)
+#endif
+
+/* usbip uses these debug wrappers over pr_debug */
+#ifndef dbg
+# define dbg(fmt, ...)               do { } while (0)
+#endif
+#ifndef usbip_dbg_xmit
+# define usbip_dbg_xmit(fmt, ...)    do { } while (0)
+#endif
+#ifndef usbip_dbg_sysfs
+# define usbip_dbg_sysfs(fmt, ...)   do { } while (0)
+#endif
+#ifndef usbip_dbg_urb
+# define usbip_dbg_urb(fmt, ...)     do { } while (0)
+#endif
+
+/*
+ * 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 */
+
+#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
+
+/* op_common is 8 bytes on the wire */
+#define OP_COMMON_SIZE      8
diff --git a/fuzzing/usbip/fuzz_devlist.c b/fuzzing/usbip/fuzz_devlist.c
@@ -0,0 +1,93 @@
+/*
+ * 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[]
+ *
+ * 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
+ */
+
+#include "fuzz-include/usbip_fuzz.h"
+#include "usbip-src/src/usbip_network.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+#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 MAX_NDEV_FUZZ 64
+
+static int fuzz_one(const uint8_t *data, size_t size)
+{
+    if (size < 4)
+        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);
+
+    uint32_t ndev_be;
+    memcpy(&ndev_be, buf, 4);
+    uint32_t ndev = ntohl(ndev_be);
+    if (ndev > MAX_NDEV_FUZZ) {
+        ndev = MAX_NDEV_FUZZ;
+        uint32_t clamped = htonl(ndev);
+        memcpy(buf, &clamped, 4);
+    }
+
+    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);
+
+    return 0;
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+
+int main(void)
+{
+    __AFL_INIT();
+    uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF;
+    while (__AFL_LOOP(10000)) {
+        fuzz_one(buf, __AFL_FUZZ_TESTCASE_LEN);
+    }
+    return 0;
+}
+
+#else
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+    return fuzz_one(data, size);
+}
+
+#endif
diff --git a/fuzzing/usbip/fuzz_import.c b/fuzzing/usbip/fuzz_import.c
@@ -0,0 +1,89 @@
+/*
+ * 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)]
+ *
+ * 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.
+ *
+ * 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
+ *
+ * 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
+ */
+
+#include "fuzz-include/usbip_fuzz.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 */
+
+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".
+     */
+    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);
+
+    /*
+     * 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 */
+
+    fuzz_reset(data, size);  /* reset buffer so send mock is happy */
+    usbip_net_send_op_import_request(FAKE_FD, busid);
+
+    return 0;
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+
+int main(void)
+{
+    __AFL_INIT();
+    uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF;
+    while (__AFL_LOOP(10000)) {
+        fuzz_one(buf, __AFL_FUZZ_TESTCASE_LEN);
+    }
+    return 0;
+}
+
+#else
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+    return fuzz_one(data, size);
+}
+
+#endif
diff --git a/fuzzing/usbip/fuzz_protocol.c b/fuzzing/usbip/fuzz_protocol.c
@@ -0,0 +1,102 @@
+/*
+ * fuzz_protocol.c — USB/IP protocol header routing 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.
+ *
+ * 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
+ */
+
+#include "fuzz-include/usbip_fuzz.h"
+
+/* usbip headers come from the kernel source tree (usbip-src/) */
+#include "usbip-src/src/usbip_network.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+#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)
+        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().
+     */
+    struct op_common op;
+    memset(&op, 0, sizeof(op));
+
+    int ret = usbip_net_recv_op_common(FAKE_FD, &op);
+    if (ret < 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().
+     */
+    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);
+        break;
+    }
+    case OP_REP_IMPORT: {
+        struct usbip_usb_device idev;
+        memset(&idev, 0, sizeof(idev));
+        usbip_net_recv_op_import_reply(FAKE_FD, &idev);
+        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();
+
+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);
+    }
+    return 0;
+}
+
+#else /* libfuzzer fallback */
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+    return fuzz_one(data, size);
+}
+
+#endif
diff --git a/fuzzing/usbip/fuzz_urb.c b/fuzzing/usbip/fuzz_urb.c
@@ -0,0 +1,124 @@
+/*
+ * fuzz_urb.c — USB/IP URB 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).
+ *
+ * Target: usbip_net_pack_pdu() and the length validation performed when
+ * processing USBIP_CMD_SUBMIT / USBIP_RET_SUBMIT messages.
+ *
+ * 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)
+ *
+ * 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]]
+ *   [... 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/src/usbip_network.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+#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)
+
+static int fuzz_one(const uint8_t *data, size_t size)
+{
+    if (size < HDR_SIZE)
+        return 0;
+
+    uint8_t buf[FUZZ_BUF_MAX];
+    size_t n = size < FUZZ_BUF_MAX ? size : FUZZ_BUF_MAX;
+    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.
+     */
+    int32_t tbl_be;
+    memcpy(&tbl_be, buf + 24, 4);
+    int32_t tbl = (int32_t)ntohl((uint32_t)tbl_be);
+    if (tbl > MAX_XFER_LEN) {
+        tbl = MAX_XFER_LEN;
+        uint32_t clamped = htonl((uint32_t)tbl);
+        memcpy(buf + 24, &clamped, 4);
+    }
+
+    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);
+
+    /*
+     * 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);
+        if (xfer_len > 0 && xfer_len <= MAX_XFER_LEN) {
+            void *payload = malloc((size_t)xfer_len);
+            if (payload) {
+                usbip_net_recv(FAKE_FD, payload, (size_t)xfer_len);
+                free(payload);
+            }
+        }
+    }
+
+    return 0;
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+
+int main(void)
+{
+    __AFL_INIT();
+    uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF;
+    while (__AFL_LOOP(10000)) {
+        fuzz_one(buf, __AFL_FUZZ_TESTCASE_LEN);
+    }
+    return 0;
+}
+
+#else
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+    return fuzz_one(data, size);
+}
+
+#endif
diff --git a/fuzzing/usbip/gen_corpus.py b/fuzzing/usbip/gen_corpus.py
@@ -0,0 +1,255 @@
+#!/usr/bin/env python3
+"""
+gen_corpus.py — generate structured USB/IP protocol seed corpus.
+
+Produces 80+ binary seeds in ./corpus/, covering:
+  1. op_common_valid       — well-formed protocol headers
+  2. op_common_malformed   — bad version / unknown op code
+  3. devlist_replies       — device-list reply bodies (ndev variations)
+  4. import_requests       — busid field edge cases
+  5. urb_submit            — CVE-2016-3955 class (length field variations)
+  6. urb_unlink            — unlink command edge cases
+  7. raw_boundary          — size boundary seeds (empty, 1 byte, max)
+"""
+
+import os
+import struct
+import itertools
+import hashlib
+
+OUT = os.path.join(os.path.dirname(__file__), "corpus")
+os.makedirs(OUT, exist_ok=True)
+
+written = 0
+
+def seed(name: str, data: bytes):
+    global written
+    safe = name.replace("/", "_").replace("\x00", "")
+    path = os.path.join(OUT, safe)
+    with open(path, "wb") as f:
+        f.write(data)
+    written += 1
+
+
+# ── constants ─────────────────────────────────────────────────────────────
+
+USBIP_VERSION   = 0x0111
+OP_REQ_DEVLIST  = 0x8005
+OP_REP_DEVLIST  = 0x0005
+OP_REQ_IMPORT   = 0x8003
+OP_REP_IMPORT   = 0x0003
+
+CMD_SUBMIT      = 0x00000001
+CMD_UNLINK      = 0x00000002
+RET_SUBMIT      = 0x00000003
+RET_UNLINK      = 0x00000004
+
+STATUS_OK       = 0x00000000
+STATUS_ERROR    = 0x00000001
+
+SYSFS_BUS_ID    = 32    # bytes
+
+def op_common(code: int, status: int = STATUS_OK, version: int = USBIP_VERSION) -> bytes:
+    return struct.pack(">HHI", version, code, status)
+
+def usbip_usb_device() -> bytes:
+    """Minimal valid usbip_usb_device struct (312 bytes, all fields safe)."""
+    path       = b"/sys/devices/pci0000:00/0000:00:01.2/usb1/1-1\x00"
+    path      += b"\x00" * (256 - len(path))
+    busid      = b"1-1\x00" + b"\x00" * (SYSFS_BUS_ID - 4)
+    busnum     = struct.pack(">I", 1)
+    devnum     = struct.pack(">I", 1)
+    speed      = struct.pack(">I", 3)       # USB_SPEED_HIGH
+    idvendor   = struct.pack(">H", 0x1234)
+    idproduct  = struct.pack(">H", 0x5678)
+    bcddevice  = struct.pack(">H", 0x0100)
+    devclass   = struct.pack(">B", 0)
+    subclass   = struct.pack(">B", 0)
+    protocol   = struct.pack(">B", 0)
+    cfg        = struct.pack(">B", 1)
+    bNumConfs  = struct.pack(">B", 1)
+    bNumIfaces = struct.pack(">B", 1)
+    return (path + busid + busnum + devnum + speed +
+            idvendor + idproduct + bcddevice +
+            devclass + subclass + protocol +
+            cfg + bNumConfs + bNumIfaces)
+
+def usbip_usb_interface() -> bytes:
+    """Minimal usbip_usb_interface struct."""
+    return struct.pack(">BBBB", 0, 0, 0, 0)
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# 1. op_common valid
+# ══════════════════════════════════════════════════════════════════════════════
+
+seed("op_common_req_devlist",   op_common(OP_REQ_DEVLIST))
+seed("op_common_rep_devlist",   op_common(OP_REP_DEVLIST))
+seed("op_common_req_import",    op_common(OP_REQ_IMPORT))
+seed("op_common_rep_import",    op_common(OP_REP_IMPORT))
+seed("op_common_status_error",  op_common(OP_REP_DEVLIST, STATUS_ERROR))
+
+# With payload after the header (exercises remainder of the stream)
+seed("op_common_devlist_ndev0",
+     op_common(OP_REP_DEVLIST) + struct.pack(">I", 0))
+seed("op_common_devlist_ndev1",
+     op_common(OP_REP_DEVLIST) + struct.pack(">I", 1) + usbip_usb_device() + usbip_usb_interface())
+seed("op_common_import_with_dev",
+     op_common(OP_REP_IMPORT) + struct.pack(">I", STATUS_OK) + usbip_usb_device())
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# 2. op_common malformed
+# ══════════════════════════════════════════════════════════════════════════════
+
+seed("op_bad_version_0000",     struct.pack(">HHHI", 0x0000, OP_REQ_DEVLIST, STATUS_OK, 0))
+seed("op_bad_version_ffff",     struct.pack(">HHHI", 0xFFFF, OP_REQ_DEVLIST, STATUS_OK, 0))
+seed("op_bad_version_0100",     struct.pack(">HHHI", 0x0100, OP_REQ_DEVLIST, STATUS_OK, 0))
+seed("op_bad_version_0200",     struct.pack(">HHHI", 0x0200, OP_REQ_DEVLIST, STATUS_OK, 0))
+seed("op_unknown_code_0001",    op_common(0x0001))
+seed("op_unknown_code_0000",    op_common(0x0000))
+seed("op_unknown_code_ffff",    op_common(0xFFFF))
+seed("op_unknown_code_7fff",    op_common(0x7FFF))
+seed("op_unknown_code_8000",    op_common(0x8000))
+seed("op_status_max",           op_common(OP_REQ_DEVLIST, 0xFFFFFFFF))
+seed("op_status_neg1",          struct.pack(">HHi", USBIP_VERSION, OP_REQ_DEVLIST, -1))
+seed("op_all_zeros",            b"\x00" * 8)
+seed("op_all_ff",               b"\xff" * 8)
+seed("op_just_version",         struct.pack(">H", USBIP_VERSION))  # truncated
+seed("op_7_bytes",              op_common(OP_REQ_DEVLIST)[:7])     # one byte short
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# 3. devlist reply bodies
+# ══════════════════════════════════════════════════════════════════════════════
+
+for ndev in [0, 1, 2, 4, 8, 16, 32, 64, 255, 256, 65535, 0xFFFFFFFF]:
+    name = f"devlist_ndev_{ndev}"
+    body = struct.pack(">I", ndev)
+    if ndev <= 2:
+        for _ in range(min(ndev, 2)):
+            body += usbip_usb_device() + usbip_usb_interface()
+    seed(name, body)
+
+# Device with multiple interfaces
+multi_iface = usbip_usb_device()
+# Patch bNumInterfaces to 3 (last byte of the struct)
+multi_iface = multi_iface[:-1] + b"\x03"
+multi_body = struct.pack(">I", 1) + multi_iface
+for _ in range(3):
+    multi_body += usbip_usb_interface()
+seed("devlist_multi_iface", multi_body)
+
+# Truncated device struct
+seed("devlist_truncated_dev", struct.pack(">I", 1) + usbip_usb_device()[:64])
+
+# Device struct with all-ff fields
+seed("devlist_dev_all_ff", struct.pack(">I", 1) + b"\xff" * len(usbip_usb_device()))
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# 4. import requests / replies (busid edge cases)
+# ══════════════════════════════════════════════════════════════════════════════
+
+def import_input(busid: bytes, extra: bytes = b"") -> bytes:
+    """Pack busid to exactly 32 bytes (no forced NUL — let fuzzer explore)."""
+    bid = busid[:SYSFS_BUS_ID]
+    bid = bid + b"\x00" * (SYSFS_BUS_ID - len(bid))
+    return bid + extra
+
+seed("import_valid_busid",       import_input(b"1-1"))
+seed("import_valid_busid_2_1",   import_input(b"2-1"))
+seed("import_no_null",           import_input(b"A" * SYSFS_BUS_ID))
+seed("import_path_traversal",    import_input(b"../../../etc/passwd"))
+seed("import_path_traversal2",   import_input(b"../../proc/self/mem"))
+seed("import_format_string",     import_input(b"%s%s%s%s%n%n"))
+seed("import_null_bytes",        import_input(b"\x00\x00\x00\x00"))
+seed("import_unicode_like",      import_input(b"\xc0\xaf\xc0\xaf\xc0\xaf"))
+seed("import_long_busid",        import_input(b"1" * SYSFS_BUS_ID))
+seed("import_busid_with_reply",  import_input(b"1-1") + struct.pack(">I", STATUS_OK) + usbip_usb_device())
+seed("import_reply_status_err",  import_input(b"1-1") + struct.pack(">I", STATUS_ERROR))
+seed("import_reply_truncated",   import_input(b"1-1") + struct.pack(">I", STATUS_OK) + usbip_usb_device()[:32])
+seed("import_dev_all_ff",        import_input(b"1-1") + struct.pack(">I", STATUS_OK) + b"\xff" * len(usbip_usb_device()))
+seed("import_empty_busid",       import_input(b""))
+seed("import_slash_busid",       import_input(b"///" * 10))
+seed("import_colon_busid",       import_input(b"0:0"))
+seed("import_large_devnum",      import_input(b"255-255"))
+seed("import_symbolic_link",     import_input(b"1-1 -> /etc/passwd\x00"))
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# 5. URB submit (CVE-2016-3955 class)
+# ══════════════════════════════════════════════════════════════════════════════
+
+def urb_header(command, seqnum=1, devid=0x10001, direction=0, ep=0,
+               transfer_flags=0, transfer_buffer_length=0, start_frame=0,
+               number_of_packets=0, interval=0, setup=b"\x00"*8) -> bytes:
+    base = struct.pack(">IIIII", command, seqnum, devid, direction, ep)
+    submit = struct.pack(">IiiiI", transfer_flags, transfer_buffer_length,
+                         start_frame, number_of_packets, interval)
+    return base + submit + setup[:8]
+
+# Normal submit
+seed("urb_submit_empty",        urb_header(CMD_SUBMIT, transfer_buffer_length=0))
+seed("urb_submit_64b",          urb_header(CMD_SUBMIT, transfer_buffer_length=64) + b"\xaa"*64)
+seed("urb_submit_512b",         urb_header(CMD_SUBMIT, transfer_buffer_length=512) + b"\xbb"*512)
+
+# Length mismatches — CVE-2016-3955 pattern
+seed("urb_ret_actual_gt_alloc", urb_header(RET_SUBMIT, transfer_buffer_length=64) + b"\xcc"*512)
+seed("urb_ret_len_neg1",        urb_header(RET_SUBMIT, transfer_buffer_length=-1))
+seed("urb_ret_len_intmin",      urb_header(RET_SUBMIT, transfer_buffer_length=-2147483648))
+seed("urb_ret_len_max",         urb_header(RET_SUBMIT, transfer_buffer_length=65536))
+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))
+
+# Invalid endpoint / direction
+seed("urb_ep_15",               urb_header(CMD_SUBMIT, ep=15))
+seed("urb_ep_overflow",         urb_header(CMD_SUBMIT, ep=0xFFFFFFFF))
+seed("urb_dir_in",              urb_header(CMD_SUBMIT, direction=1))
+seed("urb_dir_bad",             urb_header(CMD_SUBMIT, direction=0xFFFFFFFF))
+
+# setup packet edge cases (control transfers)
+seed("urb_setup_all_ff",        urb_header(CMD_SUBMIT, setup=b"\xff"*8))
+seed("urb_setup_req_clear",     urb_header(CMD_SUBMIT, setup=bytes([0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x00])))
+seed("urb_setup_get_descriptor",urb_header(CMD_SUBMIT, setup=bytes([0x80,0x06,0x00,0x01,0x00,0x00,0xFF,0x00])))
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# 6. URB unlink
+# ══════════════════════════════════════════════════════════════════════════════
+
+def urb_unlink(seqnum=1, devid=0x10001, direction=0, ep=0, unlink_seqnum=0) -> bytes:
+    base    = struct.pack(">IIIII", CMD_UNLINK, seqnum, devid, direction, ep)
+    payload = struct.pack(">I", unlink_seqnum) + b"\x00" * 24  # padding to match size
+    return base + payload
+
+seed("urb_unlink_seq0",         urb_unlink(unlink_seqnum=0))
+seed("urb_unlink_seq1",         urb_unlink(seqnum=1, unlink_seqnum=1))
+seed("urb_unlink_seq_max",      urb_unlink(unlink_seqnum=0xFFFFFFFF))
+seed("urb_unlink_seq_neg",      struct.pack(">IiIII", CMD_UNLINK, -1, 0, 0, 0) + b"\x00"*24)
+seed("urb_ret_unlink_ok",       urb_header(RET_UNLINK))
+seed("urb_all_zeros",           b"\x00" * 48)
+seed("urb_all_ff",              b"\xff" * 48)
+seed("urb_truncated",           b"\x00" * 20)  # just basic header, no cmd_submit
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# 7. Raw boundary seeds
+# ══════════════════════════════════════════════════════════════════════════════
+
+seed("raw_empty",               b"")
+seed("raw_1byte",               b"\x01")
+seed("raw_7bytes",              b"\x01\x11\x80\x05\x00\x00\x00")
+seed("raw_8bytes_op_common",    op_common(OP_REQ_DEVLIST))
+seed("raw_max_size",            b"\x01\x11\x80\x05\x00\x00\x00\x00" + b"\xde\xad\xbe\xef" * (65536 // 4))
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# Done
+# ══════════════════════════════════════════════════════════════════════════════
+
+print(f"[+] Generated {written} seeds in {OUT}/")
diff --git a/fuzzing/usbip/mock_syscalls.c b/fuzzing/usbip/mock_syscalls.c
@@ -0,0 +1,72 @@
+/*
+ * mock_syscalls.c — recv/send overrides via --wrap linker flags.
+ *
+ * afl-clang-fast links this with -Wl,--wrap=recv -Wl,--wrap=send so that
+ * all recv() calls inside the compiled usbip objects read from g_fuzz_buf
+ * instead of a real socket.  send() is a no-op (output discarded).
+ *
+ * The fuzz buffer is populated by each harness's main() / LLVMFuzzerTestOneInput
+ * via fuzz_reset() from usbip_fuzz.h.
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#define FUZZ_BUF_MAX (1 << 16)
+
+uint8_t  g_fuzz_buf[FUZZ_BUF_MAX];
+size_t   g_fuzz_len;
+size_t   g_fuzz_offset;
+
+/* usbip_debug_flag referenced by logging macros; keep at 0 */
+unsigned long usbip_debug_flag = 0;
+
+/*
+ * __wrap_recv — intercepts every recv() call from the usbip objects.
+ * Serves data from g_fuzz_buf in order; returns 0 (EOF) when exhausted.
+ *
+ * Partial reads are intentionally allowed so usbip_net_xmit's retry loop
+ * sees realistic short-read behaviour and AFL++ can explore both the
+ * "got full header" and "short read" code paths.
+ */
+ssize_t __wrap_recv(int fd, void *buf, size_t len, int flags)
+{
+    (void)fd;
+    (void)flags;
+
+    if (g_fuzz_offset >= g_fuzz_len)
+        return 0;  /* EOF — caller sees empty read and returns error */
+
+    size_t avail = g_fuzz_len - g_fuzz_offset;
+    size_t n = len < avail ? len : avail;
+    memcpy(buf, g_fuzz_buf + g_fuzz_offset, n);
+    g_fuzz_offset += n;
+    return (ssize_t)n;
+}
+
+/*
+ * __wrap_send — silently discards all outgoing data.
+ * Pretends to have sent everything so callers don't error out.
+ */
+ssize_t __wrap_send(int fd, const void *buf, size_t len, int flags)
+{
+    (void)fd;
+    (void)buf;
+    (void)flags;
+    return (ssize_t)len;
+}
+
+/* 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);
+        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
@@ -0,0 +1,140 @@
+/*
+ * net_send.c — TCP bridge for QEMU-backed AFL++ fuzzing.
+ *
+ * Reads the AFL++ test-case file (argv[1]), opens a TCP connection to
+ * host:port (argv[2]:argv[3]), sends the raw bytes to usbipd running inside
+ * the QEMU VM, then exits.
+ *
+ * Exit codes:
+ *   0  — sent OK (or server closed connection normally)
+ *   1  — ECONNRESET / SIGPIPE (daemon crashed → AFL++ marks as crash)
+ *
+ * Retries the connection 3× with 100 ms backoff to handle the brief window
+ * when usbipd is being restarted by the watchdog in /init.
+ *
+ * Usage:
+ *   afl-fuzz -S worker -i corpus -o output -t 2000 -- \
+ *     ./net_send @@ 127.0.0.1 13240
+ *
+ * Compile:
+ *   gcc -O2 -o net_send net_send.c
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <signal.h>
+#include <time.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <arpa/inet.h>
+
+#define MAX_INPUT   (1 << 16)  /* 64 KB — matches FUZZ_BUF_MAX */
+#define MAX_RETRIES 3
+#define RETRY_MS    100
+
+static void sleep_ms(int ms)
+{
+    struct timespec ts = { .tv_sec = ms / 1000, .tv_nsec = (ms % 1000) * 1000000L };
+    nanosleep(&ts, NULL);
+}
+
+static uint8_t ibuf[MAX_INPUT];
+
+int main(int argc, char *argv[])
+{
+    if (argc < 4) {
+        fprintf(stderr, "usage: %s <input_file> <host> <port>\n", argv[0]);
+        return 1;
+    }
+
+    const char *path = argv[1];
+    const char *host = argv[2];
+    int         port = atoi(argv[3]);
+
+    /* ── read input file ──────────────────────────────────────────── */
+    int fd = open(path, O_RDONLY);
+    if (fd < 0) {
+        perror("open");
+        return 1;
+    }
+    ssize_t len = read(fd, ibuf, sizeof(ibuf));
+    close(fd);
+    if (len <= 0)
+        return 0;   /* empty input — nothing to send */
+
+    /* ── ignore SIGPIPE (treat as connection error, not fatal signal) */
+    signal(SIGPIPE, SIG_IGN);
+
+    /* ── connect with retries ─────────────────────────────────────── */
+    struct sockaddr_in addr;
+    memset(&addr, 0, sizeof(addr));
+    addr.sin_family = AF_INET;
+    addr.sin_port   = htons((uint16_t)port);
+    if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) {
+        fprintf(stderr, "bad host: %s\n", host);
+        return 1;
+    }
+
+    int sock = -1;
+    for (int i = 0; i < MAX_RETRIES; i++) {
+        sock = socket(AF_INET, SOCK_STREAM, 0);
+        if (sock < 0) {
+            perror("socket");
+            return 1;
+        }
+
+        /* TCP_NODELAY: no nagle buffering — send immediately */
+        int one = 1;
+        setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
+
+        if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0)
+            break;
+
+        close(sock);
+        sock = -1;
+
+        if (errno == ECONNREFUSED) {
+            /* usbipd is restarting; wait and retry */
+            sleep_ms(RETRY_MS);
+            continue;
+        }
+        /* Any other error is fatal */
+        perror("connect");
+        return 1;
+    }
+
+    if (sock < 0) {
+        /* Could not connect after retries — treat as timeout, not crash */
+        return 0;
+    }
+
+    /* ── send the fuzz payload ────────────────────────────────────── */
+    ssize_t sent = send(sock, ibuf, (size_t)len, MSG_NOSIGNAL);
+
+    if (sent < 0) {
+        close(sock);
+        if (errno == ECONNRESET || errno == EPIPE)
+            return 1;   /* daemon crashed */
+        return 0;
+    }
+
+    /*
+     * Wait briefly for a response (or connection close).
+     * If we get ECONNRESET the daemon crashed → return 1.
+     */
+    uint8_t rbuf[256];
+    ssize_t r = recv(sock, rbuf, sizeof(rbuf), 0);
+    close(sock);
+
+    if (r < 0 && (errno == ECONNRESET || errno == EPIPE))
+        return 1;
+
+    return 0;
+}
diff --git a/fuzzing/usbip/qemu/init b/fuzzing/usbip/qemu/init
@@ -0,0 +1,54 @@
+#!/bin/sh
+#
+# /init — minimal initramfs init for the usbipd fuzzing VM.
+#
+# Goals:
+#   1. Mount virtual filesystems
+#   2. Configure loopback + eth0 networking (QEMU user networking)
+#   3. Load USB/IP kernel modules
+#   4. Run usbipd in a tight watchdog loop (restart immediately on crash)
+#
+# Boot time target: <2 seconds from kernel entry to usbipd accepting connections.
+# This script MUST NOT call any program that isn't in the initramfs.
+
+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
+
+# suppress most kernel messages on the console
+echo 1 > /proc/sys/kernel/printk
+
+# ── 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 kernel modules ──────────────────────────────────────────────────
+modprobe usbip-core 2>/dev/null  || true
+modprobe usbip-host 2>/dev/null  || true
+modprobe vhci-hcd  2>/dev/null   || true
+
+# ── usbipd watchdog loop ───────────────────────────────────────────────────
+# usbipd runs in foreground mode (no -D flag).  When it crashes or exits the
+# loop restarts it immediately — this keeps the fuzzer's "connect and send"
+# flow always finding a live daemon.
+#
+# Crash recovery time = time for this loop to spin + usbipd startup time.
+# On a minimal initramfs that's ~1-5 ms, keeping throughput high.
+
+echo "usbip-fuzz: starting usbipd watchdog"
+
+while true; do
+    /usr/sbin/usbipd 2>/dev/null
+    # usbipd exited (crash / clean exit) — restart without any delay
+done
+
+# unreachable — kernel will panic if this exits
diff --git a/fuzzing/usbip/qemu/kernel.config b/fuzzing/usbip/qemu/kernel.config
@@ -0,0 +1,118 @@
+# USB/IP fuzzing VM — minimal kernel configuration fragment.
+#
+# Applied over `make allnoconfig` using:
+#   scripts/kconfig/merge_config.sh -m .config ../qemu/kernel.config
+#   make olddefconfig
+#
+# Goal: boots in QEMU in <2 seconds with USB/IP modules available.
+# Strips everything not needed: no sound, no DRM, no filesystems beyond
+# initramfs, no USB devices other than the IP-over-TCP stack.
+
+# ── basic 64-bit SMP kernel ──────────────────────────────────────────────
+CONFIG_64BIT=y
+CONFIG_SMP=y
+CONFIG_NR_CPUS=4
+CONFIG_PREEMPT_NONE=y
+CONFIG_HZ_100=y
+
+# ── needed for kernel to function correctly ──────────────────────────────
+CONFIG_PRINTK=y
+CONFIG_BUG=y
+CONFIG_ELF_CORE=n
+CONFIG_FUTEX=y
+CONFIG_EPOLL=y
+CONFIG_SIGNALFD=y
+CONFIG_EVENTFD=y
+CONFIG_AIO=y
+CONFIG_ADVISE_SYSCALLS=y
+CONFIG_MEMFD_CREATE=y
+CONFIG_POSIX_TIMERS=y
+
+# ── memory ───────────────────────────────────────────────────────────────
+CONFIG_MEMORY_ISOLATION=n
+CONFIG_SLUB=y
+CONFIG_SLAB_MERGE_DEFAULT=y
+
+# ── networking ───────────────────────────────────────────────────────────
+CONFIG_NET=y
+CONFIG_INET=y
+CONFIG_PACKET=y
+CONFIG_UNIX=y
+CONFIG_IPV6=n
+CONFIG_TCP_CONG_CUBIC=y
+CONFIG_DEFAULT_TCP_CONG="cubic"
+CONFIG_NETFILTER=n
+CONFIG_NET_SCHED=n
+
+# ── network device drivers ───────────────────────────────────────────────
+CONFIG_NETDEVICES=y
+CONFIG_ETHERNET=y
+CONFIG_NET_VENDOR_INTEL=y
+CONFIG_E1000=y             # QEMU default NIC (e1000)
+CONFIG_VIRTIO_NET=y        # Faster option if using -device virtio-net-pci
+
+# ── USB/IP (the actual target) ───────────────────────────────────────────
+CONFIG_USB=y
+CONFIG_USB_SUPPORT=y
+CONFIG_USB_HCD_TEST_MODE=n
+CONFIG_USB_PCI=y
+CONFIG_USB_OHCI_HCD=y      # USB host controller in QEMU
+CONFIG_USB_OHCI_HCD_PCI=y
+CONFIG_USB_EHCI_HCD=y
+CONFIG_USB_EHCI_PCI=y
+CONFIG_USBIP_CORE=y
+CONFIG_USBIP_HOST=y
+CONFIG_USBIP_VHCI_HCD=y
+CONFIG_USBIP_DEBUG=n
+
+# ── modules (needed for modprobe in /init) ───────────────────────────────
+CONFIG_MODULES=y
+CONFIG_MODULE_UNLOAD=y
+CONFIG_MODULE_FORCE_UNLOAD=n
+
+# ── filesystem / initramfs ───────────────────────────────────────────────
+CONFIG_TMPFS=y
+CONFIG_PROC_FS=y
+CONFIG_PROC_SYSCTL=y
+CONFIG_SYSFS=y
+CONFIG_DEVTMPFS=y
+CONFIG_DEVTMPFS_MOUNT=y
+CONFIG_BLK_DEV_INITRD=y
+CONFIG_RD_GZIP=y
+CONFIG_INITRAMFS_SOURCE=""
+
+# ── TTY / serial console ─────────────────────────────────────────────────
+CONFIG_TTY=y
+CONFIG_VT=n
+CONFIG_SERIAL_8250=y
+CONFIG_SERIAL_8250_CONSOLE=y
+CONFIG_SERIAL_8250_NR_UARTS=4
+
+# ── PCI (needed for e1000) ───────────────────────────────────────────────
+CONFIG_PCI=y
+CONFIG_PCIEPORTBUS=y
+
+# ── clock / timers ───────────────────────────────────────────────────────
+CONFIG_RTC_CLASS=y
+
+# ── disable as much as possible for fast boot ────────────────────────────
+CONFIG_ACPI=n
+CONFIG_ACPI_SLEEP=n
+CONFIG_HIBERNATION=n
+CONFIG_SUSPEND=n
+CONFIG_PM=n
+CONFIG_SOUND=n
+CONFIG_DRM=n
+CONFIG_FB=n
+CONFIG_FRAMEBUFFER_CONSOLE=n
+CONFIG_HID=n
+CONFIG_INPUT=n
+CONFIG_SERIO=n
+CONFIG_WIRELESS=n
+CONFIG_RFKILL=n
+CONFIG_CRYPTO_HW=n
+
+# ── size optimization ────────────────────────────────────────────────────
+CONFIG_CC_OPTIMIZE_FOR_SIZE=y
+CONFIG_KERNEL_XZ=n
+CONFIG_KERNEL_GZIP=y
diff --git a/fuzzing/usbip/run-fuzzers.sh b/fuzzing/usbip/run-fuzzers.sh
@@ -0,0 +1,146 @@
+#!/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.
+#
+# Both instances share the same output/ directory for corpus cross-pollination.
+# The QEMU VM boots from bzImage + initramfs.cpio.gz before the worker starts.
+#
+# Prerequisites: setup.sh + build_fuzzers.sh must have completed.
+#
+# Usage:
+#   bash run-fuzzers.sh          # start fresh session
+#   bash run-fuzzers.sh --rerun  # kill existing session, start new one
+#   bash run-fuzzers.sh --status # show afl-whatsup output
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+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"
+DICT="${SCRIPT_DIR}/dictionaries/usbip.dict"
+TIMEOUT_MASTER=1000         # ms per execution (in-process)
+TIMEOUT_WORKER=3000         # ms per execution (network round-trip)
+
+log()  { echo "[*] $*"; }
+ok()   { echo "[+] $*"; }
+die()  { echo "[!] $*" >&2; exit 1; }
+
+# ── option handling ───────────────────────────────────────────────────────────
+case "${1:-}" in
+    --status)
+        command -v afl-whatsup > /dev/null 2>&1 && \
+            afl-whatsup "${OUTPUT_DIR}" 2>/dev/null || \
+            echo "Output dir: ${OUTPUT_DIR}"
+        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." ;;
+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.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."
+tmux list-sessions 2>/dev/null | grep -q "${SESSION}" && \
+    die "Session '${SESSION}' already running. Use --rerun to restart."
+
+# AFL++ performance settings
+sudo sysctl -w kernel.core_pattern=core > /dev/null 2>&1 || true
+echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor > /dev/null 2>&1 || true
+
+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 ───────────────────────────────────────────────────────
+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)..."
+tmux send-keys -t "${SESSION}:0.0" \
+    "AFL_SKIP_CPUFREQ=1 afl-fuzz \
+    -M master \
+    -i ${CORPUS_DIR} \
+    -o ${OUTPUT_DIR} \
+    -x ${DICT} \
+    -c ./fuzz_protocol.cmplog \
+    -t ${TIMEOUT_MASTER} \
+    -- ./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"
+tmux send-keys -t "${SESSION}:0.1" \
+    "AFL_SKIP_CPUFREQ=1 afl-fuzz \
+    -S worker \
+    -i ${CORPUS_DIR} \
+    -o ${OUTPUT_DIR} \
+    -x ${DICT} \
+    -t ${TIMEOUT_WORKER} \
+    -- ./net_send @@ 127.0.0.1 ${QEMU_HOST_PORT}" \
+    Enter
+
+# ── attach ────────────────────────────────────────────────────────────────────
+ok "Fuzzing session started."
+echo ""
+echo "  Attach:       tmux attach -t ${SESSION}"
+echo "  Status:       ${0} --status"
+echo "  Crashes:      ${OUTPUT_DIR}/master/crashes/"
+echo "  Stop:         bash cleanup.sh"
+echo ""
+
+tmux attach -t "${SESSION}"
diff --git a/fuzzing/usbip/setup.sh b/fuzzing/usbip/setup.sh
@@ -0,0 +1,256 @@
+#!/bin/bash
+# setup.sh — one-shot bootstrap for the usbipd fuzzing environment.
+#
+# What this does (in order):
+#   1. Install system dependencies
+#   2. Clone + build AFL++ with QEMU mode
+#   3. Download + configure + build minimal Linux 6.12 LTS kernel
+#   4. Build static busybox
+#   5. Build usbipd from kernel tools (instrumented for AFL++, and plain for initramfs)
+#   6. Assemble initramfs (busybox + usbipd + /init)
+#   7. Compile net_send.c
+#
+# Re-running is safe: each step checks for existing artifacts and skips.
+# Override KERNEL_VER or BUSYBOX_VER env vars to use different versions.
+#
+# Usage:
+#   bash setup.sh
+#   bash setup.sh --no-kernel   # skip kernel build (use existing bzImage)
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+KERNEL_VER="${KERNEL_VER:-6.12}"
+BUSYBOX_VER="${BUSYBOX_VER:-1.36.1}"
+AFL_REPO="https://github.com/AFLplusplus/AFLplusplus.git"
+
+SKIP_KERNEL=0
+for arg in "$@"; do
+    [[ "$arg" == "--no-kernel" ]] && SKIP_KERNEL=1
+done
+
+log()  { echo "[*] $*"; }
+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."
+
+# ── 2. AFL++ ─────────────────────────────────────────────────────────────────
+if [[ ! -x "afl-build/usr/local/bin/afl-fuzz" ]]; then
+    log "Cloning AFL++..."
+    [[ -d "AFLplusplus" ]] || git clone --depth=1 "${AFL_REPO}" AFLplusplus
+
+    log "Building AFL++ (distrib target — includes QEMU mode)..."
+    cd AFLplusplus
+
+    # QEMU mode requires python3-dev
+    sudo apt-get install -y python3-dev python3-setuptools
+
+    # make distrib builds: core + LLVM instrumentation + QEMU mode
+    make distrib -j"$(nproc)"
+
+    # Install into local prefix so we don't pollute /usr/local
+    make install PREFIX="${SCRIPT_DIR}/afl-build/usr/local"
+    cd "${SCRIPT_DIR}"
+    ok "AFL++ built."
+else
+    ok "AFL++ already built, skipping."
+fi
+
+# Add AFL++ to PATH for this session and child processes
+export PATH="${SCRIPT_DIR}/afl-build/usr/local/bin:${PATH}"
+
+# ── 3. Linux kernel ──────────────────────────────────────────────────────────
+KERNEL_DIR="linux-${KERNEL_VER}"
+KERNEL_TAR="linux-${KERNEL_VER}.tar.xz"
+KERNEL_URL="https://cdn.kernel.org/pub/linux/kernel/v6.x/${KERNEL_TAR}"
+
+if [[ "${SKIP_KERNEL}" -eq 1 ]]; then
+    warn "--no-kernel: skipping kernel build."
+elif [[ -f "bzImage" ]]; then
+    ok "bzImage already exists, skipping kernel build."
+else
+    if [[ ! -d "${KERNEL_DIR}" ]]; then
+        log "Downloading Linux ${KERNEL_VER}..."
+        wget -q --show-progress -c "${KERNEL_URL}"
+        log "Extracting..."
+        tar -xf "${KERNEL_TAR}"
+        rm -f "${KERNEL_TAR}"
+    fi
+
+    log "Configuring kernel (allnoconfig + USB/IP fragment)..."
+    cd "${KERNEL_DIR}"
+
+    make allnoconfig
+
+    # Merge our minimal USB/IP config on top of allnoconfig
+    scripts/kconfig/merge_config.sh -m .config "${SCRIPT_DIR}/qemu/kernel.config"
+
+    # Resolve any unresolved dependencies automatically
+    make olddefconfig
+
+    log "Building kernel (this takes ~15-20 min on first run)..."
+    make -j"$(nproc)" bzImage 2>&1 | tail -5
+
+    cp arch/x86/boot/bzImage "${SCRIPT_DIR}/bzImage"
+    cd "${SCRIPT_DIR}"
+    ok "Kernel built: bzImage"
+fi
+
+# ── 4. Busybox (static) ──────────────────────────────────────────────────────
+BUSYBOX_DIR="busybox-${BUSYBOX_VER}"
+BUSYBOX_TAR="${BUSYBOX_DIR}.tar.bz2"
+BUSYBOX_URL="https://busybox.net/downloads/${BUSYBOX_TAR}"
+
+if [[ -d "busybox-install" ]]; then
+    ok "Busybox already built, skipping."
+else
+    if [[ ! -d "${BUSYBOX_DIR}" ]]; then
+        log "Downloading busybox ${BUSYBOX_VER}..."
+        wget -q --show-progress -c "${BUSYBOX_URL}"
+        tar -xjf "${BUSYBOX_TAR}"
+        rm -f "${BUSYBOX_TAR}"
+    fi
+
+    log "Building static busybox..."
+    cd "${BUSYBOX_DIR}"
+    make defconfig
+    # Enable static build
+    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
+    make oldconfig < /dev/null || true
+    make -j"$(nproc)"
+    make install CONFIG_PREFIX="${SCRIPT_DIR}/busybox-install"
+    cd "${SCRIPT_DIR}"
+    ok "Busybox installed to busybox-install/"
+fi
+
+# ── 5. usbip userspace tools ─────────────────────────────────────────────────
+USBIP_SRC="${SCRIPT_DIR}/usbip-src"
+
+if [[ "${SKIP_KERNEL}" -eq 0 ]] && [[ -d "${SCRIPT_DIR}/${KERNEL_DIR}" ]]; then
+    KERNEL_USBIP="${SCRIPT_DIR}/${KERNEL_DIR}/tools/usb/usbip"
+else
+    warn "No kernel source directory found. Skipping usbip build."
+    KERNEL_USBIP=""
+fi
+
+if [[ -n "${KERNEL_USBIP}" ]]; then
+    # 5a. Plain build (for initramfs — not AFL instrumented)
+    if [[ ! -f "usbipd-plain" ]]; then
+        log "Building usbipd (plain, for initramfs)..."
+        cd "${KERNEL_USBIP}"
+        ./autogen.sh
+        ./configure --prefix="${SCRIPT_DIR}/usbipd-plain-install" \
+                    LDFLAGS="-static" \
+                    CFLAGS="-O2 -g"
+        make -j"$(nproc)"
+        make install
+        cd "${SCRIPT_DIR}"
+        cp usbipd-plain-install/sbin/usbipd usbipd-plain || \
+            cp usbipd-plain-install/usr/sbin/usbipd usbipd-plain 2>/dev/null || true
+        ok "Plain usbipd built."
+    fi
+
+    # 5b. AFL++-instrumented copy of the source (for in-process harnesses)
+    if [[ ! -d "${USBIP_SRC}" ]]; then
+        log "Copying usbip source for AFL++ instrumented build..."
+        cp -r "${KERNEL_USBIP}" "${USBIP_SRC}"
+    fi
+
+    ok "usbip source available at ${USBIP_SRC}/"
+fi
+
+# ── 6. Initramfs ──────────────────────────────────────────────────────────────
+if [[ -f "initramfs.cpio.gz" ]]; then
+    ok "initramfs.cpio.gz already exists, skipping."
+else
+    log "Assembling initramfs..."
+    rm -rf initramfs
+    mkdir -p initramfs/{bin,sbin,usr/bin,usr/sbin,proc,sys,dev,tmp,run,lib,lib64,etc}
+
+    # Busybox
+    cp busybox-install/bin/busybox initramfs/bin/busybox
+    # Install all busybox applets as symlinks
+    (cd initramfs && bin/busybox --list | while read applet; do
+        ln -sf /bin/busybox bin/"${applet}" 2>/dev/null || true
+    done)
+    # Ensure sh and ip are present
+    ln -sf /bin/busybox initramfs/bin/sh
+    ln -sf /bin/busybox initramfs/sbin/ip
+    ln -sf /bin/busybox initramfs/sbin/modprobe
+
+    # usbipd binary
+    if [[ -f "usbipd-plain" ]]; then
+        cp usbipd-plain initramfs/usr/sbin/usbipd
+    else
+        warn "usbipd-plain not found — initramfs will have no daemon!"
+        warn "Run setup.sh after kernel source is available."
+        # Create a placeholder so the initramfs structure is correct
+        echo '#!/bin/sh
+echo "usbipd not found"
+exit 1' > initramfs/usr/sbin/usbipd
+    fi
+    chmod +x initramfs/usr/sbin/usbipd
+
+    # If usbipd is dynamic, bundle its shared libraries
+    if [[ -f "usbipd-plain" ]] && file usbipd-plain | grep -q "dynamically linked"; then
+        log "Bundling shared libraries for dynamic usbipd..."
+        ldd usbipd-plain | grep -oP '(/[^ ]+\.so[^ ]*)' | while read lib; do
+            [[ -f "$lib" ]] || continue
+            libdir="initramfs/$(dirname "${lib#/}")"
+            mkdir -p "${libdir}"
+            cp "${lib}" "${libdir}/"
+        done
+        # Copy the dynamic linker
+        for ld in /lib64/ld-linux-x86-64.so.2 /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2; do
+            [[ -f "$ld" ]] && cp "$ld" initramfs/lib64/ld-linux-x86-64.so.2 && break
+        done
+    fi
+
+    # /init script
+    cp qemu/init initramfs/init
+    chmod +x initramfs/init
+
+    log "Packing initramfs..."
+    (cd initramfs && find . | cpio -oH newc 2>/dev/null | gzip -9) > initramfs.cpio.gz
+    ok "initramfs.cpio.gz: $(du -sh initramfs.cpio.gz | cut -f1)"
+fi
+
+# ── 7. net_send.c ─────────────────────────────────────────────────────────────
+if [[ ! -f "net_send" ]]; then
+    log "Compiling net_send.c..."
+    gcc -O2 -Wall -o net_send net_send.c
+    ok "net_send compiled."
+fi
+
+# ── 8. Generate corpus ────────────────────────────────────────────────────────
+if [[ ! -d "corpus" ]]; then
+    log "Generating seed corpus..."
+    python3 gen_corpus.py
+fi
+
+# ── Done ─────────────────────────────────────────────────────────────────────
+echo ""
+ok "Setup complete. Next steps:"
+echo "  1. Build harnesses:  bash build_fuzzers.sh"
+echo "  2. Start fuzzing:    bash run-fuzzers.sh"
+echo "  3. Verify QEMU:      qemu-system-x86_64 -kernel bzImage -initrd initramfs.cpio.gz \\"
+echo "                         -nographic -append 'console=ttyS0 quiet' -m 64M"
+echo "                       Then: nc 127.0.0.1 13240  (should get a response from usbipd)"