etc.git


etc

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

commit 9e6b334fa4794525ea6423bf6efb23aebd288b37
parent 05c3359fbd0b16d72964f527d796d5ddd714bb89
Author: djnn <[email protected]>
Date: Tue, 10 Mar 2026 12:32:44 +0900

feat(usbip): improving coverage;D

Diffstat:
Mfuzzing/usbip/fuzz_stub_client.c | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mfuzzing/usbip/fuzz_vhci_server.c | 103++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mfuzzing/usbip/qemu/init | 22++++++++++++++++++++++
Mfuzzing/usbip/run-fuzzers.sh | 336++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mfuzzing/usbip/setup.sh | 60+++++++++++++++++++++++++++++++++++++++++++++++-------------

5 files changed, 465 insertions(+), 167 deletions(-)

diff --git a/fuzzing/usbip/fuzz_stub_client.c b/fuzzing/usbip/fuzz_stub_client.c
@@ -58,6 +58,8 @@
 
 #define OP_REQ_IMPORT       0x8003
 #define OP_REP_IMPORT       0x0003
+#define OP_REQ_DEVLIST      0x8005
+#define OP_REP_DEVLIST      0x0005
 
 #define USBIP_CMD_SUBMIT    0x00000001
 #define USBIP_CMD_UNLINK    0x00000002
@@ -82,6 +84,10 @@ struct op_req_import {
     char             busid[SYSFS_BUS_ID_SIZE];
 } __attribute__((packed));
 
+struct op_devlist_reply {
+    uint32_t ndev;
+} __attribute__((packed));
+
 struct usbip_usb_device {
     char     path[SYSFS_PATH_MAX];
     char     busid[SYSFS_BUS_ID_SIZE];
@@ -99,6 +105,13 @@ struct usbip_usb_device {
     uint8_t  bNumInterfaces;
 } __attribute__((packed));
 
+struct usbip_usb_interface {
+    uint8_t bInterfaceClass;
+    uint8_t bInterfaceSubClass;
+    uint8_t bInterfaceProtocol;
+    uint8_t padding;
+} __attribute__((packed));
+
 struct op_rep_import {
     struct op_common        hdr;
     struct usbip_usb_device dev;
@@ -186,6 +199,61 @@ static int do_import(int sock, const char *busid)
     return 0;
 }
 
+/* Query OP_REQ_DEVLIST and return first busid found. */
+static int discover_busid(int sock, char out_busid[SYSFS_BUS_ID_SIZE])
+{
+    struct op_common req;
+    memset(&req, 0, sizeof(req));
+    req.version = htons(USBIP_VERSION);
+    req.code    = htons(OP_REQ_DEVLIST);
+    req.status  = htonl(0);
+    if (send_all(sock, &req, sizeof(req)) < 0)
+        return -1;
+
+    struct op_common rep;
+    memset(&rep, 0, sizeof(rep));
+    if (recv_all(sock, &rep, sizeof(rep)) < 0)
+        return -1;
+    if (ntohs(rep.code) != OP_REP_DEVLIST || ntohl(rep.status) != 0)
+        return -1;
+
+    struct op_devlist_reply dr;
+    memset(&dr, 0, sizeof(dr));
+    if (recv_all(sock, &dr, sizeof(dr)) < 0)
+        return -1;
+
+    uint32_t ndev = ntohl(dr.ndev);
+    if (ndev == 0)
+        return -1;
+    if (ndev > 1024)
+        ndev = 1024;
+
+    out_busid[0] = '\0';
+
+    for (uint32_t i = 0; i < ndev; i++) {
+        struct usbip_usb_device dev;
+        memset(&dev, 0, sizeof(dev));
+        if (recv_all(sock, &dev, sizeof(dev)) < 0)
+            return -1;
+
+        if (out_busid[0] == '\0') {
+            memcpy(out_busid, dev.busid, SYSFS_BUS_ID_SIZE);
+            out_busid[SYSFS_BUS_ID_SIZE - 1] = '\0';
+        }
+
+        uint8_t nif = dev.bNumInterfaces;
+        if (nif > 64)
+            nif = 64;
+        for (uint8_t j = 0; j < nif; j++) {
+            struct usbip_usb_interface inf;
+            if (recv_all(sock, &inf, sizeof(inf)) < 0)
+                return -1;
+        }
+    }
+
+    return out_busid[0] == '\0' ? -1 : 0;
+}
+
 /* ── fuzzing loop ───────────────────────────────────────────────────────────── *
  *
  * Interprets the fuzz input as a stream of CMD_SUBMIT packets:
@@ -346,13 +414,46 @@ int main(int argc, char *argv[])
     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 */
+    char busid[SYSFS_BUS_ID_SIZE];
+    memset(busid, 0, sizeof(busid));
+
+    /*
+     * Discover exported busid first. If this fails, fall back to 1-1.
+     * Reconnect before IMPORT/fuzz so the tunnel starts from a clean socket.
+     */
+    int have_busid = (discover_busid(sock, busid) == 0);
+    close(sock);
+
+    sock = socket(AF_INET, SOCK_STREAM, 0);
+    if (sock < 0) { perror("socket"); return 1; }
+    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &iotv, sizeof(iotv));
+    setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &iotv, sizeof(iotv));
+    setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
+    if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
         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 */
