Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fuzzing

Fuzzing is an approach to testing that generates random inputs for your code and uses instrumentation to monitor which branches are being triggered, with the goal of triggering all branches inside the code. In doing so, it can test your code very thoroughly and often discover edge cases that you might not have thought of when writing unit tests.

The general approach looks something like this: a fuzzer generates a randomized input, feeds it to your program, and monitors the result. If the program crashes or triggers some kind of invalid behaviour, the fuzzer records the failing input. The fuzzer uses code coverage instrumentation to track which branches are taken, and uses this feedback to guide future inputs towards unexplored paths. When a crash is found, the fuzzer attempts to reduce the input to the smallest possible reproducer.

Fuzzing is usually an effective technique for testing parsers. Fuzzing implementations are usually able to use valid, working inputs as a starting point and randomly mutate them to try to find inputs that either crash the program, or lead to some kind of invalid behaviour.

Note

Fuzzing is a popular technique for testing parsers written in memory-unsafe languages. It focusses on trying to reach all branches and testing for invalid behaviour (stack overflows, read or write out of bounds). For this reason, it is often combined with sanitizers. There is even some infrastructure for continuously running fuzzing against popular open-source libraries, done by Google’s [OSS-Fuzz][ossfuzz] project.

Because Rust is a memory-safe language, fuzzing is generally less important. Some places where you might want to use it are:

  • If your code makes a lot of use of unsafe and raw pointer access,
  • If you are trying to test the soundness of a program that interacts with memory-unsafe languages (for example, bindings for a C or C++ library).

Otherwise, it might make more sense for you to look into doing Property testing, which focusses more on testing individual components, and is more focussed on correctness rather than memory safety.

Fuzzing is a very good strategy when your code parses untrusted data. It allows you to have confidence that for any possible input, your program does not misbehave. The downside of fuzzing is that usually, it can only detect crashes. When possible, it is better to test individual pieces of code using property testing.

cargo-fuzz

cargo-fuzz is the most common way to fuzz Rust code. It is a Cargo subcommand that integrates with libFuzzer, the coverage-guided fuzzer built into LLVM. Because the Rust compiler uses LLVM as its backend, libFuzzer can instrument Rust code directly, making the integration seamless.

You can install it from crates.io:

cargo install cargo-fuzz

Initializing a fuzz project creates a fuzz/ directory inside your crate with its own Cargo.toml and a fuzz_targets/ directory for your fuzz targets:

cargo fuzz init

Each fuzz target is a small program that receives arbitrary bytes from the fuzzer and passes them to the code you want to test. For example, if you have a config file parser:

#![allow(unused)]
fn main() {
pub fn parse_config(input: &str) -> Vec<(&str, &str)> {
    let mut result = Vec::new();
    for line in input.lines() {
        if line.is_empty() {
            continue;
        }
        let parts: Vec<&str> = line.split('=').collect();
        if parts.len() != 2 {
            panic!("invalid config line: {}", line);
        }
        result.push((parts[0], parts[1]));
    }
    result
}
}

You can write a fuzz target that feeds arbitrary strings into it:

#![allow(unused)]
#![no_main]

fn main() {
use libfuzzer_sys::fuzz_target;
use fuzzing_example::parse_config;

fuzz_target!(|data: &str| {
    // We don't care about the result, we just want to make
    // sure the parser does not panic on any input.
    let _ = parse_config(data);
});
}

The fuzz_target! macro defines the entry point for the fuzzer. The closure receives data generated by libFuzzer, and you pass it to whatever function you want to test. In this case, the fuzzer will quickly discover inputs that cause the parser to panic, for example a line containing multiple = characters like a=b=c.

You run the fuzzer with:

cargo fuzz run fuzz_parse_config

The fuzzer will run indefinitely, printing status updates as it explores new code paths. When it finds a crash, it writes the failing input to fuzz/artifacts/fuzz_parse_config/ and prints the path. You can then reproduce the crash with:

cargo fuzz run fuzz_parse_config fuzz/artifacts/fuzz_parse_config/<artifact>

