From 9738119fce664ca1282a13feb1907f3bb13fd103 Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Wed, 24 Aug 2022 20:10:28 -0500 Subject: [PATCH 01/16] initial v8 commit: functional executor, no observers yet --- Cargo.toml | 1 + fuzzers/baby_fuzzer_v8/Cargo.toml | 10 ++ fuzzers/baby_fuzzer_v8/src/main.rs | 114 +++++++++++++++ libafl_v8/Cargo.toml | 11 ++ libafl_v8/src/executors.rs | 224 +++++++++++++++++++++++++++++ libafl_v8/src/handlers.rs | 0 libafl_v8/src/lib.rs | 77 ++++++++++ 7 files changed, 437 insertions(+) create mode 100644 fuzzers/baby_fuzzer_v8/Cargo.toml create mode 100644 fuzzers/baby_fuzzer_v8/src/main.rs create mode 100644 libafl_v8/Cargo.toml create mode 100644 libafl_v8/src/executors.rs create mode 100644 libafl_v8/src/handlers.rs create mode 100644 libafl_v8/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 25915ea8b3..c183aeb7fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "libafl_qemu", "libafl_sugar", "libafl_nyx", + "libafl_v8", "libafl_concolic/symcc_runtime", "libafl_concolic/symcc_libafl", "libafl_concolic/test/dump_constraints", diff --git a/fuzzers/baby_fuzzer_v8/Cargo.toml b/fuzzers/baby_fuzzer_v8/Cargo.toml new file mode 100644 index 0000000000..3f9b38989b --- /dev/null +++ b/fuzzers/baby_fuzzer_v8/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "baby_fuzzer_v8" +version = "0.8.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libafl = { path = "../../libafl" } +libafl_v8 = { path = "../../libafl_v8" } \ No newline at end of file diff --git a/fuzzers/baby_fuzzer_v8/src/main.rs b/fuzzers/baby_fuzzer_v8/src/main.rs new file mode 100644 index 0000000000..6eed319bea --- /dev/null +++ b/fuzzers/baby_fuzzer_v8/src/main.rs @@ -0,0 +1,114 @@ +use std::path::PathBuf; +#[cfg(windows)] +use std::ptr::write_volatile; + +#[cfg(feature = "tui")] +use libafl::monitors::tui::TuiMonitor; +#[cfg(not(feature = "tui"))] +use libafl::monitors::SimpleMonitor; +use libafl::{ + bolts::{current_nanos, rands::StdRand, tuples::tuple_list, AsSlice}, + corpus::{InMemoryCorpus, OnDiskCorpus}, + events::SimpleEventManager, + executors::{inprocess::InProcessExecutor, ExitKind}, + feedbacks::{CrashFeedback, MaxMapFeedback}, + fuzzer::{Fuzzer, StdFuzzer}, + generators::RandPrintablesGenerator, + inputs::{BytesInput, HasTargetBytes}, + mutators::scheduled::{havoc_mutations, StdScheduledMutator}, + observers::StdMapObserver, + prelude::ConstFeedback, + schedulers::QueueScheduler, + stages::mutational::StdMutationalStage, + state::StdState, +}; +use libafl_v8::{v8, V8Executor}; + +#[allow(clippy::similar_names)] +pub fn main() { + // The JS source we want to fuzz + let source = r#" +let value = 72; + +function LLVMFuzzerTestOneInput(data) { + if (data[0] == value) { + throw "death"; + } +} +"#; + + // Feedback to rate the interestingness of an input + let mut feedback = ConstFeedback::True; + + // A feedback to choose if an input is a solution or not + let mut objective = CrashFeedback::new(); + + // 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 + InMemoryCorpus::new(), + // Corpus in which we store solutions (crashes in this example), + // on disk so the user can get them after stopping the fuzzer + OnDiskCorpus::new(PathBuf::from("./crashes")).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(); + + // The Monitor trait define how the fuzzer stats are displayed to the user + #[cfg(not(feature = "tui"))] + let mon = SimpleMonitor::new(|s| println!("{}", s)); + #[cfg(feature = "tui")] + let mon = TuiMonitor::new(String::from("Baby Fuzzer"), false); + + // The event manager handle the various events generated during the fuzzing loop + // such as the notification of the addition of a new item to the corpus + let mut mgr = SimpleEventManager::new(mon); + + // 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); + + // Setup for v8 + let platform = v8::Platform::new(1, false).make_shared(); + v8::V8::initialize_platform(platform); + v8::V8::initialize(); + let mut isolate = v8::Isolate::new(v8::CreateParams::default()); + let mut scope = v8::HandleScope::new(&mut isolate); + let context = v8::Context::new(&mut scope); + let scope = v8::ContextScope::new(&mut scope, context); + + // Create the executor for an in-process function with just one observer + let mut executor = V8Executor::new( + scope, + source, + tuple_list!(), + &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 mut stages = tuple_list!(StdMutationalStage::new(mutator)); + + fuzzer + .fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr) + .expect("Error in the fuzzing loop"); +} diff --git a/libafl_v8/Cargo.toml b/libafl_v8/Cargo.toml new file mode 100644 index 0000000000..0e1025cdfe --- /dev/null +++ b/libafl_v8/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "libafl_v8" +version = "0.8.1" +authors = ["Addison Crump "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libafl = { path = "../libafl", version = "0.8.1" } +v8 = "0.49.0" \ No newline at end of file diff --git a/libafl_v8/src/executors.rs b/libafl_v8/src/executors.rs new file mode 100644 index 0000000000..9968fa0543 --- /dev/null +++ b/libafl_v8/src/executors.rs @@ -0,0 +1,224 @@ +use core::{ + fmt, + fmt::{Debug, Formatter}, + marker::PhantomData, +}; +use std::iter; + +use libafl::{ + executors::{Executor, ExitKind, HasObservers}, + inputs::{BytesInput, HasBytesVec, Input}, + observers::ObserversTuple, + Error, +}; +pub use v8; +use v8::{ + ArrayBuffer, ContextScope, Function, HandleScope, Local, Script, TryCatch, Uint8Array, Value, +}; + +pub struct V8Executor<'s1, 's2, EM, I, OT, S, Z> +where + I: Input + IntoJSValue, + OT: ObserversTuple, +{ + scope: ContextScope<'s1, HandleScope<'s2>>, + source: String, + observers: OT, + phantom: PhantomData<(EM, I, S, Z)>, +} + +impl<'s1, 's2, EM, I, OT, S, Z> V8Executor<'s1, 's2, EM, I, OT, S, Z> +where + I: Input + IntoJSValue, + OT: ObserversTuple, +{ + /// Create a new V8 executor. You MUST invoke `initialize_v8` before using this method. + pub fn new( + mut scope: ContextScope<'s1, HandleScope<'s2>>, + source: &str, + observers: OT, + fuzzer: &mut Z, + state: &mut S, + mgr: &mut EM, + ) -> Result { + v8::V8::assert_initialized(); + // run the script to initialise the program + { + let code = v8::String::new(&mut scope, source).unwrap(); + + let mut scope = TryCatch::new(&mut scope); + + let script = Script::compile(&mut scope, code, None).unwrap(); + script.run(&mut scope); + + if let Some(err) = js_err_to_libafl(&mut scope) { + return Err(err); + } + } + + get_harness_func(&mut scope)?; // check that the fuzz harness exists + Ok(Self { + scope, + source: source.to_string(), + observers, + phantom: Default::default(), + }) + } +} + +fn get_harness_func<'s1, 's2>( + scope: &mut ContextScope<'s1, HandleScope<'s2>>, +) -> Result, Error> { + let global = scope.get_current_context().global(scope); + let harness_name = Local::from(v8::String::new(scope, "LLVMFuzzerTestOneInput").unwrap()); + let func = global.get(scope, harness_name); + let func = if let Some(func) = func { + func + } else { + return Err(Error::illegal_state( + "LLVMFuzzerTestOneInput not defined in JS harness", + )); + }; + + match Local::::try_from(func) { + Ok(func) => { + // good to go! + Ok(func) + } + _ => Err(Error::illegal_state( + "LLVMFuzzerTestOneInput is defined in JS harness, but wasn't a function", + )), + } +} + +impl<'s1, 's2, EM, I, OT, S, Z> Executor for V8Executor<'s1, 's2, EM, I, OT, S, Z> +where + I: Input + IntoJSValue, + OT: ObserversTuple, +{ + fn run_target( + &mut self, + _fuzzer: &mut Z, + _state: &mut S, + _mgr: &mut EM, + input: &I, + ) -> Result { + let harness = get_harness_func(&mut self.scope)?; + + let scope = &mut HandleScope::new(&mut self.scope); + let recv = v8::undefined(scope); + let try_catch = &mut TryCatch::new(scope); + + let input = input.to_js_value(try_catch)?; + let res = if harness.call(try_catch, recv.into(), &[input]).is_none() { + Ok(ExitKind::Crash) + } else { + Ok(ExitKind::Ok) + }; + + res + } +} + +impl<'s1, 's2, EM, I, OT, S, Z> HasObservers for V8Executor<'s1, 's2, EM, I, OT, S, Z> +where + I: Input + IntoJSValue, + OT: ObserversTuple, +{ + fn observers(&self) -> &OT { + &self.observers + } + + fn observers_mut(&mut self) -> &mut OT { + &mut self.observers + } +} + +impl<'s1, 's2, EM, I, OT, S, Z> Debug for V8Executor<'s1, 's2, EM, I, OT, S, Z> +where + I: Input + IntoJSValue, + OT: ObserversTuple, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("V8Executor") + .field("source", &self.source) + .field("observers", &self.observers) + .finish_non_exhaustive() + } +} + +fn js_err_to_libafl(scope: &mut TryCatch) -> Option { + if !scope.has_caught() { + None + } else { + let exception = scope.exception().unwrap(); + let exception_string = exception + .to_string(scope) + .unwrap() + .to_rust_string_lossy(scope); + let message = if let Some(message) = scope.message() { + message + } else { + return Some(Error::illegal_state(format!( + "Provided script threw an error while executing: {}", + exception_string + ))); + }; + + let filename = message.get_script_resource_name(scope).map_or_else( + || "(unknown)".into(), + |s| s.to_string(scope).unwrap().to_rust_string_lossy(scope), + ); + let line_number = message.get_line_number(scope).unwrap_or_default(); + + let source_line = message + .get_source_line(scope) + .map(|s| s.to_string(scope).unwrap().to_rust_string_lossy(scope)) + .unwrap(); + + let start_column = message.get_start_column(); + let end_column = message.get_end_column(); + + let err_underline = iter::repeat(' ') + .take(start_column) + .chain(iter::repeat('^').take(end_column - start_column)) + .collect::(); + + if let Some(stack_trace) = scope.stack_trace() { + let stack_trace = unsafe { Local::::cast(stack_trace) }; + let stack_trace = stack_trace + .to_string(scope) + .map(|s| s.to_rust_string_lossy(scope)); + + if let Some(stack_trace) = stack_trace { + return Some(Error::illegal_state(format!( + "Encountered uncaught JS exception while executing: {}:{}: {}\n{}\n{}\n{}", + filename, + line_number, + exception_string, + source_line, + err_underline, + stack_trace + ))); + } + } + Some(Error::illegal_state(format!( + "Encountered uncaught JS exception while executing: {}:{}: {}\n{}\n{}", + filename, line_number, exception_string, source_line, err_underline + ))) + } +} + +pub trait IntoJSValue { + fn to_js_value<'s>(&self, scope: &mut HandleScope<'s>) -> Result, Error>; +} + +impl IntoJSValue for BytesInput { + fn to_js_value<'s>(&self, scope: &mut HandleScope<'s>) -> Result, Error> { + println!("{}: {:?}", self.bytes().len(), self); + let store = ArrayBuffer::new_backing_store_from_vec(Vec::from(self.bytes())).make_shared(); + let buffer = ArrayBuffer::with_backing_store(scope, &store); + let array = Uint8Array::new(scope, buffer, 0, self.bytes().len()).unwrap(); + Ok(array.into()) + } +} diff --git a/libafl_v8/src/handlers.rs b/libafl_v8/src/handlers.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libafl_v8/src/lib.rs b/libafl_v8/src/lib.rs new file mode 100644 index 0000000000..7d9828fd06 --- /dev/null +++ b/libafl_v8/src/lib.rs @@ -0,0 +1,77 @@ +// lints directly from main libafl +#![allow(incomplete_features)] +// For `type_eq` +#![cfg_attr(unstable_feature, feature(specialization))] +// For `type_id` and owned things +#![cfg_attr(unstable_feature, feature(intrinsics))] +// For `std::simd` +#![cfg_attr(unstable_feature, feature(portable_simd))] +#![warn(clippy::cargo)] +#![deny(clippy::cargo_common_metadata)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(clippy::all)] +#![deny(clippy::pedantic)] +#![allow( + clippy::unreadable_literal, + clippy::type_repetition_in_bounds, + clippy::missing_errors_doc, + clippy::cast_possible_truncation, + clippy::used_underscore_binding, + clippy::ptr_as_ptr, + clippy::missing_panics_doc, + clippy::missing_docs_in_private_items, + clippy::module_name_repetitions, + clippy::unreadable_literal +)] +#![cfg_attr(debug_assertions, warn( +missing_debug_implementations, +missing_docs, +//trivial_casts, +trivial_numeric_casts, +unused_extern_crates, +unused_import_braces, +unused_qualifications, +//unused_results +))] +#![cfg_attr(not(debug_assertions), deny( +missing_debug_implementations, +missing_docs, +//trivial_casts, +trivial_numeric_casts, +unused_extern_crates, +unused_import_braces, +unused_qualifications, +unused_must_use, +missing_docs, +//unused_results +))] +#![cfg_attr( + not(debug_assertions), + deny( + bad_style, + const_err, + dead_code, + improper_ctypes, + non_shorthand_field_patterns, + no_mangle_generic_items, + overflowing_literals, + path_statements, + patterns_in_fns_without_body, + private_in_public, + unconditional_recursion, + unused, + unused_allocation, + unused_comparisons, + unused_parens, + while_true + ) +)] +// Till they fix this buggy lint in clippy +#![allow(clippy::borrow_as_ptr)] +#![allow(clippy::borrow_deref_ref)] + +pub mod executors; +pub mod handlers; + +pub use executors::*; +pub use handlers::*; From 3399a3ca42f5e918f2db7b56f9ec43da35541d81 Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Fri, 26 Aug 2022 02:55:28 -0500 Subject: [PATCH 02/16] implement coverage --- fuzzers/baby_fuzzer_v8/Cargo.toml | 1 + fuzzers/baby_fuzzer_v8/js/target.js | 11 + fuzzers/baby_fuzzer_v8/src/main.rs | 170 +++++++++--- libafl_v8/Cargo.toml | 8 +- libafl_v8/src/executors.rs | 165 ++++++------ libafl_v8/src/handlers.rs | 0 libafl_v8/src/lib.rs | 13 +- libafl_v8/src/observers/json_types.rs | 84 ++++++ libafl_v8/src/observers/mod.rs | 359 ++++++++++++++++++++++++++ 9 files changed, 695 insertions(+), 116 deletions(-) create mode 100644 fuzzers/baby_fuzzer_v8/js/target.js delete mode 100644 libafl_v8/src/handlers.rs create mode 100644 libafl_v8/src/observers/json_types.rs create mode 100644 libafl_v8/src/observers/mod.rs diff --git a/fuzzers/baby_fuzzer_v8/Cargo.toml b/fuzzers/baby_fuzzer_v8/Cargo.toml index 3f9b38989b..8927aab7ad 100644 --- a/fuzzers/baby_fuzzer_v8/Cargo.toml +++ b/fuzzers/baby_fuzzer_v8/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1" libafl = { path = "../../libafl" } libafl_v8 = { path = "../../libafl_v8" } \ No newline at end of file diff --git a/fuzzers/baby_fuzzer_v8/js/target.js b/fuzzers/baby_fuzzer_v8/js/target.js new file mode 100644 index 0000000000..047bc82db2 --- /dev/null +++ b/fuzzers/baby_fuzzer_v8/js/target.js @@ -0,0 +1,11 @@ +export default function(data) { + if (data.length > 1 && data[0] === 'A'.charCodeAt(0)) { + console.log("howdy") + if (data.length > 2 && data[1] === 'B'.charCodeAt(0)) { + console.log("kachowdy") + if (data.length > 2 && data[2] === 'C'.charCodeAt(0)) { + throw "crash!"; + } + } + } +} diff --git a/fuzzers/baby_fuzzer_v8/src/main.rs b/fuzzers/baby_fuzzer_v8/src/main.rs index 6eed319bea..d4466081cc 100644 --- a/fuzzers/baby_fuzzer_v8/src/main.rs +++ b/fuzzers/baby_fuzzer_v8/src/main.rs @@ -1,6 +1,14 @@ -use std::path::PathBuf; #[cfg(windows)] use std::ptr::write_volatile; +use std::{ + cell::RefCell, + fs::{File, Permissions}, + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::{Path, PathBuf}, + rc::Rc, + str::FromStr, + sync::Arc, +}; #[cfg(feature = "tui")] use libafl::monitors::tui::TuiMonitor; @@ -8,7 +16,7 @@ use libafl::monitors::tui::TuiMonitor; use libafl::monitors::SimpleMonitor; use libafl::{ bolts::{current_nanos, rands::StdRand, tuples::tuple_list, AsSlice}, - corpus::{InMemoryCorpus, OnDiskCorpus}, + corpus::{Corpus, InMemoryCorpus, OnDiskCorpus}, events::SimpleEventManager, executors::{inprocess::InProcessExecutor, ExitKind}, feedbacks::{CrashFeedback, MaxMapFeedback}, @@ -16,29 +24,115 @@ use libafl::{ generators::RandPrintablesGenerator, inputs::{BytesInput, HasTargetBytes}, mutators::scheduled::{havoc_mutations, StdScheduledMutator}, - observers::StdMapObserver, - prelude::ConstFeedback, - schedulers::QueueScheduler, - stages::mutational::StdMutationalStage, - state::StdState, + observers::{StdMapObserver, TimeObserver}, + prelude::{ + powersched::PowerSchedule, ConstFeedback, HitcountsMapObserver, PowerQueueScheduler, + }, + schedulers::{ + IndexesLenTimeMinimizerScheduler, LenTimeMinimizerScheduler, QueueScheduler, + StdWeightedScheduler, + }, + stages::{mutational::StdMutationalStage, CalibrationStage, StdPowerMutationalStage}, + state::{HasSolutions, StdState}, +}; +use libafl_v8::{ + deno_core, + deno_core::{FsModuleLoader, JsRuntime, RuntimeOptions}, + deno_runtime, + deno_runtime::{ + inspector_server::InspectorServer, + ops::io::StdioPipe, + worker::{MainWorker, WorkerOptions}, + }, + runtime, + runtime::Runtime, + v8, + v8::{ContextScope, Global}, + JSMapObserver, Mutex, V8Executor, +}; + +use crate::{ + deno_runtime::{ops::io::Stdio, BootstrapOptions}, + v8::Context, }; -use libafl_v8::{v8, V8Executor}; #[allow(clippy::similar_names)] -pub fn main() { - // The JS source we want to fuzz - let source = r#" -let value = 72; - -function LLVMFuzzerTestOneInput(data) { - if (data[0] == value) { - throw "death"; - } -} -"#; +pub fn main() -> anyhow::Result<()> { + // setup JS + let module_loader = Rc::new(FsModuleLoader); + let inspector_server = Arc::new(InspectorServer::new( + SocketAddr::from_str("127.0.0.1:1337")?, + "baby_fuzzer".to_string(), + )); + let create_web_worker_cb = Arc::new(|_| { + unimplemented!("Web workers are not supported by baby fuzzer"); + }); + let web_worker_event_cb = Arc::new(|_| { + unimplemented!("Web workers are not supported by baby fuzzer"); + }); + let options = WorkerOptions { + bootstrap: BootstrapOptions { + args: vec![], + cpu_count: 1, + debug_flag: false, + enable_testing_features: false, + location: None, + no_color: false, + is_tty: false, + runtime_version: "".to_string(), + ts_version: "".to_string(), + unstable: false, + user_agent: "libafl".to_string(), + }, + extensions: vec![], + unsafely_ignore_certificate_errors: None, + root_cert_store: None, + maybe_inspector_server: Some(inspector_server.clone()), + should_break_on_first_statement: true, + get_error_class_fn: None, + origin_storage_dir: None, + blob_store: Default::default(), + broadcast_channel: Default::default(), + shared_array_buffer_store: None, + compiled_wasm_module_store: None, + module_loader, + npm_resolver: None, + create_web_worker_cb, + web_worker_preload_module_cb: web_worker_event_cb.clone(), + web_worker_pre_execute_module_cb: web_worker_event_cb, + format_js_error_fn: None, + seed: None, + source_map_getter: None, + stdio: Stdio { + stdin: Default::default(), + stdout: StdioPipe::File(File::create("stdout.log")?), + stderr: StdioPipe::File(File::create("stderr.log")?), + }, + }; + + let js_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("js/target.js"); + let main_module = deno_core::resolve_path(&js_path.to_string_lossy())?; + let permissions = deno_runtime::permissions::Permissions::allow_all(); + + let worker = Arc::new(Mutex::new(MainWorker::bootstrap_from_options( + main_module.clone(), + permissions, + options, + ))); + + let rt = Runtime::new()?; + + let map_observer = + HitcountsMapObserver::new(JSMapObserver::new("jsmap", &rt, worker.clone()).unwrap()); + let time_observer = TimeObserver::new("time"); // Feedback to rate the interestingness of an input - let mut feedback = ConstFeedback::True; + let mut map_feedback = MaxMapFeedback::new_tracking(&map_observer, true, false); + + let calibration = CalibrationStage::new(&map_feedback); + + let mutator = StdScheduledMutator::new(havoc_mutations()); + let power = StdPowerMutationalStage::new(mutator, &map_observer); // A feedback to choose if an input is a solution or not let mut objective = CrashFeedback::new(); @@ -54,7 +148,7 @@ function LLVMFuzzerTestOneInput(data) { OnDiskCorpus::new(PathBuf::from("./crashes")).unwrap(), // States of the feedbacks. // The feedbacks can report the data that should persist in the State. - &mut feedback, + &mut map_feedback, // Same for objective feedbacks &mut objective, ) @@ -71,25 +165,18 @@ function LLVMFuzzerTestOneInput(data) { let mut mgr = SimpleEventManager::new(mon); // A queue policy to get testcasess from the corpus - let scheduler = QueueScheduler::new(); - + let scheduler = IndexesLenTimeMinimizerScheduler::new(StdWeightedScheduler::with_schedule( + PowerSchedule::EXPLORE, + )); // A fuzzer with feedbacks and a corpus scheduler - let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); - - // Setup for v8 - let platform = v8::Platform::new(1, false).make_shared(); - v8::V8::initialize_platform(platform); - v8::V8::initialize(); - let mut isolate = v8::Isolate::new(v8::CreateParams::default()); - let mut scope = v8::HandleScope::new(&mut isolate); - let context = v8::Context::new(&mut scope); - let scope = v8::ContextScope::new(&mut scope, context); + let mut fuzzer = StdFuzzer::new(scheduler, map_feedback, objective); // Create the executor for an in-process function with just one observer let mut executor = V8Executor::new( - scope, - source, - tuple_list!(), + &rt, + worker, + main_module, + tuple_list!(map_observer, time_observer), &mut fuzzer, &mut state, &mut mgr, @@ -105,10 +192,11 @@ function LLVMFuzzerTestOneInput(data) { .expect("Failed to generate the initial corpus"); // Setup a mutational stage with a basic bytes mutator - let mutator = StdScheduledMutator::new(havoc_mutations()); - let mut stages = tuple_list!(StdMutationalStage::new(mutator)); + let mut stages = tuple_list!(calibration, power); + + while state.solutions().count() == 0 { + fuzzer.fuzz_loop_for(&mut stages, &mut executor, &mut state, &mut mgr, 1000)?; + } - fuzzer - .fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr) - .expect("Error in the fuzzing loop"); + Ok(()) } diff --git a/libafl_v8/Cargo.toml b/libafl_v8/Cargo.toml index 0e1025cdfe..2b76c4331e 100644 --- a/libafl_v8/Cargo.toml +++ b/libafl_v8/Cargo.toml @@ -7,5 +7,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +ahash = { version = "0.7", features = ["compile-time-rng"] } +# fetch deno directly because of git submodules +deno_core = { git = "https://github.com/denoland/deno.git", tag = "v1.25.0" } +deno_runtime = { git = "https://github.com/denoland/deno.git", tag = "v1.25.0" } libafl = { path = "../libafl", version = "0.8.1" } -v8 = "0.49.0" \ No newline at end of file +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1.20.1", features = ["rt", "rt-multi-thread", "sync"] } diff --git a/libafl_v8/src/executors.rs b/libafl_v8/src/executors.rs index 9968fa0543..7041a9541c 100644 --- a/libafl_v8/src/executors.rs +++ b/libafl_v8/src/executors.rs @@ -3,98 +3,132 @@ use core::{ fmt::{Debug, Formatter}, marker::PhantomData, }; -use std::iter; +use std::{ + borrow::{Borrow, BorrowMut}, + cell::RefCell, + iter, + rc::Rc, + sync::Arc, +}; +use deno_core::{v8, JsRuntime, ModuleId, ModuleSpecifier}; +use deno_runtime::worker::MainWorker; use libafl::{ executors::{Executor, ExitKind, HasObservers}, inputs::{BytesInput, HasBytesVec, Input}, observers::ObserversTuple, + state::State, Error, }; -pub use v8; +use tokio::{runtime, runtime::Runtime}; use v8::{ - ArrayBuffer, ContextScope, Function, HandleScope, Local, Script, TryCatch, Uint8Array, Value, + ArrayBuffer, Context, ContextScope, Function, HandleScope, Local, Script, TryCatch, Uint8Array, + Value, }; -pub struct V8Executor<'s1, 's2, EM, I, OT, S, Z> +use crate::{v8::Global, Mutex}; + +pub struct V8Executor<'rt, EM, I, OT, S, Z> where I: Input + IntoJSValue, OT: ObserversTuple, + S: State, { - scope: ContextScope<'s1, HandleScope<'s2>>, - source: String, + id: ModuleId, observers: OT, + rt: &'rt Runtime, + worker: Arc>, phantom: PhantomData<(EM, I, S, Z)>, } -impl<'s1, 's2, EM, I, OT, S, Z> V8Executor<'s1, 's2, EM, I, OT, S, Z> +impl<'rt, EM, I, OT, S, Z> V8Executor<'rt, EM, I, OT, S, Z> where I: Input + IntoJSValue, OT: ObserversTuple, + S: State, { - /// Create a new V8 executor. You MUST invoke `initialize_v8` before using this method. + /// Create a new V8 executor. pub fn new( - mut scope: ContextScope<'s1, HandleScope<'s2>>, - source: &str, + rt: &'rt Runtime, + worker: Arc>, + main_module: ModuleSpecifier, observers: OT, - fuzzer: &mut Z, - state: &mut S, - mgr: &mut EM, + _fuzzer: &mut Z, + _state: &mut S, + _mgr: &mut EM, ) -> Result { - v8::V8::assert_initialized(); - // run the script to initialise the program - { - let code = v8::String::new(&mut scope, source).unwrap(); - - let mut scope = TryCatch::new(&mut scope); - - let script = Script::compile(&mut scope, code, None).unwrap(); - script.run(&mut scope); - - if let Some(err) = js_err_to_libafl(&mut scope) { - return Err(err); - } - } + let copy = worker.clone(); + let id = match rt.block_on(async { + let mut locked = copy.lock().await; + let mod_id = locked.preload_main_module(&main_module).await?; + let handle = locked.js_runtime.mod_evaluate(mod_id); + locked.run_event_loop(false).await?; + handle.await??; + + Ok::(mod_id) + }) { + Err(e) => return Err(Error::unknown(e.to_string())), + Ok(id) => id, + }; - get_harness_func(&mut scope)?; // check that the fuzz harness exists Ok(Self { - scope, - source: source.to_string(), + id, observers, + rt, + worker, phantom: Default::default(), }) } -} -fn get_harness_func<'s1, 's2>( - scope: &mut ContextScope<'s1, HandleScope<'s2>>, -) -> Result, Error> { - let global = scope.get_current_context().global(scope); - let harness_name = Local::from(v8::String::new(scope, "LLVMFuzzerTestOneInput").unwrap()); - let func = global.get(scope, harness_name); - let func = if let Some(func) = func { - func - } else { - return Err(Error::illegal_state( - "LLVMFuzzerTestOneInput not defined in JS harness", - )); - }; - - match Local::::try_from(func) { - Ok(func) => { - // good to go! - Ok(func) - } - _ => Err(Error::illegal_state( - "LLVMFuzzerTestOneInput is defined in JS harness, but wasn't a function", - )), + fn invoke_harness_func(&self, input: &I) -> Result { + let id = self.id; + let copy = self.worker.clone(); + self.rt.block_on(async { + let mut locked = copy.lock().await; + + let res = { + let module_namespace = locked.js_runtime.get_module_namespace(id).unwrap(); + let mut scope = locked.js_runtime.handle_scope(); + let module_namespace = Local::::new(&mut scope, module_namespace); + + let default_export_name = v8::String::new(&mut scope, "default").unwrap(); + let harness = module_namespace + .get(&mut scope, default_export_name.into()) + .unwrap(); + + match Local::::try_from(harness) { + Ok(func) => { + let recv = v8::undefined(&mut scope); + let try_catch = &mut TryCatch::new(&mut scope); + let input = input.to_js_value(try_catch)?; + let res = func.call(try_catch, recv.into(), &[input]); + if res.is_none() { + println!("{}", js_err_to_libafl(try_catch).unwrap().to_string()); + Ok(ExitKind::Crash) + } else { + Ok(ExitKind::Ok) + } + } + Err(e) => Err(Error::illegal_state(format!( + "The default export of the fuzz harness module is not a function: {}", + e.to_string(), + ))), + } + }; + + if let Err(e) = locked.run_event_loop(false).await { + return Err(Error::illegal_state(e.to_string())); + } + res + }) } } -impl<'s1, 's2, EM, I, OT, S, Z> Executor for V8Executor<'s1, 's2, EM, I, OT, S, Z> +impl<'rt, EM, I, OT, S, Z> Executor for V8Executor<'rt, EM, I, OT, S, Z> where I: Input + IntoJSValue, OT: ObserversTuple, + S: State, { fn run_target( &mut self, @@ -103,27 +137,15 @@ where _mgr: &mut EM, input: &I, ) -> Result { - let harness = get_harness_func(&mut self.scope)?; - - let scope = &mut HandleScope::new(&mut self.scope); - let recv = v8::undefined(scope); - let try_catch = &mut TryCatch::new(scope); - - let input = input.to_js_value(try_catch)?; - let res = if harness.call(try_catch, recv.into(), &[input]).is_none() { - Ok(ExitKind::Crash) - } else { - Ok(ExitKind::Ok) - }; - - res + self.invoke_harness_func(input) } } -impl<'s1, 's2, EM, I, OT, S, Z> HasObservers for V8Executor<'s1, 's2, EM, I, OT, S, Z> +impl<'rt, EM, I, OT, S, Z> HasObservers for V8Executor<'rt, EM, I, OT, S, Z> where I: Input + IntoJSValue, OT: ObserversTuple, + S: State, { fn observers(&self) -> &OT { &self.observers @@ -134,14 +156,14 @@ where } } -impl<'s1, 's2, EM, I, OT, S, Z> Debug for V8Executor<'s1, 's2, EM, I, OT, S, Z> +impl<'rt, EM, I, OT, S, Z> Debug for V8Executor<'rt, EM, I, OT, S, Z> where I: Input + IntoJSValue, OT: ObserversTuple, + S: State, { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("V8Executor") - .field("source", &self.source) .field("observers", &self.observers) .finish_non_exhaustive() } @@ -215,7 +237,6 @@ pub trait IntoJSValue { impl IntoJSValue for BytesInput { fn to_js_value<'s>(&self, scope: &mut HandleScope<'s>) -> Result, Error> { - println!("{}: {:?}", self.bytes().len(), self); let store = ArrayBuffer::new_backing_store_from_vec(Vec::from(self.bytes())).make_shared(); let buffer = ArrayBuffer::with_backing_store(scope, &store); let array = Uint8Array::new(scope, buffer, 0, self.bytes().len()).unwrap(); diff --git a/libafl_v8/src/handlers.rs b/libafl_v8/src/handlers.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/libafl_v8/src/lib.rs b/libafl_v8/src/lib.rs index 7d9828fd06..e9fb5acf0f 100644 --- a/libafl_v8/src/lib.rs +++ b/libafl_v8/src/lib.rs @@ -71,7 +71,16 @@ missing_docs, #![allow(clippy::borrow_deref_ref)] pub mod executors; -pub mod handlers; +pub mod observers; +pub use deno_core::{self, v8}; +pub use deno_runtime; pub use executors::*; -pub use handlers::*; +pub use observers::*; +pub use tokio::{runtime, sync::Mutex}; + +pub(crate) fn forbid_deserialization() -> T { + unimplemented!( + "Deserialization is forbidden for this type; cannot cross a serialization boundary" + ) +} diff --git a/libafl_v8/src/observers/json_types.rs b/libafl_v8/src/observers/json_types.rs new file mode 100644 index 0000000000..65c9b38edf --- /dev/null +++ b/libafl_v8/src/observers/json_types.rs @@ -0,0 +1,84 @@ +// Taken from: https://github.com/denoland/deno/blob/e96933bc163fd81a276cbc169b17f76724a5ac33/cli/tools/coverage/json_types.rs +// +// Full license text: +// +// MIT License +// +// Copyright 2018-2022 the Deno authors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CoverageRange { + /// Start character index. + #[serde(rename = "startOffset")] + pub start_char_offset: usize, + /// End character index. + #[serde(rename = "endOffset")] + pub end_char_offset: usize, + pub count: i64, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FunctionCoverage { + pub function_name: String, + pub ranges: Vec, + pub is_block_coverage: bool, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ScriptCoverage { + pub script_id: String, + pub url: String, + pub functions: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StartPreciseCoverageParameters { + pub call_count: bool, + pub detailed: bool, + pub allow_triggered_updates: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StartPreciseCoverageReturnObject { + pub timestamp: f64, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TakePreciseCoverageReturnObject { + pub result: Vec, + pub timestamp: f64, +} + +// TODO(bartlomieju): remove me +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProcessCoverage { + pub result: Vec, +} diff --git a/libafl_v8/src/observers/mod.rs b/libafl_v8/src/observers/mod.rs new file mode 100644 index 0000000000..c1d7a25c46 --- /dev/null +++ b/libafl_v8/src/observers/mod.rs @@ -0,0 +1,359 @@ +mod json_types; + +use core::{ + fmt::{Debug, Formatter}, + slice::Iter, +}; +use std::{ + borrow::BorrowMut, + cell::RefCell, + collections::{hash_map::Entry, HashMap}, + hash::{Hash, Hasher}, + rc::Rc, + sync::{Arc, Mutex as StdMutex}, +}; + +use ahash::{AHashMap, AHasher}; +use deno_core::{ + futures::TryFutureExt, serde_json::Value, JsRuntime, JsRuntimeInspector, LocalInspectorSession, +}; +use deno_runtime::worker::MainWorker; +use json_types::CoverageRange; +pub use json_types::{StartPreciseCoverageParameters, TakePreciseCoverageReturnObject}; +use libafl::{ + bolts::{AsIter, AsMutSlice, HasLen}, + executors::ExitKind, + observers::{MapObserver, Observer}, + prelude::Named, + Error, +}; +use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer}; +use tokio::{ + runtime::Runtime, + sync::{oneshot::channel, Mutex}, +}; + +use super::forbid_deserialization; + +// while collisions are theoretically possible, the likelihood is vanishingly small +#[derive(Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Clone)] +struct JSCoverageEntry { + script_hash: u64, + function_hash: u64, + start_char_offset: usize, + end_char_offset: usize, +} + +#[derive(Serialize, Deserialize, Default)] +struct JSCoverageMapper { + count: bool, + idx_map: HashMap, +} + +impl JSCoverageMapper { + fn new(count: bool) -> Self { + Self { + count, + idx_map: HashMap::new(), + } + } + + fn process_coverage(&mut self, coverage: TakePreciseCoverageReturnObject, map: &mut Vec) { + let len: usize = coverage + .result + .iter() + .flat_map(|scov| scov.functions.iter()) + .map(|fcov| fcov.ranges.len()) + .sum(); + + // pre-allocate + if map.capacity() < len { + map.reserve(len - map.len()); + } + + let count_computer = if self.count { + |count| match count { + count if count <= 0 => 0, + count if count > 255 => 255, + count => count as u8, + } + } else { + |count| match count { + 0 => 0, + _ => 1, + } + }; + coverage + .result + .into_iter() + .flat_map(|scov| { + let mut hasher = AHasher::default(); + scov.script_id.hash(&mut hasher); + let script_hash = hasher.finish(); + scov.functions + .into_iter() + .map(move |fcov| (script_hash, fcov)) + }) + .flat_map(|(script_hash, fcov)| { + let mut hasher = AHasher::default(); + fcov.function_name.hash(&mut hasher); + let function_hash = hasher.finish(); + fcov.ranges + .into_iter() + .map(move |rcov| (script_hash, function_hash, rcov)) + }) + .for_each(|(script_hash, function_hash, rcov)| { + let entry = JSCoverageEntry { + script_hash, + function_hash, + start_char_offset: rcov.start_char_offset, + end_char_offset: rcov.end_char_offset, + }; + let count_computed = count_computer(rcov.count); + match self.idx_map.entry(entry) { + Entry::Occupied(entry) => { + map[*entry.get()] = count_computed; + } + Entry::Vacant(entry) => { + entry.insert(map.len()); + map.push(count_computed); + } + } + }) + } +} + +#[derive(Serialize, Deserialize)] +pub struct JSMapObserver<'rt> { + initial: u8, + initialized: bool, + last_coverage: Vec, + mapper: JSCoverageMapper, + name: String, + params: StartPreciseCoverageParameters, + #[serde(skip, default = "forbid_deserialization")] + rt: &'rt Runtime, + #[serde(skip, default = "forbid_deserialization")] + worker: Arc>, + #[serde(skip, default = "forbid_deserialization")] + inspector: Arc>, +} + +impl<'rt> JSMapObserver<'rt> { + pub fn new( + name: &str, + rt: &'rt Runtime, + worker: Arc>, + ) -> Result { + Self::new_with_parameters( + name, + rt, + worker, + StartPreciseCoverageParameters { + call_count: true, + detailed: true, + allow_triggered_updates: false, + }, + ) + } + + pub fn new_with_parameters( + name: &str, + rt: &'rt Runtime, + worker: Arc>, + params: StartPreciseCoverageParameters, + ) -> Result { + let inspector = { + let copy = worker.clone(); + rt.block_on(async { + let mut locked = copy.lock().await; + let mut session = locked.create_inspector_session().await; + if let Err(e) = locked + .with_event_loop(Box::pin( + session.post_message::<()>("Profiler.enable", None), + )) + .await + { + Err(Error::unknown(e.to_string())) + } else { + Ok(session) + } + })? + }; + Ok(Self { + initial: u8::default(), + initialized: false, + last_coverage: Vec::new(), + mapper: JSCoverageMapper::new(params.call_count), + name: name.to_string(), + params, + rt, + worker, + inspector: Arc::new(Mutex::new(inspector)), + }) + } +} + +impl<'rt> Named for JSMapObserver<'rt> { + fn name(&self) -> &str { + &self.name + } +} + +impl<'rt> Debug for JSMapObserver<'rt> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_struct("JSMapObserver") + .field("initialized", &self.initialized) + .field("name", &self.name) + .field("last_coverage", &self.last_coverage) + .field("params", &self.params) + .finish_non_exhaustive() + } +} + +impl<'rt, I, S> Observer for JSMapObserver<'rt> { + fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { + self.reset_map()?; + if !self.initialized { + let inspector = self.inspector.clone(); + let params = self.params.clone(); + let copy = self.worker.clone(); + self.rt.block_on(async { + let mut locked = copy.lock().await; + let mut session = inspector.lock().await; + if let Err(e) = locked + .with_event_loop(Box::pin( + session.post_message("Profiler.startPreciseCoverage", Some(¶ms)), + )) + .await + { + Err(Error::unknown(e.to_string())) + } else { + Ok(()) + } + })?; + self.initialized = true; + } + Ok(()) + } + + fn post_exec(&mut self, _state: &mut S, _input: &I, exit_kind: &ExitKind) -> Result<(), Error> { + let session = self.inspector.clone(); + let copy = self.worker.clone(); + let coverage = self.rt.block_on(async { + let mut locked = copy.lock().await; + let mut session = session.lock().await; + match locked + .with_event_loop(Box::pin( + session.post_message::<()>("Profiler.takePreciseCoverage", None), + )) + .await + { + Ok(value) => Ok(serde_json::from_value(value)?), + Err(e) => return Err(Error::unknown(e.to_string())), + } + })?; + if *exit_kind == ExitKind::Crash { + println!("{:?}", coverage); + println!("{:?}", self.last_coverage.len()); + } + self.mapper + .process_coverage(coverage, &mut self.last_coverage); + Ok(()) + } + + fn pre_exec_child(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { + Err(Error::unsupported("Cannot be used in a forking context")) + } + + fn post_exec_child( + &mut self, + _state: &mut S, + _input: &I, + _exit_kind: &ExitKind, + ) -> Result<(), Error> { + Err(Error::unsupported("Cannot be used in a forking context")) + } +} + +impl<'rt> HasLen for JSMapObserver<'rt> { + fn len(&self) -> usize { + self.last_coverage.len() + } +} + +impl<'rt, 'it> AsIter<'it> for JSMapObserver<'rt> { + type Item = u8; + type IntoIter = Iter<'it, u8>; + + fn as_iter(&'it self) -> Self::IntoIter { + self.last_coverage.as_slice().iter() + } +} + +impl<'rt> AsMutSlice for JSMapObserver<'rt> { + fn as_mut_slice(&mut self) -> &mut [u8] { + self.last_coverage.as_mut_slice() + } +} + +impl<'rt> MapObserver for JSMapObserver<'rt> { + type Entry = u8; + + fn get(&self, idx: usize) -> &Self::Entry { + &self.last_coverage[idx] + } + + fn get_mut(&mut self, idx: usize) -> &mut Self::Entry { + &mut self.last_coverage[idx] + } + + fn usable_count(&self) -> usize { + self.last_coverage.len() + } + + fn count_bytes(&self) -> u64 { + self.last_coverage.iter().filter(|&&e| e != 0).count() as u64 + } + + fn hash(&self) -> u64 { + let mut hasher = AHasher::default(); + self.last_coverage.hash(&mut hasher); + hasher.finish() + } + + fn initial(&self) -> Self::Entry { + self.initial + } + + fn initial_mut(&mut self) -> &mut Self::Entry { + &mut self.initial + } + + fn reset_map(&mut self) -> Result<(), Error> { + let initial = self.initial(); + let cnt = self.usable_count(); + let map = self.last_coverage.as_mut_slice(); + for x in map[0..cnt].iter_mut() { + *x = initial; + } + Ok(()) + } + + fn to_vec(&self) -> Vec { + self.last_coverage.clone() + } + + fn how_many_set(&self, indexes: &[usize]) -> usize { + let initial = self.initial(); + let cnt = self.usable_count(); + let map = self.last_coverage.as_slice(); + let mut res = 0; + for i in indexes { + if *i < cnt && map[*i] != initial { + res += 1; + } + } + res + } +} From 176e3171679d23b1c7f02effef341e8de36b4cbd Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Sat, 27 Aug 2022 04:25:52 -0500 Subject: [PATCH 03/16] remove excess debug prints --- libafl_v8/src/executors.rs | 1 - libafl_v8/src/observers/mod.rs | 4 ---- 2 files changed, 5 deletions(-) diff --git a/libafl_v8/src/executors.rs b/libafl_v8/src/executors.rs index 7041a9541c..55e39a11b4 100644 --- a/libafl_v8/src/executors.rs +++ b/libafl_v8/src/executors.rs @@ -103,7 +103,6 @@ where let input = input.to_js_value(try_catch)?; let res = func.call(try_catch, recv.into(), &[input]); if res.is_none() { - println!("{}", js_err_to_libafl(try_catch).unwrap().to_string()); Ok(ExitKind::Crash) } else { Ok(ExitKind::Ok) diff --git a/libafl_v8/src/observers/mod.rs b/libafl_v8/src/observers/mod.rs index c1d7a25c46..33757e5ae8 100644 --- a/libafl_v8/src/observers/mod.rs +++ b/libafl_v8/src/observers/mod.rs @@ -253,10 +253,6 @@ impl<'rt, I, S> Observer for JSMapObserver<'rt> { Err(e) => return Err(Error::unknown(e.to_string())), } })?; - if *exit_kind == ExitKind::Crash { - println!("{:?}", coverage); - println!("{:?}", self.last_coverage.len()); - } self.mapper .process_coverage(coverage, &mut self.last_coverage); Ok(()) From a033c3be81f97afdc1940ce55217482c6b2b3a5b Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Sun, 28 Aug 2022 03:45:54 -0500 Subject: [PATCH 04/16] add docs + module loading --- .gitignore | 5 +- fuzzers/baby_fuzzer_v8/js/target.js | 7 +- libafl_v8/Cargo.toml | 8 +- libafl_v8/LICENSE-DENO | 20 +++++ libafl_v8/src/executors.rs | 41 +++------ libafl_v8/src/lib.rs | 6 ++ libafl_v8/src/loader.rs | 119 ++++++++++++++++++++++++++ libafl_v8/src/observers/json_types.rs | 34 ++------ libafl_v8/src/observers/mod.rs | 34 ++++---- libafl_v8/src/values.rs | 22 +++++ 10 files changed, 221 insertions(+), 75 deletions(-) create mode 100644 libafl_v8/LICENSE-DENO create mode 100644 libafl_v8/src/loader.rs create mode 100644 libafl_v8/src/values.rs diff --git a/.gitignore b/.gitignore index c79b27f4ef..310d29e721 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,7 @@ __pycache__ *atomic_file_testfile* **/libxml2 **/corpus_discovered -**/libxml2-*.tar.gz \ No newline at end of file +**/libxml2-*.tar.gz + +# JS dependency files +node_modules/ diff --git a/fuzzers/baby_fuzzer_v8/js/target.js b/fuzzers/baby_fuzzer_v8/js/target.js index 047bc82db2..9c1b1be40e 100644 --- a/fuzzers/baby_fuzzer_v8/js/target.js +++ b/fuzzers/baby_fuzzer_v8/js/target.js @@ -1,9 +1,10 @@ export default function(data) { - if (data.length > 1 && data[0] === 'A'.charCodeAt(0)) { + let array = new Uint8Array(data); + if (array.length > 1 && array[0] === 'A'.charCodeAt(0)) { console.log("howdy") - if (data.length > 2 && data[1] === 'B'.charCodeAt(0)) { + if (array.length > 2 && array[1] === 'B'.charCodeAt(0)) { console.log("kachowdy") - if (data.length > 2 && data[2] === 'C'.charCodeAt(0)) { + if (array.length > 2 && array[2] === 'C'.charCodeAt(0)) { throw "crash!"; } } diff --git a/libafl_v8/Cargo.toml b/libafl_v8/Cargo.toml index 2b76c4331e..ff344d9b36 100644 --- a/libafl_v8/Cargo.toml +++ b/libafl_v8/Cargo.toml @@ -8,9 +8,11 @@ edition = "2021" [dependencies] ahash = { version = "0.7", features = ["compile-time-rng"] } -# fetch deno directly because of git submodules -deno_core = { git = "https://github.com/denoland/deno.git", tag = "v1.25.0" } -deno_runtime = { git = "https://github.com/denoland/deno.git", tag = "v1.25.0" } +deno_ast = { version = "0.17.0", features = ["transpiling"] } +# fetch deno directly because of git deps; cannot use local submodule due to workspacing +deno_core = { git = "https://github.com/denoland/deno.git", tag = "v1.24.3" } +deno_runtime = { git = "https://github.com/denoland/deno.git", tag = "v1.24.3" } +import_map = "0.12.1" libafl = { path = "../libafl", version = "0.8.1" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/libafl_v8/LICENSE-DENO b/libafl_v8/LICENSE-DENO new file mode 100644 index 0000000000..41d09e30c8 --- /dev/null +++ b/libafl_v8/LICENSE-DENO @@ -0,0 +1,20 @@ +MIT License + +Copyright 2018-2022 the Deno authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/libafl_v8/src/executors.rs b/libafl_v8/src/executors.rs index 55e39a11b4..1459ac714d 100644 --- a/libafl_v8/src/executors.rs +++ b/libafl_v8/src/executors.rs @@ -1,33 +1,30 @@ +//! Executors for JavaScript targets +//! +//! Currently, the only provided executor is `V8Executor`, but additional executors for other +//! environments may be added later for different JavaScript contexts. + use core::{ fmt, fmt::{Debug, Formatter}, marker::PhantomData, }; -use std::{ - borrow::{Borrow, BorrowMut}, - cell::RefCell, - iter, - rc::Rc, - sync::Arc, -}; +use std::{iter, sync::Arc}; -use deno_core::{v8, JsRuntime, ModuleId, ModuleSpecifier}; +use deno_core::{v8, ModuleId, ModuleSpecifier}; use deno_runtime::worker::MainWorker; use libafl::{ executors::{Executor, ExitKind, HasObservers}, - inputs::{BytesInput, HasBytesVec, Input}, + inputs::Input, observers::ObserversTuple, state::State, Error, }; -use tokio::{runtime, runtime::Runtime}; -use v8::{ - ArrayBuffer, Context, ContextScope, Function, HandleScope, Local, Script, TryCatch, Uint8Array, - Value, -}; +use tokio::runtime::Runtime; +use v8::{Function, HandleScope, Local, TryCatch}; -use crate::{v8::Global, Mutex}; +use crate::{values::IntoJSValue, Mutex}; +/// Executor which executes JavaScript using Deno and V8. pub struct V8Executor<'rt, EM, I, OT, S, Z> where I: Input + IntoJSValue, @@ -168,6 +165,7 @@ where } } +#[allow(dead_code)] fn js_err_to_libafl(scope: &mut TryCatch) -> Option { if !scope.has_caught() { None @@ -229,16 +227,3 @@ fn js_err_to_libafl(scope: &mut TryCatch) -> Option { ))) } } - -pub trait IntoJSValue { - fn to_js_value<'s>(&self, scope: &mut HandleScope<'s>) -> Result, Error>; -} - -impl IntoJSValue for BytesInput { - fn to_js_value<'s>(&self, scope: &mut HandleScope<'s>) -> Result, Error> { - let store = ArrayBuffer::new_backing_store_from_vec(Vec::from(self.bytes())).make_shared(); - let buffer = ArrayBuffer::with_backing_store(scope, &store); - let array = Uint8Array::new(scope, buffer, 0, self.bytes().len()).unwrap(); - Ok(array.into()) - } -} diff --git a/libafl_v8/src/lib.rs b/libafl_v8/src/lib.rs index e9fb5acf0f..bb7197a00c 100644 --- a/libafl_v8/src/lib.rs +++ b/libafl_v8/src/lib.rs @@ -1,3 +1,5 @@ +//! libafl executors, observers, and other necessary components for fuzzing JavaScript targets. + // lints directly from main libafl #![allow(incomplete_features)] // For `type_eq` @@ -71,13 +73,17 @@ missing_docs, #![allow(clippy::borrow_deref_ref)] pub mod executors; +pub mod loader; pub mod observers; +pub mod values; pub use deno_core::{self, v8}; pub use deno_runtime; pub use executors::*; +pub use loader::*; pub use observers::*; pub use tokio::{runtime, sync::Mutex}; +pub use values::*; pub(crate) fn forbid_deserialization() -> T { unimplemented!( diff --git a/libafl_v8/src/loader.rs b/libafl_v8/src/loader.rs new file mode 100644 index 0000000000..450d703b06 --- /dev/null +++ b/libafl_v8/src/loader.rs @@ -0,0 +1,119 @@ +//! Loader stubs to be used to resolve JavaScript dependencies + +use std::{fs::read_to_string, path::Path, pin::Pin}; + +use deno_ast::{MediaType, ParseParams, SourceTextInfo}; +use deno_core::{ + anyhow::bail, futures::FutureExt, url::Url, ModuleLoader, ModuleSource, ModuleSourceFuture, + ModuleSpecifier, ModuleType, +}; +use import_map::ImportMapWithDiagnostics; +use libafl::Error; + +/// Loader which loads dependencies in vendored deno environments. +#[derive(Debug)] +pub struct VendoredLoader { + import_map: ImportMapWithDiagnostics, +} + +impl VendoredLoader { + /// Create a new vendored loader with a path to the vendor/import_map.json. + pub fn new>(map_path: P) -> Result { + let json = read_to_string(map_path.as_ref())?; + let url = match Url::from_file_path(map_path.as_ref()) { + Ok(u) => u, + Err(_) => { + return Err(Error::illegal_argument(format!( + "Path was not found or was not absolute: {}", + map_path.as_ref().to_str().unwrap() + ))) + } + }; + if let Ok(import_map) = import_map::parse_from_json(&url, &json) { + Ok(Self { import_map }) + } else { + Err(Error::illegal_state( + "Couldn't parse the provided import map", + )) + } + } + + fn read_file_specifier(module_specifier: ModuleSpecifier) -> Pin> { + if let Ok(path) = module_specifier.to_file_path() { + async move { + // Section surrounded by SNIP below is taken from: https://github.com/denoland/deno/blob/94d369ebc65a55bd9fbf378a765c8ed88a4efe2c/core/examples/ts_module_loader.rs#L51-L88 + // License text available at: ../LICENSE-DENO + // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + // ---- SNIP ---- + let media_type = MediaType::from(&path); + let (module_type, should_transpile) = match MediaType::from(&path) { + MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => { + (ModuleType::JavaScript, false) + } + MediaType::Jsx => (ModuleType::JavaScript, true), + MediaType::TypeScript + | MediaType::Mts + | MediaType::Cts + | MediaType::Dts + | MediaType::Dmts + | MediaType::Dcts + | MediaType::Tsx => (ModuleType::JavaScript, true), + MediaType::Json => (ModuleType::Json, false), + _ => bail!("Unknown extension {:?}", path.extension()), + }; + + let code = read_to_string(&path)?; + let code = if should_transpile { + let parsed = deno_ast::parse_module(ParseParams { + specifier: module_specifier.to_string(), + text_info: SourceTextInfo::from_string(code), + media_type, + capture_tokens: false, + scope_analysis: false, + maybe_syntax: None, + })?; + parsed.transpile(&Default::default())?.text + } else { + code + }; + let module = ModuleSource { + code: code.into_bytes().into_boxed_slice(), + module_type, + module_url_specified: module_specifier.to_string(), + module_url_found: module_specifier.to_string(), + }; + Ok(module) + // ---- SNIP ---- + } + .boxed_local() + } else { + unimplemented!("File URL couldn't be parsed") + } + } +} + +impl ModuleLoader for VendoredLoader { + fn resolve( + &self, + specifier: &str, + referrer: &str, + _is_main: bool, + ) -> Result { + let referrer = deno_core::resolve_url_or_path(referrer)?; + Ok(self.import_map.import_map.resolve(specifier, &referrer)?) + } + + fn load( + &self, + module_specifier: &ModuleSpecifier, + _maybe_referrer: Option, + _is_dyn_import: bool, + ) -> Pin> { + let module_specifier = module_specifier.clone(); + if module_specifier.scheme() == "file" { + Self::read_file_specifier(module_specifier) + } else { + unimplemented!("Not attempting to resolve non-file module specifiers") + } + } +} diff --git a/libafl_v8/src/observers/json_types.rs b/libafl_v8/src/observers/json_types.rs index 65c9b38edf..26cb8a7dba 100644 --- a/libafl_v8/src/observers/json_types.rs +++ b/libafl_v8/src/observers/json_types.rs @@ -1,28 +1,12 @@ -// Taken from: https://github.com/denoland/deno/blob/e96933bc163fd81a276cbc169b17f76724a5ac33/cli/tools/coverage/json_types.rs -// -// Full license text: -// -// MIT License -// -// Copyright 2018-2022 the Deno authors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +//! Structs which are used to interact with the Inspector API +//! +//! Source is unmodified from original. Refer to: https://chromedevtools.github.io/devtools-protocol/ +//! +//! Taken from: https://github.com/denoland/deno/blob/e96933bc163fd81a276cbc169b17f76724a5ac33/cli/tools/coverage/json_types.rs + +#![allow(missing_docs)] + +// License text available at: ../../LICENSE-DENO // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use serde::{Deserialize, Serialize}; diff --git a/libafl_v8/src/observers/mod.rs b/libafl_v8/src/observers/mod.rs index 33757e5ae8..9ac746724e 100644 --- a/libafl_v8/src/observers/mod.rs +++ b/libafl_v8/src/observers/mod.rs @@ -1,3 +1,5 @@ +//! Observers for JavaScript targets + mod json_types; use core::{ @@ -5,20 +7,14 @@ use core::{ slice::Iter, }; use std::{ - borrow::BorrowMut, - cell::RefCell, collections::{hash_map::Entry, HashMap}, hash::{Hash, Hasher}, - rc::Rc, - sync::{Arc, Mutex as StdMutex}, + sync::Arc, }; -use ahash::{AHashMap, AHasher}; -use deno_core::{ - futures::TryFutureExt, serde_json::Value, JsRuntime, JsRuntimeInspector, LocalInspectorSession, -}; +use ahash::AHasher; +use deno_core::LocalInspectorSession; use deno_runtime::worker::MainWorker; -use json_types::CoverageRange; pub use json_types::{StartPreciseCoverageParameters, TakePreciseCoverageReturnObject}; use libafl::{ bolts::{AsIter, AsMutSlice, HasLen}, @@ -27,11 +23,8 @@ use libafl::{ prelude::Named, Error, }; -use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer}; -use tokio::{ - runtime::Runtime, - sync::{oneshot::channel, Mutex}, -}; +use serde::{Deserialize, Serialize}; +use tokio::{runtime::Runtime, sync::Mutex}; use super::forbid_deserialization; @@ -123,6 +116,7 @@ impl JSCoverageMapper { } } +/// Observer which inspects JavaScript coverage at either a block or function level #[derive(Serialize, Deserialize)] pub struct JSMapObserver<'rt> { initial: u8, @@ -140,6 +134,9 @@ pub struct JSMapObserver<'rt> { } impl<'rt> JSMapObserver<'rt> { + /// Create the observer with the provided name to use the provided asynchronous runtime and JS + /// worker to push inspector data. If you don't know what kind of coverage you want, use this + /// constructor. pub fn new( name: &str, rt: &'rt Runtime, @@ -157,6 +154,8 @@ impl<'rt> JSMapObserver<'rt> { ) } + /// Create the observer with the provided name to use the provided asynchronous runtime, JS + /// worker to push inspector data, and the parameters with which coverage is collected. pub fn new_with_parameters( name: &str, rt: &'rt Runtime, @@ -237,7 +236,12 @@ impl<'rt, I, S> Observer for JSMapObserver<'rt> { Ok(()) } - fn post_exec(&mut self, _state: &mut S, _input: &I, exit_kind: &ExitKind) -> Result<(), Error> { + fn post_exec( + &mut self, + _state: &mut S, + _input: &I, + _exit_kind: &ExitKind, + ) -> Result<(), Error> { let session = self.inspector.clone(); let copy = self.worker.clone(); let coverage = self.rt.block_on(async { diff --git a/libafl_v8/src/values.rs b/libafl_v8/src/values.rs new file mode 100644 index 0000000000..3592e69101 --- /dev/null +++ b/libafl_v8/src/values.rs @@ -0,0 +1,22 @@ +//! Value types for inputs passed to JavaScript targets + +use libafl::{ + inputs::{BytesInput, HasBytesVec}, + Error, +}; + +use crate::v8::{ArrayBuffer, HandleScope, Local, Value}; + +/// Trait which converts an input into a JavaScript value. This value can be any JavaScript type. +pub trait IntoJSValue { + /// Convert this input into a JavaScript value in the provided scope. + fn to_js_value<'s>(&self, scope: &mut HandleScope<'s>) -> Result, Error>; +} + +impl IntoJSValue for BytesInput { + fn to_js_value<'s>(&self, scope: &mut HandleScope<'s>) -> Result, Error> { + let store = ArrayBuffer::new_backing_store_from_vec(Vec::from(self.bytes())).make_shared(); + let buffer = ArrayBuffer::with_backing_store(scope, &store); + Ok(buffer.into()) + } +} From 3a5304bdf2a741600e42940a1435c76dc2534cc0 Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Sun, 28 Aug 2022 17:42:32 -0500 Subject: [PATCH 05/16] add type observer --- libafl_v8/src/executors.rs | 72 +--- libafl_v8/src/lib.rs | 68 ++++ libafl_v8/src/observers/cov.rs | 356 +++++++++++++++++ .../{json_types.rs => inspector_api.rs} | 33 +- libafl_v8/src/observers/mod.rs | 361 +----------------- libafl_v8/src/observers/types.rs | 311 +++++++++++++++ 6 files changed, 779 insertions(+), 422 deletions(-) create mode 100644 libafl_v8/src/observers/cov.rs rename libafl_v8/src/observers/{json_types.rs => inspector_api.rs} (70%) create mode 100644 libafl_v8/src/observers/types.rs diff --git a/libafl_v8/src/executors.rs b/libafl_v8/src/executors.rs index 1459ac714d..2a01f03fd9 100644 --- a/libafl_v8/src/executors.rs +++ b/libafl_v8/src/executors.rs @@ -8,7 +8,7 @@ use core::{ fmt::{Debug, Formatter}, marker::PhantomData, }; -use std::{iter, sync::Arc}; +use std::sync::Arc; use deno_core::{v8, ModuleId, ModuleSpecifier}; use deno_runtime::worker::MainWorker; @@ -20,7 +20,7 @@ use libafl::{ Error, }; use tokio::runtime::Runtime; -use v8::{Function, HandleScope, Local, TryCatch}; +use v8::{Function, Local, TryCatch}; use crate::{values::IntoJSValue, Mutex}; @@ -118,6 +118,11 @@ where res }) } + + /// Fetches the ID of the main module for hooking + pub fn main_module_id(&self) -> ModuleId { + self.id + } } impl<'rt, EM, I, OT, S, Z> Executor for V8Executor<'rt, EM, I, OT, S, Z> @@ -164,66 +169,3 @@ where .finish_non_exhaustive() } } - -#[allow(dead_code)] -fn js_err_to_libafl(scope: &mut TryCatch) -> Option { - if !scope.has_caught() { - None - } else { - let exception = scope.exception().unwrap(); - let exception_string = exception - .to_string(scope) - .unwrap() - .to_rust_string_lossy(scope); - let message = if let Some(message) = scope.message() { - message - } else { - return Some(Error::illegal_state(format!( - "Provided script threw an error while executing: {}", - exception_string - ))); - }; - - let filename = message.get_script_resource_name(scope).map_or_else( - || "(unknown)".into(), - |s| s.to_string(scope).unwrap().to_rust_string_lossy(scope), - ); - let line_number = message.get_line_number(scope).unwrap_or_default(); - - let source_line = message - .get_source_line(scope) - .map(|s| s.to_string(scope).unwrap().to_rust_string_lossy(scope)) - .unwrap(); - - let start_column = message.get_start_column(); - let end_column = message.get_end_column(); - - let err_underline = iter::repeat(' ') - .take(start_column) - .chain(iter::repeat('^').take(end_column - start_column)) - .collect::(); - - if let Some(stack_trace) = scope.stack_trace() { - let stack_trace = unsafe { Local::::cast(stack_trace) }; - let stack_trace = stack_trace - .to_string(scope) - .map(|s| s.to_rust_string_lossy(scope)); - - if let Some(stack_trace) = stack_trace { - return Some(Error::illegal_state(format!( - "Encountered uncaught JS exception while executing: {}:{}: {}\n{}\n{}\n{}", - filename, - line_number, - exception_string, - source_line, - err_underline, - stack_trace - ))); - } - } - Some(Error::illegal_state(format!( - "Encountered uncaught JS exception while executing: {}:{}: {}\n{}\n{}", - filename, line_number, exception_string, source_line, err_underline - ))) - } -} diff --git a/libafl_v8/src/lib.rs b/libafl_v8/src/lib.rs index bb7197a00c..113b843ea6 100644 --- a/libafl_v8/src/lib.rs +++ b/libafl_v8/src/lib.rs @@ -77,16 +77,84 @@ pub mod loader; pub mod observers; pub mod values; +use std::iter; + pub use deno_core::{self, v8}; pub use deno_runtime; pub use executors::*; +use libafl::Error; pub use loader::*; pub use observers::*; pub use tokio::{runtime, sync::Mutex}; pub use values::*; +use crate::v8::{HandleScope, Local, TryCatch}; + pub(crate) fn forbid_deserialization() -> T { unimplemented!( "Deserialization is forbidden for this type; cannot cross a serialization boundary" ) } + +/// Convert a JS error from a try/catch scope into a libafl error +pub fn js_err_to_libafl(scope: &mut TryCatch) -> Option { + if !scope.has_caught() { + None + } else { + let exception = scope.exception().unwrap(); + let exception_string = exception + .to_string(scope) + .unwrap() + .to_rust_string_lossy(scope); + let message = if let Some(message) = scope.message() { + message + } else { + return Some(Error::illegal_state(format!( + "Provided script threw an error while executing: {}", + exception_string + ))); + }; + + let filename = message.get_script_resource_name(scope).map_or_else( + || "(unknown)".into(), + |s| s.to_string(scope).unwrap().to_rust_string_lossy(scope), + ); + let line_number = message.get_line_number(scope).unwrap_or_default(); + + let source_line = message + .get_source_line(scope) + .map(|s| s.to_string(scope).unwrap().to_rust_string_lossy(scope)) + .unwrap(); + + let start_column = message.get_start_column(); + let end_column = message.get_end_column(); + + let err_underline = iter::repeat(' ') + .take(start_column) + .chain(iter::repeat('^').take(end_column - start_column)) + .collect::(); + + if let Some(stack_trace) = scope.stack_trace() { + let stack_trace = unsafe { Local::::cast(stack_trace) }; + let stack_trace = stack_trace + .to_string(scope) + .map(|s| s.to_rust_string_lossy(scope)); + + if let Some(stack_trace) = stack_trace { + return Some(Error::illegal_state(format!( + "Encountered uncaught JS exception while executing: {}:{}: {}\n{}\n{}\n{}", + filename, + line_number, + exception_string, + source_line, + err_underline, + stack_trace + ))); + } + } + Some(Error::illegal_state(format!( + "Encountered uncaught JS exception while executing: {}:{}: {}\n{}\n{}", + filename, line_number, exception_string, source_line, err_underline + ))) + } +} diff --git a/libafl_v8/src/observers/cov.rs b/libafl_v8/src/observers/cov.rs new file mode 100644 index 0000000000..5af54c32c9 --- /dev/null +++ b/libafl_v8/src/observers/cov.rs @@ -0,0 +1,356 @@ +use core::{ + fmt::{Debug, Formatter}, + slice::Iter, +}; +use std::{ + collections::{hash_map::Entry, HashMap}, + hash::{Hash, Hasher}, + sync::Arc, +}; + +use ahash::AHasher; +use deno_core::LocalInspectorSession; +use deno_runtime::worker::MainWorker; +use libafl::{ + bolts::{AsIter, AsMutSlice, HasLen}, + executors::ExitKind, + observers::{MapObserver, Observer}, + prelude::Named, + Error, +}; +use serde::{Deserialize, Serialize}; +use tokio::{runtime::Runtime, sync::Mutex}; + +pub use super::inspector_api::StartPreciseCoverageParameters; +use super::inspector_api::TakePreciseCoverageReturnObject; +use crate::forbid_deserialization; + +// while collisions are theoretically possible, the likelihood is vanishingly small +#[derive(Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Clone)] +struct JSCoverageEntry { + script_hash: u64, + function_hash: u64, + start_char_offset: usize, + end_char_offset: usize, +} + +#[derive(Serialize, Deserialize, Default)] +struct JSCoverageMapper { + count: bool, + idx_map: HashMap, +} + +impl JSCoverageMapper { + fn new(count: bool) -> Self { + Self { + count, + idx_map: HashMap::new(), + } + } + + fn process_coverage(&mut self, coverage: TakePreciseCoverageReturnObject, map: &mut Vec) { + let len: usize = coverage + .result + .iter() + .flat_map(|scov| scov.functions.iter()) + .map(|fcov| fcov.ranges.len()) + .sum(); + + // pre-allocate + if map.capacity() < len { + map.reserve(len - map.len()); + } + + let count_computer = if self.count { + |count| match count { + count if count <= 0 => 0, + count if count > 255 => 255, + count => count as u8, + } + } else { + |count| match count { + 0 => 0, + _ => 1, + } + }; + coverage + .result + .into_iter() + .flat_map(|scov| { + let mut hasher = AHasher::default(); + scov.script_id.hash(&mut hasher); + let script_hash = hasher.finish(); + scov.functions + .into_iter() + .map(move |fcov| (script_hash, fcov)) + }) + .flat_map(|(script_hash, fcov)| { + let mut hasher = AHasher::default(); + fcov.function_name.hash(&mut hasher); + let function_hash = hasher.finish(); + fcov.ranges + .into_iter() + .map(move |rcov| (script_hash, function_hash, rcov)) + }) + .for_each(|(script_hash, function_hash, rcov)| { + let entry = JSCoverageEntry { + script_hash, + function_hash, + start_char_offset: rcov.start_char_offset, + end_char_offset: rcov.end_char_offset, + }; + let count_computed = count_computer(rcov.count); + match self.idx_map.entry(entry) { + Entry::Occupied(entry) => { + map[*entry.get()] = count_computed; + } + Entry::Vacant(entry) => { + entry.insert(map.len()); + map.push(count_computed); + } + } + }) + } +} + +/// Observer which inspects JavaScript coverage at either a block or function level +#[derive(Serialize, Deserialize)] +pub struct JSMapObserver<'rt> { + initial: u8, + initialized: bool, + last_coverage: Vec, + mapper: JSCoverageMapper, + name: String, + params: StartPreciseCoverageParameters, + #[serde(skip, default = "forbid_deserialization")] + rt: &'rt Runtime, + #[serde(skip, default = "forbid_deserialization")] + worker: Arc>, + #[serde(skip, default = "forbid_deserialization")] + inspector: Arc>, +} + +impl<'rt> JSMapObserver<'rt> { + /// Create the observer with the provided name to use the provided asynchronous runtime and JS + /// worker to push inspector data. If you don't know what kind of coverage you want, use this + /// constructor. + pub fn new( + name: &str, + rt: &'rt Runtime, + worker: Arc>, + ) -> Result { + Self::new_with_parameters( + name, + rt, + worker, + StartPreciseCoverageParameters { + call_count: true, + detailed: true, + allow_triggered_updates: false, + }, + ) + } + + /// Create the observer with the provided name to use the provided asynchronous runtime, JS + /// worker to push inspector data, and the parameters with which coverage is collected. + pub fn new_with_parameters( + name: &str, + rt: &'rt Runtime, + worker: Arc>, + params: StartPreciseCoverageParameters, + ) -> Result { + let inspector = { + let copy = worker.clone(); + rt.block_on(async { + let mut locked = copy.lock().await; + let mut session = locked.create_inspector_session().await; + if let Err(e) = locked + .with_event_loop(Box::pin( + session.post_message::<()>("Profiler.enable", None), + )) + .await + { + Err(Error::unknown(e.to_string())) + } else { + Ok(session) + } + })? + }; + Ok(Self { + initial: u8::default(), + initialized: false, + last_coverage: Vec::new(), + mapper: JSCoverageMapper::new(params.call_count), + name: name.to_string(), + params, + rt, + worker, + inspector: Arc::new(Mutex::new(inspector)), + }) + } +} + +impl<'rt> Named for JSMapObserver<'rt> { + fn name(&self) -> &str { + &self.name + } +} + +impl<'rt> Debug for JSMapObserver<'rt> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_struct("JSMapObserver") + .field("initialized", &self.initialized) + .field("name", &self.name) + .field("last_coverage", &self.last_coverage) + .field("params", &self.params) + .finish_non_exhaustive() + } +} + +impl<'rt, I, S> Observer for JSMapObserver<'rt> { + fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { + self.reset_map()?; + if !self.initialized { + let inspector = self.inspector.clone(); + let params = self.params.clone(); + let copy = self.worker.clone(); + self.rt.block_on(async { + let mut locked = copy.lock().await; + let mut session = inspector.lock().await; + if let Err(e) = locked + .with_event_loop(Box::pin( + session.post_message("Profiler.startPreciseCoverage", Some(¶ms)), + )) + .await + { + Err(Error::unknown(e.to_string())) + } else { + Ok(()) + } + })?; + self.initialized = true; + } + Ok(()) + } + + fn post_exec( + &mut self, + _state: &mut S, + _input: &I, + _exit_kind: &ExitKind, + ) -> Result<(), Error> { + let session = self.inspector.clone(); + let copy = self.worker.clone(); + let coverage = self.rt.block_on(async { + let mut locked = copy.lock().await; + let mut session = session.lock().await; + match locked + .with_event_loop(Box::pin( + session.post_message::<()>("Profiler.takePreciseCoverage", None), + )) + .await + { + Ok(value) => Ok(serde_json::from_value(value)?), + Err(e) => return Err(Error::unknown(e.to_string())), + } + })?; + self.mapper + .process_coverage(coverage, &mut self.last_coverage); + Ok(()) + } + + fn pre_exec_child(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { + Err(Error::unsupported("Cannot be used in a forking context")) + } + + fn post_exec_child( + &mut self, + _state: &mut S, + _input: &I, + _exit_kind: &ExitKind, + ) -> Result<(), Error> { + Err(Error::unsupported("Cannot be used in a forking context")) + } +} + +impl<'rt> HasLen for JSMapObserver<'rt> { + fn len(&self) -> usize { + self.last_coverage.len() + } +} + +impl<'rt, 'it> AsIter<'it> for JSMapObserver<'rt> { + type Item = u8; + type IntoIter = Iter<'it, u8>; + + fn as_iter(&'it self) -> Self::IntoIter { + self.last_coverage.as_slice().iter() + } +} + +impl<'rt> AsMutSlice for JSMapObserver<'rt> { + fn as_mut_slice(&mut self) -> &mut [u8] { + self.last_coverage.as_mut_slice() + } +} + +impl<'rt> MapObserver for JSMapObserver<'rt> { + type Entry = u8; + + fn get(&self, idx: usize) -> &Self::Entry { + &self.last_coverage[idx] + } + + fn get_mut(&mut self, idx: usize) -> &mut Self::Entry { + &mut self.last_coverage[idx] + } + + fn usable_count(&self) -> usize { + self.last_coverage.len() + } + + fn count_bytes(&self) -> u64 { + self.last_coverage.iter().filter(|&&e| e != 0).count() as u64 + } + + fn hash(&self) -> u64 { + let mut hasher = AHasher::default(); + self.last_coverage.hash(&mut hasher); + hasher.finish() + } + + fn initial(&self) -> Self::Entry { + self.initial + } + + fn initial_mut(&mut self) -> &mut Self::Entry { + &mut self.initial + } + + fn reset_map(&mut self) -> Result<(), Error> { + let initial = self.initial(); + let cnt = self.usable_count(); + let map = self.last_coverage.as_mut_slice(); + for x in map[0..cnt].iter_mut() { + *x = initial; + } + Ok(()) + } + + fn to_vec(&self) -> Vec { + self.last_coverage.clone() + } + + fn how_many_set(&self, indexes: &[usize]) -> usize { + let initial = self.initial(); + let cnt = self.usable_count(); + let map = self.last_coverage.as_slice(); + let mut res = 0; + for i in indexes { + if *i < cnt && map[*i] != initial { + res += 1; + } + } + res + } +} diff --git a/libafl_v8/src/observers/json_types.rs b/libafl_v8/src/observers/inspector_api.rs similarity index 70% rename from libafl_v8/src/observers/json_types.rs rename to libafl_v8/src/observers/inspector_api.rs index 26cb8a7dba..d521adeebe 100644 --- a/libafl_v8/src/observers/json_types.rs +++ b/libafl_v8/src/observers/inspector_api.rs @@ -1,6 +1,6 @@ //! Structs which are used to interact with the Inspector API //! -//! Source is unmodified from original. Refer to: https://chromedevtools.github.io/devtools-protocol/ +//! Snipped region is unmodified from original. Refer to: https://chromedevtools.github.io/devtools-protocol/ //! //! Taken from: https://github.com/denoland/deno/blob/e96933bc163fd81a276cbc169b17f76724a5ac33/cli/tools/coverage/json_types.rs @@ -11,6 +11,8 @@ use serde::{Deserialize, Serialize}; +// ---- SNIP ---- + #[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct CoverageRange { @@ -66,3 +68,32 @@ pub struct TakePreciseCoverageReturnObject { pub struct ProcessCoverage { pub result: Vec, } + +// ---- SNIP ---- + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TakeTypeProfileReturnObject { + pub result: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ScriptTypeProfile { + pub script_id: String, + pub url: String, + pub entries: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TypeProfileEntry { + pub offset: usize, + pub types: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TypeObject { + pub name: String, +} diff --git a/libafl_v8/src/observers/mod.rs b/libafl_v8/src/observers/mod.rs index 9ac746724e..32738c1c11 100644 --- a/libafl_v8/src/observers/mod.rs +++ b/libafl_v8/src/observers/mod.rs @@ -1,359 +1,8 @@ //! Observers for JavaScript targets -mod json_types; +mod cov; +mod inspector_api; +mod types; -use core::{ - fmt::{Debug, Formatter}, - slice::Iter, -}; -use std::{ - collections::{hash_map::Entry, HashMap}, - hash::{Hash, Hasher}, - sync::Arc, -}; - -use ahash::AHasher; -use deno_core::LocalInspectorSession; -use deno_runtime::worker::MainWorker; -pub use json_types::{StartPreciseCoverageParameters, TakePreciseCoverageReturnObject}; -use libafl::{ - bolts::{AsIter, AsMutSlice, HasLen}, - executors::ExitKind, - observers::{MapObserver, Observer}, - prelude::Named, - Error, -}; -use serde::{Deserialize, Serialize}; -use tokio::{runtime::Runtime, sync::Mutex}; - -use super::forbid_deserialization; - -// while collisions are theoretically possible, the likelihood is vanishingly small -#[derive(Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Clone)] -struct JSCoverageEntry { - script_hash: u64, - function_hash: u64, - start_char_offset: usize, - end_char_offset: usize, -} - -#[derive(Serialize, Deserialize, Default)] -struct JSCoverageMapper { - count: bool, - idx_map: HashMap, -} - -impl JSCoverageMapper { - fn new(count: bool) -> Self { - Self { - count, - idx_map: HashMap::new(), - } - } - - fn process_coverage(&mut self, coverage: TakePreciseCoverageReturnObject, map: &mut Vec) { - let len: usize = coverage - .result - .iter() - .flat_map(|scov| scov.functions.iter()) - .map(|fcov| fcov.ranges.len()) - .sum(); - - // pre-allocate - if map.capacity() < len { - map.reserve(len - map.len()); - } - - let count_computer = if self.count { - |count| match count { - count if count <= 0 => 0, - count if count > 255 => 255, - count => count as u8, - } - } else { - |count| match count { - 0 => 0, - _ => 1, - } - }; - coverage - .result - .into_iter() - .flat_map(|scov| { - let mut hasher = AHasher::default(); - scov.script_id.hash(&mut hasher); - let script_hash = hasher.finish(); - scov.functions - .into_iter() - .map(move |fcov| (script_hash, fcov)) - }) - .flat_map(|(script_hash, fcov)| { - let mut hasher = AHasher::default(); - fcov.function_name.hash(&mut hasher); - let function_hash = hasher.finish(); - fcov.ranges - .into_iter() - .map(move |rcov| (script_hash, function_hash, rcov)) - }) - .for_each(|(script_hash, function_hash, rcov)| { - let entry = JSCoverageEntry { - script_hash, - function_hash, - start_char_offset: rcov.start_char_offset, - end_char_offset: rcov.end_char_offset, - }; - let count_computed = count_computer(rcov.count); - match self.idx_map.entry(entry) { - Entry::Occupied(entry) => { - map[*entry.get()] = count_computed; - } - Entry::Vacant(entry) => { - entry.insert(map.len()); - map.push(count_computed); - } - } - }) - } -} - -/// Observer which inspects JavaScript coverage at either a block or function level -#[derive(Serialize, Deserialize)] -pub struct JSMapObserver<'rt> { - initial: u8, - initialized: bool, - last_coverage: Vec, - mapper: JSCoverageMapper, - name: String, - params: StartPreciseCoverageParameters, - #[serde(skip, default = "forbid_deserialization")] - rt: &'rt Runtime, - #[serde(skip, default = "forbid_deserialization")] - worker: Arc>, - #[serde(skip, default = "forbid_deserialization")] - inspector: Arc>, -} - -impl<'rt> JSMapObserver<'rt> { - /// Create the observer with the provided name to use the provided asynchronous runtime and JS - /// worker to push inspector data. If you don't know what kind of coverage you want, use this - /// constructor. - pub fn new( - name: &str, - rt: &'rt Runtime, - worker: Arc>, - ) -> Result { - Self::new_with_parameters( - name, - rt, - worker, - StartPreciseCoverageParameters { - call_count: true, - detailed: true, - allow_triggered_updates: false, - }, - ) - } - - /// Create the observer with the provided name to use the provided asynchronous runtime, JS - /// worker to push inspector data, and the parameters with which coverage is collected. - pub fn new_with_parameters( - name: &str, - rt: &'rt Runtime, - worker: Arc>, - params: StartPreciseCoverageParameters, - ) -> Result { - let inspector = { - let copy = worker.clone(); - rt.block_on(async { - let mut locked = copy.lock().await; - let mut session = locked.create_inspector_session().await; - if let Err(e) = locked - .with_event_loop(Box::pin( - session.post_message::<()>("Profiler.enable", None), - )) - .await - { - Err(Error::unknown(e.to_string())) - } else { - Ok(session) - } - })? - }; - Ok(Self { - initial: u8::default(), - initialized: false, - last_coverage: Vec::new(), - mapper: JSCoverageMapper::new(params.call_count), - name: name.to_string(), - params, - rt, - worker, - inspector: Arc::new(Mutex::new(inspector)), - }) - } -} - -impl<'rt> Named for JSMapObserver<'rt> { - fn name(&self) -> &str { - &self.name - } -} - -impl<'rt> Debug for JSMapObserver<'rt> { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.debug_struct("JSMapObserver") - .field("initialized", &self.initialized) - .field("name", &self.name) - .field("last_coverage", &self.last_coverage) - .field("params", &self.params) - .finish_non_exhaustive() - } -} - -impl<'rt, I, S> Observer for JSMapObserver<'rt> { - fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { - self.reset_map()?; - if !self.initialized { - let inspector = self.inspector.clone(); - let params = self.params.clone(); - let copy = self.worker.clone(); - self.rt.block_on(async { - let mut locked = copy.lock().await; - let mut session = inspector.lock().await; - if let Err(e) = locked - .with_event_loop(Box::pin( - session.post_message("Profiler.startPreciseCoverage", Some(¶ms)), - )) - .await - { - Err(Error::unknown(e.to_string())) - } else { - Ok(()) - } - })?; - self.initialized = true; - } - Ok(()) - } - - fn post_exec( - &mut self, - _state: &mut S, - _input: &I, - _exit_kind: &ExitKind, - ) -> Result<(), Error> { - let session = self.inspector.clone(); - let copy = self.worker.clone(); - let coverage = self.rt.block_on(async { - let mut locked = copy.lock().await; - let mut session = session.lock().await; - match locked - .with_event_loop(Box::pin( - session.post_message::<()>("Profiler.takePreciseCoverage", None), - )) - .await - { - Ok(value) => Ok(serde_json::from_value(value)?), - Err(e) => return Err(Error::unknown(e.to_string())), - } - })?; - self.mapper - .process_coverage(coverage, &mut self.last_coverage); - Ok(()) - } - - fn pre_exec_child(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { - Err(Error::unsupported("Cannot be used in a forking context")) - } - - fn post_exec_child( - &mut self, - _state: &mut S, - _input: &I, - _exit_kind: &ExitKind, - ) -> Result<(), Error> { - Err(Error::unsupported("Cannot be used in a forking context")) - } -} - -impl<'rt> HasLen for JSMapObserver<'rt> { - fn len(&self) -> usize { - self.last_coverage.len() - } -} - -impl<'rt, 'it> AsIter<'it> for JSMapObserver<'rt> { - type Item = u8; - type IntoIter = Iter<'it, u8>; - - fn as_iter(&'it self) -> Self::IntoIter { - self.last_coverage.as_slice().iter() - } -} - -impl<'rt> AsMutSlice for JSMapObserver<'rt> { - fn as_mut_slice(&mut self) -> &mut [u8] { - self.last_coverage.as_mut_slice() - } -} - -impl<'rt> MapObserver for JSMapObserver<'rt> { - type Entry = u8; - - fn get(&self, idx: usize) -> &Self::Entry { - &self.last_coverage[idx] - } - - fn get_mut(&mut self, idx: usize) -> &mut Self::Entry { - &mut self.last_coverage[idx] - } - - fn usable_count(&self) -> usize { - self.last_coverage.len() - } - - fn count_bytes(&self) -> u64 { - self.last_coverage.iter().filter(|&&e| e != 0).count() as u64 - } - - fn hash(&self) -> u64 { - let mut hasher = AHasher::default(); - self.last_coverage.hash(&mut hasher); - hasher.finish() - } - - fn initial(&self) -> Self::Entry { - self.initial - } - - fn initial_mut(&mut self) -> &mut Self::Entry { - &mut self.initial - } - - fn reset_map(&mut self) -> Result<(), Error> { - let initial = self.initial(); - let cnt = self.usable_count(); - let map = self.last_coverage.as_mut_slice(); - for x in map[0..cnt].iter_mut() { - *x = initial; - } - Ok(()) - } - - fn to_vec(&self) -> Vec { - self.last_coverage.clone() - } - - fn how_many_set(&self, indexes: &[usize]) -> usize { - let initial = self.initial(); - let cnt = self.usable_count(); - let map = self.last_coverage.as_slice(); - let mut res = 0; - for i in indexes { - if *i < cnt && map[*i] != initial { - res += 1; - } - } - res - } -} +pub use cov::*; +pub use types::*; diff --git a/libafl_v8/src/observers/types.rs b/libafl_v8/src/observers/types.rs new file mode 100644 index 0000000000..c71d472d37 --- /dev/null +++ b/libafl_v8/src/observers/types.rs @@ -0,0 +1,311 @@ +use std::{ + collections::{hash_map::Entry, HashMap}, + fmt::{Debug, Formatter}, + hash::{Hash, Hasher}, + slice::Iter, + sync::Arc, +}; + +use ahash::AHasher; +use deno_core::LocalInspectorSession; +use deno_runtime::worker::MainWorker; +use libafl::{ + bolts::{AsIter, AsMutSlice, HasLen}, + executors::ExitKind, + observers::{MapObserver, Observer}, + prelude::Named, + Error, +}; +use serde::{Deserialize, Serialize}; +use tokio::{runtime::Runtime, sync::Mutex}; + +use crate::{forbid_deserialization, observers::inspector_api::TakeTypeProfileReturnObject}; + +// while collisions are theoretically possible, the likelihood is vanishingly small +#[derive(Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Clone)] +struct JSTypeEntry { + script_hash: u64, + offset: usize, + name_hash: u64, +} + +#[derive(Serialize, Deserialize, Default)] +struct JSTypeMapper { + idx_map: HashMap, +} + +impl JSTypeMapper { + fn new() -> Self { + Self { + idx_map: HashMap::new(), + } + } + + fn process_coverage(&mut self, coverage: TakeTypeProfileReturnObject, map: &mut Vec) { + let len: usize = coverage + .result + .iter() + .flat_map(|stp| stp.entries.iter()) + .map(|entry| entry.types.len()) + .sum(); + + // pre-allocate + if map.capacity() < len { + map.reserve(len - map.len()); + } + + coverage + .result + .into_iter() + .flat_map(|stp| { + let mut hasher = AHasher::default(); + stp.script_id.hash(&mut hasher); + let script_hash = hasher.finish(); + stp.entries + .into_iter() + .map(move |entry| (script_hash, entry)) + }) + .flat_map(|(script_hash, entry)| { + entry + .types + .into_iter() + .map(move |r#type| (script_hash, entry.offset, r#type.name)) + }) + .for_each(|(script_hash, offset, name)| { + let mut hasher = AHasher::default(); + name.hash(&mut hasher); + let name_hash = hasher.finish(); + let entry = JSTypeEntry { + script_hash, + offset, + name_hash, + }; + match self.idx_map.entry(entry) { + Entry::Occupied(entry) => { + map[*entry.get()] = 1; + } + Entry::Vacant(entry) => { + entry.insert(map.len()); + map.push(1); + } + } + }) + } +} + +/// Observer which inspects JavaScript type usage for parameters and return values +#[derive(Serialize, Deserialize)] +pub struct JSTypeObserver<'rt> { + initial: u8, + initialized: bool, + last_coverage: Vec, + name: String, + mapper: JSTypeMapper, + #[serde(skip, default = "forbid_deserialization")] + rt: &'rt Runtime, + #[serde(skip, default = "forbid_deserialization")] + worker: Arc>, + #[serde(skip, default = "forbid_deserialization")] + inspector: Arc>, +} + +impl<'rt> JSTypeObserver<'rt> { + /// Create the observer with the provided name to use the provided asynchronous runtime, JS + /// worker to push inspector data, and the parameters with which coverage is collected. + pub fn new( + name: &str, + rt: &'rt Runtime, + worker: Arc>, + ) -> Result { + let inspector = { + let copy = worker.clone(); + rt.block_on(async { + let mut locked = copy.lock().await; + let mut session = locked.create_inspector_session().await; + if let Err(e) = locked + .with_event_loop(Box::pin( + session.post_message::<()>("Profiler.enable", None), + )) + .await + { + Err(Error::unknown(e.to_string())) + } else { + Ok(session) + } + })? + }; + Ok(Self { + initial: u8::default(), + initialized: false, + last_coverage: Vec::new(), + name: name.to_string(), + mapper: JSTypeMapper::new(), + rt, + worker, + inspector: Arc::new(Mutex::new(inspector)), + }) + } +} + +impl<'rt> Named for JSTypeObserver<'rt> { + fn name(&self) -> &str { + &self.name + } +} + +impl<'rt> Debug for JSTypeObserver<'rt> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JSTypeObserver") + .field("initialized", &self.initialized) + .field("name", &self.name) + .field("last_coverage", &self.last_coverage) + .finish_non_exhaustive() + } +} + +impl<'rt, I, S> Observer for JSTypeObserver<'rt> { + fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { + self.reset_map()?; + if !self.initialized { + let inspector = self.inspector.clone(); + let copy = self.worker.clone(); + self.rt.block_on(async { + let mut locked = copy.lock().await; + let mut session = inspector.lock().await; + if let Err(e) = locked + .with_event_loop(Box::pin( + session.post_message::<()>("Profiler.startTypeProfile", None), + )) + .await + { + Err(Error::unknown(e.to_string())) + } else { + Ok(()) + } + })?; + self.initialized = true; + } + Ok(()) + } + + fn post_exec( + &mut self, + _state: &mut S, + _input: &I, + _exit_kind: &ExitKind, + ) -> Result<(), Error> { + let session = self.inspector.clone(); + let copy = self.worker.clone(); + let coverage = self.rt.block_on(async { + let mut locked = copy.lock().await; + let mut session = session.lock().await; + match locked + .with_event_loop(Box::pin( + session.post_message::<()>("Profiler.takeTypeProfile", None), + )) + .await + { + Ok(value) => Ok(serde_json::from_value(value)?), + Err(e) => return Err(Error::unknown(e.to_string())), + } + })?; + self.mapper + .process_coverage(coverage, &mut self.last_coverage); + Ok(()) + } + + fn pre_exec_child(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { + Err(Error::unsupported("Cannot be used in a forking context")) + } + + fn post_exec_child( + &mut self, + _state: &mut S, + _input: &I, + _exit_kind: &ExitKind, + ) -> Result<(), Error> { + Err(Error::unsupported("Cannot be used in a forking context")) + } +} + +impl<'rt> HasLen for JSTypeObserver<'rt> { + fn len(&self) -> usize { + self.last_coverage.len() + } +} + +impl<'rt, 'it> AsIter<'it> for JSTypeObserver<'rt> { + type Item = u8; + type IntoIter = Iter<'it, u8>; + + fn as_iter(&'it self) -> Self::IntoIter { + self.last_coverage.as_slice().iter() + } +} + +impl<'rt> AsMutSlice for JSTypeObserver<'rt> { + fn as_mut_slice(&mut self) -> &mut [u8] { + self.last_coverage.as_mut_slice() + } +} + +impl<'rt> MapObserver for JSTypeObserver<'rt> { + type Entry = u8; + + fn get(&self, idx: usize) -> &Self::Entry { + &self.last_coverage[idx] + } + + fn get_mut(&mut self, idx: usize) -> &mut Self::Entry { + &mut self.last_coverage[idx] + } + + fn usable_count(&self) -> usize { + self.last_coverage.len() + } + + fn count_bytes(&self) -> u64 { + self.last_coverage.iter().filter(|&&e| e != 0).count() as u64 + } + + fn hash(&self) -> u64 { + let mut hasher = AHasher::default(); + self.last_coverage.hash(&mut hasher); + hasher.finish() + } + + fn initial(&self) -> Self::Entry { + self.initial + } + + fn initial_mut(&mut self) -> &mut Self::Entry { + &mut self.initial + } + + fn reset_map(&mut self) -> Result<(), Error> { + let initial = self.initial(); + let cnt = self.usable_count(); + let map = self.last_coverage.as_mut_slice(); + for x in map[0..cnt].iter_mut() { + *x = initial; + } + Ok(()) + } + + fn to_vec(&self) -> Vec { + self.last_coverage.clone() + } + + fn how_many_set(&self, indexes: &[usize]) -> usize { + let initial = self.initial(); + let cnt = self.usable_count(); + let map = self.last_coverage.as_slice(); + let mut res = 0; + for i in indexes { + if *i < cnt && map[*i] != initial { + res += 1; + } + } + res + } +} From 9bbaad418e409cf842454cdb480a3c42acaaf101 Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Tue, 30 Aug 2022 02:36:24 -0500 Subject: [PATCH 06/16] reduce complexity of executor --- libafl_v8/src/executors.rs | 32 ++++++++++++++++++++++---------- libafl_v8/src/values.rs | 9 ++++++--- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/libafl_v8/src/executors.rs b/libafl_v8/src/executors.rs index 2a01f03fd9..74f4ca1aaa 100644 --- a/libafl_v8/src/executors.rs +++ b/libafl_v8/src/executors.rs @@ -13,11 +13,13 @@ use std::sync::Arc; use deno_core::{v8, ModuleId, ModuleSpecifier}; use deno_runtime::worker::MainWorker; use libafl::{ + events::{EventFirer, EventRestarter}, executors::{Executor, ExitKind, HasObservers}, + feedbacks::Feedback, inputs::Input, observers::ObserversTuple, - state::State, - Error, + state::{HasClientPerfMonitor, HasSolutions, State}, + Error, HasObjective, }; use tokio::runtime::Runtime; use v8::{Function, Local, TryCatch}; @@ -25,7 +27,7 @@ use v8::{Function, Local, TryCatch}; use crate::{values::IntoJSValue, Mutex}; /// Executor which executes JavaScript using Deno and V8. -pub struct V8Executor<'rt, EM, I, OT, S, Z> +pub struct V8Executor<'rt, I, OT, S> where I: Input + IntoJSValue, OT: ObserversTuple, @@ -35,17 +37,17 @@ where observers: OT, rt: &'rt Runtime, worker: Arc>, - phantom: PhantomData<(EM, I, S, Z)>, + phantom: PhantomData<(I, S)>, } -impl<'rt, EM, I, OT, S, Z> V8Executor<'rt, EM, I, OT, S, Z> +impl<'rt, I, OT, S> V8Executor<'rt, I, OT, S> where I: Input + IntoJSValue, OT: ObserversTuple, S: State, { /// Create a new V8 executor. - pub fn new( + pub fn new( rt: &'rt Runtime, worker: Arc>, main_module: ModuleSpecifier, @@ -53,7 +55,13 @@ where _fuzzer: &mut Z, _state: &mut S, _mgr: &mut EM, - ) -> Result { + ) -> Result + where + EM: EventFirer + EventRestarter, + OF: Feedback, + S: HasSolutions + HasClientPerfMonitor, + Z: HasObjective, + { let copy = worker.clone(); let id = match rt.block_on(async { let mut locked = copy.lock().await; @@ -100,6 +108,10 @@ where let input = input.to_js_value(try_catch)?; let res = func.call(try_catch, recv.into(), &[input]); if res.is_none() { + println!( + "{}", + crate::js_err_to_libafl(try_catch).unwrap().to_string() + ); Ok(ExitKind::Crash) } else { Ok(ExitKind::Ok) @@ -125,7 +137,7 @@ where } } -impl<'rt, EM, I, OT, S, Z> Executor for V8Executor<'rt, EM, I, OT, S, Z> +impl<'rt, EM, I, OT, S, Z> Executor for V8Executor<'rt, I, OT, S> where I: Input + IntoJSValue, OT: ObserversTuple, @@ -142,7 +154,7 @@ where } } -impl<'rt, EM, I, OT, S, Z> HasObservers for V8Executor<'rt, EM, I, OT, S, Z> +impl<'rt, I, OT, S> HasObservers for V8Executor<'rt, I, OT, S> where I: Input + IntoJSValue, OT: ObserversTuple, @@ -157,7 +169,7 @@ where } } -impl<'rt, EM, I, OT, S, Z> Debug for V8Executor<'rt, EM, I, OT, S, Z> +impl<'rt, I, OT, S> Debug for V8Executor<'rt, I, OT, S> where I: Input + IntoJSValue, OT: ObserversTuple, diff --git a/libafl_v8/src/values.rs b/libafl_v8/src/values.rs index 3592e69101..eb5a74d7ad 100644 --- a/libafl_v8/src/values.rs +++ b/libafl_v8/src/values.rs @@ -1,7 +1,8 @@ //! Value types for inputs passed to JavaScript targets use libafl::{ - inputs::{BytesInput, HasBytesVec}, + bolts::AsSlice, + inputs::{HasTargetBytes, Input}, Error, }; @@ -13,9 +14,11 @@ pub trait IntoJSValue { fn to_js_value<'s>(&self, scope: &mut HandleScope<'s>) -> Result, Error>; } -impl IntoJSValue for BytesInput { +impl IntoJSValue for B { fn to_js_value<'s>(&self, scope: &mut HandleScope<'s>) -> Result, Error> { - let store = ArrayBuffer::new_backing_store_from_vec(Vec::from(self.bytes())).make_shared(); + let store = + ArrayBuffer::new_backing_store_from_vec(Vec::from(self.target_bytes().as_slice())) + .make_shared(); let buffer = ArrayBuffer::with_backing_store(scope, &store); Ok(buffer.into()) } From f392b957349c886bf5a585ff9daf7c3f9b175e2c Mon Sep 17 00:00:00 2001 From: David CARLIER Date: Mon, 29 Aug 2022 12:28:04 +0100 Subject: [PATCH 07/16] Simplification for netbsd-specific code (#750) the cpuset api is already present in libc... --- libafl/src/bolts/core_affinity.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/libafl/src/bolts/core_affinity.rs b/libafl/src/bolts/core_affinity.rs index efe6e1f1d9..38ce99efa2 100644 --- a/libafl/src/bolts/core_affinity.rs +++ b/libafl/src/bolts/core_affinity.rs @@ -716,18 +716,14 @@ mod netbsd { use alloc::vec::Vec; use std::thread::available_parallelism; - use libc::{_cpuset, pthread_self, pthread_setaffinity_np}; + use libc::{ + _cpuset, _cpuset_create, _cpuset_destroy, _cpuset_set, _cpuset_size, pthread_self, + pthread_setaffinity_np, + }; use super::CoreId; use crate::Error; - extern "C" { - fn _cpuset_create() -> *mut _cpuset; - fn _cpuset_destroy(c: *mut _cpuset); - fn _cpuset_set(i: usize, c: *mut _cpuset) -> i32; - fn _cpuset_size(c: *const _cpuset) -> usize; - } - #[allow(trivial_numeric_casts)] pub fn get_core_ids() -> Result, Error> { Ok((0..(usize::from(available_parallelism()?))) @@ -739,7 +735,7 @@ mod netbsd { pub fn set_for_current(core_id: CoreId) -> Result<(), Error> { let set = new_cpuset(); - unsafe { _cpuset_set(core_id.id, set) }; + unsafe { _cpuset_set(core_id.id as u64, set) }; // Set the current thread's core affinity. let result = unsafe { pthread_setaffinity_np( From 55993da87e2872d267b74e3a54e8078695c1957e Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Mon, 29 Aug 2022 06:37:55 -0500 Subject: [PATCH 08/16] 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 --- fuzzers/baby_fuzzer_minimizing/.gitignore | 3 + fuzzers/baby_fuzzer_minimizing/Cargo.toml | 23 ++ fuzzers/baby_fuzzer_minimizing/README.md | 9 + fuzzers/baby_fuzzer_minimizing/src/main.rs | 142 ++++++++ libafl/src/corpus/cached.rs | 2 +- libafl/src/corpus/inmemory.rs | 5 +- libafl/src/corpus/mod.rs | 10 +- libafl/src/corpus/ondisk.rs | 154 ++++---- libafl/src/feedbacks/mod.rs | 61 ++++ libafl/src/inputs/mod.rs | 1 - libafl/src/schedulers/mod.rs | 13 +- libafl/src/stages/mod.rs | 5 + libafl/src/stages/tmin.rs | 386 +++++++++++++++++++++ 13 files changed, 732 insertions(+), 82 deletions(-) create mode 100644 fuzzers/baby_fuzzer_minimizing/.gitignore create mode 100644 fuzzers/baby_fuzzer_minimizing/Cargo.toml create mode 100644 fuzzers/baby_fuzzer_minimizing/README.md create mode 100644 fuzzers/baby_fuzzer_minimizing/src/main.rs create mode 100644 libafl/src/stages/tmin.rs diff --git a/fuzzers/baby_fuzzer_minimizing/.gitignore b/fuzzers/baby_fuzzer_minimizing/.gitignore new file mode 100644 index 0000000000..f3e9e2b204 --- /dev/null +++ b/fuzzers/baby_fuzzer_minimizing/.gitignore @@ -0,0 +1,3 @@ +corpus +minimized +solutions diff --git a/fuzzers/baby_fuzzer_minimizing/Cargo.toml b/fuzzers/baby_fuzzer_minimizing/Cargo.toml new file mode 100644 index 0000000000..550aa468a3 --- /dev/null +++ b/fuzzers/baby_fuzzer_minimizing/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "baby_fuzzer_minimizing" +version = "0.8.1" +authors = ["Andrea Fioraldi ", "Dominik Maier ", "Addison Crump "] +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/" } diff --git a/fuzzers/baby_fuzzer_minimizing/README.md b/fuzzers/baby_fuzzer_minimizing/README.md new file mode 100644 index 0000000000..0b3417897f --- /dev/null +++ b/fuzzers/baby_fuzzer_minimizing/README.md @@ -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. \ No newline at end of file diff --git a/fuzzers/baby_fuzzer_minimizing/src/main.rs b/fuzzers/baby_fuzzer_minimizing/src/main.rs new file mode 100644 index 0000000000..61430cc6c2 --- /dev/null +++ b/fuzzers/baby_fuzzer_minimizing/src/main.rs @@ -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(()) +} diff --git a/libafl/src/corpus/cached.rs b/libafl/src/corpus/cached.rs index 8ebbc5555c..328540dda3 100644 --- a/libafl/src/corpus/cached.rs +++ b/libafl/src/corpus/cached.rs @@ -46,7 +46,7 @@ where /// Replaces the testcase at the given idx #[inline] - fn replace(&mut self, idx: usize, testcase: Testcase) -> Result<(), Error> { + fn replace(&mut self, idx: usize, testcase: Testcase) -> Result, Error> { // TODO finish self.inner.replace(idx, testcase) } diff --git a/libafl/src/corpus/inmemory.rs b/libafl/src/corpus/inmemory.rs index d020b4e9fc..abb0ac8e49 100644 --- a/libafl/src/corpus/inmemory.rs +++ b/libafl/src/corpus/inmemory.rs @@ -41,12 +41,11 @@ where /// Replaces the testcase at the given idx #[inline] - fn replace(&mut self, idx: usize, testcase: Testcase) -> Result<(), Error> { + fn replace(&mut self, idx: usize, testcase: Testcase) -> 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(self.entries[idx].replace(testcase)) } /// Removes an entry from the corpus, returning it if it was present. diff --git a/libafl/src/corpus/mod.rs b/libafl/src/corpus/mod.rs index 6d7744db4e..2fa1f1babf 100644 --- a/libafl/src/corpus/mod.rs +++ b/libafl/src/corpus/mod.rs @@ -36,8 +36,8 @@ where /// Add an entry to the corpus and return its index fn add(&mut self, testcase: Testcase) -> Result; - /// Replaces the testcase at the given idx - fn replace(&mut self, idx: usize, testcase: Testcase) -> Result<(), Error>; + /// Replaces the testcase at the given idx, returning the existing. + fn replace(&mut self, idx: usize, testcase: Testcase) -> Result, Error>; /// Removes an entry from the corpus, returning it if it was present. fn remove(&mut self, idx: usize) -> Result>, Error>; @@ -177,7 +177,11 @@ pub mod pybind { } #[inline] - fn replace(&mut self, idx: usize, testcase: Testcase) -> Result<(), Error> { + fn replace( + &mut self, + idx: usize, + testcase: Testcase, + ) -> Result, Error> { unwrap_me_mut!(self.wrapper, c, { c.replace(idx, testcase) }) } diff --git a/libafl/src/corpus/ondisk.rs b/libafl/src/corpus/ondisk.rs index 882478f462..376a9991d1 100644 --- a/libafl/src/corpus/ondisk.rs +++ b/libafl/src/corpus/ondisk.rs @@ -67,79 +67,21 @@ where /// Add an entry to the corpus and return its index #[inline] fn add(&mut self, mut testcase: Testcase) -> Result { - if testcase.filename().is_none() { - // TODO walk entry metadata to ask for pieces of filename (e.g. :havoc in AFL) - let file_orig = testcase - .input() - .as_ref() - .unwrap() - .generate_name(self.entries.len()); - let mut file = file_orig.clone(); - - let mut ctr = 2; - let filename = loop { - let lockfile = format!(".{}.lafl_lock", file); - // try to create lockfile. - - if OpenOptions::new() - .write(true) - .create_new(true) - .open(self.dir_path.join(lockfile)) - .is_ok() - { - break self.dir_path.join(file); - } - - file = format!("{}-{}", &file_orig, ctr); - ctr += 1; - }; - - let filename_str = filename.to_str().expect("Invalid Path"); - testcase.set_filename(filename_str.into()); - }; - 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() - )); - let mut tmpfile_name = PathBuf::from(&filename); - tmpfile_name.set_file_name(format!( - ".{}.tmp", - tmpfile_name.file_name().unwrap().to_string_lossy() - )); - - let ondisk_meta = OnDiskMetadata { - metadata: testcase.metadata(), - exec_time: testcase.exec_time(), - executions: testcase.executions(), - }; - - let mut tmpfile = File::create(&tmpfile_name)?; - - let serialized = match self.meta_format.as_ref().unwrap() { - OnDiskMetadataFormat::Postcard => postcard::to_allocvec(&ondisk_meta)?, - OnDiskMetadataFormat::Json => serde_json::to_vec(&ondisk_meta)?, - OnDiskMetadataFormat::JsonPretty => serde_json::to_vec_pretty(&ondisk_meta)?, - }; - tmpfile.write_all(&serialized)?; - fs::rename(&tmpfile_name, &filename)?; - } - testcase - .store_input() - .expect("Could not save testcase to disk"); + 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, testcase: Testcase) -> Result<(), Error> { + fn replace(&mut self, idx: usize, mut testcase: Testcase) -> 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(()) + 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. @@ -148,7 +90,9 @@ where if idx >= self.entries.len() { Ok(None) } else { - Ok(Some(self.entries.remove(idx).into_inner())) + let prev = self.entries.remove(idx).into_inner(); + self.remove_testcase(&prev)?; + Ok(Some(prev)) } } @@ -207,6 +151,86 @@ where meta_format, }) } + + fn save_testcase(&mut self, testcase: &mut Testcase) -> 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 + .input() + .as_ref() + .unwrap() + .generate_name(self.entries.len()); + let mut file = file_orig.clone(); + + let mut ctr = 2; + let filename = loop { + let lockfile = format!(".{}.lafl_lock", file); + // try to create lockfile. + + if OpenOptions::new() + .write(true) + .create_new(true) + .open(self.dir_path.join(lockfile)) + .is_ok() + { + break self.dir_path.join(file); + } + + file = format!("{}-{}", &file_orig, ctr); + ctr += 1; + }; + + let filename_str = filename.to_str().expect("Invalid Path"); + testcase.set_filename(filename_str.into()); + }; + 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() + )); + let mut tmpfile_name = PathBuf::from(&filename); + tmpfile_name.set_file_name(format!( + ".{}.tmp", + tmpfile_name.file_name().unwrap().to_string_lossy() + )); + + let ondisk_meta = OnDiskMetadata { + metadata: testcase.metadata(), + exec_time: testcase.exec_time(), + executions: testcase.executions(), + }; + + let mut tmpfile = File::create(&tmpfile_name)?; + + let serialized = match self.meta_format.as_ref().unwrap() { + OnDiskMetadataFormat::Postcard => postcard::to_allocvec(&ondisk_meta)?, + OnDiskMetadataFormat::Json => serde_json::to_vec(&ondisk_meta)?, + OnDiskMetadataFormat::JsonPretty => serde_json::to_vec_pretty(&ondisk_meta)?, + }; + tmpfile.write_all(&serialized)?; + fs::rename(&tmpfile_name, &filename)?; + } + testcase + .store_input() + .expect("Could not save testcase to disk"); + Ok(()) + } + + fn remove_testcase(&mut self, testcase: &Testcase) -> Result<(), Error> { + if let Some(filename) = testcase.filename() { + fs::remove_file(filename)?; + } + 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)?; + } + Ok(()) + } } #[cfg(feature = "python")] /// `OnDiskCorpus` Python bindings diff --git a/libafl/src/feedbacks/mod.rs b/libafl/src/feedbacks/mod.rs index 00e081e59d..66b4dfc6e8 100644 --- a/libafl/src/feedbacks/mod.rs +++ b/libafl/src/feedbacks/mod.rs @@ -296,6 +296,61 @@ where OT: ObserversTuple; } +/// Factory for feedbacks which should be sensitive to an existing context, e.g. observer(s) from a +/// specific execution +pub trait FeedbackFactory +where + F: Feedback, + I: Input, + S: HasClientPerfMonitor, +{ + /// Create the feedback from the provided context + fn create_feedback(&self, ctx: &T) -> F; +} + +impl FeedbackFactory for FU +where + FU: Fn(&T) -> FE, + FE: Feedback, + 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 +where + F: Default, +{ + phantom: PhantomData, +} + +impl DefaultFeedbackFactory +where + F: Default, +{ + /// Create the feedback factory + #[must_use] + pub fn new() -> Self { + Self::default() + } +} + +impl FeedbackFactory for DefaultFeedbackFactory +where + F: Feedback + 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; + /// 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; + /// 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. diff --git a/libafl/src/inputs/mod.rs b/libafl/src/inputs/mod.rs index 8ff5fc2963..f7c4811039 100644 --- a/libafl/src/inputs/mod.rs +++ b/libafl/src/inputs/mod.rs @@ -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

(path: P) -> Result where P: AsRef, diff --git a/libafl/src/schedulers/mod.rs b/libafl/src/schedulers/mod.rs index 6a9949d575..0f072f7fd0 100644 --- a/libafl/src/schedulers/mod.rs +++ b/libafl/src/schedulers/mod.rs @@ -39,22 +39,17 @@ pub trait Scheduler 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, - ) -> Result<(), Error> { + /// Replaced the given testcase at the given idx + fn on_replace(&self, _state: &mut S, _idx: usize, _prev: &Testcase) -> 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, diff --git a/libafl/src/stages/mod.rs b/libafl/src/stages/mod.rs index 74d0559a08..844aec861b 100644 --- a/libafl/src/stages/mod.rs +++ b/libafl/src/stages/mod.rs @@ -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; diff --git a/libafl/src/stages/tmin.rs b/libafl/src/stages/tmin.rs new file mode 100644 index 0000000000..ead937aed6 --- /dev/null +++ b/libafl/src/stages/tmin.rs @@ -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: + Stage + FeedbackFactory +where + CS: Scheduler, + E: Executor + HasObservers, + EM: EventFirer, + F1: Feedback, + F2: Feedback, + I: Input + Hash + HasLen, + M: Mutator, + OT: ObserversTuple, + S: HasClientPerfMonitor + HasCorpus + HasExecutions + HasMaxSize, + Z: ExecutionProcessor + + ExecutesInput + + HasFeedback + + HasScheduler, +{ + /// 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; + + /// 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 +where + I: Input + HasLen, + M: Mutator, +{ + mutator: M, + factory: FF, + runs: usize, + #[allow(clippy::type_complexity)] + phantom: PhantomData<(CS, E, EM, F1, F2, I, S, T, Z)>, +} + +impl Stage + for StdTMinMutationalStage +where + CS: Scheduler, + E: Executor + HasObservers, + EM: EventFirer, + F1: Feedback, + F2: Feedback, + FF: FeedbackFactory, + I: Input + Hash + HasLen, + M: Mutator, + OT: ObserversTuple, + S: HasClientPerfMonitor + HasCorpus + HasExecutions + HasMaxSize, + Z: ExecutionProcessor + + ExecutesInput + + HasFeedback + + HasScheduler, +{ + 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 FeedbackFactory + for StdTMinMutationalStage +where + F2: Feedback, + FF: FeedbackFactory, + I: Input + HasLen, + M: Mutator, + S: HasClientPerfMonitor, +{ + fn create_feedback(&self, ctx: &T) -> F2 { + self.factory.create_feedback(ctx) + } +} + +impl TMinMutationalStage + for StdTMinMutationalStage +where + CS: Scheduler, + E: HasObservers + Executor, + EM: EventFirer, + F1: Feedback, + F2: Feedback, + FF: FeedbackFactory, + I: Input + HasLen + Hash, + M: Mutator, + OT: ObserversTuple, + S: HasClientPerfMonitor + HasCorpus + HasExecutions + HasMaxSize, + Z: ExecutionProcessor + + ExecutesInput + + HasFeedback + + HasScheduler, +{ + /// 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 { + Ok(self.runs) + } +} + +impl + StdTMinMutationalStage +where + I: Input + HasLen, + M: Mutator, +{ + /// 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 { + name: String, + obs_name: String, + orig_hash: u64, + phantom: PhantomData, +} + +impl MapEqualityFeedback { + /// 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 Named for MapEqualityFeedback { + fn name(&self) -> &str { + &self.name + } +} + +impl HasObserverName for MapEqualityFeedback { + fn observer_name(&self) -> &str { + &self.obs_name + } +} + +impl Feedback for MapEqualityFeedback +where + I: Input, + M: MapObserver, + S: HasClientPerfMonitor, +{ + fn is_interesting( + &mut self, + _state: &mut S, + _manager: &mut EM, + _input: &I, + observers: &OT, + _exit_kind: &ExitKind, + ) -> Result + where + EM: EventFirer, + OT: ObserversTuple, + { + let obs = observers + .match_name::(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 { + obs_name: String, + phantom: PhantomData, +} + +impl MapEqualityFactory +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 HasObserverName for MapEqualityFactory { + fn observer_name(&self) -> &str { + &self.obs_name + } +} + +impl FeedbackFactory, I, S, OT> for MapEqualityFactory +where + I: Input, + M: MapObserver, + OT: ObserversTuple, + S: HasClientPerfMonitor, +{ + fn create_feedback(&self, observers: &OT) -> MapEqualityFeedback { + let obs = observers + .match_name::(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, + } + } +} From 1c01d66f79e313d02de29ee285cca15c522d1066 Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Mon, 29 Aug 2022 06:38:46 -0500 Subject: [PATCH 09/16] Implement a corpus minimiser (cmin) (#739) * initial try * correct case where cull attempts to fetch non-existent corpus entries * various on_remove, on_replace implementations * ise -> ize (consistency), use TestcaseScore instead of rolling our own * oops, feature gate * documentation! * link c++ * doc-nit: correction in opt explanation don't write documentation at 0300 * better linking --- fuzzers/fuzzbench_text/src/lib.rs | 9 +- fuzzers/libfuzzer_libpng_cmin/.gitignore | 1 + fuzzers/libfuzzer_libpng_cmin/Cargo.toml | 33 +++ fuzzers/libfuzzer_libpng_cmin/Makefile.toml | 181 ++++++++++++++ fuzzers/libfuzzer_libpng_cmin/README.md | 81 +++++++ .../corpus/not_kitty.png | Bin 0 -> 218 bytes .../corpus/not_kitty_alpha.png | Bin 0 -> 376 bytes .../corpus/not_kitty_gamma.png | Bin 0 -> 228 bytes .../corpus/not_kitty_icc.png | Bin 0 -> 427 bytes fuzzers/libfuzzer_libpng_cmin/harness.cc | 191 +++++++++++++++ .../src/bin/libafl_cc.rs | 29 +++ .../src/bin/libafl_cxx.rs | 5 + fuzzers/libfuzzer_libpng_cmin/src/lib.rs | 226 ++++++++++++++++++ libafl/Cargo.toml | 1 + libafl/src/corpus/minimizer.rs | 205 ++++++++++++++++ libafl/src/corpus/mod.rs | 9 +- libafl/src/inputs/generalized.rs | 32 +-- libafl/src/schedulers/minimizer.rs | 67 +++++- libafl/src/schedulers/weighted.rs | 20 +- 19 files changed, 1059 insertions(+), 31 deletions(-) create mode 100644 fuzzers/libfuzzer_libpng_cmin/.gitignore create mode 100644 fuzzers/libfuzzer_libpng_cmin/Cargo.toml create mode 100644 fuzzers/libfuzzer_libpng_cmin/Makefile.toml create mode 100644 fuzzers/libfuzzer_libpng_cmin/README.md create mode 100644 fuzzers/libfuzzer_libpng_cmin/corpus/not_kitty.png create mode 100644 fuzzers/libfuzzer_libpng_cmin/corpus/not_kitty_alpha.png create mode 100644 fuzzers/libfuzzer_libpng_cmin/corpus/not_kitty_gamma.png create mode 100644 fuzzers/libfuzzer_libpng_cmin/corpus/not_kitty_icc.png create mode 100644 fuzzers/libfuzzer_libpng_cmin/harness.cc create mode 100644 fuzzers/libfuzzer_libpng_cmin/src/bin/libafl_cc.rs create mode 100644 fuzzers/libfuzzer_libpng_cmin/src/bin/libafl_cxx.rs create mode 100644 fuzzers/libfuzzer_libpng_cmin/src/lib.rs create mode 100644 libafl/src/corpus/minimizer.rs diff --git a/fuzzers/fuzzbench_text/src/lib.rs b/fuzzers/fuzzbench_text/src/lib.rs index 53c06a1dcf..9ca385f450 100644 --- a/fuzzers/fuzzbench_text/src/lib.rs +++ b/fuzzers/fuzzbench_text/src/lib.rs @@ -667,14 +667,7 @@ fn fuzz_text( // In case the corpus is empty (on first run), reset if state.corpus().count() < 1 { state - .load_from_directory( - &mut fuzzer, - &mut executor, - &mut mgr, - &seed_dir, - false, - &mut |_, _, path| GeneralizedInput::from_bytes_file(path), - ) + .load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &[seed_dir.clone()]) .unwrap_or_else(|_| { println!("Failed to load initial corpus at {:?}", &seed_dir); process::exit(0); diff --git a/fuzzers/libfuzzer_libpng_cmin/.gitignore b/fuzzers/libfuzzer_libpng_cmin/.gitignore new file mode 100644 index 0000000000..a977a2ca5b --- /dev/null +++ b/fuzzers/libfuzzer_libpng_cmin/.gitignore @@ -0,0 +1 @@ +libpng-* \ No newline at end of file diff --git a/fuzzers/libfuzzer_libpng_cmin/Cargo.toml b/fuzzers/libfuzzer_libpng_cmin/Cargo.toml new file mode 100644 index 0000000000..ae2835b440 --- /dev/null +++ b/fuzzers/libfuzzer_libpng_cmin/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "libfuzzer_libpng_cmin" +version = "0.8.1" +authors = ["Andrea Fioraldi ", "Dominik Maier ", "Addison Crump "] +edition = "2021" + +[features] +default = ["std"] +std = [] +# Forces a crash +crash = [] + +[profile.release] +lto = true +codegen-units = 1 +opt-level = 3 +debug = true + +[build-dependencies] +cc = { version = "1.0", features = ["parallel"] } +which = { version = "4.0.2" } + +[dependencies] +libafl = { path = "../../libafl/", features = ["default", "cmin"] } +# libafl = { path = "../../libafl/", features = ["default"] } +libafl_targets = { path = "../../libafl_targets/", features = ["sancov_pcguard_hitcounts", "libfuzzer", "sancov_cmplog"] } +# TODO Include it only when building cc +libafl_cc = { path = "../../libafl_cc/" } +mimalloc = { version = "*", default-features = false } + +[lib] +name = "libfuzzer_libpng" +crate-type = ["staticlib"] diff --git a/fuzzers/libfuzzer_libpng_cmin/Makefile.toml b/fuzzers/libfuzzer_libpng_cmin/Makefile.toml new file mode 100644 index 0000000000..3d57308674 --- /dev/null +++ b/fuzzers/libfuzzer_libpng_cmin/Makefile.toml @@ -0,0 +1,181 @@ +# Variables +[env] +FUZZER_NAME='fuzzer_libpng' +LIBAFL_CC = './target/release/libafl_cc' +LIBAFL_CXX = './target/release/libafl_cxx' +FUZZER = './target/release/${FUZZER_NAME}' +PROJECT_DIR = { script = ["pwd"] } + +[tasks.unsupported] +script_runner="@shell" +script=''' +echo "Cargo-make not integrated yet on this" +''' + +# libpng +[tasks.libpng] +linux_alias = "libpng_unix" +mac_alias = "libpng_unix" +windows_alias = "unsupported" + +[tasks.libpng_unix] +condition = { files_not_exist = ["./libpng-1.6.37"]} +script_runner="@shell" +script=''' +curl https://deac-fra.dl.sourceforge.net/project/libpng/libpng16/1.6.37/libpng-1.6.37.tar.xz --output libpng-1.6.37.tar.xz +tar -xvf libpng-1.6.37.tar.xz +''' + +# Compilers +[tasks.cxx] +linux_alias = "cxx_unix" +mac_alias = "cxx_unix" +windows_alias = "unsupported" + +[tasks.cxx_unix] +command = "cargo" +args = ["build" , "--release"] + +[tasks.cc] +linux_alias = "cc_unix" +mac_alias = "cc_unix" +windows_alias = "unsupported" + +[tasks.cc_unix] +command = "cargo" +args = ["build" , "--release"] + +[tasks.crash_cxx] +linux_alias = "crash_cxx_unix" +mac_alias = "crash_cxx_unix" +windows_alias = "unsupported" + +[tasks.crash_cxx_unix] +command = "cargo" +args = ["build" , "--release", "--features=crash"] + +[tasks.crash_cc] +linux_alias = "crash_cc_unix" +mac_alias = "crash_cc_unix" +windows_alias = "unsupported" + +[tasks.crash_cc_unix] +command = "cargo" +args = ["build" , "--release", "--features=crash"] + +# Library +[tasks.lib] +linux_alias = "lib_unix" +mac_alias = "lib_unix" +windows_alias = "unsupported" + +[tasks.lib_unix] +script_runner="@shell" +script=''' +cd libpng-1.6.37 && ./configure --enable-shared=no --with-pic=yes --enable-hardware-optimizations=yes +cd "${PROJECT_DIR}" +make -C libpng-1.6.37 CC="${PROJECT_DIR}/target/release/libafl_cc" CXX="${PROJECT_DIR}/target/release/libafl_cxx" +''' +dependencies = [ "libpng", "cxx", "cc" ] + +# Library +[tasks.crash_lib] +linux_alias = "crash_lib_unix" +mac_alias = "crash_lib_unix" +windows_alias = "unsupported" + +[tasks.crash_lib_unix] +script_runner="@shell" +script=''' +cd libpng-1.6.37 && ./configure --enable-shared=no --with-pic=yes --enable-hardware-optimizations=yes +cd "${PROJECT_DIR}" +make -C libpng-1.6.37 CC="${PROJECT_DIR}/target/release/libafl_cc" CXX="${PROJECT_DIR}/target/release/libafl_cxx" +''' +dependencies = [ "libpng", "crash_cxx", "crash_cc" ] + +# Harness +[tasks.fuzzer] +linux_alias = "fuzzer_unix" +mac_alias = "fuzzer_unix" +windows_alias = "unsupported" + +[tasks.fuzzer_unix] +command = "target/release/libafl_cxx" +args = ["${PROJECT_DIR}/harness.cc", "${PROJECT_DIR}/libpng-1.6.37/.libs/libpng16.a", "-I", "${PROJECT_DIR}/libpng-1.6.37/", "-o", "${FUZZER_NAME}", "-lm", "-lz"] +dependencies = [ "lib", "cxx", "cc" ] + +# Crashing Harness +[tasks.fuzzer_crash] +linux_alias = "fuzzer_crash_unix" +mac_alias = "fuzzer_crash_unix" +windows_alias = "unsupported" + +[tasks.fuzzer_crash_unix] +command = "target/release/libafl_cxx" +args = ["${PROJECT_DIR}/harness.cc", "${PROJECT_DIR}/libpng-1.6.37/.libs/libpng16.a", "-I", "${PROJECT_DIR}/libpng-1.6.37/", "-o", "${FUZZER_NAME}_crash", "-lm", "-lz"] +dependencies = [ "crash_lib", "crash_cxx", "crash_cc" ] + +# Run the fuzzer +[tasks.run] +linux_alias = "run_unix" +mac_alias = "run_unix" +windows_alias = "unsupported" + +[tasks.run_unix] +script_runner = "@shell" +script=''' +./${FUZZER_NAME} & +sleep 0.2 +./${FUZZER_NAME} 2>/dev/null +''' +dependencies = [ "fuzzer" ] + + +# Run the fuzzer with a crash +[tasks.crash] +linux_alias = "crash_unix" +mac_alias = "crash_unix" +windows_alias = "unsupported" + +[tasks.crash_unix] +script_runner = "@shell" +script=''' +./${FUZZER_NAME}_crash & +sleep 0.2 +./${FUZZER_NAME}_crash 2>/dev/null +''' +dependencies = [ "fuzzer_crash" ] + + + +# Test +[tasks.test] +linux_alias = "test_unix" +mac_alias = "test_unix" +windows_alias = "unsupported" + +[tasks.test_unix] +script_runner = "@shell" +script=''' +rm -rf libafl_unix_shmem_server || true +timeout 11s ./${FUZZER_NAME} & +sleep 0.2 +timeout 10s ./${FUZZER_NAME} >/dev/null 2>/dev/null & +''' +dependencies = [ "fuzzer" ] + +# Clean up +[tasks.clean] +linux_alias = "clean_unix" +mac_alias = "clean_unix" +windows_alias = "unsupported" + +[tasks.clean_unix] +# Disable default `clean` definition +clear = true +script_runner="@shell" +script=''' +rm -f ./${FUZZER_NAME} +make -C libpng-1.6.37 clean +cargo clean +''' diff --git a/fuzzers/libfuzzer_libpng_cmin/README.md b/fuzzers/libfuzzer_libpng_cmin/README.md new file mode 100644 index 0000000000..930a21025d --- /dev/null +++ b/fuzzers/libfuzzer_libpng_cmin/README.md @@ -0,0 +1,81 @@ +# Libfuzzer for libpng + +This folder contains an example fuzzer for libpng, using LLMP for fast multi-process fuzzing and crash detection. + +In contrast to other fuzzer examples, this setup uses `fuzz_loop_for`, to occasionally respawn the fuzzer executor. +While this costs performance, it can be useful for targets with memory leaks or other instabilities. +If your target is really instable, however, consider exchanging the `InProcessExecutor` for a `ForkserverExecutor` instead. + +It also uses the `introspection` feature, printing fuzzer stats during execution. + +To show off crash detection, we added a `ud2` instruction to the harness, edit harness.cc if you want a non-crashing example. +It has been tested on Linux. + +## Build + +To build this example, run + +```bash +cargo build --release +``` + +This will build the library with the fuzzer (src/lib.rs) with the libfuzzer compatibility layer and the SanitizerCoverage runtime functions for coverage feedback. +In addition, it will also build two C and C++ compiler wrappers (bin/libafl_c(libafl_c/xx).rs) that you must use to compile the target. + +The compiler wrappers, `libafl_cc` and libafl_cxx`, will end up in `./target/release/` (or `./target/debug`, in case you did not build with the `--release` flag). + +Then download libpng, and unpack the archive: +```bash +wget https://deac-fra.dl.sourceforge.net/project/libpng/libpng16/1.6.37/libpng-1.6.37.tar.xz +tar -xvf libpng-1.6.37.tar.xz +``` + +Now compile libpng, using the libafl_cc compiler wrapper: + +```bash +cd libpng-1.6.37 +./configure +make CC="$(pwd)/../target/release/libafl_cc" CXX="$(pwd)/../target/release/libafl_cxx" -j `nproc` +``` + +You can find the static lib at `libpng-1.6.37/.libs/libpng16.a`. + +Now, we have to build the libfuzzer harness and link all together to create our fuzzer binary. + +``` +cd .. +./target/release/libafl_cxx ./harness.cc libpng-1.6.37/.libs/libpng16.a -I libpng-1.6.37/ -o fuzzer_libpng -lz -lm +``` + +Afterward, the fuzzer will be ready to run. +Note that, unless you use the `launcher`, you will have to run the binary multiple times to actually start the fuzz process, see `Run` in the following. +This allows you to run multiple different builds of the same fuzzer alongside, for example, with and without ASAN (`-fsanitize=address`) or with different mutators. + +## Run + +The first time you run the binary, the broker will open a tcp port (currently on port `1337`), waiting for fuzzer clients to connect. This port is local and only used for the initial handshake. All further communication happens via shared map, to be independent of the kernel. Currently, you must run the clients from the libfuzzer_libpng directory for them to be able to access the PNG corpus. + +``` +./fuzzer_libpng + +[libafl/src/bolts/llmp.rs:407] "We're the broker" = "We\'re the broker" +Doing broker things. Run this tool again to start fuzzing in a client. +``` + +And after running the above again in a separate terminal: + +``` +[libafl/src/bolts/llmp.rs:1464] "New connection" = "New connection" +[libafl/src/bolts/llmp.rs:1464] addr = 127.0.0.1:33500 +[libafl/src/bolts/llmp.rs:1464] stream.peer_addr().unwrap() = 127.0.0.1:33500 +[LOG Debug]: Loaded 4 initial testcases. +[New Testcase #2] clients: 3, corpus: 6, objectives: 0, executions: 5, exec/sec: 0 +< fuzzing stats > +``` + +As this example uses in-process fuzzing, we added a Restarting Event Manager (`setup_restarting_mgr`). +This means each client will start itself again to listen for crashes and timeouts. +By restarting the actual fuzzer, it can recover from these exit conditions. + +In any real-world scenario, you should use `taskset` to pin each client to an empty CPU core, the lib does not pick an empty core automatically (yet). + diff --git a/fuzzers/libfuzzer_libpng_cmin/corpus/not_kitty.png b/fuzzers/libfuzzer_libpng_cmin/corpus/not_kitty.png new file mode 100644 index 0000000000000000000000000000000000000000..eff7c1707b936a8f8df725814f604d454b78b5c3 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5X_yc@GT+_~+`TzevkY_wIZRYx+5&y#hyq+?%!C8<`)MX5lF!N|bSRM)^r*U&J;z}U*bz{;0L z1Vuw`eoAIqC5i?kD`P_|6GMoGiCWXn12ss3YzWRzD=AMbN@Z|N$xljE@XSq2PYp^< WOsOn9nQ8-6#Ng@b=d#Wzp$PyV*n0l} literal 0 HcmV?d00001 diff --git a/fuzzers/libfuzzer_libpng_cmin/corpus/not_kitty_gamma.png b/fuzzers/libfuzzer_libpng_cmin/corpus/not_kitty_gamma.png new file mode 100644 index 0000000000000000000000000000000000000000..939d9d29a9b9f95bac5e9a72854361ee85469921 GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmTQ929t;oCfmw1AIbU z)6Sgv|NlRbXFM})=KnKxKI=t+9LW;bh?3y^w370~qErUQl>DSr1<%~X^wgl##FWay zlc_d9MbVxvjv*GO?@o5)YH;9THa`3B|5>?^8?LvjJ}xLe>!7e@k)r^sLedir0mCVe z=5sMjEm$*~tHD+}{NS_$nMdb|ABqg-@UGMMsZ=uY-X%Cq@&3vmZ%&@H{P?6&+U!yq VvuXWlo?M_c44$rjF6*2UngF4cP+$N6 literal 0 HcmV?d00001 diff --git a/fuzzers/libfuzzer_libpng_cmin/corpus/not_kitty_icc.png b/fuzzers/libfuzzer_libpng_cmin/corpus/not_kitty_icc.png new file mode 100644 index 0000000000000000000000000000000000000000..f0c7804d99829cc6307c1c6ae9915cf42d555414 GIT binary patch literal 427 zcmV;c0aX5pP)9xSWu9|B*4Isn^#g47m^r~thH)GiR<@yX0fO)OF<2Kt#qCldyUF#H?{4jV?XGw9)psxE&K1B1m^ z1_tH{2(hG@3=G>_85ksPA;eS`Ffj19FfeR8pIlm01~rBeWCZ{dbvfq;rA3DT000kA zOjJc?%*_A){{R30GnreSaefwW^{L9a%BKPWN%_+AW3auXJt}l zVPtu6$z?nM003J_L_t(I%iWVf3V=Wi12fJ3|IHp$*hSlV@t||fKp?cDK@bHXV&o_g zF_hw;3ILUGteXmeJsVfSmcVJno)^MdQwU3bFHCtNG)uY>mLcD%`0UBaIq~Fq8#dBr V12uok3~c}a002ovPDHLkV1nKBo!S5Z literal 0 HcmV?d00001 diff --git a/fuzzers/libfuzzer_libpng_cmin/harness.cc b/fuzzers/libfuzzer_libpng_cmin/harness.cc new file mode 100644 index 0000000000..5c36517376 --- /dev/null +++ b/fuzzers/libfuzzer_libpng_cmin/harness.cc @@ -0,0 +1,191 @@ +// libpng_read_fuzzer.cc +// Copyright 2017-2018 Glenn Randers-Pehrson +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that may +// be found in the LICENSE file https://cs.chromium.org/chromium/src/LICENSE + +// Last changed in libpng 1.6.35 [July 15, 2018] + +// The modifications in 2017 by Glenn Randers-Pehrson include +// 1. addition of a PNG_CLEANUP macro, +// 2. setting the option to ignore ADLER32 checksums, +// 3. adding "#include " which is needed on some platforms +// to provide memcpy(). +// 4. adding read_end_info() and creating an end_info structure. +// 5. adding calls to png_set_*() transforms commonly used by browsers. + +#include +#include +#include + +#include + +#define PNG_INTERNAL +#include "png.h" + +#define PNG_CLEANUP \ + if (png_handler.png_ptr) { \ + if (png_handler.row_ptr) { \ + png_free(png_handler.png_ptr, png_handler.row_ptr); \ + } \ + if (png_handler.end_info_ptr) { \ + png_destroy_read_struct(&png_handler.png_ptr, &png_handler.info_ptr, \ + &png_handler.end_info_ptr); \ + } else if (png_handler.info_ptr) { \ + png_destroy_read_struct(&png_handler.png_ptr, &png_handler.info_ptr, \ + nullptr); \ + } else { \ + png_destroy_read_struct(&png_handler.png_ptr, nullptr, nullptr); \ + } \ + png_handler.png_ptr = nullptr; \ + png_handler.row_ptr = nullptr; \ + png_handler.info_ptr = nullptr; \ + png_handler.end_info_ptr = nullptr; \ + } + +struct BufState { + const uint8_t *data; + size_t bytes_left; +}; + +struct PngObjectHandler { + png_infop info_ptr = nullptr; + png_structp png_ptr = nullptr; + png_infop end_info_ptr = nullptr; + png_voidp row_ptr = nullptr; + BufState *buf_state = nullptr; + + ~PngObjectHandler() { + if (row_ptr) { png_free(png_ptr, row_ptr); } + if (end_info_ptr) { + png_destroy_read_struct(&png_ptr, &info_ptr, &end_info_ptr); + } else if (info_ptr) { + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + } else { + png_destroy_read_struct(&png_ptr, nullptr, nullptr); + } + delete buf_state; + } +}; + +void user_read_data(png_structp png_ptr, png_bytep data, size_t length) { + BufState *buf_state = static_cast(png_get_io_ptr(png_ptr)); + if (length > buf_state->bytes_left) { png_error(png_ptr, "read error"); } + memcpy(data, buf_state->data, length); + buf_state->bytes_left -= length; + buf_state->data += length; +} + +static const int kPngHeaderSize = 8; + +// Entry point for LibFuzzer. +// Roughly follows the libpng book example: +// http://www.libpng.org/pub/png/book/chapter13.html +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < kPngHeaderSize) { return 0; } + + std::vector v(data, data + size); + if (png_sig_cmp(v.data(), 0, kPngHeaderSize)) { + // not a PNG. + return 0; + } + + PngObjectHandler png_handler; + png_handler.png_ptr = nullptr; + png_handler.row_ptr = nullptr; + png_handler.info_ptr = nullptr; + png_handler.end_info_ptr = nullptr; + + png_handler.png_ptr = + png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png_handler.png_ptr) { return 0; } + + png_handler.info_ptr = png_create_info_struct(png_handler.png_ptr); + if (!png_handler.info_ptr) { + PNG_CLEANUP + return 0; + } + + png_handler.end_info_ptr = png_create_info_struct(png_handler.png_ptr); + if (!png_handler.end_info_ptr) { + PNG_CLEANUP + return 0; + } + + png_set_crc_action(png_handler.png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE); +#ifdef PNG_IGNORE_ADLER32 + png_set_option(png_handler.png_ptr, PNG_IGNORE_ADLER32, PNG_OPTION_ON); +#endif + + // Setting up reading from buffer. + png_handler.buf_state = new BufState(); + png_handler.buf_state->data = data + kPngHeaderSize; + png_handler.buf_state->bytes_left = size - kPngHeaderSize; + png_set_read_fn(png_handler.png_ptr, png_handler.buf_state, user_read_data); + png_set_sig_bytes(png_handler.png_ptr, kPngHeaderSize); + + if (setjmp(png_jmpbuf(png_handler.png_ptr))) { + PNG_CLEANUP + return 0; + } + + // Reading. + png_read_info(png_handler.png_ptr, png_handler.info_ptr); + + // reset error handler to put png_deleter into scope. + if (setjmp(png_jmpbuf(png_handler.png_ptr))) { + PNG_CLEANUP + return 0; + } + + png_uint_32 width, height; + int bit_depth, color_type, interlace_type, compression_type; + int filter_type; + + if (!png_get_IHDR(png_handler.png_ptr, png_handler.info_ptr, &width, &height, + &bit_depth, &color_type, &interlace_type, &compression_type, + &filter_type)) { + PNG_CLEANUP + return 0; + } + + // This is going to be too slow. + if (width && height > 100000000 / width) { + PNG_CLEANUP +#ifdef HAS_DUMMY_CRASH + #ifdef __aarch64__ + asm volatile(".word 0xf7f0a000\n"); + #else + asm("ud2"); + #endif +#endif + return 0; + } + + // Set several transforms that browsers typically use: + png_set_gray_to_rgb(png_handler.png_ptr); + png_set_expand(png_handler.png_ptr); + png_set_packing(png_handler.png_ptr); + png_set_scale_16(png_handler.png_ptr); + png_set_tRNS_to_alpha(png_handler.png_ptr); + + int passes = png_set_interlace_handling(png_handler.png_ptr); + + png_read_update_info(png_handler.png_ptr, png_handler.info_ptr); + + png_handler.row_ptr = + png_malloc(png_handler.png_ptr, + png_get_rowbytes(png_handler.png_ptr, png_handler.info_ptr)); + + for (int pass = 0; pass < passes; ++pass) { + for (png_uint_32 y = 0; y < height; ++y) { + png_read_row(png_handler.png_ptr, + static_cast(png_handler.row_ptr), nullptr); + } + } + + png_read_end(png_handler.png_ptr, png_handler.end_info_ptr); + + PNG_CLEANUP + return 0; +} diff --git a/fuzzers/libfuzzer_libpng_cmin/src/bin/libafl_cc.rs b/fuzzers/libfuzzer_libpng_cmin/src/bin/libafl_cc.rs new file mode 100644 index 0000000000..e82a4be918 --- /dev/null +++ b/fuzzers/libfuzzer_libpng_cmin/src/bin/libafl_cc.rs @@ -0,0 +1,29 @@ +use std::env; + +use libafl_cc::{ClangWrapper, CompilerWrapper}; + +pub fn main() { + let args: Vec = env::args().collect(); + if args.len() > 1 { + let mut dir = env::current_exe().unwrap(); + + dir.pop(); + + let mut cc = ClangWrapper::new(); + if let Some(code) = cc + .cpp(true) // this will find the appropriate c++ lib for z3 + // silence the compiler wrapper output, needed for some configure scripts. + .silence(true) + .parse_args(&args) + .expect("Failed to parse the command line") + .link_staticlib(&dir, "libfuzzer_libpng") + .add_arg("-fsanitize-coverage=trace-pc-guard") + .run() + .expect("Failed to run the wrapped compiler") + { + std::process::exit(code); + } + } else { + panic!("LibAFL CC: No Arguments given"); + } +} diff --git a/fuzzers/libfuzzer_libpng_cmin/src/bin/libafl_cxx.rs b/fuzzers/libfuzzer_libpng_cmin/src/bin/libafl_cxx.rs new file mode 100644 index 0000000000..ce786239b0 --- /dev/null +++ b/fuzzers/libfuzzer_libpng_cmin/src/bin/libafl_cxx.rs @@ -0,0 +1,5 @@ +pub mod libafl_cc; + +fn main() { + libafl_cc::main() +} diff --git a/fuzzers/libfuzzer_libpng_cmin/src/lib.rs b/fuzzers/libfuzzer_libpng_cmin/src/lib.rs new file mode 100644 index 0000000000..c23609cddd --- /dev/null +++ b/fuzzers/libfuzzer_libpng_cmin/src/lib.rs @@ -0,0 +1,226 @@ +//! A libfuzzer-like fuzzer with llmp-multithreading support and restarts +//! The example harness is built for libpng. +use mimalloc::MiMalloc; +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +use core::time::Duration; +#[cfg(feature = "crash")] +use std::ptr; +use std::{env, path::PathBuf}; + +use libafl::{ + bolts::{ + current_nanos, + rands::StdRand, + tuples::{tuple_list, Merge}, + AsSlice, + }, + corpus::{ + minimizer::{CorpusMinimizer, StdCorpusMinimizer}, + Corpus, InMemoryCorpus, OnDiskCorpus, + }, + events::{setup_restarting_mgr_std, EventConfig, EventFirer, EventRestarter, LogSeverity}, + executors::{inprocess::InProcessExecutor, ExitKind, TimeoutExecutor}, + feedback_or, feedback_or_fast, + feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback, TimeoutFeedback}, + fuzzer::{Fuzzer, StdFuzzer}, + inputs::{BytesInput, HasTargetBytes}, + monitors::MultiMonitor, + mutators::{ + scheduled::{havoc_mutations, tokens_mutations, StdScheduledMutator}, + token_mutations::Tokens, + }, + observers::{HitcountsMapObserver, StdMapObserver, TimeObserver}, + schedulers::{ + powersched::PowerSchedule, IndexesLenTimeMinimizerScheduler, StdWeightedScheduler, + }, + stages::{calibrate::CalibrationStage, power::StdPowerMutationalStage}, + state::{HasCorpus, HasMetadata, StdState}, + Error, +}; +use libafl_targets::{libfuzzer_initialize, libfuzzer_test_one_input, EDGES_MAP, MAX_EDGES_NUM}; + +/// The main fn, `no_mangle` as it is a C main +#[cfg(not(test))] +#[no_mangle] +pub fn libafl_main() { + // Registry the metadata types used in this fuzzer + // Needed only on no_std + //RegistryBuilder::register::(); + + println!( + "Workdir: {:?}", + env::current_dir().unwrap().to_string_lossy().to_string() + ); + fuzz( + &[PathBuf::from("./corpus")], + PathBuf::from("./crashes"), + 1337, + ) + .expect("An error occurred while fuzzing"); +} + +/// The actual fuzzer +#[cfg(not(test))] +fn fuzz(corpus_dirs: &[PathBuf], objective_dir: PathBuf, broker_port: u16) -> Result<(), Error> { + // 'While the stats are state, they are usually used in the broker - which is likely never restarted + let monitor = MultiMonitor::new(|s| println!("{}", s)); + + // The restarting state will spawn the same process again as child, then restarted it each time it crashes. + let (state, mut restarting_mgr) = + match setup_restarting_mgr_std(monitor, broker_port, EventConfig::AlwaysUnique) { + Ok(res) => res, + Err(err) => match err { + Error::ShuttingDown => { + return Ok(()); + } + _ => { + panic!("Failed to setup the restarter: {}", err); + } + }, + }; + + // Create an observation channel using the coverage map + let edges = unsafe { &mut EDGES_MAP[0..MAX_EDGES_NUM] }; + let edges_observer = HitcountsMapObserver::new(StdMapObserver::new("edges", edges)); + + let minimizer = StdCorpusMinimizer::new(&edges_observer); + + // Create an observation channel to keep track of the execution time + let time_observer = TimeObserver::new("time"); + + let map_feedback = MaxMapFeedback::new_tracking(&edges_observer, true, false); + + let calibration = CalibrationStage::new(&map_feedback); + + // Feedback to rate the interestingness of an input + // This one is composed by two Feedbacks in OR + let mut feedback = feedback_or!( + // New maximization map feedback linked to the edges observer and the feedback state + map_feedback, + // Time feedback, this one does not need a feedback state + TimeFeedback::new_with_observer(&time_observer) + ); + + // A feedback to choose if an input is a solution or not + let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new()); + + // If not restarting, create a State from scratch + let mut state = state.unwrap_or_else(|| { + StdState::new( + // RNG + StdRand::with_seed(current_nanos()), + // Corpus that will be evolved, we keep it in memory for performance + InMemoryCorpus::new(), + // Corpus in which we store solutions (crashes in this example), + // on disk so the user can get them after stopping the fuzzer + OnDiskCorpus::new(objective_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() + }); + + println!("We're a client, let's fuzz :)"); + + // Create a PNG dictionary if not existing + if state.metadata().get::().is_none() { + state.add_metadata(Tokens::from([ + vec![137, 80, 78, 71, 13, 10, 26, 10], // PNG header + "IHDR".as_bytes().to_vec(), + "IDAT".as_bytes().to_vec(), + "PLTE".as_bytes().to_vec(), + "IEND".as_bytes().to_vec(), + ])); + } + + // Setup a basic mutator with a mutational stage + + let mutator = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations())); + + let power = StdPowerMutationalStage::new(mutator, &edges_observer); + + let mut stages = tuple_list!(calibration, power); + + // A minimization+queue policy to get testcasess from the corpus + let scheduler = IndexesLenTimeMinimizerScheduler::new(StdWeightedScheduler::with_schedule( + PowerSchedule::FAST, + )); + + // A fuzzer with feedbacks and a corpus scheduler + let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); + + // The wrapped harness function, calling out to the LLVM-style harness + let mut harness = |input: &BytesInput| { + let target = input.target_bytes(); + let buf = target.as_slice(); + #[cfg(feature = "crash")] + if buf.len() > 4 && buf[4] == 0 { + unsafe { + eprintln!("Crashing (for testing purposes)"); + let addr = ptr::null_mut(); + *addr = 1; + } + } + libfuzzer_test_one_input(buf); + ExitKind::Ok + }; + + // Create the executor for an in-process function with one observer for edge coverage and one for the execution time + let mut executor = TimeoutExecutor::new( + InProcessExecutor::new( + &mut harness, + tuple_list!(edges_observer, time_observer), + &mut fuzzer, + &mut state, + &mut restarting_mgr, + )?, + // 10 seconds timeout + Duration::new(10, 0), + ); + + // The actual target run starts here. + // Call LLVMFUzzerInitialize() if present. + let args: Vec = env::args().collect(); + if libfuzzer_initialize(&args) == -1 { + println!("Warning: LLVMFuzzerInitialize failed with -1") + } + + // In case the corpus is empty (on first run), reset + if state.corpus().count() < 1 { + state + .load_initial_inputs(&mut fuzzer, &mut executor, &mut restarting_mgr, corpus_dirs) + .unwrap_or_else(|_| panic!("Failed to load initial corpus at {:?}", &corpus_dirs)); + println!("We imported {} inputs from disk.", state.corpus().count()); + } + + // This fuzzer restarts after 1 mio `fuzz_one` executions. + // Each fuzz_one will internally do many executions of the target. + // If your target is very instable, setting a low count here may help. + // However, you will lose a lot of performance that way. + let iters = 10_000; + fuzzer.fuzz_loop_for( + &mut stages, + &mut executor, + &mut state, + &mut restarting_mgr, + iters, + )?; + + let orig_size = state.corpus().count(); + let msg = "Started distillation...".to_string(); + restarting_mgr.log(&mut state, LogSeverity::Info, msg)?; + minimizer.minimize(&mut fuzzer, &mut executor, &mut restarting_mgr, &mut state)?; + let msg = format!("Distilled out {} cases", orig_size - state.corpus().count()); + restarting_mgr.log(&mut state, LogSeverity::Info, msg)?; + + // It's important, that we store the state before restarting! + // Else, the parent will not respawn a new child and quit. + restarting_mgr.on_restart(&mut state)?; + + Ok(()) +} diff --git a/libafl/Cargo.toml b/libafl/Cargo.toml index 54c8c27b07..d9b33193a6 100644 --- a/libafl/Cargo.toml +++ b/libafl/Cargo.toml @@ -26,6 +26,7 @@ qemu_cli = ["cli"] frida_cli = ["cli"] afl_exec_sec = [] # calculate exec/sec like AFL errors_backtrace = ["backtrace"] +cmin = ["z3"] # corpus minimisation # features hiding dependencies licensed under GPL gpl = [] diff --git a/libafl/src/corpus/minimizer.rs b/libafl/src/corpus/minimizer.rs new file mode 100644 index 0000000000..eefe19b7c3 --- /dev/null +++ b/libafl/src/corpus/minimizer.rs @@ -0,0 +1,205 @@ +//! Whole corpus minimizers, for reducing the number of samples/the total size/the average runtime +//! of your corpus. + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use core::{hash::Hash, marker::PhantomData}; + +use hashbrown::{HashMap, HashSet}; +use num_traits::ToPrimitive; +use z3::{ast::Bool, Config, Context, Optimize}; + +use crate::{ + bolts::AsIter, + corpus::Corpus, + events::EventManager, + executors::{Executor, HasObservers}, + inputs::Input, + observers::{MapObserver, ObserversTuple}, + schedulers::{LenTimeMulTestcaseScore, Scheduler, TestcaseScore}, + state::{HasCorpus, HasMetadata}, + Error, Evaluator, HasScheduler, +}; + +/// `CorpusMinimizers` minimize corpora according to internal logic. See various implementations for +/// details. +pub trait CorpusMinimizer +where + I: Input, + S: HasCorpus, +{ + /// Minimize the corpus of the provided state. + fn minimize( + &self, + fuzzer: &mut Z, + executor: &mut EX, + manager: &mut EM, + state: &mut S, + ) -> Result<(), Error> + where + CS: Scheduler, + EX: Executor + HasObservers, + EM: EventManager, + OT: ObserversTuple, + Z: Evaluator + HasScheduler; +} + +/// Minimizes a corpus according to coverage maps, weighting by the specified `TestcaseScore`. +/// +/// Algorithm based on WMOPT: +#[derive(Debug)] +pub struct MapCorpusMinimizer +where + E: Copy + Hash + Eq, + I: Input, + for<'a> O: MapObserver + AsIter<'a, Item = E>, + S: HasMetadata + HasCorpus, + TS: TestcaseScore, +{ + obs_name: String, + phantom: PhantomData<(E, I, O, S, TS)>, +} + +/// Standard corpus minimizer, which weights inputs by length and time. +pub type StdCorpusMinimizer = + MapCorpusMinimizer>; + +impl MapCorpusMinimizer +where + E: Copy + Hash + Eq, + I: Input, + for<'a> O: MapObserver + AsIter<'a, Item = E>, + S: HasMetadata + HasCorpus, + TS: TestcaseScore, +{ + /// Constructs a new `MapCorpusMinimizer` from a provided observer. This observer will be used + /// in the future to get observed maps from an executed input. + pub fn new(obs: &O) -> Self { + Self { + obs_name: obs.name().to_string(), + phantom: PhantomData, + } + } +} + +impl CorpusMinimizer for MapCorpusMinimizer +where + E: Copy + Hash + Eq, + I: Input, + for<'a> O: MapObserver + AsIter<'a, Item = E>, + S: HasMetadata + HasCorpus, + TS: TestcaseScore, +{ + fn minimize( + &self, + fuzzer: &mut Z, + executor: &mut EX, + manager: &mut EM, + state: &mut S, + ) -> Result<(), Error> + where + CS: Scheduler, + EX: Executor + HasObservers, + EM: EventManager, + OT: ObserversTuple, + Z: Evaluator + HasScheduler, + { + let cfg = Config::default(); + let ctx = Context::new(&cfg); + let opt = Optimize::new(&ctx); + + let mut seed_exprs = HashMap::new(); + let mut cov_map = HashMap::new(); + + for idx in 0..state.corpus().count() { + let (weight, input) = { + let mut testcase = state.corpus().get(idx)?.borrow_mut(); + let weight = TS::compute(&mut *testcase, state)? + .to_u64() + .expect("Weight must be computable."); + let input = testcase + .input() + .as_ref() + .expect("Input must be available.") + .clone(); + (weight, input) + }; + + // Execute the input; we cannot rely on the metadata already being present. + executor.observers_mut().pre_exec_all(state, &input)?; + let kind = executor.run_target(fuzzer, state, manager, &input)?; + executor + .observers_mut() + .post_exec_all(state, &input, &kind)?; + + let seed_expr = Bool::fresh_const(&ctx, "seed"); + let obs: &O = executor + .observers() + .match_name::(&self.obs_name) + .expect("Observer must be present."); + + // Store coverage, mapping coverage map indices to hit counts (if present) and the + // associated seeds for the map indices with those hit counts. + for (i, e) in obs.as_iter().copied().enumerate() { + cov_map + .entry(i) + .or_insert_with(HashMap::new) + .entry(e) + .or_insert_with(HashSet::new) + .insert(seed_expr.clone()); + } + + // Keep track of that seed's index and weight + seed_exprs.insert(seed_expr, (idx, weight)); + } + + for (_, cov) in cov_map { + for (_, seeds) in cov { + // At least one seed for each hit count of each coverage map index + if let Some(reduced) = seeds.into_iter().reduce(|s1, s2| s1 | s2) { + opt.assert(&reduced); + } + } + } + for (seed, (_, weight)) in &seed_exprs { + // opt will attempt to minimise the number of violated assertions. + // + // To tell opt to minimize the number of seeds, we tell opt to maximize the number of + // not seeds. + // + // Additionally, each seed has a weight associated with them; the higher, the more z3 + // doesn't want to violate the assertion. Thus, inputs which have higher weights will be + // less likely to appear in the final corpus -- provided all their coverage points are + // hit by at least one other input. + opt.assert_soft(&!seed, *weight, None); + } + + // Perform the optimization! + opt.check(&[]); + + let res = if let Some(model) = opt.get_model() { + let mut removed = Vec::with_capacity(state.corpus().count()); + for (seed, (idx, _)) in seed_exprs { + // if the model says the seed isn't there, mark it for deletion + if !model.eval(&seed, true).unwrap().as_bool().unwrap() { + removed.push(idx); + } + } + // reverse order; if indexes are stored in a vec, we need to remove from back to front + removed.sort_unstable_by(|idx1, idx2| idx2.cmp(idx1)); + for idx in removed { + let removed = state.corpus_mut().remove(idx)?; + // scheduler needs to know we've removed the input, or it will continue to try + // to use now-missing inputs + fuzzer.scheduler_mut().on_remove(state, idx, &removed)?; + } + Ok(()) + } else { + Err(Error::unknown("Corpus minimization failed; unsat.")) + }; + + res + } +} diff --git a/libafl/src/corpus/mod.rs b/libafl/src/corpus/mod.rs index 2fa1f1babf..c79e71477d 100644 --- a/libafl/src/corpus/mod.rs +++ b/libafl/src/corpus/mod.rs @@ -13,11 +13,16 @@ pub use ondisk::OnDiskCorpus; #[cfg(feature = "std")] pub mod cached; -use core::cell::RefCell; - #[cfg(feature = "std")] pub use cached::CachedOnDiskCorpus; +#[cfg(feature = "cmin")] +pub mod minimizer; +use core::cell::RefCell; + +#[cfg(feature = "cmin")] +pub use minimizer::*; + use crate::{inputs::Input, Error}; /// Corpus with all current testcases diff --git a/libafl/src/inputs/generalized.rs b/libafl/src/inputs/generalized.rs index fa0fcccdbd..50a5030c53 100644 --- a/libafl/src/inputs/generalized.rs +++ b/libafl/src/inputs/generalized.rs @@ -43,6 +43,22 @@ impl Input for GeneralizedInput { format!("{:016x}", hasher.finish()) } + /// Load from a plain file of bytes + #[cfg(feature = "std")] + fn from_file

(path: P) -> Result + where + P: AsRef, + { + let mut file = File::open(path)?; + let mut bytes: Vec = vec![]; + file.read_to_end(&mut bytes)?; + Ok(Self { + bytes, + generalized: None, + grimoire_mutated: false, + }) + } + /// An hook executed before being added to the corpus fn wrapped_as_testcase(&mut self) { // remove generalized for inputs generated with bit-level mutations @@ -206,20 +222,4 @@ impl GeneralizedInput { pub fn generalized_mut(&mut self) -> &mut Option> { &mut self.generalized } - - /// Load from a plain file of bytes - #[cfg(feature = "std")] - pub fn from_bytes_file

(path: P) -> Result - where - P: AsRef, - { - let mut file = File::open(path)?; - let mut bytes: Vec = vec![]; - file.read_to_end(&mut bytes)?; - Ok(Self { - bytes, - generalized: None, - grimoire_mutated: false, - }) - } } diff --git a/libafl/src/schedulers/minimizer.rs b/libafl/src/schedulers/minimizer.rs index cf27d387a3..c82512b191 100644 --- a/libafl/src/schedulers/minimizer.rs +++ b/libafl/src/schedulers/minimizer.rs @@ -1,7 +1,8 @@ //! The Minimizer schedulers are a family of corpus schedulers that feed the fuzzer -// with testcases only from a subset of the total corpus. +//! with testcases only from a subset of the total corpus. -use core::marker::PhantomData; +use alloc::vec::Vec; +use core::{cmp::Ordering, marker::PhantomData}; use hashbrown::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; @@ -99,7 +100,67 @@ where idx: usize, testcase: &Option>, ) -> Result<(), Error> { - self.base.on_remove(state, idx, testcase) + self.base.on_remove(state, idx, testcase)?; + let mut entries = if let Some(meta) = state.metadata_mut().get_mut::() { + let entries = meta + .map + .drain_filter(|_, other_idx| *other_idx == idx) + .map(|(entry, _)| entry) + .collect::>(); + meta.map + .values_mut() + .filter(|other_idx| **other_idx > idx) + .for_each(|other_idx| { + *other_idx -= 1; + }); + entries + } else { + return Ok(()); + }; + entries.sort_unstable(); // this should already be sorted, but just in case + let mut map = HashMap::new(); + for i in 0..state.corpus().count() { + let mut old = state.corpus().get(i)?.borrow_mut(); + let factor = F::compute(&mut *old, state)?; + if let Some(old_map) = old.metadata_mut().get_mut::() { + let mut e_iter = entries.iter(); + let mut map_iter = old_map.as_slice().iter(); // ASSERTION: guaranteed to be in order? + + // manual set intersection + let mut entry = e_iter.next(); + let mut map_entry = map_iter.next(); + while let Some(e) = entry { + if let Some(me) = map_entry { + match e.cmp(me) { + Ordering::Less => { + entry = e_iter.next(); + } + Ordering::Equal => { + // if we found a better factor, prefer it + map.entry(*e) + .and_modify(|(f, idx)| { + if *f > factor { + *f = factor; + *idx = i; + } + }) + .or_insert((factor, i)); + } + Ordering::Greater => { + map_entry = map_iter.next(); + } + } + } else { + break; + } + } + } + } + if let Some(meta) = state.metadata_mut().get_mut::() { + meta.map + .extend(map.into_iter().map(|(entry, (_, idx))| (entry, idx))); + } + Ok(()) } /// Gets the next entry diff --git a/libafl/src/schedulers/weighted.rs b/libafl/src/schedulers/weighted.rs index 1408cbb1a1..878f2db1f5 100644 --- a/libafl/src/schedulers/weighted.rs +++ b/libafl/src/schedulers/weighted.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::{ bolts::rands::Rand, - corpus::{Corpus, SchedulerTestcaseMetaData}, + corpus::{Corpus, SchedulerTestcaseMetaData, Testcase}, inputs::Input, schedulers::{ powersched::{PowerSchedule, SchedulerMetadata}, @@ -264,6 +264,22 @@ where Ok(()) } + fn on_replace(&self, state: &mut S, idx: usize, _testcase: &Testcase) -> Result<(), Error> { + // Recreate the alias table + self.on_add(state, idx) + } + + fn on_remove( + &self, + state: &mut S, + _idx: usize, + _testcase: &Option>, + ) -> Result<(), Error> { + // Recreate the alias table + self.create_alias_table(state)?; + Ok(()) + } + #[allow(clippy::similar_names, clippy::cast_precision_loss)] fn next(&self, state: &mut S) -> Result { if state.corpus().count() == 0 { @@ -283,7 +299,7 @@ where let current_cycles = wsmeta.runs_in_current_cycle(); - if current_cycles > corpus_counts { + if current_cycles >= corpus_counts { wsmeta.set_runs_current_cycle(0); } else { wsmeta.set_runs_current_cycle(current_cycles + 1); From 8a7cda89cde9e63e3003674cddf160b6ddb5e58b Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Mon, 29 Aug 2022 13:44:22 +0200 Subject: [PATCH 10/16] Skippable stage, generator wrapper for Grimoire (#748) * Skippable stage, generator wrapper for Grimoire * more fancy wrapper --- libafl/src/generators/mod.rs | 47 ++++++++++++++++++++- libafl/src/inputs/bytes.rs | 2 +- libafl/src/inputs/generalized.rs | 14 ++++++- libafl/src/stages/mod.rs | 71 ++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 3 deletions(-) diff --git a/libafl/src/generators/mod.rs b/libafl/src/generators/mod.rs index 6a7dbe1c13..d8d92ca8b1 100644 --- a/libafl/src/generators/mod.rs +++ b/libafl/src/generators/mod.rs @@ -5,7 +5,7 @@ use core::{cmp::min, marker::PhantomData}; use crate::{ bolts::rands::Rand, - inputs::{bytes::BytesInput, Input}, + inputs::{bytes::BytesInput, GeneralizedInput, Input}, state::HasRand, Error, }; @@ -33,6 +33,51 @@ where fn generate_dummy(&self, state: &mut S) -> I; } +/// A Generator that produces [`GeneralizedInput`]s from a wrapped [`BytesInput`] generator +#[derive(Clone, Debug)] +pub struct GeneralizedInputBytesGenerator { + bytes_generator: G, + phantom: PhantomData, +} + +impl GeneralizedInputBytesGenerator +where + S: HasRand, + G: Generator, +{ + /// Creates a new [`GeneralizedInputBytesGenerator`] by wrapping a bytes generator. + pub fn new(bytes_generator: G) -> Self { + Self { + bytes_generator, + phantom: PhantomData, + } + } +} + +impl From for GeneralizedInputBytesGenerator +where + S: HasRand, + G: Generator, +{ + fn from(bytes_generator: G) -> Self { + Self::new(bytes_generator) + } +} + +impl Generator for GeneralizedInputBytesGenerator +where + S: HasRand, + G: Generator, +{ + fn generate(&mut self, state: &mut S) -> Result { + Ok(self.bytes_generator.generate(state)?.into()) + } + + fn generate_dummy(&self, state: &mut S) -> GeneralizedInput { + self.bytes_generator.generate_dummy(state).into() + } +} + #[derive(Clone, Debug)] /// Generates random bytes pub struct RandBytesGenerator diff --git a/libafl/src/inputs/bytes.rs b/libafl/src/inputs/bytes.rs index 2ab274d9fc..9e3b8353ed 100644 --- a/libafl/src/inputs/bytes.rs +++ b/libafl/src/inputs/bytes.rs @@ -20,7 +20,7 @@ use crate::{ #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash)] pub struct BytesInput { /// The raw input bytes - bytes: Vec, + pub(crate) bytes: Vec, } impl Input for BytesInput { diff --git a/libafl/src/inputs/generalized.rs b/libafl/src/inputs/generalized.rs index 50a5030c53..a32040b9c2 100644 --- a/libafl/src/inputs/generalized.rs +++ b/libafl/src/inputs/generalized.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use crate::Error; use crate::{ bolts::{ownedref::OwnedSlice, HasLen}, - inputs::{HasBytesVec, HasTargetBytes, Input}, + inputs::{BytesInput, HasBytesVec, HasTargetBytes, Input}, }; /// An item of the generalized input @@ -122,6 +122,18 @@ impl From<&[u8]> for GeneralizedInput { } } +impl From for GeneralizedInput { + fn from(bytes_input: BytesInput) -> Self { + Self::new(bytes_input.bytes) + } +} + +impl From<&BytesInput> for GeneralizedInput { + fn from(bytes_input: &BytesInput) -> Self { + bytes_input.bytes().into() + } +} + impl GeneralizedInput { /// Creates a new bytes input using the given bytes #[must_use] diff --git a/libafl/src/stages/mod.rs b/libafl/src/stages/mod.rs index 844aec861b..0a32c006e3 100644 --- a/libafl/src/stages/mod.rs +++ b/libafl/src/stages/mod.rs @@ -260,6 +260,77 @@ where } } +/// The decision if the [`SkippableStage`] should be skipped +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum SkippableStageDecision { + /// Return to indicate that this [`Stage`] should be executed + Perform, + /// Return to indicate that this [`Stage`] should be skipped + Skip, +} + +impl From for SkippableStageDecision { + fn from(b: bool) -> SkippableStageDecision { + if b { + SkippableStageDecision::Perform + } else { + SkippableStageDecision::Skip + } + } +} + +/// The [`SkippableStage`] wraps any [`Stage`] so that it can be skipped, according to a condition. +#[derive(Debug, Clone)] +pub struct SkippableStage +where + CD: FnMut(&mut S) -> SkippableStageDecision, + ST: Stage, +{ + wrapped_stage: ST, + condition: CD, + phantom: PhantomData<(E, EM, S, Z)>, +} + +impl SkippableStage +where + CD: FnMut(&mut S) -> SkippableStageDecision, + ST: Stage, +{ + /// Create a new [`SkippableStage`] + pub fn new(wrapped_stage: ST, condition: CD) -> Self { + Self { + wrapped_stage, + condition, + phantom: PhantomData, + } + } +} + +impl Stage for SkippableStage +where + CD: FnMut(&mut S) -> SkippableStageDecision, + ST: Stage, +{ + /// Run the stage + #[inline] + fn perform( + &mut self, + fuzzer: &mut Z, + executor: &mut E, + state: &mut S, + manager: &mut EM, + corpus_idx: usize, + ) -> Result<(), Error> { + let condition = &mut self.condition; + if condition(state) == SkippableStageDecision::Perform { + self.wrapped_stage + .perform(fuzzer, executor, state, manager, corpus_idx) + } else { + Ok(()) + } + } +} + /// `Stage` Python bindings #[cfg(feature = "python")] #[allow(missing_docs)] From 98cc70b9858b6aad4db3c9809aaa7c08daa65a5d Mon Sep 17 00:00:00 2001 From: Patrick Gersch Date: Mon, 29 Aug 2022 14:43:00 +0200 Subject: [PATCH 11/16] MapFeedback: Adding support for with_name() (#752) * Adding support for with_name() * Adding with_name() function description --- libafl/src/feedbacks/map.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/libafl/src/feedbacks/map.rs b/libafl/src/feedbacks/map.rs index fec1132bb8..9458003ed3 100644 --- a/libafl/src/feedbacks/map.rs +++ b/libafl/src/feedbacks/map.rs @@ -633,6 +633,21 @@ where } } + /// Creating a new `MapFeedback` with a specific name. This is usefully whenever the same + /// feedback is needed twice, but with a different history. Using `new()` always results in the + /// same name and therefore also the same history. + #[must_use] + pub fn with_name(name: &'static str, map_observer: &O) -> Self { + Self { + indexes: None, + novelties: None, + name: name.to_string(), + observer_name: map_observer.name().to_string(), + stats_name: create_stats_name(name), + phantom: PhantomData, + } + } + /// Create new `MapFeedback` specifying if it must track indexes of used entries and/or novelties #[must_use] pub fn with_names_tracking( From c82070c18172c0fcadc80bf6657cc4cbaff3c42f Mon Sep 17 00:00:00 2001 From: David CARLIER Date: Tue, 30 Aug 2022 02:37:17 +0100 Subject: [PATCH 12/16] dragonflybsd build fix for core affinity. (#753) supporting most of linux sched api here. --- libafl/src/bolts/core_affinity.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libafl/src/bolts/core_affinity.rs b/libafl/src/bolts/core_affinity.rs index 38ce99efa2..deee4ed85d 100644 --- a/libafl/src/bolts/core_affinity.rs +++ b/libafl/src/bolts/core_affinity.rs @@ -193,24 +193,29 @@ pub fn parse_core_bind_arg(args: &str) -> Result, Error> { // Linux Section -#[cfg(any(target_os = "android", target_os = "linux"))] +#[cfg(any(target_os = "android", target_os = "linux", target_os = "dragonfly"))] #[inline] fn get_core_ids_helper() -> Result, Error> { linux::get_core_ids() } -#[cfg(any(target_os = "android", target_os = "linux"))] +#[cfg(any(target_os = "android", target_os = "linux", target_os = "dragonfly"))] #[inline] fn set_for_current_helper(core_id: CoreId) -> Result<(), Error> { linux::set_for_current(core_id) } -#[cfg(any(target_os = "android", target_os = "linux"))] +#[cfg(any(target_os = "android", target_os = "linux", target_os = "dragonfly"))] mod linux { use alloc::{string::ToString, vec::Vec}; use std::mem; + #[cfg(target_os = "dragonfly")] + use libc::{cpu_set_t, sched_getaffinity, sched_setaffinity, CPU_ISSET, CPU_SET}; + #[cfg(not(target_os = "dragonfly"))] use libc::{cpu_set_t, sched_getaffinity, sched_setaffinity, CPU_ISSET, CPU_SET, CPU_SETSIZE}; + #[cfg(target_os = "dragonfly")] + const CPU_SETSIZE: libc::c_int = 256; use super::CoreId; use crate::Error; From eeb03bb15760c2b45e46bf79679617e8d435eb8f Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Tue, 30 Aug 2022 06:01:12 -0500 Subject: [PATCH 13/16] make static v8 accessors --- libafl_v8/Cargo.toml | 1 + libafl_v8/src/executors.rs | 55 ++++++++++------------- libafl_v8/src/lib.rs | 58 +++++++++++++++++++++--- libafl_v8/src/observers/cov.rs | 74 +++++++++---------------------- libafl_v8/src/observers/types.rs | 76 ++++++++++---------------------- 5 files changed, 121 insertions(+), 143 deletions(-) diff --git a/libafl_v8/Cargo.toml b/libafl_v8/Cargo.toml index ff344d9b36..98ef426a4a 100644 --- a/libafl_v8/Cargo.toml +++ b/libafl_v8/Cargo.toml @@ -14,6 +14,7 @@ deno_core = { git = "https://github.com/denoland/deno.git", tag = "v1.24.3" } deno_runtime = { git = "https://github.com/denoland/deno.git", tag = "v1.24.3" } import_map = "0.12.1" libafl = { path = "../libafl", version = "0.8.1" } +send_wrapper = "0.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.20.1", features = ["rt", "rt-multi-thread", "sync"] } diff --git a/libafl_v8/src/executors.rs b/libafl_v8/src/executors.rs index 74f4ca1aaa..27d2867bdf 100644 --- a/libafl_v8/src/executors.rs +++ b/libafl_v8/src/executors.rs @@ -8,10 +8,8 @@ use core::{ fmt::{Debug, Formatter}, marker::PhantomData, }; -use std::sync::Arc; use deno_core::{v8, ModuleId, ModuleSpecifier}; -use deno_runtime::worker::MainWorker; use libafl::{ events::{EventFirer, EventRestarter}, executors::{Executor, ExitKind, HasObservers}, @@ -21,13 +19,12 @@ use libafl::{ state::{HasClientPerfMonitor, HasSolutions, State}, Error, HasObjective, }; -use tokio::runtime::Runtime; use v8::{Function, Local, TryCatch}; -use crate::{values::IntoJSValue, Mutex}; +use crate::{values::IntoJSValue, RUNTIME, WORKER}; /// Executor which executes JavaScript using Deno and V8. -pub struct V8Executor<'rt, I, OT, S> +pub struct V8Executor where I: Input + IntoJSValue, OT: ObserversTuple, @@ -35,12 +32,10 @@ where { id: ModuleId, observers: OT, - rt: &'rt Runtime, - worker: Arc>, phantom: PhantomData<(I, S)>, } -impl<'rt, I, OT, S> V8Executor<'rt, I, OT, S> +impl V8Executor where I: Input + IntoJSValue, OT: ObserversTuple, @@ -48,8 +43,6 @@ where { /// Create a new V8 executor. pub fn new( - rt: &'rt Runtime, - worker: Arc>, main_module: ModuleSpecifier, observers: OT, _fuzzer: &mut Z, @@ -62,16 +55,19 @@ where S: HasSolutions + HasClientPerfMonitor, Z: HasObjective, { - let copy = worker.clone(); - let id = match rt.block_on(async { - let mut locked = copy.lock().await; - let mod_id = locked.preload_main_module(&main_module).await?; - let handle = locked.js_runtime.mod_evaluate(mod_id); - locked.run_event_loop(false).await?; - handle.await??; - - Ok::(mod_id) - }) { + let id = match unsafe { RUNTIME.as_ref() } + .expect("Runtime must be initialized before creating executors; use initialize_v8!") + .block_on(async { + let worker = unsafe { WORKER.as_mut() }.expect( + "Worker must be initialized before creating executors; use initialize_v8!", + ); + let mod_id = worker.preload_main_module(&main_module).await?; + let handle = worker.js_runtime.mod_evaluate(mod_id); + worker.run_event_loop(false).await?; + handle.await??; + + Ok::(mod_id) + }) { Err(e) => return Err(Error::unknown(e.to_string())), Ok(id) => id, }; @@ -79,21 +75,18 @@ where Ok(Self { id, observers, - rt, - worker, phantom: Default::default(), }) } fn invoke_harness_func(&self, input: &I) -> Result { let id = self.id; - let copy = self.worker.clone(); - self.rt.block_on(async { - let mut locked = copy.lock().await; + unsafe { RUNTIME.as_ref() }.unwrap().block_on(async { + let worker = unsafe { WORKER.as_mut() }.unwrap(); let res = { - let module_namespace = locked.js_runtime.get_module_namespace(id).unwrap(); - let mut scope = locked.js_runtime.handle_scope(); + let module_namespace = worker.js_runtime.get_module_namespace(id).unwrap(); + let mut scope = worker.js_runtime.handle_scope(); let module_namespace = Local::::new(&mut scope, module_namespace); let default_export_name = v8::String::new(&mut scope, "default").unwrap(); @@ -124,7 +117,7 @@ where } }; - if let Err(e) = locked.run_event_loop(false).await { + if let Err(e) = worker.run_event_loop(false).await { return Err(Error::illegal_state(e.to_string())); } res @@ -137,7 +130,7 @@ where } } -impl<'rt, EM, I, OT, S, Z> Executor for V8Executor<'rt, I, OT, S> +impl Executor for V8Executor where I: Input + IntoJSValue, OT: ObserversTuple, @@ -154,7 +147,7 @@ where } } -impl<'rt, I, OT, S> HasObservers for V8Executor<'rt, I, OT, S> +impl HasObservers for V8Executor where I: Input + IntoJSValue, OT: ObserversTuple, @@ -169,7 +162,7 @@ where } } -impl<'rt, I, OT, S> Debug for V8Executor<'rt, I, OT, S> +impl Debug for V8Executor where I: Input + IntoJSValue, OT: ObserversTuple, diff --git a/libafl_v8/src/lib.rs b/libafl_v8/src/lib.rs index 113b843ea6..632c91db88 100644 --- a/libafl_v8/src/lib.rs +++ b/libafl_v8/src/lib.rs @@ -77,23 +77,69 @@ pub mod loader; pub mod observers; pub mod values; -use std::iter; +use std::{io, iter, sync::Arc}; +use deno_core::LocalInspectorSession; pub use deno_core::{self, v8}; pub use deno_runtime; +use deno_runtime::worker::MainWorker; pub use executors::*; use libafl::Error; pub use loader::*; pub use observers::*; -pub use tokio::{runtime, sync::Mutex}; +use send_wrapper::SendWrapper; +pub use tokio::runtime; +use tokio::{runtime::Runtime, sync::Mutex}; pub use values::*; use crate::v8::{HandleScope, Local, TryCatch}; -pub(crate) fn forbid_deserialization() -> T { - unimplemented!( - "Deserialization is forbidden for this type; cannot cross a serialization boundary" - ) +/// Runtime for the libafl v8 crate. Must be accessed from the main fuzzer thread. +pub(crate) static mut RUNTIME: Option> = None; +/// Worker for the libafl v8 crate. Must be accessed from the v8 worker thread. +pub(crate) static mut WORKER: Option> = None; + +/// Create an inspector for this fuzzer instance +pub(crate) fn create_inspector() -> Arc> { + let inspector = unsafe { RUNTIME.as_ref() } + .expect("Runtime must be initialized before creating inspector sessions; use libafl_v8::initialize_v8!") + .block_on(async { + let worker = unsafe { WORKER.as_mut() } + .expect( + "Worker must be initialized before creating inspector sessions; use libafl_v8::initialize_v8!", + ); + let mut session = worker.create_inspector_session().await; + if let Err(e) = worker + .with_event_loop(Box::pin( + session.post_message::<()>("Profiler.enable", None), + )) + .await + { + Err(Error::unknown(e.to_string())) + } else { + Ok(session) + } + }).expect("Couldn't create the inspector"); + Arc::new(Mutex::new(inspector)) +} + +/// Initialize the v8 execution environment for this fuzzer instance +pub fn initialize_v8(worker: MainWorker) -> io::Result<()> { + let runtime = runtime::Builder::new_current_thread().build()?; + runtime.block_on(async { + unsafe { + WORKER = Some(SendWrapper::new(worker)); + } + }); + unsafe { + RUNTIME = Some(SendWrapper::new(runtime)); + } + Ok(()) +} + +/// Check if the v8 execution environment is initialized for this fuzzer instance +pub fn v8_is_initialized() -> bool { + unsafe { RUNTIME.is_some() } } /// Convert a JS error from a try/catch scope into a libafl error diff --git a/libafl_v8/src/observers/cov.rs b/libafl_v8/src/observers/cov.rs index 5af54c32c9..ef4ed3b291 100644 --- a/libafl_v8/src/observers/cov.rs +++ b/libafl_v8/src/observers/cov.rs @@ -10,7 +10,6 @@ use std::{ use ahash::AHasher; use deno_core::LocalInspectorSession; -use deno_runtime::worker::MainWorker; use libafl::{ bolts::{AsIter, AsMutSlice, HasLen}, executors::ExitKind, @@ -19,11 +18,11 @@ use libafl::{ Error, }; use serde::{Deserialize, Serialize}; -use tokio::{runtime::Runtime, sync::Mutex}; +use tokio::sync::Mutex; pub use super::inspector_api::StartPreciseCoverageParameters; use super::inspector_api::TakePreciseCoverageReturnObject; -use crate::forbid_deserialization; +use crate::{create_inspector, RUNTIME, WORKER}; // while collisions are theoretically possible, the likelihood is vanishingly small #[derive(Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Clone)] @@ -115,34 +114,24 @@ impl JSCoverageMapper { /// Observer which inspects JavaScript coverage at either a block or function level #[derive(Serialize, Deserialize)] -pub struct JSMapObserver<'rt> { +pub struct JSMapObserver { initial: u8, initialized: bool, last_coverage: Vec, mapper: JSCoverageMapper, name: String, params: StartPreciseCoverageParameters, - #[serde(skip, default = "forbid_deserialization")] - rt: &'rt Runtime, - #[serde(skip, default = "forbid_deserialization")] - worker: Arc>, - #[serde(skip, default = "forbid_deserialization")] + #[serde(skip, default = "create_inspector")] inspector: Arc>, } -impl<'rt> JSMapObserver<'rt> { +impl JSMapObserver { /// Create the observer with the provided name to use the provided asynchronous runtime and JS /// worker to push inspector data. If you don't know what kind of coverage you want, use this /// constructor. - pub fn new( - name: &str, - rt: &'rt Runtime, - worker: Arc>, - ) -> Result { + pub fn new(name: &str) -> Result { Self::new_with_parameters( name, - rt, - worker, StartPreciseCoverageParameters { call_count: true, detailed: true, @@ -155,27 +144,8 @@ impl<'rt> JSMapObserver<'rt> { /// worker to push inspector data, and the parameters with which coverage is collected. pub fn new_with_parameters( name: &str, - rt: &'rt Runtime, - worker: Arc>, params: StartPreciseCoverageParameters, ) -> Result { - let inspector = { - let copy = worker.clone(); - rt.block_on(async { - let mut locked = copy.lock().await; - let mut session = locked.create_inspector_session().await; - if let Err(e) = locked - .with_event_loop(Box::pin( - session.post_message::<()>("Profiler.enable", None), - )) - .await - { - Err(Error::unknown(e.to_string())) - } else { - Ok(session) - } - })? - }; Ok(Self { initial: u8::default(), initialized: false, @@ -183,20 +153,18 @@ impl<'rt> JSMapObserver<'rt> { mapper: JSCoverageMapper::new(params.call_count), name: name.to_string(), params, - rt, - worker, - inspector: Arc::new(Mutex::new(inspector)), + inspector: create_inspector(), }) } } -impl<'rt> Named for JSMapObserver<'rt> { +impl Named for JSMapObserver { fn name(&self) -> &str { &self.name } } -impl<'rt> Debug for JSMapObserver<'rt> { +impl Debug for JSMapObserver { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.debug_struct("JSMapObserver") .field("initialized", &self.initialized) @@ -207,17 +175,16 @@ impl<'rt> Debug for JSMapObserver<'rt> { } } -impl<'rt, I, S> Observer for JSMapObserver<'rt> { +impl Observer for JSMapObserver { fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { self.reset_map()?; if !self.initialized { let inspector = self.inspector.clone(); let params = self.params.clone(); - let copy = self.worker.clone(); - self.rt.block_on(async { - let mut locked = copy.lock().await; + unsafe { RUNTIME.as_ref() }.unwrap().block_on(async { + let worker = unsafe { WORKER.as_mut() }.unwrap(); let mut session = inspector.lock().await; - if let Err(e) = locked + if let Err(e) = worker .with_event_loop(Box::pin( session.post_message("Profiler.startPreciseCoverage", Some(¶ms)), )) @@ -240,11 +207,10 @@ impl<'rt, I, S> Observer for JSMapObserver<'rt> { _exit_kind: &ExitKind, ) -> Result<(), Error> { let session = self.inspector.clone(); - let copy = self.worker.clone(); - let coverage = self.rt.block_on(async { - let mut locked = copy.lock().await; + let coverage = unsafe { RUNTIME.as_ref() }.unwrap().block_on(async { + let worker = unsafe { WORKER.as_mut() }.unwrap(); let mut session = session.lock().await; - match locked + match worker .with_event_loop(Box::pin( session.post_message::<()>("Profiler.takePreciseCoverage", None), )) @@ -273,13 +239,13 @@ impl<'rt, I, S> Observer for JSMapObserver<'rt> { } } -impl<'rt> HasLen for JSMapObserver<'rt> { +impl HasLen for JSMapObserver { fn len(&self) -> usize { self.last_coverage.len() } } -impl<'rt, 'it> AsIter<'it> for JSMapObserver<'rt> { +impl<'it> AsIter<'it> for JSMapObserver { type Item = u8; type IntoIter = Iter<'it, u8>; @@ -288,13 +254,13 @@ impl<'rt, 'it> AsIter<'it> for JSMapObserver<'rt> { } } -impl<'rt> AsMutSlice for JSMapObserver<'rt> { +impl AsMutSlice for JSMapObserver { fn as_mut_slice(&mut self) -> &mut [u8] { self.last_coverage.as_mut_slice() } } -impl<'rt> MapObserver for JSMapObserver<'rt> { +impl MapObserver for JSMapObserver { type Entry = u8; fn get(&self, idx: usize) -> &Self::Entry { diff --git a/libafl_v8/src/observers/types.rs b/libafl_v8/src/observers/types.rs index c71d472d37..a8f54e7c82 100644 --- a/libafl_v8/src/observers/types.rs +++ b/libafl_v8/src/observers/types.rs @@ -8,7 +8,6 @@ use std::{ use ahash::AHasher; use deno_core::LocalInspectorSession; -use deno_runtime::worker::MainWorker; use libafl::{ bolts::{AsIter, AsMutSlice, HasLen}, executors::ExitKind, @@ -17,9 +16,11 @@ use libafl::{ Error, }; use serde::{Deserialize, Serialize}; -use tokio::{runtime::Runtime, sync::Mutex}; +use tokio::sync::Mutex; -use crate::{forbid_deserialization, observers::inspector_api::TakeTypeProfileReturnObject}; +use crate::{ + create_inspector, observers::inspector_api::TakeTypeProfileReturnObject, RUNTIME, WORKER, +}; // while collisions are theoretically possible, the likelihood is vanishingly small #[derive(Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Clone)] @@ -95,65 +96,38 @@ impl JSTypeMapper { /// Observer which inspects JavaScript type usage for parameters and return values #[derive(Serialize, Deserialize)] -pub struct JSTypeObserver<'rt> { +pub struct JSTypeObserver { initial: u8, initialized: bool, last_coverage: Vec, name: String, mapper: JSTypeMapper, - #[serde(skip, default = "forbid_deserialization")] - rt: &'rt Runtime, - #[serde(skip, default = "forbid_deserialization")] - worker: Arc>, - #[serde(skip, default = "forbid_deserialization")] + #[serde(skip, default = "create_inspector")] inspector: Arc>, } -impl<'rt> JSTypeObserver<'rt> { +impl JSTypeObserver { /// Create the observer with the provided name to use the provided asynchronous runtime, JS /// worker to push inspector data, and the parameters with which coverage is collected. - pub fn new( - name: &str, - rt: &'rt Runtime, - worker: Arc>, - ) -> Result { - let inspector = { - let copy = worker.clone(); - rt.block_on(async { - let mut locked = copy.lock().await; - let mut session = locked.create_inspector_session().await; - if let Err(e) = locked - .with_event_loop(Box::pin( - session.post_message::<()>("Profiler.enable", None), - )) - .await - { - Err(Error::unknown(e.to_string())) - } else { - Ok(session) - } - })? - }; + pub fn new(name: &str) -> Result { Ok(Self { initial: u8::default(), initialized: false, last_coverage: Vec::new(), name: name.to_string(), mapper: JSTypeMapper::new(), - rt, - worker, - inspector: Arc::new(Mutex::new(inspector)), + inspector: create_inspector(), }) } } -impl<'rt> Named for JSTypeObserver<'rt> { +impl Named for JSTypeObserver { fn name(&self) -> &str { &self.name } } -impl<'rt> Debug for JSTypeObserver<'rt> { +impl Debug for JSTypeObserver { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("JSTypeObserver") .field("initialized", &self.initialized) @@ -163,16 +137,15 @@ impl<'rt> Debug for JSTypeObserver<'rt> { } } -impl<'rt, I, S> Observer for JSTypeObserver<'rt> { +impl Observer for JSTypeObserver { fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { self.reset_map()?; if !self.initialized { let inspector = self.inspector.clone(); - let copy = self.worker.clone(); - self.rt.block_on(async { - let mut locked = copy.lock().await; + unsafe { RUNTIME.as_ref() }.unwrap().block_on(async { + let worker = unsafe { WORKER.as_mut() }.unwrap(); let mut session = inspector.lock().await; - if let Err(e) = locked + if let Err(e) = worker .with_event_loop(Box::pin( session.post_message::<()>("Profiler.startTypeProfile", None), )) @@ -194,12 +167,11 @@ impl<'rt, I, S> Observer for JSTypeObserver<'rt> { _input: &I, _exit_kind: &ExitKind, ) -> Result<(), Error> { - let session = self.inspector.clone(); - let copy = self.worker.clone(); - let coverage = self.rt.block_on(async { - let mut locked = copy.lock().await; - let mut session = session.lock().await; - match locked + let inspector = self.inspector.clone(); + let coverage = unsafe { RUNTIME.as_ref() }.unwrap().block_on(async { + let worker = unsafe { WORKER.as_mut() }.unwrap(); + let mut session = inspector.lock().await; + match worker .with_event_loop(Box::pin( session.post_message::<()>("Profiler.takeTypeProfile", None), )) @@ -228,13 +200,13 @@ impl<'rt, I, S> Observer for JSTypeObserver<'rt> { } } -impl<'rt> HasLen for JSTypeObserver<'rt> { +impl HasLen for JSTypeObserver { fn len(&self) -> usize { self.last_coverage.len() } } -impl<'rt, 'it> AsIter<'it> for JSTypeObserver<'rt> { +impl<'it> AsIter<'it> for JSTypeObserver { type Item = u8; type IntoIter = Iter<'it, u8>; @@ -243,13 +215,13 @@ impl<'rt, 'it> AsIter<'it> for JSTypeObserver<'rt> { } } -impl<'rt> AsMutSlice for JSTypeObserver<'rt> { +impl AsMutSlice for JSTypeObserver { fn as_mut_slice(&mut self) -> &mut [u8] { self.last_coverage.as_mut_slice() } } -impl<'rt> MapObserver for JSTypeObserver<'rt> { +impl MapObserver for JSTypeObserver { type Entry = u8; fn get(&self, idx: usize) -> &Self::Entry { From 4a3fdad41b319a6c94cba8e4380aa5c43b8edc69 Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Tue, 30 Aug 2022 06:26:16 -0500 Subject: [PATCH 14/16] attach mapping data to state --- libafl_v8/src/observers/cov.rs | 40 ++++++++++++++++++-------------- libafl_v8/src/observers/types.rs | 38 ++++++++++++++++++------------ 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/libafl_v8/src/observers/cov.rs b/libafl_v8/src/observers/cov.rs index ef4ed3b291..bdd2adf97a 100644 --- a/libafl_v8/src/observers/cov.rs +++ b/libafl_v8/src/observers/cov.rs @@ -15,6 +15,7 @@ use libafl::{ executors::ExitKind, observers::{MapObserver, Observer}, prelude::Named, + state::HasMetadata, Error, }; use serde::{Deserialize, Serialize}; @@ -33,12 +34,14 @@ struct JSCoverageEntry { end_char_offset: usize, } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug)] struct JSCoverageMapper { count: bool, idx_map: HashMap, } +libafl::impl_serdeany!(JSCoverageMapper); + impl JSCoverageMapper { fn new(count: bool) -> Self { Self { @@ -118,11 +121,10 @@ pub struct JSMapObserver { initial: u8, initialized: bool, last_coverage: Vec, - mapper: JSCoverageMapper, name: String, params: StartPreciseCoverageParameters, - #[serde(skip, default = "create_inspector")] - inspector: Arc>, + #[serde(skip, default)] + inspector: Option>>, } impl JSMapObserver { @@ -150,10 +152,9 @@ impl JSMapObserver { initial: u8::default(), initialized: false, last_coverage: Vec::new(), - mapper: JSCoverageMapper::new(params.call_count), name: name.to_string(), params, - inspector: create_inspector(), + inspector: Some(create_inspector()), }) } } @@ -175,11 +176,14 @@ impl Debug for JSMapObserver { } } -impl Observer for JSMapObserver { +impl Observer for JSMapObserver +where + S: HasMetadata, +{ fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { self.reset_map()?; if !self.initialized { - let inspector = self.inspector.clone(); + let inspector = self.inspector.as_ref().unwrap().clone(); let params = self.params.clone(); unsafe { RUNTIME.as_ref() }.unwrap().block_on(async { let worker = unsafe { WORKER.as_mut() }.unwrap(); @@ -200,13 +204,8 @@ impl Observer for JSMapObserver { Ok(()) } - fn post_exec( - &mut self, - _state: &mut S, - _input: &I, - _exit_kind: &ExitKind, - ) -> Result<(), Error> { - let session = self.inspector.clone(); + fn post_exec(&mut self, state: &mut S, _input: &I, _exit_kind: &ExitKind) -> Result<(), Error> { + let session = self.inspector.as_ref().unwrap().clone(); let coverage = unsafe { RUNTIME.as_ref() }.unwrap().block_on(async { let worker = unsafe { WORKER.as_mut() }.unwrap(); let mut session = session.lock().await; @@ -220,8 +219,15 @@ impl Observer for JSMapObserver { Err(e) => return Err(Error::unknown(e.to_string())), } })?; - self.mapper - .process_coverage(coverage, &mut self.last_coverage); + let mapper = if let Some(mapper) = state.metadata_mut().get_mut::() { + mapper + } else { + state + .metadata_mut() + .insert::(JSCoverageMapper::new(self.params.call_count)); + state.metadata_mut().get_mut::().unwrap() + }; + mapper.process_coverage(coverage, &mut self.last_coverage); Ok(()) } diff --git a/libafl_v8/src/observers/types.rs b/libafl_v8/src/observers/types.rs index a8f54e7c82..50a65eec1d 100644 --- a/libafl_v8/src/observers/types.rs +++ b/libafl_v8/src/observers/types.rs @@ -13,6 +13,7 @@ use libafl::{ executors::ExitKind, observers::{MapObserver, Observer}, prelude::Named, + state::HasMetadata, Error, }; use serde::{Deserialize, Serialize}; @@ -30,11 +31,13 @@ struct JSTypeEntry { name_hash: u64, } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug)] struct JSTypeMapper { idx_map: HashMap, } +libafl::impl_serdeany!(JSTypeMapper); + impl JSTypeMapper { fn new() -> Self { Self { @@ -102,8 +105,8 @@ pub struct JSTypeObserver { last_coverage: Vec, name: String, mapper: JSTypeMapper, - #[serde(skip, default = "create_inspector")] - inspector: Arc>, + #[serde(skip, default)] + inspector: Option>>, } impl JSTypeObserver { @@ -116,7 +119,7 @@ impl JSTypeObserver { last_coverage: Vec::new(), name: name.to_string(), mapper: JSTypeMapper::new(), - inspector: create_inspector(), + inspector: Some(create_inspector()), }) } } @@ -137,11 +140,14 @@ impl Debug for JSTypeObserver { } } -impl Observer for JSTypeObserver { +impl Observer for JSTypeObserver +where + S: HasMetadata, +{ fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { self.reset_map()?; if !self.initialized { - let inspector = self.inspector.clone(); + let inspector = self.inspector.as_ref().unwrap().clone(); unsafe { RUNTIME.as_ref() }.unwrap().block_on(async { let worker = unsafe { WORKER.as_mut() }.unwrap(); let mut session = inspector.lock().await; @@ -161,13 +167,8 @@ impl Observer for JSTypeObserver { Ok(()) } - fn post_exec( - &mut self, - _state: &mut S, - _input: &I, - _exit_kind: &ExitKind, - ) -> Result<(), Error> { - let inspector = self.inspector.clone(); + fn post_exec(&mut self, state: &mut S, _input: &I, _exit_kind: &ExitKind) -> Result<(), Error> { + let inspector = self.inspector.as_ref().unwrap().clone(); let coverage = unsafe { RUNTIME.as_ref() }.unwrap().block_on(async { let worker = unsafe { WORKER.as_mut() }.unwrap(); let mut session = inspector.lock().await; @@ -181,8 +182,15 @@ impl Observer for JSTypeObserver { Err(e) => return Err(Error::unknown(e.to_string())), } })?; - self.mapper - .process_coverage(coverage, &mut self.last_coverage); + let mapper = if let Some(mapper) = state.metadata_mut().get_mut::() { + mapper + } else { + state + .metadata_mut() + .insert::(JSTypeMapper::new()); + state.metadata_mut().get_mut::().unwrap() + }; + mapper.process_coverage(coverage, &mut self.last_coverage); Ok(()) } From 6a98eb456ae478e6c8a10ac13faa254852004563 Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Tue, 30 Aug 2022 06:48:33 -0500 Subject: [PATCH 15/16] forcibly fail observers used in non-alwaysunique --- libafl_v8/src/observers/cov.rs | 37 ++++++++++++++++++++------------ libafl_v8/src/observers/types.rs | 37 ++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/libafl_v8/src/observers/cov.rs b/libafl_v8/src/observers/cov.rs index bdd2adf97a..574be5c79b 100644 --- a/libafl_v8/src/observers/cov.rs +++ b/libafl_v8/src/observers/cov.rs @@ -116,11 +116,15 @@ impl JSCoverageMapper { } /// Observer which inspects JavaScript coverage at either a block or function level +/// +/// This observer must not have its contents reused by other fuzzer instances; use +/// EventConfig::AlwaysUnique or *this will crash*. #[derive(Serialize, Deserialize)] pub struct JSMapObserver { initial: u8, initialized: bool, - last_coverage: Vec, + #[serde(skip, default)] + last_coverage: Option>, name: String, params: StartPreciseCoverageParameters, #[serde(skip, default)] @@ -151,7 +155,7 @@ impl JSMapObserver { Ok(Self { initial: u8::default(), initialized: false, - last_coverage: Vec::new(), + last_coverage: Some(Vec::new()), name: name.to_string(), params, inspector: Some(create_inspector()), @@ -227,7 +231,7 @@ where .insert::(JSCoverageMapper::new(self.params.call_count)); state.metadata_mut().get_mut::().unwrap() }; - mapper.process_coverage(coverage, &mut self.last_coverage); + mapper.process_coverage(coverage, self.last_coverage.as_mut().unwrap()); Ok(()) } @@ -247,7 +251,7 @@ where impl HasLen for JSMapObserver { fn len(&self) -> usize { - self.last_coverage.len() + self.last_coverage.as_ref().unwrap().len() } } @@ -256,13 +260,13 @@ impl<'it> AsIter<'it> for JSMapObserver { type IntoIter = Iter<'it, u8>; fn as_iter(&'it self) -> Self::IntoIter { - self.last_coverage.as_slice().iter() + self.last_coverage.as_ref().unwrap().as_slice().iter() } } impl AsMutSlice for JSMapObserver { fn as_mut_slice(&mut self) -> &mut [u8] { - self.last_coverage.as_mut_slice() + self.last_coverage.as_mut().unwrap().as_mut_slice() } } @@ -270,24 +274,29 @@ impl MapObserver for JSMapObserver { type Entry = u8; fn get(&self, idx: usize) -> &Self::Entry { - &self.last_coverage[idx] + &self.last_coverage.as_ref().unwrap()[idx] } fn get_mut(&mut self, idx: usize) -> &mut Self::Entry { - &mut self.last_coverage[idx] + &mut self.last_coverage.as_mut().unwrap()[idx] } fn usable_count(&self) -> usize { - self.last_coverage.len() + self.last_coverage.as_ref().unwrap().len() } fn count_bytes(&self) -> u64 { - self.last_coverage.iter().filter(|&&e| e != 0).count() as u64 + self.last_coverage + .as_ref() + .unwrap() + .iter() + .filter(|&&e| e != 0) + .count() as u64 } fn hash(&self) -> u64 { let mut hasher = AHasher::default(); - self.last_coverage.hash(&mut hasher); + self.last_coverage.as_ref().unwrap().hash(&mut hasher); hasher.finish() } @@ -302,7 +311,7 @@ impl MapObserver for JSMapObserver { fn reset_map(&mut self) -> Result<(), Error> { let initial = self.initial(); let cnt = self.usable_count(); - let map = self.last_coverage.as_mut_slice(); + let map = self.last_coverage.as_mut().unwrap().as_mut_slice(); for x in map[0..cnt].iter_mut() { *x = initial; } @@ -310,13 +319,13 @@ impl MapObserver for JSMapObserver { } fn to_vec(&self) -> Vec { - self.last_coverage.clone() + self.last_coverage.as_ref().unwrap().clone() } fn how_many_set(&self, indexes: &[usize]) -> usize { let initial = self.initial(); let cnt = self.usable_count(); - let map = self.last_coverage.as_slice(); + let map = self.last_coverage.as_ref().unwrap().as_slice(); let mut res = 0; for i in indexes { if *i < cnt && map[*i] != initial { diff --git a/libafl_v8/src/observers/types.rs b/libafl_v8/src/observers/types.rs index 50a65eec1d..421413ec80 100644 --- a/libafl_v8/src/observers/types.rs +++ b/libafl_v8/src/observers/types.rs @@ -98,11 +98,15 @@ impl JSTypeMapper { } /// Observer which inspects JavaScript type usage for parameters and return values +/// +/// This observer must not have its contents reused by other fuzzer instances; use +/// EventConfig::AlwaysUnique or *this will crash*. #[derive(Serialize, Deserialize)] pub struct JSTypeObserver { initial: u8, initialized: bool, - last_coverage: Vec, + #[serde(skip, default)] + last_coverage: Option>, name: String, mapper: JSTypeMapper, #[serde(skip, default)] @@ -116,7 +120,7 @@ impl JSTypeObserver { Ok(Self { initial: u8::default(), initialized: false, - last_coverage: Vec::new(), + last_coverage: Some(Vec::new()), name: name.to_string(), mapper: JSTypeMapper::new(), inspector: Some(create_inspector()), @@ -190,7 +194,7 @@ where .insert::(JSTypeMapper::new()); state.metadata_mut().get_mut::().unwrap() }; - mapper.process_coverage(coverage, &mut self.last_coverage); + mapper.process_coverage(coverage, self.last_coverage.as_mut().unwrap()); Ok(()) } @@ -210,7 +214,7 @@ where impl HasLen for JSTypeObserver { fn len(&self) -> usize { - self.last_coverage.len() + self.last_coverage.as_ref().unwrap().len() } } @@ -219,13 +223,13 @@ impl<'it> AsIter<'it> for JSTypeObserver { type IntoIter = Iter<'it, u8>; fn as_iter(&'it self) -> Self::IntoIter { - self.last_coverage.as_slice().iter() + self.last_coverage.as_ref().unwrap().as_slice().iter() } } impl AsMutSlice for JSTypeObserver { fn as_mut_slice(&mut self) -> &mut [u8] { - self.last_coverage.as_mut_slice() + self.last_coverage.as_mut().unwrap().as_mut_slice() } } @@ -233,24 +237,29 @@ impl MapObserver for JSTypeObserver { type Entry = u8; fn get(&self, idx: usize) -> &Self::Entry { - &self.last_coverage[idx] + &self.last_coverage.as_ref().unwrap()[idx] } fn get_mut(&mut self, idx: usize) -> &mut Self::Entry { - &mut self.last_coverage[idx] + &mut self.last_coverage.as_mut().unwrap()[idx] } fn usable_count(&self) -> usize { - self.last_coverage.len() + self.last_coverage.as_ref().unwrap().len() } fn count_bytes(&self) -> u64 { - self.last_coverage.iter().filter(|&&e| e != 0).count() as u64 + self.last_coverage + .as_ref() + .unwrap() + .iter() + .filter(|&&e| e != 0) + .count() as u64 } fn hash(&self) -> u64 { let mut hasher = AHasher::default(); - self.last_coverage.hash(&mut hasher); + self.last_coverage.as_ref().unwrap().hash(&mut hasher); hasher.finish() } @@ -265,7 +274,7 @@ impl MapObserver for JSTypeObserver { fn reset_map(&mut self) -> Result<(), Error> { let initial = self.initial(); let cnt = self.usable_count(); - let map = self.last_coverage.as_mut_slice(); + let map = self.last_coverage.as_mut().unwrap().as_mut_slice(); for x in map[0..cnt].iter_mut() { *x = initial; } @@ -273,13 +282,13 @@ impl MapObserver for JSTypeObserver { } fn to_vec(&self) -> Vec { - self.last_coverage.clone() + self.last_coverage.as_ref().unwrap().clone() } fn how_many_set(&self, indexes: &[usize]) -> usize { let initial = self.initial(); let cnt = self.usable_count(); - let map = self.last_coverage.as_slice(); + let map = self.last_coverage.as_ref().unwrap().as_slice(); let mut res = 0; for i in indexes { if *i < cnt && map[*i] != initial { From 8a03a35d999f13197bafd49cdbf9c95dbe537ea4 Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Tue, 30 Aug 2022 08:18:08 -0500 Subject: [PATCH 16/16] make RUNTIME and WORKER accessible --- libafl_v8/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libafl_v8/src/lib.rs b/libafl_v8/src/lib.rs index 632c91db88..31ec99c272 100644 --- a/libafl_v8/src/lib.rs +++ b/libafl_v8/src/lib.rs @@ -95,9 +95,9 @@ pub use values::*; use crate::v8::{HandleScope, Local, TryCatch}; /// Runtime for the libafl v8 crate. Must be accessed from the main fuzzer thread. -pub(crate) static mut RUNTIME: Option> = None; -/// Worker for the libafl v8 crate. Must be accessed from the v8 worker thread. -pub(crate) static mut WORKER: Option> = None; +pub static mut RUNTIME: Option> = None; +/// Worker for the libafl v8 crate. Must be accessed from the v8 worker thread, via `RUNTIME`. +pub static mut WORKER: Option> = None; /// Create an inspector for this fuzzer instance pub(crate) fn create_inspector() -> Arc> {