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) <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-03-27 00:33:34 +00:00
parent 81edffd2f8
commit bb3c603172
52 changed files with 2128 additions and 199 deletions

View file

@ -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/<target>/`. Response validation is in `lib/src/fuzz_helpers.rs`.
Crash artifacts: `lib/fuzz-cargo/artifacts/<target>/`.
### 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_<target>.slice` for manual stop.
Crashes: `lib/fuzz-afl/output/<target>/*/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

View file

@ -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/<target>/`.
### 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/<target>/`.
### 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/<target>/*/crashes/`. SymCC companion log: `lib/fuzz-afl/output/<target>/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

View file

@ -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<Output = ()>) {
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<dyn UsbInterfaceHandler>,
)
.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<dyn UsbInterfaceHandler>,
)
.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<dyn UsbInterfaceHandler>,
)
.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 <target>"
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/<target>/`.
### 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/<target>/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/<target>/` to `lib/fuzz-cargo/artifacts/<target>/`.
- [ ] **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/`.

View file

@ -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/<target>/`
5. Output dir: `lib/fuzz-afl/output/<target>/` (AFL++ standard layout: `crashes/`, `queue/`, etc.)
### `fuzz-clean-afl`
```
nix run .#fuzz-clean-afl -- fuzz_urb_hid
```
Replays files in `output/<target>/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/<target>/`
- AFL++: `lib/fuzz-afl/corpus/<target>/`
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

View file

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

298
flake.nix
View file

@ -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 <target>"
echo "Usage: fuzz-clean-cargo <target>"
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 <target>"
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";

View file

@ -24,4 +24,4 @@ env_logger = "0.11.7"
[features]
default = []
serde = ["dep:serde"]
fuzz = ["dep:arbitrary"]
fuzz = ["dep:arbitrary", "tokio/rt-multi-thread"]

3
lib/fuzz-afl/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
target/
corpus/
output/

383
lib/fuzz-afl/Cargo.lock generated Normal file
View file

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

33
lib/fuzz-afl/Cargo.toml Normal file
View file

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

View file

@ -0,0 +1,5 @@
fn main() {
afl::fuzz!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_handle_client(data);
});
}

View file

@ -0,0 +1,5 @@
fn main() {
afl::fuzz!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_parse_command(data);
});
}

View file

@ -0,0 +1,5 @@
fn main() {
afl::fuzz!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_urb_cdc(data);
});
}

View file

@ -0,0 +1,5 @@
fn main() {
afl::fuzz!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_urb_hid(data);
});
}

View file

@ -0,0 +1,5 @@
fn main() {
afl::fuzz!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_urb_uac(data);
});
}

View file

@ -0,0 +1,7 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_handle_client(data);
});

View file

@ -0,0 +1,7 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_parse_command(data);
});

View file

@ -0,0 +1,7 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_urb_cdc(data);
});

View file

@ -0,0 +1,7 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_urb_hid(data);
});

View file

@ -0,0 +1,7 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_urb_uac(data);
});

View file

@ -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<dyn UsbInterfaceHandler>,
)
.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);
});
});

View file

@ -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));
});

View file

@ -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<dyn UsbInterfaceHandler>,
)
.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);
});
});

View file

@ -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<dyn UsbInterfaceHandler>,
)
.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);
});
});

View file

@ -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);
});
});

125
lib/src/fuzz_harness.rs Normal file
View file

@ -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<Output = ()>) {
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<dyn UsbInterfaceHandler>,
)
.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<dyn UsbInterfaceHandler>,
)
.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<dyn UsbInterfaceHandler>,
)
.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);
});
}

View file

@ -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::*;

View file

@ -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<PathBuf> {
- 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<PathBuf> {
- 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<PathBuf> {