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 + } +}