Fuzzing Rust expert
This skill provides the agent with the knowledge and tools to write, build, and
validate fuzz targets for Rust projects integrated into OSS-Fuzz. Rust fuzzing
uses cargo-fuzz with the libfuzzer_sys crate, which drives libFuzzer.
Fundamental Concepts
OSS-Fuzz base image
Rust projects must use the Rust base builder image:
FROM gcr.io/oss-fuzz-base/base-builder-rust
Set language: rust in project.yaml.
Harness structure
Rust fuzz targets live in fuzz/fuzz_targets/<name>.rs within the crate being
fuzzed. The minimal harness using raw bytes:
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
// Call into the target. The fuzzer will mutate `data` on every iteration.
let _ = my_crate::parse(data);
});
For structured fuzzing using the arbitrary crate (preferred when the target
expects typed input):
#![no_main]
use libfuzzer_sys::fuzz_target;
use arbitrary::Arbitrary;
#[derive(Debug, Arbitrary)]
struct MyInput {
header: u8,
payload: Vec<u8>,
flags: u32,
}
fuzz_target!(|input: MyInput| {
let _ = my_crate::process(input.header, &input.payload, input.flags);
});
The Cargo.toml for the fuzz directory must declare dependencies:
# fuzz/Cargo.toml
[package]
name = "my-crate-fuzz"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
libfuzzer-sys = "0.4"
arbitrary = { version = "1", features = ["derive"] } # only if using structured fuzzing
[dependencies.my-crate]
path = ".."
[[bin]]
name = "fuzz_target_name"
path = "fuzz_targets/fuzz_target_name.rs"
test = false
doc = false
Building in OSS-Fuzz
build.sh uses cargo fuzz build and then copies binaries to $OUT:
# build.sh
cd $SRC/<crate-dir>
cargo fuzz build -O # -O enables release optimisations; important for performance
FUZZ_TARGET_OUTPUT_DIR=$SRC/<crate-dir>/target/x86_64-unknown-linux-gnu/release
for f in fuzz/fuzz_targets/*.rs; do
name=$(basename "${f%.*}")
cp "$FUZZ_TARGET_OUTPUT_DIR/$name" "$OUT/"
done
If the project requires a nightly toolchain, set it in the Dockerfile:
ENV RUSTUP_TOOLCHAIN=nightly
Or pin to a specific nightly for reproducibility:
ENV RUSTUP_TOOLCHAIN=nightly-2025-07-03
Seed corpus and dictionaries
- Place seed files in
fuzz/corpus/<target_name>/within the repo; they are automatically picked up by cargo-fuzz and can be zipped for OSS-Fuzz. - To ship a corpus with OSS-Fuzz copy a zip to
$OUT/<target_name>_seed_corpus.zip. - Dictionaries go to
$OUT/<target_name>.dict. - For targets that parse a structured format, generating seeds with a script
beats hand-picking a few files — random mutation rarely passes the parser's
early checks (note: cargo-fuzz's
arbitraryis the better route when the target takes typed data rather than a byte format). See the structured seed generation reference.
Characteristics of good Rust fuzzing harnesses
- Targets attack surface: parsers, deserializers, protocol implementations, unsafe blocks, and any API that processes untrusted bytes.
- Uses structured fuzzing (
arbitrary) when the target expects typed data rather than raw bytes — this dramatically improves coverage. - Handles expected errors:
ResultandOptionshould be let-bound and ignored (let _ = ...). Only actual panics are findings. - Avoids panicking on every input: do not use
.unwrap()or.expect()on fallible operations driven by fuzz input — these create false positives. - Avoids non-determinism: no
rand, no system time, no thread spawning inside the fuzz callback. - Is fast: avoid I/O, heavy allocations, or expensive setup inside the
callback; do those outside the
fuzz_target!macro if needed withLazyLock/OnceLock. - Considers
unsafecode: if the crate hasunsafeblocks, write harnesses that exercise those paths — this is where memory bugs can still occur in Rust. - Builds in release mode (
-O): fuzzing in debug mode is much slower.
What Rust fuzzing finds
Rust's ownership model prevents most memory-corruption bugs, but fuzzing still finds:
- Panics:
unwrap/expectonNone/Err, index out of bounds, arithmetic overflow in debug mode, explicitpanic!calls. unsafebugs: use-after-free, out-of-bounds reads/writes, undefined behaviour inunsafeblocks — these are real memory bugs.- Logic errors: incorrect parsing, data corruption, wrong output.
- Hangs: infinite loops triggered by crafted input.
- Stack overflows: deep recursion on adversarial input.
Operational guidelines
- Always validate with:
python3 infra/helper.py build_fuzzers <project> python3 infra/helper.py check_build <project> python3 infra/helper.py run_fuzzer <project> <fuzzer_name> -- -max_total_time=30 - An instant crash usually means a
panic!on every input — check for.unwrap()calls on fuzz-driven paths and replace withif letor?. - Run
cargo buildandcargo testinside the crate before adding the fuzz harness to catch pre-existing compilation errors. - When iterating locally clone the upstream repo and switch the Dockerfile from
RUN git clonetoCOPYto avoid network round-trips. - If the crate uses
no_std, uselibfuzzer-sys'sno_stdfeature and ensure the harness does not rely onstd. - Document why each entry point was chosen and what class of bugs it may find.