This crate contains oracles, generators, and fuzz targets for use with fuzzing engines (e.g. libFuzzer). This doesn't contain the actual `libfuzzer_sys::fuzz_target!` definitions (those are in the `peepmatic-fuzz` crate) but does those definitions are one liners calling out to functions defined in this crate.
120 lines
4.2 KiB
Rust
120 lines
4.2 KiB
Rust
//! Utilities for fuzzing.
|
|
//!
|
|
//! The actual fuzz targets are defined in `peepmatic/fuzz/*`. This crate just
|
|
//! has oracles and generators for fuzzing.
|
|
|
|
#![deny(missing_debug_implementations)]
|
|
#![deny(missing_docs)]
|
|
|
|
use arbitrary::{Arbitrary, Unstructured};
|
|
use rand::prelude::*;
|
|
use std::fmt::Debug;
|
|
use std::panic;
|
|
use std::time;
|
|
|
|
pub mod automata;
|
|
pub mod compile;
|
|
pub mod interp;
|
|
pub mod parser;
|
|
|
|
/// A quickcheck-style runner for fuzz targets.
|
|
///
|
|
/// This is *not* intended to replace a long-running, coverage-guided fuzzing
|
|
/// engine like libFuzzer! This is only for defining quick, purely random tests
|
|
/// for use with `cargo test` and CI.
|
|
pub fn check<A>(mut f: impl FnMut(A))
|
|
where
|
|
A: Clone + Debug + Arbitrary,
|
|
{
|
|
let seed = rand::thread_rng().gen();
|
|
let mut rng = rand::rngs::SmallRng::seed_from_u64(seed);
|
|
|
|
const INITIAL_LENGTH: usize = 16;
|
|
const MAX_LENGTH: usize = 4096;
|
|
|
|
let mut buf: Vec<u8> = (0..INITIAL_LENGTH).map(|_| rng.gen()).collect();
|
|
let mut num_checked = 0;
|
|
|
|
let time_budget = time::Duration::from_secs(2);
|
|
let then = time::Instant::now();
|
|
|
|
let (failing_input, panic_info) = loop {
|
|
if num_checked > 0 && time::Instant::now().duration_since(then) > time_budget {
|
|
eprintln!("Checked {} random inputs.", num_checked);
|
|
return;
|
|
}
|
|
|
|
match <A as Arbitrary>::arbitrary_take_rest(Unstructured::new(&buf)) {
|
|
Ok(input) => {
|
|
num_checked += 1;
|
|
eprintln!("Checking input: {:#?}", input);
|
|
if let Err(p) = panic::catch_unwind(panic::AssertUnwindSafe(|| f(input.clone()))) {
|
|
break (input, p);
|
|
}
|
|
}
|
|
Err(e @ arbitrary::Error::NotEnoughData) => {
|
|
eprintln!("warning: {}", e);
|
|
if *buf.last().unwrap() == 0 {
|
|
if buf.len() < MAX_LENGTH {
|
|
let new_size = std::cmp::min(buf.len() * 2, MAX_LENGTH);
|
|
eprintln!("Growing buffer size to {}", new_size);
|
|
let delta = new_size - buf.len();
|
|
buf.reserve(delta);
|
|
for _ in 0..delta {
|
|
buf.push(rng.gen());
|
|
}
|
|
continue;
|
|
} else {
|
|
// Regenerate `buf` in the loop below and see if that
|
|
// fixes things...
|
|
eprintln!("Regenerating buffer data.");
|
|
}
|
|
} else {
|
|
// Shrink values in the end of `buf`, which is where
|
|
// `Arbitrary` pulls container lengths from. Then try again.
|
|
eprintln!("Shrinking buffer's tail values.");
|
|
let i = (buf.len() as f64).sqrt() as usize;
|
|
for j in i..buf.len() {
|
|
buf[j] /= 2;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("warning: {}", e);
|
|
// Usually this happens because `A` requires a sequence utf-8
|
|
// bytes but its given sequence wasn't valid utf-8. Just skip
|
|
// this iteration and try again after we've updated `buf` below.
|
|
}
|
|
};
|
|
|
|
// Double the size of the buffer every so often, so we don't only
|
|
// explore small inputs.
|
|
if num_checked == buf.len() {
|
|
buf.resize(std::cmp::min(buf.len() * 2, MAX_LENGTH), 0);
|
|
}
|
|
|
|
for i in 0..buf.len() {
|
|
buf[i] = rng.gen();
|
|
}
|
|
};
|
|
|
|
// Shrink the failing input.
|
|
let mut smallest_failing_input = failing_input;
|
|
let mut panic_info = panic_info;
|
|
'shrinking: loop {
|
|
eprintln!("Smallest failing input: {:#?}", smallest_failing_input);
|
|
for input in smallest_failing_input.shrink() {
|
|
if let Err(p) = panic::catch_unwind(panic::AssertUnwindSafe(|| f(input.clone()))) {
|
|
smallest_failing_input = input;
|
|
panic_info = p;
|
|
continue 'shrinking;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Resume the panic for the smallest input.
|
|
panic::resume_unwind(panic_info);
|
|
}
|