Add test case minimising stage (tmin) (#735)
* add test case minimising stage * general purpose minimiser impl, with fuzzer example * reorganise, document, and other cleanup * correct python API return value * correct some docs * nit: versioning in fuzzers * ise -> ize
This commit is contained in:
parent
556bdc828c
commit
d6e72560dc
3
fuzzers/baby_fuzzer_minimizing/.gitignore
vendored
Normal file
3
fuzzers/baby_fuzzer_minimizing/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
corpus
|
||||
minimized
|
||||
solutions
|
23
fuzzers/baby_fuzzer_minimizing/Cargo.toml
Normal file
23
fuzzers/baby_fuzzer_minimizing/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "baby_fuzzer_minimizing"
|
||||
version = "0.8.1"
|
||||
authors = ["Andrea Fioraldi <andreafioraldi@gmail.com>", "Dominik Maier <domenukk@gmail.com>", "Addison Crump <research@addisoncrump.info>"]
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
tui = []
|
||||
std = []
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
debug = true
|
||||
|
||||
[dependencies]
|
||||
libafl = { path = "../../libafl/" }
|
9
fuzzers/baby_fuzzer_minimizing/README.md
Normal file
9
fuzzers/baby_fuzzer_minimizing/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Baby fuzzer
|
||||
|
||||
This is a minimalistic example about how to create a libafl based fuzzer which leverages minimisation.
|
||||
|
||||
The fuzzer steps until a crash occurs, minimising each corpus entry as it is discovered. Then, once a
|
||||
solution is found, it attempts to minimise that as well.
|
||||
|
||||
The tested program is a simple Rust function without any instrumentation.
|
||||
For real fuzzing, you will want to add some sort to add coverage or other feedback.
|
142
fuzzers/baby_fuzzer_minimizing/src/main.rs
Normal file
142
fuzzers/baby_fuzzer_minimizing/src/main.rs
Normal file
@ -0,0 +1,142 @@
|
||||
use std::path::PathBuf;
|
||||
#[cfg(windows)]
|
||||
use std::ptr::write_volatile;
|
||||
|
||||
use libafl::prelude::*;
|
||||
|
||||
/// Coverage map with explicit assignments due to the lack of instrumentation
|
||||
static mut SIGNALS: [u8; 16] = [0; 16];
|
||||
|
||||
/// Assign a signal to the signals map
|
||||
fn signals_set(idx: usize) {
|
||||
unsafe { SIGNALS[idx] = 1 };
|
||||
}
|
||||
|
||||
#[allow(clippy::similar_names)]
|
||||
pub fn main() -> Result<(), Error> {
|
||||
// The closure that we want to fuzz
|
||||
let mut harness = |input: &BytesInput| {
|
||||
let target = input.target_bytes();
|
||||
let buf = target.as_slice();
|
||||
signals_set(0);
|
||||
if !buf.is_empty() && buf[0] == b'a' {
|
||||
signals_set(1);
|
||||
if buf.len() > 1 && buf[1] == b'b' {
|
||||
signals_set(2);
|
||||
if buf.len() > 2 && buf[2] == b'c' {
|
||||
return ExitKind::Crash;
|
||||
}
|
||||
}
|
||||
}
|
||||
ExitKind::Ok
|
||||
};
|
||||
|
||||
// Create an observation channel using the signals map
|
||||
let observer =
|
||||
unsafe { StdMapObserver::new_from_ptr("signals", SIGNALS.as_mut_ptr(), SIGNALS.len()) };
|
||||
|
||||
let factory = MapEqualityFactory::new_from_observer(&observer);
|
||||
|
||||
// Feedback to rate the interestingness of an input
|
||||
let mut feedback = MaxMapFeedback::new(&observer);
|
||||
|
||||
// A feedback to choose if an input is a solution or not
|
||||
let mut objective = CrashFeedback::new();
|
||||
|
||||
// The Monitor trait define how the fuzzer stats are displayed to the user
|
||||
let mon = SimpleMonitor::new(|s| println!("{}", s));
|
||||
|
||||
let mut mgr = SimpleEventManager::new(mon);
|
||||
|
||||
let corpus_dir = PathBuf::from("./corpus");
|
||||
let solution_dir = PathBuf::from("./solutions");
|
||||
|
||||
// create a State from scratch
|
||||
let mut state = StdState::new(
|
||||
// RNG
|
||||
StdRand::with_seed(current_nanos()),
|
||||
// Corpus that will be evolved, we keep it in memory for performance
|
||||
OnDiskCorpus::new(&corpus_dir).unwrap(),
|
||||
// Corpus in which we store solutions (crashes in this example),
|
||||
// on disk so the user can get them after stopping the fuzzer
|
||||
OnDiskCorpus::new(&solution_dir).unwrap(),
|
||||
// States of the feedbacks.
|
||||
// The feedbacks can report the data that should persist in the State.
|
||||
&mut feedback,
|
||||
// Same for objective feedbacks
|
||||
&mut objective,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// A queue policy to get testcasess from the corpus
|
||||
let scheduler = QueueScheduler::new();
|
||||
|
||||
// A fuzzer with feedbacks and a corpus scheduler
|
||||
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
|
||||
|
||||
// Create the executor for an in-process function with just one observer
|
||||
let mut executor = InProcessExecutor::new(
|
||||
&mut harness,
|
||||
tuple_list!(observer),
|
||||
&mut fuzzer,
|
||||
&mut state,
|
||||
&mut mgr,
|
||||
)
|
||||
.expect("Failed to create the Executor");
|
||||
|
||||
// Generator of printable bytearrays of max size 32
|
||||
let mut generator = RandPrintablesGenerator::new(32);
|
||||
|
||||
// Generate 8 initial inputs
|
||||
state
|
||||
.generate_initial_inputs(&mut fuzzer, &mut executor, &mut generator, &mut mgr, 8)
|
||||
.expect("Failed to generate the initial corpus");
|
||||
|
||||
// Setup a mutational stage with a basic bytes mutator
|
||||
let mutator = StdScheduledMutator::new(havoc_mutations());
|
||||
let minimizer = StdScheduledMutator::new(havoc_mutations());
|
||||
let mut stages = tuple_list!(
|
||||
StdMutationalStage::new(mutator),
|
||||
StdTMinMutationalStage::new(minimizer, factory, 128)
|
||||
);
|
||||
|
||||
while state.solutions().is_empty() {
|
||||
fuzzer.fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr)?;
|
||||
}
|
||||
|
||||
let minimized_dir = PathBuf::from("./minimized");
|
||||
|
||||
let mut state = StdState::new(
|
||||
StdRand::with_seed(current_nanos()),
|
||||
OnDiskCorpus::new(&minimized_dir).unwrap(),
|
||||
InMemoryCorpus::new(),
|
||||
&mut (),
|
||||
&mut (),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The Monitor trait define how the fuzzer stats are displayed to the user
|
||||
let mon = SimpleMonitor::new(|s| println!("{}", s));
|
||||
|
||||
let mut mgr = SimpleEventManager::new(mon);
|
||||
|
||||
let minimizer = StdScheduledMutator::new(havoc_mutations());
|
||||
let mut stages = tuple_list!(StdTMinMutationalStage::new(
|
||||
minimizer,
|
||||
CrashFeedbackFactory::default(),
|
||||
1 << 10
|
||||
));
|
||||
|
||||
let scheduler = QueueScheduler::new();
|
||||
|
||||
// A fuzzer with feedbacks and a corpus scheduler
|
||||
let mut fuzzer = StdFuzzer::new(scheduler, (), ());
|
||||
|
||||
// Create the executor for an in-process function with just one observer
|
||||
let mut executor = InProcessExecutor::new(&mut harness, (), &mut fuzzer, &mut state, &mut mgr)?;
|
||||
|
||||
state.load_initial_inputs_forced(&mut fuzzer, &mut executor, &mut mgr, &[solution_dir])?;
|
||||
stages.perform_all(&mut fuzzer, &mut executor, &mut state, &mut mgr, 0)?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -46,7 +46,7 @@ where
|
||||
|
||||
/// Replaces the testcase at the given idx
|
||||
#[inline]
|
||||
fn replace(&mut self, idx: usize, testcase: Testcase<I>) -> Result<(), Error> {
|
||||
fn replace(&mut self, idx: usize, testcase: Testcase<I>) -> Result<Testcase<I>, Error> {
|
||||
// TODO finish
|
||||
self.inner.replace(idx, testcase)
|
||||
}
|
||||
|
@ -41,12 +41,11 @@ where
|
||||
|
||||
/// Replaces the testcase at the given idx
|
||||
#[inline]
|
||||
fn replace(&mut self, idx: usize, testcase: Testcase<I>) -> Result<(), Error> {
|
||||
fn replace(&mut self, idx: usize, testcase: Testcase<I>) -> Result<Testcase<I>, Error> {
|
||||
if idx >= self.entries.len() {
|
||||
return Err(Error::key_not_found(format!("Index {} out of bounds", idx)));
|
||||
}
|
||||
self.entries[idx] = RefCell::new(testcase);
|
||||
Ok(())
|
||||
Ok(self.entries[idx].replace(testcase))
|
||||
}
|
||||
|
||||
/// Removes an entry from the corpus, returning it if it was present.
|
||||
|
@ -36,8 +36,8 @@ where
|
||||
/// Add an entry to the corpus and return its index
|
||||
fn add(&mut self, testcase: Testcase<I>) -> Result<usize, Error>;
|
||||
|
||||
/// Replaces the testcase at the given idx
|
||||
fn replace(&mut self, idx: usize, testcase: Testcase<I>) -> Result<(), Error>;
|
||||
/// Replaces the testcase at the given idx, returning the existing.
|
||||
fn replace(&mut self, idx: usize, testcase: Testcase<I>) -> Result<Testcase<I>, Error>;
|
||||
|
||||
/// Removes an entry from the corpus, returning it if it was present.
|
||||
fn remove(&mut self, idx: usize) -> Result<Option<Testcase<I>>, Error>;
|
||||
@ -177,7 +177,11 @@ pub mod pybind {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn replace(&mut self, idx: usize, testcase: Testcase<BytesInput>) -> Result<(), Error> {
|
||||
fn replace(
|
||||
&mut self,
|
||||
idx: usize,
|
||||
testcase: Testcase<BytesInput>,
|
||||
) -> Result<Testcase<BytesInput>, Error> {
|
||||
unwrap_me_mut!(self.wrapper, c, { c.replace(idx, testcase) })
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,92 @@ where
|
||||
/// Add an entry to the corpus and return its index
|
||||
#[inline]
|
||||
fn add(&mut self, mut testcase: Testcase<I>) -> Result<usize, Error> {
|
||||
self.save_testcase(&mut testcase)?;
|
||||
self.entries.push(RefCell::new(testcase));
|
||||
Ok(self.entries.len() - 1)
|
||||
}
|
||||
|
||||
/// Replaces the testcase at the given idx
|
||||
#[inline]
|
||||
fn replace(&mut self, idx: usize, mut testcase: Testcase<I>) -> Result<Testcase<I>, Error> {
|
||||
if idx >= self.entries.len() {
|
||||
return Err(Error::key_not_found(format!("Index {} out of bounds", idx)));
|
||||
}
|
||||
self.save_testcase(&mut testcase)?;
|
||||
let previous = self.entries[idx].replace(testcase);
|
||||
self.remove_testcase(&previous)?;
|
||||
Ok(previous)
|
||||
}
|
||||
|
||||
/// Removes an entry from the corpus, returning it if it was present.
|
||||
#[inline]
|
||||
fn remove(&mut self, idx: usize) -> Result<Option<Testcase<I>>, Error> {
|
||||
if idx >= self.entries.len() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let prev = self.entries.remove(idx).into_inner();
|
||||
self.remove_testcase(&prev)?;
|
||||
Ok(Some(prev))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get by id
|
||||
#[inline]
|
||||
fn get(&self, idx: usize) -> Result<&RefCell<Testcase<I>>, Error> {
|
||||
Ok(&self.entries[idx])
|
||||
}
|
||||
|
||||
/// Current testcase scheduled
|
||||
#[inline]
|
||||
fn current(&self) -> &Option<usize> {
|
||||
&self.current
|
||||
}
|
||||
|
||||
/// Current testcase scheduled (mutable)
|
||||
#[inline]
|
||||
fn current_mut(&mut self) -> &mut Option<usize> {
|
||||
&mut self.current
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> OnDiskCorpus<I>
|
||||
where
|
||||
I: Input,
|
||||
{
|
||||
/// Creates the [`OnDiskCorpus`].
|
||||
/// Will error, if [`std::fs::create_dir_all()`] failed for `dir_path`.
|
||||
pub fn new<P>(dir_path: P) -> Result<Self, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
fn new<I: Input>(dir_path: PathBuf) -> Result<OnDiskCorpus<I>, Error> {
|
||||
fs::create_dir_all(&dir_path)?;
|
||||
Ok(OnDiskCorpus {
|
||||
entries: vec![],
|
||||
current: None,
|
||||
dir_path,
|
||||
meta_format: None,
|
||||
})
|
||||
}
|
||||
new(dir_path.as_ref().to_path_buf())
|
||||
}
|
||||
|
||||
/// Creates the [`OnDiskCorpus`] specifying the type of `Metadata` to be saved to disk.
|
||||
/// Will error, if [`std::fs::create_dir_all()`] failed for `dir_path`.
|
||||
pub fn new_save_meta(
|
||||
dir_path: PathBuf,
|
||||
meta_format: Option<OnDiskMetadataFormat>,
|
||||
) -> Result<Self, Error> {
|
||||
fs::create_dir_all(&dir_path)?;
|
||||
Ok(Self {
|
||||
entries: vec![],
|
||||
current: None,
|
||||
dir_path,
|
||||
meta_format,
|
||||
})
|
||||
}
|
||||
|
||||
fn save_testcase(&mut self, testcase: &mut Testcase<I>) -> Result<(), Error> {
|
||||
if testcase.filename().is_none() {
|
||||
// TODO walk entry metadata to ask for pieces of filename (e.g. :havoc in AFL)
|
||||
let file_orig = testcase
|
||||
@ -128,84 +214,22 @@ where
|
||||
testcase
|
||||
.store_input()
|
||||
.expect("Could not save testcase to disk");
|
||||
self.entries.push(RefCell::new(testcase));
|
||||
Ok(self.entries.len() - 1)
|
||||
}
|
||||
|
||||
/// Replaces the testcase at the given idx
|
||||
#[inline]
|
||||
fn replace(&mut self, idx: usize, testcase: Testcase<I>) -> Result<(), Error> {
|
||||
if idx >= self.entries.len() {
|
||||
return Err(Error::key_not_found(format!("Index {} out of bounds", idx)));
|
||||
}
|
||||
self.entries[idx] = RefCell::new(testcase);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes an entry from the corpus, returning it if it was present.
|
||||
#[inline]
|
||||
fn remove(&mut self, idx: usize) -> Result<Option<Testcase<I>>, Error> {
|
||||
if idx >= self.entries.len() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(self.entries.remove(idx).into_inner()))
|
||||
fn remove_testcase(&mut self, testcase: &Testcase<I>) -> Result<(), Error> {
|
||||
if let Some(filename) = testcase.filename() {
|
||||
fs::remove_file(filename)?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get by id
|
||||
#[inline]
|
||||
fn get(&self, idx: usize) -> Result<&RefCell<Testcase<I>>, Error> {
|
||||
Ok(&self.entries[idx])
|
||||
}
|
||||
|
||||
/// Current testcase scheduled
|
||||
#[inline]
|
||||
fn current(&self) -> &Option<usize> {
|
||||
&self.current
|
||||
}
|
||||
|
||||
/// Current testcase scheduled (mutable)
|
||||
#[inline]
|
||||
fn current_mut(&mut self) -> &mut Option<usize> {
|
||||
&mut self.current
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> OnDiskCorpus<I>
|
||||
where
|
||||
I: Input,
|
||||
{
|
||||
/// Creates the [`OnDiskCorpus`].
|
||||
/// Will error, if [`std::fs::create_dir_all()`] failed for `dir_path`.
|
||||
pub fn new<P>(dir_path: P) -> Result<Self, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
fn new<I: Input>(dir_path: PathBuf) -> Result<OnDiskCorpus<I>, Error> {
|
||||
fs::create_dir_all(&dir_path)?;
|
||||
Ok(OnDiskCorpus {
|
||||
entries: vec![],
|
||||
current: None,
|
||||
dir_path,
|
||||
meta_format: None,
|
||||
})
|
||||
if self.meta_format.is_some() {
|
||||
let mut filename = PathBuf::from(testcase.filename().as_ref().unwrap());
|
||||
filename.set_file_name(format!(
|
||||
".{}.metadata",
|
||||
filename.file_name().unwrap().to_string_lossy()
|
||||
));
|
||||
fs::remove_file(filename)?;
|
||||
}
|
||||
new(dir_path.as_ref().to_path_buf())
|
||||
}
|
||||
|
||||
/// Creates the [`OnDiskCorpus`] specifying the type of `Metadata` to be saved to disk.
|
||||
/// Will error, if [`std::fs::create_dir_all()`] failed for `dir_path`.
|
||||
pub fn new_save_meta(
|
||||
dir_path: PathBuf,
|
||||
meta_format: Option<OnDiskMetadataFormat>,
|
||||
) -> Result<Self, Error> {
|
||||
fs::create_dir_all(&dir_path)?;
|
||||
Ok(Self {
|
||||
entries: vec![],
|
||||
current: None,
|
||||
dir_path,
|
||||
meta_format,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "python")]
|
||||
|
@ -296,6 +296,61 @@ where
|
||||
OT: ObserversTuple<I, S>;
|
||||
}
|
||||
|
||||
/// Factory for feedbacks which should be sensitive to an existing context, e.g. observer(s) from a
|
||||
/// specific execution
|
||||
pub trait FeedbackFactory<F, I, S, T>
|
||||
where
|
||||
F: Feedback<I, S>,
|
||||
I: Input,
|
||||
S: HasClientPerfMonitor,
|
||||
{
|
||||
/// Create the feedback from the provided context
|
||||
fn create_feedback(&self, ctx: &T) -> F;
|
||||
}
|
||||
|
||||
impl<FE, FU, I, S, T> FeedbackFactory<FE, I, S, T> for FU
|
||||
where
|
||||
FU: Fn(&T) -> FE,
|
||||
FE: Feedback<I, S>,
|
||||
I: Input,
|
||||
S: HasClientPerfMonitor,
|
||||
{
|
||||
fn create_feedback(&self, ctx: &T) -> FE {
|
||||
self(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/// A feedback factory which merely invokes `::default()` for the feedback type provided
|
||||
#[derive(Default, Debug, Copy, Clone)]
|
||||
pub struct DefaultFeedbackFactory<F>
|
||||
where
|
||||
F: Default,
|
||||
{
|
||||
phantom: PhantomData<F>,
|
||||
}
|
||||
|
||||
impl<F> DefaultFeedbackFactory<F>
|
||||
where
|
||||
F: Default,
|
||||
{
|
||||
/// Create the feedback factory
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, I, S, T> FeedbackFactory<F, I, S, T> for DefaultFeedbackFactory<F>
|
||||
where
|
||||
F: Feedback<I, S> + Default,
|
||||
I: Input,
|
||||
S: HasClientPerfMonitor,
|
||||
{
|
||||
fn create_feedback(&self, _ctx: &T) -> F {
|
||||
F::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Eager `OR` combination of two feedbacks
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LogicEagerOr {}
|
||||
@ -776,6 +831,9 @@ impl Default for CrashFeedback {
|
||||
}
|
||||
}
|
||||
|
||||
/// A feedback factory for crash feedbacks
|
||||
pub type CrashFeedbackFactory = DefaultFeedbackFactory<CrashFeedback>;
|
||||
|
||||
/// A [`TimeoutFeedback`] reduces the timeout value of a run.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct TimeoutFeedback {}
|
||||
@ -827,6 +885,9 @@ impl Default for TimeoutFeedback {
|
||||
}
|
||||
}
|
||||
|
||||
/// A feedback factory for timeout feedbacks
|
||||
pub type TimeoutFeedbackFactory = DefaultFeedbackFactory<TimeoutFeedback>;
|
||||
|
||||
/// Nop feedback that annotates execution time in the new testcase, if any
|
||||
/// for this Feedback, the testcase is never interesting (use with an OR).
|
||||
/// It decides, if the given [`TimeObserver`] value of a run is interesting.
|
||||
|
@ -62,7 +62,6 @@ pub trait Input: Clone + Serialize + serde::de::DeserializeOwned + Debug {
|
||||
}
|
||||
|
||||
/// Load the content of this input from a file
|
||||
#[cfg(feature = "std")]
|
||||
fn from_file<P>(path: P) -> Result<Self, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
|
@ -39,22 +39,17 @@ pub trait Scheduler<I, S>
|
||||
where
|
||||
I: Input,
|
||||
{
|
||||
/// Add an entry to the corpus and return its index
|
||||
/// Added an entry to the corpus at the given index
|
||||
fn on_add(&self, _state: &mut S, _idx: usize) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replaces the testcase at the given idx
|
||||
fn on_replace(
|
||||
&self,
|
||||
_state: &mut S,
|
||||
_idx: usize,
|
||||
_testcase: &Testcase<I>,
|
||||
) -> Result<(), Error> {
|
||||
/// Replaced the given testcase at the given idx
|
||||
fn on_replace(&self, _state: &mut S, _idx: usize, _prev: &Testcase<I>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes an entry from the corpus, returning it if it was present.
|
||||
/// Removed the given entry from the corpus at the given index
|
||||
fn on_remove(
|
||||
&self,
|
||||
_state: &mut S,
|
||||
|
@ -8,6 +8,11 @@ Other stages may enrich [`crate::corpus::Testcase`]s with metadata.
|
||||
pub mod mutational;
|
||||
pub use mutational::{MutationalStage, StdMutationalStage};
|
||||
|
||||
pub mod tmin;
|
||||
pub use tmin::{
|
||||
MapEqualityFactory, MapEqualityFeedback, StdTMinMutationalStage, TMinMutationalStage,
|
||||
};
|
||||
|
||||
pub mod push;
|
||||
|
||||
pub mod tracing;
|
||||
|
386
libafl/src/stages/tmin.rs
Normal file
386
libafl/src/stages/tmin.rs
Normal file
@ -0,0 +1,386 @@
|
||||
//! The [`TMinMutationalStage`] is a stage which will attempt to minimize corpus entries.
|
||||
|
||||
use alloc::string::{String, ToString};
|
||||
use core::{
|
||||
fmt::Debug,
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use ahash::AHasher;
|
||||
|
||||
#[cfg(feature = "introspection")]
|
||||
use crate::monitors::PerfFeature;
|
||||
use crate::{
|
||||
bolts::{tuples::Named, HasLen},
|
||||
corpus::{Corpus, Testcase},
|
||||
events::EventFirer,
|
||||
executors::{Executor, ExitKind, HasObservers},
|
||||
feedbacks::{Feedback, FeedbackFactory, HasObserverName},
|
||||
inputs::Input,
|
||||
mark_feature_time,
|
||||
mutators::Mutator,
|
||||
observers::{MapObserver, ObserversTuple},
|
||||
schedulers::Scheduler,
|
||||
stages::Stage,
|
||||
start_timer,
|
||||
state::{HasClientPerfMonitor, HasCorpus, HasExecutions, HasMaxSize},
|
||||
Error, ExecutesInput, ExecutionProcessor, HasFeedback, HasScheduler,
|
||||
};
|
||||
|
||||
/// Mutational stage which minimizes corpus entries.
|
||||
///
|
||||
/// You must provide at least one mutator that actually reduces size.
|
||||
pub trait TMinMutationalStage<CS, E, EM, F1, F2, I, M, OT, S, Z>:
|
||||
Stage<E, EM, S, Z> + FeedbackFactory<F2, I, S, OT>
|
||||
where
|
||||
CS: Scheduler<I, S>,
|
||||
E: Executor<EM, I, S, Z> + HasObservers<I, OT, S>,
|
||||
EM: EventFirer<I>,
|
||||
F1: Feedback<I, S>,
|
||||
F2: Feedback<I, S>,
|
||||
I: Input + Hash + HasLen,
|
||||
M: Mutator<I, S>,
|
||||
OT: ObserversTuple<I, S>,
|
||||
S: HasClientPerfMonitor + HasCorpus<I> + HasExecutions + HasMaxSize,
|
||||
Z: ExecutionProcessor<I, OT, S>
|
||||
+ ExecutesInput<I, OT, S, Z>
|
||||
+ HasFeedback<F1, I, S>
|
||||
+ HasScheduler<CS, I, S>,
|
||||
{
|
||||
/// The mutator registered for this stage
|
||||
fn mutator(&self) -> &M;
|
||||
|
||||
/// The mutator registered for this stage (mutable)
|
||||
fn mutator_mut(&mut self) -> &mut M;
|
||||
|
||||
/// Gets the number of iterations this mutator should run for.
|
||||
fn iterations(&self, state: &mut S, corpus_idx: usize) -> Result<usize, Error>;
|
||||
|
||||
/// Runs this (mutational) stage for new objectives
|
||||
#[allow(clippy::cast_possible_wrap)] // more than i32 stages on 32 bit system - highly unlikely...
|
||||
fn perform_minification(
|
||||
&mut self,
|
||||
fuzzer: &mut Z,
|
||||
executor: &mut E,
|
||||
state: &mut S,
|
||||
manager: &mut EM,
|
||||
base_corpus_idx: usize,
|
||||
) -> Result<(), Error> {
|
||||
let orig_max_size = state.max_size();
|
||||
// basically copy-pasted from mutational.rs
|
||||
let num = self.iterations(state, base_corpus_idx)?;
|
||||
|
||||
start_timer!(state);
|
||||
let mut base = state
|
||||
.corpus()
|
||||
.get(base_corpus_idx)?
|
||||
.borrow_mut()
|
||||
.load_input()?
|
||||
.clone();
|
||||
let mut hasher = AHasher::new_with_keys(0, 0);
|
||||
base.hash(&mut hasher);
|
||||
let base_hash = hasher.finish();
|
||||
mark_feature_time!(state, PerfFeature::GetInputFromCorpus);
|
||||
|
||||
fuzzer.execute_input(state, executor, manager, &base)?;
|
||||
let observers = executor.observers();
|
||||
|
||||
let mut feedback = self.create_feedback(observers);
|
||||
|
||||
let mut i = 0;
|
||||
loop {
|
||||
if i >= num {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut next_i = i + 1;
|
||||
let mut input = base.clone();
|
||||
|
||||
let before_len = input.len();
|
||||
|
||||
state.set_max_size(before_len);
|
||||
|
||||
start_timer!(state);
|
||||
self.mutator_mut().mutate(state, &mut input, i as i32)?;
|
||||
mark_feature_time!(state, PerfFeature::Mutate);
|
||||
|
||||
let corpus_idx = if input.len() < before_len {
|
||||
// run the input
|
||||
let exit_kind = fuzzer.execute_input(state, executor, manager, &input)?;
|
||||
let observers = executor.observers();
|
||||
|
||||
// let the fuzzer process this execution -- it's possible that we find something
|
||||
// interesting, or even a solution
|
||||
let (_, corpus_idx) = fuzzer.process_execution(
|
||||
state,
|
||||
manager,
|
||||
input.clone(),
|
||||
observers,
|
||||
&exit_kind,
|
||||
false,
|
||||
)?;
|
||||
|
||||
if feedback.is_interesting(state, manager, &input, observers, &exit_kind)? {
|
||||
// we found a reduced corpus entry! use the smaller base
|
||||
base = input;
|
||||
|
||||
// do more runs! maybe we can minify further
|
||||
next_i = 0;
|
||||
}
|
||||
|
||||
corpus_idx
|
||||
} else {
|
||||
// we can't guarantee that the mutators provided will necessarily reduce size, so
|
||||
// skip any mutations that actually increase size so we don't waste eval time
|
||||
None
|
||||
};
|
||||
|
||||
start_timer!(state);
|
||||
self.mutator_mut().post_exec(state, i as i32, corpus_idx)?;
|
||||
mark_feature_time!(state, PerfFeature::MutatePostExec);
|
||||
|
||||
i = next_i;
|
||||
}
|
||||
|
||||
let mut hasher = AHasher::new_with_keys(0, 0);
|
||||
base.hash(&mut hasher);
|
||||
let new_hash = hasher.finish();
|
||||
if base_hash != new_hash {
|
||||
let mut testcase = Testcase::with_executions(base, *state.executions());
|
||||
fuzzer
|
||||
.feedback_mut()
|
||||
.append_metadata(state, &mut testcase)?;
|
||||
let prev = state.corpus_mut().replace(base_corpus_idx, testcase)?;
|
||||
fuzzer
|
||||
.scheduler_mut()
|
||||
.on_replace(state, base_corpus_idx, &prev)?;
|
||||
}
|
||||
|
||||
state.set_max_size(orig_max_size);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The default corpus entry minimising mutational stage
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StdTMinMutationalStage<CS, E, EM, F1, F2, FF, I, M, S, T, Z>
|
||||
where
|
||||
I: Input + HasLen,
|
||||
M: Mutator<I, S>,
|
||||
{
|
||||
mutator: M,
|
||||
factory: FF,
|
||||
runs: usize,
|
||||
#[allow(clippy::type_complexity)]
|
||||
phantom: PhantomData<(CS, E, EM, F1, F2, I, S, T, Z)>,
|
||||
}
|
||||
|
||||
impl<CS, E, EM, F1, F2, FF, I, M, OT, S, Z> Stage<E, EM, S, Z>
|
||||
for StdTMinMutationalStage<CS, E, EM, F1, F2, FF, I, M, S, OT, Z>
|
||||
where
|
||||
CS: Scheduler<I, S>,
|
||||
E: Executor<EM, I, S, Z> + HasObservers<I, OT, S>,
|
||||
EM: EventFirer<I>,
|
||||
F1: Feedback<I, S>,
|
||||
F2: Feedback<I, S>,
|
||||
FF: FeedbackFactory<F2, I, S, OT>,
|
||||
I: Input + Hash + HasLen,
|
||||
M: Mutator<I, S>,
|
||||
OT: ObserversTuple<I, S>,
|
||||
S: HasClientPerfMonitor + HasCorpus<I> + HasExecutions + HasMaxSize,
|
||||
Z: ExecutionProcessor<I, OT, S>
|
||||
+ ExecutesInput<I, OT, S, Z>
|
||||
+ HasFeedback<F1, I, S>
|
||||
+ HasScheduler<CS, I, S>,
|
||||
{
|
||||
fn perform(
|
||||
&mut self,
|
||||
fuzzer: &mut Z,
|
||||
executor: &mut E,
|
||||
state: &mut S,
|
||||
manager: &mut EM,
|
||||
corpus_idx: usize,
|
||||
) -> Result<(), Error> {
|
||||
self.perform_minification(fuzzer, executor, state, manager, corpus_idx)?;
|
||||
|
||||
#[cfg(feature = "introspection")]
|
||||
state.introspection_monitor_mut().finish_stage();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<CS, E, EM, F1, F2, FF, I, M, S, T, Z> FeedbackFactory<F2, I, S, T>
|
||||
for StdTMinMutationalStage<CS, E, EM, F1, F2, FF, I, M, S, T, Z>
|
||||
where
|
||||
F2: Feedback<I, S>,
|
||||
FF: FeedbackFactory<F2, I, S, T>,
|
||||
I: Input + HasLen,
|
||||
M: Mutator<I, S>,
|
||||
S: HasClientPerfMonitor,
|
||||
{
|
||||
fn create_feedback(&self, ctx: &T) -> F2 {
|
||||
self.factory.create_feedback(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<CS, E, EM, F1, F2, FF, I, M, OT, S, Z> TMinMutationalStage<CS, E, EM, F1, F2, I, M, OT, S, Z>
|
||||
for StdTMinMutationalStage<CS, E, EM, F1, F2, FF, I, M, S, OT, Z>
|
||||
where
|
||||
CS: Scheduler<I, S>,
|
||||
E: HasObservers<I, OT, S> + Executor<EM, I, S, Z>,
|
||||
EM: EventFirer<I>,
|
||||
F1: Feedback<I, S>,
|
||||
F2: Feedback<I, S>,
|
||||
FF: FeedbackFactory<F2, I, S, OT>,
|
||||
I: Input + HasLen + Hash,
|
||||
M: Mutator<I, S>,
|
||||
OT: ObserversTuple<I, S>,
|
||||
S: HasClientPerfMonitor + HasCorpus<I> + HasExecutions + HasMaxSize,
|
||||
Z: ExecutionProcessor<I, OT, S>
|
||||
+ ExecutesInput<I, OT, S, Z>
|
||||
+ HasFeedback<F1, I, S>
|
||||
+ HasScheduler<CS, I, S>,
|
||||
{
|
||||
/// The mutator, added to this stage
|
||||
#[inline]
|
||||
fn mutator(&self) -> &M {
|
||||
&self.mutator
|
||||
}
|
||||
|
||||
/// The list of mutators, added to this stage (as mutable ref)
|
||||
#[inline]
|
||||
fn mutator_mut(&mut self) -> &mut M {
|
||||
&mut self.mutator
|
||||
}
|
||||
|
||||
/// Gets the number of iterations from a fixed number of runs
|
||||
fn iterations(&self, _state: &mut S, _corpus_idx: usize) -> Result<usize, Error> {
|
||||
Ok(self.runs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<CS, E, EM, F1, F2, FF, I, M, S, T, Z>
|
||||
StdTMinMutationalStage<CS, E, EM, F1, F2, FF, I, M, S, T, Z>
|
||||
where
|
||||
I: Input + HasLen,
|
||||
M: Mutator<I, S>,
|
||||
{
|
||||
/// Creates a new minimising mutational stage that will minimize provided corpus entries
|
||||
pub fn new(mutator: M, factory: FF, runs: usize) -> Self {
|
||||
Self {
|
||||
mutator,
|
||||
factory,
|
||||
runs,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A feedback which checks if the hash of the currently observed map is equal to the original hash
|
||||
/// provided
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MapEqualityFeedback<M> {
|
||||
name: String,
|
||||
obs_name: String,
|
||||
orig_hash: u64,
|
||||
phantom: PhantomData<M>,
|
||||
}
|
||||
|
||||
impl<M> MapEqualityFeedback<M> {
|
||||
/// Create a new map equality feedback -- can be used with feedback logic
|
||||
#[must_use]
|
||||
pub fn new(name: &str, obs_name: &str, orig_hash: u64) -> Self {
|
||||
MapEqualityFeedback {
|
||||
name: name.to_string(),
|
||||
obs_name: obs_name.to_string(),
|
||||
orig_hash,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Named for MapEqualityFeedback<M> {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> HasObserverName for MapEqualityFeedback<M> {
|
||||
fn observer_name(&self) -> &str {
|
||||
&self.obs_name
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, M, S> Feedback<I, S> for MapEqualityFeedback<M>
|
||||
where
|
||||
I: Input,
|
||||
M: MapObserver,
|
||||
S: HasClientPerfMonitor,
|
||||
{
|
||||
fn is_interesting<EM, OT>(
|
||||
&mut self,
|
||||
_state: &mut S,
|
||||
_manager: &mut EM,
|
||||
_input: &I,
|
||||
observers: &OT,
|
||||
_exit_kind: &ExitKind,
|
||||
) -> Result<bool, Error>
|
||||
where
|
||||
EM: EventFirer<I>,
|
||||
OT: ObserversTuple<I, S>,
|
||||
{
|
||||
let obs = observers
|
||||
.match_name::<M>(self.observer_name())
|
||||
.expect("Should have been provided valid observer name.");
|
||||
Ok(obs.hash() == self.orig_hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// A feedback factory for ensuring that the maps for minimized inputs are the same
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MapEqualityFactory<M> {
|
||||
obs_name: String,
|
||||
phantom: PhantomData<M>,
|
||||
}
|
||||
|
||||
impl<M> MapEqualityFactory<M>
|
||||
where
|
||||
M: MapObserver,
|
||||
{
|
||||
/// Creates a new map equality feedback for the given observer
|
||||
pub fn new_from_observer(obs: &M) -> Self {
|
||||
Self {
|
||||
obs_name: obs.name().to_string(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> HasObserverName for MapEqualityFactory<M> {
|
||||
fn observer_name(&self) -> &str {
|
||||
&self.obs_name
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, M, OT, S> FeedbackFactory<MapEqualityFeedback<M>, I, S, OT> for MapEqualityFactory<M>
|
||||
where
|
||||
I: Input,
|
||||
M: MapObserver,
|
||||
OT: ObserversTuple<I, S>,
|
||||
S: HasClientPerfMonitor,
|
||||
{
|
||||
fn create_feedback(&self, observers: &OT) -> MapEqualityFeedback<M> {
|
||||
let obs = observers
|
||||
.match_name::<M>(self.observer_name())
|
||||
.expect("Should have been provided valid observer name.");
|
||||
MapEqualityFeedback {
|
||||
name: "MapEq".to_string(),
|
||||
obs_name: self.obs_name.clone(),
|
||||
orig_hash: obs.hash(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user