Finding bugs with cargo-fuzz
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.
Fuzzing helps discover:
// 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
# 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
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),
}
});
| 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 |
unwrap(), expect(), index out of bounds// 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
ArbitraryRun this code in the official Rust Playground