Fuzzing

Finding bugs with cargo-fuzz

advanced
fuzzingcargo-fuzzsecurity
🎮 Interactive Playground

What is Fuzzing?

Fuzzing (fuzz testing) automatically generates malformed or random inputs to find crashes, memory errors, and security vulnerabilities. It's particularly effective at finding edge cases that humans miss.

The Problem

Fuzzing helps discover:

  • Memory safety issues: Buffer overflows, use-after-free
  • Panics: Unexpected unwrap failures, index out of bounds
  • Logic errors: Invalid state transitions
  • Parsing bugs: Malformed input handling

Example Code

// src/lib.rs - Code to fuzz

/// Parse a simple key=value format
pub fn parse_config(input: &str) -> Result<Vec<(String, String)>, ParseError> {
    let mut result = Vec::new();

    for line in input.lines() {
        let line = line.trim();

        // Skip empty lines and comments
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        // Parse key=value
        let (key, value) = line.split_once('=')
            .ok_or(ParseError::MissingEquals)?;

        let key = key.trim();
        let value = value.trim();

        if key.is_empty() {
            return Err(ParseError::EmptyKey);
        }

        result.push((key.to_string(), value.to_string()));
    }

    Ok(result)
}

#[derive(Debug, PartialEq)]
pub enum ParseError {
    MissingEquals,
    EmptyKey,
}

/// Parse a simple expression: number op number
pub fn evaluate_expr(input: &str) -> Result<i64, ExprError> {
    let input = input.trim();

    // Find operator
    let op_pos = input.find(|c| c == '+' || c == '-' || c == '*' || c == '/')
        .ok_or(ExprError::NoOperator)?;

    let left: i64 = input[..op_pos].trim()
        .parse()
        .map_err(|_| ExprError::InvalidNumber)?;

    let op = input.chars().nth(op_pos).unwrap();

    let right: i64 = input[op_pos + 1..].trim()
        .parse()
        .map_err(|_| ExprError::InvalidNumber)?;

    match op {
        '+' => Ok(left.checked_add(right).ok_or(ExprError::Overflow)?),
        '-' => Ok(left.checked_sub(right).ok_or(ExprError::Overflow)?),
        '*' => Ok(left.checked_mul(right).ok_or(ExprError::Overflow)?),
        '/' => {
            if right == 0 {
                Err(ExprError::DivisionByZero)
            } else {
                Ok(left / right)
            }
        }
        _ => Err(ExprError::InvalidOperator),
    }
}

#[derive(Debug, PartialEq)]
pub enum ExprError {
    NoOperator,
    InvalidNumber,
    InvalidOperator,
    DivisionByZero,
    Overflow,
}

/// Deserialize a custom binary format
pub fn parse_binary_message(data: &[u8]) -> Result<Message, BinaryError> {
    if data.len() < 4 {
        return Err(BinaryError::TooShort);
    }

    // First byte: message type
    let msg_type = data[0];

    // Bytes 1-2: payload length (big endian)
    let payload_len = u16::from_be_bytes([data[1], data[2]]) as usize;

    // Byte 3: flags
    let flags = data[3];

    // Rest: payload
    if data.len() < 4 + payload_len {
        return Err(BinaryError::IncompletePayload);
    }

    let payload = data[4..4 + payload_len].to_vec();

    Ok(Message {
        msg_type,
        flags,
        payload,
    })
}

#[derive(Debug, PartialEq)]
pub struct Message {
    pub msg_type: u8,
    pub flags: u8,
    pub payload: Vec<u8>,
}

#[derive(Debug, PartialEq)]
pub enum BinaryError {
    TooShort,
    IncompletePayload,
}

/// URL parser (simplified)
pub fn parse_url(input: &str) -> Result<Url, UrlError> {
    let input = input.trim();

    // Find scheme
    let scheme_end = input.find("://").ok_or(UrlError::MissingScheme)?;
    let scheme = &input[..scheme_end];

    if !matches!(scheme, "http" | "https" | "ftp") {
        return Err(UrlError::InvalidScheme);
    }

    let rest = &input[scheme_end + 3..];

    // Find host (until / or end)
    let (host, path) = match rest.find('/') {
        Some(pos) => (&rest[..pos], &rest[pos..]),
        None => (rest, "/"),
    };

    if host.is_empty() {
        return Err(UrlError::EmptyHost);
    }

    Ok(Url {
        scheme: scheme.to_string(),
        host: host.to_string(),
        path: path.to_string(),
    })
}

#[derive(Debug, PartialEq)]
pub struct Url {
    pub scheme: String,
    pub host: String,
    pub path: String,
}

#[derive(Debug, PartialEq)]
pub enum UrlError {
    MissingScheme,
    InvalidScheme,
    EmptyHost,
}