+    const char *target_busid = have_busid ? busid : "1-1";
+    if (do_import(sock, target_busid) < 0) {
+        /* Import failed — try fallback busid before giving up. */
+        if (strcmp(target_busid, "1-1") != 0 && do_import(sock, "1-1") == 0) {
+            /* fallback succeeded */
+        } else {
+            close(sock);
+            return 0;
+        }
+    }
+
+    if (have_busid) {
+        fprintf(stderr, "[stub] using discovered busid: %s\n", target_busid);
+    } else {
+        fprintf(stderr, "[stub] devlist discovery failed, fallback busid: 1-1\n");
+    }
+
+    {
+        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_vhci_server.c b/fuzzing/usbip/fuzz_vhci_server.c
@@ -332,6 +332,54 @@ static int send_all(int fd, const void *buf, size_t len)
     return 0;
 }
 
+/* Send in small fuzz-driven chunks to exercise short-read/reassembly paths. */
+static int send_fragmented(int fd, const uint8_t *buf, size_t len,
+                           const uint8_t *fuzz, size_t fuzz_len, size_t *fuzz_off)
+{
+    size_t done = 0;
+    while (done < len) {
+        uint8_t stepb = 0;
+        if (*fuzz_off < fuzz_len)
+            stepb = fuzz[(*fuzz_off)++];
+        size_t chunk = (size_t)(stepb % 31u) + 1u; /* 1..31 bytes */
+        if (chunk > len - done)
+            chunk = len - done;
+        if (send_all(fd, buf + done, chunk) < 0)
+            return -1;
+        done += chunk;
+    }
+    return 0;
+}
+
+/* Send either as one write or fragmented based on fuzz byte. */
+static int send_fuzzed(int fd, const uint8_t *buf, size_t len,
+                       const uint8_t *fuzz, size_t fuzz_len, size_t *fuzz_off)
+{
+    uint8_t mode = 0;
+    if (*fuzz_off < fuzz_len)
+        mode = fuzz[(*fuzz_off)++];
+    if (mode & 1)
+        return send_fragmented(fd, buf, len, fuzz, fuzz_len, fuzz_off);
+    return send_all(fd, buf, len);
+}
+
+/* Copy as many bytes as available from fuzz stream, zero-pad the rest. */
+static size_t fill_from_fuzz(uint8_t *dst, size_t want,
+                             const uint8_t *fuzz, size_t fuzz_len,
+                             size_t *fuzz_off)
+{
+    size_t copied = 0;
+    if (*fuzz_off < fuzz_len) {
+        size_t avail = fuzz_len - *fuzz_off;
+        copied = want < avail ? want : avail;
+        memcpy(dst, fuzz + *fuzz_off, copied);
+        *fuzz_off += copied;
+    }
+    if (copied < want)
+        memset(dst + copied, 0x00, want - copied);
+    return copied;
+}
+
 /* ── handshake: read OP_REQ_IMPORT, send OP_REP_IMPORT ─────────────────────── */
 
 static int do_handshake(int sock)
