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
|
/// Replaces the testcase at the given idx
|
||||||
#[inline]
|
#[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
|
// TODO finish
|
||||||
self.inner.replace(idx, testcase)
|
self.inner.replace(idx, testcase)
|
||||||
}
|
}
|
||||||
|
@ -41,12 +41,11 @@ where
|
|||||||
|
|
||||||
/// Replaces the testcase at the given idx
|
/// Replaces the testcase at the given idx
|
||||||
#[inline]
|
#[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() {
|
if idx >= self.entries.len() {
|
||||||
return Err(Error::key_not_found(format!("Index {} out of bounds", idx)));
|
return Err(Error::key_not_found(format!("Index {} out of bounds", idx)));
|
||||||
}
|
}
|
||||||
self.entries[idx] = RefCell::new(testcase);
|
Ok(self.entries[idx].replace(testcase))
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes an entry from the corpus, returning it if it was present.
|
/// 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
|
/// Add an entry to the corpus and return its index
|
||||||
fn add(&mut self, testcase: Testcase<I>) -> Result<usize, Error>;
|
fn add(&mut self, testcase: Testcase<I>) -> Result<usize, Error>;
|
||||||
|
|
||||||
/// Replaces the testcase at the given idx
|
/// Replaces the testcase at the given idx, returning the existing.
|
||||||
fn replace(&mut self, idx: usize, testcase: Testcase<I>) -> Result<(), Error>;
|
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.
|
/// Removes an entry from the corpus, returning it if it was present.
|
||||||
fn remove(&mut self, idx: usize) -> Result<Option<Testcase<I>>, Error>;
|
fn remove(&mut self, idx: usize) -> Result<Option<Testcase<I>>, Error>;
|
||||||
@ -177,7 +177,11 @@ pub mod pybind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[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) })
|
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
|
/// Add an entry to the corpus and return its index
|
||||||
#[inline]
|
#[inline]
|
||||||
fn add(&mut self, mut testcase: Testcase<I>) -> Result<usize, Error> {
|
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() {
|
if testcase.filename().is_none() {
|
||||||
// TODO walk entry metadata to ask for pieces of filename (e.g. :havoc in AFL)
|
// TODO walk entry metadata to ask for pieces of filename (e.g. :havoc in AFL)
|
||||||
let file_orig = testcase
|
let file_orig = testcase
|
||||||
@ -128,84 +214,22 @@ where
|
|||||||
testcase
|
testcase
|
||||||
.store_input()
|
.store_input()
|
||||||
.expect("Could not save testcase to disk");
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes an entry from the corpus, returning it if it was present.
|
fn remove_testcase(&mut self, testcase: &Testcase<I>) -> Result<(), Error> {
|
||||||
#[inline]
|
if let Some(filename) = testcase.filename() {
|
||||||
fn remove(&mut self, idx: usize) -> Result<Option<Testcase<I>>, Error> {
|
fs::remove_file(filename)?;
|
||||||
if idx >= self.entries.len() {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
Ok(Some(self.entries.remove(idx).into_inner()))
|
|
||||||
}
|
}
|
||||||
}
|
if self.meta_format.is_some() {
|
||||||
|
let mut filename = PathBuf::from(testcase.filename().as_ref().unwrap());
|
||||||
/// Get by id
|
filename.set_file_name(format!(
|
||||||
#[inline]
|
".{}.metadata",
|
||||||
fn get(&self, idx: usize) -> Result<&RefCell<Testcase<I>>, Error> {
|
filename.file_name().unwrap().to_string_lossy()
|
||||||
Ok(&self.entries[idx])
|
));
|
||||||
}
|
fs::remove_file(filename)?;
|
||||||
|
|
||||||
/// 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())
|
Ok(())
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "python")]
|
#[cfg(feature = "python")]
|
||||||
|
@ -296,6 +296,61 @@ where
|
|||||||
OT: ObserversTuple<I, S>;
|
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
|
/// Eager `OR` combination of two feedbacks
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LogicEagerOr {}
|
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.
|
/// A [`TimeoutFeedback`] reduces the timeout value of a run.
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct TimeoutFeedback {}
|
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
|
/// Nop feedback that annotates execution time in the new testcase, if any
|
||||||
/// for this Feedback, the testcase is never interesting (use with an OR).
|
/// for this Feedback, the testcase is never interesting (use with an OR).
|
||||||
/// It decides, if the given [`TimeObserver`] value of a run is interesting.
|
/// 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
|
/// Load the content of this input from a file
|
||||||
#[cfg(feature = "std")]
|
|
||||||
fn from_file<P>(path: P) -> Result<Self, Error>
|
fn from_file<P>(path: P) -> Result<Self, Error>
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
|
@ -39,22 +39,17 @@ pub trait Scheduler<I, S>
|
|||||||
where
|
where
|
||||||
I: Input,
|
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> {
|
fn on_add(&self, _state: &mut S, _idx: usize) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces the testcase at the given idx
|
/// Replaced the given testcase at the given idx
|
||||||
fn on_replace(
|
fn on_replace(&self, _state: &mut S, _idx: usize, _prev: &Testcase<I>) -> Result<(), Error> {
|
||||||
&self,
|
|
||||||
_state: &mut S,
|
|
||||||
_idx: usize,
|
|
||||||
_testcase: &Testcase<I>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
Ok(())
|
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(
|
fn on_remove(
|
||||||
&self,
|
&self,
|
||||||
_state: &mut S,
|
_state: &mut S,
|
||||||
|
@ -8,6 +8,11 @@ Other stages may enrich [`crate::corpus::Testcase`]s with metadata.
|
|||||||
pub mod mutational;
|
pub mod mutational;
|
||||||
pub use mutational::{MutationalStage, StdMutationalStage};
|
pub use mutational::{MutationalStage, StdMutationalStage};
|
||||||
|
|
||||||
|
pub mod tmin;
|
||||||
|
pub use tmin::{
|
||||||
|
MapEqualityFactory, MapEqualityFeedback, StdTMinMutationalStage, TMinMutationalStage,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod push;
|
pub mod push;
|
||||||
|
|
||||||
pub mod tracing;
|
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