Note

cargo-fuzz requires a nightly Rust compiler because it relies on LLVM’s sanitizer instrumentation, which is not yet stabilized. You can use it with cargo +nightly fuzz run ... or by setting your project to use nightly via a rust-toolchain.toml file.

Structured Fuzzing with Arbitrary

By default, the fuzzer provides raw bytes (&[u8] or &str). For more complex inputs, you can derive the Arbitrary trait from the arbitrary crate, which lets the fuzzer generate structured data directly. This is useful when your code expects a specific input type rather than raw bytes.

#![allow(unused)]
fn main() {
use libfuzzer_sys::arbitrary::Arbitrary;

#[derive(Arbitrary, Debug)]
struct Config {
    timeout: u32,
    retries: u8,
    verbose: bool,
}

fuzz_target!(|config: Config| {
    // test with structured input
    apply_config(&config);
});
}

This approach tends to be more effective than raw byte fuzzing for code that doesn’t directly parse bytes, because the fuzzer doesn’t waste time generating inputs that fail to deserialize.

Corpus Management

The fuzzer maintains a corpus of interesting inputs: ones that triggered new code paths. Over time, this corpus grows and helps the fuzzer explore deeper into your code. You can seed the corpus with known valid inputs to give it a head start:

mkdir -p fuzz/corpus/fuzz_parse_config
echo "key=value" > fuzz/corpus/fuzz_parse_config/seed1.txt
cargo fuzz run fuzz_parse_config

You can also minimize the corpus periodically to remove redundant entries:

cargo fuzz cmin fuzz_parse_config

afl.rs

afl.rs is a Rust wrapper around American Fuzzy Lop (AFL), one of the original coverage-guided fuzzers. AFL takes a different approach than libFuzzer: it works by forking your program for each test case rather than calling a function in a loop. This makes it somewhat slower per iteration, but it can catch issues that cause the entire process to hang or enter infinite loops, which libFuzzer cannot easily detect.

AFL also comes with a set of companion tools for corpus management and crash triage. It has a distinctive terminal UI that displays real-time statistics about the fuzzing campaign, including execution speed, code coverage, and crash counts.

In general, cargo-fuzz (libFuzzer) is the more common choice in the Rust ecosystem and is easier to set up. afl.rs is worth considering if you need its specific capabilities, such as hang detection, or if you want to run both fuzzers in parallel for better coverage.

When to use Fuzzing

Fuzzing is most valuable when your code handles untrusted or complex input. Good candidates for fuzzing include:

  • Parsers for file formats, network protocols, or configuration files
  • Serialization and deserialization code
  • Compression and decompression libraries
  • Cryptographic implementations
  • Any code with significant unsafe blocks

For pure Rust code without unsafe, fuzzing still catches panics (unwrap failures, index out of bounds, arithmetic overflow in debug mode) and logic bugs that manifest as crashes. If you are more interested in testing correctness properties rather than crash-freedom, property testing is often a better fit.

Reading

Rust-Fuzz Book by Rust Fuzz Book

This book explains what fuzz testing is, and how it can be implemented in Rust using afl.rs and cargo-fuzz.

Yevgeny explains why you should fuzz your Rust code, and shows you how to do it in GitLab. GitLab has some features that make running fuzzing inside GitLab CI quite convenient.

Fuzzing Solana by Addison Crump

Addison shows how Rust can be used to fuzz the Solana eBPF JIT compiler, and outlines the security vulnerabilities found using this approach.

What is my fuzzer doing? by Tweede Golf

This article explores how to understand and interpret what a fuzzer is doing during a campaign, including how to read coverage data and identify areas where the fuzzer is getting stuck.

Effective Fuzzing: a dav1d case study by Google Project Zero

A detailed case study on fuzzing the dav1d AV1 decoder. Demonstrates the practical impact of fuzzing on a real-world, performance-critical codec and the kinds of bugs it uncovers.

Explains how coverage-guided fuzzing works under the hood, and how extending the instrumentation beyond basic block coverage can improve fuzzing effectiveness.