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
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue