etc.git


etc

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

commit 17c47f62716fa9f03e65d78366ac78cb5d53a834
parent 9e6b334fa4794525ea6423bf6efb23aebd288b37
Author: djnn <[email protected]>
Date: Sun, 22 Mar 2026 20:45:51 +0900

feat(fuzzers): experimenting with syzkaller

Diffstat:
Afuzzing/linux-usb-stack/backup-crashes.sh | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/linux-usb-stack/build.sh | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/linux-usb-stack/cleanup.sh | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/linux-usb-stack/qemu/init | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/linux-usb-stack/qemu/kernel.config | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/linux-usb-stack/run-fuzzers.sh | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/linux-usb-stack/setup.sh | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/linux-usb-stack/syzkaller-gadget-fw.cfg | 42++++++++++++++++++++++++++++++++++++++++++
Afuzzing/linux-usb-stack/syzkaller-raw-gadget.cfg | 44++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/linux-usb-stack/syzkaller-usbfs.cfg | 47+++++++++++++++++++++++++++++++++++++++++++++++

10 files changed, 1132 insertions(+), 0 deletions(-)

diff --git a/fuzzing/linux-usb-stack/backup-crashes.sh b/fuzzing/linux-usb-stack/backup-crashes.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+# backup-crashes.sh — archive unique syzkaller crashes with deduplication.
+#
+# Intended to run as a cron job or in a background loop:
+#   crontab -e
+#   */15 * * * *  /path/to/linux-usb-stack/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
+#
+# syzkaller stores crashes as directories under workdir-*/crashes/<id>/
+# Each crash dir contains: description, log0, report0 (and possibly log1, report1, etc.)
+#
+# This script:
+#   1. Scans workdir-*/crashes/ for crash dirs newer than .last_backup stamp
+#   2. Deduplicates by sha256 of the report0 file (unique crash signatures)
+#   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}"
+
+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 directories ────────────────────────────────────────────────
+# syzkaller crash dirs contain a 'report0' file with the kernel crash report
+TMPLIST="$(mktemp)"
+trap "rm -f ${TMPLIST}" EXIT
+
+find workdir-*/crashes/ -maxdepth 2 -name "report0" -newer "${STAMP_FILE}" \
+    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 report(s) since last backup."
+
+# ── deduplicate by sha256 of report0 ─────────────────────────────────────────
+TMPSTAGE="$(mktemp -d)"
+trap "rm -rf ${TMPSTAGE} ${TMPLIST}" EXIT
+
+NEW=0
+while IFS= read -r report; do
+    hash="$(sha256sum "${report}" | cut -d' ' -f1)"
+    if ! grep -qF "${hash}" "${SEEN_HASHES}" 2>/dev/null; then
+        # Archive the entire crash directory (includes description, logs, reports)
+        crash_dir="$(dirname "${report}")"
+        crash_name="$(basename "${crash_dir}")"
+        cp -r "${crash_dir}" "${TMPSTAGE}/${crash_name}"
+        echo "${hash}" >> "${SEEN_HASHES}"
+        NEW=$((NEW + 1))
+    fi
+done < "${TMPLIST}"
+
+if [[ "${NEW}" -eq 0 ]]; then
+    log "All ${TOTAL} new reports 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})"
+
+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/linux-usb-stack/build.sh b/fuzzing/linux-usb-stack/build.sh
@@ -0,0 +1,95 @@
+#!/bin/bash
+# build.sh — incremental rebuild for the linux-usb-stack fuzzing environment.
+#
+# Re-runs after kernel or syzkaller source changes.
+# Does NOT re-download sources; run setup.sh for first-time setup.
+#
+# Usage:
+#   bash build.sh              # rebuild kernel + syzkaller + initramfs
+#   bash build.sh --kernel     # rebuild kernel only
+#   bash build.sh --syzkaller  # rebuild syzkaller only
+#   bash build.sh --initramfs  # repack initramfs only
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+DO_KERNEL=1
+DO_SYZKALLER=1
+DO_INITRAMFS=1
+
+if [[ $# -gt 0 ]]; then
+    DO_KERNEL=0; DO_SYZKALLER=0; DO_INITRAMFS=0
+    for arg in "$@"; do
+        [[ "${arg}" == "--kernel"     ]] && DO_KERNEL=1
+        [[ "${arg}" == "--syzkaller"  ]] && DO_SYZKALLER=1
+        [[ "${arg}" == "--initramfs"  ]] && DO_INITRAMFS=1
+    done
+fi
+
+log()  { echo "[*] $*"; }
+ok()   { echo "[+] $*"; }
+die()  { echo "[!] $*" >&2; exit 1; }
+
+# ── Go path ──────────────────────────────────────────────────────────────────
+for cand in "${SCRIPT_DIR}/go/bin/go" /usr/local/go/bin/go; do
+    [[ -x "${cand}" ]] && export PATH="$(dirname "${cand}"):${PATH}" && break
+done
+export GOPATH="${SCRIPT_DIR}/gopath"
+
+# ── kernel ───────────────────────────────────────────────────────────────────
+if [[ "${DO_KERNEL}" -eq 1 ]]; then
+    [[ -d "linux" ]] || die "linux/ not found. Run setup.sh first."
+    log "Rebuilding kernel..."
+    cd linux
+    make -j"$(nproc)" bzImage 2>&1 | tee /tmp/kbuild.log | \
+        grep -E "^\s*(CC|LD|AR|error:|warning: )" || \
+        { grep "error:" /tmp/kbuild.log | tail -20; exit 1; }
+    cp arch/x86/boot/bzImage "${SCRIPT_DIR}/bzImage"
+    cd "${SCRIPT_DIR}"
+    ok "Kernel rebuilt: bzImage"
+fi
+
+# ── syzkaller ─────────────────────────────────────────────────────────────────
+if [[ "${DO_SYZKALLER}" -eq 1 ]]; then
+    [[ -d "syzkaller" ]] || die "syzkaller/ not found. Run setup.sh first."
+    log "Rebuilding syzkaller..."
+    cd syzkaller
+    make all -j"$(nproc)" 2>&1 | tail -5
+    cd "${SCRIPT_DIR}"
+    ok "syzkaller rebuilt."
+fi
+
+# ── initramfs ─────────────────────────────────────────────────────────────────
+if [[ "${DO_INITRAMFS}" -eq 1 ]]; then
+    [[ -d "busybox-install" ]]            || die "busybox-install/ not found. Run setup.sh first."
+    [[ -f "dropbear-install/sbin/dropbear" ]] || die "dropbear not found. Run setup.sh first."
+    [[ -f "qemu/id_rsa.pub" ]]            || die "qemu/id_rsa.pub not found. Run setup.sh first."
+
+    log "Repacking initramfs..."
+    rm -rf initramfs
+    mkdir -p initramfs/{bin,sbin,usr/bin,usr/sbin,proc,sys,dev,tmp,run,lib,lib64,etc,etc/dropbear,root/.ssh}
+
+    cp busybox-install/bin/busybox initramfs/bin/busybox
+    (cd initramfs && bin/busybox --list | while read applet; do
+        ln -sf /bin/busybox bin/"${applet}" 2>/dev/null || true
+    done)
+    ln -sf /bin/busybox initramfs/bin/sh
+    ln -sf /bin/busybox initramfs/sbin/ip
+    ln -sf /bin/busybox initramfs/sbin/modprobe
+
+    cp dropbear-install/sbin/dropbear    initramfs/sbin/dropbear
+    cp dropbear-install/sbin/dropbearkey initramfs/sbin/dropbearkey
+    chmod +x initramfs/sbin/dropbear initramfs/sbin/dropbearkey
+
+    cp qemu/id_rsa.pub initramfs/root/.ssh/authorized_keys
+    chmod 700 initramfs/root/.ssh
+    chmod 600 initramfs/root/.ssh/authorized_keys
+
+    cp qemu/init initramfs/init
+    chmod +x initramfs/init
+
+    (cd initramfs && find . | cpio -oH newc 2>/dev/null | gzip -9) > qemu/initramfs.cpio.gz
+    ok "initramfs repacked: $(du -sh qemu/initramfs.cpio.gz | cut -f1)"
+fi
diff --git a/fuzzing/linux-usb-stack/cleanup.sh b/fuzzing/linux-usb-stack/cleanup.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+# cleanup.sh — terminate all fuzzing processes and optionally remove build artifacts.
+#
+# Options:
+#   --keep-corpus    preserve workdir-*/ (crash data + corpus)
+#   --keep-builds    preserve compiled binaries, kernel, syzkaller, initramfs
+#   --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="usb-fuzz"
+
+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 syz-manager and QEMU ─────────────────────────────────────────────────
+pkill -u "$(id -un)" -f "syz-manager"       2>/dev/null || true
+pkill -u "$(id -un)" -f "syz-fuzzer"        2>/dev/null || true
+pkill -u "$(id -un)" -f "syz-executor"      2>/dev/null || true
+pkill -u "$(id -un)" qemu-system-x86_64     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 kernel + initramfs..."
+    rm -f bzImage
+    rm -f qemu/initramfs.cpio.gz
+    rm -rf initramfs/
+
+    log "Removing source trees (kernel, busybox, dropbear, syzkaller, Go)..."
+    rm -rf linux/
+    rm -rf busybox-*/  busybox-install/
+    rm -rf dropbear-*/ dropbear-install/
+    rm -rf syzkaller/
+    rm -rf go/ gopath/
+
+    log "Removing SSH keys..."
+    rm -f qemu/id_rsa qemu/id_rsa.pub
+
+    ok "Build artifacts removed."
+fi
+
+# ── remove fuzzing data ───────────────────────────────────────────────────────
+if [[ "${KEEP_CORPUS}" -eq 0 ]]; then
+    log "Removing workdirs and fuzzing output..."
+    rm -rf workdir-raw-gadget/ workdir-gadget-fw/ workdir-usbfs/
+    rm -f .last_backup .seen_crash_hashes
+    ok "Workdirs removed."
+else
+    log "Keeping workdir-*/ (--keep-corpus)."
+fi
+
+ok "Cleanup complete."
diff --git a/fuzzing/linux-usb-stack/qemu/init b/fuzzing/linux-usb-stack/qemu/init
@@ -0,0 +1,89 @@
+#!/bin/sh
+#
+# /init — minimal initramfs init for the linux-usb-stack fuzzing VM.
+#
+# Goals:
+#   1. Mount virtual filesystems (proc, sysfs, debugfs, configfs)
+#   2. Configure networking for syzkaller SSH access (10.0.2.15/24)
+#   3. Load USB kernel modules (raw_gadget, dummy_hcd, xhci_pci, ehci_pci)
+#   4. Start dropbear SSH server on port 22
+#
+# syzkaller connects via SSH, uploads syz-executor, and drives the fuzzer.
+# The VM stays alive in a sleep loop until syzkaller kills it.
+
+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
+mount -t tmpfs    tmpfs    /run
+
+# debugfs: required for KCOV (/sys/kernel/debug/kcov)
+mount -t debugfs  debugfs  /sys/kernel/debug 2>/dev/null || true
+
+# configfs: required for USB gadget framework
+# syzkaller-gadget-fw profile creates gadget dirs here
+mount -t configfs configfs /sys/kernel/config 2>/dev/null || true
+
+# ── kernel tuning ──────────────────────────────────────────────────────
+echo 1 > /proc/sys/kernel/printk
+echo 1 > /proc/sys/kernel/panic_on_oops
+echo 1 > /proc/sys/kernel/panic
+
+# ── networking (QEMU user-mode: 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 kernel modules ─────────────────────────────────────────────────
+# If built as =y these are no-ops. If =m they load here.
+# dummy_hcd: virtual USB host+device controller pair.
+#   Required for raw_gadget to work — raw_gadget presents a fake USB device
+#   to the kernel's USB host stack via dummy_hcd's host side.
+modprobe dummy_hcd      2>/dev/null || true
+
+# usb_raw_gadget: exposes gadget endpoint to userspace via /dev/raw-gadget.
+#   syzkaller's syz_usb_connect pseudo-syscall opens this device and sends
+#   crafted USB descriptors to the kernel.
+modprobe usb_raw_gadget 2>/dev/null || true
+
+# Host controller drivers: exercised when dummy_hcd connects a fake device
+# and the kernel's USB core tries to enumerate it through xhci/ehci code.
+modprobe xhci_pci       2>/dev/null || true
+modprobe ehci_pci       2>/dev/null || true
+
+# USB class drivers: bind automatically when a compatible fake device enumerates.
+modprobe usbhid         2>/dev/null || true
+modprobe usb_storage    2>/dev/null || true
+
+# ── SSH setup for syzkaller ────────────────────────────────────────────
+# authorized_keys was baked in at initramfs build time (setup.sh injects
+# the public key matching qemu/id_rsa used by syz-manager).
+mkdir -p /root/.ssh
+chmod 700 /root/.ssh
+# authorized_keys is already present from initramfs assembly
+chmod 600 /root/.ssh/authorized_keys 2>/dev/null || true
+
+# Generate ephemeral dropbear host keys (new each VM boot)
+mkdir -p /etc/dropbear
+dropbearkey -t rsa   -f /etc/dropbear/dropbear_rsa_host_key  2>/dev/null
+dropbearkey -t ecdsa -f /etc/dropbear/dropbear_ecdsa_host_key 2>/dev/null
+
+# Start dropbear SSH daemon
+# -s: no password auth  -g: no root password login  -E: log to stderr
+dropbear -s -g -E -p 22
+
+echo "usb-fuzz: VM ready — SSH on :22, configfs at /sys/kernel/config"
+
+# ── keep init alive ────────────────────────────────────────────────────
+# syzkaller SSHs in and runs syz-executor independently.
+# Kernel panics from KASAN/UBSAN cause an immediate reboot (panic_on_oops=1),
+# which syzkaller detects as a VM crash → crash is recorded → VM rebooted.
+while true; do
+    sleep 60
+done
diff --git a/fuzzing/linux-usb-stack/qemu/kernel.config b/fuzzing/linux-usb-stack/qemu/kernel.config
@@ -0,0 +1,186 @@
+# Linux USB stack fuzzing VM — minimal kernel configuration fragment.
+#
+# Applied over `make allnoconfig` using:
+#   scripts/kconfig/merge_config.sh -m .config ../qemu/kernel.config
+#   make olddefconfig
+#
+# Targets: USB core/enumeration, USB gadget framework, host controller drivers.
+# Instrument with KASAN + UBSAN + KCOV for maximum crash visibility.
+# syzkaller uses KCOV for coverage feedback and KASAN to detect memory errors.
+
+# ── 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
+# KASAN requires slab merging disabled — merged slabs collapse redzone separation
+CONFIG_SLAB_MERGE_DEFAULT=n
+
+# ── 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
+CONFIG_VIRTIO_NET=y
+
+# ── PCI (needed for e1000 and USB host controllers) ──────────────────────
+CONFIG_PCI=y
+CONFIG_PCIEPORTBUS=y
+
+# ── USB core ─────────────────────────────────────────────────────────────
+CONFIG_USB_SUPPORT=y
+CONFIG_USB=y
+CONFIG_USB_PCI=y
+CONFIG_USB_HCD_TEST_MODE=n
+CONFIG_USB_ANNOUNCE_NEW_DEVICES=y
+
+# ── USB host controllers (xHCI + EHCI exercised by dummy_hcd enumeration) ─
+CONFIG_USB_XHCI_HCD=y
+CONFIG_USB_XHCI_PCI=y
+CONFIG_USB_EHCI_HCD=y
+CONFIG_USB_EHCI_PCI=y
+CONFIG_USB_OHCI_HCD=y
+CONFIG_USB_OHCI_HCD_PCI=y
+
+# ── raw_gadget + dummy_hcd — the core of syz_usb_connect fuzzing ─────────
+#
+# dummy_hcd acts as both a USB host controller and a USB device controller
+# simultaneously. raw_gadget exposes the device side to userspace, so
+# syzkaller can present arbitrary USB descriptors to the kernel's USB core.
+#
+# Attack flow: syzkaller writes crafted descriptors via raw_gadget →
+#   USB core parses them (CONFIG_USB) → host controller driver handles URBs
+#   (CONFIG_USB_XHCI_HCD etc.) → class drivers bind (HID, storage, etc.)
+CONFIG_USB_GADGET=y
+CONFIG_USB_DUMMY_HCD=y
+CONFIG_USB_RAW_GADGET=y
+
+# ── USB gadget framework (configfs interface) ────────────────────────────
+# Exercised by syzkaller-gadget-fw profile: mkdir/write on configfs paths.
+CONFIG_CONFIGFS_FS=y
+CONFIG_USB_CONFIGFS=y
+CONFIG_USB_CONFIGFS_SERIAL=y
+CONFIG_USB_CONFIGFS_ACM=y
+CONFIG_USB_CONFIGFS_NCM=y
+CONFIG_USB_CONFIGFS_MASS_STORAGE=y
+CONFIG_USB_CONFIGFS_F_HID=y
+CONFIG_USB_CONFIGFS_F_UVC=y
+
+# ── USB class drivers (bind when fake device enumerates via dummy_hcd) ───
+CONFIG_USB_HID=y
+CONFIG_HID=y
+CONFIG_HID_GENERIC=y
+CONFIG_USB_HIDDEV=y
+CONFIG_USB_STORAGE=y
+CONFIG_USB_ACM=y
+CONFIG_USB_SERIAL=y
+CONFIG_USB_SERIAL_GENERIC=y
+
+# ── modules ──────────────────────────────────────────────────────────────
+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
+
+# ── clock / timers ───────────────────────────────────────────────────────
+CONFIG_RTC_CLASS=y
+CONFIG_HW_RANDOM=y
+
+# ── namespaces (syzkaller sandbox support) ───────────────────────────────
+CONFIG_NAMESPACES=y
+CONFIG_UTS_NS=y
+CONFIG_IPC_NS=y
+CONFIG_PID_NS=y
+CONFIG_NET_NS=y
+CONFIG_USER_NS=y
+CONFIG_CGROUPS=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_INPUT=n
+CONFIG_SERIO=n
+CONFIG_WIRELESS=n
+CONFIG_RFKILL=n
+CONFIG_CRYPTO_HW=n
+
+# ── sanitizers (KASAN, KCOV, UBSAN) ─────────────────────────────────────
+# KASAN: catches heap overflows, UAF, OOB reads.
+# Overhead: ~2x slowdown, 8x extra RAM. Use -m 2048M in QEMU.
+CONFIG_KASAN=y
+CONFIG_KASAN_GENERIC=y
+CONFIG_KASAN_OUTLINE=y
+CONFIG_KASAN_TAGS=n
+
+# UBSAN: catches undefined behaviour (integer overflow, OOB, etc.)
+CONFIG_UBSAN=y
+CONFIG_UBSAN_BOUNDS=y
+CONFIG_UBSAN_SANITIZE_ALL=y
+
+CONFIG_DEBUG_LIST=y
+
+# KCOV: syzkaller's coverage feedback mechanism.
+# syzkaller reads /sys/kernel/debug/kcov per-task to guide mutation.
+CONFIG_KCOV=y
+CONFIG_KCOV_ENABLE_COMPARISONS=y
+CONFIG_KCOV_INSTRUMENT_ALL=y
+
+CONFIG_DEBUG_FS=y
+CONFIG_PANIC_ON_OOPS=y
+
+CONFIG_CC_OPTIMIZE_FOR_SIZE=n
+CONFIG_KERNEL_XZ=n
+CONFIG_KERNEL_GZIP=y
diff --git a/fuzzing/linux-usb-stack/run-fuzzers.sh b/fuzzing/linux-usb-stack/run-fuzzers.sh
@@ -0,0 +1,174 @@
+#!/bin/bash
+# run-fuzzers.sh — launch linux-usb-stack syzkaller fuzzing in tmux.
+#
+# Three fuzzing profiles targeting different USB attack surfaces:
+#
+#   --raw-gadget  malicious USB device via syz_usb_* + raw_gadget (DEFAULT)
+#                 Exercises: USB core descriptor parsing, host controller drivers,
+#                 class driver binding on enumeration.
+#
+#   --gadget-fw   USB gadget framework via configfs syscalls
+#                 Exercises: composite gadget, function drivers, gadget lifecycle.
+#
+#   --usbfs       userspace → kernel via /dev/bus/usb ioctls (usbfs/devio)
+#                 Exercises: URB submission/cancellation, control transfers,
+#                 concurrent access bugs, ioctl integer overflows.
+#
+#   --all         all three profiles simultaneously (uses 6 QEMU VMs total)
+#
+# Usage:
+#   bash run-fuzzers.sh [--raw-gadget|--gadget-fw|--usbfs|--all]
+#   bash run-fuzzers.sh --status   # show crash counts
+#   bash run-fuzzers.sh --rerun    # kill existing session and restart
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+SESSION="usb-fuzz"
+PROFILE="raw-gadget"
+
+log()  { echo "[*] $*"; }
+ok()   { echo "[+] $*"; }
+die()  { echo "[!] $*" >&2; exit 1; }
+
+case "${1:-}" in
+    --status)
+        # Pull Prometheus metrics from each running syz-manager instance.
+        # syzkaller exposes /metrics in Prometheus text format — machine-readable,
+        # no HTML parsing needed. Falls back to disk-based crash count if the
+        # manager isn't responding (e.g. still starting up, or not launched).
+        _status_profile() {
+            local name="${1}" port="${2}"
+            local metrics crash_count corpus_size coverage exec_total uptime
+
+            metrics="$(curl -sf --max-time 2 "http://127.0.0.1:${port}/metrics" 2>/dev/null || true)"
+
+            if [[ -n "${metrics}" ]]; then
+                _metric() { echo "${metrics}" | grep "^${1} " | awk '{print $2}' | head -1; }
+                crash_count="$(_metric syz_crash_count 2>/dev/null || echo '?')"
+                corpus_size="$(_metric syz_corpus_size 2>/dev/null || echo '?')"
+                coverage="$(_metric syz_coverage      2>/dev/null || echo '?')"
+                exec_total="$(_metric syz_exec_total  2>/dev/null || echo '?')"
+                uptime="$(_metric syz_uptime_secs     2>/dev/null || echo '?')"
+                printf "  %-12s  UP    crashes=%-4s corpus=%-6s coverage=%-8s execs=%s\n" \
+                    "${name}" "${crash_count:-?}" "${corpus_size:-?}" "${coverage:-?}" "${exec_total:-?}"
+            else
+                # Manager not responding — count crash dirs on disk as fallback
+                local disk_crashes=0
+                [[ -d "workdir-${name}/crashes" ]] && \
+                    disk_crashes=$(find "workdir-${name}/crashes" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
+                printf "  %-12s  DOWN  (crashes on disk: %d)\n" "${name}" "${disk_crashes}"
+            fi
+        }
+
+        echo "=== syzkaller USB stack fuzzer status ==="
+        echo ""
+        _status_profile "raw-gadget" 56741
+        _status_profile "gadget-fw"  56742
+        _status_profile "usbfs"      56743
+        echo ""
+        echo "  Web UIs:  http://127.0.0.1:56741  (raw-gadget)"
+        echo "            http://127.0.0.1:56742  (gadget-fw)"
+        echo "            http://127.0.0.1:56743  (usbfs)"
+        echo ""
+        echo "  Tip: curl -s http://127.0.0.1:PORT/metrics | grep '^syz_'"
+        exit 0 ;;
+    --rerun)
+        tmux kill-session -t "${SESSION}" 2>/dev/null || true
+        ;;
+    --raw-gadget|"")
+        PROFILE="raw-gadget" ;;
+    --gadget-fw)
+        PROFILE="gadget-fw" ;;
+    --usbfs)
+        PROFILE="usbfs" ;;
+    --all)
+        PROFILE="all" ;;
+    *) die "Unknown option: ${1}. Use --raw-gadget, --gadget-fw, --usbfs, --all, --status, --rerun." ;;
+esac
+
+# ── preflight checks ─────────────────────────────────────────────────────────
+[[ -x "syzkaller/bin/linux_amd64/syz-manager" ]] || die "syz-manager not found. Run setup.sh first."
+[[ -f "bzImage" ]]                               || die "bzImage not found. Run setup.sh first."
+[[ -f "qemu/initramfs.cpio.gz" ]]               || die "qemu/initramfs.cpio.gz not found. Run setup.sh first."
+[[ -f "qemu/id_rsa" ]]                           || die "qemu/id_rsa not found. Run setup.sh first."
+command -v qemu-system-x86_64 > /dev/null        || die "qemu-system-x86_64 not found."
+
+tmux list-sessions 2>/dev/null | grep -q "^${SESSION}:" && \
+    die "Session '${SESSION}' already running. Use --rerun to restart or --status to check."
+
+# ── generate config from template (substitute __DIR__ with actual path) ──────
+gen_config() {
+    local profile="${1}"
+    local tmpfile
+    tmpfile="$(mktemp /tmp/syz-${profile}-XXXXXX.cfg)"
+    sed "s|__DIR__|${SCRIPT_DIR}|g" "${SCRIPT_DIR}/syzkaller-${profile}.cfg" > "${tmpfile}"
+    echo "${tmpfile}"
+}
+
+# ── launch a single syz-manager instance in a named tmux window ──────────────
+launch_profile() {
+    local profile="${1}"
+    local cfg
+    cfg="$(gen_config "${profile}")"
+
+    mkdir -p "workdir-${profile}"
+
+    local port
+    case "${profile}" in
+        raw-gadget) port=56741 ;;
+        gadget-fw)  port=56742 ;;
+        usbfs)      port=56743 ;;
+    esac
+
+    log "Launching syz-manager for profile '${profile}' (http://127.0.0.1:${port})..."
+    tmux new-window -t "${SESSION}" -n "${profile}"
+    tmux send-keys -t "${SESSION}:${profile}" \
+        "${SCRIPT_DIR}/syzkaller/bin/linux_amd64/syz-manager -config ${cfg}" \
+        Enter
+}
+
+# ── create tmux session ───────────────────────────────────────────────────────
+log "Creating tmux session '${SESSION}'..."
+tmux new-session -d -s "${SESSION}" -x 220 -y 50
+
+# The first window is created automatically; rename it to match the first profile.
+# We'll use it for the status monitor.
+
+if [[ "${PROFILE}" == "all" ]]; then
+    launch_profile "raw-gadget"
+    launch_profile "gadget-fw"
+    launch_profile "usbfs"
+else
+    launch_profile "${PROFILE}"
+fi
+
+# ── status window ─────────────────────────────────────────────────────────────
+tmux rename-window -t "${SESSION}:0" "status"
+tmux send-keys -t "${SESSION}:status" \
+    "watch -n 15 'bash ${SCRIPT_DIR}/run-fuzzers.sh --status'" \
+    Enter
+
+tmux select-window -t "${SESSION}:status"
+
+ok "Fuzzing session started."
+echo ""
+echo "  Attach:    tmux attach -t ${SESSION}"
+echo "  Status:    ${0} --status"
+echo "  Profile:   ${PROFILE}"
+echo ""
+case "${PROFILE}" in
+    raw-gadget) echo "  Web UI:    http://127.0.0.1:56741" ;;
+    gadget-fw)  echo "  Web UI:    http://127.0.0.1:56742" ;;
+    usbfs)      echo "  Web UI:    http://127.0.0.1:56743" ;;
+    all)        echo "  Web UIs:   http://127.0.0.1:56741  (raw-gadget)"
+                echo "             http://127.0.0.1:56742  (gadget-fw)"
+                echo "             http://127.0.0.1:56743  (usbfs)" ;;
+esac
+echo "  Crashes:   workdir-*/crashes/"
+echo "  Stop:      bash cleanup.sh"
+echo ""
+
+tmux attach -t "${SESSION}"
diff --git a/fuzzing/linux-usb-stack/setup.sh b/fuzzing/linux-usb-stack/setup.sh
@@ -0,0 +1,281 @@
+#!/bin/bash
+# setup.sh — one-shot bootstrap for the linux-usb-stack syzkaller fuzzing environment.
+#
+# What this does (in order):
+#   1. Install system dependencies
+#   2. Install Go (from upstream if system Go < 1.21)
+#   3. Clone + build syzkaller
+#   4. Download + configure + build Linux mainline kernel
+#   5. Build static busybox
+#   6. Build static dropbear (SSH server for syzkaller VM access)
+#   7. Generate SSH key pair for syzkaller ↔ VM communication
+#   8. Assemble initramfs (busybox + dropbear + authorized_keys + /init)
+#   9. Create workdirs for each fuzzing profile
+#
+# Re-running is safe: each step checks for existing artifacts and skips.
+#
+# Usage:
+#   bash setup.sh
+#   bash setup.sh --no-kernel      # skip kernel build (use existing bzImage)
+#   bash setup.sh --no-syzkaller   # skip syzkaller build
+#   bash setup.sh --skip-deps      # skip apt-get installs
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+BUSYBOX_VER="${BUSYBOX_VER:-1.36.1}"
+DROPBEAR_VER="${DROPBEAR_VER:-2024.86}"
+GO_MIN_VER="1.21"
+GO_INSTALL_VER="${GO_INSTALL_VER:-1.22.4}"  # upstream Go version to install if needed
+SYZKALLER_REPO="https://github.com/google/syzkaller"
+LINUX_REPO="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git"
+
+SKIP_KERNEL=0
+SKIP_SYZKALLER=0
+SKIP_DEPS=0
+for arg in "$@"; do
+    [[ "${arg}" == "--no-kernel"    ]] && SKIP_KERNEL=1
+    [[ "${arg}" == "--no-syzkaller" ]] && SKIP_SYZKALLER=1
+    [[ "${arg}" == "--skip-deps"    ]] && SKIP_DEPS=1
+done
+
+log()  { echo "[*] $*"; }
+ok()   { echo "[+] $*"; }
+warn() { echo "[!] $*"; }
+
+# ── 1. System dependencies ───────────────────────────────────────────────────
+if [[ "${SKIP_DEPS}" -eq 1 ]]; then
+    ok "Skipping dependency install (--skip-deps)."
+else
+    log "Installing system dependencies..."
+    sudo apt-get update -qq
+    sudo apt-get install -y \
+        build-essential git curl wget python3 python3-pip tmux \
+        libelf-dev libssl-dev flex bison bc cpio \
+        libncurses-dev libncurses5-dev \
+        pkg-config autoconf automake libtool \
+        qemu-system-x86 \
+        gcc-multilib \
+        openssh-client \
+        zlib1g-dev \
+        gdb
+    ok "System dependencies installed."
+fi
+
+# ── 2. Go ────────────────────────────────────────────────────────────────────
+# syzkaller requires Go >= 1.21. Check system Go; install upstream if too old.
+GO_BIN=""
+for cand in "$(go env GOROOT 2>/dev/null)/bin/go" "${SCRIPT_DIR}/go/bin/go" /usr/local/go/bin/go; do
+    if [[ -x "${cand}" ]]; then
+        ver="$("${cand}" version | awk '{print $3}' | sed 's/go//')"
+        major="${ver%%.*}"
+        minor="${ver#*.}"; minor="${minor%%.*}"
+        req_minor="${GO_MIN_VER#*.}"
+        if [[ "${major}" -gt 1 ]] || [[ "${major}" -eq 1 && "${minor}" -ge "${req_minor}" ]]; then
+            GO_BIN="${cand}"
+            break
+        fi
+    fi
+done
+
+if [[ -z "${GO_BIN}" ]]; then
+    log "Go >= ${GO_MIN_VER} not found. Installing Go ${GO_INSTALL_VER} from upstream..."
+    GO_ARCHIVE="go${GO_INSTALL_VER}.linux-amd64.tar.gz"
+    GO_URL="https://golang.org/dl/${GO_ARCHIVE}"
+    wget -q --show-progress -c "${GO_URL}" -O "${GO_ARCHIVE}"
+    tar -C "${SCRIPT_DIR}" -xzf "${GO_ARCHIVE}"
+    rm -f "${GO_ARCHIVE}"
+    GO_BIN="${SCRIPT_DIR}/go/bin/go"
+    ok "Go ${GO_INSTALL_VER} installed to ${SCRIPT_DIR}/go/"
+else
+    ok "Go found: ${GO_BIN} ($(${GO_BIN} version | awk '{print $3}'))"
+fi
+
+export PATH="$(dirname "${GO_BIN}"):${PATH}"
+export GOPATH="${SCRIPT_DIR}/gopath"
+export GOROOT="$(dirname "$(dirname "${GO_BIN}")")"
+
+# ── 3. syzkaller ─────────────────────────────────────────────────────────────
+if [[ "${SKIP_SYZKALLER}" -eq 1 ]]; then
+    ok "Skipping syzkaller build (--no-syzkaller)."
+elif [[ -x "${SCRIPT_DIR}/syzkaller/bin/linux_amd64/syz-manager" ]]; then
+    ok "syzkaller already built, skipping."
+else
+    log "Cloning syzkaller..."
+    [[ -d "syzkaller" ]] || git clone --depth=1 "${SYZKALLER_REPO}" syzkaller
+
+    log "Building syzkaller (this takes a few minutes)..."
+    cd syzkaller
+    make all -j"$(nproc)" 2>&1 | tail -5
+    cd "${SCRIPT_DIR}"
+    ok "syzkaller built: syzkaller/bin/linux_amd64/syz-manager"
+fi
+
+# ── 4. Linux kernel (mainline) ───────────────────────────────────────────────
+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 "linux" ]]; then
+        log "Cloning Linux mainline (shallow — latest tag only)..."
+        git clone --depth=1 "${LINUX_REPO}" linux
+    fi
+
+    log "Configuring kernel (allnoconfig + USB stack fragment)..."
+    cd linux
+
+    # GCC 15+ defaults to C23, where 'bool'/'false' are keywords — patch compressed/Makefile
+    if gcc --version 2>&1 | grep -qE "gcc \(.*\) (1[5-9]|[2-9][0-9])\."; then
+        sed -i 's/^KBUILD_CFLAGS := /KBUILD_CFLAGS := -std=gnu11 /' \
+            arch/x86/boot/compressed/Makefile
+        warn "GCC 15+ detected: patched compressed/Makefile with -std=gnu11"
+    fi
+
+    make allnoconfig
+    scripts/kconfig/merge_config.sh -m .config "${SCRIPT_DIR}/qemu/kernel.config"
+    make olddefconfig
+
+    log "Building kernel (this takes ~20-30 min on first run)..."
+    make -j"$(nproc)" bzImage 2>&1 | tee /tmp/kbuild.log | \
+        grep -E "^\s*(CC|LD|AR|error:|warning: )" || \
+        { grep "error:" /tmp/kbuild.log | tail -20; exit 1; }
+
+    cp arch/x86/boot/bzImage "${SCRIPT_DIR}/bzImage"
+    cd "${SCRIPT_DIR}"
+    ok "Kernel built: bzImage"
+fi
+
+# ── 5. 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
+    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
+    sed -i 's/CONFIG_TC=y/CONFIG_TC=n/' .config
+    echo "CONFIG_TC=n" >> .config
+    make oldconfig < /dev/null || true
+    make -j"$(nproc)"
+    make install CONFIG_PREFIX="${SCRIPT_DIR}/busybox-install"
+    cd "${SCRIPT_DIR}"
+    ok "Busybox installed to busybox-install/"
+fi
+
+# ── 6. Dropbear (static SSH server) ──────────────────────────────────────────
+# syzkaller connects to VMs via SSH. We bake a static dropbear into the initramfs
+# so the VM runs a minimal SSH server without needing a full OS.
+DROPBEAR_DIR="dropbear-${DROPBEAR_VER}"
+DROPBEAR_TAR="${DROPBEAR_DIR}.tar.bz2"
+DROPBEAR_URL="https://matt.ucc.asn.au/dropbear/releases/${DROPBEAR_TAR}"
+
+if [[ -f "dropbear-install/sbin/dropbear" ]]; then
+    ok "Dropbear already built, skipping."
+else
+    if [[ ! -d "${DROPBEAR_DIR}" ]]; then
+        log "Downloading dropbear ${DROPBEAR_VER}..."
+        wget -q --show-progress -c "${DROPBEAR_URL}"
+        tar -xjf "${DROPBEAR_TAR}"
+        rm -f "${DROPBEAR_TAR}"
+    fi
+
+    log "Building static dropbear (SSH server + dropbearkey)..."
+    cd "${DROPBEAR_DIR}"
+    ./configure \
+        --prefix="${SCRIPT_DIR}/dropbear-install" \
+        --disable-syslog \
+        --disable-pam \
+        --disable-utmp \
+        --disable-wtmp \
+        --disable-lastlog \
+        CFLAGS="-O2 -static" \
+        LDFLAGS="-static"
+
+    # Build only the server + key generator (not the client)
+    make PROGRAMS="dropbear dropbearkey" -j"$(nproc)"
+    mkdir -p "${SCRIPT_DIR}/dropbear-install/sbin"
+    cp dropbear "${SCRIPT_DIR}/dropbear-install/sbin/dropbear"
+    cp dropbearkey "${SCRIPT_DIR}/dropbear-install/sbin/dropbearkey"
+    cd "${SCRIPT_DIR}"
+    ok "Dropbear installed to dropbear-install/"
+fi
+
+# ── 7. SSH key pair ───────────────────────────────────────────────────────────
+# syz-manager uses qemu/id_rsa to SSH into VMs.
+# The matching public key is baked into initramfs as authorized_keys.
+if [[ -f "qemu/id_rsa" ]]; then
+    ok "SSH key pair already exists."
+else
+    log "Generating SSH key pair for syzkaller ↔ VM access..."
+    ssh-keygen -t rsa -b 4096 -f qemu/id_rsa -N "" -C "syz-manager@usb-fuzz"
+    ok "SSH keys: qemu/id_rsa (private), qemu/id_rsa.pub (public)"
+fi
+
+# ── 8. Initramfs ─────────────────────────────────────────────────────────────
+if [[ -f "qemu/initramfs.cpio.gz" ]]; then
+    ok "qemu/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,etc/dropbear,root/.ssh}
+
+    # Busybox — all applets as symlinks
+    cp busybox-install/bin/busybox initramfs/bin/busybox
+    (cd initramfs && bin/busybox --list | while read applet; do
+        ln -sf /bin/busybox bin/"${applet}" 2>/dev/null || true
+    done)
+    ln -sf /bin/busybox initramfs/bin/sh
+    ln -sf /bin/busybox initramfs/sbin/ip
+    ln -sf /bin/busybox initramfs/sbin/modprobe
+
+    # Dropbear (static, so no shared libraries needed)
+    cp dropbear-install/sbin/dropbear    initramfs/sbin/dropbear
+    cp dropbear-install/sbin/dropbearkey initramfs/sbin/dropbearkey
+    chmod +x initramfs/sbin/dropbear initramfs/sbin/dropbearkey
+
+    # Bake in the SSH public key so syzkaller can authenticate
+    cp qemu/id_rsa.pub initramfs/root/.ssh/authorized_keys
+    chmod 700 initramfs/root/.ssh
+    chmod 600 initramfs/root/.ssh/authorized_keys
+
+    # 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) > qemu/initramfs.cpio.gz
+    ok "qemu/initramfs.cpio.gz: $(du -sh qemu/initramfs.cpio.gz | cut -f1)"
+fi
+
+# ── 9. Workdirs ───────────────────────────────────────────────────────────────
+for profile in raw-gadget gadget-fw usbfs; do
+    mkdir -p "workdir-${profile}"
+done
+ok "Workdirs created: workdir-raw-gadget/ workdir-gadget-fw/ workdir-usbfs/"
+
+# ── Done ─────────────────────────────────────────────────────────────────────
+echo ""
+ok "Setup complete. Next steps:"
+echo "  1. Start fuzzing:    bash run-fuzzers.sh [--raw-gadget|--gadget-fw|--usbfs|--all]"
+echo "  2. Web UI:           http://127.0.0.1:56741  (raw-gadget profile)"
+echo "                       http://127.0.0.1:56742  (gadget-fw profile)"
+echo "                       http://127.0.0.1:56743  (usbfs profile)"
+echo "  3. Verify QEMU:      qemu-system-x86_64 -kernel bzImage -initrd qemu/initramfs.cpio.gz \\"
+echo "                         -nographic -append 'console=ttyS0 quiet' -m 2048M"
+echo "  4. Stop:             bash cleanup.sh"
diff --git a/fuzzing/linux-usb-stack/syzkaller-gadget-fw.cfg b/fuzzing/linux-usb-stack/syzkaller-gadget-fw.cfg
@@ -0,0 +1,42 @@
+{
+    "target": "linux/amd64",
+    "http": "127.0.0.1:56742",
+    "workdir": "__DIR__/workdir-gadget-fw",
+    "kernel_obj": "__DIR__/linux",
+    "sshkey": "__DIR__/qemu/id_rsa",
+    "syzkaller": "__DIR__/syzkaller",
+    "sandbox": "none",
+    "procs": 4,
+    "type": "qemu",
+    "vm": {
+        "count": 2,
+        "kernel": "__DIR__/bzImage",
+        "initrd": "__DIR__/qemu/initramfs.cpio.gz",
+        "cpu": 2,
+        "mem": 2048,
+        "cmdline": "console=ttyS0 panic=-1 oops=panic kasan_multi_shot nokaslr"
+    },
+    "enable_syscalls": [
+        "syz_usb_connect",
+        "syz_usb_connect_ex",
+        "syz_usb_disconnect",
+        "syz_usb_control_io",
+        "syz_usb_ep_write",
+        "syz_usb_ep_read",
+        "openat$usb_func_fs",
+        "ioctl$FUNCTIONFS_FIFO_STATUS",
+        "ioctl$FUNCTIONFS_FIFO_FLUSH",
+        "ioctl$FUNCTIONFS_CLEAR_HALT",
+        "ioctl$FUNCTIONFS_INTERFACE_REVMAP",
+        "ioctl$FUNCTIONFS_ENDPOINT_REVMAP",
+        "ioctl$FUNCTIONFS_ENDPOINT_DESC",
+        "write$usb_func_fs",
+        "read$usb_func_fs",
+        "mkdir",
+        "rmdir",
+        "symlink",
+        "write",
+        "read",
+        "openat"
+    ]
+}
diff --git a/fuzzing/linux-usb-stack/syzkaller-raw-gadget.cfg b/fuzzing/linux-usb-stack/syzkaller-raw-gadget.cfg
@@ -0,0 +1,44 @@
+{
+    "target": "linux/amd64",
+    "http": "127.0.0.1:56741",
+    "workdir": "__DIR__/workdir-raw-gadget",
+    "kernel_obj": "__DIR__/linux",
+    "sshkey": "__DIR__/qemu/id_rsa",
+    "syzkaller": "__DIR__/syzkaller",
+    "sandbox": "none",
+    "procs": 4,
+    "type": "qemu",
+    "vm": {
+        "count": 2,
+        "kernel": "__DIR__/bzImage",
+        "initrd": "__DIR__/qemu/initramfs.cpio.gz",
+        "cpu": 2,
+        "mem": 2048,
+        "cmdline": "console=ttyS0 panic=-1 oops=panic kasan_multi_shot nokaslr"
+    },
+    "enable_syscalls": [
+        "syz_usb_connect",
+        "syz_usb_connect_ex",
+        "syz_usb_disconnect",
+        "syz_usb_control_io",
+        "syz_usb_ep_write",
+        "syz_usb_ep_read",
+        "openat$/dev/raw-gadget",
+        "ioctl$USB_RAW_IOCTL_INIT",
+        "ioctl$USB_RAW_IOCTL_RUN",
+        "ioctl$USB_RAW_IOCTL_EVENT_FETCH",
+        "ioctl$USB_RAW_IOCTL_EP0_WRITE",
+        "ioctl$USB_RAW_IOCTL_EP0_READ",
+        "ioctl$USB_RAW_IOCTL_EP_ENABLE",
+        "ioctl$USB_RAW_IOCTL_EP_DISABLE",
+        "ioctl$USB_RAW_IOCTL_EP_WRITE",
+        "ioctl$USB_RAW_IOCTL_EP_READ",
+        "ioctl$USB_RAW_IOCTL_CONFIGURE",
+        "ioctl$USB_RAW_IOCTL_VBUS_DRAW",
+        "ioctl$USB_RAW_IOCTL_EPS_INFO",
+        "ioctl$USB_RAW_IOCTL_EP0_STALL",
+        "ioctl$USB_RAW_IOCTL_EP_SET_HALT",
+        "ioctl$USB_RAW_IOCTL_EP_CLEAR_HALT",
+        "ioctl$USB_RAW_IOCTL_EP_SET_WEDGE"
+    ]
+}
diff --git a/fuzzing/linux-usb-stack/syzkaller-usbfs.cfg b/fuzzing/linux-usb-stack/syzkaller-usbfs.cfg
@@ -0,0 +1,47 @@
+{
+    "target": "linux/amd64",
+    "http": "127.0.0.1:56743",
+    "workdir": "__DIR__/workdir-usbfs",
+    "kernel_obj": "__DIR__/linux",
+    "sshkey": "__DIR__/qemu/id_rsa",
+    "syzkaller": "__DIR__/syzkaller",
+    "sandbox": "none",
+    "procs": 4,
+    "type": "qemu",
+    "vm": {
+        "count": 2,
+        "kernel": "__DIR__/bzImage",
+        "initrd": "__DIR__/qemu/initramfs.cpio.gz",
+        "cpu": 2,
+        "mem": 2048,
+        "cmdline": "console=ttyS0 panic=-1 oops=panic kasan_multi_shot nokaslr"
+    },
+    "enable_syscalls": [
+        "syz_usb_connect",
+        "syz_usb_connect_ex",
+        "syz_usb_disconnect",
+        "openat$/dev/bus/usb",
+        "ioctl$USBDEVFS_CONTROL",
+        "ioctl$USBDEVFS_BULK",
+        "ioctl$USBDEVFS_RESETEP",
+        "ioctl$USBDEVFS_SETINTERFACE",
+        "ioctl$USBDEVFS_SETCONFIGURATION",
+        "ioctl$USBDEVFS_GETDRIVER",
+        "ioctl$USBDEVFS_SUBMITURB",
+        "ioctl$USBDEVFS_DISCARDURB",
+        "ioctl$USBDEVFS_REAPURB",
+        "ioctl$USBDEVFS_REAPURBNDELAY",
+        "ioctl$USBDEVFS_CLAIMINTERFACE",
+        "ioctl$USBDEVFS_RELEASEINTERFACE",
+        "ioctl$USBDEVFS_CONNECTINFO",
+        "ioctl$USBDEVFS_IOCTL",
+        "ioctl$USBDEVFS_RESET",
+        "ioctl$USBDEVFS_CLEAR_HALT",
+        "ioctl$USBDEVFS_DISCONNECT",
+        "ioctl$USBDEVFS_CONNECT",
+        "ioctl$USBDEVFS_ALLOC_STREAMS",
+        "ioctl$USBDEVFS_FREE_STREAMS",
+        "ioctl$USBDEVFS_DROP_PRIVILEGES",
+        "ioctl$USBDEVFS_GET_SPEED"
+    ]
+}