moodmosaic

Mocking I/O To Go Faster

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.

The Problem

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.

The Solution: Trait Abstraction

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)
    }
}

Using mockall for Test Doubles

Add 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>;
    }
}

The Fuzzing Harness

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);
});

Key-Value Store Example

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);
});

Performance Impact

Benchmark results:

80x speedup. More iterations, better coverage.

Best Practices

Goal: fast iteration, not perfect simulation.

When Not To Use Test Doubles

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.


Next: CI, Seeds, And Corpora Hygiene