Skip to content

Commit

Permalink
Add support for defining custom mutators
Browse files Browse the repository at this point in the history
This adds support for defining custom mutators, as described in
https://github.com/google/fuzzing/blob/master/docs/structure-aware-fuzzing.md
  • Loading branch information
fitzgen committed May 13, 2021
1 parent b882dd6 commit 70d77fb
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Expand Up @@ -21,4 +21,9 @@ arbitrary-derive = ["arbitrary/derive"]
members = [
"./example",
"./example_arbitrary",
"./example_mutator",
]

[dev-dependencies]
flate2 = "1.0.20"
rand = "0.8.3"
15 changes: 15 additions & 0 deletions ci/script.sh
Expand Up @@ -39,3 +39,18 @@ RUST_LIBFUZZER_DEBUG_PATH=$(pwd)/debug_output \
cat $(pwd)/debug_output
grep -q Rgb $(pwd)/debug_output
popd

pushd ./example_mutator
cargo rustc \
--release \
-- \
-Cpasses='sancov' \
-Cllvm-args=-sanitizer-coverage-level=3 \
-Cllvm-args=-sanitizer-coverage-trace-compares \
-Cllvm-args=-sanitizer-coverage-inline-8bit-counters \
-Cllvm-args=-sanitizer-coverage-stack-depth \
-Cllvm-args=-sanitizer-coverage-trace-geps \
-Cllvm-args=-sanitizer-coverage-prune-blocks=0 \
-Zsanitizer=address
(! $CARGO_TARGET_DIR/release/example_mutator -runs=10000000)
popd
11 changes: 11 additions & 0 deletions example_mutator/Cargo.toml
@@ -0,0 +1,11 @@
[package]
name = "example_mutator"
version = "0.1.0"
authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
flate2 = "1.0.20"
libfuzzer-sys = { path = ".." }
55 changes: 55 additions & 0 deletions example_mutator/src/main.rs
@@ -0,0 +1,55 @@
#![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);

// 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(data: &[u8]) -> Option<Vec<u8>> {
let mut decoder = GzDecoder::new(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")
}
208 changes: 208 additions & 0 deletions src/lib.rs
Expand Up @@ -17,6 +17,8 @@ extern "C" {
// We do not actually cross the FFI bound here.
#[allow(improper_ctypes)]
fn rust_fuzzer_test_input(input: &[u8]);

fn LLVMFuzzerMutate(data: *mut u8, size: usize, max_size: usize) -> usize;
}

#[doc(hidden)]
Expand Down Expand Up @@ -188,3 +190,209 @@ macro_rules! fuzz_target {
}
};
}

/// 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](macro.fuzz_target.html#arbitrary-input-types) 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_run
/// #![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);
///
/// # let first_mutation = |_, _, _, _| todo!();
/// # let second_mutation = |_, _, _, _| todo!();
/// # let third_mutation = |_, _, _, _| todo!();
/// # let fourth_mutation = |_, _, _, _| todo!();
/// // 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_run
/// #![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);
///
/// // 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](https://github.com/google/fuzzing/blob/master/docs/structure-aware-fuzzing.md#example-compression).
///
/// ## 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`](https://serde.rs/) 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.
#[macro_export]
macro_rules! fuzz_mutator {
(
|
$data:ident : &mut [u8] ,
$size:ident : usize ,
$max_size:ident : usize ,
$seed:ident : u32 $(,)*
|
$body:block
) => {
/// Auto-generated function.
#[export_name = "LLVMFuzzerCustomMutator"]
pub fn rust_fuzzer_custom_mutator(
$data: *mut u8,
$size: usize,
$max_size: usize,
$seed: std::os::raw::c_uint,
) -> usize {
// Depending on if we are growing or shrinking the test case, `size`
// might be larger or smaller than `max_size`. The `data`'s capacity
// is the maximum of the two.
let len = std::cmp::max($max_size, $size);
let $data: &mut [u8] = unsafe { std::slice::from_raw_parts_mut($data, len) };

// `unsigned int` is generally a `u32`, but not on all targets. Do
// an infallible (and potentially lossy, but that's okay because it
// preserves determinism) conversion.
let $seed = $seed as u32;

// Truncate the new size if it is larger than the max.
let new_size = { $body };
std::cmp::min(new_size, $max_size)
}
};
}

/// The default `libFuzzer` mutator.
///
/// You generally don't have to use this at all unless you're defining a
/// custom mutator with [the `fuzz_mutator!` macro][crate::fuzz_mutator].
///
/// Mutates `data[..size]` in place and returns the new size of the mutated
/// data.
///
/// # Example
///
/// ```no_run
/// // Create some data in a buffer.
/// let mut data = vec![0; 128];
/// data[..5].copy_from_slice(b"hello");
///
/// // Ask `libFuzzer` to mutate the data.
/// let new_size = libfuzzer_sys::fuzzer_mutate(&mut data, 5);
///
/// // Get the mutated data out of the buffer.
/// let mutated_data = &data[..new_size];
/// ```
pub fn fuzzer_mutate(data: &mut [u8], size: usize) -> usize {
assert!(size <= data.len());
let new_size = unsafe { LLVMFuzzerMutate(data.as_mut_ptr(), size, data.len()) };
assert!(new_size <= data.len());
new_size
}

0 comments on commit 70d77fb

Please sign in to comment.