feat(fuzz): add AFL++ with cargo-afl fuzzing support

Add a second fuzzing engine alongside the existing libFuzzer/cargo-fuzz
setup. AFL++ runs with persistent mode (afl::fuzz! macro), LLVM plugins
(CmpLog, IJON), and a SymCC concolic companion for hybrid fuzzing.

- cargo-afl built from afl.rs with a patch for CARGO_AFL_DIR /
  CARGO_AFL_LLVM_DIR env-var overrides
- AFL++ built with LLVM 22 plugins to match rust-nightly
- Persistent-mode fuzz targets in lib/fuzz-afl/
- --jobs N parallel fuzzing: main instance in foreground, secondaries
  and SymCC companion as systemd transient units in a slice
- Ctrl+c / exit cleans up all background processes via slice stop
- AFL_AUTORESUME=1 for clean restarts after previous runs
- fuzz-clean-afl collects crashes from all instance directories
- Shared harness logic in lib/src/fuzz_harness.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-03-27 00:33:34 +00:00
parent 81edffd2f8
commit bb3c603172
52 changed files with 2128 additions and 199 deletions

View file

@ -0,0 +1,836 @@
# AFL++ and SymCC Fuzzing Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add AFL++ with SymCC concolic execution to complement existing libFuzzer fuzzing, targeting deeper coverage on protocol parsing and connection lifecycle.
**Architecture:** Extract shared harness logic from existing fuzz targets into `lib/src/fuzz_harness.rs`. Rename `lib/fuzz/` to `lib/fuzz-cargo/`, add `lib/fuzz-afl/` with stdin-based wrappers. New Nix package outputs for latest AFL++ and SymCC, with `fuzz-afl` and `fuzz-clean-afl` apps that build both AFL++ and SymCC-instrumented binaries and run AFL++ with SymCC as companion mutator.
**Tech Stack:** Rust, AFL++ (afl-cc), SymCC (symcc), Nix flakes, tokio, cargo-fuzz
**Spec:** `docs/superpowers/specs/2026-03-26-afl-symcc-fuzzing-design.md`
---
## File Map
| Action | File | Responsibility |
|--------|------|----------------|
| Create | `lib/src/fuzz_harness.rs` | Shared harness functions for all 5 fuzz targets |
| Modify | `lib/src/lib.rs:32-33` | Add `pub mod fuzz_harness` behind `cfg(fuzz)` |
| Rename | `lib/fuzz/``lib/fuzz-cargo/` | libFuzzer directory rename |
| Modify | `lib/fuzz-cargo/fuzz_targets/*.rs` (all 5) | Thin wrappers calling `fuzz_harness` |
| Create | `lib/fuzz-afl/Cargo.toml` | AFL++ crate manifest |
| Create | `lib/fuzz-afl/.gitignore` | Ignore target/, corpus/, output/ |
| Create | `lib/fuzz-afl/afl_targets/*.rs` (5 files) | Stdin-based wrappers calling `fuzz_harness` |
| Modify | `flake.nix` | New packages, apps, devShell; rename existing apps |
| Modify | `CLAUDE.md` | Update fuzzing docs |
| Modify | `lib/fuzz-cargo/gen_corpus.rs` | No changes needed (paths are relative to cwd) |
---
### Task 1: Extract shared harness into `fuzz_harness.rs`
**Files:**
- Create: `lib/src/fuzz_harness.rs`
- Modify: `lib/src/lib.rs:32-33`
- [ ] **Step 1: Create `lib/src/fuzz_harness.rs`**
```rust
//! Shared fuzz harness functions.
//!
//! Each function sets up a device, feeds `data` through a MockSocket,
//! runs the target, and validates the output. Called by both libFuzzer
//! and AFL++ wrappers.
use std::sync::Arc;
use crate::mock::MockSocket;
use crate::{
ClassCode, UsbDevice, UsbEndpoint, UsbInterfaceHandler, UsbIpServer,
cdc::{UsbCdcAcmHandler, CDC_ACM_SUBCLASS},
hid::UsbHidKeyboardHandler,
usbip_protocol::UsbIpCommand,
};
fn run_with_tokio(fut: impl std::future::Future<Output = ()>) {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(fut);
}
pub fn run_fuzz_parse_command(data: &[u8]) {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let mut socket = MockSocket::new(data.to_vec());
let _ = rt.block_on(UsbIpCommand::read_from_socket(&mut socket));
}
pub fn run_fuzz_handle_client(data: &[u8]) {
run_with_tokio(async {
let handler = Arc::new(UsbHidKeyboardHandler::new_keyboard());
let device = UsbDevice::new(0)
.unwrap()
.with_interface(
ClassCode::HID as u8,
0x00,
0x00,
Some("Fuzz HID Keyboard"),
vec![UsbEndpoint {
address: 0x81,
attributes: 0x03,
max_packet_size: 0x08,
interval: 10,
..Default::default()
}],
handler as Arc<dyn UsbInterfaceHandler>,
)
.unwrap();
let server = UsbIpServer::new_simulated(vec![device]);
let mock = MockSocket::new(data.to_vec());
let output = mock.output_handle();
let _ = crate::handler(mock, Arc::new(server)).await;
let output_bytes = output.lock().unwrap();
crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes);
});
}
pub fn run_fuzz_urb_hid(data: &[u8]) {
run_with_tokio(async {
let handler = Arc::new(UsbHidKeyboardHandler::new_keyboard());
let device = UsbDevice::new(0)
.unwrap()
.with_interface(
ClassCode::HID as u8,
0x00,
0x00,
Some("Fuzz HID Keyboard"),
vec![UsbEndpoint {
address: 0x81,
attributes: 0x03,
max_packet_size: 0x08,
interval: 10,
..Default::default()
}],
handler as Arc<dyn UsbInterfaceHandler>,
)
.unwrap();
let mock = MockSocket::new(data.to_vec());
let output = mock.output_handle();
let _ = crate::handle_urb_loop(mock, Arc::new(device)).await;
let output_bytes = output.lock().unwrap();
crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes);
});
}
pub fn run_fuzz_urb_cdc(data: &[u8]) {
run_with_tokio(async {
let handler = Arc::new(UsbCdcAcmHandler::new());
let device = UsbDevice::new(0)
.unwrap()
.with_interface(
ClassCode::CDC as u8,
CDC_ACM_SUBCLASS,
0x00,
Some("Fuzz CDC ACM"),
UsbCdcAcmHandler::endpoints(),
handler as Arc<dyn UsbInterfaceHandler>,
)
.unwrap();
let mock = MockSocket::new(data.to_vec());
let output = mock.output_handle();
let _ = crate::handle_urb_loop(mock, Arc::new(device)).await;
let output_bytes = output.lock().unwrap();
crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes);
});
}
pub fn run_fuzz_urb_uac(data: &[u8]) {
run_with_tokio(async {
let device = crate::uac::build_uac_loopback_device().unwrap();
let mock = MockSocket::new(data.to_vec());
let output = mock.output_handle();
let _ = crate::handle_urb_loop(mock, Arc::new(device)).await;
let output_bytes = output.lock().unwrap();
crate::fuzz_helpers::assert_usbip_responses_valid(&output_bytes);
});
}
```
- [ ] **Step 2: Register the module in `lib/src/lib.rs`**
After the existing `pub mod fuzz_helpers;` line (line 33), add the new module:
```rust
#[cfg(feature = "fuzz")]
pub mod fuzz_helpers;
#[cfg(feature = "fuzz")]
pub mod fuzz_harness;
```
- [ ] **Step 3: Verify it compiles**
Run: `nix develop -c cargo build -p usbip-rs --features fuzz`
Expected: Compiles without errors.
- [ ] **Step 4: Commit**
```bash
git add lib/src/fuzz_harness.rs lib/src/lib.rs
git commit -m "feat(fuzz): extract shared harness into fuzz_harness.rs"
```
---
### Task 2: Rename `lib/fuzz/` to `lib/fuzz-cargo/` and update wrappers
**Files:**
- Rename: `lib/fuzz/``lib/fuzz-cargo/`
- Modify: `lib/fuzz-cargo/fuzz_targets/fuzz_parse_command.rs`
- Modify: `lib/fuzz-cargo/fuzz_targets/fuzz_handle_client.rs`
- Modify: `lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs`
- Modify: `lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs`
- Modify: `lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs`
- [ ] **Step 1: Rename the directory**
```bash
git mv lib/fuzz lib/fuzz-cargo
```
- [ ] **Step 2: Replace `fuzz_parse_command.rs` with thin wrapper**
Replace the entire contents of `lib/fuzz-cargo/fuzz_targets/fuzz_parse_command.rs`:
```rust
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_parse_command(data);
});
```
- [ ] **Step 3: Replace `fuzz_handle_client.rs` with thin wrapper**
Replace the entire contents of `lib/fuzz-cargo/fuzz_targets/fuzz_handle_client.rs`:
```rust
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_handle_client(data);
});
```
- [ ] **Step 4: Replace `fuzz_urb_hid.rs` with thin wrapper**
Replace the entire contents of `lib/fuzz-cargo/fuzz_targets/fuzz_urb_hid.rs`:
```rust
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_urb_hid(data);
});
```
- [ ] **Step 5: Replace `fuzz_urb_uac.rs` with thin wrapper**
Replace the entire contents of `lib/fuzz-cargo/fuzz_targets/fuzz_urb_uac.rs`:
```rust
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_urb_uac(data);
});
```
- [ ] **Step 6: Replace `fuzz_urb_cdc.rs` with thin wrapper**
Replace the entire contents of `lib/fuzz-cargo/fuzz_targets/fuzz_urb_cdc.rs`:
```rust
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_urb_cdc(data);
});
```
- [ ] **Step 7: Verify cargo-fuzz still works**
Run: `nix develop .#fuzz -c bash -c "cd lib/fuzz-cargo && cargo fuzz list"`
Expected: Lists all 5 targets.
Run: `nix develop .#fuzz -c bash -c "cd lib/fuzz-cargo && cargo fuzz build"`
Expected: All targets build successfully.
- [ ] **Step 8: Commit**
```bash
git add -A lib/fuzz-cargo lib/fuzz
git commit -m "refactor(fuzz): rename lib/fuzz to lib/fuzz-cargo, use shared harness"
```
---
### Task 3: Create `lib/fuzz-afl/` with AFL++ targets
**Files:**
- Create: `lib/fuzz-afl/Cargo.toml`
- Create: `lib/fuzz-afl/.gitignore`
- Create: `lib/fuzz-afl/afl_targets/fuzz_parse_command.rs`
- Create: `lib/fuzz-afl/afl_targets/fuzz_handle_client.rs`
- Create: `lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs`
- Create: `lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs`
- Create: `lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs`
- [ ] **Step 1: Create `lib/fuzz-afl/Cargo.toml`**
```toml
[package]
name = "usbip-rs-fuzz-afl"
version = "0.0.0"
publish = false
edition = "2024"
[dependencies]
usbip-rs = { path = "..", features = ["fuzz"] }
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time", "io-util"] }
[workspace]
members = ["."]
[[bin]]
name = "fuzz_parse_command"
path = "afl_targets/fuzz_parse_command.rs"
[[bin]]
name = "fuzz_handle_client"
path = "afl_targets/fuzz_handle_client.rs"
[[bin]]
name = "fuzz_urb_hid"
path = "afl_targets/fuzz_urb_hid.rs"
[[bin]]
name = "fuzz_urb_uac"
path = "afl_targets/fuzz_urb_uac.rs"
[[bin]]
name = "fuzz_urb_cdc"
path = "afl_targets/fuzz_urb_cdc.rs"
```
- [ ] **Step 2: Create `lib/fuzz-afl/.gitignore`**
```
target/
corpus/
output/
```
- [ ] **Step 3: Create `lib/fuzz-afl/afl_targets/fuzz_parse_command.rs`**
```rust
use std::io::Read;
fn main() {
let mut data = Vec::new();
std::io::stdin().read_to_end(&mut data).unwrap();
usbip_rs::fuzz_harness::run_fuzz_parse_command(&data);
}
```
- [ ] **Step 4: Create `lib/fuzz-afl/afl_targets/fuzz_handle_client.rs`**
```rust
use std::io::Read;
fn main() {
let mut data = Vec::new();
std::io::stdin().read_to_end(&mut data).unwrap();
usbip_rs::fuzz_harness::run_fuzz_handle_client(&data);
}
```
- [ ] **Step 5: Create `lib/fuzz-afl/afl_targets/fuzz_urb_hid.rs`**
```rust
use std::io::Read;
fn main() {
let mut data = Vec::new();
std::io::stdin().read_to_end(&mut data).unwrap();
usbip_rs::fuzz_harness::run_fuzz_urb_hid(&data);
}
```
- [ ] **Step 6: Create `lib/fuzz-afl/afl_targets/fuzz_urb_uac.rs`**
```rust
use std::io::Read;
fn main() {
let mut data = Vec::new();
std::io::stdin().read_to_end(&mut data).unwrap();
usbip_rs::fuzz_harness::run_fuzz_urb_uac(&data);
}
```
- [ ] **Step 7: Create `lib/fuzz-afl/afl_targets/fuzz_urb_cdc.rs`**
```rust
use std::io::Read;
fn main() {
let mut data = Vec::new();
std::io::stdin().read_to_end(&mut data).unwrap();
usbip_rs::fuzz_harness::run_fuzz_urb_cdc(&data);
}
```
- [ ] **Step 8: Verify the AFL++ targets compile with regular cargo**
Run: `nix develop -c cargo build --manifest-path lib/fuzz-afl/Cargo.toml`
Expected: All 5 binaries build successfully (without AFL++ instrumentation, just verifying the code compiles).
- [ ] **Step 9: Commit**
```bash
git add lib/fuzz-afl
git commit -m "feat(fuzz): add AFL++ fuzz targets in lib/fuzz-afl"
```
---
### Task 4: Add AFL++ and SymCC Nix packages
**Files:**
- Modify: `flake.nix` (packages section)
- [ ] **Step 1: Add AFL++ package override**
In the `packages` set in `flake.nix`, after the `inherit usbip-rs;` line, add:
```nix
aflplusplus = pkgs.aflplusplus.overrideAttrs (old: {
version = "4.35c-unstable-2026-03-26";
src = pkgs.fetchFromGitHub {
owner = "AFLplusplus";
repo = "AFLplusplus";
rev = "e5a8ba39ecf97d05e286fdd4e01da96554dbf64f";
hash = lib.fakeHash;
};
});
```
- [ ] **Step 2: Build AFL++ to get the correct hash**
Run: `nix build .#aflplusplus 2>&1 | grep 'got:'`
Take the hash from the error output and replace `lib.fakeHash` with the actual hash string.
- [ ] **Step 3: Verify AFL++ builds**
Run: `nix build .#aflplusplus`
Expected: Builds successfully. If the newer commit breaks patches in the nixpkgs derivation, fix the override by adding `postPatch`, `patches`, or other phase overrides as needed.
- [ ] **Step 4: Add SymCC package override**
In the same `packages` set, add:
```nix
symcc = pkgs.symcc.overrideAttrs (old: {
version = "1.0-unstable-2026-03-26";
src = pkgs.fetchFromGitHub {
owner = "eurecom-s3";
repo = "symcc";
rev = "3b8acabf06c83b92facccde7f6dfb191b1a163b3";
hash = lib.fakeHash;
fetchSubmodules = true;
};
});
```
- [ ] **Step 5: Build SymCC to get the correct hash**
Run: `nix build .#symcc 2>&1 | grep 'got:'`
Take the hash from the error output and replace `lib.fakeHash` with the actual hash string.
- [ ] **Step 6: Verify SymCC builds**
Run: `nix build .#symcc`
Expected: Builds successfully. If the newer commit breaks the cmake build, fix the override.
- [ ] **Step 7: Verify both binaries work**
Run: `nix build .#aflplusplus && ls $(nix build .#aflplusplus --print-out-paths --no-link)/bin/afl-fuzz`
Expected: `afl-fuzz` binary exists.
Run: `nix build .#symcc && ls $(nix build .#symcc --print-out-paths --no-link)/bin/symcc`
Expected: `symcc` binary exists.
- [ ] **Step 8: Commit**
```bash
git add flake.nix
git commit -m "feat(nix): add aflplusplus and symcc package outputs"
```
---
### Task 5: Rename existing Nix apps and update paths
**Files:**
- Modify: `flake.nix` (apps section, fuzz-env, devShells)
- [ ] **Step 1: Update `fuzz-env` path**
In the `fuzz-env` string in `flake.nix`, change:
```nix
cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/lib"
```
to:
```nix
cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/lib/fuzz-cargo"
```
(The `cargo fuzz` commands expect to be run from within the fuzz crate directory, which is now `lib/fuzz-cargo/` instead of `lib/`.)
- [ ] **Step 2: Rename `fuzz-usbip` to `fuzz-cargo`**
In the `apps` section, rename the `fuzz-usbip` variable and app to `fuzz-cargo`:
Change the script variable name from `fuzz-usbip` to `fuzz-cargo` and the binary name from `"fuzz-usbip"` to `"fuzz-cargo"`. Update the app entry from `fuzz-usbip` to `fuzz-cargo`.
- [ ] **Step 3: Rename `fuzz-clean-usbip` to `fuzz-clean-cargo`**
Same pattern: rename the script variable, binary name, and app entry from `fuzz-clean-usbip` to `fuzz-clean-cargo`.
- [ ] **Step 4: Update `gen-fuzz-corpus` path**
In the `gen-fuzz-corpus` script, change:
```nix
${pkgs.rustc}/bin/rustc "$root/lib/fuzz/gen_corpus.rs" -o /tmp/gen-fuzz-corpus
cd "$root/lib/fuzz"
```
to:
```nix
${pkgs.rustc}/bin/rustc "$root/lib/fuzz-cargo/gen_corpus.rs" -o /tmp/gen-fuzz-corpus
cd "$root/lib/fuzz-cargo"
```
- [ ] **Step 5: Verify renamed apps work**
Run: `nix run .#fuzz-cargo`
Expected: Lists the 5 fuzz targets.
Run: `nix run .#gen-fuzz-corpus`
Expected: Generates seed files under `lib/fuzz-cargo/corpus/`.
- [ ] **Step 6: Commit**
```bash
git add flake.nix
git commit -m "refactor(nix): rename fuzz apps to fuzz-cargo/fuzz-clean-cargo"
```
---
### Task 6: Add `fuzz-afl` and `fuzz-clean-afl` Nix apps
**Files:**
- Modify: `flake.nix` (apps section)
- [ ] **Step 1: Add `fuzz-afl-env` helper**
In the `apps` `let` block, after `fuzz-env`, add:
```nix
afl-env = ''
export PATH="${rust-nightly}/bin:${self.packages.${system}.aflplusplus}/bin:${self.packages.${system}.symcc}/bin:${pkgs.stdenv.cc}/bin:${pkgs.pkg-config}/bin:${pkgs.coreutils}/bin:$PATH"
export PKG_CONFIG_PATH="${pkgs.libusb1.dev}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux '':${pkgs.udev.dev}/lib/pkgconfig''}
root="$(${pkgs.git}/bin/git rev-parse --show-toplevel)"
'';
```
- [ ] **Step 2: Add `fuzz-afl` app script**
In the `apps` `let` block, add:
```nix
fuzz-afl = pkgs.writeShellScriptBin "fuzz-afl" ''
set -euo pipefail
${afl-env}
manifest="$root/lib/fuzz-afl/Cargo.toml"
if [ $# -eq 0 ]; then
echo "Available targets:"
${pkgs.gnugrep}/bin/grep -oP '(?<=name = ")[^"]+' "$manifest" | while read -r name; do
echo " $name"
done
exit 0
fi
target="$1"
shift
afl_target_dir="$root/lib/fuzz-afl/target/afl"
symcc_target_dir="$root/lib/fuzz-afl/target/symcc"
echo "Building $target with AFL++ instrumentation..."
CC=afl-cc CXX=afl-c++ RUSTFLAGS="-C linker=afl-cc" \
CARGO_TARGET_DIR="$afl_target_dir" \
cargo build --manifest-path "$manifest" --release --bin "$target"
echo "Building $target with SymCC instrumentation..."
CC=symcc CXX=sym++ RUSTFLAGS="-C linker=symcc" \
CARGO_TARGET_DIR="$symcc_target_dir" \
cargo build --manifest-path "$manifest" --release --bin "$target"
corpus_dir="$root/lib/fuzz-afl/corpus/$target"
output_dir="$root/lib/fuzz-afl/output/$target"
mkdir -p "$corpus_dir" "$output_dir"
# Ensure corpus has at least one seed
if [ -z "$(ls -A "$corpus_dir" 2>/dev/null)" ]; then
echo "Warning: corpus dir is empty, creating minimal seed"
printf '\x00' > "$corpus_dir/seed-minimal"
fi
echo "Starting AFL++ with SymCC companion on $target..."
afl-fuzz \
-i "$corpus_dir" \
-o "$output_dir" \
-c "$symcc_target_dir/release/$target" \
"$@" \
-- "$afl_target_dir/release/$target"
'';
```
- [ ] **Step 3: Add `fuzz-clean-afl` app script**
```nix
fuzz-clean-afl = pkgs.writeShellScriptBin "fuzz-clean-afl" ''
set -euo pipefail
${afl-env}
manifest="$root/lib/fuzz-afl/Cargo.toml"
if [ $# -eq 0 ]; then
echo "Usage: fuzz-clean-afl <target>"
echo "Available targets:"
${pkgs.gnugrep}/bin/grep -oP '(?<=name = ")[^"]+' "$manifest" | while read -r name; do
echo " $name"
done
exit 1
fi
target="$1"
afl_target_dir="$root/lib/fuzz-afl/target/afl"
dir="$root/lib/fuzz-afl/output/$target/default/crashes"
if [ ! -d "$dir" ]; then
echo "No crashes directory: $dir"
exit 0
fi
shopt -s nullglob
files=("$dir"/id:*)
if [ ''${#files[@]} -eq 0 ]; then
echo "No crash files to test."
exit 0
fi
echo "Building $target with AFL++ instrumentation..."
CC=afl-cc CXX=afl-c++ RUSTFLAGS="-C linker=afl-cc" \
CARGO_TARGET_DIR="$afl_target_dir" \
cargo build --manifest-path "$manifest" --release --bin "$target"
echo "Testing ''${#files[@]} crash files for $target..."
removed=0
kept=0
for f in "''${files[@]}"; do
if timeout 30 "$afl_target_dir/release/$target" < "$f" >/dev/null 2>&1; then
rm "$f"
removed=$((removed + 1))
else
kept=$((kept + 1))
fi
echo -ne "\r tested $((removed + kept))/''${#files[@]}, removed $removed, kept $kept"
done
echo ""
echo "Done: removed $removed fixed, kept $kept still-crashing."
'';
```
- [ ] **Step 4: Register both apps**
In the `apps` attrset at the bottom of `flake.nix`, add:
```nix
fuzz-afl = {
type = "app";
program = "${fuzz-afl}/bin/fuzz-afl";
};
fuzz-clean-afl = {
type = "app";
program = "${fuzz-clean-afl}/bin/fuzz-clean-afl";
};
```
- [ ] **Step 5: Verify `fuzz-afl` lists targets**
Run: `nix run .#fuzz-afl`
Expected: Lists all 5 targets.
- [ ] **Step 6: Commit**
```bash
git add flake.nix
git commit -m "feat(nix): add fuzz-afl and fuzz-clean-afl apps"
```
---
### Task 7: Add `fuzz-afl` devShell
**Files:**
- Modify: `flake.nix` (devShells section)
- [ ] **Step 1: Add `fuzz-afl` devShell**
In the `devShells` attrset, add after the `fuzz` entry:
```nix
fuzz-afl = pkgs.mkShell {
buildInputs = [
rust-nightly
self.packages.${system}.aflplusplus
self.packages.${system}.symcc
pkgs.libusb1
] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [
pkgs.udev
];
nativeBuildInputs = [ pkgs.stdenv.cc pkgs.pkg-config ];
};
```
- [ ] **Step 2: Verify the devShell enters**
Run: `nix develop .#fuzz-afl -c bash -c "which afl-fuzz && which symcc"`
Expected: Prints paths to both binaries.
- [ ] **Step 3: Commit**
```bash
git add flake.nix
git commit -m "feat(nix): add fuzz-afl devShell"
```
---
### Task 8: Update CLAUDE.md
**Files:**
- Modify: `CLAUDE.md`
- [ ] **Step 1: Update the Fuzzing section**
Replace the existing fuzzing section in `CLAUDE.md` with:
```markdown
## Fuzzing
Fuzz targets exercise host-side codepaths against untrusted client input.
### libFuzzer (cargo-fuzz)
Targets are in `lib/fuzz-cargo/` and use `cargo fuzz`.
```bash
nix run .#fuzz-cargo # List targets
nix run .#fuzz-cargo -- fuzz_urb_hid # Single process
nix run .#fuzz-cargo -- fuzz_urb_hid --fork=8 # Parallel (overnight)
nix run .#fuzz-clean-cargo -- fuzz_urb_hid # Prune fixed artifacts
```
Crash artifacts: `lib/fuzz-cargo/artifacts/<target>/`.
### AFL++ with SymCC
Targets are in `lib/fuzz-afl/` and use AFL++ with SymCC as a companion concolic execution engine.
```bash
nix run .#fuzz-afl # List targets
nix run .#fuzz-afl -- fuzz_urb_hid # Run AFL++ + SymCC
nix run .#fuzz-clean-afl -- fuzz_urb_hid # Prune fixed crashes
```
Crashes: `lib/fuzz-afl/output/<target>/default/crashes/`. Corpora are separate between engines; copy manually if desired.
### Shared harness
All fuzz target logic lives in `lib/src/fuzz_harness.rs`. Response validation is in `lib/src/fuzz_helpers.rs`.
```
- [ ] **Step 2: Update the "Fixing fuzzer crashes" section artifact path**
Update the crash artifacts reference from `lib/fuzz/artifacts/<target>/` to `lib/fuzz-cargo/artifacts/<target>/`.
- [ ] **Step 3: Commit**
```bash
git add CLAUDE.md
git commit -m "docs: update CLAUDE.md for renamed fuzz dirs and AFL++ setup"
```
---
### Task 9: End-to-end verification
- [ ] **Step 1: Verify libFuzzer still works end-to-end**
Run: `nix run .#fuzz-cargo -- fuzz_parse_command -- -max_total_time=10`
Expected: Runs for 10 seconds without errors, exercises the target.
- [ ] **Step 2: Verify AFL++ builds and starts**
Run: `nix run .#fuzz-afl -- fuzz_parse_command` (cancel after it starts fuzzing)
Expected: Both AFL++ and SymCC binaries build, AFL++ starts fuzzing with SymCC companion, shows the AFL++ status screen.
- [ ] **Step 3: Verify fuzz-clean-afl handles empty state**
Run: `nix run .#fuzz-clean-afl -- fuzz_parse_command`
Expected: Reports "No crashes directory" or "No crash files to test."
- [ ] **Step 4: Verify gen-fuzz-corpus still works**
Run: `nix run .#gen-fuzz-corpus`
Expected: Generates seed files under `lib/fuzz-cargo/corpus/`.