@@ -526,12 +574,8 @@ static int fuzz_loop(int sock, const uint8_t *fuzz, size_t fuzz_len)
             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;
+            /* Overlay fuzz bytes onto the 28-byte ret_submit union. */
+            fill_from_fuzz(resp.u.raw, sizeof(resp.u.raw), fuzz, fuzz_len, &fuzz_off);
 
             /* Clamp actual_length for the host-side payload send */
             int32_t actual_len = ntohl(resp.u.ret_submit.actual_length);
@@ -539,12 +583,46 @@ static int fuzz_loop(int sock, const uint8_t *fuzz, size_t fuzz_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)
+            /*
+             * Fuzz response semantics to hit vhci_rx dispatch/error branches:
+             *   mode 0: normal RET_SUBMIT (deep path)
+             *   mode 1: wrong command RET_UNLINK for CMD_SUBMIT
+             *   mode 2: unknown command value
+             *   mode 3: wrong seqnum
+             *   mode 4: truncate header and close
+             */
+            uint8_t dispatch_mode = 0;
+            if (fuzz_off < fuzz_len)
+                dispatch_mode = (uint8_t)(fuzz[fuzz_off++] % 5u);
+
+            if (dispatch_mode == 1) {
+                resp.base.command = htonl(USBIP_RET_UNLINK);
+            } else if (dispatch_mode == 2) {
+                uint32_t bad = 0xdead0000u;
+                if (fuzz_off + 4 <= fuzz_len) {
+                    memcpy(&bad, fuzz + fuzz_off, 4);
+                    fuzz_off += 4;
+                }
+                resp.base.command = htonl(bad);
+            } else if (dispatch_mode == 3) {
+                uint32_t seq = ntohl(resp.base.seqnum);
+                seq ^= 1u;
+                resp.base.seqnum = htonl(seq);
+            } else if (dispatch_mode == 4) {
+                size_t trunc = 1;
+                if (fuzz_off < fuzz_len)
+                    trunc = (size_t)(fuzz[fuzz_off++] % (sizeof(resp) - 1)) + 1;
+                if (send_fuzzed(sock, (const uint8_t *)&resp, trunc, fuzz, fuzz_len, &fuzz_off) < 0)
+                    return 1;
+                return 0;
+            }
+
+            if (send_fuzzed(sock, (const uint8_t *)&resp, sizeof(resp), fuzz, fuzz_len, &fuzz_off) < 0)
                 return 1;
 
             /* Send xbuff (actual_length bytes) */
             if (send_len > 0) {
-                if (send_all(sock, payload, (size_t)send_len) < 0)
+                if (send_fuzzed(sock, payload, (size_t)send_len, fuzz, fuzz_len, &fuzz_off) < 0)
                     return 1;
             }
 
@@ -579,7 +657,14 @@ static int fuzz_loop(int sock, const uint8_t *fuzz, size_t fuzz_len)
                     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)
+                        /*
+                         * IMPORTANT: drive ISO descriptor bytes from testcase
+                         * data (not constant zeros). This reaches additional
+                         * usbip_pad_iso() branches that depend on per-packet
+                         * offset/actual_length values.
+                         */
+                        fill_from_fuzz(iso_buf, iso_send, fuzz, fuzz_len, &fuzz_off);
+                        if (send_fuzzed(sock, iso_buf, (size_t)iso_send, fuzz, fuzz_len, &fuzz_off) < 0)
                             return 1;
                     }
                 }
diff --git a/fuzzing/usbip/qemu/init b/fuzzing/usbip/qemu/init
@@ -44,6 +44,28 @@ modprobe usbip-core 2>/dev/null  || true
 modprobe usbip-host 2>/dev/null  || true
 modprobe vhci-hcd  2>/dev/null   || true
 
