This post is part of the Input Coverage > Code Coverage series.
Fuzzers need speed. I/O is slow. Use test doubles.
Test doubles replace real dependencies with fake implementations. See Gerard Meszaros’ xUnit Test Patterns book for the full taxonomy.
I/O kills fuzzer speed. File reads take milliseconds. Fuzzers need thousands of iterations per second.
Solution: inject test doubles instead of direct I/O calls.
Consider this function that processes configuration files:
use std::fs;
pub fn process_config(path: &str, data: &[u8]) -> Result<bool, Box<dyn std::error::Error>> {
let config = fs::read_to_string(path)?;
let lines: Vec<&str> = config.lines().collect();
if data.len() < 4 {
return Ok(false);
}
let key = std::str::from_utf8(&data[..4])?;
Ok(lines.iter().any(|line| line.contains(key)))
}
Fuzzing this function means hitting the filesystem on every iteration. Even with an SSD, you’re looking at ~100 executions per second instead of the 10,000+ you want.
Wrap I/O in a trait:
pub trait ConfigReader {
type Error;
fn read_config(&self, path: &str) -> Result<String, Self::Error>;
}
pub fn process_config_with_reader<R: ConfigReader>(
reader: &R,
path: &str,
data: &[u8],
) -> Result<bool, Box<dyn std::error::Error>>
where
R::Error: std::error::Error + 'static,
{
let config = reader.read_config(path)?;
let lines: Vec<&str> = config.lines().collect();
if data.len() < 4 {
return Ok(false);
}
let key = std::str::from_utf8(&data[..4])?;
Ok(lines.iter().any(|line| line.contains(key)))
}
// Production implementation.
pub struct FileReader;
impl ConfigReader for FileReader {
type Error = std::io::Error;
fn read_config(&self, path: &str) -> Result<String, Self::Error> {
std::fs::read_to_string(path)
}
}
mockall
for Test DoublesAdd mockall
to your Cargo.toml
:
[dev-dependencies]
mockall = "0.11"
Generate test doubles automatically:
use mockall::automock;
#[automock]
pub trait ConfigReader {
type Error;
fn read_config(&self, path: &str) -> Result<String, Self::Error>;
}
automock
breaks with associated types. Use manual test doubles:
use mockall::mock;
mock! {
pub ConfigReader {}
impl ConfigReader for ConfigReader {
type Error = std::io::Error;
fn read_config(&self, path: &str) -> Result<String, std::io::Error>;
}
}
Now your fuzzer runs at full speed with a stub:
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let mut stub = MockConfigReader::new();
// Set up deterministic stub behavior.
stub.expect_read_config()
.returning(|_| Ok("key1=value1\nkey2=value2\nspecial=config".to_string()));
let _ = process_config_with_reader(&stub, "dummy.conf", data);
});
For more complex I/O patterns, like database access:
use mockall::mock;
mock! {
pub Kv {}
impl Kv for Kv {
type Err = std::io::Error;
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, std::io::Error>;
fn set(&self, key: &[u8], value: &[u8]) -> Result<(), std::io::Error>;
}
}
pub fn process_with_kv<K: Kv>(kv: &K, data: &[u8]) -> bool {
if data.len() < 8 {
return false;
}
let key = &data[..8];
let value = &data[8..];
// Try to get existing value.
if let Ok(Some(existing)) = kv.get(key) {
return existing == value;
}
// Store new value.
kv.set(key, value).is_ok()
}
// Fuzzing harness with fake.
fuzz_target!(|data: &[u8]| {
let mut fake = MockKv::new();
fake.expect_get()
.returning(|key| {
if key == b"deadbeef" {
Ok(Some(b"cached".to_vec()))
} else {
Ok(None)
}
});
fake.expect_set()
.returning(|_, _| Ok(()));
let _ = process_with_kv(&fake, data);
});
Benchmark results:
80x speedup. More iterations, better coverage.
Goal: fast iteration, not perfect simulation.
Skip test doubles when testing integration layers or hunting filesystem/network bugs. Use real I/O to stress file handles, network timeouts, disk errors. Run these slower harnesses separately.