Fuzzing is an automated testing technique that feeds random or mutated inputs to your code to find bugs, crashes, and security vulnerabilities.
Fuzzing helps find:
Hypothesis generates test inputs automatically to verify properties of your code. This template uses Hypothesis as the primary fuzzing tool for Python.
Already included in dev dependencies:
[project.optional-dependencies]
dev = [
"hypothesis>=6.92.0",
]
from hypothesis import given, strategies as st
@given(st.integers(), st.integers())
def test_add_commutative(a: int, b: int) -> None:
"""Addition should be commutative: a + b = b + a"""
assert add(a, b) == add(b, a)
Common strategies for input generation:
import hypothesis.strategies as st
# Basic types
st.integers() # Any integer
st.integers(min_value=0) # Non-negative
st.floats() # Any float
st.text() # Any string
st.booleans() # True/False
# Collections
st.lists(st.integers()) # Lists of integers
st.lists(st.text(), min_size=1) # Non-empty lists
# Custom types
st.builds(MyClass) # Instances of MyClass
# Run all hypothesis tests
uv run pytest tests/fuzzing/test_hypothesis_*.py -v
# With more examples
uv run pytest tests/fuzzing/ --hypothesis-seed=42
# CI profile (more thorough)
uv run pytest tests/fuzzing/ --hypothesis-profile=ci
In pyproject.toml:
[tool.pytest.ini_options]
addopts = [
"--hypothesis-show-statistics",
]
cargo-fuzz provides structure-aware fuzzing backed by LLVM’s libFuzzer.
# Install cargo-fuzz
cargo install cargo-fuzz
# Initialize (if not already done)
cd rust
cargo fuzz init
Create fuzz/fuzz_targets/fuzz_calculator.rs:
#![no_main]
use libfuzzer_sys::fuzz_target;
use centaur_example::calculator::{add, divide};
fuzz_target!(|data: &[u8]| {
if data.len() < 8 {
return;
}
let a = i32::from_le_bytes([data[0], data[1], data[2], data[3]]);
let b = i32::from_le_bytes([data[4], data[5], data[6], data[7]]);
let _ = add(a, b);
if b != 0 {
match divide(a, b) {
Ok(_) => {},
Err(_) => panic!("divide should succeed for non-zero"),
}
}
});
# List targets
cargo fuzz list
# Run target
cargo +nightly fuzz run fuzz_calculator
# Run for specific time (seconds)
cargo +nightly fuzz run fuzz_calculator -- -max_total_time=60
# Run with memory limit
cargo +nightly fuzz run fuzz_calculator -- -rss_limit_mb=2048
# Use specific corpus
cargo +nightly fuzz run fuzz_calculator corpus/
# Address Sanitizer (memory errors)
cargo +nightly fuzz run fuzz_calculator -- -sanitizer=address
# Undefined Behavior Sanitizer
cargo +nightly fuzz run fuzz_calculator -- -sanitizer=undefined
Alternative fuzzer with different fuzzing strategies.
cargo install honggfuzz
# Run honggfuzz
cargo hfuzz run hfuzz_target
# With coverage
HFUZZ_RUN_ARGS="--coverage" cargo hfuzz run hfuzz_target
Similar to Hypothesis but for Rust.
In Cargo.toml:
[dev-dependencies]
proptest = "1.4"
use proptest::prelude::*;
proptest! {
#[test]
fn test_add_commutative(a: i32, b: i32) {
assert_eq!(add(a, b), add(b, a));
}
#[test]
fn test_divide_reciprocal(a in 1..1000i32, b in 1..1000i32) {
let result = divide(a, b).unwrap();
assert_eq!(result, a / b);
}
}
Fuzzing runs on a schedule or manual trigger:
# Runs fuzzing for limited time
- name: Run fuzzing
run: |
timeout 60 cargo +nightly fuzz run fuzz_target || true
For production projects:
Both fuzzers automatically save inputs that increase coverage:
fuzz/corpus/fuzz_target/
├── artifact1
├── artifact2
└── ...
# cargo-fuzz
cargo fuzz run target corpus/
# atheris
python fuzz_script.py corpus/
# cargo-fuzz
cargo fuzz cmin target
# Reduces corpus to minimal set covering all coverage
When a crash is found, artifacts are saved:
# cargo-fuzz
cargo fuzz run target fuzz/artifacts/target/crash-xxx
# atheris
python fuzz_script.py < artifacts/crash-xxx
# Run with debugger
cargo fuzz run --debug target artifacts/crash-xxx
# Get stack trace
RUST_BACKTRACE=1 cargo fuzz run target artifacts/crash-xxx
# cargo-fuzz coverage report
cargo fuzz coverage target
# Generate HTML report
cargo cov -- show target/...
Fuzzers show: