Rust Fuzzing with AFL
- Version: 1.0.0
- Updated: 2026-08-23
- Reference snapshot: cargo-afl 0.18.x, including
fuzz_with_reset!and optional omission of CmpLog instrumentation.
Mission
Build fuzzing that finds real bugs, reproduces them deterministically, and turns every confirmed crash into a regression test.
Treat the harness, oracle, corpus, campaign configuration, and triage path as one system. A fast harness with a weak oracle is incomplete. A strong oracle with hidden global state is unreliable. A crash without a minimized reproducer and regression test is unfinished work.
Activate this skill when
Use this skill for requests involving:
- Rust fuzzing, AFL, AFL++,
cargo-afl, or theaflcrate. - Parser, decoder, deserializer, decompressor, protocol, bytecode, image, archive, or file-format fuzzing.
- Differential, round-trip, state-machine, no-panic, invariant, or resource-limit fuzzing.
- Native Rust code containing
unsafe, FFI, custom allocators, SIMD, threading, or manually managed buffers. - Corpus minimization, dictionaries, multi-core campaigns, CI fuzzing, crash triage, or fuzz regressions.
- Converting an existing
cargo-fuzztarget to AFL, or sharing one target body across engines.
Do not replace focused unit tests or property tests with fuzzing. Use them together.
Load supporting material selectively
Read only the references needed for the task:
- Harnesses and oracles:
references/harness-design.md - Corpus, dictionaries, campaigns, CmpLog, and parallelism:
references/campaign-operations.md - Reproduction, minimization, sanitizers, and regressions:
references/triage-and-regressions.md - CI, platform constraints, workspaces, and version policy:
references/ci-and-platforms.md - Failure diagnosis:
references/troubleshooting.md - Primary documentation snapshot:
references/sources.md
Reusable snippets are under templates/. They contain explicit REPLACE_* markers and must be adapted to the repository rather than copied blindly.
Non-negotiable rules
- Inspect before editing. Read the workspace manifests, crate graph, feature flags, MSRV, existing fuzz/property tests, fixtures, public APIs,
unsafeblocks, FFI boundaries, global state, and likely hot paths. - Isolate fuzz-only dependencies. Prefer a dedicated non-published package such as
fuzz/afl/. Do not makeafla production dependency. - Keep the target in-process. Call the library API directly. Do not fuzz through a CLI, shell command, network socket, or production service unless source constraints make that unavoidable.
- Make each iteration deterministic. Control randomness, time, locale, environment, filesystem state, thread scheduling, caches, and static mutable state.
- Crash on violated invariants. Do not catch and discard panics that represent bugs. Use ordinary
afl::fuzz!unless there is a documented reason for a no-hook variant. - Bound work, not semantics. Cap pathological allocations, recursion, operation counts, decompressed output, and wall-clock work. Avoid so much pre-validation that malformed inputs never reach the target.
- No external side effects. Never use production credentials, real network endpoints, user home directories, persistent databases, or shared temporary paths. Use isolated per-process temporary state when a filesystem is essential.
- Do not invoke privileged tuning automatically.
cargo afl system-configmay invokesudo. Explain the change and obtain explicit approval before running it. - Preserve evidence. Do not delete or overwrite a corpus, output directory, or crash artifact without an explicit safe path and a backup or replacement plan.
- Finish the loop. Reproduce, minimize, diagnose, fix, and add a direct regression test. A campaign is not complete merely because a crash file exists.
Standard workflow
1. Establish the target and threat model
Identify:
- The exact API accepting attacker-controlled or untrusted data.
- The reachable implementation, including feature-gated and native dependencies.
- The bugs that matter: panic, memory safety, incorrect acceptance, semantic divergence, unbounded resource use, state corruption, or nondeterminism.
- The maximum meaningful input and output sizes.
- Whether the target has global/static state or spawns threads.
- Whether malformed inputs are expected to return
Errrather than crash.
Rank candidate targets by:
- Untrusted input exposure.
unsafeor FFI density.- Parser/decoder complexity.
- Existing bug history.
- Lack of test coverage.
- Cheap in-process execution.
- Availability of a strong oracle.
Start with a narrow, high-value API. Split unrelated formats or modes into separate targets.
2. Choose an oracle before writing the harness
Preferred oracles, in descending order of value:
- Differential: two independent implementations agree after normalization.
- Round-trip:
decode(encode(x)) == normalize(x)ordecompress(compress(x)) == x. - Canonicalization/idempotence:
canonicalize(canonicalize(x)) == canonicalize(x). - State-machine invariants: legal operation sequences preserve model constraints.
- Metamorphic: transformations preserve a defined result.
- Resource bound: output, allocations, recursion, or operations stay within a justified cap.
- No-panic: arbitrary input cannot panic, abort, hang, or violate memory safety.
Do not treat every Err as a bug. Do not unwrap() expected parse failures merely to manufacture crashes.
3. Choose the harness architecture
Prefer an engine-neutral target body:
pub fn fuzz_one(data: &[u8]) {
// Call the real API and assert the chosen oracle.
}
Then keep the AFL adapter thin:
fn main() {
afl::fuzz!(|data: &[u8]| {
fuzz_support::fuzz_one(data);
});
}
This permits deterministic replay from a normal test or binary and makes it easier to add a second engine later.
Use raw &[u8] by default. Use a typed arbitrary::Arbitrary input when API call sequences or semantically meaningful parameter combinations matter more than malformed byte-level structure.
4. Add an isolated AFL package
Recommended layout:
project/
├── Cargo.toml
├── src/
├── tests/
└── fuzz/
└── afl/
├── Cargo.toml
├── corpus/
│ └── target-name/
├── dictionaries/
├── src/
│ ├── lib.rs
│ └── bin/
│ └── target_name.rs
└── artifacts/
Set publish = false. Match the repository's Rust edition and MSRV. Either add the package to the root workspace or make it an intentionally separate nested workspace with an empty [workspace] table.
The current cargo-afl build path sets fuzzing-oriented code generation options, including debug assertions and overflow checks. Avoid replacing its RUSTFLAGS wholesale.
When library code uses #[cfg(fuzzing)], register the custom cfg with the repository's unexpected_cfgs lint. cargo-afl enables cfg(fuzzing) by default across the dependency graph; AFL_NO_CFG_FUZZING=1 disables that behavior.
5. Install and verify the toolchain
Prerequisites include a Rust toolchain, a C compiler, and make.
cargo install cargo-afl --locked
cargo afl --version
For a deliberate upgrade:
cargo install --force cargo-afl --locked
After changing the Rust toolchain, rebuild the AFL runtime when required:
cargo afl config --build
Record the Rust version, cargo-afl version, bundled AFL++ version, target triple, build features, and relevant environment variables in campaign notes.
6. Build and smoke-test the target
From the AFL package:
cargo afl build --release --bin target_name
Verify direct replay before starting a campaign:
printf 'seed' | target/release/target_name
Then run a bounded smoke campaign:
cargo afl fuzz \
-i corpus/target-name \
-o artifacts/target-name \
-V 60 \
-- target/release/target_name
Before scaling, confirm:
- The executable is instrumented and starts under AFL++.
- At least one seed completes normally.
- Expected invalid inputs return without crashing.
- The map is not empty.
- Execution speed is plausible for the target.
- Stability is high and does not collapse after the first iteration.
- Input size and work limits prevent trivial memory or timeout exhaustion.
7. Build the corpus and dictionary
Seed with the smallest diverse inputs that exercise distinct grammar and semantic features:
- Empty or minimum valid input when meaningful.
- One minimal valid example per major mode/version.
- A few near-valid malformed examples.
- Boundary lengths, integer extrema, nesting, repeated sections, and optional fields.
- Previously fixed regression inputs, after reviewing whether they are safe to publish.
Minimize coverage-equivalent seeds:
cargo afl cmin \
-i corpus-raw/target-name \
-o corpus/target-name \
-- target/release/target_name
Use a dictionary for stable protocol tokens, magic values, keywords, delimiters, tags, and field names:
cargo afl fuzz \
-i corpus/target-name \
-o artifacts/target-name \
-x dictionaries/format.dict \
-- target/release/target_name
Keep tokens short and semantically useful. Do not use a huge dictionary as a substitute for a good harness.
8. Run the campaign
A single instance:
cargo afl fuzz \
-i corpus/target-name \
-o artifacts/target-name \
-- target/release/target_name
Resume an existing AFL++ output directory:
cargo afl fuzz \
-i - \
-o artifacts/target-name \
-- target/release/target_name
Use one process per core for serious campaigns. Share one output directory, give every instance a unique -M or -S name, and vary power schedules or target variants.
cargo afl fuzz enables CmpLog by default. Keep CmpLog on at most one or two workers. Pass -c - to additional workers. For builds where no worker will use CmpLog, omit its instrumentation entirely:
AFLRS_NO_CMPLOG=1 cargo afl build --release --bin target_name
Do not mix binaries built with incompatible instrumentation or target features in one campaign without documenting the topology.
9. Monitor and tune based on evidence
Monitor all workers:
cargo afl whatsup -s artifacts/target-name
Inspect individual fuzzer_stats, queue growth, unique crashes, hangs, execution speed, cycles, map density, and stability.
Diagnose before tuning:
- Low stability: leaked state, nondeterminism, threads, uninitialized data, or incomplete persistent reset.
- Very low speed: excessive setup, logging, allocation, large inputs, external I/O, debug wrappers, or an overly broad target.
- No new paths: wrong API, empty instrumentation, seeds rejected too early, hard checksums/encryption/compression barriers, or a weak input model.
- Mostly timeouts/OOM: missing resource bounds or an unrealistic timeout/memory limit.
- Many duplicate crashes: minimize and group by root-cause stack, not filename alone.
Use fuzz_with_reset! when persistent iterations reuse static state that can be explicitly cleared. If correct reset is impossible, isolate state per iteration or use a non-persistent adapter rather than accepting poor stability.
10. Triage every result
For each crash or hang:
- Copy the artifact to an immutable triage location.
- Reproduce with the exact campaign binary and environment.
- Confirm reproducibility across multiple runs.
- Minimize with
cargo afl tmin. - Reproduce through the shared non-AFL target body.
- Obtain a symbolized backtrace.
- Classify the oracle and root cause.
- Test under an appropriate sanitizer or Miri when
unsafe/FFI is implicated. - Fix the root cause, not the harness symptom.
- Add the minimized input as a direct regression test.
- Re-run the target and relevant test suite.
Example minimization:
cargo afl tmin \
-i path/to/crash \
-o triage/minimized.bin \
-- target/release/target_name
Example direct replay:
RUST_BACKTRACE=1 target/release/target_name < triage/minimized.bin
11. Add CI without pretending CI is a full campaign
CI fuzzing should:
- Build every fuzz target.
- Replay all committed regression inputs.
- Run a short bounded AFL++ smoke campaign on selected targets.
- Reuse a small minimized corpus.
- Upload new crash/hang artifacts.
- Fail when a real new crash is found.
- Use deterministic names and avoid privileged host tuning.
Use -S ci rather than a main node for a CI worker. In constrained hosted environments, AFL_SKIP_CPUFREQ=1 and AFL_NO_AFFINITY=1 may be required. AFL_FAST_CAL=1 and AFL_CMPLOG_ONLY_NEW=1 are useful for short runs or large reused corpora.
Long-running campaigns belong on dedicated workers with persisted corpus and output, not solely in pull-request CI.
Harness choice matrix
| Situation | Preferred input | Primary oracle |
|---|---|---|
| Parser/decoder accepts bytes | &[u8] |
no-panic + semantic invariants |
| Serializer and parser both exist | typed or bytes | round-trip/canonicalization |
| Rust and reference implementation exist | &[u8] or typed |
differential |
| Compressor/decompressor | bytes + bounded parameters | round-trip + output cap |
| Stateful API | bounded Vec<Operation> |
model/state invariants |
| Unsafe/FFI boundary | bytes with explicit size caps | crash + ASan/Miri replay |
| Text grammar | bytes first; &str only when required |
parser invariants + dictionary |
| Concurrent algorithm | deterministic sequential core first | model; use Loom for schedules |
Stop conditions
Stop or redesign a target when:
- Stability remains poor after state isolation/reset.
- The harness spends most time in setup, logging, I/O, or unrelated code.
- Nearly all inputs are rejected before reaching meaningful logic.
- The oracle is ambiguous and generates false positives.
- Inputs can trigger unbounded work unrelated to the intended threat model.
- The campaign topology or output directory is not reproducible.
- A crash cannot be replayed outside the fuzzer because the harness hides required state.
Do not silently continue a low-quality campaign.
Required deliverables
When implementing fuzzing in a repository, produce:
- A dedicated AFL fuzz package or a documented reason for another layout.
- One or more focused targets with named oracles.
- A small reviewed seed corpus.
- A dictionary when the format has stable tokens.
- Reproducible build and run commands.
- A bounded local/CI smoke command.
- A crash minimization and replay path.
- Regression tests for confirmed findings.
- Documentation of version, platform, features, resource limits, and known gaps.
- Verification results, including anything not run.
Completion checklist
- Real API called in-process.
- Fuzz dependency isolated from production.
- Target builds with
cargo afl build. - Harness is deterministic and side-effect isolated.
- Oracle is explicit and low-noise.
- Input/output/work limits are justified.
- Seed corpus is small and diverse.
- Dictionary is present when useful.
- Persistent state is reset or avoided.
- CmpLog is limited to one or two workers.
- Parallel workers use unique names and shared output.
- Crash replay and minimization commands are documented.
- Confirmed bugs become direct regression tests.
- CI smoke run is bounded and preserves artifacts.
- Toolchain and campaign metadata are recorded.