Fuzz Testing
Ensuring Batin handles any input without panicking.
Why Fuzz?
Fuzz testing finds bugs that humans miss:
What fuzzing catches:
- Buffer overflows
- Integer overflows
- Panics on edge cases
- Infinite loops
- Memory leaks
Setup
Install cargo-fuzz
# Requires nightly Rust
rustup install nightly
# Install cargo-fuzz
cargo install cargo-fuzz
Project Structure
fuzz/
├── Cargo.toml # Fuzz targets configuration
└── fuzz_targets/
└── fuzz_detect.rs # Main fuzz target
Fuzz Target
// fuzz/fuzz_targets/fuzz_detect.rs
#![no_main]
use batin::{DetectionConfig, FileType};
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
// The fuzzer generates random byte sequences
// We want to ensure no panics regardless of input
let config = DetectionConfig::default();
// This should NEVER panic
let _ = FileType::from_bytes(data, &config);
});
Running the Fuzzer
Basic Run
cargo +nightly fuzz run fuzz_detect
Time-Limited Run
# Run for 5 minutes
cargo +nightly fuzz run fuzz_detect -- -max_total_time=300
Corpus Mode
# Use existing corpus of test inputs
cargo +nightly fuzz run fuzz_detect -- corpus/
Fuzz Targets
Target 1: Basic Detection
fuzz_target!(|data: &[u8]| {
let config = DetectionConfig::default();
let _ = FileType::from_bytes(data, &config);
});
Target 2: Entropy Analysis
fuzz_target!(|data: &[u8]| {
let _ = batin::detection::calculate_shannon_entropy(data);
let _ = batin::detection::chi_square_test(data);
let _ = batin::detection::analyze_entropy(data, 7.2);
});
Target 3: Polyglot Detection
fuzz_target!(|data: &[u8]| {
let db = batin::detection::SignatureDatabase::default();
let _ = batin::detection::detect_polyglot(data, &db);
});
Target 4: Configuration Variations
#[derive(Debug, Arbitrary)]
struct FuzzConfig {
max_read_bytes: u16,
enable_entropy: bool,
enable_polyglot: bool,
enable_embedded: bool,
}
fuzz_target!(|input: (FuzzConfig, &[u8])| {
let (fuzz_config, data) = input;
let config = DetectionConfig {
max_read_bytes: fuzz_config.max_read_bytes as usize,
enable_entropy: fuzz_config.enable_entropy,
enable_polyglot: fuzz_config.enable_polyglot,
enable_embedded: fuzz_config.enable_embedded,
..Default::default()
};
let _ = FileType::from_bytes(data, &config);
});
Interpreting Results
Normal Output
#1234567 BINGO; ... corpus: 42/456Kb
#1234567- Iterations completedcorpus: 42- Unique inputs found- No crashes = success!
Crash Found
==12345== ERROR: libFuzzer: deadly signal
SUMMARY: libFuzzer: deadly signal
MS: 2 InsertByte-ChangeByte-; base unit: abc...
artifact_prefix='./fuzz/artifacts/fuzz_detect/'; Test unit written to ./fuzz/artifacts/fuzz_detect/crash-xyz
The crash input is saved for reproduction.
Reproducing Crashes
Run Specific Input
cargo +nightly fuzz run fuzz_detect -- fuzz/artifacts/fuzz_detect/crash-xyz
Minimize Crash Input
cargo +nightly fuzz tmin fuzz_detect -- fuzz/artifacts/fuzz_detect/crash-xyz
This finds the smallest input that still triggers the crash.
Continuous Fuzzing
GitHub Actions Integration
# .github/workflows/fuzz.yml
name: Fuzz Testing
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight
jobs:
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install nightly
run: rustup install nightly
- name: Install cargo-fuzz
run: cargo install cargo-fuzz
- name: Run fuzzer
run: cargo +nightly fuzz run fuzz_detect -- -max_total_time=600
- name: Upload crashes
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crashes
path: fuzz/artifacts/
OSS-Fuzz Integration
Batin can be integrated with OSS-Fuzz for continuous public fuzzing:
# fuzz/oss-fuzz/Dockerfile
FROM gcr.io/oss-fuzz-base/base-builder-rust
RUN git clone https://github.com/ahmeddwalid/batin
WORKDIR batin
COPY build.sh $SRC/
Coverage-Guided Fuzzing
Generate Coverage Report
# Run with coverage
cargo +nightly fuzz coverage fuzz_detect
# View coverage
cargo cov -- show target/*/coverage/fuzz_detect --format=html
This shows which code paths the fuzzer has explored.
Best Practices
1. Start Simple
// Start with basic input
fuzz_target!(|data: &[u8]| {
let _ = some_function(data);
});
2. Add Complexity Gradually
// Then add structured input
#[derive(Arbitrary)]
struct Input {
header: [u8; 8],
body: Vec<u8>,
}
fuzz_target!(|input: Input| {
// ...
});
3. Seed the Corpus
Add known valid files to fuzz/corpus/fuzz_detect/:
- Valid PDFs, PNGs, EXEs
- Edge cases
- Previous crash inputs
4. Run Regularly
- Daily CI runs
- Before releases
- After major changes
Fixing Fuzz Bugs
Example: Panic on Short Input
Crash input: [0x89, 0x50] (2 bytes)
Root cause:
// BAD: Assumes at least 8 bytes
let slice = &data[0..8];
Fix:
// GOOD: Check length first
if data.len() < 8 {
return Err(DetectionError::Unsupported);
}
let slice = &data[0..8];
Example: Integer Overflow
Crash input: Large file with specific structure
Root cause:
// BAD: Can overflow
let total = count * size;
Fix:
// GOOD: Checked arithmetic
let total = count.checked_mul(size)
.ok_or(DetectionError::CorruptedStructure("overflow"))?;
Zero Panics Guarantee
Batin's #![forbid(unsafe_code)] and fuzz testing together ensure:
- No crashes on any input
- Safe handling of malformed files
- Attackers cannot cause DoS via crafted files
This is critical for a security tool processing untrusted content.