diff --git a/fuzzing/libusb/cleanup.sh b/fuzzing/libusb/cleanup.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+# cleanup.sh — kill fuzzers and remove artifacts.
+# Usage: bash cleanup.sh [--keep-corpus] [--keep-builds] [--keep-source] [--crashes-only]
+
+set -uo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+SESSION="libusb-fuzz"
+KEEP_CORPUS=0 KEEP_BUILDS=0 KEEP_SOURCE=0 CRASHES_ONLY=0
+
+for arg in "$@"; do
+ case "${arg}" in
+ --keep-corpus) KEEP_CORPUS=1 ;;
+ --keep-builds) KEEP_BUILDS=1 ;;
+ --keep-source) KEEP_SOURCE=1 ;;
+ --crashes-only) CRASHES_ONLY=1 ;;
+ *) echo "[!] Unknown option: ${arg}" >&2 ;;
+ esac
+done
+
+log() { echo "[*] $*"; }
+ok() { echo "[+] $*"; }
+
+tmux kill-session -t "${SESSION}" 2>/dev/null || true
+
+pkill -u "$(id -un)" -f "afl-fuzz.*libusb" 2>/dev/null || true
+pkill -u "$(id -un)" -f "fuzz_descriptor" 2>/dev/null || true
+pkill -u "$(id -un)" -f "fuzz_bos" 2>/dev/null || true
+pkill -u "$(id -un)" -f "fuzz_iad" 2>/dev/null || true
+pkill -u "$(id -un)" -f "fuzz_usbfs" 2>/dev/null || true
+
+[[ "${CRASHES_ONLY}" -eq 1 ]] && { ok "Processes killed."; exit 0; }
+
+if [[ "${KEEP_BUILDS}" -eq 0 ]]; then
+ rm -f fuzz_descriptor fuzz_descriptor.cmplog
+ rm -f fuzz_bos fuzz_bos.cmplog
+ rm -f fuzz_iad fuzz_iad.cmplog
+ rm -f fuzz_usbfs fuzz_usbfs.cmplog
+ ok "Binaries removed."
+fi
+
+if [[ "${KEEP_CORPUS}" -eq 0 ]]; then
+ rm -rf corpus/ output/
+ ok "Corpus and output removed."
+fi
+
+if [[ "${KEEP_SOURCE}" -eq 0 ]]; then
+ rm -rf libusb-src/ AFLplusplus/ afl-build/
+ ok "Source trees removed."
+fi
+
+ok "Done."
diff --git a/fuzzing/libusb/dictionaries/usb.dict b/fuzzing/libusb/dictionaries/usb.dict
@@ -0,0 +1,103 @@
+# USB descriptor type bytes
+dt_device="\x01"
+dt_config="\x02"
+dt_string="\x03"
+dt_interface="\x04"
+dt_endpoint="\x05"
+dt_qualifier="\x06"
+dt_other_speed="\x07"
+dt_interface_power="\x08"
+dt_bos="\x0f"
+dt_device_capability="\x10"
+dt_ss_endpoint_companion="\x30"
+dt_iad="\x0b"
+
+# Standard descriptor sizes as bLength values
+blen_device="\x12"
+blen_config="\x09"
+blen_interface="\x09"
+blen_endpoint="\x07"
+blen_endpoint_audio="\x09"
+blen_bos="\x05"
+blen_devcap_min="\x03"
+blen_iad="\x08"
+
+# BOS device capability types
+bt_wireless_usb="\x01"
+bt_usb20_ext="\x02"
+bt_ss_devcap="\x03"
+bt_container_id="\x04"
+bt_platform="\x05"
+bt_ptm="\x06"
+bt_ssplus="\x0a"
+
+# USB 2.0 extension capability size (7 bytes)
+blen_usb20_ext="\x07"
+# SuperSpeed capability size (10 bytes)
+blen_ss_cap="\x0a"
+# Container ID size (20 bytes)
+blen_container_id="\x14"
+# Platform descriptor minimum size (20 bytes) — exact value triggers underflow
+blen_platform_min="\x14"
+# bLength values just below/above the platform underflow threshold
+blen_platform_under1="\x13"
+blen_platform_under2="\x12"
+blen_platform_above="\x15"
+
+# Interesting bLength values (boundaries)
+blen_zero="\x00"
+blen_one="\x01"
+blen_two="\x02"
+blen_max="\xff"
+
+# wTotalLength values (little-endian 16-bit)
+wtotal_9="\x09\x00"
+wtotal_18="\x12\x00"
+wtotal_32="\x20\x00"
+wtotal_255="\xff\x00"
+wtotal_zero="\x00\x00"
+wtotal_one="\x01\x00"
+wtotal_max="\xff\xff"
+wtotal_intmax="\xff\x7f"
+
+# bNumInterfaces / bNumEndpoints limits
+bnum_zero="\x00"
+bnum_one="\x01"
+bnum_max_ifaces="\x20"
+bnum_max_endpoints="\x20"
+bnum_255="\xff"
+
+# Config descriptor header (bLength=9, bDescriptorType=0x02)
+config_header="\x09\x02"
+# Config with wTotalLength=9 (empty — no interfaces)
+config_empty="\x09\x02\x09\x00\x00\x00\x00\x80\x00"
+# Config with wTotalLength=0xFFFF
+config_wtotal_max="\x09\x02\xff\xff"
+
+# Interface descriptor header (bLength=9, bDescriptorType=0x04)
+iface_header="\x09\x04"
+# Endpoint bulk-out header (bLength=7, bDescriptorType=0x05)
+endpoint_bulk_out="\x07\x05\x01\x02"
+# Endpoint interrupt-in
+endpoint_interrupt_in="\x07\x05\x81\x03"
+
+# BOS header (bLength=5, bDescriptorType=0x0F)
+bos_header="\x05\x0f"
+# BOS with wTotalLength=5 (no capabilities)
+bos_empty="\x05\x0f\x05\x00\x00"
+
+# IAD header (bLength=8, bDescriptorType=0x0B)
+iad_header="\x08\x0b"
+
+# Endpoint type bits in bmAttributes
+ep_control="\x00"
+ep_isoch="\x01"
+ep_bulk="\x02"
+ep_interrupt="\x03"
+
+# USB speed constants
+speed_low="\x01"
+speed_full="\x02"
+speed_high="\x03"
+speed_super="\x05"
+speed_super_plus="\x06"
diff --git a/fuzzing/libusb/fuzz-include/config.h b/fuzzing/libusb/fuzz-include/config.h
@@ -0,0 +1,17 @@
+#ifndef LIBUSB_FUZZ_CONFIG_H
+#define LIBUSB_FUZZ_CONFIG_H
+
+#define VERSION "1.0.29-fuzz"
+#define PLATFORM_POSIX 1
+#define HAVE_SYS_TIME_H 1
+#define HAVE_CLOCK_GETTIME 1
+#define HAVE_PIPE2 1
+#define HAVE_NFDS_T 1
+#define _GNU_SOURCE 1
+#define DEFAULT_VISIBILITY __attribute__((visibility("default")))
+#define PRINTF_FORMAT(a, b) __attribute__((__format__(__printf__, a, b)))
+
+/* ENABLE_LOGGING intentionally not defined: libusbi.h's #ifdef branch skips
+ * logging setup and all usbi_err/warn/dbg macros expand to (void)(ctx). */
+
+#endif
diff --git a/fuzzing/libusb/fuzz-include/libusb_fuzz.h b/fuzzing/libusb/fuzz-include/libusb_fuzz.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <limits.h>
+
+#define FUZZ_BUF_MAX (1 << 16)
+
+extern uint8_t g_fuzz_buf[FUZZ_BUF_MAX];
+extern size_t g_fuzz_len;
+extern size_t g_fuzz_offset;
+
+static inline void fuzz_reset(const uint8_t *data, size_t size)
+{
+ size_t n = size < FUZZ_BUF_MAX ? size : FUZZ_BUF_MAX;
+ memcpy(g_fuzz_buf, data, n);
+ g_fuzz_len = n;
+ g_fuzz_offset = 0;
+}
diff --git a/fuzzing/libusb/fuzz_bos.c b/fuzzing/libusb/fuzz_bos.c
@@ -0,0 +1,63 @@
+/*
+ * Harness: BOS descriptor parsing + all 5 device-capability extractors.
+ * Key bugs: platform bLength underflow (line 1268), SSPlus OOB (line 1115).
+ */
+
+#include "fuzz-include/libusb_fuzz.h"
+#include "libusb-src/libusb/descriptor.c"
+
+static int fuzz_one(const uint8_t *data, size_t size)
+{
+ if (size < 1)
+ return 0;
+
+ struct libusb_bos_descriptor *bos = NULL;
+ int r = parse_bos(NULL, &bos,
+ data, (int)(size > (size_t)INT_MAX ? INT_MAX : size));
+ if (r != LIBUSB_SUCCESS || !bos)
+ return 0;
+
+ for (uint8_t i = 0; i < bos->bNumDeviceCaps; i++) {
+ struct libusb_bos_dev_capability_descriptor *cap = bos->dev_capability[i];
+
+ struct libusb_usb_2_0_extension_descriptor *usb20 = NULL;
+ libusb_get_usb_2_0_extension_descriptor(NULL, cap, &usb20);
+ if (usb20) libusb_free_usb_2_0_extension_descriptor(usb20);
+
+ struct libusb_ss_usb_device_capability_descriptor *ss = NULL;
+ libusb_get_ss_usb_device_capability_descriptor(NULL, cap, &ss);
+ if (ss) libusb_free_ss_usb_device_capability_descriptor(ss);
+
+ struct libusb_ssplus_usb_device_capability_descriptor *ssplus = NULL;
+ libusb_get_ssplus_usb_device_capability_descriptor(NULL, cap, &ssplus);
+ if (ssplus) libusb_free_ssplus_usb_device_capability_descriptor(ssplus);
+
+ struct libusb_container_id_descriptor *cid = NULL;
+ libusb_get_container_id_descriptor(NULL, cap, &cid);
+ if (cid) libusb_free_container_id_descriptor(cid);
+
+ struct libusb_platform_descriptor *plat = NULL;
+ libusb_get_platform_descriptor(NULL, cap, &plat);
+ if (plat) libusb_free_platform_descriptor(plat);
+ }
+
+ libusb_free_bos_descriptor(bos);
+ return 0;
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+int main(void)
+{
+ __AFL_INIT();
+ uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF;
+ while (__AFL_LOOP(10000))
+ fuzz_one(buf, __AFL_FUZZ_TESTCASE_LEN);
+ return 0;
+}
+#else
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+ return fuzz_one(data, size);
+}
+#endif
diff --git a/fuzzing/libusb/fuzz_descriptor.c b/fuzzing/libusb/fuzz_descriptor.c
@@ -0,0 +1,37 @@
+/*
+ * Harness: config/interface/endpoint descriptor chain via raw_desc_to_config().
+ * Targets: parse_configuration -> parse_interface -> parse_endpoint (all static).
+ */
+
+#include "fuzz-include/libusb_fuzz.h"
+#include "libusb-src/libusb/descriptor.c"
+
+static int fuzz_one(const uint8_t *data, size_t size)
+{
+ if (size < 1)
+ return 0;
+
+ struct libusb_config_descriptor *config = NULL;
+ int r = raw_desc_to_config(NULL, data,
+ (int)(size > (size_t)INT_MAX ? INT_MAX : size), &config);
+ if (r == LIBUSB_SUCCESS && config)
+ libusb_free_config_descriptor(config);
+ return 0;
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+int main(void)
+{
+ __AFL_INIT();
+ uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF;
+ while (__AFL_LOOP(10000))
+ fuzz_one(buf, __AFL_FUZZ_TESTCASE_LEN);
+ return 0;
+}
+#else
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+ return fuzz_one(data, size);
+}
+#endif
diff --git a/fuzzing/libusb/fuzz_iad.c b/fuzzing/libusb/fuzz_iad.c
@@ -0,0 +1,36 @@
+/*
+ * Harness: IAD two-pass parsing via raw_desc_to_iad_array().
+ */
+
+#include "fuzz-include/libusb_fuzz.h"
+#include "libusb-src/libusb/descriptor.c"
+
+static int fuzz_one(const uint8_t *data, size_t size)
+{
+ if (size < 1)
+ return 0;
+
+ struct libusb_interface_association_descriptor_array *iad_array = NULL;
+ int r = raw_desc_to_iad_array(NULL, data,
+ (int)(size > (size_t)INT_MAX ? INT_MAX : size), &iad_array);
+ if (r == LIBUSB_SUCCESS && iad_array)
+ libusb_free_interface_association_descriptors(iad_array);
+ return 0;
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+int main(void)
+{
+ __AFL_INIT();
+ uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF;
+ while (__AFL_LOOP(10000))
+ fuzz_one(buf, __AFL_FUZZ_TESTCASE_LEN);
+ return 0;
+}
+#else
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+ return fuzz_one(data, size);
+}
+#endif
diff --git a/fuzzing/libusb/fuzz_stubs.c b/fuzzing/libusb/fuzz_stubs.c
@@ -0,0 +1,51 @@
+/*
+ * Definitions for extern symbols declared in libusbi.h but normally defined
+ * in core.c / sync.c, which we don't compile in fuzz builds.
+ */
+
+#include "fuzz-include/libusb_fuzz.h"
+#include "libusb-src/libusb/libusbi.h"
+
+uint8_t g_fuzz_buf[FUZZ_BUF_MAX];
+size_t g_fuzz_len;
+size_t g_fuzz_offset;
+
+/* Zero-init backend; all function pointers are NULL — never called by parsers */
+const struct usbi_os_backend usbi_backend;
+
+struct libusb_context *usbi_default_context = NULL;
+struct libusb_context *usbi_fallback_context = NULL;
+
+usbi_mutex_static_t active_contexts_lock = USBI_MUTEX_INITIALIZER;
+struct list_head active_contexts_list;
+
+usbi_mutex_static_t linux_hotplug_lock = USBI_MUTEX_INITIALIZER;
+
+/* Transfer stubs — referenced by public API functions compiled in via
+ * descriptor.c but never called from our harness entry points. */
+int API_EXPORTED libusb_control_transfer(libusb_device_handle *dev_handle,
+ uint8_t request_type, uint8_t bRequest, uint16_t wValue, uint16_t wIndex,
+ unsigned char *data, uint16_t wLength, unsigned int timeout)
+{
+ (void)dev_handle; (void)request_type; (void)bRequest;
+ (void)wValue; (void)wIndex; (void)data; (void)wLength; (void)timeout;
+ return LIBUSB_ERROR_NOT_SUPPORTED;
+}
+
+int API_EXPORTED libusb_bulk_transfer(libusb_device_handle *dev_handle,
+ unsigned char endpoint, unsigned char *data, int length,
+ int *actual_length, unsigned int timeout)
+{
+ (void)dev_handle; (void)endpoint; (void)data;
+ (void)length; (void)actual_length; (void)timeout;
+ return LIBUSB_ERROR_NOT_SUPPORTED;
+}
+
+int API_EXPORTED libusb_interrupt_transfer(libusb_device_handle *dev_handle,
+ unsigned char endpoint, unsigned char *data, int length,
+ int *actual_length, unsigned int timeout)
+{
+ (void)dev_handle; (void)endpoint; (void)data;
+ (void)length; (void)actual_length; (void)timeout;
+ return LIBUSB_ERROR_NOT_SUPPORTED;
+}
diff --git a/fuzzing/libusb/fuzz_usbfs.c b/fuzzing/libusb/fuzz_usbfs.c
@@ -0,0 +1,63 @@
+/*
+ * Harness: linux_usbfs parse_config_descriptors via direct priv injection.
+ * Input: raw descriptor blob — 18-byte device desc + config descriptors.
+ */
+
+#include "fuzz-include/libusb_fuzz.h"
+#include "libusb-src/libusb/libusbi.h"
+
+int fuzz_parse_config_descriptors(struct libusb_device *dev);
+
+/* Must match layout in fuzz_usbfs_shim.c */
+struct linux_device_priv {
+ char *sysfs_dir;
+ void *descriptors;
+ size_t descriptors_len;
+ void *config_descriptors;
+ int active_config;
+};
+
+static int fuzz_one(const uint8_t *data, size_t size)
+{
+ if (size < LIBUSB_DT_DEVICE_SIZE)
+ return 0;
+
+ size_t dev_padded = PTR_ALIGN(sizeof(struct libusb_device));
+ uint8_t *block = calloc(1, dev_padded + sizeof(struct linux_device_priv));
+ if (!block)
+ return 0;
+
+ struct libusb_device *dev = (struct libusb_device *)block;
+ struct linux_device_priv *priv = (struct linux_device_priv *)(block + dev_padded);
+
+ memcpy(&dev->device_descriptor, data, LIBUSB_DT_DEVICE_SIZE);
+
+ priv->descriptors = malloc(size);
+ if (!priv->descriptors) { free(block); return 0; }
+ memcpy(priv->descriptors, data, size);
+ priv->descriptors_len = size;
+
+ fuzz_parse_config_descriptors(dev);
+
+ free(priv->config_descriptors);
+ free(priv->descriptors);
+ free(block);
+ return 0;
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+int main(void)
+{
+ __AFL_INIT();
+ uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF;
+ while (__AFL_LOOP(10000))
+ fuzz_one(buf, __AFL_FUZZ_TESTCASE_LEN);
+ return 0;
+}
+#else
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+ return fuzz_one(data, size);
+}
+#endif
diff --git a/fuzzing/libusb/fuzz_usbfs_shim.c b/fuzzing/libusb/fuzz_usbfs_shim.c
@@ -0,0 +1,120 @@
+/*
+ * Extraction of parse_config_descriptors() + seek_to_next_config() from
+ * linux_usbfs.c. Verbatim logic, local type definitions, fuzz-visible entry point.
+ */
+
+#include "fuzz-include/libusb_fuzz.h"
+#include "libusb-src/libusb/libusbi.h"
+
+struct config_descriptor {
+ struct usbi_configuration_descriptor *desc;
+ size_t actual_len;
+};
+
+struct linux_device_priv {
+ char *sysfs_dir;
+ void *descriptors;
+ size_t descriptors_len;
+ struct config_descriptor *config_descriptors;
+ int active_config;
+};
+
+static int seek_to_next_config(struct libusb_context *ctx,
+ uint8_t *buffer, size_t len)
+{
+ struct usbi_descriptor_header *header;
+ int offset = LIBUSB_DT_CONFIG_SIZE;
+
+ buffer += LIBUSB_DT_CONFIG_SIZE;
+ len -= LIBUSB_DT_CONFIG_SIZE;
+
+ while (len > 0) {
+ if (len < 2) {
+ usbi_err(ctx, "remaining descriptor length too small %zu/2", len);
+ return LIBUSB_ERROR_IO;
+ }
+ header = (struct usbi_descriptor_header *)buffer;
+ if (header->bDescriptorType == LIBUSB_DT_CONFIG)
+ return offset;
+ if (header->bLength < 2) {
+ usbi_err(ctx, "invalid descriptor bLength %hhu", header->bLength);
+ return LIBUSB_ERROR_IO;
+ }
+ if (len < header->bLength) {
+ usbi_err(ctx, "bLength overflow by %zu bytes",
+ (size_t)header->bLength - len);
+ return LIBUSB_ERROR_IO;
+ }
+ offset += header->bLength;
+ buffer += header->bLength;
+ len -= header->bLength;
+ }
+
+ usbi_err(ctx, "config descriptor not found");
+ return LIBUSB_ERROR_IO;
+}
+
+int fuzz_parse_config_descriptors(struct libusb_device *dev)
+{
+ struct libusb_context *ctx = DEVICE_CTX(dev);
+ struct linux_device_priv *priv = usbi_get_device_priv(dev);
+ struct usbi_device_descriptor *device_desc;
+ uint8_t idx, num_configs;
+ uint8_t *buffer;
+ size_t remaining;
+
+ device_desc = priv->descriptors;
+ num_configs = device_desc->bNumConfigurations;
+ if (num_configs == 0)
+ return 0;
+
+ priv->config_descriptors = malloc(
+ num_configs * sizeof(priv->config_descriptors[0]));
+ if (!priv->config_descriptors)
+ return LIBUSB_ERROR_NO_MEM;
+
+ buffer = (uint8_t *)priv->descriptors + LIBUSB_DT_DEVICE_SIZE;
+ remaining = priv->descriptors_len - LIBUSB_DT_DEVICE_SIZE;
+
+ for (idx = 0; idx < num_configs; idx++) {
+ struct usbi_configuration_descriptor *config_desc;
+ uint16_t config_len;
+
+ if (remaining < LIBUSB_DT_CONFIG_SIZE) {
+ usbi_err(ctx, "short descriptor read %zu/%d",
+ remaining, LIBUSB_DT_CONFIG_SIZE);
+ return LIBUSB_ERROR_IO;
+ }
+
+ config_desc = (struct usbi_configuration_descriptor *)buffer;
+ if (config_desc->bDescriptorType != LIBUSB_DT_CONFIG) {
+ usbi_err(ctx, "not a config descriptor (type 0x%02x)",
+ config_desc->bDescriptorType);
+ return LIBUSB_ERROR_IO;
+ } else if (config_desc->bLength < LIBUSB_DT_CONFIG_SIZE) {
+ usbi_err(ctx, "invalid descriptor bLength %u", config_desc->bLength);
+ return LIBUSB_ERROR_IO;
+ }
+
+ config_len = libusb_le16_to_cpu(config_desc->wTotalLength);
+ if (config_len < LIBUSB_DT_CONFIG_SIZE) {
+ usbi_err(ctx, "invalid wTotalLength %u", config_len);
+ return LIBUSB_ERROR_IO;
+ }
+
+ /* sysfs_dir == NULL → usbfs path: wTotalLength used directly */
+ if (!priv->sysfs_dir) {
+ if (config_len > remaining) {
+ usbi_warn(ctx, "short descriptor read %zu/%u", remaining, config_len);
+ config_len = (uint16_t)remaining;
+ }
+ }
+
+ priv->config_descriptors[idx].desc = config_desc;
+ priv->config_descriptors[idx].actual_len = config_len;
+ buffer += config_len;
+ remaining -= config_len;
+ }
+
+ return LIBUSB_SUCCESS;
+}
diff --git a/fuzzing/libusb/gen_corpus.py b/fuzzing/libusb/gen_corpus.py
@@ -0,0 +1,369 @@
+#!/usr/bin/env python3
+"""
+setup-corpus.sh — generate seed corpus for libusb fuzzing harnesses.
+
+Produces ~90 binary seed files across four subdirectories:
+ corpus/descriptor/ config/interface/endpoint chain seeds
+ corpus/bos/ BOS descriptor and device capability seeds
+ corpus/iad/ IAD two-pass parsing seeds
+ corpus/usbfs/ Raw device descriptor blob seeds (linux_usbfs path)
+"""
+
+import os
+import struct
+
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+CORPUS_DIR = os.path.join(SCRIPT_DIR, "corpus")
+
+def write_seed(subdir, name, data):
+ path = os.path.join(CORPUS_DIR, subdir, name)
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, "wb") as f:
+ f.write(data)
+ print(f" {subdir}/{name} ({len(data)} bytes)")
+
+# ─── CONFIG DESCRIPTOR SEEDS ─────────────────────────────────────────────────
+# Format: [bLength][bDescriptorType=0x02][wTotalLength LE16][bNumInterfaces]
+# [bConfigurationValue][iConfiguration][bmAttributes][MaxPower]
+# [... interface + endpoint descriptors ...]
+
+def config_header(total_len, num_ifaces=1, value=1):
+ return struct.pack("<BBHBBBBB",
+ 9, 0x02, total_len, num_ifaces, value, 0, 0x80, 50)
+
+def iface_desc(num_eps=1, iface_num=0, alt=0):
+ return struct.pack("<BBBBBBBBB",
+ 9, 0x04, iface_num, alt, num_eps, 0xFF, 0x00, 0x00, 0)
+
+def endpoint_desc(addr=0x81, attrs=0x02, mps=512):
+ return struct.pack("<BBBBHB",
+ 7, 0x05, addr, attrs, mps, 0)
+
+print("[*] Generating descriptor/ seeds...")
+
+# Minimal valid: 9-byte header, zero interfaces, wTotalLength=9
+write_seed("descriptor", "config_minimal_valid",
+ config_header(9, num_ifaces=0))
+
+# One interface + one endpoint (total = 9+9+7 = 25 bytes)
+body = config_header(25, num_ifaces=1) + iface_desc(num_eps=1) + endpoint_desc()
+write_seed("descriptor", "config_one_iface_one_ep", body)
+
+# One interface + max endpoints (32 endpoints = 9+9+32*7 = 242 bytes)
+eps = b"".join(endpoint_desc(addr=0x80 | i, attrs=i % 4) for i in range(32))
+total = 9 + 9 + len(eps)
+body = config_header(total, num_ifaces=1) + iface_desc(num_eps=32) + eps
+write_seed("descriptor", "config_max_endpoints", body)
+
+# wTotalLength > actual data (triggers short-read path)
+write_seed("descriptor", "config_wtotallength_mismatch",
+ config_header(0xFFFF, num_ifaces=1) + iface_desc(1) + endpoint_desc())
+
+# wTotalLength = 0
+write_seed("descriptor", "config_wtotallength_zero",
+ struct.pack("<BBHBBBBB", 9, 0x02, 0, 0, 1, 0, 0x80, 50))
+
+# wTotalLength = 1 (below minimum)
+write_seed("descriptor", "config_wtotallength_one",
+ struct.pack("<BBHBBBBB", 9, 0x02, 1, 0, 1, 0, 0x80, 50))
+
+# bNumInterfaces = 32 (USB_MAXINTERFACES limit)
+ifaces = b"".join(iface_desc(0, i) for i in range(32))
+total = 9 + len(ifaces)
+write_seed("descriptor", "config_bNumInterfaces_32",
+ config_header(total, num_ifaces=32) + ifaces)
+
+# bNumInterfaces = 255 (over-max, exercises the > USB_MAXINTERFACES check)
+write_seed("descriptor", "config_bNumInterfaces_255",
+ struct.pack("<BBHBBBBB", 9, 0x02, 9, 255, 1, 0, 0x80, 50))
+
+# bLength = 1 (below LIBUSB_DT_CONFIG_SIZE=9)
+write_seed("descriptor", "config_bLength_1",
+ struct.pack("<BB", 1, 0x02) + b"\x00" * 16)
+
+# bLength = 2 (just a header)
+write_seed("descriptor", "config_bLength_2",
+ struct.pack("<BB", 2, 0x02) + b"\x00" * 16)
+
+# Truncated at various offsets
+for n in [0, 1, 4, 8]:
+ write_seed("descriptor", f"config_truncated_{n}", b"\x09\x02" + b"\x20\x00" + b"\x00" * n)
+
+# All 0xFF bytes
+write_seed("descriptor", "config_all_ff", b"\xff" * 64)
+
+# All 0x00 bytes (hits multiple early-return paths)
+write_seed("descriptor", "config_all_zeros", b"\x00" * 32)
+
+# Class-specific descriptor before interface (bDescriptorType=0x24)
+class_desc = struct.pack("<BBB", 4, 0x24, 0x01) # 4-byte class desc
+total = 9 + len(class_desc) + 9 + 7
+body = config_header(total) + class_desc + iface_desc(1) + endpoint_desc()
+write_seed("descriptor", "config_class_desc_before_iface", body)
+
+# Two interfaces, two alt settings each
+alts = iface_desc(1, 0, 0) + iface_desc(1, 0, 1) + iface_desc(1, 1, 0) + iface_desc(1, 1, 1)
+eps2 = endpoint_desc() * 4
+total = 9 + len(alts) + len(eps2)
+write_seed("descriptor", "config_two_ifaces_two_alts",
+ config_header(total, num_ifaces=2) + alts + eps2)
+
+# Audio endpoint (bLength=9 for synch fields)
+audio_ep = struct.pack("<BBBBHBBB", 9, 0x05, 0x81, 0x05, 256, 1, 0, 0)
+total = 9 + 9 + len(audio_ep)
+write_seed("descriptor", "config_audio_endpoint",
+ config_header(total) + iface_desc(1) + audio_ep)
+
+# ─── BOS DESCRIPTOR SEEDS ────────────────────────────────────────────────────
+# Format: [bLength=5][bDescriptorType=0x0F][wTotalLength LE16][bNumDeviceCaps]
+# [capability descriptors...]
+
+def bos_header(total_len, num_caps):
+ return struct.pack("<BBHB", 5, 0x0F, total_len, num_caps)
+
+def cap_usb20_ext():
+ # bLength=7, bDescriptorType=0x10, bDevCapabilityType=0x02, bmAttributes=LE32
+ return struct.pack("<BBB I", 7, 0x10, 0x02, 0x00000002)
+
+def cap_ss():
+ # bLength=10, type=0x10, captype=0x03, bmAttr, wSpeedsSupported, bFuncSupport, bU1, bU2
+ return struct.pack("<BBB B H B B B", 10, 0x10, 0x03, 0, 0x000E, 1, 10, 40)
+
+def cap_container_id():
+ # bLength=20, type=0x10, captype=0x04, bReserved=0, ContainerID[16]
+ return struct.pack("<BBB B", 20, 0x10, 0x04, 0) + b"\xDE\xAD\xBE\xEF" * 4
+
+def cap_ssplus(num_sublinks):
+ # bLength = 12 + 4*num_sublinks
+ # bmAttr lower nibble = (num_sublinks - 1) for sublinks count
+ blen = 12 + 4 * num_sublinks
+ bmattr = struct.pack("<I", (num_sublinks - 1) & 0xF)
+ sublinks = struct.pack("<I", 0x000A4000) * num_sublinks # lane speed attr
+ return struct.pack("<BBB", blen, 0x10, 0x0A) + bmattr + struct.pack("<HBB", 0, 0, 0) + sublinks
+
+def cap_platform(cap_data_len):
+ # bLength = 3 + 1 + 16 + cap_data_len = 20 + cap_data_len
+ blen = 20 + cap_data_len
+ uuid = b"\x38\xB6\x08\xBF\xD0\xBB\x3D\x4B\x87\xC0\x52\x0E\x3B\x8F\xE8\xB7" # WinUSB UUID
+ return struct.pack("<BBB B", blen, 0x10, 0x05, 0) + uuid + b"\x00" * cap_data_len
+
+print("[*] Generating bos/ seeds...")
+
+# Minimal BOS: 5-byte header, no capabilities
+write_seed("bos", "bos_minimal", bos_header(5, 0))
+
+# One USB 2.0 extension capability
+c = cap_usb20_ext()
+write_seed("bos", "bos_one_usb20_ext", bos_header(5 + len(c), 1) + c)
+
+# One SuperSpeed capability
+c = cap_ss()
+write_seed("bos", "bos_one_ss_cap", bos_header(5 + len(c), 1) + c)
+
+# One Container ID capability
+c = cap_container_id()
+write_seed("bos", "bos_one_container_id", bos_header(5 + len(c), 1) + c)
+
+# SSPlus with 1 sublink (minimum, triggers sublink loop)
+c = cap_ssplus(1)
+write_seed("bos", "bos_ssplus_1_sublink", bos_header(5 + len(c), 1) + c)
+
+# SSPlus with 15 sublinks (high sublink count, stresses OOB path at line 1115)
+c = cap_ssplus(15)
+write_seed("bos", "bos_ssplus_15_sublinks", bos_header(5 + len(c), 1) + c)
+
+# Platform with 0 cap_data bytes (bLength=20, minimum, edge case for underflow)
+c = cap_platform(0)
+write_seed("bos", "bos_platform_cap_data_0", bos_header(5 + len(c), 1) + c)
+
+# Platform with 4 cap_data bytes (bLength=24, normal)
+c = cap_platform(4)
+write_seed("bos", "bos_platform_cap_data_4", bos_header(5 + len(c), 1) + c)
+
+# Platform with bLength manually set to 19 (below MIN=20, triggers check)
+raw = bytearray(cap_platform(0))
+raw[0] = 19
+write_seed("bos", "bos_platform_blen_19", bos_header(5 + 19, 1) + bytes(raw[:19]))
+
+# Platform with bLength=18 (well below minimum)
+raw[0] = 18
+write_seed("bos", "bos_platform_blen_18", bos_header(5 + 18, 1) + bytes(raw[:18]))
+
+# All 5 capability types in one BOS
+caps = cap_usb20_ext() + cap_ss() + cap_container_id() + cap_ssplus(2) + cap_platform(2)
+write_seed("bos", "bos_all_five_caps", bos_header(5 + len(caps), 5) + caps)
+
+# bNumDeviceCaps=255 with truncated data (tests allocation vs actual caps)
+write_seed("bos", "bos_bNumCaps_255_truncated",
+ bos_header(5 + 7, 255) + cap_usb20_ext())
+
+# wTotalLength=0xFFFF
+write_seed("bos", "bos_wtotal_max",
+ struct.pack("<BBHB", 5, 0x0F, 0xFFFF, 1) + cap_usb20_ext())
+
+# All zeros
+write_seed("bos", "bos_all_zeros", b"\x00" * 16)
+
+# Truncated BOS (4 bytes — below LIBUSB_DT_BOS_SIZE=5)
+write_seed("bos", "bos_truncated_4", b"\x05\x0f\x05\x00")
+
+# Unknown capability type (bDevCapabilityType=0xFF)
+c = struct.pack("<BBB I", 7, 0x10, 0xFF, 0xDEADBEEF)
+write_seed("bos", "bos_unknown_cap_type", bos_header(5 + len(c), 1) + c)
+
+# Capability with bLength=2 (below 3-byte minimum)
+c = struct.pack("<BB", 2, 0x10) + b"\xFF" * 5
+write_seed("bos", "bos_cap_blen_2", bos_header(5 + 2, 1) + c[:2])
+
+# SSPlus bmAttributes sublink count = 0 (no sublink speed attrs)
+c = struct.pack("<BBB I H B B", 12, 0x10, 0x0A, 0, 0, 0, 0)
+write_seed("bos", "bos_ssplus_0_sublinks", bos_header(5 + len(c), 1) + c)
+
+# ─── IAD SEEDS ───────────────────────────────────────────────────────────────
+# IAD embedded within a config descriptor
+# [config header 9B][IAD 8B each][interface descriptors 9B each]
+
+def iad_desc(first_iface=0, count=2, func_class=0xEF):
+ return struct.pack("<BBBBBBBB",
+ 8, 0x0B, first_iface, count, func_class, 0x02, 0x01, 0)
+
+print("[*] Generating iad/ seeds...")
+
+# No IADs (just a valid config)
+body = config_header(9, 0)
+write_seed("iad", "iad_none", body)
+
+# One IAD + two interfaces
+iad = iad_desc(0, 2)
+ifaces = iface_desc(1, 0) + iface_desc(1, 1)
+total = 9 + len(iad) + len(ifaces) + 7 * 2
+body = config_header(total, 2) + iad + ifaces + endpoint_desc() + endpoint_desc(0x02)
+write_seed("iad", "iad_one", body)
+
+# Two IADs with non-overlapping interface ranges
+iad1 = iad_desc(0, 2)
+iad2 = iad_desc(2, 2)
+ifaces = b"".join(iface_desc(0, i) for i in range(4))
+total = 9 + len(iad1) + len(iad2) + len(ifaces)
+write_seed("iad", "iad_two_sequential",
+ config_header(total, 4) + iad1 + iad2 + ifaces)
+
+# Two IADs with overlapping interface ranges
+iad_overlap1 = iad_desc(0, 3)
+iad_overlap2 = iad_desc(1, 3)
+ifaces = b"".join(iface_desc(0, i) for i in range(4))
+total = 9 + len(iad_overlap1) + len(iad_overlap2) + len(ifaces)
+write_seed("iad", "iad_two_overlapping",
+ config_header(total, 4) + iad_overlap1 + iad_overlap2 + ifaces)
+
+# bInterfaceCount = 0
+iad_zero = struct.pack("<BBBBBBBB", 8, 0x0B, 0, 0, 0xEF, 0x02, 0x01, 0)
+write_seed("iad", "iad_bInterfaceCount_0",
+ config_header(9 + 8, 0) + iad_zero)
+
+# bInterfaceCount = 255
+iad_max = struct.pack("<BBBBBBBB", 8, 0x0B, 0, 255, 0xEF, 0x02, 0x01, 0)
+write_seed("iad", "iad_bInterfaceCount_255",
+ config_header(9 + 8, 0) + iad_max)
+
+# IAD bLength = 2 (minimum header — too short for IAD but passes header check)
+iad_short = struct.pack("<BB", 2, 0x0B) + b"\xFF" * 6
+write_seed("iad", "iad_bLength_2",
+ config_header(9 + 2, 0) + iad_short[:2])
+
+# IAD bLength = 7 (one below standard 8)
+iad_7 = struct.pack("<BBBBBBB", 7, 0x0B, 0, 2, 0xEF, 0x02, 0x01)
+write_seed("iad", "iad_bLength_7",
+ config_header(9 + 7, 0) + iad_7)
+
+# Many IADs (tests uint8_t count field — 32 IADs)
+iads_32 = b"".join(iad_desc(i * 2, 2) for i in range(32))
+total = 9 + len(iads_32)
+write_seed("iad", "iad_32_entries",
+ config_header(total, 0) + iads_32)
+
+# IAD after interface (not at start of config data)
+iface_first = iface_desc(0, 0)
+iad_after = iad_desc(1, 1)
+iface_second = iface_desc(0, 1)
+total = 9 + len(iface_first) + len(iad_after) + len(iface_second)
+write_seed("iad", "iad_after_interface",
+ config_header(total, 2) + iface_first + iad_after + iface_second)
+
+# All zeros (hits early-return paths in both passes)
+write_seed("iad", "iad_all_zeros", b"\x00" * 32)
+
+# ─── USBFS SEEDS ─────────────────────────────────────────────────────────────
+# Format: [18-byte device descriptor][config descriptor(s)]
+# The device descriptor's bNumConfigurations controls how many configs are expected.
+
+def device_desc(num_configs=1, vendor=0x0483, product=0x5740):
+ return struct.pack("<BBHBBBBHHHBBBB",
+ 18, 0x01, # bLength, bDescriptorType
+ 0x0200, # bcdUSB (USB 2.0)
+ 0x00, # bDeviceClass
+ 0x00, # bDeviceSubClass
+ 0x00, # bDeviceProtocol
+ 64, # bMaxPacketSize0
+ vendor, # idVendor
+ product, # idProduct
+ 0x0100, # bcdDevice
+ 1, 2, 3, # iManufacturer, iProduct, iSerialNumber
+ num_configs)
+
+print("[*] Generating usbfs/ seeds...")
+
+# Valid 1-config device
+dev = device_desc(1)
+cfg = config_header(25) + iface_desc(1) + endpoint_desc()
+write_seed("usbfs", "usbfs_valid_1config", dev + cfg)
+
+# Valid 2-config device
+dev = device_desc(2)
+cfg1 = config_header(18, 1, value=1) + iface_desc(0)
+cfg2 = config_header(18, 1, value=2) + iface_desc(0)
+write_seed("usbfs", "usbfs_valid_2configs", dev + cfg1 + cfg2)
+
+# bNumConfigurations = 0 (should return immediately)
+write_seed("usbfs", "usbfs_bNumConfigs_0", device_desc(0))
+
+# bNumConfigurations = 255 with no config data (only device desc)
+write_seed("usbfs", "usbfs_bNumConfigs_255_no_data", device_desc(255))
+
+# bNumConfigurations = 255 with one small config (loop runs until remaining exhausted)
+write_seed("usbfs", "usbfs_bNumConfigs_255_one_config",
+ device_desc(255) + config_header(9, 0))
+
+# wTotalLength > remaining (truncation path)
+dev = device_desc(1)
+cfg = struct.pack("<BBHBBBBB", 9, 0x02, 0xFFFF, 0, 1, 0, 0x80, 50)
+write_seed("usbfs", "usbfs_wtotallength_overflow", dev + cfg)
+
+# Device descriptor only (no config data)
+write_seed("usbfs", "usbfs_device_desc_only", device_desc(1))
+
+# All zeros (18+ bytes — hits bDescriptorType != LIBUSB_DT_CONFIG check)
+write_seed("usbfs", "usbfs_all_zeros", b"\x00" * 32)
+
+# Short descriptor (17 bytes — below LIBUSB_DT_DEVICE_SIZE=18, harness skips)
+write_seed("usbfs", "usbfs_short_17", b"\x12\x01" + b"\x00" * 15)
+
+# Device descriptor + config with bLength < LIBUSB_DT_CONFIG_SIZE (bad config)
+dev = device_desc(1)
+bad_cfg = struct.pack("<BB", 1, 0x02) + b"\x00" * 20
+write_seed("usbfs", "usbfs_config_blen_too_short", dev + bad_cfg)
+
+# Normal device + config where bDescriptorType != 0x02 (not a config desc)
+dev = device_desc(1)
+not_cfg = struct.pack("<BBHBBBBB", 9, 0x04, 9, 0, 1, 0, 0x80, 50) # type=interface
+write_seed("usbfs", "usbfs_wrong_config_type", dev + not_cfg)
+
+# wTotalLength = 0 (below minimum)
+dev = device_desc(1)
+zero_len = struct.pack("<BBHBBBBB", 9, 0x02, 0, 0, 1, 0, 0x80, 50)
+write_seed("usbfs", "usbfs_wtotallength_0", dev + zero_len)
+
+print(f"\n[+] Corpus generated in {CORPUS_DIR}/")
+for d in ["descriptor", "bos", "iad", "usbfs"]:
+ n = len(os.listdir(os.path.join(CORPUS_DIR, d)))
+ print(f" {d}/: {n} seeds")
diff --git a/fuzzing/libusb/mock_io.c b/fuzzing/libusb/mock_io.c
@@ -0,0 +1,74 @@
+/*
+ * Syscall intercepts for the linux_usbfs I/O path.
+ * Linked via -Wl,--wrap=open,--wrap=read,--wrap=close,--wrap=ioctl,--wrap=write
+ *
+ * Compiled into fuzz_usbfs but never called by its direct-injection path.
+ * Available for a future harness that exercises initialize_device().
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdarg.h>
+#include <sys/types.h>
+#include <errno.h>
+
+#define FUZZ_BUF_MAX (1 << 16)
+#define FAKE_FD 42
+
+extern uint8_t g_fuzz_buf[FUZZ_BUF_MAX];
+extern size_t g_fuzz_len;
+extern size_t g_fuzz_offset;
+
+int __wrap_open(const char *path, int flags, ...)
+{
+ (void)flags;
+ if (path && __builtin_strstr(path, "/dev/bus/usb"))
+ return FAKE_FD;
+ errno = ENOENT;
+ return -1;
+}
+
+ssize_t __wrap_read(int fd, void *buf, size_t len)
+{
+ extern ssize_t __real_read(int, void *, size_t);
+ if (fd != FAKE_FD)
+ return __real_read(fd, buf, len);
+ if (g_fuzz_offset >= g_fuzz_len)
+ return 0;
+ size_t avail = g_fuzz_len - g_fuzz_offset;
+ size_t n = len < avail ? len : avail;
+ memcpy(buf, g_fuzz_buf + g_fuzz_offset, n);
+ g_fuzz_offset += n;
+ return (ssize_t)n;
+}
+
+int __wrap_ioctl(int fd, unsigned long request, ...)
+{
+ (void)fd;
+ /* GET_CAPABILITIES → return all flags */
+ if ((request & 0xFF) == 26) {
+ va_list ap; va_start(ap, request);
+ uint32_t *c = va_arg(ap, uint32_t *); va_end(ap);
+ if (c) *c = 0x1F;
+ return 0;
+ }
+ /* GET_SPEED → SuperSpeed Plus */
+ if ((request & 0xFF) == 31) return 6;
+ return 0;
+}
+
+int __wrap_close(int fd)
+{
+ extern int __real_close(int);
+ return fd == FAKE_FD ? 0 : __real_close(fd);
+}
+
+ssize_t __wrap_write(int fd, const void *buf, size_t len)
+{
+ extern ssize_t __real_write(int, const void *, size_t);
+ if (fd == 2 || fd == 198 || fd == 199)
+ return __real_write(fd, buf, len);
+ (void)buf;
+ return (ssize_t)len;
+}
diff --git a/fuzzing/libusb/run-all.sh b/fuzzing/libusb/run-all.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+# run-all.sh — launch libusb fuzzing in tmux (8 instances across 4 harnesses).
+# Usage: bash run-all.sh [--rerun|--status]
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+AFL_BIN="${SCRIPT_DIR}/afl-build/usr/local/bin"
+[[ -d "${AFL_BIN}" ]] && export PATH="${AFL_BIN}:${PATH}"
+
+SESSION="libusb-fuzz"
+OUTPUT="${SCRIPT_DIR}/output"
+CORPUS="${SCRIPT_DIR}/corpus"
+DICT="${SCRIPT_DIR}/dictionaries/usb.dict"
+
+log() { echo "[*] $*"; }
+ok() { echo "[+] $*"; }
+die() { echo "[!] $*" >&2; exit 1; }
+
+case "${1:-}" in
+ --status)
+ afl-whatsup "${OUTPUT}" 2>/dev/null || echo "Output: ${OUTPUT}"
+ exit 0 ;;
+ --rerun)
+ tmux kill-session -t "${SESSION}" 2>/dev/null || true ;;
+ "") ;;
+ *) die "Unknown option: ${1}" ;;
+esac
+
+command -v afl-fuzz > /dev/null 2>&1 || die "afl-fuzz not found."
+for h in fuzz_descriptor fuzz_bos fuzz_iad fuzz_usbfs; do
+ [[ -f "${h}" ]] || die "${h} not found. Run setup.sh first."
+ [[ -f "${h}.cmplog" ]] || die "${h}.cmplog not found."
+done
+tmux list-sessions 2>/dev/null | grep -q "${SESSION}" && \
+ die "Session '${SESSION}' already running. Use --rerun."
+
+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}"
+
+export AFL_SKIP_CPUFREQ=1
+export AFL_AUTORESUME=1
+export AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1
+export ASAN_OPTIONS="abort_on_error=1:symbolize=0:detect_leaks=0:allocator_may_return_null=1"
+export UBSAN_OPTIONS="print_stacktrace=0:halt_on_error=1"
+
+afl_cmd() {
+ local role="$1" flag="$2" target="$3" corpus_sub="$4"
+ local cmplog=""
+ [[ "${flag}" == "master" ]] && cmplog="-c ./${target}.cmplog"
+ echo "afl-fuzz ${role} -i '${CORPUS}/${corpus_sub}' -o '${OUTPUT}' -x '${DICT}' -t 500 -p fast -m none ${cmplog} -- './${target}'"
+}
+
+log "Starting session '${SESSION}'..."
+tmux new-session -d -s "${SESSION}" -n "desc+bos" -x 220 -y 50
+tmux split-window -t "${SESSION}:0" -h
+tmux split-window -t "${SESSION}:0.0" -v
+tmux split-window -t "${SESSION}:0.2" -v
+tmux new-window -t "${SESSION}" -n "iad+usbfs"
+tmux split-window -t "${SESSION}:1" -h
+tmux split-window -t "${SESSION}:1.0" -v
+tmux split-window -t "${SESSION}:1.2" -v
+tmux new-window -t "${SESSION}" -n "status"
+sleep 0.5
+
+send() { tmux send-keys -t "${SESSION}:${1}.${2}" "${3}" Enter; }
+
+send 0 0 "$(afl_cmd '-M master_descriptor' master fuzz_descriptor descriptor)"
+send 0 1 "$(afl_cmd '-S sec_descriptor' sec fuzz_descriptor descriptor)"
+send 0 2 "$(afl_cmd '-M master_bos' master fuzz_bos bos)"
+send 0 3 "$(afl_cmd '-S sec_bos' sec fuzz_bos bos)"
+send 1 0 "$(afl_cmd '-M master_iad' master fuzz_iad iad)"
+send 1 1 "$(afl_cmd '-S sec_iad' sec fuzz_iad iad)"
+send 1 2 "$(afl_cmd '-M master_usbfs' master fuzz_usbfs usbfs)"
+send 1 3 "$(afl_cmd '-S sec_usbfs' sec fuzz_usbfs usbfs)"
+send 2 0 "watch -n 15 'afl-whatsup ${OUTPUT} 2>/dev/null || echo waiting...'"
+
+ok "Session started. Attach: tmux attach -t ${SESSION}"
diff --git a/fuzzing/libusb/setup.sh b/fuzzing/libusb/setup.sh
@@ -0,0 +1,91 @@
+#!/bin/bash
+# setup.sh — bootstrap libusb fuzzing environment.
+# Usage: bash setup.sh [--skip-deps] [--skip-afl]
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+AFL_REPO="https://github.com/AFLplusplus/AFLplusplus.git"
+LIBUSB_TAG="v1.0.29"
+LIBUSB_REPO="https://github.com/libusb/libusb.git"
+AFL_BIN="${SCRIPT_DIR}/afl-build/usr/local/bin"
+
+SKIP_DEPS=0
+SKIP_AFL=0
+for arg in "$@"; do
+ [[ "$arg" == "--skip-deps" ]] && SKIP_DEPS=1
+ [[ "$arg" == "--skip-afl" ]] && SKIP_AFL=1
+done
+
+log() { echo "[*] $*"; }
+ok() { echo "[+] $*"; }
+die() { echo "[!] $*" >&2; exit 1; }
+
+# deps
+if [[ "${SKIP_DEPS}" -eq 0 ]]; then
+ log "Installing dependencies..."
+ sudo apt-get update -qq
+ sudo apt-get install -y \
+ build-essential git tmux python3 \
+ clang llvm lld pkg-config gdb
+ ok "Dependencies installed."
+fi
+
+# AFL++
+if [[ "${SKIP_AFL}" -eq 1 ]]; then
+ command -v afl-clang-fast > /dev/null 2>&1 || die "afl-clang-fast not found."
+elif [[ -x "${AFL_BIN}/afl-clang-fast" ]]; then
+ ok "AFL++ already built."
+ export PATH="${AFL_BIN}:${PATH}"
+else
+ log "Building AFL++..."
+ [[ -d "AFLplusplus" ]] || git clone --depth=1 "${AFL_REPO}" AFLplusplus
+ cd AFLplusplus
+ make source-only -j"$(nproc)"
+ make install PREFIX="${SCRIPT_DIR}/afl-build/usr/local"
+ cd "${SCRIPT_DIR}"
+ export PATH="${AFL_BIN}:${PATH}"
+ ok "AFL++ built."
+fi
+
+# libusb source
+if [[ -d "libusb-src/.git" ]]; then
+ ok "libusb-src present."
+else
+ log "Cloning libusb ${LIBUSB_TAG}..."
+ git clone --depth=1 --branch "${LIBUSB_TAG}" "${LIBUSB_REPO}" libusb-src
+ ok "libusb-src cloned."
+fi
+
+# build harnesses
+log "Building harnesses..."
+CC="afl-clang-fast"
+SRC="libusb-src/libusb"
+CFLAGS="-fsanitize=address,undefined -g -O1 -fno-omit-frame-pointer"
+CFLAGS+=" -I fuzz-include -I ${SRC} -DFUZZ_BUILD"
+CFLAGS+=" -Wno-implicit-function-declaration -Wno-incompatible-pointer-types -Wno-unused-function -Wno-unused-variable"
+
+build() {
+ local name="$1" extra_src="${2:-}" extra_flags="${3:-}"
+ log " ${name}..."
+ ${CC} ${CFLAGS} ${extra_flags} "${name}.c" fuzz_stubs.c ${extra_src} -o "${name}" -lpthread
+ AFL_LLVM_CMPLOG=1 ${CC} ${CFLAGS} ${extra_flags} "${name}.c" fuzz_stubs.c ${extra_src} -o "${name}.cmplog" -lpthread
+}
+
+build fuzz_descriptor
+build fuzz_bos
+build fuzz_iad
+build fuzz_usbfs "fuzz_usbfs_shim.c" "-I ${SRC}/os"
+ok "Harnesses built."
+
+# corpus
+if [[ -d "corpus/descriptor" && "$(ls corpus/descriptor | wc -l)" -gt 5 ]]; then
+ ok "Corpus present."
+else
+ log "Generating corpus..."
+ python3 gen_corpus.py
+fi
+
+ok "Done. Run: bash run-all.sh"
| | | | | | | | | | | | | |