cargo-fuzz-rust
Author and run cargo-fuzz - Rust fuzzing via libFuzzer with cargo integration. Covers `cargo install cargo-fuzz`, `cargo fuzz init` + `cargo fuzz add <target>` for harness scaffolding, the `fuzz_target!` macro for entry-point declaration, the `Arbitrary` trait for structured input mutation, and `cargo fuzz run` invocation. Requires Rust nightly. Use for fuzz testing Rust libraries - cargo-fuzz wraps libFuzzer with native Rust ergonomics. Composes with sanitiser-integration-reference + corpus-management-reference.
cargo-fuzz-rust
Overview
cargo-fuzz (per github.com/rust-fuzz/cargo-fuzz) requires Rust nightly because libFuzzer integration depends on unstable compiler features.
For sanitiser pairing: cargo-fuzz auto-enables ASan by default (per the cargo-fuzz README). See sanitiser-integration-reference for ASan + UBSan composition. For corpus discipline see corpus-management-reference.
When to use
For raw libFuzzer in C/C++ with Rust FFI see libfuzzer-cpp.
Authoring
Install
Per the cargo-fuzz README:
# Rust nightly is required
rustup install nightly
# Install cargo-fuzz
cargo install cargo-fuzzInitialise
In your crate root:
cargo fuzz initThis creates a fuzz/ subdirectory:
fuzz/
Cargo.toml
fuzz_targets/
fuzz_target_1.rs # generated default targetAdd a fuzz target
cargo fuzz add parse_queryCreates fuzz/fuzz_targets/parse_query.rs:
#![no_main]
use libfuzzer_sys::fuzz_target;
use my_crate::parser;
fuzz_target!(|data: &[u8]| {
let _ = parser::parse_query(data);
});Per the cargo-fuzz docs, fuzz_target! is the macro that wires up the libFuzzer entry point (LLVMFuzzerTestOneInput under the hood). The closure body is what runs per input.
Structured input via Arbitrary
Raw byte slices work for binary formats; for structured inputs use the arbitrary crate:
#![no_main]
use libfuzzer_sys::fuzz_target;
use arbitrary::Arbitrary;
#[derive(Debug, Arbitrary)]
struct Request {
host: String,
port: u16,
body: Vec<u8>,
}
fuzz_target!(|req: Request| {
let _ = handle_request(&req.host, req.port, &req.body);
});The fuzzer mutates the underlying byte stream; Arbitrary deserialises to the typed struct. Add arbitrary = { version = "1", features = ["derive"] } to fuzz/Cargo.toml.
Running
Basic run
# Nightly toolchain required
cargo +nightly fuzz run parse_queryThis builds the target with libFuzzer instrumentation + ASan and runs indefinitely.
Common options
| Option | Effect |
|---|---|
--release | Release-mode build (faster, less debug info) |
--debug-assertions | Keep debug assertions in release mode |
--sanitizer=<name> | address (default), leak, memory, thread, none |
--jobs=N | Parallel workers |
--no-default-features | Disable default cargo-fuzz features |
-- <libFuzzer-flag> | Pass through to libFuzzer (e.g., -max_total_time=300) |
cargo +nightly fuzz run parse_query -- -max_total_time=300Sanitiser variants
# UBSan via none sanitiser + custom RUSTFLAGS
RUSTFLAGS="-Cpasses=sancov-module -Cllvm-args=-sanitizer-coverage-level=4 -Zsanitizer=undefined" \
cargo +nightly fuzz run --sanitizer=none parse_query
# MSan
cargo +nightly fuzz run --sanitizer=memory parse_queryReproducing a crash
cargo +nightly fuzz run parse_query \
fuzz/artifacts/parse_query/crash-<sha1>Or:
cargo +nightly fuzz fmt parse_query \
fuzz/artifacts/parse_query/crash-<sha1>
# Prints the crash input in a Rust-readable formatCrash artefacts location
Per cargo-fuzz convention:
fuzz/
corpus/
parse_query/ # evolved corpus
artifacts/
parse_query/
crash-<sha1> # crash artefacts
leak-<sha1>
timeout-<sha1>Parsing results
Sanitiser report format is identical to libFuzzer / ASan - see sanitiser-integration-reference "Reading a sanitiser report."
cargo fuzz fmt decodes binary inputs into a Rust-readable form (useful when using Arbitrary - recovers the struct).
CI integration
jobs:
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@nightly
- run: cargo install cargo-fuzz
- uses: actions/cache@v4
with:
path: |
fuzz/corpus
~/.cargo/registry
target
key: fuzz-${{ github.sha }}
restore-keys: fuzz-
- name: Smoke fuzz (5 min per target)
run: |
for target in $(cargo fuzz list); do
timeout 300 cargo +nightly fuzz run $target -- -max_total_time=300 || true
done
- uses: actions/upload-artifact@v4
if: always()
with:
name: fuzz-artifacts
path: fuzz/artifacts/Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Using stable toolchain | cargo-fuzz needs nightly | rustup install nightly; use cargo +nightly fuzz |
Raw &[u8] for structured input | Mutation hits format errors more than logic | Use Arbitrary + a custom struct |
| Empty seed corpus | Fuzzer wanders; slow path discovery | Drop a few representative inputs in fuzz/corpus/<target>/ |
Ignoring --release | Debug builds slow iteration | Use --release for long campaigns |
No cargo fuzz fmt on crash | Hard-to-read crash inputs | Always cargo fuzz fmt before filing a bug |
Committing fuzz/artifacts/ to repo | Repo bloat | .gitignore artifacts; persist via CI cache |
| Mixing fuzz targets in one file | Cargo treats each fuzz_targets/*.rs as one binary | One file per target |