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:

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:

It struggles with:

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.