diff --git a/crates/fuzz/src/lib.rs b/crates/fuzz/src/lib.rs index acc6b0f936..97b62f958b 100644 --- a/crates/fuzz/src/lib.rs +++ b/crates/fuzz/src/lib.rs @@ -1,6 +1,7 @@ pub mod config; mod crash_inputs; mod error; +mod module; #[cfg(feature = "differential")] pub mod oracle; mod value; @@ -9,5 +10,6 @@ pub use self::{ config::{FuzzSmithConfig, FuzzWasmiConfig}, crash_inputs::generate_crash_inputs, error::{FuzzError, TrapCode}, + module::{FuzzModule, WasmSource, WatSource}, value::{FuzzVal, FuzzValType}, }; diff --git a/crates/fuzz/src/module.rs b/crates/fuzz/src/module.rs new file mode 100644 index 0000000000..1ef6cefa97 --- /dev/null +++ b/crates/fuzz/src/module.rs @@ -0,0 +1,87 @@ +use arbitrary::Unstructured; +use std::fmt::{self, Debug}; + +/// A Wasm module fuzz input. +pub struct FuzzModule { + module: wasm_smith::Module, +} + +impl FuzzModule { + /// Creates a new [`FuzzModule`] from the given `config` and fuzz input bytes, `u`. + pub fn new( + config: impl Into, + u: &mut Unstructured, + ) -> arbitrary::Result { + let config = config.into(); + let module = wasm_smith::Module::new(config, u)?; + Ok(Self { module }) + } + + /// Ensure that all of this Wasm module’s functions will terminate when executed. + /// + /// Read more about this API [here](wasm_smith::Module::ensure_termination). + pub fn ensure_termination(&mut self, default_fuel: u32) { + if let Err(err) = self.module.ensure_termination(default_fuel) { + panic!("unexpected invalid Wasm module: {err}") + } + } + + /// Returns the machine readble [`WasmSource`] code. + pub fn wasm(&self) -> WasmSource { + WasmSource { + bytes: self.module.to_bytes(), + } + } +} + +impl Debug for FuzzModule { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let config = self.module.config(); + let wat = self.wasm().to_wat(); + f.debug_struct("FuzzModule") + .field("config", config) + .field("wat", &wat) + .finish() + } +} + +/// A `.wasm` source code. +pub struct WasmSource { + bytes: Vec, +} + +impl WasmSource { + /// Consumes `self` and returns the underlying bytes of the [`WasmSource`]. + pub fn into_bytes(self) -> Box<[u8]> { + self.bytes.into() + } + + /// Returns the underlying bytes of the [`WasmSource`]. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes[..] + } + + /// Converts the [`WasmSource`] to human readable `.wat` formatted source. + /// + /// The returned [`WatSource`] is convenience for debugging. + pub fn to_wat(&self) -> WatSource { + let wat = match wasmprinter::print_bytes(&self.bytes[..]) { + Ok(wat) => wat, + Err(err) => panic!("invalid Wasm: {err}"), + }; + WatSource { text: wat } + } +} + +/// A `.wat` source code. +/// +/// Convenience type for debug printing `.wat` formatted Wasm source code. +pub struct WatSource { + text: String, +} + +impl fmt::Debug for WatSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\n{}", self.text) + } +} diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs index 042f20184f..5364bacc64 100644 --- a/fuzz/fuzz_targets/differential.rs +++ b/fuzz/fuzz_targets/differential.rs @@ -1,22 +1,22 @@ #![no_main] -use std::fmt; - use arbitrary::{Arbitrary, Unstructured}; use libfuzzer_sys::fuzz_target; use wasmi::Val; use wasmi_fuzz::{ config::FuzzSmithConfig, oracle::{ChosenOracle, DifferentialOracle, DifferentialOracleMeta, WasmiOracle}, + FuzzModule, FuzzVal, }; /// Fuzzing input for differential fuzzing. +#[derive(Debug)] pub struct FuzzInput { /// The chosen Wasm runtime oracle to compare against Wasmi. chosen_oracle: ChosenOracle, /// The fuzzed Wasm module and its configuration. - smith_module: wasm_smith::Module, + module: FuzzModule, } impl<'a> Arbitrary<'a> for FuzzInput { @@ -28,43 +28,19 @@ impl<'a> Arbitrary<'a> for FuzzInput { WasmiOracle::configure(&mut fuzz_config); chosen_oracle.configure(&mut fuzz_config); let smith_config: wasm_smith::Config = fuzz_config.into(); - let mut smith_module = wasm_smith::Module::new(smith_config, u)?; - smith_module.ensure_termination(1_000 /* fuel */) - .expect("`ensure_termination` can only fail for modules that have not been created by `wasm_smith`"); + let mut smith_module = FuzzModule::new(smith_config, u)?; + smith_module.ensure_termination(1_000 /* fuel */); Ok(Self { chosen_oracle, - smith_module, + module: smith_module, }) } } -impl fmt::Debug for FuzzInput { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - /// Prints the internal `String` as `Display` on `Debug` output. - pub struct Text(String); - impl fmt::Debug for Text { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "\n{}", self.0.as_str()) - } - } - - let wasm = self.smith_module.to_bytes(); - let wat = match wasmprinter::print_bytes(wasm) { - Ok(wat) => wat, - Err(err) => format!("invalid Wasm: {err}"), - }; - f.debug_struct("FuzzInput") - .field("chosen_oracle", &self.chosen_oracle) - .field("module", &self.smith_module) - .field("wasm", &Text(wat)) - .finish() - } -} - impl FuzzInput { /// Returns the fuzzed Wasm input bytes. pub fn wasm(&self) -> Box<[u8]> { - self.smith_module.to_bytes().into() + self.module.wasm().into_bytes() } } diff --git a/fuzz/fuzz_targets/execute.rs b/fuzz/fuzz_targets/execute.rs index 1c7a2fd03c..a382aa32dc 100644 --- a/fuzz/fuzz_targets/execute.rs +++ b/fuzz/fuzz_targets/execute.rs @@ -13,32 +13,61 @@ use wasmi::{ StoreLimitsBuilder, Val, }; -use wasmi_fuzz::{config::ValidationMode, FuzzVal, FuzzValType, FuzzWasmiConfig}; +use wasmi_fuzz::{ + config::ValidationMode, + FuzzModule, + FuzzSmithConfig, + FuzzVal, + FuzzValType, + FuzzWasmiConfig, +}; -fuzz_target!(|seed: &[u8]| { - let mut u = Unstructured::new(seed); - let Ok(wasmi_config) = FuzzWasmiConfig::arbitrary(&mut u) else { - return; - }; - let Ok(mut fuzz_config) = wasmi_fuzz::FuzzSmithConfig::arbitrary(&mut u) else { - return; - }; - fuzz_config.export_everything(); - let Ok(smith_module) = wasm_smith::Module::new(fuzz_config.into(), &mut u) else { - return; - }; - let wasm_bytes = smith_module.to_bytes(); - let wasm = wasm_bytes.as_slice(); +#[derive(Debug)] +pub struct FuzzInput<'a> { + config: FuzzWasmiConfig, + module: FuzzModule, + u: Unstructured<'a>, +} + +impl<'a> Arbitrary<'a> for FuzzInput<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let config = FuzzWasmiConfig::arbitrary(u)?; + let mut fuzz_config = FuzzSmithConfig::arbitrary(u)?; + fuzz_config.export_everything(); + let module = FuzzModule::new(fuzz_config, u)?; + Ok(Self { + config, + module, + u: Unstructured::new(&[]), + }) + } + + fn arbitrary_take_rest(mut u: Unstructured<'a>) -> arbitrary::Result { + Self::arbitrary(&mut u).map(|mut input| { + input.u = u; + input + }) + } +} + +fuzz_target!(|input: FuzzInput| { + let FuzzInput { + config, + module, + mut u, + } = input; + let wasm_bytes = module.wasm().into_bytes(); + let wasm = &wasm_bytes[..]; - let config = { - let mut config = Config::from(wasmi_config); + let engine_config = { + let mut config = Config::from(config); // We use Wasmi's built-in fuel metering since it is way faster // than `wasm_smith`'s fuel metering and thus allows the fuzzer // to expand its test coverage faster. config.consume_fuel(true); config }; - let engine = Engine::new(&config); + let engine = Engine::new(&engine_config); let linker = Linker::new(&engine); let limiter = StoreLimitsBuilder::new() .memory_size(1000 * 0x10000) @@ -48,7 +77,7 @@ fuzz_target!(|seed: &[u8]| { let Ok(_) = store.set_fuel(1000) else { return; }; - if matches!(wasmi_config.validation_mode, ValidationMode::Unchecked) { + if matches!(config.validation_mode, ValidationMode::Unchecked) { // We validate the Wasm module before handing it over to Wasmi // despite `wasm_smith` stating to only produce valid Wasm. // Translating an invalid Wasm module is undefined behavior. @@ -56,7 +85,7 @@ fuzz_target!(|seed: &[u8]| { return; } } - let status = match wasmi_config.validation_mode { + let status = match config.validation_mode { ValidationMode::Checked => Module::new(&engine, wasm), ValidationMode::Unchecked => { // Safety: we have just checked Wasm validity above. diff --git a/fuzz/fuzz_targets/translate.rs b/fuzz/fuzz_targets/translate.rs index 341e107d9c..ff97e72120 100644 --- a/fuzz/fuzz_targets/translate.rs +++ b/fuzz/fuzz_targets/translate.rs @@ -5,25 +5,32 @@ use libfuzzer_sys::fuzz_target; use wasmi::{Config, Engine, Module}; use wasmi_fuzz::{ config::{ParsingMode, ValidationMode}, + FuzzModule, FuzzWasmiConfig, }; -fuzz_target!(|seed: &[u8]| { - let mut u = Unstructured::new(seed); - let Ok(wasmi_config) = FuzzWasmiConfig::arbitrary(&mut u) else { - return; - }; - let Ok(fuzz_config) = wasmi_fuzz::FuzzSmithConfig::arbitrary(&mut u) else { - return; - }; - let Ok(smith_module) = wasm_smith::Module::new(fuzz_config.into(), &mut u) else { - return; - }; - let wasm_bytes = smith_module.to_bytes(); - let wasm = wasm_bytes.as_slice(); - let config = Config::from(wasmi_config); - let engine = Engine::new(&config); - if matches!(wasmi_config.validation_mode, ValidationMode::Unchecked) { +#[derive(Debug)] +pub struct FuzzInput { + config: FuzzWasmiConfig, + module: FuzzModule, +} + +impl<'a> Arbitrary<'a> for FuzzInput { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let config = FuzzWasmiConfig::arbitrary(u)?; + let fuzz_config = wasmi_fuzz::FuzzSmithConfig::arbitrary(u)?; + let module = wasmi_fuzz::FuzzModule::new(fuzz_config, u)?; + Ok(Self { config, module }) + } +} + +fuzz_target!(|input: FuzzInput| { + let FuzzInput { config, module } = input; + let wasm_source = module.wasm(); + let wasm = wasm_source.as_bytes(); + let engine_config = Config::from(config); + let engine = Engine::new(&engine_config); + if matches!(config.validation_mode, ValidationMode::Unchecked) { // We validate the Wasm module before handing it over to Wasmi // despite `wasm_smith` stating to only produce valid Wasm. // Translating an invalid Wasm module is undefined behavior. @@ -31,7 +38,7 @@ fuzz_target!(|seed: &[u8]| { return; } } - let status = match (wasmi_config.parsing_mode, wasmi_config.validation_mode) { + let status = match (config.parsing_mode, config.validation_mode) { (ParsingMode::Streaming, ValidationMode::Checked) => Module::new_streaming(&engine, wasm), (ParsingMode::Buffered, ValidationMode::Checked) => Module::new(&engine, wasm), (ParsingMode::Streaming, ValidationMode::Unchecked) => {