From bb3c60317217264da2f74f2e641e96519e9ea178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=AD=C3=B0=20Steinn=20Geirsson?= Date: Fri, 27 Mar 2026 00:33:34 +0000 Subject: [PATCH] feat(fuzz): add AFL++ with cargo-afl fuzzing support Add a second fuzzing engine alongside the existing libFuzzer/cargo-fuzz setup. AFL++ runs with persistent mode (afl::fuzz! macro), LLVM plugins (CmpLog, IJON), and a SymCC concolic companion for hybrid fuzzing. - cargo-afl built from afl.rs with a patch for CARGO_AFL_DIR / CARGO_AFL_LLVM_DIR env-var overrides - AFL++ built with LLVM 22 plugins to match rust-nightly - Persistent-mode fuzz targets in lib/fuzz-afl/ - --jobs N parallel fuzzing: main instance in foreground, secondaries and SymCC companion as systemd transient units in a slice - Ctrl+c / exit cleans up all background processes via slice stop - AFL_AUTORESUME=1 for clean restarts after previous runs - fuzz-clean-afl collects crashes from all instance directories - Shared harness logic in lib/src/fuzz_harness.rs Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 37 +- README.md | 62 +- .../plans/2026-03-26-afl-symcc-fuzzing.md | 836 ++++++++++++++++++ .../2026-03-26-afl-symcc-fuzzing-design.md | 193 ++++ .../2026-03-26-cargo-afl-migration-design.md | 116 +++ flake.nix | 298 ++++++- lib/Cargo.toml | 2 +- lib/fuzz-afl/.gitignore | 3 + lib/fuzz-afl/Cargo.lock | 383 ++++++++ lib/fuzz-afl/Cargo.toml | 33 + .../afl_targets/fuzz_handle_client.rs | 5 + .../afl_targets/fuzz_parse_command.rs | 5 + lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs | 5 + lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs | 5 + lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs | 5 + lib/{fuzz => fuzz-cargo}/.gitignore | 0 lib/{fuzz => fuzz-cargo}/Cargo.toml | 0 .../fuzz_handle_client/seed-devlist-only | Bin .../seed-devlist-then-import | Bin .../fuzz_handle_client/seed-import-enumerate | Bin .../fuzz_handle_client/seed-import-hid-full | Bin .../corpus/fuzz_parse_command/seed-devlist | Bin .../corpus/fuzz_parse_command/seed-import | Bin .../corpus/fuzz_urb_cdc/seed-cdc-bulk-in | Bin .../corpus/fuzz_urb_cdc/seed-cdc-bulk-out | Bin .../fuzz_urb_cdc/seed-cdc-class-requests | Bin .../corpus/fuzz_urb_cdc/seed-cdc-interrupt-in | Bin .../corpus/fuzz_urb_cdc/seed-enumerate | Bin .../corpus/fuzz_urb_hid/seed-enumerate | Bin .../corpus/fuzz_urb_hid/seed-get-status | Bin .../fuzz_urb_hid/seed-hid-class-requests | Bin .../corpus/fuzz_urb_hid/seed-hid-interrupt-in | Bin .../corpus/fuzz_urb_hid/seed-unlink | Bin .../corpus/fuzz_urb_uac/seed-enumerate | Bin .../corpus/fuzz_urb_uac/seed-uac-iso-in | Bin .../corpus/fuzz_urb_uac/seed-uac-iso-out | Bin .../fuzz_urb_uac/seed-uac-iso-out-multi | Bin .../fuzz_urb_uac/seed-uac-set-interface | Bin .../fuzz_targets/fuzz_handle_client.rs | 7 + .../fuzz_targets/fuzz_parse_command.rs | 7 + lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs | 7 + lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs | 7 + lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs | 7 + lib/{fuzz => fuzz-cargo}/gen_corpus.rs | 0 lib/fuzz/fuzz_targets/fuzz_handle_client.rs | 43 - lib/fuzz/fuzz_targets/fuzz_parse_command.rs | 13 - lib/fuzz/fuzz_targets/fuzz_urb_cdc.rs | 36 - lib/fuzz/fuzz_targets/fuzz_urb_hid.rs | 42 - lib/fuzz/fuzz_targets/fuzz_urb_uac.rs | 21 - lib/src/fuzz_harness.rs | 125 +++ lib/src/lib.rs | 2 + nix/cargo-afl-env-paths.patch | 22 + 52 files changed, 2128 insertions(+), 199 deletions(-) create mode 100644 docs/superpowers/plans/2026-03-26-afl-symcc-fuzzing.md create mode 100644 docs/superpowers/specs/2026-03-26-afl-symcc-fuzzing-design.md create mode 100644 docs/superpowers/specs/2026-03-26-cargo-afl-migration-design.md create mode 100644 lib/fuzz-afl/.gitignore create mode 100644 lib/fuzz-afl/Cargo.lock create mode 100644 lib/fuzz-afl/Cargo.toml create mode 100644 lib/fuzz-afl/afl_targets/fuzz_handle_client.rs create mode 100644 lib/fuzz-afl/afl_targets/fuzz_parse_command.rs create mode 100644 lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs create mode 100644 lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs create mode 100644 lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs rename lib/{fuzz => fuzz-cargo}/.gitignore (100%) rename lib/{fuzz => fuzz-cargo}/Cargo.toml (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_handle_client/seed-devlist-only (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_handle_client/seed-devlist-then-import (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_handle_client/seed-import-enumerate (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_handle_client/seed-import-hid-full (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_parse_command/seed-devlist (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_parse_command/seed-import (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_cdc/seed-cdc-bulk-in (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_cdc/seed-cdc-bulk-out (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_cdc/seed-cdc-class-requests (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_cdc/seed-cdc-interrupt-in (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_cdc/seed-enumerate (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_hid/seed-enumerate (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_hid/seed-get-status (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_hid/seed-hid-class-requests (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_hid/seed-hid-interrupt-in (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_hid/seed-unlink (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_uac/seed-enumerate (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_uac/seed-uac-iso-in (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_uac/seed-uac-iso-out (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_uac/seed-uac-iso-out-multi (100%) rename lib/{fuzz => fuzz-cargo}/corpus/fuzz_urb_uac/seed-uac-set-interface (100%) create mode 100644 lib/fuzz-cargo/fuzz_targets/fuzz_handle_client.rs create mode 100644 lib/fuzz-cargo/fuzz_targets/fuzz_parse_command.rs create mode 100644 lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs create mode 100644 lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs create mode 100644 lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs rename lib/{fuzz => fuzz-cargo}/gen_corpus.rs (100%) delete mode 100644 lib/fuzz/fuzz_targets/fuzz_handle_client.rs delete mode 100644 lib/fuzz/fuzz_targets/fuzz_parse_command.rs delete mode 100644 lib/fuzz/fuzz_targets/fuzz_urb_cdc.rs delete mode 100644 lib/fuzz/fuzz_targets/fuzz_urb_hid.rs delete mode 100644 lib/fuzz/fuzz_targets/fuzz_urb_uac.rs create mode 100644 lib/src/fuzz_harness.rs create mode 100644 nix/cargo-afl-env-paths.patch diff --git a/CLAUDE.md b/CLAUDE.md index f40f8cd..59e6267 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,16 +23,41 @@ nix develop -c cargo test -p usbip-rs ## Fuzzing -Fuzz targets are in `lib/fuzz/` and exercise host-side codepaths against untrusted client input. +Fuzz targets exercise host-side codepaths against untrusted client input. + +### libFuzzer (cargo-fuzz) + +Targets are in `lib/fuzz-cargo/` and use `cargo fuzz`. ```bash -nix run .#fuzz-usbip # List targets -nix run .#fuzz-usbip -- fuzz_urb_hid # Single process -nix run .#fuzz-usbip -- fuzz_urb_hid --fork=8 # Parallel (overnight) -nix run .#fuzz-clean-usbip -- fuzz_urb_hid # Prune fixed artifacts +nix run .#fuzz-cargo # List targets +nix run .#fuzz-cargo -- fuzz_urb_hid # Single process +nix run .#fuzz-cargo -- fuzz_urb_hid --fork=8 # Parallel (overnight) +nix run .#fuzz-clean-cargo -- fuzz_urb_hid # Prune fixed artifacts ``` -Crash artifacts: `lib/fuzz/artifacts//`. Response validation is in `lib/src/fuzz_helpers.rs`. +Crash artifacts: `lib/fuzz-cargo/artifacts//`. + +### AFL++ with cargo-afl + +Targets are in `lib/fuzz-afl/` and use `cargo-afl` with LLVM plugins for persistent mode, CmpLog, and IJON. AFL++ runs as the primary fuzzer with CmpLog auto-enabled (`-c0`); a SymCC-instrumented binary is also built for manual concolic execution. + +```bash +nix run .#fuzz-afl # List targets +nix run .#fuzz-afl -- fuzz_urb_hid # Run AFL++ (persistent + CmpLog) +nix run .#fuzz-afl -- fuzz_urb_hid --jobs=8 # Parallel (main + 7 secondaries) +nix run .#fuzz-clean-afl -- fuzz_urb_hid # Prune fixed crashes +``` + +The Nix package for `cargo-afl` is built in this repo from `rust-fuzz/afl.rs` with a patch adding `CARGO_AFL_DIR` / `CARGO_AFL_LLVM_DIR` env var overrides (see `nix/cargo-afl-env-paths.patch`). AFL++ is built with LLVM 22 plugins to match rust-nightly's LLVM version. + +Background processes (secondary AFL++ instances, SymCC companion) run as systemd transient units in a slice. Ctrl+c or exit cleans up everything; `systemctl --user stop fuzz_afl_.slice` for manual stop. + +Crashes: `lib/fuzz-afl/output//*/crashes/`. Corpora are separate between engines; copy manually if desired. + +### Shared harness + +All fuzz target logic lives in `lib/src/fuzz_harness.rs`. Response validation is in `lib/src/fuzz_helpers.rs`. ### Fixing fuzzer crashes diff --git a/README.md b/README.md index 9df0d53..4aa4326 100644 --- a/README.md +++ b/README.md @@ -76,40 +76,60 @@ The UAC1 loopback device advertises 48kHz 16-bit stereo playback and capture. Au ## Fuzzing -Fuzz targets exercise the host-side codepaths that process untrusted client data. Requires nightly Rust (provided by the nix app). +Fuzz targets exercise the host-side codepaths that process untrusted client data. Requires nightly Rust (provided by the Nix apps). Two fuzzing engines are available: libFuzzer (via cargo-fuzz) and AFL++ (via cargo-afl). -```bash -# List available fuzz targets -nix run .#fuzz-usbip - -# Run a single fuzz target -nix run .#fuzz-usbip -- fuzz_urb_hid - -# Run with 8 parallel processes, auto-restart on crash (for overnight runs) -nix run .#fuzz-usbip -- fuzz_urb_hid --fork=8 - -# Re-check saved crash artifacts and delete the ones that no longer reproduce -nix run .#fuzz-clean-usbip -- fuzz_urb_hid -``` - -Targets: +Targets (shared across both engines): - `fuzz_parse_command` — protocol deserialization - `fuzz_handle_client` — full connection lifecycle (negotiation + URB loop) - `fuzz_urb_hid` — URB loop with HID keyboard device - `fuzz_urb_uac` — URB loop with UAC1 audio device (isochronous) - `fuzz_urb_cdc` — URB loop with CDC ACM serial device (bulk) -Crash artifacts are saved to `lib/fuzz/artifacts//`. +### libFuzzer (cargo-fuzz) + +```bash +nix run .#fuzz-cargo # List targets +nix run .#fuzz-cargo -- fuzz_urb_hid # Single process +nix run .#fuzz-cargo -- fuzz_urb_hid --fork=8 # Parallel (overnight) +nix run .#fuzz-clean-cargo -- fuzz_urb_hid # Prune fixed artifacts +``` + +Crash artifacts: `lib/fuzz-cargo/artifacts//`. + +### AFL++ (cargo-afl) + +AFL++ runs with persistent mode, LLVM plugins (CmpLog, IJON), and a SymCC concolic companion. The main fuzzer instance runs in the foreground; secondary instances and the SymCC companion run as systemd transient units for proper process management. + +```bash +nix run .#fuzz-afl # List targets +nix run .#fuzz-afl -- fuzz_urb_hid # Single instance + SymCC +nix run .#fuzz-afl -- fuzz_urb_hid --jobs 8 # 1 main + 7 secondaries + SymCC + +# Stop background processes manually (also cleaned up on ctrl+c/exit) +systemctl --user stop fuzz_afl_fuzz_urb_hid.slice + +# Prune fixed crashes +nix run .#fuzz-clean-afl -- fuzz_urb_hid + +# View aggregate stats across all instances +nix develop .#fuzz-afl -c bash -c 'afl-whatsup lib/fuzz-afl/output/fuzz_urb_hid' +``` + +Crashes: `lib/fuzz-afl/output//*/crashes/`. SymCC companion log: `lib/fuzz-afl/output//symcc/companion.log`. + +Corpora are separate between engines; copy manually if desired. ## Project structure ``` -├── cli/ CLI binary (usbip-rs) -├── lib/ Library crate (usbip-rs) +├── cli/ CLI binary (usbip-rs) +├── lib/ Library crate (usbip-rs) │ ├── src/ -│ ├── fuzz/ Fuzz targets (cargo-fuzz) +│ ├── fuzz-cargo/ Fuzz targets (libFuzzer / cargo-fuzz) +│ ├── fuzz-afl/ Fuzz targets (AFL++ / cargo-afl) │ └── examples/ -└── flake.nix Nix build +├── nix/ Nix patches (cargo-afl env-var overrides) +└── flake.nix Nix build, devShells, fuzzing apps ``` ## License diff --git a/docs/superpowers/plans/2026-03-26-afl-symcc-fuzzing.md b/docs/superpowers/plans/2026-03-26-afl-symcc-fuzzing.md new file mode 100644 index 0000000..ea0da73 --- /dev/null +++ b/docs/superpowers/plans/2026-03-26-afl-symcc-fuzzing.md @@ -0,0 +1,836 @@ +# AFL++ and SymCC Fuzzing Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add AFL++ with SymCC concolic execution to complement existing libFuzzer fuzzing, targeting deeper coverage on protocol parsing and connection lifecycle. + +**Architecture:** Extract shared harness logic from existing fuzz targets into `lib/src/fuzz_harness.rs`. Rename `lib/fuzz/` to `lib/fuzz-cargo/`, add `lib/fuzz-afl/` with stdin-based wrappers. New Nix package outputs for latest AFL++ and SymCC, with `fuzz-afl` and `fuzz-clean-afl` apps that build both AFL++ and SymCC-instrumented binaries and run AFL++ with SymCC as companion mutator. + +**Tech Stack:** Rust, AFL++ (afl-cc), SymCC (symcc), Nix flakes, tokio, cargo-fuzz + +**Spec:** `docs/superpowers/specs/2026-03-26-afl-symcc-fuzzing-design.md` + +--- + +## File Map + +| Action | File | Responsibility | +|--------|------|----------------| +| Create | `lib/src/fuzz_harness.rs` | Shared harness functions for all 5 fuzz targets | +| Modify | `lib/src/lib.rs:32-33` | Add `pub mod fuzz_harness` behind `cfg(fuzz)` | +| Rename | `lib/fuzz/` → `lib/fuzz-cargo/` | libFuzzer directory rename | +| Modify | `lib/fuzz-cargo/fuzz_targets/*.rs` (all 5) | Thin wrappers calling `fuzz_harness` | +| Create | `lib/fuzz-afl/Cargo.toml` | AFL++ crate manifest | +| Create | `lib/fuzz-afl/.gitignore` | Ignore target/, corpus/, output/ | +| Create | `lib/fuzz-afl/afl_targets/*.rs` (5 files) | Stdin-based wrappers calling `fuzz_harness` | +| Modify | `flake.nix` | New packages, apps, devShell; rename existing apps | +| Modify | `CLAUDE.md` | Update fuzzing docs | +| Modify | `lib/fuzz-cargo/gen_corpus.rs` | No changes needed (paths are relative to cwd) | + +--- + +### Task 1: Extract shared harness into `fuzz_harness.rs` + +**Files:** +- Create: `lib/src/fuzz_harness.rs` +- Modify: `lib/src/lib.rs:32-33` + +- [ ] **Step 1: Create `lib/src/fuzz_harness.rs`** + +```rust +//! Shared fuzz harness functions. +//! +//! Each function sets up a device, feeds `data` through a MockSocket, +//! runs the target, and validates the output. Called by both libFuzzer +//! and AFL++ wrappers. + +use std::sync::Arc; + +use crate::mock::MockSocket; +use crate::{ + ClassCode, UsbDevice, UsbEndpoint, UsbInterfaceHandler, UsbIpServer, + cdc::{UsbCdcAcmHandler, CDC_ACM_SUBCLASS}, + hid::UsbHidKeyboardHandler, + usbip_protocol::UsbIpCommand, +}; + +fn run_with_tokio(fut: impl std::future::Future) { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(fut); +} + +pub fn run_fuzz_parse_command(data: &[u8]) { + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + let mut socket = MockSocket::new(data.to_vec()); + let _ = rt.block_on(UsbIpCommand::read_from_socket(&mut socket)); +} + +pub fn run_fuzz_handle_client(data: &[u8]) { + run_with_tokio(async { + let handler = Arc::new(UsbHidKeyboardHandler::new_keyboard()); + let device = UsbDevice::new(0) + .unwrap() + .with_interface( + ClassCode::HID as u8, + 0x00, + 0x00, + Some("Fuzz HID Keyboard"), + vec![UsbEndpoint { + address: 0x81, + attributes: 0x03, + max_packet_size: 0x08, + interval: 10, + ..Default::default() + }], + handler as Arc, + ) + .unwrap(); + + let server = UsbIpServer::new_simulated(vec![device]); + let mock = MockSocket::new(data.to_vec()); + let output = mock.output_handle(); + let _ = crate::handler(mock, Arc::new(server)).await; + let output_bytes = output.lock().unwrap(); + crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); + }); +} + +pub fn run_fuzz_urb_hid(data: &[u8]) { + run_with_tokio(async { + let handler = Arc::new(UsbHidKeyboardHandler::new_keyboard()); + let device = UsbDevice::new(0) + .unwrap() + .with_interface( + ClassCode::HID as u8, + 0x00, + 0x00, + Some("Fuzz HID Keyboard"), + vec![UsbEndpoint { + address: 0x81, + attributes: 0x03, + max_packet_size: 0x08, + interval: 10, + ..Default::default() + }], + handler as Arc, + ) + .unwrap(); + + let mock = MockSocket::new(data.to_vec()); + let output = mock.output_handle(); + let _ = crate::handle_urb_loop(mock, Arc::new(device)).await; + let output_bytes = output.lock().unwrap(); + crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); + }); +} + +pub fn run_fuzz_urb_cdc(data: &[u8]) { + run_with_tokio(async { + let handler = Arc::new(UsbCdcAcmHandler::new()); + let device = UsbDevice::new(0) + .unwrap() + .with_interface( + ClassCode::CDC as u8, + CDC_ACM_SUBCLASS, + 0x00, + Some("Fuzz CDC ACM"), + UsbCdcAcmHandler::endpoints(), + handler as Arc, + ) + .unwrap(); + + let mock = MockSocket::new(data.to_vec()); + let output = mock.output_handle(); + let _ = crate::handle_urb_loop(mock, Arc::new(device)).await; + let output_bytes = output.lock().unwrap(); + crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); + }); +} + +pub fn run_fuzz_urb_uac(data: &[u8]) { + run_with_tokio(async { + let device = crate::uac::build_uac_loopback_device().unwrap(); + + let mock = MockSocket::new(data.to_vec()); + let output = mock.output_handle(); + let _ = crate::handle_urb_loop(mock, Arc::new(device)).await; + let output_bytes = output.lock().unwrap(); + crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); + }); +} +``` + +- [ ] **Step 2: Register the module in `lib/src/lib.rs`** + +After the existing `pub mod fuzz_helpers;` line (line 33), add the new module: + +```rust +#[cfg(feature = "fuzz")] +pub mod fuzz_helpers; +#[cfg(feature = "fuzz")] +pub mod fuzz_harness; +``` + +- [ ] **Step 3: Verify it compiles** + +Run: `nix develop -c cargo build -p usbip-rs --features fuzz` +Expected: Compiles without errors. + +- [ ] **Step 4: Commit** + +```bash +git add lib/src/fuzz_harness.rs lib/src/lib.rs +git commit -m "feat(fuzz): extract shared harness into fuzz_harness.rs" +``` + +--- + +### Task 2: Rename `lib/fuzz/` to `lib/fuzz-cargo/` and update wrappers + +**Files:** +- Rename: `lib/fuzz/` → `lib/fuzz-cargo/` +- Modify: `lib/fuzz-cargo/fuzz_targets/fuzz_parse_command.rs` +- Modify: `lib/fuzz-cargo/fuzz_targets/fuzz_handle_client.rs` +- Modify: `lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs` +- Modify: `lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs` +- Modify: `lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs` + +- [ ] **Step 1: Rename the directory** + +```bash +git mv lib/fuzz lib/fuzz-cargo +``` + +- [ ] **Step 2: Replace `fuzz_parse_command.rs` with thin wrapper** + +Replace the entire contents of `lib/fuzz-cargo/fuzz_targets/fuzz_parse_command.rs`: + +```rust +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_parse_command(data); +}); +``` + +- [ ] **Step 3: Replace `fuzz_handle_client.rs` with thin wrapper** + +Replace the entire contents of `lib/fuzz-cargo/fuzz_targets/fuzz_handle_client.rs`: + +```rust +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_handle_client(data); +}); +``` + +- [ ] **Step 4: Replace `fuzz_urb_hid.rs` with thin wrapper** + +Replace the entire contents of `lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs`: + +```rust +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_urb_hid(data); +}); +``` + +- [ ] **Step 5: Replace `fuzz_urb_uac.rs` with thin wrapper** + +Replace the entire contents of `lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs`: + +```rust +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_urb_uac(data); +}); +``` + +- [ ] **Step 6: Replace `fuzz_urb_cdc.rs` with thin wrapper** + +Replace the entire contents of `lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs`: + +```rust +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_urb_cdc(data); +}); +``` + +- [ ] **Step 7: Verify cargo-fuzz still works** + +Run: `nix develop .#fuzz -c bash -c "cd lib/fuzz-cargo && cargo fuzz list"` +Expected: Lists all 5 targets. + +Run: `nix develop .#fuzz -c bash -c "cd lib/fuzz-cargo && cargo fuzz build"` +Expected: All targets build successfully. + +- [ ] **Step 8: Commit** + +```bash +git add -A lib/fuzz-cargo lib/fuzz +git commit -m "refactor(fuzz): rename lib/fuzz to lib/fuzz-cargo, use shared harness" +``` + +--- + +### Task 3: Create `lib/fuzz-afl/` with AFL++ targets + +**Files:** +- Create: `lib/fuzz-afl/Cargo.toml` +- Create: `lib/fuzz-afl/.gitignore` +- Create: `lib/fuzz-afl/afl_targets/fuzz_parse_command.rs` +- Create: `lib/fuzz-afl/afl_targets/fuzz_handle_client.rs` +- Create: `lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs` +- Create: `lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs` +- Create: `lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs` + +- [ ] **Step 1: Create `lib/fuzz-afl/Cargo.toml`** + +```toml +[package] +name = "usbip-rs-fuzz-afl" +version = "0.0.0" +publish = false +edition = "2024" + +[dependencies] +usbip-rs = { path = "..", features = ["fuzz"] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "time", "io-util"] } + +[workspace] +members = ["."] + +[[bin]] +name = "fuzz_parse_command" +path = "afl_targets/fuzz_parse_command.rs" + +[[bin]] +name = "fuzz_handle_client" +path = "afl_targets/fuzz_handle_client.rs" + +[[bin]] +name = "fuzz_urb_hid" +path = "afl_targets/fuzz_urb_hid.rs" + +[[bin]] +name = "fuzz_urb_uac" +path = "afl_targets/fuzz_urb_uac.rs" + +[[bin]] +name = "fuzz_urb_cdc" +path = "afl_targets/fuzz_urb_cdc.rs" +``` + +- [ ] **Step 2: Create `lib/fuzz-afl/.gitignore`** + +``` +target/ +corpus/ +output/ +``` + +- [ ] **Step 3: Create `lib/fuzz-afl/afl_targets/fuzz_parse_command.rs`** + +```rust +use std::io::Read; + +fn main() { + let mut data = Vec::new(); + std::io::stdin().read_to_end(&mut data).unwrap(); + usbip_rs::fuzz_harness::run_fuzz_parse_command(&data); +} +``` + +- [ ] **Step 4: Create `lib/fuzz-afl/afl_targets/fuzz_handle_client.rs`** + +```rust +use std::io::Read; + +fn main() { + let mut data = Vec::new(); + std::io::stdin().read_to_end(&mut data).unwrap(); + usbip_rs::fuzz_harness::run_fuzz_handle_client(&data); +} +``` + +- [ ] **Step 5: Create `lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs`** + +```rust +use std::io::Read; + +fn main() { + let mut data = Vec::new(); + std::io::stdin().read_to_end(&mut data).unwrap(); + usbip_rs::fuzz_harness::run_fuzz_urb_hid(&data); +} +``` + +- [ ] **Step 6: Create `lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs`** + +```rust +use std::io::Read; + +fn main() { + let mut data = Vec::new(); + std::io::stdin().read_to_end(&mut data).unwrap(); + usbip_rs::fuzz_harness::run_fuzz_urb_uac(&data); +} +``` + +- [ ] **Step 7: Create `lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs`** + +```rust +use std::io::Read; + +fn main() { + let mut data = Vec::new(); + std::io::stdin().read_to_end(&mut data).unwrap(); + usbip_rs::fuzz_harness::run_fuzz_urb_cdc(&data); +} +``` + +- [ ] **Step 8: Verify the AFL++ targets compile with regular cargo** + +Run: `nix develop -c cargo build --manifest-path lib/fuzz-afl/Cargo.toml` +Expected: All 5 binaries build successfully (without AFL++ instrumentation, just verifying the code compiles). + +- [ ] **Step 9: Commit** + +```bash +git add lib/fuzz-afl +git commit -m "feat(fuzz): add AFL++ fuzz targets in lib/fuzz-afl" +``` + +--- + +### Task 4: Add AFL++ and SymCC Nix packages + +**Files:** +- Modify: `flake.nix` (packages section) + +- [ ] **Step 1: Add AFL++ package override** + +In the `packages` set in `flake.nix`, after the `inherit usbip-rs;` line, add: + +```nix +aflplusplus = pkgs.aflplusplus.overrideAttrs (old: { + version = "4.35c-unstable-2026-03-26"; + src = pkgs.fetchFromGitHub { + owner = "AFLplusplus"; + repo = "AFLplusplus"; + rev = "e5a8ba39ecf97d05e286fdd4e01da96554dbf64f"; + hash = lib.fakeHash; + }; +}); +``` + +- [ ] **Step 2: Build AFL++ to get the correct hash** + +Run: `nix build .#aflplusplus 2>&1 | grep 'got:'` + +Take the hash from the error output and replace `lib.fakeHash` with the actual hash string. + +- [ ] **Step 3: Verify AFL++ builds** + +Run: `nix build .#aflplusplus` +Expected: Builds successfully. If the newer commit breaks patches in the nixpkgs derivation, fix the override by adding `postPatch`, `patches`, or other phase overrides as needed. + +- [ ] **Step 4: Add SymCC package override** + +In the same `packages` set, add: + +```nix +symcc = pkgs.symcc.overrideAttrs (old: { + version = "1.0-unstable-2026-03-26"; + src = pkgs.fetchFromGitHub { + owner = "eurecom-s3"; + repo = "symcc"; + rev = "3b8acabf06c83b92facccde7f6dfb191b1a163b3"; + hash = lib.fakeHash; + fetchSubmodules = true; + }; +}); +``` + +- [ ] **Step 5: Build SymCC to get the correct hash** + +Run: `nix build .#symcc 2>&1 | grep 'got:'` + +Take the hash from the error output and replace `lib.fakeHash` with the actual hash string. + +- [ ] **Step 6: Verify SymCC builds** + +Run: `nix build .#symcc` +Expected: Builds successfully. If the newer commit breaks the cmake build, fix the override. + +- [ ] **Step 7: Verify both binaries work** + +Run: `nix build .#aflplusplus && ls $(nix build .#aflplusplus --print-out-paths --no-link)/bin/afl-fuzz` +Expected: `afl-fuzz` binary exists. + +Run: `nix build .#symcc && ls $(nix build .#symcc --print-out-paths --no-link)/bin/symcc` +Expected: `symcc` binary exists. + +- [ ] **Step 8: Commit** + +```bash +git add flake.nix +git commit -m "feat(nix): add aflplusplus and symcc package outputs" +``` + +--- + +### Task 5: Rename existing Nix apps and update paths + +**Files:** +- Modify: `flake.nix` (apps section, fuzz-env, devShells) + +- [ ] **Step 1: Update `fuzz-env` path** + +In the `fuzz-env` string in `flake.nix`, change: + +```nix +cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/lib" +``` + +to: + +```nix +cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/lib/fuzz-cargo" +``` + +(The `cargo fuzz` commands expect to be run from within the fuzz crate directory, which is now `lib/fuzz-cargo/` instead of `lib/`.) + +- [ ] **Step 2: Rename `fuzz-usbip` to `fuzz-cargo`** + +In the `apps` section, rename the `fuzz-usbip` variable and app to `fuzz-cargo`: + +Change the script variable name from `fuzz-usbip` to `fuzz-cargo` and the binary name from `"fuzz-usbip"` to `"fuzz-cargo"`. Update the app entry from `fuzz-usbip` to `fuzz-cargo`. + +- [ ] **Step 3: Rename `fuzz-clean-usbip` to `fuzz-clean-cargo`** + +Same pattern: rename the script variable, binary name, and app entry from `fuzz-clean-usbip` to `fuzz-clean-cargo`. + +- [ ] **Step 4: Update `gen-fuzz-corpus` path** + +In the `gen-fuzz-corpus` script, change: + +```nix +${pkgs.rustc}/bin/rustc "$root/lib/fuzz/gen_corpus.rs" -o /tmp/gen-fuzz-corpus +cd "$root/lib/fuzz" +``` + +to: + +```nix +${pkgs.rustc}/bin/rustc "$root/lib/fuzz-cargo/gen_corpus.rs" -o /tmp/gen-fuzz-corpus +cd "$root/lib/fuzz-cargo" +``` + +- [ ] **Step 5: Verify renamed apps work** + +Run: `nix run .#fuzz-cargo` +Expected: Lists the 5 fuzz targets. + +Run: `nix run .#gen-fuzz-corpus` +Expected: Generates seed files under `lib/fuzz-cargo/corpus/`. + +- [ ] **Step 6: Commit** + +```bash +git add flake.nix +git commit -m "refactor(nix): rename fuzz apps to fuzz-cargo/fuzz-clean-cargo" +``` + +--- + +### Task 6: Add `fuzz-afl` and `fuzz-clean-afl` Nix apps + +**Files:** +- Modify: `flake.nix` (apps section) + +- [ ] **Step 1: Add `fuzz-afl-env` helper** + +In the `apps` `let` block, after `fuzz-env`, add: + +```nix +afl-env = '' + export PATH="${rust-nightly}/bin:${self.packages.${system}.aflplusplus}/bin:${self.packages.${system}.symcc}/bin:${pkgs.stdenv.cc}/bin:${pkgs.pkg-config}/bin:${pkgs.coreutils}/bin:$PATH" + export PKG_CONFIG_PATH="${pkgs.libusb1.dev}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux '':${pkgs.udev.dev}/lib/pkgconfig''} + root="$(${pkgs.git}/bin/git rev-parse --show-toplevel)" +''; +``` + +- [ ] **Step 2: Add `fuzz-afl` app script** + +In the `apps` `let` block, add: + +```nix +fuzz-afl = pkgs.writeShellScriptBin "fuzz-afl" '' + set -euo pipefail + ${afl-env} + manifest="$root/lib/fuzz-afl/Cargo.toml" + + if [ $# -eq 0 ]; then + echo "Available targets:" + ${pkgs.gnugrep}/bin/grep -oP '(?<=name = ")[^"]+' "$manifest" | while read -r name; do + echo " $name" + done + exit 0 + fi + + target="$1" + shift + + afl_target_dir="$root/lib/fuzz-afl/target/afl" + symcc_target_dir="$root/lib/fuzz-afl/target/symcc" + + echo "Building $target with AFL++ instrumentation..." + CC=afl-cc CXX=afl-c++ RUSTFLAGS="-C linker=afl-cc" \ + CARGO_TARGET_DIR="$afl_target_dir" \ + cargo build --manifest-path "$manifest" --release --bin "$target" + + echo "Building $target with SymCC instrumentation..." + CC=symcc CXX=sym++ RUSTFLAGS="-C linker=symcc" \ + CARGO_TARGET_DIR="$symcc_target_dir" \ + cargo build --manifest-path "$manifest" --release --bin "$target" + + corpus_dir="$root/lib/fuzz-afl/corpus/$target" + output_dir="$root/lib/fuzz-afl/output/$target" + mkdir -p "$corpus_dir" "$output_dir" + + # Ensure corpus has at least one seed + if [ -z "$(ls -A "$corpus_dir" 2>/dev/null)" ]; then + echo "Warning: corpus dir is empty, creating minimal seed" + printf '\x00' > "$corpus_dir/seed-minimal" + fi + + echo "Starting AFL++ with SymCC companion on $target..." + afl-fuzz \ + -i "$corpus_dir" \ + -o "$output_dir" \ + -c "$symcc_target_dir/release/$target" \ + "$@" \ + -- "$afl_target_dir/release/$target" +''; +``` + +- [ ] **Step 3: Add `fuzz-clean-afl` app script** + +```nix +fuzz-clean-afl = pkgs.writeShellScriptBin "fuzz-clean-afl" '' + set -euo pipefail + ${afl-env} + manifest="$root/lib/fuzz-afl/Cargo.toml" + + if [ $# -eq 0 ]; then + echo "Usage: fuzz-clean-afl " + echo "Available targets:" + ${pkgs.gnugrep}/bin/grep -oP '(?<=name = ")[^"]+' "$manifest" | while read -r name; do + echo " $name" + done + exit 1 + fi + + target="$1" + afl_target_dir="$root/lib/fuzz-afl/target/afl" + dir="$root/lib/fuzz-afl/output/$target/default/crashes" + + if [ ! -d "$dir" ]; then + echo "No crashes directory: $dir" + exit 0 + fi + + shopt -s nullglob + files=("$dir"/id:*) + if [ ''${#files[@]} -eq 0 ]; then + echo "No crash files to test." + exit 0 + fi + + echo "Building $target with AFL++ instrumentation..." + CC=afl-cc CXX=afl-c++ RUSTFLAGS="-C linker=afl-cc" \ + CARGO_TARGET_DIR="$afl_target_dir" \ + cargo build --manifest-path "$manifest" --release --bin "$target" + + echo "Testing ''${#files[@]} crash files for $target..." + removed=0 + kept=0 + for f in "''${files[@]}"; do + if timeout 30 "$afl_target_dir/release/$target" < "$f" >/dev/null 2>&1; then + rm "$f" + removed=$((removed + 1)) + else + kept=$((kept + 1)) + fi + echo -ne "\r tested $((removed + kept))/''${#files[@]}, removed $removed, kept $kept" + done + echo "" + echo "Done: removed $removed fixed, kept $kept still-crashing." +''; +``` + +- [ ] **Step 4: Register both apps** + +In the `apps` attrset at the bottom of `flake.nix`, add: + +```nix +fuzz-afl = { + type = "app"; + program = "${fuzz-afl}/bin/fuzz-afl"; +}; +fuzz-clean-afl = { + type = "app"; + program = "${fuzz-clean-afl}/bin/fuzz-clean-afl"; +}; +``` + +- [ ] **Step 5: Verify `fuzz-afl` lists targets** + +Run: `nix run .#fuzz-afl` +Expected: Lists all 5 targets. + +- [ ] **Step 6: Commit** + +```bash +git add flake.nix +git commit -m "feat(nix): add fuzz-afl and fuzz-clean-afl apps" +``` + +--- + +### Task 7: Add `fuzz-afl` devShell + +**Files:** +- Modify: `flake.nix` (devShells section) + +- [ ] **Step 1: Add `fuzz-afl` devShell** + +In the `devShells` attrset, add after the `fuzz` entry: + +```nix +fuzz-afl = pkgs.mkShell { + buildInputs = [ + rust-nightly + self.packages.${system}.aflplusplus + self.packages.${system}.symcc + pkgs.libusb1 + ] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ + pkgs.udev + ]; + nativeBuildInputs = [ pkgs.stdenv.cc pkgs.pkg-config ]; +}; +``` + +- [ ] **Step 2: Verify the devShell enters** + +Run: `nix develop .#fuzz-afl -c bash -c "which afl-fuzz && which symcc"` +Expected: Prints paths to both binaries. + +- [ ] **Step 3: Commit** + +```bash +git add flake.nix +git commit -m "feat(nix): add fuzz-afl devShell" +``` + +--- + +### Task 8: Update CLAUDE.md + +**Files:** +- Modify: `CLAUDE.md` + +- [ ] **Step 1: Update the Fuzzing section** + +Replace the existing fuzzing section in `CLAUDE.md` with: + +```markdown +## Fuzzing + +Fuzz targets exercise host-side codepaths against untrusted client input. + +### libFuzzer (cargo-fuzz) + +Targets are in `lib/fuzz-cargo/` and use `cargo fuzz`. + +```bash +nix run .#fuzz-cargo # List targets +nix run .#fuzz-cargo -- fuzz_urb_hid # Single process +nix run .#fuzz-cargo -- fuzz_urb_hid --fork=8 # Parallel (overnight) +nix run .#fuzz-clean-cargo -- fuzz_urb_hid # Prune fixed artifacts +``` + +Crash artifacts: `lib/fuzz-cargo/artifacts//`. + +### AFL++ with SymCC + +Targets are in `lib/fuzz-afl/` and use AFL++ with SymCC as a companion concolic execution engine. + +```bash +nix run .#fuzz-afl # List targets +nix run .#fuzz-afl -- fuzz_urb_hid # Run AFL++ + SymCC +nix run .#fuzz-clean-afl -- fuzz_urb_hid # Prune fixed crashes +``` + +Crashes: `lib/fuzz-afl/output//default/crashes/`. Corpora are separate between engines; copy manually if desired. + +### Shared harness + +All fuzz target logic lives in `lib/src/fuzz_harness.rs`. Response validation is in `lib/src/fuzz_helpers.rs`. +``` + +- [ ] **Step 2: Update the "Fixing fuzzer crashes" section artifact path** + +Update the crash artifacts reference from `lib/fuzz/artifacts//` to `lib/fuzz-cargo/artifacts//`. + +- [ ] **Step 3: Commit** + +```bash +git add CLAUDE.md +git commit -m "docs: update CLAUDE.md for renamed fuzz dirs and AFL++ setup" +``` + +--- + +### Task 9: End-to-end verification + +- [ ] **Step 1: Verify libFuzzer still works end-to-end** + +Run: `nix run .#fuzz-cargo -- fuzz_parse_command -- -max_total_time=10` +Expected: Runs for 10 seconds without errors, exercises the target. + +- [ ] **Step 2: Verify AFL++ builds and starts** + +Run: `nix run .#fuzz-afl -- fuzz_parse_command` (cancel after it starts fuzzing) +Expected: Both AFL++ and SymCC binaries build, AFL++ starts fuzzing with SymCC companion, shows the AFL++ status screen. + +- [ ] **Step 3: Verify fuzz-clean-afl handles empty state** + +Run: `nix run .#fuzz-clean-afl -- fuzz_parse_command` +Expected: Reports "No crashes directory" or "No crash files to test." + +- [ ] **Step 4: Verify gen-fuzz-corpus still works** + +Run: `nix run .#gen-fuzz-corpus` +Expected: Generates seed files under `lib/fuzz-cargo/corpus/`. diff --git a/docs/superpowers/specs/2026-03-26-afl-symcc-fuzzing-design.md b/docs/superpowers/specs/2026-03-26-afl-symcc-fuzzing-design.md new file mode 100644 index 0000000..f4598cc --- /dev/null +++ b/docs/superpowers/specs/2026-03-26-afl-symcc-fuzzing-design.md @@ -0,0 +1,193 @@ +# AFL++ and SymCC Fuzzing Support + +## Goal + +Extend the fuzzing infrastructure to support AFL++ with SymCC as a concolic execution companion, complementing the existing libFuzzer setup. SymCC's constraint solving targets deeper coverage through complex branch conditions in protocol parsing, while AFL++ provides fast coverage-guided mutation. Both engines maintain separate corpora; the user copies inputs between them manually. + +## Security Context + +The primary fuzzing targets are `fuzz_parse_command` and `fuzz_handle_client` — these exercise the protocol parsing and connection lifecycle code that faces untrusted client input. The three device-specific targets (`fuzz_urb_hid`, `fuzz_urb_cdc`, `fuzz_urb_uac`) are included for completeness to exercise shared codepaths, but their device handlers are debug tools and not security-critical. + +## File Layout + +``` +lib/ + fuzz-cargo/ # renamed from fuzz/ (libFuzzer via cargo-fuzz) + Cargo.toml # unchanged except path + fuzz_targets/ # thin wrappers calling fuzz_harness + fuzz_parse_command.rs + fuzz_handle_client.rs + fuzz_urb_hid.rs + fuzz_urb_uac.rs + fuzz_urb_cdc.rs + gen_corpus.rs + fuzz-afl/ # new (AFL++ + SymCC) + Cargo.toml # depends on usbip-rs with "fuzz" feature, no afl crate + afl_targets/ # stdin-based wrappers calling fuzz_harness + fuzz_parse_command.rs + fuzz_handle_client.rs + fuzz_urb_hid.rs + fuzz_urb_uac.rs + fuzz_urb_cdc.rs + src/ + fuzz_harness.rs # shared harness logic (behind cfg "fuzz") + ...existing files... +``` + +## Shared Harness Layer + +A new module `lib/src/fuzz_harness.rs`, gated behind `#[cfg(feature = "fuzz")]`, provides one function per target: + +- `run_fuzz_parse_command(data: &[u8])` +- `run_fuzz_handle_client(data: &[u8])` +- `run_fuzz_urb_hid(data: &[u8])` +- `run_fuzz_urb_uac(data: &[u8])` +- `run_fuzz_urb_cdc(data: &[u8])` + +Each function encapsulates: tokio runtime creation, device/handler setup, MockSocket wiring, execution, and response validation via `fuzz_helpers::assert_usbip_responses_valid`. + +The libFuzzer wrappers (in `fuzz-cargo/fuzz_targets/`) become thin calls: + +```rust +#![no_main] +use libfuzzer_sys::fuzz_target; +fuzz_target!(|data: &[u8]| { usbip_rs::fuzz_harness::run_fuzz_urb_hid(data); }); +``` + +The AFL++ wrappers (in `fuzz-afl/afl_targets/`) read from stdin: + +```rust +use std::io::Read; +fn main() { + let mut data = Vec::new(); + std::io::stdin().read_to_end(&mut data).unwrap(); + usbip_rs::fuzz_harness::run_fuzz_urb_hid(&data); +} +``` + +No `afl` crate dependency — AFL++ instruments via the compiler/linker, and SymCC reuses the same stdin-based binary compiled with a different compiler. + +## Nix Packages + +Two new flake package outputs extending nixpkgs derivations with latest upstream commits: + +### `packages.aflplusplus` + +```nix +aflplusplus = pkgs.aflplusplus.overrideAttrs (old: { + version = "4.35c-unstable-2026-03-26"; + src = pkgs.fetchFromGitHub { + owner = "AFLplusplus"; + repo = "AFLplusplus"; + rev = "e5a8ba39ecf97d05e286fdd4e01da96554dbf64f"; + hash = "..."; # to be determined at build time + }; +}); +``` + +### `packages.symcc` + +```nix +symcc = pkgs.symcc.overrideAttrs (old: { + version = "1.0-unstable-2026-03-26"; + src = pkgs.fetchFromGitHub { + owner = "eurecom-s3"; + repo = "symcc"; + rev = "3b8acabf06c83b92facccde7f6dfb191b1a163b3"; + hash = "..."; # to be determined at build time + fetchSubmodules = true; + }; +}); +``` + +If the newer commits break the existing nixpkgs build phases or patches, the derivation overrides will include fixes. + +## Build Strategy + +Both AFL++ and SymCC instrument via the compiler/linker. The same Rust source is compiled twice with different toolchains: + +### AFL++ instrumented build + +```bash +export CC=afl-cc +export CXX=afl-c++ +export RUSTFLAGS="-C linker=afl-cc" +export CARGO_TARGET_DIR=target/afl-release +cargo build --manifest-path lib/fuzz-afl/Cargo.toml --release +``` + +### SymCC instrumented build + +```bash +export CC=symcc +export CXX=sym++ +export RUSTFLAGS="-C linker=symcc" +export CARGO_TARGET_DIR=target/symcc-release +cargo build --manifest-path lib/fuzz-afl/Cargo.toml --release +``` + +Separate `CARGO_TARGET_DIR` values prevent the two instrumented builds from clobbering each other. + +## Nix Apps + +### `fuzz-cargo` (renamed from `fuzz-usbip`) + +Same behavior as before, updated paths to `lib/fuzz-cargo/`. + +``` +nix run .#fuzz-cargo # List targets +nix run .#fuzz-cargo -- fuzz_urb_hid # Single process +nix run .#fuzz-cargo -- fuzz_urb_hid --fork=8 # Parallel +``` + +### `fuzz-clean-cargo` (renamed from `fuzz-clean-usbip`) + +Same behavior, updated paths to `lib/fuzz-cargo/`. + +### `fuzz-afl` + +Always runs AFL++ with SymCC as companion mutator (no opt-in flag). + +``` +nix run .#fuzz-afl # List targets +nix run .#fuzz-afl -- fuzz_urb_hid # Run AFL++ + SymCC +``` + +The app: +1. Builds the AFL++-instrumented binary (`afl-cc` as linker) +2. Builds the SymCC-instrumented binary (`symcc` as linker) +3. Runs `afl-fuzz -c /path/to/symcc-binary` to use SymCC as a companion mutator +4. Corpus dir: `lib/fuzz-afl/corpus//` +5. Output dir: `lib/fuzz-afl/output//` (AFL++ standard layout: `crashes/`, `queue/`, etc.) + +### `fuzz-clean-afl` + +``` +nix run .#fuzz-clean-afl -- fuzz_urb_hid +``` + +Replays files in `output//crashes/` against the AFL++-instrumented binary. Removes inputs that no longer crash. + +### `gen-fuzz-corpus` + +Updated path to `lib/fuzz-cargo/gen_corpus.rs`. Generates seeds for libFuzzer corpus; user copies to AFL++ manually if desired. + +## Nix DevShell + +A new `fuzz-afl` devShell providing `aflplusplus`, `symcc`, `rust-nightly`, and the usual build dependencies (libusb1, udev, pkg-config) for manual use outside the app wrappers. + +## Corpus Management + +Each engine maintains its own corpus: +- libFuzzer: `lib/fuzz-cargo/corpus//` +- AFL++: `lib/fuzz-afl/corpus//` + +Corpora are kept separate. The user manually copies inputs between engines when desired. No automated sync. + +## Rename Impacts + +Renaming `lib/fuzz/` to `lib/fuzz-cargo/` requires updates to: +- `flake.nix`: `fuzz-env` paths, app names, `gen-fuzz-corpus` path +- `CLAUDE.md`: fuzzing documentation references +- Existing corpus directories and artifact paths move with the rename +- The corpus generator (`gen_corpus.rs`) output paths diff --git a/docs/superpowers/specs/2026-03-26-cargo-afl-migration-design.md b/docs/superpowers/specs/2026-03-26-cargo-afl-migration-design.md new file mode 100644 index 0000000..f94958a --- /dev/null +++ b/docs/superpowers/specs/2026-03-26-cargo-afl-migration-design.md @@ -0,0 +1,116 @@ +# cargo-afl Migration Design + +**Date:** 2026-03-26 +**Status:** Approved + +## Goal + +Switch from the current manual AFL++ instrumentation setup to cargo-afl for best-practices AFL fuzzing with persistent mode, CmpLog (via LLVM plugins), and SymCC companion support. + +## Overview + +The current `fuzz-afl` setup manually invokes `afl-cc` as a linker with sancov RUSTFLAGS. Fuzz targets read from stdin. There is no persistent mode, no CmpLog, and SymCC is built but only available for manual use. + +The new setup uses cargo-afl (from rust-fuzz/afl.rs), which provides: +- **Persistent mode**: Shared-memory testcase delivery via `__afl_persistent_loop`, no process-per-input overhead +- **CmpLog with LLVM plugins**: Full comparison logging (instructions, switches, routines) for solving magic bytes +- **Deferred forkserver**: Fork after initialization, paying setup cost once +- **Standard tooling**: `cargo afl build`, `cargo afl fuzz`, `cargo afl cmin`, `cargo afl tmin`, etc. + +## Section 1: Nix Packaging + +### 1a. AFL++ with LLVM 22 plugins + +Extend the existing `aflplusplus` override in `flake.nix` to build against `llvmPackages_22` (matching rustc nightly's LLVM 22.1.0). The derivation produces: +- AFL++ binaries: `afl-fuzz`, `afl-cc`, `afl-cmin`, `afl-tmin`, etc. +- `afl-compiler-rt.o`: Compiler runtime linked into instrumented binaries +- Plugin `.so` files: `SanitizerCoveragePCGUARD.so`, `cmplog-instructions-pass.so`, `cmplog-switches-pass.so`, `cmplog-routines-pass.so`, `split-switches-pass.so`, `afl-llvm-dict2file.so`, `afl-llvm-ijon-pass.so` + +The plugins must be compiled against the same LLVM version that rustc nightly uses. Rustc nightly 1.96.0 uses LLVM 22.1.0; nixpkgs provides `llvmPackages_22` with 22.1.0-rc3. + +### 1b. cargo-afl + +Build from `https://github.com/rust-fuzz/afl.rs.git` at commit `644c06a` using `rustPlatform.buildRustPackage`. + +Apply a patch to `cargo-afl-common/src/lib.rs` that adds two environment variable overrides: +- `CARGO_AFL_DIR` — overrides `afl_dir()` return value (directory containing `bin/afl-fuzz` etc.) +- `CARGO_AFL_LLVM_DIR` — overrides `afl_llvm_dir()` return value (directory containing `afl-compiler-rt.o` and plugin `.so` files) + +When set, these skip the XDG/version-based path construction entirely. When unset, cargo-afl falls back to the original XDG behavior. This avoids polluting the user's home directory. + +The Nix wrapper scripts and devShell set these env vars pointing at the AFL++ Nix store outputs. + +## Section 2: Fuzz Target Changes + +### afl crate dependency + +Add the `afl` crate to `lib/fuzz-afl/Cargo.toml`. This is the library component of afl.rs that provides the `fuzz!()` macro, persistent mode support, and panic-as-abort hook. + +### Target wrapper rewrites + +Rewrite all 5 target wrappers in `lib/fuzz-afl/afl_targets/` from stdin-reading to the `fuzz!()` macro: + +**Before:** +```rust +use std::io::Read; +fn main() { + let mut data = Vec::new(); + std::io::stdin().read_to_end(&mut data).unwrap(); + usbip_rs::fuzz_harness::run_fuzz_urb_hid(&data); +} +``` + +**After:** +```rust +fn main() { + afl::fuzz!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_urb_hid(data); + }); +} +``` + +The `fuzz!()` macro provides: +- **Persistent mode**: Process stays alive, AFL++ feeds test cases via shared memory +- **Deferred forkserver**: Forking happens after initialization +- **Panic-as-crash**: Installs a panic hook that calls `abort()`, so AFL++ detects assertion failures + +### Shared harness unchanged + +`lib/src/fuzz_harness.rs` and `lib/src/fuzz_helpers.rs` are not modified. The targets remain thin wrappers calling the same harness functions. + +## Section 3: Wrapper Script & DevShell Changes + +### fuzz-afl app rewrite + +1. Set `CARGO_AFL_DIR` and `CARGO_AFL_LLVM_DIR` pointing at Nix-built AFL++ outputs +2. Build AFL++ target: `cargo afl build` (handles instrumentation flags, plugin loading, compiler-rt linking) +3. Build SymCC target: same as current (`CC=symcc CXX=sym++ RUSTFLAGS="-C linker=symcc"` with separate `CARGO_TARGET_DIR`) +4. Run: `cargo afl fuzz -i $corpus -o $output "$@" -- ./target/release/$target` + - cargo-afl prepends `-c0` for CmpLog automatically + - SymCC binary path printed for manual companion use +5. User can pass extra afl-fuzz flags via `"$@"` + +### fuzz-clean-afl app update + +- Rebuild with `cargo afl build` instead of manual RUSTFLAGS +- Test crash files against the cargo-afl-built binary (same validation logic) + +### fuzz-afl devShell + +- Replace raw `aflplusplus` with `cargo-afl` package +- Keep `symcc`, `rust-nightly`, `libusb1`, `udev`, `pkg-config` +- Set `CARGO_AFL_DIR` and `CARGO_AFL_LLVM_DIR` env vars for interactive use + +### Old aflplusplus package + +Drop the standalone `aflplusplus` package from flake.nix if nothing else uses it — superseded by the LLVM-22-plugin-enabled build. + +## Section 4: Testing & Verification + +- `nix develop .#fuzz-afl` provides a working shell with `cargo afl build`/`cargo afl fuzz` +- `cargo afl fuzz --version` reports AFL++ version and "with plugins" +- `nix run .#fuzz-afl -- fuzz_urb_hid` builds with plugins, starts AFL++ with CmpLog, runs in persistent mode +- AFL++ status screen shows `[persist]` mode indicator and CmpLog stats +- SymCC binary path printed and functional +- `cargo afl build --verbose` shows `-Z llvm-plugins=...` flags +- Existing corpora in `lib/fuzz-afl/corpus/` and `lib/fuzz-afl/output/` preserved diff --git a/flake.nix b/flake.nix index 06ef18c..b7faa75 100644 --- a/flake.nix +++ b/flake.nix @@ -55,6 +55,54 @@ packages = { default = usbip-rs; inherit usbip-rs; + + # AFL++ built with LLVM 22 plugins to match rust-nightly's LLVM. + # Plugins (CmpLog, IJON, SanitizerCoveragePCGUARD, etc.) are LLVM + # passes loaded via rustc's -Z llvm-plugins flag and must be + # compiled against the same LLVM major version. + aflplusplus = (pkgs.aflplusplus.override { + clang = pkgs.llvmPackages_22.clang; + llvm = pkgs.llvmPackages_22.libllvm.dev; + llvmPackages = pkgs.llvmPackages_22; + }).overrideAttrs (old: { + version = "4.35c-unstable-2026-03-26"; + src = pkgs.fetchFromGitHub { + owner = "AFLplusplus"; + repo = "AFLplusplus"; + rev = "e5a8ba39ecf97d05e286fdd4e01da96554dbf64f"; + hash = "sha256-QtGazGShjybvjOONoWjqSg/c+l5sPpaFuuTI2S85YQM="; + }; + # The performance test script exits non-zero when skipped in + # sandboxed builds. Disable installCheck since we only need + # the binaries and plugins. + doInstallCheck = false; + }); + + cargo-afl = pkgs.rustPlatform.buildRustPackage { + pname = "cargo-afl"; + version = "0.17.1-unstable-2026-03-26"; + src = pkgs.fetchFromGitHub { + owner = "rust-fuzz"; + repo = "afl.rs"; + rev = "644c06a7dd8330db92d987bf1efa9d7a6cf2e3c1"; + hash = "sha256-wxUL++xRhTIRQ6v0acuJ9OZQkem5HhQllfulKId85X4="; + }; + cargoHash = "sha256-aCz6zG9PwhoyEXNo+qvuiBIVIQ14XEs6mTj2dbHvNYY="; + patches = [ ./nix/cargo-afl-env-paths.patch ]; + cargoBuildFlags = [ "-p" "cargo-afl" ]; + doCheck = false; + }; + + symcc = pkgs.symcc.overrideAttrs (old: { + version = "1.0-unstable-2026-03-26"; + src = pkgs.fetchFromGitHub { + owner = "eurecom-s3"; + repo = "symcc"; + rev = "3b8acabf06c83b92facccde7f6dfb191b1a163b3"; + hash = "sha256-k87bwp2dAFga/5ui8fepJ/ZIHl4WTyVFhfgvTLBxP6c="; + fetchSubmodules = true; + }; + }); }; checks = { @@ -76,6 +124,20 @@ ]; nativeBuildInputs = [ pkgs.stdenv.cc pkgs.pkg-config ]; }; + + fuzz-afl = pkgs.mkShell { + buildInputs = [ + rust-nightly + self.packages.${system}.cargo-afl + self.packages.${system}.symcc + pkgs.libusb1 + ] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ + pkgs.udev + ]; + nativeBuildInputs = [ pkgs.stdenv.cc pkgs.pkg-config ]; + CARGO_AFL_DIR = "${self.packages.${system}.aflplusplus}"; + CARGO_AFL_LLVM_DIR = "${self.packages.${system}.aflplusplus}/lib/afl"; + }; }; apps = let @@ -84,11 +146,11 @@ export PKG_CONFIG_PATH="${pkgs.libusb1.dev}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux '':${pkgs.udev.dev}/lib/pkgconfig''} cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/lib" ''; - fuzz-usbip = pkgs.writeShellScriptBin "fuzz-usbip" '' + fuzz-cargo = pkgs.writeShellScriptBin "fuzz-cargo" '' set -euo pipefail ${fuzz-env} if [ $# -eq 0 ]; then - cargo fuzz list + cargo fuzz list --fuzz-dir fuzz-cargo else target="$1" shift @@ -102,25 +164,25 @@ done if [ "$fork" -gt 0 ]; then while true; do - cargo fuzz run "$target" -- -max_len=1048576 "-fork=$fork" "''${args[@]}" || true + cargo fuzz run --fuzz-dir fuzz-cargo "$target" -- -max_len=1048576 "-fork=$fork" "''${args[@]}" || true echo "--- fuzzer exited, restarting (artifacts saved) ---" done else - cargo fuzz run "$target" -- -max_len=1048576 "''${args[@]}" + cargo fuzz run --fuzz-dir fuzz-cargo "$target" -- -max_len=1048576 "''${args[@]}" fi fi ''; - fuzz-clean-usbip = pkgs.writeShellScriptBin "fuzz-clean-usbip" '' + fuzz-clean-cargo = pkgs.writeShellScriptBin "fuzz-clean-cargo" '' set -euo pipefail ${fuzz-env} if [ $# -eq 0 ]; then - echo "Usage: fuzz-clean-usbip " + echo "Usage: fuzz-clean-cargo " echo "Available targets:" - cargo fuzz list + cargo fuzz list --fuzz-dir fuzz-cargo exit 1 fi target="$1" - dir="fuzz/artifacts/$target" + dir="fuzz-cargo/artifacts/$target" if [ ! -d "$dir" ]; then echo "No artifacts directory: $dir" exit 0 @@ -132,7 +194,7 @@ exit 0 fi echo "Building $target..." - if ! cargo fuzz build "$target" 2>&1; then + if ! cargo fuzz build --fuzz-dir fuzz-cargo "$target" 2>&1; then echo "Build failed — not touching artifacts." exit 1 fi @@ -140,7 +202,7 @@ removed=0 kept=0 for f in "''${files[@]}"; do - if timeout 30 cargo fuzz run "$target" "$f" -- -max_len=1048576 >/dev/null 2>&1; then + if timeout 30 cargo fuzz run --fuzz-dir fuzz-cargo "$target" "$f" -- -max_len=1048576 >/dev/null 2>&1; then rm "$f" removed=$((removed + 1)) else @@ -155,18 +217,222 @@ set -euo pipefail export PATH="${pkgs.stdenv.cc}/bin:$PATH" root="$(${pkgs.git}/bin/git rev-parse --show-toplevel)" - ${pkgs.rustc}/bin/rustc "$root/lib/fuzz/gen_corpus.rs" -o /tmp/gen-fuzz-corpus - cd "$root/lib/fuzz" + ${pkgs.rustc}/bin/rustc "$root/lib/fuzz-cargo/gen_corpus.rs" -o /tmp/gen-fuzz-corpus + cd "$root/lib/fuzz-cargo" + /tmp/gen-fuzz-corpus + cd "$root/lib/fuzz-afl" /tmp/gen-fuzz-corpus ''; + afl-env = '' + export CARGO_AFL_DIR="${self.packages.${system}.aflplusplus}" + export CARGO_AFL_LLVM_DIR="${self.packages.${system}.aflplusplus}/lib/afl" + export PATH="${self.packages.${system}.cargo-afl}/bin:${rust-nightly}/bin:${self.packages.${system}.symcc}/bin:${pkgs.stdenv.cc}/bin:${pkgs.pkg-config}/bin:${pkgs.coreutils}/bin:$PATH" + export PKG_CONFIG_PATH="${pkgs.libusb1.dev}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux '':${pkgs.udev.dev}/lib/pkgconfig''} + root="$(${pkgs.git}/bin/git rev-parse --show-toplevel)" + ''; + symcc-companion = pkgs.writeShellScriptBin "symcc-companion" '' + set -uo pipefail + output_dir="$1" + symcc_binary="$2" + symcc_workdir="$output_dir/symcc" + logfile="$symcc_workdir/companion.log" + mkdir -p "$symcc_workdir/queue" "$symcc_workdir/tmp" + + log() { echo "[symcc $(date +%H:%M:%S)] $*" >> "$logfile"; } + log "companion started (pid $$)" + + # Wait for any AFL++ instance to create a queue directory + while ! ls -d "$output_dir"/*/queue >/dev/null 2>&1; do sleep 1; done + log "AFL++ queue detected" + + processed="$symcc_workdir/.processed" + touch "$processed" + shopt -s nullglob + while true; do + # Process queue entries from all AFL++ instances + for f in "$output_dir"/*/queue/id:*; do + [ -f "$f" ] || continue + # Track by full path to avoid collisions across instances + grep -qxF "$f" "$processed" 2>/dev/null && continue + rm -rf "$symcc_workdir/tmp"/* + SYMCC_OUTPUT_DIR="$symcc_workdir/tmp" \ + ${pkgs.coreutils}/bin/timeout 30 "$symcc_binary" < "$f" >/dev/null 2>&1 || true + new_count=0 + for new in "$symcc_workdir/tmp"/*; do + [ -f "$new" ] || continue + # Write to symcc/queue/ — AFL++ auto-syncs from all + # instance directories under the output dir + cp "$new" "$symcc_workdir/queue/id:symcc_''${RANDOM}_''${RANDOM}" 2>/dev/null || true + new_count=$((new_count + 1)) + done + log "processed $(basename "$f") -> $new_count new inputs" + echo "$f" >> "$processed" + done + sleep 5 + done + ''; + fuzz-afl = pkgs.writeShellScriptBin "fuzz-afl" '' + set -euo pipefail + ${afl-env} + manifest="$root/lib/fuzz-afl/Cargo.toml" + + if [ $# -eq 0 ]; then + echo "Available targets:" + ${pkgs.gawk}/bin/awk '/^\[\[bin\]\]/{found=1} found && /^name = "/{gsub(/"/, "", $3); print " " $3; found=0}' "$manifest" + exit 0 + fi + + target="$1" + shift + + # Parse --jobs=N or --jobs N from arguments + jobs=1 + afl_args=() + skip_next=false + for arg in "$@"; do + if $skip_next; then jobs="$arg"; skip_next=false; continue; fi + case "$arg" in + --jobs=*) jobs="''${arg#--jobs=}" ;; + --jobs) skip_next=true ;; + *) afl_args+=("$arg") ;; + esac + done + + afl_target_dir="$root/lib/fuzz-afl/target/afl" + symcc_target_dir="$root/lib/fuzz-afl/target/symcc" + + echo "Building $target with cargo-afl (plugins + CmpLog)..." + CARGO_TARGET_DIR="$afl_target_dir" \ + cargo afl build --manifest-path "$manifest" --release --bin "$target" + + echo "Building $target with SymCC instrumentation..." + CC=symcc CXX=sym++ RUSTFLAGS="-C linker=symcc -Clink-arg=$CARGO_AFL_LLVM_DIR/afl-compiler-rt.o" \ + CARGO_TARGET_DIR="$symcc_target_dir" \ + cargo build --manifest-path "$manifest" --release --bin "$target" + + corpus_dir="$root/lib/fuzz-afl/corpus/$target" + output_dir="$root/lib/fuzz-afl/output/$target" + mkdir -p "$corpus_dir" "$output_dir" + + # Ensure corpus has at least one seed + if [ -z "$(ls -A "$corpus_dir" 2>/dev/null)" ]; then + echo "Warning: corpus dir is empty, creating minimal seed" + printf '\x00' > "$corpus_dir/seed-minimal" + fi + + # Use a systemd slice for proper process management. + # All background fuzzers and the SymCC companion run as + # transient systemd units; stopping the slice kills them all. + slice="fuzz_afl_$target" + ${pkgs.systemd}/bin/systemctl --user stop "$slice.slice" 2>/dev/null || true + # Clean stale AFL++ lock files from killed previous runs + rm -f "$output_dir"/*/is_main_node "$output_dir"/*/.cur_input 2>/dev/null || true + trap '${pkgs.systemd}/bin/systemctl --user stop "$slice.slice" 2>/dev/null || true' EXIT + + # SymCC companion + ${pkgs.systemd}/bin/systemd-run --user --collect \ + --slice="$slice" --unit="$slice-symcc" --quiet \ + ${symcc-companion}/bin/symcc-companion \ + "$output_dir" "$symcc_target_dir/release/$target" + + # Secondary AFL++ instances (--jobs > 1) + for i in $(seq 2 "$jobs"); do + ${pkgs.systemd}/bin/systemd-run --user --collect \ + --slice="$slice" --unit="$slice-s$i" --quiet \ + --setenv=AFL_AUTORESUME=1 \ + "$CARGO_AFL_DIR/bin/afl-fuzz" \ + -S "secondary_$i" -p fast \ + -i "$corpus_dir" -o "$output_dir" \ + -- "$afl_target_dir/release/$target" + done + + # Determine main instance flags + if [ "$jobs" -gt 1 ]; then + main_flag="-M main" + else + main_flag="" + fi + + echo "Starting AFL++ on $target..." + echo " jobs : $jobs''${jobs:+ (main + $((jobs-1)) secondaries)}" + echo " persistent mode : yes (afl::fuzz! macro)" + echo " CmpLog : -c 0 -l 2AT" + echo " power schedule : -p fast" + echo " LLVM plugins : loaded" + echo " SymCC companion : running (log: $output_dir/symcc/companion.log)" + echo " stop all : systemctl --user stop $slice.slice" + echo "" + # Main instance runs in foreground so the user sees the TUI. + # shellcheck disable=SC2086 + AFL_AUTORESUME=1 "$CARGO_AFL_DIR/bin/afl-fuzz" \ + $main_flag \ + -c 0 -l 2AT -p fast \ + -i "$corpus_dir" \ + -o "$output_dir" \ + "''${afl_args[@]}" \ + -- "$afl_target_dir/release/$target" + ''; + fuzz-clean-afl = pkgs.writeShellScriptBin "fuzz-clean-afl" '' + set -euo pipefail + ${afl-env} + manifest="$root/lib/fuzz-afl/Cargo.toml" + + if [ $# -eq 0 ]; then + echo "Usage: fuzz-clean-afl " + echo "Available targets:" + ${pkgs.gawk}/bin/awk '/^\[\[bin\]\]/{found=1} found && /^name = "/{gsub(/"/, "", $3); print " " $3; found=0}' "$manifest" + exit 1 + fi + + target="$1" + afl_target_dir="$root/lib/fuzz-afl/target/afl" + + # Collect crashes from all AFL++ instance directories + shopt -s nullglob + files=() + for d in "$root/lib/fuzz-afl/output/$target"/*/crashes; do + files+=("$d"/id:*) + done + if [ ''${#files[@]} -eq 0 ]; then + echo "No crash files to test." + exit 0 + fi + + echo "Building $target with cargo-afl..." + CARGO_TARGET_DIR="$afl_target_dir" \ + cargo afl build --manifest-path "$manifest" --release --bin "$target" + + echo "Testing ''${#files[@]} crash files for $target..." + removed=0 + kept=0 + for f in "''${files[@]}"; do + if timeout 30 "$afl_target_dir/release/$target" < "$f" >/dev/null 2>&1; then + rm "$f" + removed=$((removed + 1)) + else + kept=$((kept + 1)) + fi + echo -ne "\r tested $((removed + kept))/''${#files[@]}, removed $removed, kept $kept" + done + echo "" + echo "Done: removed $removed fixed, kept $kept still-crashing." + ''; in { - fuzz-usbip = { + fuzz-cargo = { type = "app"; - program = "${fuzz-usbip}/bin/fuzz-usbip"; + program = "${fuzz-cargo}/bin/fuzz-cargo"; }; - fuzz-clean-usbip = { + fuzz-clean-cargo = { type = "app"; - program = "${fuzz-clean-usbip}/bin/fuzz-clean-usbip"; + program = "${fuzz-clean-cargo}/bin/fuzz-clean-cargo"; + }; + fuzz-afl = { + type = "app"; + program = "${fuzz-afl}/bin/fuzz-afl"; + }; + fuzz-clean-afl = { + type = "app"; + program = "${fuzz-clean-afl}/bin/fuzz-clean-afl"; }; gen-fuzz-corpus = { type = "app"; diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b3a4bab..6d58a0c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -24,4 +24,4 @@ env_logger = "0.11.7" [features] default = [] serde = ["dep:serde"] -fuzz = ["dep:arbitrary"] +fuzz = ["dep:arbitrary", "tokio/rt-multi-thread"] diff --git a/lib/fuzz-afl/.gitignore b/lib/fuzz-afl/.gitignore new file mode 100644 index 0000000..14d7a7a --- /dev/null +++ b/lib/fuzz-afl/.gitignore @@ -0,0 +1,3 @@ +target/ +corpus/ +output/ diff --git a/lib/fuzz-afl/Cargo.lock b/lib/fuzz-afl/Cargo.lock new file mode 100644 index 0000000..e3b6199 --- /dev/null +++ b/lib/fuzz-afl/Cargo.lock @@ -0,0 +1,383 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "afl" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62656735672273d859b4e3dcd2d3c6dbe2f4decee62e3c206aad2363b1a2f9e2" +dependencies = [ + "home", + "libc", + "rustc_version", + "xdg", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "io-kit-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d3a048d09fbb6597dbf7c69f40d14df4a49487db1487191618c893fc3b1c26" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nusb" +version = "0.2.3" +source = "git+https://git.dsg.is/dsg/nusb.git?rev=1239c676#1239c6765ab478b19b143544a467fadbb472197b" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "futures-core", + "io-kit-sys", + "linux-raw-sys", + "log", + "once_cell", + "rustix", + "slab", + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "usbip-rs" +version = "0.8.0" +dependencies = [ + "arbitrary", + "log", + "num-derive", + "num-traits", + "nusb", + "tokio", + "tokio-util", +] + +[[package]] +name = "usbip-rs-fuzz-afl" +version = "0.0.0" +dependencies = [ + "afl", + "tokio", + "usbip-rs", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "xdg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" diff --git a/lib/fuzz-afl/Cargo.toml b/lib/fuzz-afl/Cargo.toml new file mode 100644 index 0000000..6db6884 --- /dev/null +++ b/lib/fuzz-afl/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "usbip-rs-fuzz-afl" +version = "0.0.0" +publish = false +edition = "2024" + +[dependencies] +usbip-rs = { path = "..", features = ["fuzz"] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "time", "io-util"] } +afl = "0.17.1" + +[workspace] +members = ["."] + +[[bin]] +name = "fuzz_parse_command" +path = "afl_targets/fuzz_parse_command.rs" + +[[bin]] +name = "fuzz_handle_client" +path = "afl_targets/fuzz_handle_client.rs" + +[[bin]] +name = "fuzz_urb_hid" +path = "afl_targets/fuzz_urb_hid.rs" + +[[bin]] +name = "fuzz_urb_uac" +path = "afl_targets/fuzz_urb_uac.rs" + +[[bin]] +name = "fuzz_urb_cdc" +path = "afl_targets/fuzz_urb_cdc.rs" diff --git a/lib/fuzz-afl/afl_targets/fuzz_handle_client.rs b/lib/fuzz-afl/afl_targets/fuzz_handle_client.rs new file mode 100644 index 0000000..72ed0f2 --- /dev/null +++ b/lib/fuzz-afl/afl_targets/fuzz_handle_client.rs @@ -0,0 +1,5 @@ +fn main() { + afl::fuzz!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_handle_client(data); + }); +} diff --git a/lib/fuzz-afl/afl_targets/fuzz_parse_command.rs b/lib/fuzz-afl/afl_targets/fuzz_parse_command.rs new file mode 100644 index 0000000..9dbf28d --- /dev/null +++ b/lib/fuzz-afl/afl_targets/fuzz_parse_command.rs @@ -0,0 +1,5 @@ +fn main() { + afl::fuzz!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_parse_command(data); + }); +} diff --git a/lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs b/lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs new file mode 100644 index 0000000..1ad21bc --- /dev/null +++ b/lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs @@ -0,0 +1,5 @@ +fn main() { + afl::fuzz!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_urb_cdc(data); + }); +} diff --git a/lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs b/lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs new file mode 100644 index 0000000..2b9b6d3 --- /dev/null +++ b/lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs @@ -0,0 +1,5 @@ +fn main() { + afl::fuzz!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_urb_hid(data); + }); +} diff --git a/lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs b/lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs new file mode 100644 index 0000000..3a0034e --- /dev/null +++ b/lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs @@ -0,0 +1,5 @@ +fn main() { + afl::fuzz!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_urb_uac(data); + }); +} diff --git a/lib/fuzz/.gitignore b/lib/fuzz-cargo/.gitignore similarity index 100% rename from lib/fuzz/.gitignore rename to lib/fuzz-cargo/.gitignore diff --git a/lib/fuzz/Cargo.toml b/lib/fuzz-cargo/Cargo.toml similarity index 100% rename from lib/fuzz/Cargo.toml rename to lib/fuzz-cargo/Cargo.toml diff --git a/lib/fuzz/corpus/fuzz_handle_client/seed-devlist-only b/lib/fuzz-cargo/corpus/fuzz_handle_client/seed-devlist-only similarity index 100% rename from lib/fuzz/corpus/fuzz_handle_client/seed-devlist-only rename to lib/fuzz-cargo/corpus/fuzz_handle_client/seed-devlist-only diff --git a/lib/fuzz/corpus/fuzz_handle_client/seed-devlist-then-import b/lib/fuzz-cargo/corpus/fuzz_handle_client/seed-devlist-then-import similarity index 100% rename from lib/fuzz/corpus/fuzz_handle_client/seed-devlist-then-import rename to lib/fuzz-cargo/corpus/fuzz_handle_client/seed-devlist-then-import diff --git a/lib/fuzz/corpus/fuzz_handle_client/seed-import-enumerate b/lib/fuzz-cargo/corpus/fuzz_handle_client/seed-import-enumerate similarity index 100% rename from lib/fuzz/corpus/fuzz_handle_client/seed-import-enumerate rename to lib/fuzz-cargo/corpus/fuzz_handle_client/seed-import-enumerate diff --git a/lib/fuzz/corpus/fuzz_handle_client/seed-import-hid-full b/lib/fuzz-cargo/corpus/fuzz_handle_client/seed-import-hid-full similarity index 100% rename from lib/fuzz/corpus/fuzz_handle_client/seed-import-hid-full rename to lib/fuzz-cargo/corpus/fuzz_handle_client/seed-import-hid-full diff --git a/lib/fuzz/corpus/fuzz_parse_command/seed-devlist b/lib/fuzz-cargo/corpus/fuzz_parse_command/seed-devlist similarity index 100% rename from lib/fuzz/corpus/fuzz_parse_command/seed-devlist rename to lib/fuzz-cargo/corpus/fuzz_parse_command/seed-devlist diff --git a/lib/fuzz/corpus/fuzz_parse_command/seed-import b/lib/fuzz-cargo/corpus/fuzz_parse_command/seed-import similarity index 100% rename from lib/fuzz/corpus/fuzz_parse_command/seed-import rename to lib/fuzz-cargo/corpus/fuzz_parse_command/seed-import diff --git a/lib/fuzz/corpus/fuzz_urb_cdc/seed-cdc-bulk-in b/lib/fuzz-cargo/corpus/fuzz_urb_cdc/seed-cdc-bulk-in similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_cdc/seed-cdc-bulk-in rename to lib/fuzz-cargo/corpus/fuzz_urb_cdc/seed-cdc-bulk-in diff --git a/lib/fuzz/corpus/fuzz_urb_cdc/seed-cdc-bulk-out b/lib/fuzz-cargo/corpus/fuzz_urb_cdc/seed-cdc-bulk-out similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_cdc/seed-cdc-bulk-out rename to lib/fuzz-cargo/corpus/fuzz_urb_cdc/seed-cdc-bulk-out diff --git a/lib/fuzz/corpus/fuzz_urb_cdc/seed-cdc-class-requests b/lib/fuzz-cargo/corpus/fuzz_urb_cdc/seed-cdc-class-requests similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_cdc/seed-cdc-class-requests rename to lib/fuzz-cargo/corpus/fuzz_urb_cdc/seed-cdc-class-requests diff --git a/lib/fuzz/corpus/fuzz_urb_cdc/seed-cdc-interrupt-in b/lib/fuzz-cargo/corpus/fuzz_urb_cdc/seed-cdc-interrupt-in similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_cdc/seed-cdc-interrupt-in rename to lib/fuzz-cargo/corpus/fuzz_urb_cdc/seed-cdc-interrupt-in diff --git a/lib/fuzz/corpus/fuzz_urb_cdc/seed-enumerate b/lib/fuzz-cargo/corpus/fuzz_urb_cdc/seed-enumerate similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_cdc/seed-enumerate rename to lib/fuzz-cargo/corpus/fuzz_urb_cdc/seed-enumerate diff --git a/lib/fuzz/corpus/fuzz_urb_hid/seed-enumerate b/lib/fuzz-cargo/corpus/fuzz_urb_hid/seed-enumerate similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_hid/seed-enumerate rename to lib/fuzz-cargo/corpus/fuzz_urb_hid/seed-enumerate diff --git a/lib/fuzz/corpus/fuzz_urb_hid/seed-get-status b/lib/fuzz-cargo/corpus/fuzz_urb_hid/seed-get-status similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_hid/seed-get-status rename to lib/fuzz-cargo/corpus/fuzz_urb_hid/seed-get-status diff --git a/lib/fuzz/corpus/fuzz_urb_hid/seed-hid-class-requests b/lib/fuzz-cargo/corpus/fuzz_urb_hid/seed-hid-class-requests similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_hid/seed-hid-class-requests rename to lib/fuzz-cargo/corpus/fuzz_urb_hid/seed-hid-class-requests diff --git a/lib/fuzz/corpus/fuzz_urb_hid/seed-hid-interrupt-in b/lib/fuzz-cargo/corpus/fuzz_urb_hid/seed-hid-interrupt-in similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_hid/seed-hid-interrupt-in rename to lib/fuzz-cargo/corpus/fuzz_urb_hid/seed-hid-interrupt-in diff --git a/lib/fuzz/corpus/fuzz_urb_hid/seed-unlink b/lib/fuzz-cargo/corpus/fuzz_urb_hid/seed-unlink similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_hid/seed-unlink rename to lib/fuzz-cargo/corpus/fuzz_urb_hid/seed-unlink diff --git a/lib/fuzz/corpus/fuzz_urb_uac/seed-enumerate b/lib/fuzz-cargo/corpus/fuzz_urb_uac/seed-enumerate similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_uac/seed-enumerate rename to lib/fuzz-cargo/corpus/fuzz_urb_uac/seed-enumerate diff --git a/lib/fuzz/corpus/fuzz_urb_uac/seed-uac-iso-in b/lib/fuzz-cargo/corpus/fuzz_urb_uac/seed-uac-iso-in similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_uac/seed-uac-iso-in rename to lib/fuzz-cargo/corpus/fuzz_urb_uac/seed-uac-iso-in diff --git a/lib/fuzz/corpus/fuzz_urb_uac/seed-uac-iso-out b/lib/fuzz-cargo/corpus/fuzz_urb_uac/seed-uac-iso-out similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_uac/seed-uac-iso-out rename to lib/fuzz-cargo/corpus/fuzz_urb_uac/seed-uac-iso-out diff --git a/lib/fuzz/corpus/fuzz_urb_uac/seed-uac-iso-out-multi b/lib/fuzz-cargo/corpus/fuzz_urb_uac/seed-uac-iso-out-multi similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_uac/seed-uac-iso-out-multi rename to lib/fuzz-cargo/corpus/fuzz_urb_uac/seed-uac-iso-out-multi diff --git a/lib/fuzz/corpus/fuzz_urb_uac/seed-uac-set-interface b/lib/fuzz-cargo/corpus/fuzz_urb_uac/seed-uac-set-interface similarity index 100% rename from lib/fuzz/corpus/fuzz_urb_uac/seed-uac-set-interface rename to lib/fuzz-cargo/corpus/fuzz_urb_uac/seed-uac-set-interface diff --git a/lib/fuzz-cargo/fuzz_targets/fuzz_handle_client.rs b/lib/fuzz-cargo/fuzz_targets/fuzz_handle_client.rs new file mode 100644 index 0000000..ddcc356 --- /dev/null +++ b/lib/fuzz-cargo/fuzz_targets/fuzz_handle_client.rs @@ -0,0 +1,7 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_handle_client(data); +}); diff --git a/lib/fuzz-cargo/fuzz_targets/fuzz_parse_command.rs b/lib/fuzz-cargo/fuzz_targets/fuzz_parse_command.rs new file mode 100644 index 0000000..598664e --- /dev/null +++ b/lib/fuzz-cargo/fuzz_targets/fuzz_parse_command.rs @@ -0,0 +1,7 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_parse_command(data); +}); diff --git a/lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs b/lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs new file mode 100644 index 0000000..888edd5 --- /dev/null +++ b/lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs @@ -0,0 +1,7 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_urb_cdc(data); +}); diff --git a/lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs b/lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs new file mode 100644 index 0000000..76b0662 --- /dev/null +++ b/lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs @@ -0,0 +1,7 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_urb_hid(data); +}); diff --git a/lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs b/lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs new file mode 100644 index 0000000..db86580 --- /dev/null +++ b/lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs @@ -0,0 +1,7 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + usbip_rs::fuzz_harness::run_fuzz_urb_uac(data); +}); diff --git a/lib/fuzz/gen_corpus.rs b/lib/fuzz-cargo/gen_corpus.rs similarity index 100% rename from lib/fuzz/gen_corpus.rs rename to lib/fuzz-cargo/gen_corpus.rs diff --git a/lib/fuzz/fuzz_targets/fuzz_handle_client.rs b/lib/fuzz/fuzz_targets/fuzz_handle_client.rs deleted file mode 100644 index a677b5c..0000000 --- a/lib/fuzz/fuzz_targets/fuzz_handle_client.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![no_main] - -use libfuzzer_sys::fuzz_target; -use std::sync::Arc; -use usbip_rs::mock::MockSocket; -use usbip_rs::{ - ClassCode, UsbDevice, UsbEndpoint, UsbInterfaceHandler, UsbIpServer, - hid::UsbHidKeyboardHandler, -}; - -fuzz_target!(|data: &[u8]| { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - rt.block_on(async { - let handler = Arc::new(UsbHidKeyboardHandler::new_keyboard()); - let device = UsbDevice::new(0) - .unwrap() - .with_interface( - ClassCode::HID as u8, - 0x00, - 0x00, - Some("Fuzz HID Keyboard"), - vec![UsbEndpoint { - address: 0x81, - attributes: 0x03, - max_packet_size: 0x08, - interval: 10, - ..Default::default() - }], - handler as Arc, - ) - .unwrap(); - - let server = UsbIpServer::new_simulated(vec![device]); - let mock = MockSocket::new(data.to_vec()); - let output = mock.output_handle(); - let _ = usbip_rs::handler(mock, Arc::new(server)).await; - let output_bytes = output.lock().unwrap(); - usbip_rs::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); - }); -}); diff --git a/lib/fuzz/fuzz_targets/fuzz_parse_command.rs b/lib/fuzz/fuzz_targets/fuzz_parse_command.rs deleted file mode 100644 index 7c47fec..0000000 --- a/lib/fuzz/fuzz_targets/fuzz_parse_command.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![no_main] - -use libfuzzer_sys::fuzz_target; -use usbip_rs::mock::MockSocket; -use usbip_rs::usbip_protocol::UsbIpCommand; - -fuzz_target!(|data: &[u8]| { - let rt = tokio::runtime::Builder::new_current_thread() - .build() - .unwrap(); - let mut socket = MockSocket::new(data.to_vec()); - let _ = rt.block_on(UsbIpCommand::read_from_socket(&mut socket)); -}); diff --git a/lib/fuzz/fuzz_targets/fuzz_urb_cdc.rs b/lib/fuzz/fuzz_targets/fuzz_urb_cdc.rs deleted file mode 100644 index 27485ca..0000000 --- a/lib/fuzz/fuzz_targets/fuzz_urb_cdc.rs +++ /dev/null @@ -1,36 +0,0 @@ -#![no_main] - -use libfuzzer_sys::fuzz_target; -use std::sync::Arc; -use usbip_rs::mock::MockSocket; -use usbip_rs::{ - ClassCode, UsbDevice, UsbInterfaceHandler, - cdc::{UsbCdcAcmHandler, CDC_ACM_SUBCLASS}, -}; - -fuzz_target!(|data: &[u8]| { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - rt.block_on(async { - let handler = Arc::new(UsbCdcAcmHandler::new()); - let device = UsbDevice::new(0) - .unwrap() - .with_interface( - ClassCode::CDC as u8, - CDC_ACM_SUBCLASS, - 0x00, - Some("Fuzz CDC ACM"), - UsbCdcAcmHandler::endpoints(), - handler as Arc, - ) - .unwrap(); - - let mock = MockSocket::new(data.to_vec()); - let output = mock.output_handle(); - let _ = usbip_rs::handle_urb_loop(mock, Arc::new(device)).await; - let output_bytes = output.lock().unwrap(); - usbip_rs::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); - }); -}); diff --git a/lib/fuzz/fuzz_targets/fuzz_urb_hid.rs b/lib/fuzz/fuzz_targets/fuzz_urb_hid.rs deleted file mode 100644 index c475163..0000000 --- a/lib/fuzz/fuzz_targets/fuzz_urb_hid.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![no_main] - -use libfuzzer_sys::fuzz_target; -use std::sync::Arc; -use usbip_rs::mock::MockSocket; -use usbip_rs::{ - ClassCode, UsbDevice, UsbEndpoint, UsbInterfaceHandler, - hid::UsbHidKeyboardHandler, -}; - -fuzz_target!(|data: &[u8]| { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - rt.block_on(async { - let handler = Arc::new(UsbHidKeyboardHandler::new_keyboard()); - let device = UsbDevice::new(0) - .unwrap() - .with_interface( - ClassCode::HID as u8, - 0x00, - 0x00, - Some("Fuzz HID Keyboard"), - vec![UsbEndpoint { - address: 0x81, - attributes: 0x03, - max_packet_size: 0x08, - interval: 10, - ..Default::default() - }], - handler as Arc, - ) - .unwrap(); - - let mock = MockSocket::new(data.to_vec()); - let output = mock.output_handle(); - let _ = usbip_rs::handle_urb_loop(mock, Arc::new(device)).await; - let output_bytes = output.lock().unwrap(); - usbip_rs::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); - }); -}); diff --git a/lib/fuzz/fuzz_targets/fuzz_urb_uac.rs b/lib/fuzz/fuzz_targets/fuzz_urb_uac.rs deleted file mode 100644 index b64dbb3..0000000 --- a/lib/fuzz/fuzz_targets/fuzz_urb_uac.rs +++ /dev/null @@ -1,21 +0,0 @@ -#![no_main] - -use libfuzzer_sys::fuzz_target; -use std::sync::Arc; -use usbip_rs::mock::MockSocket; - -fuzz_target!(|data: &[u8]| { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - rt.block_on(async { - let device = usbip_rs::uac::build_uac_loopback_device().unwrap(); - - let mock = MockSocket::new(data.to_vec()); - let output = mock.output_handle(); - let _ = usbip_rs::handle_urb_loop(mock, Arc::new(device)).await; - let output_bytes = output.lock().unwrap(); - usbip_rs::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); - }); -}); diff --git a/lib/src/fuzz_harness.rs b/lib/src/fuzz_harness.rs new file mode 100644 index 0000000..13c8e64 --- /dev/null +++ b/lib/src/fuzz_harness.rs @@ -0,0 +1,125 @@ +//! Shared fuzz harness functions. +//! +//! Each function sets up a device, feeds `data` through a MockSocket, +//! runs the target, and validates the output. Called by both libFuzzer +//! and AFL++ wrappers. + +use std::sync::Arc; + +use crate::mock::MockSocket; +use crate::{ + ClassCode, UsbDevice, UsbEndpoint, UsbInterfaceHandler, UsbIpServer, + cdc::{UsbCdcAcmHandler, CDC_ACM_SUBCLASS}, + hid::UsbHidKeyboardHandler, + usbip_protocol::UsbIpCommand, +}; + +fn run_with_tokio(fut: impl std::future::Future) { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(fut); +} + +pub fn run_fuzz_parse_command(data: &[u8]) { + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + let mut socket = MockSocket::new(data.to_vec()); + let _ = rt.block_on(UsbIpCommand::read_from_socket(&mut socket)); +} + +pub fn run_fuzz_handle_client(data: &[u8]) { + run_with_tokio(async { + let handler = Arc::new(UsbHidKeyboardHandler::new_keyboard()); + let device = UsbDevice::new(0) + .unwrap() + .with_interface( + ClassCode::HID as u8, + 0x00, + 0x00, + Some("Fuzz HID Keyboard"), + vec![UsbEndpoint { + address: 0x81, + attributes: 0x03, + max_packet_size: 0x08, + interval: 10, + ..Default::default() + }], + handler as Arc, + ) + .unwrap(); + + let server = UsbIpServer::new_simulated(vec![device]); + let mock = MockSocket::new(data.to_vec()); + let output = mock.output_handle(); + let _ = crate::handler(mock, Arc::new(server)).await; + let output_bytes = output.lock().unwrap(); + crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); + }); +} + +pub fn run_fuzz_urb_hid(data: &[u8]) { + run_with_tokio(async { + let handler = Arc::new(UsbHidKeyboardHandler::new_keyboard()); + let device = UsbDevice::new(0) + .unwrap() + .with_interface( + ClassCode::HID as u8, + 0x00, + 0x00, + Some("Fuzz HID Keyboard"), + vec![UsbEndpoint { + address: 0x81, + attributes: 0x03, + max_packet_size: 0x08, + interval: 10, + ..Default::default() + }], + handler as Arc, + ) + .unwrap(); + + let mock = MockSocket::new(data.to_vec()); + let output = mock.output_handle(); + let _ = crate::handle_urb_loop(mock, Arc::new(device)).await; + let output_bytes = output.lock().unwrap(); + crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); + }); +} + +pub fn run_fuzz_urb_cdc(data: &[u8]) { + run_with_tokio(async { + let handler = Arc::new(UsbCdcAcmHandler::new()); + let device = UsbDevice::new(0) + .unwrap() + .with_interface( + ClassCode::CDC as u8, + CDC_ACM_SUBCLASS, + 0x00, + Some("Fuzz CDC ACM"), + UsbCdcAcmHandler::endpoints(), + handler as Arc, + ) + .unwrap(); + + let mock = MockSocket::new(data.to_vec()); + let output = mock.output_handle(); + let _ = crate::handle_urb_loop(mock, Arc::new(device)).await; + let output_bytes = output.lock().unwrap(); + crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); + }); +} + +pub fn run_fuzz_urb_uac(data: &[u8]) { + run_with_tokio(async { + let device = crate::uac::build_uac_loopback_device().unwrap(); + + let mock = MockSocket::new(data.to_vec()); + let output = mock.output_handle(); + let _ = crate::handle_urb_loop(mock, Arc::new(device)).await; + let output_bytes = output.lock().unwrap(); + crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes); + }); +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index e9b011b..dc8b12d 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -31,6 +31,8 @@ pub mod usbip_protocol; mod util; #[cfg(feature = "fuzz")] pub mod fuzz_helpers; +#[cfg(feature = "fuzz")] +pub mod fuzz_harness; pub use consts::*; pub use device::*; pub use endpoint::*; diff --git a/nix/cargo-afl-env-paths.patch b/nix/cargo-afl-env-paths.patch new file mode 100644 index 0000000..597cfb5 --- /dev/null +++ b/nix/cargo-afl-env-paths.patch @@ -0,0 +1,22 @@ +--- a/cargo-afl-common/src/lib.rs ++++ b/cargo-afl-common/src/lib.rs +@@ -64,11 +64,17 @@ fn pkg_version() -> String { + } + + pub fn afl_dir() -> Result { +- data_dir("afl") ++ if let Ok(val) = env::var("CARGO_AFL_DIR") { ++ return Ok(PathBuf::from(val)); ++ } ++ data_dir("afl") + } + + pub fn afl_llvm_dir() -> Result { +- data_dir("afl-llvm") ++ if let Ok(val) = env::var("CARGO_AFL_LLVM_DIR") { ++ return Ok(PathBuf::from(val)); ++ } ++ data_dir("afl-llvm") + } + + pub fn object_file_path() -> Result {