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:
parent
81edffd2f8
commit
bb3c603172
52 changed files with 2128 additions and 199 deletions
37
CLAUDE.md
37
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/<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
|
||||
|
||||
|
|
|
|||
62
README.md
62
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/<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
|
||||
|
|
|
|||
836
docs/superpowers/plans/2026-03-26-afl-symcc-fuzzing.md
Normal file
836
docs/superpowers/plans/2026-03-26-afl-symcc-fuzzing.md
Normal 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/`.
|
||||
193
docs/superpowers/specs/2026-03-26-afl-symcc-fuzzing-design.md
Normal file
193
docs/superpowers/specs/2026-03-26-afl-symcc-fuzzing-design.md
Normal 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
|
||||
116
docs/superpowers/specs/2026-03-26-cargo-afl-migration-design.md
Normal file
116
docs/superpowers/specs/2026-03-26-cargo-afl-migration-design.md
Normal 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
298
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 <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";
|
||||
|
|
|
|||
|
|
@ -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
3
lib/fuzz-afl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
target/
|
||||
corpus/
|
||||
output/
|
||||
383
lib/fuzz-afl/Cargo.lock
generated
Normal file
383
lib/fuzz-afl/Cargo.lock
generated
Normal 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
33
lib/fuzz-afl/Cargo.toml
Normal 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"
|
||||
5
lib/fuzz-afl/afl_targets/fuzz_handle_client.rs
Normal file
5
lib/fuzz-afl/afl_targets/fuzz_handle_client.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
afl::fuzz!(|data: &[u8]| {
|
||||
usbip_rs::fuzz_harness::run_fuzz_handle_client(data);
|
||||
});
|
||||
}
|
||||
5
lib/fuzz-afl/afl_targets/fuzz_parse_command.rs
Normal file
5
lib/fuzz-afl/afl_targets/fuzz_parse_command.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
afl::fuzz!(|data: &[u8]| {
|
||||
usbip_rs::fuzz_harness::run_fuzz_parse_command(data);
|
||||
});
|
||||
}
|
||||
5
lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs
Normal file
5
lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
afl::fuzz!(|data: &[u8]| {
|
||||
usbip_rs::fuzz_harness::run_fuzz_urb_cdc(data);
|
||||
});
|
||||
}
|
||||
5
lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs
Normal file
5
lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
afl::fuzz!(|data: &[u8]| {
|
||||
usbip_rs::fuzz_harness::run_fuzz_urb_hid(data);
|
||||
});
|
||||
}
|
||||
5
lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs
Normal file
5
lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
afl::fuzz!(|data: &[u8]| {
|
||||
usbip_rs::fuzz_harness::run_fuzz_urb_uac(data);
|
||||
});
|
||||
}
|
||||
7
lib/fuzz-cargo/fuzz_targets/fuzz_handle_client.rs
Normal file
7
lib/fuzz-cargo/fuzz_targets/fuzz_handle_client.rs
Normal 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);
|
||||
});
|
||||
7
lib/fuzz-cargo/fuzz_targets/fuzz_parse_command.rs
Normal file
7
lib/fuzz-cargo/fuzz_targets/fuzz_parse_command.rs
Normal 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);
|
||||
});
|
||||
7
lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs
Normal file
7
lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs
Normal 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);
|
||||
});
|
||||
7
lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs
Normal file
7
lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs
Normal 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);
|
||||
});
|
||||
7
lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs
Normal file
7
lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs
Normal 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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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
125
lib/src/fuzz_harness.rs
Normal 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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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::*;
|
||||
|
|
|
|||
22
nix/cargo-afl-env-paths.patch
Normal file
22
nix/cargo-afl-env-paths.patch
Normal 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> {
|
||||
Loading…
Add table
Add a link
Reference in a new issue