Fuzz Testing
Automated fuzz testing to discover crashes, panics, and edge cases using cargo-fuzz.
Reference
Section titled “Reference”| Field | Value |
|---|---|
| Workflow | .github/workflows/fuzz-testing.yml |
| Tool | cargo-fuzz (libFuzzer) |
| Trigger | Manual (workflow_dispatch); a daily cron is present but commented out |
| Goal | Find unexpected inputs that cause crashes |
How fuzzing works
Section titled “How fuzzing works”The fuzzer generates random or mutated inputs and feeds them to a target function:
- Generate inputs — create random or mutated test inputs.
- Execute — run the target function with each input.
- Monitor — detect crashes, panics, timeouts, and memory errors.
- Minimize — reduce a crashing input to a minimal reproducible case.
- Report — save crash artifacts for investigation.
CI behavior
Section titled “CI behavior”The workflow runs:
- On demand via
workflow_dispatch(the only active trigger). - Duration: 5 minutes per target (configurable).
A daily schedule: cron (0 2 * * *) is present in the workflow but commented out. Uncomment the schedule: block in .github/workflows/fuzz-testing.yml to run fuzzing daily.
On a crash it creates a GitHub issue and uploads crash artifacts (90-day retention).
Successful run output (no crashes)
Section titled “Successful run output (no crashes)”#0 READ units: 1234#1 pulse cov: 234 ft: 456 corp: 10/1234b...Done 10000 runs in 300 seconds- units: inputs tested.
- cov: code coverage.
- ft: features covered.
- corp: corpus size.
Crash output
Section titled “Crash output”==1234==ERROR: AddressSanitizer: heap-buffer-overflowREAD of size 1 at 0x...The crashing input is saved to fuzz/artifacts/<target>/crash-<hash>.
Corpus layout
Section titled “Corpus layout”fuzz/corpus/parse_input/├── 0a1b2c3d4e5f... # Auto-generated interesting cases├── 1b2c3d4e5f6a...└── seed_inputs/ # Your seed corpusThe fuzzer automatically saves interesting inputs that reach new coverage.
Security benefits
Section titled “Security benefits”Fuzz testing finds buffer overflows, integer overflows, assertion failures, panics and unwraps, memory leaks, and logic errors triggered by edge-case inputs.
How-to
Section titled “How-to”Initialize fuzzing
Section titled “Initialize fuzzing”# Install cargo-fuzz (requires nightly Rust)cargo install cargo-fuzz
# Initialize fuzz targetscargo fuzz initThis creates:
fuzz/├── Cargo.toml└── fuzz_targets/ └── fuzz_target_1.rsVerify: cargo fuzz list prints the generated target.
Create a fuzz target
Section titled “Create a fuzz target”Write fuzz/fuzz_targets/parse_input.rs:
#![no_main]
use libfuzzer_sys::fuzz_target;use rust_template::parse;
fuzz_target!(|data: &[u8]| { // Convert bytes to string if let Ok(s) = std::str::from_utf8(data) { // Fuzz the parse function let _ = parse(s); }});For structured input, derive Arbitrary:
#![no_main]
use libfuzzer_sys::fuzz_target;use arbitrary::Arbitrary;
#[derive(Arbitrary, Debug)]struct FuzzInput { value: i32, flag: bool, data: Vec<u8>,}
fuzz_target!(|input: FuzzInput| { // Fuzz with structured input process(input.value, input.flag, &input.data);});Verify: cargo fuzz list shows the new target.
Run fuzzing locally
Section titled “Run fuzzing locally”# List fuzz targetscargo fuzz list
# Run a target for 60 secondscargo fuzz run parse_input -- -max_total_time=60
# Run with more parallel jobscargo fuzz run parse_input -- -jobs=4
# Run against a saved corpuscargo fuzz run parse_input fuzz/corpus/parse_inputVerify: the run prints cov:/corp: lines and ends with Done N runs.
Investigate a crash
Section titled “Investigate a crash”-
Reproduce with the saved artifact:
Terminal window cargo fuzz run parse_input fuzz/artifacts/parse_input/crash-* -
Minimize the crashing input:
Terminal window cargo fuzz tmin parse_input crash_artifact -
Add debug output to the target if needed:
fuzz_target!(|data: &[u8]| {eprintln!("Input length: {}", data.len());if let Ok(s) = std::str::from_utf8(data) {eprintln!("Input: {:?}", s);let _ = parse(s);}});
Verify: re-running with the minimized artifact still reproduces the crash, then fix the bug and confirm it no longer does.
Manage the corpus
Section titled “Manage the corpus”Seed initial inputs in fuzz/corpus/<target>/:
mkdir -p fuzz/corpus/parse_inputecho "valid input" > fuzz/corpus/parse_input/valid1echo "" > fuzz/corpus/parse_input/emptyecho "🦀" > fuzz/corpus/parse_input/unicodeVerify: cargo fuzz run parse_input fuzz/corpus/parse_input loads the seeds.
Configure fuzzing
Section titled “Configure fuzzing”# In the workflow — adjust per-target durationduration: '600' # 10 minutes# Limit memory usagecargo fuzz run target -- -rss_limit_mb=2048Add a dictionary of domain keywords at fuzz/dict/target.dict:
"keyword1""keyword2""special_token"cargo fuzz run target -- -dict=fuzz/dict/target.dictVerify: the run reports the dictionary loaded.
Common fuzz target patterns
Section titled “Common fuzz target patterns”// Parsersfuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { let _ = parser::parse(s); }});
// Deserializationfuzz_target!(|data: &[u8]| { let _: Result<MyStruct, _> = serde_json::from_slice(data);});
// Binary protocolsfuzz_target!(|data: &[u8]| { let _ = decode_packet(data);});State machines via a sequence of arbitrary actions:
#[derive(Arbitrary, Debug)]enum Action { Start, Process(u8), Stop,}
fuzz_target!(|actions: Vec<Action>| { let mut state = State::new(); for action in actions { state.handle(action); }});A complete structured target with input constraints:
#![no_main]
use libfuzzer_sys::fuzz_target;use arbitrary::Arbitrary;
#[derive(Arbitrary, Debug)]struct Config { timeout: u32, retries: u8, url: String,}
fuzz_target!(|config: Config| { // Validate constraints if config.timeout > 0 && config.timeout < 10000 { if config.retries <= 10 { if config.url.len() < 256 { // Fuzz the actual function let _ = process_request(&config); } } }});
fn process_request(config: &Config) -> Result<(), Error> { // Implementation Ok(())}Troubleshooting
Section titled “Troubleshooting”Slow fuzzing:
cargo fuzz run target -- -jobs=8cargo fuzz run target -- -max_len=1024Out of memory:
cargo fuzz run target -- -rss_limit_mb=2048rm -rf fuzz/corpus/target/*No new coverage — the fuzzer may be stuck. Add a better seed corpus, a dictionary, or switch to structured fuzzing with arbitrary.
Best practices
Section titled “Best practices”- Start simple — fuzz one function at a time.
- Use a seed corpus — guide the fuzzer with valid examples.
- Run long sessions — hours or days, not minutes.
- Minimize crashes — use
cargo fuzz tminfor debugging. - Fuzz continuously — run in CI regularly.
- Fuzz multiple targets — cover different entry points.
Why this matters
Section titled “Why this matters”Hand-written tests check the inputs a developer thought of; fuzzing checks the inputs nobody thought of. By mutating inputs toward new code coverage, a fuzzer drives execution into the malformed, adversarial, and boundary cases where parsers, decoders, and deserializers actually break. Because it runs unattended and saves any crash as a minimized, replayable artifact, fuzzing turns “we hope this handles bad input” into a reproducible bug report — and enabling the (commented-out) daily schedule keeps probing as the code evolves, catching regressions long-running campaigns would otherwise surface only by luck.