usbip-rs/docs/superpowers/specs/2026-03-26-cargo-afl-migration-design.md
Davíð Steinn Geirsson bb3c603172 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>
2026-03-27 00:33:34 +00:00

5.3 KiB

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:

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:

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