Finding Bugs Before They Find You: Fuzzing Rust with cargo-fuzz
I've written a lot of tests in my time. Unit tests, integration tests, property-based tests. But there's one category of testing that always felt like magic: fuzzing. The idea that you can just... run your code with random inputs and have it find bugs for you? That sounds too good to be true.
Turns out it's real, and it's spectacular.
What Fuzzing Actually Is
Fuzzing is automated testing with random inputs. But not pure random — modern fuzzers use coverage-guided approaches. They track which code paths get exercised and prioritize inputs that discover new territory. It's like a curious explorer who remembers every room they've visited and actively tries to find doors they haven't opened yet.
The most famous fuzzing success story is probably Heartbleed — a buffer overread bug in OpenSSL that went undetected for years until fuzzing caught it. In Rust, fuzzing has found bugs in the standard library, cargo itself, and countless popular crates.
Enter cargo-fuzz
The Rust fuzzing ecosystem has a few options, but cargo-fuzz is the go-to for Cargo projects. It uses libFuzzer under the hood and integrates seamlessly with your existing workflow.
Installation
cargo install cargo-fuzz
That's it. One command and you're ready to break your code.
Your First Fuzz Target
Let's fuzz something real. I'll use the url crate as an example — it's a parsing library, and parsers are prime fuzzing territory.
First, set up a project:
cargo new fuzzing-demo
cd fuzzing-demo
cargo-fuzz init
This creates a fuzz/ directory with a starter harness. The magic happens in fuzz/fuzz_targets/fuzz_target_1.rs:
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
// Your fuzzing logic here
// If this function panics or crashes, you've found a bug!
});
Now let's fuzz the URL parser. First, add the dependency:
cargo add url
Update the fuzz target:
fuzz edit fuzz_target_1
#![no_main]
use libfuzzer_sys::fuzz_target;
use url::Url;
fuzz_target!(|data: &[u8]| {
// Try interpreting the bytes as a UTF-8 string
if let Ok(s) = std::str::from_utf8(data) {
// Try to parse it as a URL
let _ = Url::parse(s);
}
});
Running the Fuzzer
cargo fuzz run fuzz_target_1
Watch the magic. The fuzzer generates random byte sequences, feeds them to your code, and tracks coverage. When it finds inputs that trigger new code paths, it keeps those inputs and mutates them further. This is coverage-guided fuzzing in action.
If something crashes, you'll see:
Failing input:
...
And you can replay that specific input:
cargo fuzz run fuzz_target_1 fuzz/crashes/xxx
What I Found When I Fuzzed My Own Code
The first time I ran cargo-fuzz on a parser I'd written, it found a panic within 30 seconds. Thirty seconds. I had written tests. I had checked edge cases. But the fuzzer found a string of unicode characters that caused an integer overflow in my length calculations.
That's the thing about fuzzing: it thinks about input space differently than humans do. We test what should work. Fuzzers test what could break.
Real-World Results
The Rust Fuzzing Authority's trophy case documents real bugs found by fuzzing in the wild:
serde_json: buffer overflow on malformed inputregex: catastrophic backtrackingimage: heap buffer overflowtoml: parser panics on crafted input
These aren't theoretical bugs. They're real CVEs, real security issues, real crashes in production code — caught before release.
When to Use Fuzzing
Fuzzing shines for:
- Parsers — any code that interprets external input
- Serialization/deserialization — encoding and decoding
- Cryptographic code — constant-time guarantees, key handling
- CLI tools — argument parsing, file processing
It struggles with:
- Code that requires very specific inputs to not crash
- GUI applications
- Network services (use fuzzing on the parsing layer, not the network layer)
Integrating with CI
The real power is continuous fuzzing. Add this to your CI:
# .github/workflows/fuzz.yml
name: Fuzz
on: [push, pull_request]
jobs:
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rust-fuzz/setup-fuzz-action@v1
- run: cargo fuzz build
- run: cargo fuzz run fuzz_target_1 --timeout=60
Run it nightly. Let the fuzzer work while you sleep.
The Bigger Picture
Fuzzing isn't just about finding bugs — it's about confidence. When your parser has been fed millions of random inputs and hasn't crashed, you sleep better. When your serialization code survives coverage-guided mutation, you trust it more.
Rust gives you memory safety by default. Fuzzing gives you input safety — confidence that your code handles the unexpected gracefully.
Try It
Pick a crate you maintain. Any parser, any deserializer, any function that takes external input. Run cargo-fuzz init, write one target, and let it run for five minutes.
You'll either find a bug or gain confidence. Either way, you win.