| name | testing-fuzzing |
| description | Design and implement fuzzing harnesses for crash discovery, security auditing, and correctness verification. Coverage-guided, structure-aware, differential, and grammar-based fuzzing. Use when: testing parsers, protocols, serialization, cryptography, state machines, file formats, or any input-processing code. cargo-fuzz, AFL++, bolero, proptest, libFuzzer, fast-check, sanitizers. |
| metadata | {"filePattern":["**/fuzz*","**/fuzz_targets/**","**/Cargo.toml"],"bashPattern":["\\b(cargo.fuzz|cargo-fuzz|libfuzzer|afl|honggfuzz|bolero|fuzz_target)\\b"],"priority":60} |
Fuzzing
The One Rule: Throw random-but-intelligent inputs at code until something breaks.
Profile the fuzzer, not the code. A fuzzer that never reaches deep code paths finds nothing.
The Loop (Mandatory)
1. TARGET → Identify the narrowest input-processing boundary
2. SEED → Craft a minimal corpus of valid + boundary inputs
3. STRUCTURE → Derive Arbitrary impl if input has grammar/schema
4. HARNESS → Write fuzz target: guard size, assert invariants, never panic in harness
5. SANITIZE → Enable ASan + UBSan minimum; MSan for unsafe code
6. RUN → Coverage-guided fuzzing, monitor new edge discovery rate
7. TRIAGE → Minimize crashes, classify root causes, write regression tests
8. ITERATE → New corpus → deeper coverage → new crashes → repeat
Target Selection Matrix (Mandatory)
Before writing a harness, score the attack surface:
| Target Function | Untrusted Input? | Complexity (1-5) | Unsafe Code? | Prior CVEs? | Score |
|---|
| func_name | Y/N | cyclomatic | Y/N | count | Sum |
Rule: Fuzz highest-scoring targets first. Don't fuzz internal helpers
that only receive validated data — fuzz the validation boundary itself.
Decision Tree: Which Fuzzing Approach?
What are you fuzzing?
│
├─ Raw bytes → parser/deserializer
│ └─ COVERAGE-GUIDED (cargo-fuzz / libFuzzer)
│ Best for: finding crashes in binary parsers, image decoders, protocol parsers
│
├─ Structured input → has grammar/schema
│ └─ STRUCTURE-AWARE (Arbitrary derive + cargo-fuzz)
│ Best for: API fuzzing, config validation, SQL, JSON with constraints
│
├─ Two implementations of same thing
│ └─ DIFFERENTIAL (both in one harness, compare outputs)
│ Best for: porting across languages, optimized vs reference impl
│
├─ Protocol with state machine
│ └─ STATEFUL (sequence of operations)
│ Best for: file systems, databases, network protocols
│
├─ Crypto / timing sensitive
│ └─ SPECIALIZED (SideFuzz, ct-fuzz, dudect)
│ Best for: constant-time verification, side-channel detection
│
└─ Already have proptest tests
└─ UNIFIED (Bolero — same harness, any engine)
Best for: upgrading property tests to full fuzzing without rewrite
Harness Patterns (The Five Archetypes)
Archetype 1: Crash Detector (Raw Bytes)
The simplest and most common. Feed arbitrary bytes, ensure no panics.
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if data.len() > 1_000_000 { return; }
let _ = my_crate::parse_message(data);
});
Invariant: let _ = for expected failures. assert! for properties that
must hold even on garbage input (e.g., "parser never corrupts global state").
Archetype 2: Round-Trip (Serialize ↔ Deserialize)
The gold standard for serialization code. Tests both directions simultaneously.
#![no_main]
use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
#[derive(Debug, Arbitrary)]
struct FuzzInput {
raw: Vec<u8>,
values: Vec<FuzzValue>,
}
#[derive(Debug, Arbitrary)]
enum FuzzValue {
Null,
Integer(i64),
Float(f64),
Text(String),
Blob(Vec<u8>),
}
fuzz_target!(|input: FuzzInput| {
if input.raw.len() <= 65536 {
let _ = parse_record(&input.raw);
}
if !input.values.is_empty() && input.values.len() <= 1000 {
let values: Vec<Value> = input.values.iter().map(|v| v.into()).collect();
let serialized = serialize_record(&values);
= (&serialized)
.();
(values, deserialized,
);
}
});
Archetype 3: Differential (Two Implementations)
Compare implementations that should agree. Every divergence is a bug in one of them.
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let ours = my_parser::parse(data);
let reference = reference_parser::parse(data);
match (ours, reference) {
(Ok(a), Ok(b)) => {
assert_eq!(a, b, "DIVERGENCE: both accepted but outputs differ");
}
(Err(_), Err(_)) => {
}
(Ok(_), Err(_)) => {
}
(Err(e), Ok(_)) => {
panic!("CONFORMANCE BUG: reference accepts input we reject: {e}");
}
}
});
Archetype 4: Stateful (Operation Sequences)
Fuzz sequences of operations on stateful systems. The killer pattern for databases,
file systems, and protocol stacks.
#![no_main]
use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
#[derive(Debug, Arbitrary)]
enum Op {
Insert { key: u8, value: Vec<u8> },
Get { key: u8 },
Delete { key: u8 },
BeginTx,
Commit,
Rollback,
Checkpoint,
}
#[derive(Debug, Arbitrary)]
struct FuzzSession { ops: Vec<Op> }
fuzz_target!(|session: FuzzSession| {
if session.ops.len() > 200 { return; }
if session.ops.iter().any(|op| matches!(op, Op::Insert { value, .. } if value.len() > 10_000)) {
return;
}
let db = Database::open_in_memory();
let mut model = BTreeMap::new();
for op in &session.ops {
match op {
Op::Insert { key, value } => {
let _ = db.insert(&[*key], value);
model.insert(*key, value.());
}
Op::Get { key } => {
= db.(&[*key]);
= model.(key);
(db_result, model_result) {
((db_val), (model_val)) => {
(&db_val, model_val,
);
}
(, ) => {}
((_), ) => (),
(, (_)) => (),
}
}
Op::Delete { key } => {
= db.(&[*key]);
model.(key);
}
_ => { = db.(op); }
}
}
db.().();
});
Archetype 5: Grammar-Based (Syntax-Aware)
Generate syntactically valid-ish inputs for language/protocol parsers.
#![no_main]
use arbitrary::{Arbitrary, Unstructured};
use libfuzzer_sys::fuzz_target;
#[derive(Debug)]
struct FuzzSQL(String);
impl<'a> Arbitrary<'a> for FuzzSQL {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let stmt = match u.int_in_range(0..=4)? {
0 => format!("SELECT {} FROM {}", fuzz_columns(u)?, fuzz_table(u)?),
1 => format!("INSERT INTO {} VALUES ({})", fuzz_table(u)?, fuzz_values(u)?),
2 => format!("DELETE FROM {} WHERE {}", fuzz_table(u)?, fuzz_predicate(u)?),
3 => format!("UPDATE {} SET {} WHERE {}", fuzz_table(u)?,
fuzz_assignment(u)?, fuzz_predicate(u)?),
_ => (, (u)?, (u)?),
};
((stmt))
}
}
fuzz_target!(|sql: FuzzSQL| {
= Database::();
= db.(&sql.);
db.().();
});
Sanitizer Discipline (Non-Negotiable)
cargo fuzz run my_target
RUSTFLAGS="-Zsanitizer=undefined" cargo +nightly fuzz run my_target
RUSTFLAGS="-Zsanitizer=memory" cargo +nightly fuzz run my_target
RUSTFLAGS="-Zsanitizer=thread" cargo +nightly fuzz run my_target
| Sanitizer | Catches | Overhead | When |
|---|
| ASan | Buffer overflow, UAF, double-free | 2x | Always |
| UBSan | Integer overflow, alignment | 1.2x | Always |
| MSan | Uninitialized reads | 3x | Unsafe code |
| TSan | Data races | 5-15x | Concurrent code |
Anti-pattern: Running without sanitizers. A fuzzer without ASan is a
random test generator, not a security tool.
Corpus Engineering
Seed Corpus (Day 0)
mkdir -p fuzz/corpus/my_target
echo '{}' > fuzz/corpus/my_target/empty_object
echo '{"key": "value"}' > fuzz/corpus/my_target/simple
echo '{"nested": {"deep": [1,2,3]}}' > fuzz/corpus/my_target/nested
printf '' > fuzz/corpus/my_target/empty
printf '\x00' > fuzz/corpus/my_target/null_byte
printf '\xff' > fuzz/corpus/my_target/high_byte
head -c 65536 /dev/urandom > fuzz/corpus/my_target/large_random
Corpus Maintenance
cargo fuzz cmin my_target
cargo fuzz tmin my_target artifacts/my_target/crash-abc123
cargo fuzz coverage my_target
Rule: Minimize corpus weekly. A 100K-entry corpus is slower than a
500-entry corpus with the same coverage.
Triage Pipeline
When fuzzing finds a crash:
1. MINIMIZE → cargo fuzz tmin my_target crash-xxx
2. REPRODUCE → cargo fuzz run my_target artifacts/crash-xxx (deterministic?)
3. CLASSIFY → Memory bug? Logic bug? Panic? OOM? Timeout?
4. ROOT-CAUSE → Read the stack trace, write the fix
5. REGRESSION → Convert to permanent unit test:
#[test]
fn regression_crash_abc123() {
let input = include_bytes!("../fuzz/artifacts/my_target/crash-abc123");
let _ = my_crate::parse(input); // Must not panic after fix
}
6. RE-FUZZ → Run again to find deeper bugs now that shallow ones are fixed
Bolero: Write Once, Fuzz Everywhere
use bolero::check;
#[test]
fn fuzz_parser_bolero() {
check!()
.with_type::<Vec<u8>>()
.cloned()
.for_each(|data| {
let _ = my_crate::parse(&data);
});
}
cargo bolero test fuzz_parser_bolero --engine libfuzzer
cargo bolero test fuzz_parser_bolero --engine afl
cargo bolero test fuzz_parser_bolero --engine honggfuzz
cargo test fuzz_parser_bolero
Why Bolero: Write the harness once. Run with libFuzzer for deep coverage,
AFL++ for deterministic mutation, honggfuzz for hardware-assisted feedback,
and plain cargo test for CI regression. Zero harness duplication.
Anti-Patterns (Hard Constraints)
| ✗ Never | Why | Fix |
|---|
| Fuzz target is the entire program | Coverage too thin, never reaches interesting code | Narrow to one parser/function |
| No input size guards | OOM kills mask real bugs | if data.len() > MAX { return; } |
unwrap() in the harness itself | Harness crash ≠ code bug | Use let _ = for expected failures |
| No seed corpus | Fuzzer wastes hours on trivially invalid inputs | Seed with valid + boundary examples |
| Never minimize crashes | 100KB crash input is undebuggable | Always cargo fuzz tmin |
| Run without sanitizers | Miss the bugs fuzzing is best at finding | ASan + UBSan minimum |
| Fuzz forever, never triage | Crashes pile up without fixes | Triage daily, fix weekly |
| Check error message strings | Brittle, breaks on rewording | Check error types/categories |
Checklist (Before Shipping Fuzz Harness)
References
Relationship to Other Testing Skills
| Technique | Use INSTEAD when | Use TOGETHER when |
|---|
| /testing-metamorphic | Logic bugs, not crashes | Fuzzing generates inputs, MRs check relations |
| /testing-conformance-harnesses | Spec compliance, not crashes | Fuzz-generated inputs feed conformance checks |
| /extreme-software-optimization | Perf, not correctness | Fuzz after optimization to verify no regressions |
| /lean-formal-feedback-loop | Need mathematical proof | Fuzzing finds counterexamples, proofs close the gap |