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>
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
.sofiles: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— overridesafl_dir()return value (directory containingbin/afl-fuzzetc.)CARGO_AFL_LLVM_DIR— overridesafl_llvm_dir()return value (directory containingafl-compiler-rt.oand plugin.sofiles)
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
- Set
CARGO_AFL_DIRandCARGO_AFL_LLVM_DIRpointing at Nix-built AFL++ outputs - Build AFL++ target:
cargo afl build(handles instrumentation flags, plugin loading, compiler-rt linking) - Build SymCC target: same as current (
CC=symcc CXX=sym++ RUSTFLAGS="-C linker=symcc"with separateCARGO_TARGET_DIR) - Run:
cargo afl fuzz -i $corpus -o $output "$@" -- ./target/release/$target- cargo-afl prepends
-c0for CmpLog automatically - SymCC binary path printed for manual companion use
- cargo-afl prepends
- User can pass extra afl-fuzz flags via
"$@"
fuzz-clean-afl app update
- Rebuild with
cargo afl buildinstead of manual RUSTFLAGS - Test crash files against the cargo-afl-built binary (same validation logic)
fuzz-afl devShell
- Replace raw
aflpluspluswithcargo-aflpackage - Keep
symcc,rust-nightly,libusb1,udev,pkg-config - Set
CARGO_AFL_DIRandCARGO_AFL_LLVM_DIRenv 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-aflprovides a working shell withcargo afl build/cargo afl fuzzcargo afl fuzz --versionreports AFL++ version and "with plugins"nix run .#fuzz-afl -- fuzz_urb_hidbuilds 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 --verboseshows-Z llvm-plugins=...flags- Existing corpora in
lib/fuzz-afl/corpus/andlib/fuzz-afl/output/preserved