libfuzzer_sys

Macro fuzz_mutator

Source
macro_rules! fuzz_mutator {
    (
        |
        $data:ident : &mut [u8] ,
        $size:ident : usize ,
        $max_size:ident : usize ,
        $seed:ident : u32 $(,)*
        |
        $body:block
    ) => { ... };
}
Expand description

Define a custom mutator.

This is optional, and libFuzzer will use its own, default mutation strategy if this is not provided.

You might consider using a custom mutator when your fuzz target is very particular about the shape of its input:

  • You want to fuzz “deeper” than just the parser.
  • The input contains checksums that have to match the hash of some subset of the data or else the whole thing is invalid, and therefore mutating any of that subset means you need to recompute the checksums.
  • Small random changes to the input buffer make it invalid.

That is, a custom mutator is useful in similar situations where a T: Arbitrary input type is useful. Note that the two approaches are not mutually exclusive; you can use whichever is easier for your problem domain or both!

§Implementation Contract

The original, unmodified input is given in data[..size].

You must modify the data in place and return the new size.

The new size should not be greater than max_size. If this is not the case, then the data will be truncated to fit within max_size. Note that max_size < size is possible when shrinking test cases.

You must produce the same mutation given the same seed. Generally, when choosing what kind of mutation to make or where to mutate, you should start by creating a random number generator (RNG) that is seeded with the given seed and then consult the RNG whenever making a decision:

#![no_main]

use rand::{rngs::StdRng, Rng, SeedableRng};

libfuzzer_sys::fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, seed: u32| {
    let mut rng = StdRng::seed_from_u64(seed as u64);

    // Choose which of our four supported kinds of mutations we want to make.
    match rng.gen_range(0..4) {
        0 => first_mutation(rng, data, size, max_size),
        1 => second_mutation(rng, data, size, max_size),
        2 => third_mutation(rng, data, size, max_size),
        3 => fourth_mutation(rng, data, size, max_size),
        _ => unreachable!()
    }
});

§Example: Compression

Consider a simple fuzz target that takes compressed data as input, decompresses it, and then asserts that the decompressed data doesn’t begin with “boom”. It is difficult for libFuzzer (or any other fuzzer) to crash this fuzz target because nearly all mutations it makes will invalidate the compression format. Therefore, we use a custom mutator that decompresses the raw input, mutates the decompressed data, and then recompresses it. This allows libFuzzer to quickly discover crashing inputs.

#![no_main]

use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use libfuzzer_sys::{fuzz_mutator, fuzz_target};
use std::io::{Read, Write};

fuzz_target!(|data: &[u8]| {
    // Decompress the input data and crash if it starts with "boom".
    if let Some(data) = decompress(data) {
        if data.starts_with(b"boom") {
            panic!();
        }
    }
});

fuzz_mutator!(
    |data: &mut [u8], size: usize, max_size: usize, _seed: u32| {
        // Decompress the input data. If that fails, use a dummy value.
        let mut decompressed = decompress(&data[..size]).unwrap_or_else(|| b"hi".to_vec());

        // Mutate the decompressed data with `libFuzzer`'s default mutator. Make
        // the `decompressed` vec's extra capacity available for insertion
        // mutations via `resize`.
        let len = decompressed.len();
        let cap = decompressed.capacity();
        decompressed.resize(cap, 0);
        let new_decompressed_size = libfuzzer_sys::fuzzer_mutate(&mut decompressed, len, cap);

        // Recompress the mutated data.
        let compressed = compress(&decompressed[..new_decompressed_size]);

        // Copy the recompressed mutated data into `data` and return the new size.
        let new_size = std::cmp::min(max_size, compressed.len());
        data[..new_size].copy_from_slice(&compressed[..new_size]);
        new_size
    }
);

fn decompress(compressed_data: &[u8]) -> Option<Vec<u8>> {
    let mut decoder = GzDecoder::new(compressed_data);
    let mut decompressed = Vec::new();
    if decoder.read_to_end(&mut decompressed).is_ok() {
        Some(decompressed)
    } else {
        None
    }
}

fn compress(data: &[u8]) -> Vec<u8> {
    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
    encoder
        .write_all(data)
        .expect("writing into a vec is infallible");
    encoder.finish().expect("writing into a vec is infallible")
}

This example is inspired by a similar example from the official libFuzzer docs.

§More Example Ideas

  • A PNG custom mutator that decodes a PNG, mutates the image, and then re-encodes the mutated image as a new PNG.

  • A serde custom mutator that deserializes your structure, mutates it, and then reserializes it.

  • A Wasm binary custom mutator that inserts, replaces, and removes a bytecode instruction in a function’s body.

  • An HTTP request custom mutator that inserts, replaces, and removes a header from an HTTP request.