diff --git a/Cargo.lock b/Cargo.lock index 9d6bc34c70..aa4250660e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1958,7 +1958,9 @@ version = "0.38.0" dependencies = [ "arbitrary", "wasm-smith", + "wasmi 0.31.2", "wasmi 0.38.0", + "wasmtime", ] [[package]] diff --git a/crates/fuzz/Cargo.toml b/crates/fuzz/Cargo.toml index 0670a8a6ef..9c48ebb7b7 100644 --- a/crates/fuzz/Cargo.toml +++ b/crates/fuzz/Cargo.toml @@ -16,5 +16,11 @@ publish = false [dependencies] wasmi = { workspace = true, features = ["std"] } +wasmi-stack = { package = "wasmi", feature = ["std"], version = "0.31.2", optional = true } +wasmtime = { version = "26.0.0", feature = ["std"], optional = true } wasm-smith = "0.219.1" arbitrary = "1.3.2" + +[features] +default = ["differential"] +differential = ["dep:wasmi-stack", "dep:wasmtime"] diff --git a/crates/fuzz/src/error.rs b/crates/fuzz/src/error.rs new file mode 100644 index 0000000000..5a8f1c6f60 --- /dev/null +++ b/crates/fuzz/src/error.rs @@ -0,0 +1,25 @@ +#[derive(Debug, PartialEq, Eq)] +pub enum FuzzError { + Trap(TrapCode), + Other, +} + +impl FuzzError { + /// Returns `true` if `self` may be of non-deterministic origin. + pub fn is_non_deterministic(&self) -> bool { + matches!(self, Self::Trap(TrapCode::StackOverflow) | Self::Other) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TrapCode { + UnreachableCodeReached, + MemoryOutOfBounds, + TableOutOfBounds, + IndirectCallToNull, + IntegerDivisionByZero, + IntegerOverflow, + BadConversionToInteger, + StackOverflow, + BadSignature, +} diff --git a/crates/fuzz/src/lib.rs b/crates/fuzz/src/lib.rs index 56a1606c30..9178a00818 100644 --- a/crates/fuzz/src/lib.rs +++ b/crates/fuzz/src/lib.rs @@ -1,8 +1,11 @@ pub mod config; -mod oracle; +mod error; +#[cfg(feature = "differential")] +pub mod oracle; mod value; pub use self::{ config::{FuzzSmithConfig, FuzzWasmiConfig}, - value::{FuzzRefTy, FuzzVal, FuzzValType}, + error::{FuzzError, TrapCode}, + value::{FuzzVal, FuzzValType}, }; diff --git a/crates/fuzz/src/oracle/exports.rs b/crates/fuzz/src/oracle/exports.rs new file mode 100644 index 0000000000..0a3cbd2222 --- /dev/null +++ b/crates/fuzz/src/oracle/exports.rs @@ -0,0 +1,134 @@ +use core::slice; +use wasmi::FuncType; + +/// Names of exported Wasm objects from a fuzzed Wasm module. +#[derive(Debug, Default)] +pub struct ModuleExports { + /// Names of exported functions. + funcs: StringSequence, + /// The types of exported functions. + func_types: Vec, + /// Names of exported global variables. + globals: StringSequence, + /// Names of exported linear memories. + memories: StringSequence, + /// Names of exported tables. + tables: StringSequence, +} + +impl ModuleExports { + /// Pushes an exported function `name` to `self`. + pub(crate) fn push_func(&mut self, name: &str, ty: FuncType) { + self.funcs.push(name); + self.func_types.push(ty); + } + + /// Pushes an exported global `name` to `self`. + pub(crate) fn push_global(&mut self, name: &str) { + self.globals.push(name); + } + + /// Pushes an exported memory `name` to `self`. + pub(crate) fn push_memory(&mut self, name: &str) { + self.memories.push(name); + } + + /// Pushes an exported table `name` to `self`. + pub(crate) fn push_table(&mut self, name: &str) { + self.tables.push(name); + } + + /// Returns an iterator yielding the names of the exported Wasm functions. + pub fn funcs(&self) -> ExportedFuncsIter { + ExportedFuncsIter { + names: self.funcs.iter(), + types: self.func_types.iter(), + } + } + + /// Returns an iterator yielding the names of the exported Wasm globals. + pub fn globals(&self) -> StringSequenceIter { + self.globals.iter() + } + + /// Returns an iterator yielding the names of the exported Wasm memories. + pub fn memories(&self) -> StringSequenceIter { + self.memories.iter() + } + + /// Returns an iterator yielding the names of the exported Wasm tables. + pub fn tables(&self) -> StringSequenceIter { + self.tables.iter() + } +} + +/// Iterator yieling the exported functions of a fuzzed Wasm module. +#[derive(Debug)] +pub struct ExportedFuncsIter<'a> { + /// The names of the exported Wasm functions. + names: StringSequenceIter<'a>, + /// The types of the exported Wasm functions. + types: slice::Iter<'a, FuncType>, +} + +impl<'a> Iterator for ExportedFuncsIter<'a> { + type Item = (&'a str, &'a FuncType); + + #[inline] + fn next(&mut self) -> Option { + let name = self.names.next()?; + let ty = self.types.next()?; + Some((name, ty)) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.names.size_hint() + } +} + +/// An append-only sequence of strings. +#[derive(Debug, Default)] +pub struct StringSequence { + /// The underlying sequence of strings. + strings: Vec>, +} + +impl StringSequence { + /// Pushes another string `s` to `self`. + pub fn push(&mut self, s: &str) { + self.strings.push(Box::from(s)); + } + + /// Returns an iterator over the strings in `self`. + /// + /// The iterator yields the strings in order of their insertion. + pub fn iter(&self) -> StringSequenceIter { + StringSequenceIter { + iter: self.strings.iter(), + } + } +} + +/// An iterator yielding the strings of a sequence of strings. +#[derive(Debug)] +pub struct StringSequenceIter<'a> { + /// The underlying iterator over strings. + iter: slice::Iter<'a, Box>, +} + +impl<'a> Iterator for StringSequenceIter<'a> { + type Item = &'a str; + + #[inline] + fn next(&mut self) -> Option { + self.iter.next().map(|s| &**s) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl ExactSizeIterator for StringSequenceIter<'_> {} diff --git a/crates/fuzz/src/oracle/mod.rs b/crates/fuzz/src/oracle/mod.rs index d875ebd9a6..9f7c537de8 100644 --- a/crates/fuzz/src/oracle/mod.rs +++ b/crates/fuzz/src/oracle/mod.rs @@ -1 +1,77 @@ +pub use self::{ + exports::{ModuleExports, StringSequenceIter}, + wasmi::WasmiOracle, + wasmi_stack::WasmiStackOracle, + wasmtime::WasmtimeOracle, +}; +use crate::{FuzzError, FuzzSmithConfig, FuzzVal}; +use arbitrary::{Arbitrary, Unstructured}; + +mod exports; mod wasmi; +mod wasmi_stack; +mod wasmtime; + +/// Trait implemented by differential fuzzing oracles. +pub trait DifferentialOracle { + /// Returns the name of the differential fuzzing oracle. + fn name(&self) -> &'static str; + + /// Calls the exported function with `name` and `params` and returns the result. + fn call(&mut self, name: &str, params: &[FuzzVal]) -> Result, FuzzError>; + + /// Returns the value of the global named `name` if any. + fn get_global(&mut self, name: &str) -> Option; + + /// Returns the bytes of the memory named `name` if any. + fn get_memory(&mut self, name: &str) -> Option<&[u8]>; +} + +/// Trait implemented by differential fuzzing oracles. +pub trait DifferentialOracleMeta: Sized { + /// Tells `config` about the minimum viable configuration possible for this oracle. + fn configure(config: &mut FuzzSmithConfig); + + /// Sets up the Wasm fuzzing oracle for the given `wasm` binary if possible. + fn setup(wasm: &[u8]) -> Option; +} + +/// A chosen differnential fuzzing oracle. +#[derive(Debug, Default, Copy, Clone)] +pub enum ChosenOracle { + /// The legacy Wasmi v0.31 oracle. + #[default] + WasmiStack, + /// The Wasmtime oracle. + Wasmtime, +} + +impl Arbitrary<'_> for ChosenOracle { + fn arbitrary(u: &mut Unstructured) -> arbitrary::Result { + let index = u8::arbitrary(u).unwrap_or_default(); + let chosen = match index { + 0 => Self::Wasmtime, + _ => Self::WasmiStack, + }; + Ok(chosen) + } +} + +impl ChosenOracle { + /// Configures `fuzz_config` for the chosen differential fuzzing oracle. + pub fn configure(&self, fuzz_config: &mut FuzzSmithConfig) { + match self { + ChosenOracle::WasmiStack => WasmiStackOracle::configure(fuzz_config), + ChosenOracle::Wasmtime => WasmtimeOracle::configure(fuzz_config), + } + } + + /// Sets up the chosen differential fuzzing oracle. + pub fn setup(&self, wasm: &[u8]) -> Option> { + let oracle: Box = match self { + ChosenOracle::WasmiStack => Box::new(WasmiStackOracle::setup(wasm)?), + ChosenOracle::Wasmtime => Box::new(WasmtimeOracle::setup(wasm)?), + }; + Some(oracle) + } +} diff --git a/crates/fuzz/src/oracle/wasmi.rs b/crates/fuzz/src/oracle/wasmi.rs new file mode 100644 index 0000000000..42e9dcf7d7 --- /dev/null +++ b/crates/fuzz/src/oracle/wasmi.rs @@ -0,0 +1,187 @@ +use crate::{ + oracle::{DifferentialOracle, DifferentialOracleMeta}, + FuzzError, + FuzzSmithConfig, + FuzzVal, + FuzzValType, +}; +use wasmi::{ + core::ValType, + Config, + Engine, + Instance, + Linker, + Module, + StackLimits, + Store, + StoreLimits, + StoreLimitsBuilder, + Val, +}; + +use super::ModuleExports; + +/// Differential fuzzing backend for the register-machine Wasmi. +#[derive(Debug)] +pub struct WasmiOracle { + store: Store, + instance: Instance, + params: Vec, + results: Vec, +} + +impl WasmiOracle { + /// Returns the Wasm module export names. + pub fn exports(&self) -> ModuleExports { + let mut exports = ModuleExports::default(); + for export in self.instance.exports(&self.store) { + let name = export.name(); + match export.ty(&self.store) { + wasmi::ExternType::Func(ty) => exports.push_func(name, ty), + wasmi::ExternType::Global(_) => exports.push_global(name), + wasmi::ExternType::Memory(_) => exports.push_memory(name), + wasmi::ExternType::Table(_) => exports.push_table(name), + }; + } + exports + } +} + +impl DifferentialOracleMeta for WasmiOracle { + fn configure(_config: &mut FuzzSmithConfig) {} + + fn setup(wasm: &[u8]) -> Option + where + Self: Sized, + { + let mut config = Config::default(); + // We set custom limits since Wasmi (register) might use more + // stack space than Wasmi (stack) for some malicious recursive workloads. + // Wasmtime technically suffers from the same problems (register machine) + // but can offset them due to its superior optimizations. + // + // We increase the maximum stack space for Wasmi (register) to avoid + // common stack overflows in certain generated fuzz test cases this way. + config.set_stack_limits( + StackLimits::new( + 1024, // 1 kiB + 1024 * 1024 * 10, // 10 MiB + 1024, + ) + .unwrap(), + ); + let engine = Engine::new(&config); + let linker = Linker::new(&engine); + let limiter = StoreLimitsBuilder::new() + .memory_size(1000 * 0x10000) + .build(); + let mut store = Store::new(&engine, limiter); + store.limiter(|lim| lim); + let module = Module::new(store.engine(), wasm).unwrap(); + let Ok(unstarted_instance) = linker.instantiate(&mut store, &module) else { + return None; + }; + let Ok(instance) = unstarted_instance.ensure_no_start(&mut store) else { + return None; + }; + Some(Self { + store, + instance, + params: Vec::new(), + results: Vec::new(), + }) + } +} + +impl DifferentialOracle for WasmiOracle { + fn name(&self) -> &'static str { + "Wasmi" + } + + fn call(&mut self, name: &str, params: &[FuzzVal]) -> Result, FuzzError> { + let Some(func) = self.instance.get_func(&self.store, name) else { + panic!( + "{}: could not find exported function: \"{name}\"", + self.name(), + ) + }; + let ty = func.ty(&self.store); + self.params.clear(); + self.results.clear(); + self.params.extend(params.iter().cloned().map(Val::from)); + self.results + .extend(ty.results().iter().copied().map(Val::default)); + func.call(&mut self.store, &self.params[..], &mut self.results[..]) + .map_err(FuzzError::from)?; + let results = self.results.iter().cloned().map(FuzzVal::from).collect(); + Ok(results) + } + + fn get_global(&mut self, name: &str) -> Option { + let value = self + .instance + .get_global(&self.store, name)? + .get(&self.store); + Some(FuzzVal::from(value)) + } + + fn get_memory(&mut self, name: &str) -> Option<&[u8]> { + let data = self + .instance + .get_memory(&self.store, name)? + .data(&self.store); + Some(data) + } +} + +impl From for ValType { + fn from(ty: FuzzValType) -> Self { + match ty { + FuzzValType::I32 => Self::I32, + FuzzValType::I64 => Self::I64, + FuzzValType::F32 => Self::F32, + FuzzValType::F64 => Self::F64, + FuzzValType::FuncRef => Self::FuncRef, + FuzzValType::ExternRef => Self::ExternRef, + } + } +} + +impl From for FuzzVal { + fn from(value: Val) -> Self { + match value { + Val::I32(value) => Self::I32(value), + Val::I64(value) => Self::I64(value), + Val::F32(value) => Self::F32(value.into()), + Val::F64(value) => Self::F64(value.into()), + Val::FuncRef(value) => Self::FuncRef { + is_null: value.is_null(), + }, + Val::ExternRef(value) => Self::ExternRef { + is_null: value.is_null(), + }, + } + } +} + +impl From for FuzzError { + fn from(error: wasmi::Error) -> Self { + use wasmi::core::TrapCode; + let Some(trap_code) = error.as_trap_code() else { + return FuzzError::Other; + }; + let trap_code = match trap_code { + TrapCode::UnreachableCodeReached => crate::TrapCode::UnreachableCodeReached, + TrapCode::MemoryOutOfBounds => crate::TrapCode::MemoryOutOfBounds, + TrapCode::TableOutOfBounds => crate::TrapCode::TableOutOfBounds, + TrapCode::IndirectCallToNull => crate::TrapCode::IndirectCallToNull, + TrapCode::IntegerDivisionByZero => crate::TrapCode::IntegerDivisionByZero, + TrapCode::IntegerOverflow => crate::TrapCode::IntegerOverflow, + TrapCode::BadConversionToInteger => crate::TrapCode::BadConversionToInteger, + TrapCode::StackOverflow => crate::TrapCode::StackOverflow, + TrapCode::BadSignature => crate::TrapCode::BadSignature, + TrapCode::OutOfFuel | TrapCode::GrowthOperationLimited => return FuzzError::Other, + }; + FuzzError::Trap(trap_code) + } +} diff --git a/crates/fuzz/src/oracle/wasmi/mod.rs b/crates/fuzz/src/oracle/wasmi/mod.rs deleted file mode 100644 index 730b0463ad..0000000000 --- a/crates/fuzz/src/oracle/wasmi/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{FuzzRefTy, FuzzVal, FuzzValType}; -use wasmi::{core::ValType, ExternRef, FuncRef, Val}; - -impl From for ValType { - fn from(ty: FuzzValType) -> Self { - match ty { - FuzzValType::I32 => Self::I32, - FuzzValType::I64 => Self::I64, - FuzzValType::F32 => Self::F32, - FuzzValType::F64 => Self::F64, - FuzzValType::FuncRef => Self::FuncRef, - FuzzValType::ExternRef => Self::ExternRef, - } - } -} - -impl From for Val { - fn from(value: FuzzVal) -> Self { - match value { - FuzzVal::I32(value) => Self::I32(value), - FuzzVal::I64(value) => Self::I64(value), - FuzzVal::F32(value) => Self::F32(value.into()), - FuzzVal::F64(value) => Self::F64(value.into()), - FuzzVal::Null(ref_ty) => match ref_ty { - FuzzRefTy::Func => Self::FuncRef(FuncRef::null()), - FuzzRefTy::Extern => Self::ExternRef(ExternRef::null()), - }, - } - } -} diff --git a/crates/fuzz/src/oracle/wasmi_stack.rs b/crates/fuzz/src/oracle/wasmi_stack.rs new file mode 100644 index 0000000000..7e4c7469b1 --- /dev/null +++ b/crates/fuzz/src/oracle/wasmi_stack.rs @@ -0,0 +1,165 @@ +use crate::{ + oracle::{DifferentialOracle, DifferentialOracleMeta}, + FuzzError, + FuzzSmithConfig, + FuzzVal, +}; +use wasmi_stack::{ + Config, + Engine, + Error, + ExternRef, + FuncRef, + Instance, + Linker, + Module, + Store, + StoreLimits, + StoreLimitsBuilder, + Value, +}; + +/// Differential fuzzing backend for the stack-machine Wasmi. +#[derive(Debug)] +pub struct WasmiStackOracle { + store: Store, + instance: Instance, + params: Vec, + results: Vec, +} + +impl DifferentialOracleMeta for WasmiStackOracle { + fn configure(config: &mut FuzzSmithConfig) { + config.disable_multi_memory(); + } + + fn setup(wasm: &[u8]) -> Option + where + Self: Sized, + { + let mut config = Config::default(); + config.wasm_tail_call(true); + config.wasm_extended_const(true); + let engine = Engine::new(&config); + let linker = Linker::new(&engine); + let limiter = StoreLimitsBuilder::new() + .memory_size(1000 * 0x10000) + .build(); + let mut store = Store::new(&engine, limiter); + store.limiter(|lim| lim); + let module = Module::new(store.engine(), wasm).unwrap(); + let Ok(unstarted_instance) = linker.instantiate(&mut store, &module) else { + return None; + }; + let Ok(instance) = unstarted_instance.ensure_no_start(&mut store) else { + return None; + }; + Some(Self { + store, + instance, + params: Vec::new(), + results: Vec::new(), + }) + } +} + +impl DifferentialOracle for WasmiStackOracle { + fn name(&self) -> &'static str { + "Wasmi v0.31" + } + + fn call(&mut self, name: &str, params: &[FuzzVal]) -> Result, FuzzError> { + let Some(func) = self.instance.get_func(&self.store, name) else { + panic!( + "{}: could not find exported function: \"{name}\"", + self.name() + ) + }; + let ty = func.ty(&self.store); + self.params.clear(); + self.results.clear(); + self.params.extend(params.iter().cloned().map(Value::from)); + self.results + .extend(ty.results().iter().copied().map(Value::default)); + func.call(&mut self.store, &self.params[..], &mut self.results[..])?; + let results = self.results.iter().cloned().map(FuzzVal::from).collect(); + Ok(results) + } + + fn get_global(&mut self, name: &str) -> Option { + let value = self + .instance + .get_global(&self.store, name)? + .get(&self.store); + Some(FuzzVal::from(value)) + } + + fn get_memory(&mut self, name: &str) -> Option<&[u8]> { + let data = self + .instance + .get_memory(&self.store, name)? + .data(&self.store); + Some(data) + } +} + +impl From for FuzzVal { + fn from(value: Value) -> Self { + match value { + Value::I32(value) => Self::I32(value), + Value::I64(value) => Self::I64(value), + Value::F32(value) => Self::F32(value.into()), + Value::F64(value) => Self::F64(value.into()), + Value::FuncRef(value) => Self::FuncRef { + is_null: value.is_null(), + }, + Value::ExternRef(value) => Self::ExternRef { + is_null: value.is_null(), + }, + } + } +} + +impl From for Value { + fn from(value: FuzzVal) -> Self { + match value { + FuzzVal::I32(value) => Self::I32(value), + FuzzVal::I64(value) => Self::I64(value), + FuzzVal::F32(value) => Self::F32(value.into()), + FuzzVal::F64(value) => Self::F64(value.into()), + FuzzVal::FuncRef { is_null } => { + assert!(is_null); + Self::FuncRef(FuncRef::null()) + } + FuzzVal::ExternRef { is_null } => { + assert!(is_null); + Self::ExternRef(ExternRef::null()) + } + } + } +} + +impl From for FuzzError { + fn from(error: Error) -> Self { + use wasmi_stack::core::TrapCode; + let Error::Trap(trap) = error else { + return FuzzError::Other; + }; + let Some(trap_code) = trap.trap_code() else { + return FuzzError::Other; + }; + let trap_code = match trap_code { + TrapCode::UnreachableCodeReached => crate::TrapCode::UnreachableCodeReached, + TrapCode::MemoryOutOfBounds => crate::TrapCode::MemoryOutOfBounds, + TrapCode::TableOutOfBounds => crate::TrapCode::TableOutOfBounds, + TrapCode::IndirectCallToNull => crate::TrapCode::IndirectCallToNull, + TrapCode::IntegerDivisionByZero => crate::TrapCode::IntegerDivisionByZero, + TrapCode::IntegerOverflow => crate::TrapCode::IntegerOverflow, + TrapCode::BadConversionToInteger => crate::TrapCode::BadConversionToInteger, + TrapCode::StackOverflow => crate::TrapCode::StackOverflow, + TrapCode::BadSignature => crate::TrapCode::BadSignature, + TrapCode::OutOfFuel | TrapCode::GrowthOperationLimited => return FuzzError::Other, + }; + FuzzError::Trap(trap_code) + } +} diff --git a/crates/fuzz/src/oracle/wasmtime.rs b/crates/fuzz/src/oracle/wasmtime.rs new file mode 100644 index 0000000000..ed16450572 --- /dev/null +++ b/crates/fuzz/src/oracle/wasmtime.rs @@ -0,0 +1,146 @@ +use crate::{ + oracle::{DifferentialOracle, DifferentialOracleMeta}, + FuzzError, + FuzzVal, +}; +use wasmtime::{Config, Engine, Instance, Linker, Module, Store, StoreLimitsBuilder, Val}; + +/// Differential fuzzing backend for Wasmtime. +pub struct WasmtimeOracle { + store: Store, + instance: Instance, + params: Vec, + results: Vec, +} + +impl DifferentialOracleMeta for WasmtimeOracle { + fn configure(_config: &mut crate::FuzzSmithConfig) {} + + fn setup(wasm: &[u8]) -> Option + where + Self: Sized, + { + let mut config = Config::default(); + // We disabled backtraces since they sometimes become so large + // that the entire output is obliterated by them. Generally we are + // more interested what kind of error occurred and now how an error + // occurred. + config.wasm_backtrace(false); + let engine = Engine::default(); + let linker = Linker::new(&engine); + let limiter = StoreLimitsBuilder::new() + .memory_size(1000 * 0x10000) + .build(); + let mut store = Store::new(&engine, limiter); + store.limiter(|lim| lim); + let module = Module::new(store.engine(), wasm).unwrap(); + let Ok(instance) = linker.instantiate(&mut store, &module) else { + return None; + }; + Some(Self { + store, + instance, + params: Vec::new(), + results: Vec::new(), + }) + } +} + +impl DifferentialOracle for WasmtimeOracle { + fn name(&self) -> &'static str { + "Wasmtime" + } + + fn call(&mut self, name: &str, params: &[FuzzVal]) -> Result, FuzzError> { + let Some(func) = self.instance.get_func(&mut self.store, name) else { + panic!( + "{}: could not find exported function: \"{name}\"", + self.name(), + ) + }; + let ty = func.ty(&self.store); + self.params.clear(); + self.results.clear(); + self.params.extend(params.iter().cloned().map(Val::from)); + self.results + .extend(ty.results().map(|ty| Val::default_for_ty(&ty).unwrap())); + func.call(&mut self.store, &self.params[..], &mut self.results[..])?; + let results = self.results.iter().cloned().map(FuzzVal::from).collect(); + Ok(results) + } + + fn get_global(&mut self, name: &str) -> Option { + let value = self + .instance + .get_global(&mut self.store, name)? + .get(&mut self.store); + Some(FuzzVal::from(value)) + } + + fn get_memory(&mut self, name: &str) -> Option<&[u8]> { + let data = self + .instance + .get_memory(&mut self.store, name)? + .data(&mut self.store); + Some(data) + } +} + +impl From for FuzzVal { + fn from(value: Val) -> Self { + match value { + Val::I32(value) => Self::I32(value), + Val::I64(value) => Self::I64(value), + Val::F32(value) => Self::F32(f32::from_bits(value)), + Val::F64(value) => Self::F64(f64::from_bits(value)), + Val::FuncRef(value) => Self::FuncRef { + is_null: value.is_none(), + }, + Val::ExternRef(value) => Self::ExternRef { + is_null: value.is_none(), + }, + val => panic!("Wasmtime: unsupported `Val`: {val:?}"), + } + } +} + +impl From for Val { + fn from(value: FuzzVal) -> Self { + match value { + FuzzVal::I32(value) => Self::I32(value), + FuzzVal::I64(value) => Self::I64(value), + FuzzVal::F32(value) => Self::F32(value.to_bits()), + FuzzVal::F64(value) => Self::F64(value.to_bits()), + FuzzVal::FuncRef { is_null } => { + assert!(is_null); + Self::FuncRef(None) + } + FuzzVal::ExternRef { is_null } => { + assert!(is_null); + Self::ExternRef(None) + } + } + } +} + +impl From for FuzzError { + fn from(error: wasmtime::Error) -> Self { + use wasmtime::Trap; + let Some(trap_code) = error.downcast_ref::() else { + return FuzzError::Other; + }; + let trap_code = match trap_code { + Trap::UnreachableCodeReached => crate::TrapCode::UnreachableCodeReached, + Trap::MemoryOutOfBounds => crate::TrapCode::MemoryOutOfBounds, + Trap::TableOutOfBounds => crate::TrapCode::TableOutOfBounds, + Trap::IndirectCallToNull => crate::TrapCode::IndirectCallToNull, + Trap::IntegerDivisionByZero => crate::TrapCode::IntegerDivisionByZero, + Trap::IntegerOverflow => crate::TrapCode::IntegerOverflow, + Trap::BadConversionToInteger => crate::TrapCode::BadConversionToInteger, + Trap::StackOverflow => crate::TrapCode::StackOverflow, + Trap::BadSignature => crate::TrapCode::BadSignature, + _ => return FuzzError::Other, + }; + FuzzError::Trap(trap_code) + } +} diff --git a/crates/fuzz/src/value.rs b/crates/fuzz/src/value.rs index 46345ed711..1f47caf114 100644 --- a/crates/fuzz/src/value.rs +++ b/crates/fuzz/src/value.rs @@ -38,18 +38,26 @@ pub enum FuzzVal { I64(i64), F32(f32), F64(f64), - Null(FuzzRefTy), + FuncRef { is_null: bool }, + ExternRef { is_null: bool }, } -/// A Wasm reference type. -#[derive(Debug, Copy, Clone)] -pub enum FuzzRefTy { - /// The Wasm `funcref` type. - Func, - /// The Wasm `externref` type. - Extern, +impl PartialEq for FuzzVal { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::I32(l), Self::I32(r)) => l == r, + (Self::I64(l), Self::I64(r)) => l == r, + (Self::F32(l), Self::F32(r)) => l.to_bits() == r.to_bits(), + (Self::F64(l), Self::F64(r)) => l.to_bits() == r.to_bits(), + (Self::FuncRef { is_null: l }, Self::FuncRef { is_null: r }) => l == r, + (Self::ExternRef { is_null: l }, Self::ExternRef { is_null: r }) => l == r, + _ => false, + } + } } +impl Eq for FuzzVal {} + impl FuzzVal { /// Creates a new [`FuzzVal`] of the given `ty` initialized by `u`. pub fn with_type(ty: FuzzValType, u: &mut Unstructured) -> Self { @@ -58,8 +66,27 @@ impl FuzzVal { FuzzValType::I64 => Self::I64(i64::arbitrary(u).unwrap_or_default()), FuzzValType::F32 => Self::F32(f32::arbitrary(u).unwrap_or_default()), FuzzValType::F64 => Self::F64(f64::arbitrary(u).unwrap_or_default()), - FuzzValType::FuncRef => Self::Null(FuzzRefTy::Func), - FuzzValType::ExternRef => Self::Null(FuzzRefTy::Extern), + FuzzValType::FuncRef => Self::FuncRef { is_null: true }, + FuzzValType::ExternRef => Self::ExternRef { is_null: true }, + } + } +} + +impl From for wasmi::Val { + fn from(value: FuzzVal) -> Self { + match value { + FuzzVal::I32(value) => Self::I32(value), + FuzzVal::I64(value) => Self::I64(value), + FuzzVal::F32(value) => Self::F32(value.into()), + FuzzVal::F64(value) => Self::F64(value.into()), + FuzzVal::FuncRef { is_null } => { + assert!(is_null); + Self::FuncRef(wasmi::FuncRef::null()) + } + FuzzVal::ExternRef { is_null } => { + assert!(is_null); + Self::ExternRef(wasmi::ExternRef::null()) + } } } } diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs index 98ea20db95..1bf43106bd 100644 --- a/fuzz/fuzz_targets/differential.rs +++ b/fuzz/fuzz_targets/differential.rs @@ -2,634 +2,24 @@ use arbitrary::{Arbitrary, Unstructured}; use libfuzzer_sys::fuzz_target; -use std::{collections::hash_map::RandomState, mem}; -use wasmi as wasmi_reg; -use wasmi_fuzz::FuzzSmithConfig; -use wasmi_reg::core::{F32, F64}; - -/// Names of exported items. -#[derive(Debug, Default)] -pub struct Exports { - /// Names of exported functions. - funcs: Vec>, - /// Names of exported global variables. - globals: Vec>, - /// Names of exported linear memories. - memories: Vec>, - /// Names of exported tables. - tables: Vec>, -} - -/// Trait implemented by differential fuzzing backends. -trait DifferentialTarget: Sized { - /// The value type of the backend. - type Value; - /// The error type of the backend. - type Error; - - /// Sets up the store and exported functions for the backend if possible. - fn setup(wasm: &[u8]) -> Option; - - /// Calls the exported function with `name` and returns the result. - fn call(&mut self, name: &str) -> Result, Self::Error>; - - /// Returns the value of the global named `name` if any. - fn get_global(&mut self, name: &str) -> Option; - - /// Returns the bytes of the memory named `name` if any. - fn get_memory(&mut self, name: &str) -> Option<&[u8]>; -} - -/// Differential fuzzing backend for the register-machine Wasmi. -#[derive(Debug)] -struct WasmiRegister { - store: wasmi_reg::Store, - instance: wasmi_reg::Instance, - params: Vec, - results: Vec, -} - -impl WasmiRegister { - /// Returns the names of all exported items. - pub fn exports(&self) -> Exports { - let mut exports = Exports::default(); - for export in self.instance.exports(&self.store) { - let name = export.name(); - let dst = match export.ty(&self.store) { - wasmi::ExternType::Func(_) => &mut exports.funcs, - wasmi::ExternType::Global(_) => &mut exports.globals, - wasmi::ExternType::Memory(_) => &mut exports.memories, - wasmi::ExternType::Table(_) => &mut exports.tables, - }; - dst.push(name.into()); - } - exports - } - - fn type_to_value(ty: &wasmi_reg::core::ValType) -> wasmi_reg::Val { - match ty { - wasmi_reg::core::ValType::I32 => wasmi_reg::Val::I32(1), - wasmi_reg::core::ValType::I64 => wasmi_reg::Val::I64(1), - wasmi_reg::core::ValType::F32 => wasmi_reg::Val::F32(1.0.into()), - wasmi_reg::core::ValType::F64 => wasmi_reg::Val::F64(1.0.into()), - unsupported => panic!( - "differential fuzzing does not support reference types, yet but found: {unsupported:?}" - ), - } - } -} - -impl DifferentialTarget for WasmiRegister { - type Value = FuzzValue; - type Error = wasmi_reg::Error; - - fn call(&mut self, name: &str) -> Result, Self::Error> { - let Some(func) = self.instance.get_func(&self.store, name) else { - panic!( - "wasmi (register) is missing exported function {name} that exists in wasmi (register)" - ) - }; - let ty = func.ty(&self.store); - self.params.clear(); - self.results.clear(); - self.params - .extend(ty.params().iter().map(Self::type_to_value)); - self.results - .extend(ty.results().iter().map(Self::type_to_value)); - func.call(&mut self.store, &self.params[..], &mut self.results[..])?; - let results = self.results.iter().map(FuzzValue::from).collect(); - Ok(results) - } - - fn setup(wasm: &[u8]) -> Option { - use wasmi_reg::{Config, Engine, Linker, Module, StackLimits, Store, StoreLimitsBuilder}; - let mut config = Config::default(); - // We set custom limits since Wasmi (register) might use more - // stack space than Wasmi (stack) for some malicious recursive workloads. - // Wasmtime technically suffers from the same problems (register machine) - // but can offset them due to its superior optimizations. - // - // We increase the maximum stack space for Wasmi (register) to avoid - // common stack overflows in certain generated fuzz test cases this way. - config.set_stack_limits( - StackLimits::new( - 1024, // 1 kiB - 1024 * 1024 * 10, // 10 MiB - 1024, - ) - .unwrap(), - ); - let engine = Engine::new(&config); - let linker = Linker::new(&engine); - let limiter = StoreLimitsBuilder::new() - .memory_size(1000 * 0x10000) - .build(); - let mut store = Store::new(&engine, limiter); - store.limiter(|lim| lim); - let module = Module::new(store.engine(), wasm).unwrap(); - let Ok(preinstance) = linker.instantiate(&mut store, &module) else { - return None; - }; - let Ok(instance) = preinstance.ensure_no_start(&mut store) else { - return None; - }; - Some(Self { - store, - instance, - params: Vec::new(), - results: Vec::new(), - }) - } - - fn get_global(&mut self, name: &str) -> Option { - let value = self - .instance - .get_global(&self.store, name)? - .get(&self.store); - Some(FuzzValue::from(&value)) - } - - fn get_memory(&mut self, name: &str) -> Option<&[u8]> { - let data = self - .instance - .get_memory(&self.store, name)? - .data(&self.store); - Some(data) - } -} - -/// Differential fuzzing backend for the stack-machine Wasmi. -#[derive(Debug)] -struct WasmiStack { - store: wasmi_stack::Store, - instance: wasmi_stack::Instance, - params: Vec, - results: Vec, -} - -impl WasmiStack { - fn type_to_value(ty: &wasmi_stack::core::ValueType) -> wasmi_stack::Value { - use wasmi_stack::core::ValueType; - match ty { - ValueType::I32 => wasmi_stack::Value::I32(1), - ValueType::I64 => wasmi_stack::Value::I64(1), - ValueType::F32 => wasmi_stack::Value::F32(1.0.into()), - ValueType::F64 => wasmi_stack::Value::F64(1.0.into()), - unsupported => panic!( - "differential fuzzing does not support reference types, yet but found: {unsupported:?}" - ), - } - } -} - -impl DifferentialTarget for WasmiStack { - type Value = wasmi_stack::Value; - type Error = wasmi_stack::Error; - - fn call(&mut self, name: &str) -> Result, Self::Error> { - let Some(func) = self.instance.get_func(&self.store, name) else { - panic!( - "wasmi (stack) is missing exported function {name} that exists in wasmi (register)" - ) - }; - let ty = func.ty(&self.store); - self.params.clear(); - self.results.clear(); - self.params - .extend(ty.params().iter().map(Self::type_to_value)); - self.results - .extend(ty.results().iter().map(Self::type_to_value)); - func.call(&mut self.store, &self.params[..], &mut self.results[..])?; - let results = self.results.iter().map(FuzzValue::from).collect(); - Ok(results) - } - - fn setup(wasm: &[u8]) -> Option { - use wasmi_stack::{Engine, Linker, Module, Store, StoreLimitsBuilder}; - let engine = Engine::default(); - let linker = Linker::new(&engine); - let limiter = StoreLimitsBuilder::new() - .memory_size(1000 * 0x10000) - .build(); - let mut store = Store::new(&engine, limiter); - store.limiter(|lim| lim); - let module = Module::new(store.engine(), wasm).unwrap(); - let Ok(preinstance) = linker.instantiate(&mut store, &module) else { - return None; - }; - let Ok(instance) = preinstance.ensure_no_start(&mut store) else { - return None; - }; - Some(Self { - store, - instance, - params: Vec::new(), - results: Vec::new(), - }) - } - - fn get_global(&mut self, name: &str) -> Option { - let value = self - .instance - .get_global(&self.store, name)? - .get(&self.store); - Some(FuzzValue::from(&value)) - } - - fn get_memory(&mut self, name: &str) -> Option<&[u8]> { - let data = self - .instance - .get_memory(&self.store, name)? - .data(&self.store); - Some(data) - } -} - -/// Differential fuzzing backend for Wasmtime. -struct Wasmtime { - store: wasmtime::Store, - instance: wasmtime::Instance, - params: Vec, - results: Vec, -} - -impl Wasmtime { - fn type_to_value(ty: wasmtime::ValType) -> wasmtime::Val { - match ty { - wasmtime::ValType::I32 => wasmtime::Val::I32(1), - wasmtime::ValType::I64 => wasmtime::Val::I64(1), - wasmtime::ValType::F32 => wasmtime::Val::F32(1.0_f32.to_bits()), - wasmtime::ValType::F64 => wasmtime::Val::F64(1.0_f64.to_bits()), - unsupported => panic!( - "differential fuzzing does not support reference types, yet but found: {unsupported:?}" - ), - } - } -} - -impl DifferentialTarget for Wasmtime { - type Value = wasmtime::Val; - type Error = wasmtime::Error; - - fn call(&mut self, name: &str) -> Result, Self::Error> { - let Some(func) = self.instance.get_func(&mut self.store, name) else { - panic!("wasmtime is missing exported function {name} that exists in wasmi (register)") - }; - let ty = func.ty(&self.store); - self.params.clear(); - self.results.clear(); - self.params.extend(ty.params().map(Self::type_to_value)); - self.results.extend(ty.results().map(Self::type_to_value)); - func.call(&mut self.store, &self.params[..], &mut self.results[..])?; - let results = self.results.iter().map(FuzzValue::from).collect(); - Ok(results) - } - - fn setup(wasm: &[u8]) -> Option { - use wasmtime::{Config, Engine, Linker, Module, Store, StoreLimitsBuilder}; - let mut config = Config::default(); - // We disabled backtraces since they sometimes become so large - // that the entire output is obliterated by them. Generally we are - // more interested what kind of error occurred and now how an error - // occurred. - config.wasm_backtrace(false); - let engine = Engine::default(); - let linker = Linker::new(&engine); - let limiter = StoreLimitsBuilder::new() - .memory_size(1000 * 0x10000) - .build(); - let mut store = Store::new(&engine, limiter); - store.limiter(|lim| lim); - let module = Module::new(store.engine(), wasm).unwrap(); - let Ok(instance) = linker.instantiate(&mut store, &module) else { - return None; - }; - Some(Self { - store, - instance, - params: Vec::new(), - results: Vec::new(), - }) - } - - fn get_global(&mut self, name: &str) -> Option { - let value = self - .instance - .get_global(&mut self.store, name)? - .get(&mut self.store); - Some(FuzzValue::from(&value)) - } - - fn get_memory(&mut self, name: &str) -> Option<&[u8]> { - let data = self - .instance - .get_memory(&mut self.store, name)? - .data(&mut self.store); - Some(data) - } -} - -#[derive(Debug, Default)] -pub struct UnmatchedState { - globals: Vec, - memories: Vec, -} - -impl UnmatchedState { - fn new( - exports: &Exports, - wasmi_reg: &mut WasmiRegister, - wasmi_stack: &mut WasmiStack, - wasmtime: &mut Wasmtime, - ) -> Self { - let mut unmatched = Self::default(); - unmatched.extract_unmatched_globals(&exports.globals, wasmi_reg, wasmi_stack, wasmtime); - unmatched.extract_unmatched_memories(&exports.memories, wasmi_reg, wasmi_stack, wasmtime); - unmatched - } - - fn is_empty(&self) -> bool { - self.globals.is_empty() && self.memories.is_empty() - } - - fn extract_unmatched_globals( - &mut self, - globals: &[Box], - wasmi_reg: &mut WasmiRegister, - wasmi_stack: &mut WasmiStack, - wasmtime: &mut Wasmtime, - ) { - for name in globals { - let Some(value_reg) = wasmi_reg.get_global(name) else { - panic!("missing global for Wasmi (register): {name}") - }; - let Some(value_stack) = wasmi_stack.get_global(name) else { - panic!("missing global for Wasmi (stack): {name}") - }; - let Some(value_wasmtime) = wasmtime.get_global(name) else { - panic!("missing global for Wasmtime: {name}") - }; - if let Some(entry) = UnmatchedGlobal::new(name, value_reg, value_stack, value_wasmtime) - { - self.push_global(entry); - } - } - } - - fn extract_unmatched_memories( - &mut self, - memories: &[Box], - wasmi_reg: &mut WasmiRegister, - wasmi_stack: &mut WasmiStack, - wasmtime: &mut Wasmtime, - ) { - for name in memories { - let Some(memory_reg) = wasmi_reg.get_memory(name) else { - panic!("missing linear memory for Wasmi (register): {name}") - }; - let Some(memory_stack) = wasmi_stack.get_memory(name) else { - panic!("missing linear memory for Wasmi (stack): {name}") - }; - let Some(memory_wasmtime) = wasmtime.get_memory(name) else { - panic!("missing linear memory for Wasmtime: {name}") - }; - if let Some(entry) = - UnmatchedMemory::new(name, memory_reg, memory_stack, memory_wasmtime) - { - self.push_memory(entry); - } - } - } - - fn push_global(&mut self, global: UnmatchedGlobal) { - self.globals.push(global); - } - - fn push_memory(&mut self, memory: UnmatchedMemory) { - self.memories.push(memory); - } -} - -#[allow(dead_code)] // Note: dead code analysis somehow ignores Debug impl usage. -#[derive(Debug)] -pub struct UnmatchedGlobal { - name: Box, - value_register: FuzzValue, - value_stack: FuzzValue, - value_wasmtime: FuzzValue, -} - -impl UnmatchedGlobal { - /// Returns an [`UnmatchedGlobal`] if either `value_stack` or `value_wasmtime` differs from `value_register`. - /// - /// Returns `None` otherwise. - pub fn new( - name: &str, - value_register: FuzzValue, - value_stack: FuzzValue, - value_wasmtime: FuzzValue, - ) -> Option { - if (value_register != value_stack) || (value_register != value_wasmtime) { - return Some(Self { - name: name.into(), - value_register, - value_stack, - value_wasmtime, - }); - } - None - } -} - -#[allow(dead_code)] // Note: dead code analysis somehow ignores Debug impl usage. -#[derive(Debug)] -pub struct UnmatchedMemory { - name: Box, - hash_register: u64, - hash_stack: u64, - hash_wasmtime: u64, -} - -impl UnmatchedMemory { - /// Returns an [`UnmatchedMemory`] if either `memory_stack` or `memory_wasmtime` differs from `memory_register`. - /// - /// Returns `None` otherwise. - pub fn new( - name: &str, - memory_register: &[u8], - memory_stack: &[u8], - memory_wasmtime: &[u8], - ) -> Option { - use std::hash::BuildHasher as _; - let hasher = RandomState::new(); - let hash_register = hasher.hash_one(memory_register); - let hash_stack = hasher.hash_one(memory_stack); - let hash_wasmtime = hasher.hash_one(memory_wasmtime); - if (hash_register != hash_stack) || (hash_register != hash_wasmtime) { - return Some(Self { - name: name.into(), - hash_register, - hash_stack, - hash_wasmtime, - }); - } - None - } -} - -#[derive(Debug)] -pub struct FuzzContext { - wasm: Vec, - wasmi_register: WasmiRegister, - wasmi_stack: WasmiStack, - exports: Exports, -} - -impl FuzzContext { - pub fn run(&mut self) { - for name in mem::take(&mut self.exports.funcs) { - let result_reg = self.wasmi_register.call(&name); - let result_stack = self.wasmi_stack.call(&name); - match (result_reg, result_stack) { - (Err(err_reg), Err(err_stack)) => self.both_error(&name, err_reg, err_stack), - (Ok(ok_reg), Err(err_stack)) => self.reg_ok_stack_err(&name, &ok_reg, err_stack), - (Err(err_reg), Ok(ok_stack)) => self.reg_err_stack_ok(&name, err_reg, &ok_stack), - (Ok(ok_reg), Ok(ok_stack)) => self.both_ok(&name, &ok_reg, &ok_stack), - } - } - } - - fn unmatched_state(&mut self, wasmtime: &mut Wasmtime) -> UnmatchedState { - UnmatchedState::new( - &self.exports, - &mut self.wasmi_register, - &mut self.wasmi_stack, - wasmtime, - ) - } - - fn both_error( - &mut self, - func_name: &str, - error_reg: ::Error, - error_stack: ::Error, - ) { - let errstr_reg = error_reg.to_string(); - let errstr_stack = error_stack.to_string(); - if errstr_reg == errstr_stack { - // Bail out since both Wasmi (register) and Wasmi (stack) agree on the execution failure. - return; - } - let Some(mut wasmtime) = ::setup(&self.wasm) else { - panic!("failed to setup Wasmtime fuzzing backend"); - }; - let result_wasmtime = wasmtime.call(func_name); - let unmatched_state = self.unmatched_state(&mut wasmtime); - panic!( - "\ - Wasmi (register) and Wasmi (stack) both fail with different error codes:\n\ - \x20 Function: {func_name:?}\n\ - \x20 Wasmi (register): {errstr_reg}\n\ - \x20 Wasmi (stack) : {errstr_stack}\n\ - \x20 Wasmtime : {result_wasmtime:?}\n\ - \n\ - {unmatched_state:#?}", - ) - } - - fn reg_ok_stack_err( - &mut self, - func_name: &str, - results_reg: &[FuzzValue], - error_stack: ::Error, - ) { - let errstr_stack = error_stack.to_string(); - let Some(mut wasmtime) = ::setup(&self.wasm) else { - panic!("failed to setup Wasmtime fuzzing backend"); - }; - let result_wasmtime = wasmtime.call(func_name); - let unmatched_state = self.unmatched_state(&mut wasmtime); - panic!( - "\ - Wasmi (register) succeeded and Wasmi (stack) failed:\n\ - \x20 Function: {func_name:?}\n\ - \x20 Wasmi (register): {results_reg:?}\n\ - \x20 Wasmi (stack) : {errstr_stack}\n\ - \x20 Wasmtime : {result_wasmtime:?}\n\ - \n\ - {unmatched_state:#?}", - ) - } - - fn reg_err_stack_ok( - &mut self, - func_name: &str, - error_reg: ::Error, - results_stack: &[FuzzValue], - ) { - let errstr_reg = error_reg.to_string(); - let Some(mut wasmtime) = ::setup(&self.wasm) else { - panic!("failed to setup Wasmtime fuzzing backend"); - }; - let results_wasmtime = wasmtime.call(func_name); - let unmatched_state = self.unmatched_state(&mut wasmtime); - panic!( - "\ - Wasmi (register) failed and Wasmi (stack) succeeded:\n\ - \x20 Function: {func_name:?}\n\ - \x20 Wasmi (register): {errstr_reg}\n\ - \x20 Wasmi (stack) : {results_stack:?}\n\ - \x20 Wasmtime : {results_wasmtime:?}\n\ - \n\ - {unmatched_state:#?}", - ) - } - - fn both_ok(&mut self, func_name: &str, results_reg: &[FuzzValue], results_stack: &[FuzzValue]) { - if results_reg == results_stack { - // Bail out since both Wasmi (register) and Wasmi (stack) agree on the execution results. - return; - } - let Some(mut wasmtime) = ::setup(&self.wasm) else { - panic!("failed to setup Wasmtime fuzzing backend"); - }; - let results_wasmtime = wasmtime.call(func_name).unwrap_or_else(|error| { - panic!("failed to execute func ({func_name}) via Wasmtime fuzzing backend: {error}") - }); - let text = match ( - &results_wasmtime[..] == results_reg, - &results_wasmtime[..] == results_stack, - ) { - (true, false) => "Wasmi (stack) disagrees with Wasmi (register) and Wasmtime", - (false, true) => "Wasmi (register) disagrees with Wasmi (stack) and Wasmtime", - (false, false) => "Wasmi (register), Wasmi (stack) and Wasmtime disagree", - (true, true) => unreachable!("results_reg and results_stack differ"), - }; - let unmatched_state = self.unmatched_state(&mut wasmtime); - println!( - "{text} for function execution: {func_name:?}\n\ - \x20 Wasmi (register): {results_reg:?}\n\ - \x20 Wasmi (stack) : {results_stack:?}\n\ - \x20 Wasmtime : {results_wasmtime:?}\n\ - \n\ - {unmatched_state:#?}", - ); - if &results_wasmtime[..] != results_reg || !unmatched_state.is_empty() { - panic!() - } - } -} +use wasmi::Val; +use wasmi_fuzz::{ + config::FuzzSmithConfig, + oracle::{ChosenOracle, DifferentialOracle, DifferentialOracleMeta, WasmiOracle}, + FuzzVal, +}; fuzz_target!(|seed: &[u8]| { - let mut unstructured = Unstructured::new(seed); - let Ok(mut fuzz_config) = FuzzSmithConfig::arbitrary(&mut unstructured) else { + let mut u = Unstructured::new(seed); + let Ok(mut fuzz_config) = FuzzSmithConfig::arbitrary(&mut u) else { return; }; + let chosen_oracle = ChosenOracle::arbitrary(&mut u).unwrap_or_default(); fuzz_config.enable_nan_canonicalization(); fuzz_config.export_everything(); - fuzz_config.disable_multi_memory(); // TODO: only disable for Wasmi (stack) - let Ok(mut smith_module) = wasm_smith::Module::new(fuzz_config.into(), &mut unstructured) - else { + WasmiOracle::configure(&mut fuzz_config); + chosen_oracle.configure(&mut fuzz_config); + let Ok(mut smith_module) = wasm_smith::Module::new(fuzz_config.into(), &mut u) else { return; }; // Note: We cannot use built-in fuel metering of the different engines since that @@ -637,87 +27,154 @@ fuzz_target!(|seed: &[u8]| { let Ok(_) = smith_module.ensure_termination(1_000 /* fuel */) else { return; }; - let wasm = smith_module.to_bytes(); - let Some(wasmi_register) = ::setup(&wasm[..]) else { + let wasm_bytes = smith_module.to_bytes(); + let wasm = wasm_bytes.as_slice(); + let Some(mut wasmi_oracle) = WasmiOracle::setup(wasm) else { return; }; - let Some(wasmi_stack) = ::setup(&wasm[..]) else { - panic!("wasmi (register) succeeded to create Context while wasmi (stack) failed"); - }; - let exports = wasmi_register.exports(); - let mut context = FuzzContext { - wasm, - wasmi_register, - wasmi_stack, - exports, + let Some(mut chosen_oracle) = chosen_oracle.setup(wasm) else { + return; }; - context.run(); -}); - -#[derive(Debug, Copy, Clone)] -pub enum FuzzValue { - I32(i32), - I64(i64), - F32(F32), - F64(F64), -} - -impl PartialEq for FuzzValue { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::I32(lhs), Self::I32(rhs)) => lhs == rhs, - (Self::I64(lhs), Self::I64(rhs)) => lhs == rhs, - (Self::F32(lhs), Self::F32(rhs)) => { - if lhs.is_nan() && rhs.is_nan() { - // TODO: we might want to test if NaN bits are the same. - return true; + let exports = wasmi_oracle.exports(); + let mut params = Vec::new(); + // True as long as differential execution is deterministic between both oracles. + let mut deterministic = true; + for (name, func_type) in exports.funcs() { + params.clear(); + params.extend( + func_type + .params() + .iter() + .copied() + .map(Val::default) + .map(FuzzVal::from), + ); + let params = ¶ms[..]; + let result_wasmi = wasmi_oracle.call(name, params); + let result_oracle = chosen_oracle.call(name, params); + // Note: If either of the oracles returns a non-deterministic error we ignore it + // to avoid having to deal with non-deterministic behavior between oracles. + if let Err(wasmi_err) = &result_wasmi { + if wasmi_err.is_non_deterministic() { + deterministic = false; + continue; + } + } + if let Err(oracle_err) = &result_wasmi { + if oracle_err.is_non_deterministic() { + deterministic = false; + continue; + } + } + let wasmi_name = wasmi_oracle.name(); + let oracle_name = chosen_oracle.name(); + match (result_wasmi, result_oracle) { + (Ok(wasmi_results), Ok(oracle_results)) => { + if wasmi_results == oracle_results { + continue; } - lhs == rhs + panic!( + "\ + function call returned different values:\n\ + \tfunc: {name}\n\ + \tparams: {params:?}\n\ + \t{wasmi_name}: {wasmi_results:?}\n\ + \t{oracle_name}: {oracle_results:?}\n\ + " + ) } - (Self::F64(lhs), Self::F64(rhs)) => { - if lhs.is_nan() && rhs.is_nan() { - // TODO: we might want to test if NaN bits are the same. - return true; + (Err(wasmi_err), Err(oracle_err)) => { + if wasmi_err == oracle_err { + continue; } - lhs == rhs + panic!( + "\ + function call returned different errors:\n\ + \tfunc: {name}\n\ + \tparams: {params:?}\n\ + \t{wasmi_name}: {wasmi_err:?}\n\ + \t{oracle_name}: {oracle_err:?}\n\ + " + ) + } + (Ok(wasmi_results), Err(oracle_err)) => { + panic!( + "\ + function call returned results and error:\n\ + \tfunc: {name}\n\ + \tparams: {params:?}\n\ + \t{wasmi_name}: {wasmi_results:?}\n\ + \t{oracle_name}: {oracle_err:?}\n\ + " + ) + } + (Err(wasmi_err), Ok(oracle_results)) => { + panic!( + "\ + function call returned results and error:\n\ + \tfunc: {name}\n\ + \tparams: {params:?}\n\ + \t{wasmi_name}: {wasmi_err:?}\n\ + \t{oracle_name}: {oracle_results:?}\n\ + " + ) } - _ => false, } } -} - -impl From<&wasmi_reg::Val> for FuzzValue { - fn from(value: &wasmi_reg::Val) -> Self { - match value { - wasmi_reg::Val::I32(value) => Self::I32(*value), - wasmi_reg::Val::I64(value) => Self::I64(*value), - wasmi_reg::Val::F32(value) => Self::F32(*value), - wasmi_reg::Val::F64(value) => Self::F64(*value), - _ => panic!("unsupported value type"), - } + if !deterministic { + // We bail out and do not check global state since potential non-determinism + // has been detected previously which could have led to non-deterministic changes + // to Wasm global state. + return; } -} - -impl From<&wasmi_stack::Value> for FuzzValue { - fn from(value: &wasmi_stack::Value) -> Self { - match value { - wasmi_stack::Value::I32(value) => Self::I32(*value), - wasmi_stack::Value::I64(value) => Self::I64(*value), - wasmi_stack::Value::F32(value) => Self::F32(F32::from_bits(value.to_bits())), - wasmi_stack::Value::F64(value) => Self::F64(F64::from_bits(value.to_bits())), - _ => panic!("unsupported value type"), + for name in exports.globals() { + let wasmi_val = wasmi_oracle.get_global(name); + let oracle_val = chosen_oracle.get_global(name); + if wasmi_val == oracle_val { + continue; } + let wasmi_name = wasmi_oracle.name(); + let oracle_name = chosen_oracle.name(); + panic!( + "\ + encountered unequal globals:\n\ + \tglobal: {name}\n\ + \t{wasmi_name}: {wasmi_val:?}\n\ + \t{oracle_name}: {oracle_val:?}\n\ + " + ) } -} - -impl From<&wasmtime::Val> for FuzzValue { - fn from(value: &wasmtime::Val) -> Self { - match value { - wasmtime::Val::I32(value) => Self::I32(*value), - wasmtime::Val::I64(value) => Self::I64(*value), - wasmtime::Val::F32(value) => Self::F32(F32::from_bits(*value)), - wasmtime::Val::F64(value) => Self::F64(F64::from_bits(*value)), - _ => panic!("unsupported value type"), + for name in exports.memories() { + let Some(wasmi_mem) = wasmi_oracle.get_memory(name) else { + continue; + }; + let Some(oracle_mem) = chosen_oracle.get_memory(name) else { + continue; + }; + if wasmi_mem == oracle_mem { + continue; + } + let mut first_nonmatching = 0; + let mut byte_wasmi = 0; + let mut byte_oracle = 0; + for (n, (mem0, mem1)) in wasmi_mem.iter().zip(oracle_mem).enumerate() { + if mem0 != mem1 { + first_nonmatching = n; + byte_wasmi = *mem0; + byte_oracle = *mem1; + break; + } } + let wasmi_name = wasmi_oracle.name(); + let oracle_name = chosen_oracle.name(); + panic!( + "\ + encountered unequal memories:\n\ + \tmemory: {name}\n\ + \tindex first non-matching: {first_nonmatching}\n\ + \t{wasmi_name}: {byte_wasmi:?}\n\ + \t{oracle_name}: {byte_oracle:?}\n\ + " + ) } -} +});