+# ── best-effort auto export for stub-side fuzzing ──────────────────────────
+# fuzz_stub_client expects OP_REQ_IMPORT busid=1-1 by default. In minimal VMs
+# there may be no exportable USB devices; when one exists, bind it automatically.
+USBIP_BIN=""
+if [ -x /usr/sbin/usbip ]; then
+    USBIP_BIN=/usr/sbin/usbip
+elif [ -x /usr/bin/usbip ]; then
+    USBIP_BIN=/usr/bin/usbip
+fi
+
+if [ -n "$USBIP_BIN" ]; then
+    BUSID="$($USBIP_BIN list -l 2>/dev/null | sed -n 's/^- busid \\([^ ]*\\) .*/\\1/p' | head -n 1)"
+    if [ -n "$BUSID" ]; then
+        $USBIP_BIN bind -b "$BUSID" >/dev/null 2>&1 || true
+        echo "usbip-fuzz: auto-bound export busid=$BUSID"
+    else
+        echo "usbip-fuzz: no exportable USB device found (stub fuzzing may idle)"
+    fi
+else
+    echo "usbip-fuzz: usbip tool missing in initramfs (stub fuzzing may idle)"
+fi
+
 # ── 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"
diff --git a/fuzzing/usbip/run-fuzzers.sh b/fuzzing/usbip/run-fuzzers.sh
@@ -1,32 +1,16 @@
 #!/bin/bash
-# run-fuzzers.sh — launch the usbipd fuzzing environment in a tmux session.
+# run-fuzzers.sh — launch USB/IP fuzzing in tmux with a 2-fuzzer budget.
 #
-# 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).
+# Default profile (max-impact):
+#   Pane 1: in-process fuzz_urb (fast, high exec/sec, CmpLog-enabled)
+#   Pane 2: vhci kernel fuzzer (fuzz_vhci_server) against QEMU VM
 #
-# 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.
+# Optional profile:
+#   --inproc  : protocol + urb only (no QEMU)
+#   --stub    : fuzz_urb + stub kernel fuzzer (fuzz_stub_client) against QEMU VM
 #
-# 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.
-#
-# 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
+# This script intentionally keeps only two AFL++ instances active.
+# For vhci mode it also launches a helper QEMU window in the same tmux session.
 
 set -euo pipefail
 
@@ -41,16 +25,25 @@ OUTPUT_DIR="${SCRIPT_DIR}/output"
 CORPUS_DIR="${SCRIPT_DIR}/corpus"
 CORPUS_PROTOCOL="${CORPUS_DIR}/protocol"
 CORPUS_URB="${CORPUS_DIR}/urb"
+CORPUS_VHCI="${CORPUS_DIR}/vhci"
+CORPUS_STUB="${CORPUS_DIR}/stub"
 DICT="${SCRIPT_DIR}/dictionaries/usbip.dict"
-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)
+TIMEOUT=1000
+VHCI_TIMEOUT=5000
+STUB_TIMEOUT=5000
+NICE=10
+PROFILE="max-impact"
+VMLINUX="${SCRIPT_DIR}/bzImage"
+VMLINUX_QEMU="${SCRIPT_DIR}/qemu/bzImage"
+INITRD_VHCI="${SCRIPT_DIR}/initramfs-vhci.cpio.gz"
+INITRD_VHCI_QEMU="${SCRIPT_DIR}/qemu/initramfs-vhci.cpio.gz"
+INITRD_SERVER="${SCRIPT_DIR}/initramfs.cpio.gz"
+INITRD_SERVER_QEMU="${SCRIPT_DIR}/qemu/initramfs.cpio.gz"
 
 log()  { echo "[*] $*"; }
 ok()   { echo "[+] $*"; }
 die()  { echo "[!] $*" >&2; exit 1; }
 
-# ── option handling ───────────────────────────────────────────────────────────
 case "${1:-}" in
     --status)
         command -v afl-whatsup > /dev/null 2>&1 && \
