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
| | | | |