View file

@ -0,0 +1,193 @@
# AFL++ and SymCC Fuzzing Support
## Goal
Extend the fuzzing infrastructure to support AFL++ with SymCC as a concolic execution companion, complementing the existing libFuzzer setup. SymCC's constraint solving targets deeper coverage through complex branch conditions in protocol parsing, while AFL++ provides fast coverage-guided mutation. Both engines maintain separate corpora; the user copies inputs between them manually.
## Security Context
The primary fuzzing targets are `fuzz_parse_command` and `fuzz_handle_client` — these exercise the protocol parsing and connection lifecycle code that faces untrusted client input. The three device-specific targets (`fuzz_urb_hid`, `fuzz_urb_cdc`, `fuzz_urb_uac`) are included for completeness to exercise shared codepaths, but their device handlers are debug tools and not security-critical.
## File Layout
```
lib/
fuzz-cargo/ # renamed from fuzz/ (libFuzzer via cargo-fuzz)
Cargo.toml # unchanged except path
fuzz_targets/ # thin wrappers calling fuzz_harness
fuzz_parse_command.rs
fuzz_handle_client.rs
fuzz_urb_hid.rs
fuzz_urb_uac.rs
fuzz_urb_cdc.rs
gen_corpus.rs
fuzz-afl/ # new (AFL++ + SymCC)
Cargo.toml # depends on usbip-rs with "fuzz" feature, no afl crate
afl_targets/ # stdin-based wrappers calling fuzz_harness
fuzz_parse_command.rs
fuzz_handle_client.rs
fuzz_urb_hid.rs
fuzz_urb_uac.rs
fuzz_urb_cdc.rs
src/
fuzz_harness.rs # shared harness logic (behind cfg "fuzz")
...existing files...
```
## Shared Harness Layer
A new module `lib/src/fuzz_harness.rs`, gated behind `#[cfg(feature = "fuzz")]`, provides one function per target:
- `run_fuzz_parse_command(data: &[u8])`
- `run_fuzz_handle_client(data: &[u8])`
- `run_fuzz_urb_hid(data: &[u8])`
- `run_fuzz_urb_uac(data: &[u8])`
- `run_fuzz_urb_cdc(data: &[u8])`
Each function encapsulates: tokio runtime creation, device/handler setup, MockSocket wiring, execution, and response validation via `fuzz_helpers::assert_usbip_responses_valid`.
The libFuzzer wrappers (in `fuzz-cargo/fuzz_targets/`) become thin calls:
```rust
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| { usbip_rs::fuzz_harness::run_fuzz_urb_hid(data); });
```
The AFL++ wrappers (in `fuzz-afl/afl_targets/`) read from stdin:
```rust
use std::io::Read;
fn main() {
let mut data = Vec::new();
std::io::stdin().read_to_end(&mut data).unwrap();
usbip_rs::fuzz_harness::run_fuzz_urb_hid(&data);
}
```
No `afl` crate dependency — AFL++ instruments via the compiler/linker, and SymCC reuses the same stdin-based binary compiled with a different compiler.
## Nix Packages
Two new flake package outputs extending nixpkgs derivations with latest upstream commits:
### `packages.aflplusplus`
```nix
aflplusplus = pkgs.aflplusplus.overrideAttrs (old: {
version = "4.35c-unstable-2026-03-26";
src = pkgs.fetchFromGitHub {
owner = "AFLplusplus";
repo = "AFLplusplus";
rev = "e5a8ba39ecf97d05e286fdd4e01da96554dbf64f";
hash = "..."; # to be determined at build time
};
});
```
### `packages.symcc`
```nix
symcc = pkgs.symcc.overrideAttrs (old: {
version = "1.0-unstable-2026-03-26";
src = pkgs.fetchFromGitHub {
owner = "eurecom-s3";
repo = "symcc";
rev = "3b8acabf06c83b92facccde7f6dfb191b1a163b3";
hash = "..."; # to be determined at build time
fetchSubmodules = true;
};
});
```
If the newer commits break the existing nixpkgs build phases or patches, the derivation overrides will include fixes.
## Build Strategy
Both AFL++ and SymCC instrument via the compiler/linker. The same Rust source is compiled twice with different toolchains:
### AFL++ instrumented build
```bash
export CC=afl-cc
export CXX=afl-c++
export RUSTFLAGS="-C linker=afl-cc"
export CARGO_TARGET_DIR=target/afl-release
cargo build --manifest-path lib/fuzz-afl/Cargo.toml --release
```
### SymCC instrumented build
```bash
export CC=symcc
export CXX=sym++
export RUSTFLAGS="-C linker=symcc"
export CARGO_TARGET_DIR=target/symcc-release
cargo build --manifest-path lib/fuzz-afl/Cargo.toml --release
```
Separate `CARGO_TARGET_DIR` values prevent the two instrumented builds from clobbering each other.
## Nix Apps
### `fuzz-cargo` (renamed from `fuzz-usbip`)
Same behavior as before, updated paths to `lib/fuzz-cargo/`.
```
nix run .#fuzz-cargo # List targets
nix run .#fuzz-cargo -- fuzz_urb_hid # Single process
nix run .#fuzz-cargo -- fuzz_urb_hid --fork=8 # Parallel
```
### `fuzz-clean-cargo` (renamed from `fuzz-clean-usbip`)
Same behavior, updated paths to `lib/fuzz-cargo/`.
### `fuzz-afl`
Always runs AFL++ with SymCC as companion mutator (no opt-in flag).
```
nix run .#fuzz-afl # List targets
nix run .#fuzz-afl -- fuzz_urb_hid # Run AFL++ + SymCC
```
The app:
1. Builds the AFL++-instrumented binary (`afl-cc` as linker)
2. Builds the SymCC-instrumented binary (`symcc` as linker)
3. Runs `afl-fuzz -c /path/to/symcc-binary` to use SymCC as a companion mutator
4. Corpus dir: `lib/fuzz-afl/corpus/<target>/`
5. Output dir: `lib/fuzz-afl/output/<target>/` (AFL++ standard layout: `crashes/`, `queue/`, etc.)
### `fuzz-clean-afl`
```
nix run .#fuzz-clean-afl -- fuzz_urb_hid
```
Replays files in `output/<target>/crashes/` against the AFL++-instrumented binary. Removes inputs that no longer crash.
### `gen-fuzz-corpus`
Updated path to `lib/fuzz-cargo/gen_corpus.rs`. Generates seeds for libFuzzer corpus; user copies to AFL++ manually if desired.
## Nix DevShell
A new `fuzz-afl` devShell providing `aflplusplus`, `symcc`, `rust-nightly`, and the usual build dependencies (libusb1, udev, pkg-config) for manual use outside the app wrappers.
## Corpus Management
Each engine maintains its own corpus:
- libFuzzer: `lib/fuzz-cargo/corpus/<target>/`
- AFL++: `lib/fuzz-afl/corpus/<target>/`
Corpora are kept separate. The user manually copies inputs between engines when desired. No automated sync.
## Rename Impacts
Renaming `lib/fuzz/` to `lib/fuzz-cargo/` requires updates to:
- `flake.nix`: `fuzz-env` paths, app names, `gen-fuzz-corpus` path
- `CLAUDE.md`: fuzzing documentation references
- Existing corpus directories and artifact paths move with the rename
- The corpus generator (`gen_corpus.rs`) output paths