@@ -60,144 +53,207 @@ case "${1:-}" in
     --rerun)
         tmux kill-session -t "${SESSION}" 2>/dev/null || true
         ;;
-    "") ;;
-    *) die "Unknown option: ${1}. Use --rerun or --status." ;;
+    --inproc)
+        PROFILE="inproc"
+        ;;
+    --stub)
+        PROFILE="stub"
+        ;;
+    --max-impact|"")
+        ;;
+    *) die "Unknown option: ${1}. Use --rerun, --status, --inproc, --stub, --max-impact." ;;
 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 "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."
+[[ -f "fuzz_urb" ]] || die "fuzz_urb not found. Run build_fuzzers.sh first."
+[[ -f "fuzz_urb.cmplog" ]] || die "fuzz_urb.cmplog not found."
+[[ -d "${CORPUS_URB}" ]] || die "corpus/urb/ 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."
 
-# 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}"
 
-# ── create tmux session (2×2 grid) ────────────────────────────────────────────
+if [[ "${PROFILE}" == "max-impact" || "${PROFILE}" == "stub" ]]; then
+    if [[ -f "${VMLINUX}" ]]; then
+        KERNEL_PATH="${VMLINUX}"
+    elif [[ -f "${VMLINUX_QEMU}" ]]; then
+        KERNEL_PATH="${VMLINUX_QEMU}"
+    else
+        die "Kernel image not found (expected bzImage or qemu/bzImage)."
+    fi
+fi
+
+if [[ "${PROFILE}" == "max-impact" ]]; then
+    [[ -f "fuzz_vhci_server" ]] || die "fuzz_vhci_server not found. Run build_fuzzers.sh first."
+    [[ -d "${CORPUS_VHCI}" ]] || die "corpus/vhci/ not found. Re-run gen_corpus.py first."
+
+    if [[ -f "${INITRD_VHCI}" ]]; then
+        INITRD_PATH="${INITRD_VHCI}"
+    elif [[ -f "${INITRD_VHCI_QEMU}" ]]; then
+        INITRD_PATH="${INITRD_VHCI_QEMU}"
+    else
+        die "initramfs-vhci.cpio.gz not found. Re-run setup.sh."
+    fi
+fi
+
+if [[ "${PROFILE}" == "stub" ]]; then
+    [[ -f "fuzz_stub_client" ]] || die "fuzz_stub_client not found. Run build_fuzzers.sh first."
+    [[ -d "${CORPUS_STUB}" ]] || die "corpus/stub/ not found. Re-run gen_corpus.py first."
+
+    if [[ -f "${INITRD_SERVER}" ]]; then
+        INITRD_PATH="${INITRD_SERVER}"
+    elif [[ -f "${INITRD_SERVER_QEMU}" ]]; then
+        INITRD_PATH="${INITRD_SERVER_QEMU}"
+    else
+        die "initramfs.cpio.gz not found. Re-run setup.sh."
+    fi
+fi
+
 log "Creating tmux session '${SESSION}'..."
 tmux new-session -d -s "${SESSION}" -x 220 -y 50
-
-# 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_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_PROTOCOL} \
-    -o ${OUTPUT_DIR} \
-    -x ${DICT} \
-    -c ./fuzz_protocol.cmplog \
-    -t ${TIMEOUT} \
-    -p fast \
-    -m none \
-    -- ./fuzz_protocol" \
-    Enter
 
-# ── 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_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
+if [[ "${PROFILE}" == "inproc" ]]; then
+    [[ -f "fuzz_protocol" ]] || die "fuzz_protocol not found. Run build_fuzzers.sh first."
+    [[ -f "fuzz_protocol.cmplog" ]] || die "fuzz_protocol.cmplog not found."
+    [[ -d "${CORPUS_PROTOCOL}" ]] || die "corpus/protocol/ not found. Re-run gen_corpus.py first."
 
