cargo fuzz is a powerful tool that leverages libFuzzer to automatically discover bugs in your Rust code by feeding it malformed or unexpected inputs.

Here’s how it works in practice. Let’s say you have a simple Rust function that parses a string into a custom data structure.

// src/lib.rs
#[derive(Debug, PartialEq)]
pub struct Config {
    pub key: String,
    pub value: u32,
}

pub fn parse_config(input: &str) -> Result<Config, &'static str> {
    let parts: Vec<&str> = input.split('=').collect();
    if parts.len() != 2 {
        return Err("Invalid format: expected key=value");
    }

    let key = parts[0].to_string();
    let value = match parts[1].parse::<u32>() {
        Ok(v) => v,
        Err(_) => return Err("Invalid value: expected unsigned integer"),
    };

    Ok(Config { key, value })
}

To fuzz this, you first need to set up cargo fuzz.

cargo install cargo-fuzz
cargo fuzz build

This creates a fuzz directory in your project. Inside, you’ll find a Cargo.toml and a fuzz.rs file. You’ll modify fuzz.rs to tell cargo fuzz what to fuzz.

// fuzz/fuzz.rs
use arbitrary::{Arbitrary, Unstructured};
use your_crate_name::parse_config; // Replace your_crate_name with your actual crate name

#[derive(Debug, Arbitrary)]
struct FuzzInput<'a> {
    input_string: &'a str,
}

fuzz_target!(|data: &[u8]| {
    let mut unstructured = Unstructured::new(data);
    if let Ok(fuzz_input) = FuzzInput::arbitrary(&mut unstructured) {
        let _ = parse_config(fuzz_input.input_string);
    }
});

The FuzzInput struct uses the arbitrary crate to generate various inputs. The fuzz_target! macro is where the magic happens: it takes raw bytes (data), turns them into a structured input (FuzzInput), and then calls your target function (parse_config). libFuzzer will then try to optimize the generated data to trigger crashes or panics in parse_config.

After setting up fuzz.rs, build your fuzz targets:

cargo fuzz build

Now you can run the fuzzer:

cargo fuzz run fuzz_parse_config

cargo fuzz will start generating inputs and running your parse_config function. If it finds an input that causes a panic or an unrecoverable error, it will stop and save that input to a file (e.g., fuzz/artifacts/fuzz_parse_config/crash-<hash>). You can then use this file to reproduce the bug.

The core problem cargo fuzz and libFuzzer solve is the combinatorial explosion of possible inputs for a given function. Manually writing test cases for every edge case, invalid format, or unexpected character sequence is practically impossible. Fuzzing automates this by intelligently exploring the input space, guided by how often certain inputs trigger new code paths or uncover bugs. It’s like having an infinitely patient, slightly malicious user trying to break your code.

Let’s dive deeper into the mental model. libFuzzer (which cargo fuzz wraps) operates in a feedback loop. It starts with an initial set of seed inputs. For each input, it executes the target function and instruments the code to track which code paths are executed. It then mutates the input in various ways (e.g., changing bytes, adding/removing bytes, bit flips) and re-executes the target function. If a mutation leads to the execution of new code paths (coverage), that mutated input is kept and becomes a basis for further mutations. If a mutation causes a crash (panic, abort, etc.), that input is saved as a "crashing input." This process continues, with libFuzzer prioritizing inputs that increase code coverage, as these are more likely to reveal bugs.

The arbitrary crate is crucial here because libFuzzer provides raw bytes. You need a way to translate those bytes into meaningful data structures that your function expects. arbitrary handles this translation. By deriving Arbitrary for your input structs, you give arbitrary the rules for constructing complex data from flat byte slices. The Unstructured struct is the bridge between the raw bytes and the Arbitrary implementation.

A key aspect of effective fuzzing is providing good seed inputs. If your initial seeds are too simple or don’t cover important input structures, the fuzzer might not explore relevant parts of your code. For parse_config, good seeds would include valid inputs, slightly malformed inputs, inputs with different delimiters, and inputs with non-numeric characters where numbers are expected. You can add seed corpus directories to your fuzz folder.

The one thing most people don’t realize is how aggressively libFuzzer can optimize input generation based on coverage. It’s not just random mutation; it’s a guided search. If a specific sequence of byte changes consistently leads to new code paths being hit, libFuzzer will spend a lot of time exploring variations around that sequence. This is why it can find bugs in surprisingly complex code with relatively few mutations. It learns which parts of the input matter for reaching new states.

The next step after finding and fixing bugs is to integrate your crashing inputs into your regular test suite as regression tests.

Want structured learning?

Take the full Cargo course →