View file

@ -0,0 +1,116 @@
# cargo-afl Migration Design
**Date:** 2026-03-26
**Status:** Approved
## Goal
Switch from the current manual AFL++ instrumentation setup to cargo-afl for best-practices AFL fuzzing with persistent mode, CmpLog (via LLVM plugins), and SymCC companion support.
## Overview
The current `fuzz-afl` setup manually invokes `afl-cc` as a linker with sancov RUSTFLAGS. Fuzz targets read from stdin. There is no persistent mode, no CmpLog, and SymCC is built but only available for manual use.
The new setup uses cargo-afl (from rust-fuzz/afl.rs), which provides:
- **Persistent mode**: Shared-memory testcase delivery via `__afl_persistent_loop`, no process-per-input overhead
- **CmpLog with LLVM plugins**: Full comparison logging (instructions, switches, routines) for solving magic bytes
- **Deferred forkserver**: Fork after initialization, paying setup cost once
- **Standard tooling**: `cargo afl build`, `cargo afl fuzz`, `cargo afl cmin`, `cargo afl tmin`, etc.
## Section 1: Nix Packaging
### 1a. AFL++ with LLVM 22 plugins
Extend the existing `aflplusplus` override in `flake.nix` to build against `llvmPackages_22` (matching rustc nightly's LLVM 22.1.0). The derivation produces:
- AFL++ binaries: `afl-fuzz`, `afl-cc`, `afl-cmin`, `afl-tmin`, etc.
- `afl-compiler-rt.o`: Compiler runtime linked into instrumented binaries
- Plugin `.so` files: `SanitizerCoveragePCGUARD.so`, `cmplog-instructions-pass.so`, `cmplog-switches-pass.so`, `cmplog-routines-pass.so`, `split-switches-pass.so`, `afl-llvm-dict2file.so`, `afl-llvm-ijon-pass.so`
The plugins must be compiled against the same LLVM version that rustc nightly uses. Rustc nightly 1.96.0 uses LLVM 22.1.0; nixpkgs provides `llvmPackages_22` with 22.1.0-rc3.
### 1b. cargo-afl
Build from `https://github.com/rust-fuzz/afl.rs.git` at commit `644c06a` using `rustPlatform.buildRustPackage`.
Apply a patch to `cargo-afl-common/src/lib.rs` that adds two environment variable overrides:
- `CARGO_AFL_DIR` — overrides `afl_dir()` return value (directory containing `bin/afl-fuzz` etc.)
- `CARGO_AFL_LLVM_DIR` — overrides `afl_llvm_dir()` return value (directory containing `afl-compiler-rt.o` and plugin `.so` files)
When set, these skip the XDG/version-based path construction entirely. When unset, cargo-afl falls back to the original XDG behavior. This avoids polluting the user's home directory.
The Nix wrapper scripts and devShell set these env vars pointing at the AFL++ Nix store outputs.
## Section 2: Fuzz Target Changes
### afl crate dependency
Add the `afl` crate to `lib/fuzz-afl/Cargo.toml`. This is the library component of afl.rs that provides the `fuzz!()` macro, persistent mode support, and panic-as-abort hook.
### Target wrapper rewrites
Rewrite all 5 target wrappers in `lib/fuzz-afl/afl_targets/` from stdin-reading to the `fuzz!()` macro:
**Before:**
```rust
use std::io::Read;
fn main() {
let mut data = Vec::new();
std::io::stdin().read_to_end(&mut data).unwrap();
usbip_rs::fuzz_harness::run_fuzz_urb_hid(&data);
}
```
**After:**
```rust
fn main() {
afl::fuzz!(|data: &[u8]| {
usbip_rs::fuzz_harness::run_fuzz_urb_hid(data);
});
}
```
The `fuzz!()` macro provides:
- **Persistent mode**: Process stays alive, AFL++ feeds test cases via shared memory
- **Deferred forkserver**: Forking happens after initialization
- **Panic-as-crash**: Installs a panic hook that calls `abort()`, so AFL++ detects assertion failures
### Shared harness unchanged
`lib/src/fuzz_harness.rs` and `lib/src/fuzz_helpers.rs` are not modified. The targets remain thin wrappers calling the same harness functions.
## Section 3: Wrapper Script & DevShell Changes
### fuzz-afl app rewrite
1. Set `CARGO_AFL_DIR` and `CARGO_AFL_LLVM_DIR` pointing at Nix-built AFL++ outputs
2. Build AFL++ target: `cargo afl build` (handles instrumentation flags, plugin loading, compiler-rt linking)
3. Build SymCC target: same as current (`CC=symcc CXX=sym++ RUSTFLAGS="-C linker=symcc"` with separate `CARGO_TARGET_DIR`)
4. Run: `cargo afl fuzz -i $corpus -o $output "$@" -- ./target/release/$target`
- cargo-afl prepends `-c0` for CmpLog automatically
- SymCC binary path printed for manual companion use
5. User can pass extra afl-fuzz flags via `"$@"`
### fuzz-clean-afl app update
- Rebuild with `cargo afl build` instead of manual RUSTFLAGS
- Test crash files against the cargo-afl-built binary (same validation logic)
### fuzz-afl devShell
- Replace raw `aflplusplus` with `cargo-afl` package
- Keep `symcc`, `rust-nightly`, `libusb1`, `udev`, `pkg-config`
- Set `CARGO_AFL_DIR` and `CARGO_AFL_LLVM_DIR` env vars for interactive use
### Old aflplusplus package
Drop the standalone `aflplusplus` package from flake.nix if nothing else uses it — superseded by the LLVM-22-plugin-enabled build.
## Section 4: Testing & Verification
- `nix develop .#fuzz-afl` provides a working shell with `cargo afl build`/`cargo afl fuzz`
- `cargo afl fuzz --version` reports AFL++ version and "with plugins"
- `nix run .#fuzz-afl -- fuzz_urb_hid` builds with plugins, starts AFL++ with CmpLog, runs in persistent mode
- AFL++ status screen shows `[persist]` mode indicator and CmpLog stats
- SymCC binary path printed and functional
- `cargo afl build --verbose` shows `-Z llvm-plugins=...` flags
- Existing corpora in `lib/fuzz-afl/corpus/` and `lib/fuzz-afl/output/` preserved