-# ── 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} \
-    -p fast \
-    -m none \
-    -- ./fuzz_devlist" \
-    Enter
+    log "Launching AFL++ master (fuzz_protocol)..."
+    tmux send-keys -t "${SESSION}:0.0" \
+        "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_PROTOCOL} \
+        -o ${OUTPUT_DIR} \
+        -x ${DICT} \
+        -c ./fuzz_protocol.cmplog \
+        -t ${TIMEOUT} \
+        -p fast \
+        -m none \
+        -- ./fuzz_protocol" \
+        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
+    log "Launching AFL++ secondary (fuzz_urb)..."
+    tmux send-keys -t "${SESSION}:0.1" \
+        "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
+elif [[ "${PROFILE}" == "max-impact" ]]; then
+    log "Launching AFL++ master (fuzz_urb, in-process lane)..."
+    tmux send-keys -t "${SESSION}:0.0" \
+        "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_URB} \
+        -o ${OUTPUT_DIR} \
+        -x ${DICT} \
+        -c ./fuzz_urb.cmplog \
+        -t ${TIMEOUT} \
+        -p fast \
+        -m none \
+        -- ./fuzz_urb" \
+        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'" \
+    log "Launching AFL++ secondary (fuzz_vhci_server, kernel lane)..."
+    tmux send-keys -t "${SESSION}:0.1" \
+        "AFL_SKIP_CPUFREQ=1 AFL_AUTORESUME=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 \
+        nice -n ${NICE} afl-fuzz \
+        -S vhci \
+        -i ${CORPUS_VHCI} \
+        -o ${OUTPUT_DIR} \
+        -x ${DICT} \
+        -t ${VHCI_TIMEOUT} \
+        -m none \
+        -- ./fuzz_vhci_server @@ 0.0.0.0 13241" \
+        Enter
+
+    log "Starting vhci QEMU VM window..."
+    tmux new-window -t "${SESSION}" -n vm-vhci
+    tmux send-keys -t "${SESSION}:vm-vhci" \
+        "qemu-system-x86_64 \
+        -kernel ${KERNEL_PATH} \
+        -initrd ${INITRD_PATH} \
+        -nographic \
+        -append 'console=ttyS0 quiet panic=-1 oops=panic kasan_multi_shot' \
+        -m 512M \
+        -net nic,model=e1000 -net user" \
+        Enter
+else
+    log "Launching AFL++ master (fuzz_urb, in-process lane)..."
+    tmux send-keys -t "${SESSION}:0.0" \
+        "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_URB} \
+        -o ${OUTPUT_DIR} \
+        -x ${DICT} \
+        -c ./fuzz_urb.cmplog \
+        -t ${TIMEOUT} \
+        -p fast \
+        -m none \
+        -- ./fuzz_urb" \
+        Enter
+
+    log "Launching AFL++ secondary (fuzz_stub_client, kernel lane)..."
+    tmux send-keys -t "${SESSION}:0.1" \
+        "AFL_SKIP_CPUFREQ=1 AFL_AUTORESUME=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 \
+        nice -n ${NICE} afl-fuzz \
+        -S stub \
+        -i ${CORPUS_STUB} \
+        -o ${OUTPUT_DIR} \
+        -x ${DICT} \
+        -t ${STUB_TIMEOUT} \
+        -m none \
+        -- ./fuzz_stub_client @@ 127.0.0.1 13240" \
+        Enter
+
+    log "Starting stub QEMU VM window..."
+    tmux new-window -t "${SESSION}" -n vm-stub
+    tmux send-keys -t "${SESSION}:vm-stub" \
+        "qemu-system-x86_64 \
+        -kernel ${KERNEL_PATH} \
+        -initrd ${INITRD_PATH} \
+        -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:13240-:3240" \
+        Enter
+fi
+
+log "Adding status window..."
+tmux new-window -t "${SESSION}" -n status
+tmux send-keys -t "${SESSION}:status" \
+    "watch -n 10 '$(command -v afl-whatsup) ${OUTPUT_DIR} 2>/dev/null || echo \"output dir: ${OUTPUT_DIR}\"'" \
     Enter
 
 tmux select-window -t "${SESSION}:0"
 