// Unit tests to verify basic functionality
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_config() {
        let input = "key=value\nfoo=bar";
        let result = parse_config(input).unwrap();
        assert_eq!(result.len(), 2);
    }

    #[test]
    fn test_evaluate_expr() {
        assert_eq!(evaluate_expr("2 + 3"), Ok(5));
        assert_eq!(evaluate_expr("10 / 0"), Err(ExprError::DivisionByZero));
    }

    #[test]
    fn test_parse_binary() {
        let data = [0x01, 0x00, 0x03, 0x00, b'a', b'b', b'c'];
        let msg = parse_binary_message(&data).unwrap();
        assert_eq!(msg.payload, vec![b'a', b'b', b'c']);
    }

    #[test]
    fn test_parse_url() {
        let url = parse_url("https://example.com/path").unwrap();
        assert_eq!(url.scheme, "https");
        assert_eq!(url.host, "example.com");
    }
}
// fuzz/fuzz_targets/fuzz_config.rs
// Run with: cargo +nightly fuzz run fuzz_config

#![no_main]
use libfuzzer_sys::fuzz_target;
use my_crate::parse_config;

fuzz_target!(|data: &[u8]| {
    // Try to parse random bytes as UTF-8 string
    if let Ok(s) = std::str::from_utf8(data) {
        // Should not panic regardless of input
        let _ = parse_config(s);
    }
});
// fuzz/fuzz_targets/fuzz_expr.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use my_crate::evaluate_expr;

fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        // Should handle any string without panicking
        let _ = evaluate_expr(s);
    }
});
// fuzz/fuzz_targets/fuzz_binary.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use my_crate::parse_binary_message;

fuzz_target!(|data: &[u8]| {
    // Should handle any byte sequence without panicking
    let _ = parse_binary_message(data);
});
// fuzz/fuzz_targets/fuzz_url.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use my_crate::parse_url;

fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        let _ = parse_url(s);
    }
});
# fuzz/Cargo.toml
[package]
name = "my_crate-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
arbitrary = { version = "1", features = ["derive"] }

[dependencies.my_crate]
path = ".."

[[bin]]
name = "fuzz_config"
path = "fuzz_targets/fuzz_config.rs"
test = false
doc = false

[[bin]]
name = "fuzz_expr"
path = "fuzz_targets/fuzz_expr.rs"
test = false
doc = false

[[bin]]
name = "fuzz_binary"
path = "fuzz_targets/fuzz_binary.rs"
test = false
doc = false

[[bin]]
name = "fuzz_url"
path = "fuzz_targets/fuzz_url.rs"
test = false
doc = false

Setting Up cargo-fuzz

# Install cargo-fuzz (requires nightly)
cargo install cargo-fuzz

# Initialize fuzzing for your project
cargo fuzz init

# Create a new fuzz target
cargo fuzz add fuzz_config

# Run the fuzzer
cargo +nightly fuzz run fuzz_config

# Run for specific time
cargo +nightly fuzz run fuzz_config -- -max_total_time=60

# See coverage
cargo +nightly fuzz coverage fuzz_config

Why This Works

  1. Mutation-based: Mutates inputs that triggered new code paths
  2. Coverage-guided: Focuses on inputs that explore new branches
  3. Continuous: Runs until stopped, finding deeper bugs over time
  4. Reproducible: Crashes are saved for reproduction

Structured Fuzzing with Arbitrary

use arbitrary::{Arbitrary, Unstructured};

#[derive(Debug, Arbitrary)]
struct FuzzInput {
    operation: Operation,
    value: i32,
    data: Vec<u8>,
}

#[derive(Debug, Arbitrary)]
enum Operation {
    Add,
    Remove,
    Update,
}

fuzz_target!(|input: FuzzInput| {
    // Fuzz with structured input instead of raw bytes
    match input.operation {
        Operation::Add => handle_add(input.value),
        Operation::Remove => handle_remove(input.value),
        Operation::Update => handle_update(input.value, &input.data),
    }
});

Fuzzing Best Practices

| Practice | Description |

|----------|-------------|

| Start simple | Fuzz parsing functions first |

| Use assertions | Add debug_assert! for invariants |

| Seed corpus | Provide valid inputs as starting points |

| Run continuously | Fuzz in CI, overnight, etc. |

| Fix immediately | Crashes indicate real bugs |

⚠️ Common Findings

  • Panics: unwrap(), expect(), index out of bounds
  • Integer overflow: In debug mode, these panic
  • Infinite loops: Fuzzer will timeout
  • Memory issues: With ASan, finds use-after-free, leaks

⚠️ Anti-patterns

// DON'T: Ignore fuzzer findings
fuzz_target!(|data: &[u8]| {
    let _ = std::panic::catch_unwind(|| {
        parse(data); // Hiding panics defeats the purpose!
    });
});

// DO: Let panics propagate
fuzz_target!(|data: &[u8]| {
    let _ = parse(data); // Return Result, don't panic
});

// DON'T: Fuzz pure computation
fuzz_target!(|x: u64| {
    let _ = x + 1; // Nothing interesting to find
});

// DO: Fuzz parsing, state machines, complex logic

Exercises

  1. Add fuzzing to your project's parser
  2. Create a structured fuzz target using Arbitrary
  3. Set up continuous fuzzing in CI (OSS-Fuzz style)
  4. Use coverage data to improve test corpus

🎮 Try it Yourself

🎮

Fuzzing - Playground

Run this code in the official Rust Playground