-# ── attach ────────────────────────────────────────────────────────────────────
 ok "Fuzzing session started."
 echo ""
 echo "  Attach:       tmux attach -t ${SESSION}"
 echo "  Status:       ${0} --status"
+echo "  Profile:      ${PROFILE}"
 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)"
+[[ "${PROFILE}" == "inproc" ]] && echo "                ${OUTPUT_DIR}/urb/crashes/"
+[[ "${PROFILE}" == "max-impact" ]] && echo "                ${OUTPUT_DIR}/vhci/crashes/"
+[[ "${PROFILE}" == "max-impact" ]] && echo "  QEMU console: tmux window 'vm-vhci'"
+[[ "${PROFILE}" == "stub" ]] && echo "                ${OUTPUT_DIR}/stub/crashes/"
+[[ "${PROFILE}" == "stub" ]] && echo "  QEMU console: tmux window 'vm-stub'"
 echo "  Stop:         bash cleanup.sh"
 echo ""
 
diff --git a/fuzzing/usbip/setup.sh b/fuzzing/usbip/setup.sh
@@ -227,21 +227,41 @@ 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
+    # usbip client tool (needed by /init auto-bind and /init-vhci attach loop)
+    USBIP_TOOL_SRC=""
+    for cand in \
+        usbipd-plain-install/sbin/usbip \
+        usbipd-plain-install/usr/sbin/usbip \
+        usbipd-plain-install/bin/usbip \
+        usbipd-plain-install/usr/bin/usbip; do
+        [[ -f "${cand}" ]] && USBIP_TOOL_SRC="${cand}" && break
+    done
+    if [[ -n "${USBIP_TOOL_SRC}" ]]; then
+        cp "${USBIP_TOOL_SRC}" initramfs/usr/sbin/usbip
+        chmod +x initramfs/usr/sbin/usbip
+    else
+        warn "usbip client tool not found in usbipd-plain-install (vhci mode may fail)"
     fi
 
+    # If usbipd or usbip are dynamic, bundle their shared libraries
+    for bin in usbipd-plain "${USBIP_TOOL_SRC:-}"; do
+        [[ -n "${bin}" ]] || continue
+        [[ -f "${bin}" ]] || continue
+        if file "${bin}" | grep -q "dynamically linked"; then
+            log "Bundling shared libraries for dynamic $(basename "${bin}")..."
+            ldd "${bin}" | grep -oP '(/[^ ]+\\.so[^ ]*)' | while read lib; do
+                [[ -f "${lib}" ]] || continue
+                libdir="initramfs/$(dirname "${lib#/}")"
+                mkdir -p "${libdir}"
+                cp "${lib}" "${libdir}/"
+            done
+        fi
+    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
+
     # /init script
     cp qemu/init initramfs/init
     chmod +x initramfs/init
@@ -251,6 +271,20 @@ exit 1' > initramfs/usr/sbin/usbipd
     ok "initramfs.cpio.gz: $(du -sh initramfs.cpio.gz | cut -f1)"
 fi
 
+# Build vhci client initramfs from the base image, replacing /init.
+if [[ -f "initramfs-vhci.cpio.gz" ]]; then
+    ok "initramfs-vhci.cpio.gz already exists, skipping."
+else
+    [[ -d "initramfs" ]] || { warn "initramfs/ tree missing, cannot build initramfs-vhci.cpio.gz"; exit 1; }
+    log "Assembling initramfs-vhci.cpio.gz..."
+    rm -rf initramfs-vhci
+    cp -a initramfs initramfs-vhci
+    cp qemu/init-vhci initramfs-vhci/init
+    chmod +x initramfs-vhci/init
+    (cd initramfs-vhci && find . | cpio -oH newc 2>/dev/null | gzip -9) > initramfs-vhci.cpio.gz
+    ok "initramfs-vhci.cpio.gz: $(du -sh initramfs-vhci.cpio.gz | cut -f1)"
+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