diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs index d8b072a072..47ed2ebb6d 100644 --- a/crates/fuzzing/src/generators/config.rs +++ b/crates/fuzzing/src/generators/config.rs @@ -27,14 +27,23 @@ pub struct Config { impl Config { /// Indicates that this configuration is being used for differential - /// execution so only a single function should be generated since that's all - /// that's going to be exercised. + /// execution. + /// + /// The purpose of this function is to update the configuration which was + /// generated to be compatible with execution in multiple engines. The goal + /// is to produce the exact same result in all engines so we need to paper + /// over things like nan differences and memory/table behavior differences. pub fn set_differential_config(&mut self) { let config = &mut self.module_config.config; + // Disable the start function for now. + // + // TODO: should probably allow this after testing it works with the new + // differential setup in all engines. config.allow_start_export = false; - // Make sure there's a type available for the function. + // Make it more likely that there are types available to generate a + // function with. config.min_types = 1; config.max_types = config.max_types.max(1); @@ -70,15 +79,6 @@ impl Config { // can paper over NaN differences between engines. config.canonicalize_nans = true; - // When diffing against a non-wasmtime engine then disable wasm - // features to get selectively re-enabled against each differential - // engine. - config.bulk_memory_enabled = false; - config.reference_types_enabled = false; - config.simd_enabled = false; - config.memory64_enabled = false; - config.threads_enabled = false; - // If using the pooling allocator, update the instance limits too if let InstanceAllocationStrategy::Pooling { instance_limits: limits, @@ -103,34 +103,6 @@ impl Config { } } - /// Force `self` to be a configuration compatible with `other`. This is - /// useful for differential execution to avoid unhelpful fuzz crashes when - /// one engine has a feature enabled and the other does not. - pub fn make_compatible_with(&mut self, other: &Self) { - // Use the same `wasm-smith` configuration as `other` because this is - // used for determining what Wasm features are enabled in the engine - // (see `to_wasmtime`). - self.module_config = other.module_config.clone(); - - // Use the same allocation strategy between the two configs. - // - // Ideally this wouldn't be necessary, but, during differential - // evaluation, if the `lhs` is using ondemand and the `rhs` is using the - // pooling allocator (or vice versa), then the module may have been - // generated in such a way that is incompatible with the other - // allocation strategy. - // - // We can remove this in the future when it's possible to access the - // fields of `wasm_smith::Module` to constrain the pooling allocator - // based on what was actually generated. - self.wasmtime.strategy = other.wasmtime.strategy.clone(); - if let InstanceAllocationStrategy::Pooling { .. } = &other.wasmtime.strategy { - // Also use the same memory configuration when using the pooling - // allocator. - self.wasmtime.memory_config = other.wasmtime.memory_config.clone(); - } - } - /// Uses this configuration and the supplied source of data to generate /// a wasm module. /// @@ -416,6 +388,31 @@ pub struct WasmtimeConfig { native_unwind_info: bool, } +impl WasmtimeConfig { + /// Force `self` to be a configuration compatible with `other`. This is + /// useful for differential execution to avoid unhelpful fuzz crashes when + /// one engine has a feature enabled and the other does not. + pub fn make_compatible_with(&mut self, other: &Self) { + // Use the same allocation strategy between the two configs. + // + // Ideally this wouldn't be necessary, but, during differential + // evaluation, if the `lhs` is using ondemand and the `rhs` is using the + // pooling allocator (or vice versa), then the module may have been + // generated in such a way that is incompatible with the other + // allocation strategy. + // + // We can remove this in the future when it's possible to access the + // fields of `wasm_smith::Module` to constrain the pooling allocator + // based on what was actually generated. + self.strategy = other.strategy.clone(); + if let InstanceAllocationStrategy::Pooling { .. } = &other.strategy { + // Also use the same memory configuration when using the pooling + // allocator. + self.memory_config = other.memory_config.clone(); + } + } +} + #[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)] enum OptLevel { None, diff --git a/crates/fuzzing/src/generators/value.rs b/crates/fuzzing/src/generators/value.rs index 9386c137bd..e4d5102023 100644 --- a/crates/fuzzing/src/generators/value.rs +++ b/crates/fuzzing/src/generators/value.rs @@ -13,6 +13,8 @@ pub enum DiffValue { F32(u32), F64(u64), V128(u128), + FuncRef { null: bool }, + ExternRef { null: bool }, } impl DiffValue { @@ -23,6 +25,8 @@ impl DiffValue { DiffValue::F32(_) => DiffValueType::F32, DiffValue::F64(_) => DiffValueType::F64, DiffValue::V128(_) => DiffValueType::V128, + DiffValue::FuncRef { .. } => DiffValueType::FuncRef, + DiffValue::ExternRef { .. } => DiffValueType::ExternRef, } } @@ -51,7 +55,18 @@ impl DiffValue { (1.0f32).to_bits(), f32::MAX.to_bits(), ]; - DiffValue::F32(biased_arbitrary_value(u, known_f32_values)?) + let bits = biased_arbitrary_value(u, known_f32_values)?; + + // If the chosen bits are NAN then always use the canonical bit + // pattern of nan to enable better compatibility with engines + // where arbitrary nan patterns can't make their way into wasm + // (e.g. v8 through JS can't do that). + let bits = if f32::from_bits(bits).is_nan() { + f32::NAN.to_bits() + } else { + bits + }; + DiffValue::F32(bits) } F64 => { // TODO once `to_bits` is stable as a `const` function, move @@ -66,9 +81,23 @@ impl DiffValue { (1.0f64).to_bits(), f64::MAX.to_bits(), ]; - DiffValue::F64(biased_arbitrary_value(u, known_f64_values)?) + let bits = biased_arbitrary_value(u, known_f64_values)?; + // See `f32` above for why canonical nan patterns are always + // used. + let bits = if f64::from_bits(bits).is_nan() { + f64::NAN.to_bits() + } else { + bits + }; + DiffValue::F64(bits) } V128 => DiffValue::V128(biased_arbitrary_value(u, KNOWN_U128_VALUES)?), + + // TODO: this isn't working in most engines so just always pass a + // null in which if an engine supports this is should at least + // support doing that. + FuncRef => DiffValue::FuncRef { null: true }, + ExternRef => DiffValue::ExternRef { null: true }, }; arbitrary::Result::Ok(val) } @@ -111,6 +140,8 @@ impl Hash for DiffValue { DiffValue::F32(n) => n.hash(state), DiffValue::F64(n) => n.hash(state), DiffValue::V128(n) => n.hash(state), + DiffValue::ExternRef { null } => null.hash(state), + DiffValue::FuncRef { null } => null.hash(state), } } } @@ -144,13 +175,15 @@ impl PartialEq for DiffValue { let r0 = f64::from_bits(*r0); l0 == r0 || (l0.is_nan() && r0.is_nan()) } + (Self::FuncRef { null: a }, Self::FuncRef { null: b }) => a == b, + (Self::ExternRef { null: a }, Self::ExternRef { null: b }) => a == b, _ => false, } } } /// Enumerate the supported value types. -#[derive(Clone, Debug, Arbitrary, Hash)] +#[derive(Copy, Clone, Debug, Arbitrary, Hash)] #[allow(missing_docs)] pub enum DiffValueType { I32, @@ -158,6 +191,8 @@ pub enum DiffValueType { F32, F64, V128, + FuncRef, + ExternRef, } impl TryFrom for DiffValueType { @@ -170,8 +205,8 @@ impl TryFrom for DiffValueType { F32 => Ok(Self::F32), F64 => Ok(Self::F64), V128 => Ok(Self::V128), - FuncRef => Err("unable to convert reference types"), - ExternRef => Err("unable to convert reference types"), + FuncRef => Ok(Self::FuncRef), + ExternRef => Ok(Self::ExternRef), } } } diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index 678841a7b9..45c05858c9 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -20,12 +20,10 @@ mod stacks; use self::diff_wasmtime::WasmtimeInstance; use self::engine::DiffInstance; -use crate::generators::{self, DiffValue}; +use crate::generators::{self, DiffValue, DiffValueType}; use arbitrary::Arbitrary; pub use stacks::check_stacks; use std::cell::Cell; -use std::collections::hash_map::DefaultHasher; -use std::hash::Hasher; use std::rc::Rc; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; use std::sync::{Arc, Condvar, Mutex}; @@ -34,9 +32,7 @@ use wasmtime::*; use wasmtime_wast::WastContext; #[cfg(not(any(windows, target_arch = "s390x")))] -pub use self::v8::*; -#[cfg(not(any(windows, target_arch = "s390x")))] -mod v8; +mod diff_v8; static CNT: AtomicUsize = AtomicUsize::new(0); @@ -343,12 +339,24 @@ pub fn differential( rhs: &mut WasmtimeInstance, name: &str, args: &[DiffValue], + result_tys: &[DiffValueType], ) -> anyhow::Result<()> { - log::debug!("Evaluating: {}({:?})", name, args); - let lhs_results = lhs.evaluate(name, args); + log::debug!("Evaluating: `{}` with {:?}", name, args); + let lhs_results = match lhs.evaluate(name, args, result_tys) { + Ok(Some(results)) => Ok(results), + Err(e) => Err(e), + // this engine couldn't execute this type signature, so discard this + // execution by returning success. + Ok(None) => return Ok(()), + }; log::debug!(" -> results on {}: {:?}", lhs.name(), &lhs_results); - let rhs_results = rhs.evaluate(name, args); + + let rhs_results = rhs + .evaluate(name, args, result_tys) + // wasmtime should be able to invoke any signature, so unwrap this result + .map(|results| results.unwrap()); log::debug!(" -> results on {}: {:?}", rhs.name(), &rhs_results); + match (lhs_results, rhs_results) { // If the evaluation succeeds, we compare the results. (Ok(lhs_results), Ok(rhs_results)) => assert_eq!(lhs_results, rhs_results), @@ -362,19 +370,26 @@ pub fn differential( (Err(_), Ok(_)) => panic!("only the `lhs` ({}) failed for this input", lhs.name()), }; - let hash = |i: &mut dyn DiffInstance| -> anyhow::Result { - let mut hasher = DefaultHasher::new(); - i.hash(&mut hasher)?; - Ok(hasher.finish()) - }; - - if lhs.is_hashable() && rhs.is_hashable() { - log::debug!("Hashing instances:"); - let lhs_hash = hash(lhs)?; - log::debug!(" -> hash of {}: {:?}", lhs.name(), lhs_hash); - let rhs_hash = hash(rhs)?; - log::debug!(" -> hash of {}: {:?}", rhs.name(), rhs_hash); - assert_eq!(lhs_hash, rhs_hash); + for (global, ty) in rhs.exported_globals() { + log::debug!("Comparing global `{global}`"); + let lhs = match lhs.get_global(&global, ty) { + Some(val) => val, + None => continue, + }; + let rhs = rhs.get_global(&global, ty).unwrap(); + assert_eq!(lhs, rhs); + } + for (memory, shared) in rhs.exported_memories() { + log::debug!("Comparing memory `{memory}`"); + let lhs = match lhs.get_memory(&memory, shared) { + Some(val) => val, + None => continue, + }; + let rhs = rhs.get_memory(&memory, shared).unwrap(); + if lhs == rhs { + continue; + } + panic!("memories have differing values"); } Ok(()) diff --git a/crates/fuzzing/src/oracles/diff_spec.rs b/crates/fuzzing/src/oracles/diff_spec.rs index fc5d22b880..d545d24ce0 100644 --- a/crates/fuzzing/src/oracles/diff_spec.rs +++ b/crates/fuzzing/src/oracles/diff_spec.rs @@ -1,10 +1,11 @@ //! Evaluate an exported Wasm function using the WebAssembly specification //! reference interpreter. -use crate::generators::{DiffValue, ModuleConfig}; +use crate::generators::{DiffValue, DiffValueType, ModuleConfig}; use crate::oracles::engine::{DiffEngine, DiffInstance}; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, bail, Error, Result}; use wasm_spec_interpreter::Value; +use wasmtime::Trap; /// A wrapper for `wasm-spec-interpreter` as a [`DiffEngine`]. pub struct SpecInterpreter; @@ -13,19 +14,36 @@ impl SpecInterpreter { /// Build a new [`SpecInterpreter`] but only if the configuration does not /// rely on features that the current bindings (i.e., /// `wasm-spec-interpreter`) do not support. - pub fn new(config: &ModuleConfig) -> Result> { + pub fn new(config: &ModuleConfig) -> Result { if config.config.reference_types_enabled { bail!("the spec interpreter bindings do not support reference types") } + // TODO: right now the interpreter bindings only execute the first + // function in the module so if there's possibly more than one function + // it's not possible to run the other function. This should be fixed + // with improvements to the ocaml bindings to the interpreter. if config.config.max_funcs > 1 { - // TODO bail!("the spec interpreter bindings can only support one function for now") } + + // TODO: right now the instantiation step for the interpreter does + // nothing and the evaluation step performs an instantiation followed by + // an execution. This means that instantiations which fail in other + // engines will "succeed" in the interpreter because the error is + // delayed to the execution. This should be fixed by making + // instantiation a first-class primitive in our interpreter bindings. if config.config.max_tables > 0 { - // TODO bail!("the spec interpreter bindings do not fail as they should with out-of-bounds table accesses") } - Ok(Box::new(Self)) + + if config.config.memory64_enabled { + bail!("memory64 not implemented in spec interpreter"); + } + + if config.config.threads_enabled { + bail!("spec interpreter does not support the threading proposal"); + } + Ok(Self) } } @@ -34,12 +52,17 @@ impl DiffEngine for SpecInterpreter { "spec" } - fn instantiate(&self, wasm: &[u8]) -> Result> { + fn instantiate(&mut self, wasm: &[u8]) -> Result> { // TODO: ideally we would avoid copying the module bytes here. Ok(Box::new(SpecInstance { wasm: wasm.to_vec(), })) } + + fn assert_error_match(&self, trap: &Trap, err: Error) { + // TODO: implement this for the spec interpreter + drop((trap, err)); + } } struct SpecInstance { @@ -55,7 +78,8 @@ impl DiffInstance for SpecInstance { &mut self, _function_name: &str, arguments: &[DiffValue], - ) -> Result> { + _results: &[DiffValueType], + ) -> Result>> { // The spec interpreter needs some work before it can fully support this // interface: // - TODO adapt `wasm-spec-interpreter` to use function name to select @@ -64,17 +88,19 @@ impl DiffInstance for SpecInstance { // so we can hash memory, globals, etc. let arguments = arguments.iter().map(Value::from).collect(); match wasm_spec_interpreter::interpret(&self.wasm, Some(arguments)) { - Ok(results) => Ok(results.into_iter().map(Value::into).collect()), + Ok(results) => Ok(Some(results.into_iter().map(Value::into).collect())), Err(err) => Err(anyhow!(err)), } } - fn is_hashable(&self) -> bool { - false + fn get_global(&mut self, _name: &str, _ty: DiffValueType) -> Option { + // TODO: should implement this + None } - fn hash(&mut self, _state: &mut std::collections::hash_map::DefaultHasher) -> Result<()> { - unimplemented!() + fn get_memory(&mut self, _name: &str, _shared: bool) -> Option> { + // TODO: should implement this + None } } @@ -86,6 +112,7 @@ impl From<&DiffValue> for Value { DiffValue::F32(n) => Value::F32(n as i32), DiffValue::F64(n) => Value::F64(n as i64), DiffValue::V128(n) => Value::V128(n.to_le_bytes().to_vec()), + DiffValue::FuncRef { .. } | DiffValue::ExternRef { .. } => unimplemented!(), } } } @@ -121,3 +148,11 @@ impl Into for Value { pub fn setup_ocaml_runtime() { wasm_spec_interpreter::setup_ocaml_runtime(); } + +#[test] +fn smoke() { + if !wasm_spec_interpreter::support_compiled_in() { + return; + } + crate::oracles::engine::smoke_test_engine(|config| SpecInterpreter::new(&config.module_config)) +} diff --git a/crates/fuzzing/src/oracles/diff_v8.rs b/crates/fuzzing/src/oracles/diff_v8.rs new file mode 100644 index 0000000000..f0e1a1a38c --- /dev/null +++ b/crates/fuzzing/src/oracles/diff_v8.rs @@ -0,0 +1,321 @@ +use crate::generators::{DiffValue, DiffValueType, ModuleConfig}; +use crate::oracles::engine::{DiffEngine, DiffInstance}; +use anyhow::{bail, Error, Result}; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Once; +use wasmtime::Trap; +use wasmtime::TrapCode; + +pub struct V8Engine { + isolate: Rc>, +} + +impl V8Engine { + pub fn new(config: &ModuleConfig) -> Result { + static INIT: Once = Once::new(); + + INIT.call_once(|| { + let platform = v8::new_default_platform(0, false).make_shared(); + v8::V8::initialize_platform(platform); + v8::V8::initialize(); + }); + + // FIXME: reference types are disabled for now as we seemingly keep finding + // a segfault in v8. This is found relatively quickly locally and keeps + // getting found by oss-fuzz and currently we don't think that there's + // really much we can do about it. For the time being disable reference + // types entirely. An example bug is + // https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=45662 + if config.config.reference_types_enabled { + bail!("reference types are buggy in v8"); + } + + if config.config.memory64_enabled { + bail!("memory64 not enabled by default in v8"); + } + + Ok(Self { + isolate: Rc::new(RefCell::new(v8::Isolate::new(Default::default()))), + }) + } +} + +impl DiffEngine for V8Engine { + fn name(&self) -> &'static str { + "v8" + } + + fn instantiate(&mut self, wasm: &[u8]) -> Result> { + // Setup a new `Context` in which we'll be creating this instance and + // executing code. + let mut isolate = self.isolate.borrow_mut(); + let isolate = &mut **isolate; + let mut scope = v8::HandleScope::new(isolate); + let context = v8::Context::new(&mut scope); + let global = context.global(&mut scope); + let mut scope = v8::ContextScope::new(&mut scope, context); + + // Move the `wasm` into JS and then invoke `new WebAssembly.Module`. + let buf = v8::ArrayBuffer::new_backing_store_from_boxed_slice(wasm.into()); + let buf = v8::SharedRef::from(buf); + let name = v8::String::new(&mut scope, "WASM_BINARY").unwrap(); + let buf = v8::ArrayBuffer::with_backing_store(&mut scope, &buf); + global.set(&mut scope, name.into(), buf.into()); + let module = eval(&mut scope, "new WebAssembly.Module(WASM_BINARY)").unwrap(); + let name = v8::String::new(&mut scope, "WASM_MODULE").unwrap(); + global.set(&mut scope, name.into(), module); + + // Using our `WASM_MODULE` run instantiation. Note that it's guaranteed + // that nothing is imported into differentially-executed modules so + // this is expected to only take the module argument. + let instance = eval(&mut scope, "new WebAssembly.Instance(WASM_MODULE)")?; + + Ok(Box::new(V8Instance { + isolate: self.isolate.clone(), + context: v8::Global::new(&mut scope, context), + instance: v8::Global::new(&mut scope, instance), + })) + } + + fn assert_error_match(&self, wasmtime: &Trap, err: Error) { + let v8 = err.to_string(); + let wasmtime_msg = wasmtime.to_string(); + let verify_wasmtime = |msg: &str| { + assert!(wasmtime_msg.contains(msg), "{}\n!=\n{}", wasmtime_msg, v8); + }; + let verify_v8 = |msg: &[&str]| { + assert!( + msg.iter().any(|msg| v8.contains(msg)), + "{:?}\n\t!=\n{}", + wasmtime_msg, + v8 + ); + }; + match wasmtime.trap_code() { + Some(TrapCode::MemoryOutOfBounds) => { + return verify_v8(&[ + "memory access out of bounds", + "data segment is out of bounds", + ]) + } + Some(TrapCode::UnreachableCodeReached) => { + return verify_v8(&[ + "unreachable", + // All the wasms we test use wasm-smith's + // `ensure_termination` option which will `unreachable` when + // "fuel" runs out within the wasm module itself. This + // sometimes manifests as a call stack size exceeded in v8, + // however, since v8 sometimes has different limits on the + // call-stack especially when it's run multiple times. To + // get these error messages to line up allow v8 to say the + // call stack size exceeded when wasmtime says we hit + // unreachable. + "Maximum call stack size exceeded", + ]); + } + Some(TrapCode::IntegerDivisionByZero) => { + return verify_v8(&["divide by zero", "remainder by zero"]) + } + Some(TrapCode::StackOverflow) => { + return verify_v8(&[ + "call stack size exceeded", + // Similar to the above comment in `UnreachableCodeReached` + // if wasmtime hits a stack overflow but v8 ran all the way + // to when the `unreachable` instruction was hit then that's + // ok. This just means that wasmtime either has less optimal + // codegen or different limits on the stack than v8 does, + // which isn't an issue per-se. + "unreachable", + ]); + } + Some(TrapCode::IndirectCallToNull) => return verify_v8(&["null function"]), + Some(TrapCode::TableOutOfBounds) => { + return verify_v8(&[ + "table initializer is out of bounds", + "table index is out of bounds", + ]) + } + Some(TrapCode::BadSignature) => return verify_v8(&["function signature mismatch"]), + Some(TrapCode::IntegerOverflow) | Some(TrapCode::BadConversionToInteger) => { + return verify_v8(&[ + "float unrepresentable in integer range", + "divide result unrepresentable", + ]) + } + other => log::debug!("unknown code {:?}", other), + } + + verify_wasmtime("not possibly present in an error, just panic please"); + } +} + +struct V8Instance { + isolate: Rc>, + context: v8::Global, + instance: v8::Global, +} + +impl DiffInstance for V8Instance { + fn name(&self) -> &'static str { + "v8" + } + + fn evaluate( + &mut self, + function_name: &str, + arguments: &[DiffValue], + result_tys: &[DiffValueType], + ) -> Result>> { + let mut isolate = self.isolate.borrow_mut(); + let isolate = &mut **isolate; + let mut scope = v8::HandleScope::new(isolate); + let context = v8::Local::new(&mut scope, &self.context); + let global = context.global(&mut scope); + let mut scope = v8::ContextScope::new(&mut scope, context); + + // See https://webassembly.github.io/spec/js-api/index.html#tojsvalue + // for how the Wasm-to-JS conversions are done. + let mut params = Vec::new(); + for arg in arguments { + params.push(match *arg { + DiffValue::I32(n) => v8::Number::new(&mut scope, n.into()).into(), + DiffValue::F32(n) => v8::Number::new(&mut scope, f32::from_bits(n).into()).into(), + DiffValue::F64(n) => v8::Number::new(&mut scope, f64::from_bits(n)).into(), + DiffValue::I64(n) => v8::BigInt::new_from_i64(&mut scope, n).into(), + DiffValue::FuncRef { null } | DiffValue::ExternRef { null } => { + assert!(null); + v8::null(&mut scope).into() + } + // JS doesn't support v128 parameters + DiffValue::V128(_) => return Ok(None), + }); + } + // JS doesn't support v128 return values + for ty in result_tys { + if let DiffValueType::V128 = ty { + return Ok(None); + } + } + + let name = v8::String::new(&mut scope, "WASM_INSTANCE").unwrap(); + let instance = v8::Local::new(&mut scope, &self.instance); + global.set(&mut scope, name.into(), instance); + let name = v8::String::new(&mut scope, "EXPORT_NAME").unwrap(); + let func_name = v8::String::new(&mut scope, function_name).unwrap(); + global.set(&mut scope, name.into(), func_name.into()); + let name = v8::String::new(&mut scope, "ARGS").unwrap(); + let params = v8::Array::new_with_elements(&mut scope, ¶ms); + global.set(&mut scope, name.into(), params.into()); + let v8_vals = eval(&mut scope, "WASM_INSTANCE.exports[EXPORT_NAME](...ARGS)")?; + + let mut results = Vec::new(); + match result_tys.len() { + 0 => assert!(v8_vals.is_undefined()), + 1 => results.push(get_diff_value(&v8_vals, result_tys[0], &mut scope)), + _ => { + let array = v8::Local::<'_, v8::Array>::try_from(v8_vals).unwrap(); + for (i, ty) in result_tys.iter().enumerate() { + let v8 = array.get_index(&mut scope, i as u32).unwrap(); + results.push(get_diff_value(&v8, *ty, &mut scope)); + } + } + } + Ok(Some(results)) + } + + fn get_global(&mut self, global_name: &str, ty: DiffValueType) -> Option { + if let DiffValueType::V128 = ty { + return None; + } + let mut isolate = self.isolate.borrow_mut(); + let mut scope = v8::HandleScope::new(&mut *isolate); + let context = v8::Local::new(&mut scope, &self.context); + let global = context.global(&mut scope); + let mut scope = v8::ContextScope::new(&mut scope, context); + + let name = v8::String::new(&mut scope, "GLOBAL_NAME").unwrap(); + let memory_name = v8::String::new(&mut scope, global_name).unwrap(); + global.set(&mut scope, name.into(), memory_name.into()); + let val = eval(&mut scope, "WASM_INSTANCE.exports[GLOBAL_NAME].value").unwrap(); + Some(get_diff_value(&val, ty, &mut scope)) + } + + fn get_memory(&mut self, memory_name: &str, shared: bool) -> Option> { + let mut isolate = self.isolate.borrow_mut(); + let mut scope = v8::HandleScope::new(&mut *isolate); + let context = v8::Local::new(&mut scope, &self.context); + let global = context.global(&mut scope); + let mut scope = v8::ContextScope::new(&mut scope, context); + + let name = v8::String::new(&mut scope, "MEMORY_NAME").unwrap(); + let memory_name = v8::String::new(&mut scope, memory_name).unwrap(); + global.set(&mut scope, name.into(), memory_name.into()); + let v8 = eval(&mut scope, "WASM_INSTANCE.exports[MEMORY_NAME].buffer").unwrap(); + let v8_data = if shared { + v8::Local::<'_, v8::SharedArrayBuffer>::try_from(v8) + .unwrap() + .get_backing_store() + } else { + v8::Local::<'_, v8::ArrayBuffer>::try_from(v8) + .unwrap() + .get_backing_store() + }; + + Some(v8_data.iter().map(|i| i.get()).collect()) + } +} + +/// Evaluates the JS `code` within `scope`, returning either the result of the +/// computation or the stringified exception if one happened. +fn eval<'s>(scope: &mut v8::HandleScope<'s>, code: &str) -> Result> { + let mut tc = v8::TryCatch::new(scope); + let mut scope = v8::EscapableHandleScope::new(&mut tc); + let source = v8::String::new(&mut scope, code).unwrap(); + let script = v8::Script::compile(&mut scope, source, None).unwrap(); + match script.run(&mut scope) { + Some(val) => Ok(scope.escape(val)), + None => { + drop(scope); + assert!(tc.has_caught()); + bail!( + "{}", + tc.message() + .unwrap() + .get(&mut tc) + .to_rust_string_lossy(&mut tc) + ) + } + } +} + +fn get_diff_value( + val: &v8::Local<'_, v8::Value>, + ty: DiffValueType, + scope: &mut v8::HandleScope<'_>, +) -> DiffValue { + match ty { + DiffValueType::I32 => DiffValue::I32(val.to_int32(scope).unwrap().value() as i32), + DiffValueType::I64 => { + let (val, todo) = val.to_big_int(scope).unwrap().i64_value(); + assert!(todo); + DiffValue::I64(val) + } + DiffValueType::F32 => { + DiffValue::F32((val.to_number(scope).unwrap().value() as f32).to_bits()) + } + DiffValueType::F64 => DiffValue::F64(val.to_number(scope).unwrap().value().to_bits()), + DiffValueType::FuncRef => DiffValue::FuncRef { + null: val.is_null(), + }, + DiffValueType::ExternRef => DiffValue::ExternRef { + null: val.is_null(), + }, + DiffValueType::V128 => unreachable!(), + } +} + +#[test] +fn smoke() { + crate::oracles::engine::smoke_test_engine(|config| V8Engine::new(&config.module_config)) +} diff --git a/crates/fuzzing/src/oracles/diff_wasmi.rs b/crates/fuzzing/src/oracles/diff_wasmi.rs index 09cb197585..efc9160c0b 100644 --- a/crates/fuzzing/src/oracles/diff_wasmi.rs +++ b/crates/fuzzing/src/oracles/diff_wasmi.rs @@ -1,9 +1,9 @@ //! Evaluate an exported Wasm function using the wasmi interpreter. -use crate::generators::{DiffValue, ModuleConfig}; +use crate::generators::{DiffValue, DiffValueType, ModuleConfig}; use crate::oracles::engine::{DiffEngine, DiffInstance}; -use anyhow::{bail, Context, Result}; -use std::hash::Hash; +use anyhow::{bail, Context, Error, Result}; +use wasmtime::Trap; /// A wrapper for `wasmi` as a [`DiffEngine`]. pub struct WasmiEngine; @@ -11,7 +11,7 @@ pub struct WasmiEngine; impl WasmiEngine { /// Build a new [`WasmiEngine`] but only if the configuration does not rely /// on features that `wasmi` does not support. - pub fn new(config: &ModuleConfig) -> Result> { + pub fn new(config: &ModuleConfig) -> Result { if config.config.reference_types_enabled { bail!("wasmi does not support reference types") } @@ -27,7 +27,16 @@ impl WasmiEngine { if config.config.sign_extension_enabled { bail!("wasmi does not support sign-extension") } - Ok(Box::new(Self)) + if config.config.memory64_enabled { + bail!("wasmi does not support memory64"); + } + if config.config.bulk_memory_enabled { + bail!("wasmi does not support bulk memory"); + } + if config.config.threads_enabled { + bail!("wasmi does not support threads"); + } + Ok(Self) } } @@ -36,17 +45,17 @@ impl DiffEngine for WasmiEngine { "wasmi" } - fn instantiate(&self, wasm: &[u8]) -> Result> { + fn instantiate(&mut self, wasm: &[u8]) -> Result> { let module = wasmi::Module::from_buffer(wasm).context("unable to validate Wasm module")?; let instance = wasmi::ModuleInstance::new(&module, &wasmi::ImportsBuilder::default()) .context("unable to instantiate module in wasmi")?; let instance = instance.assert_no_start(); - let exports = list_export_names(wasm); - Ok(Box::new(WasmiInstance { - module, - exports, - instance, - })) + Ok(Box::new(WasmiInstance { module, instance })) + } + + fn assert_error_match(&self, trap: &Trap, err: Error) { + // TODO: should implement this for `wasmi` + drop((trap, err)); } } @@ -55,9 +64,6 @@ struct WasmiInstance { #[allow(dead_code)] // reason = "the module must live as long as its reference" module: wasmi::Module, instance: wasmi::ModuleRef, - /// `wasmi`'s instances have no way of listing their exports so, in order to - /// properly hash the instance, we keep track of the export names. - exports: Vec, } impl DiffInstance for WasmiInstance { @@ -65,7 +71,12 @@ impl DiffInstance for WasmiInstance { "wasmi" } - fn evaluate(&mut self, function_name: &str, arguments: &[DiffValue]) -> Result> { + fn evaluate( + &mut self, + function_name: &str, + arguments: &[DiffValue], + _results: &[DiffValueType], + ) -> Result>> { let arguments: Vec<_> = arguments.iter().map(wasmi::RuntimeValue::from).collect(); let export = self .instance @@ -77,60 +88,34 @@ impl DiffInstance for WasmiInstance { let function = export.as_func().context("wasmi export is not a function")?; let result = wasmi::FuncInstance::invoke(&function, &arguments, &mut wasmi::NopExternals) .context("failed while invoking function in wasmi")?; - Ok(if let Some(result) = result { + Ok(Some(if let Some(result) = result { vec![result.into()] } else { vec![] - }) - } - - fn is_hashable(&self) -> bool { - true + })) } - fn hash(&mut self, state: &mut std::collections::hash_map::DefaultHasher) -> Result<()> { - for export_name in &self.exports { - if let Some(export) = self.instance.export_by_name(export_name) { - match export { - wasmi::ExternVal::Func(_) => {} - wasmi::ExternVal::Table(_) => {} // TODO eventually we can hash whether the values are null or non-null. - wasmi::ExternVal::Memory(m) => { - // `wasmi` memory may be stored non-contiguously; copy - // it out to a contiguous chunk. - let mut buffer: Vec = vec![0; m.current_size().0 * 65536]; - m.get_into(0, &mut buffer[..]) - .expect("can access wasmi memory"); - buffer.hash(state) - } - wasmi::ExternVal::Global(g) => { - let val: DiffValue = g.get().into(); - val.hash(state); - } - } - } else { - panic!("unable to find export: {}", export_name) - } + fn get_global(&mut self, name: &str, _ty: DiffValueType) -> Option { + match self.instance.export_by_name(name) { + Some(wasmi::ExternVal::Global(g)) => Some(g.get().into()), + _ => unreachable!(), } - Ok(()) } -} -/// List the names of all exported items in a binary Wasm module. -fn list_export_names(wasm: &[u8]) -> Vec { - let mut exports = vec![]; - for payload in wasmparser::Parser::new(0).parse_all(&wasm) { - match payload.unwrap() { - wasmparser::Payload::ExportSection(s) => { - for export in s { - exports.push(export.unwrap().name.to_string()); - } - } - _ => { - // Ignore any other sections. + fn get_memory(&mut self, name: &str, shared: bool) -> Option> { + assert!(!shared); + match self.instance.export_by_name(name) { + Some(wasmi::ExternVal::Memory(m)) => { + // `wasmi` memory may be stored non-contiguously; copy + // it out to a contiguous chunk. + let mut buffer: Vec = vec![0; m.current_size().0 * 65536]; + m.get_into(0, &mut buffer[..]) + .expect("can access wasmi memory"); + Some(buffer) } + _ => unreachable!(), } } - exports } impl From<&DiffValue> for wasmi::RuntimeValue { @@ -141,7 +126,9 @@ impl From<&DiffValue> for wasmi::RuntimeValue { DiffValue::I64(n) => I64(n), DiffValue::F32(n) => F32(wasmi::nan_preserving_float::F32::from_bits(n)), DiffValue::F64(n) => F64(wasmi::nan_preserving_float::F64::from_bits(n)), - DiffValue::V128(_) => unimplemented!(), + DiffValue::V128(_) | DiffValue::FuncRef { .. } | DiffValue::ExternRef { .. } => { + unimplemented!() + } } } } @@ -158,21 +145,7 @@ impl Into for wasmi::RuntimeValue { } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_list_export_names() { - let wat = r#"(module - (func (export "a") (result i32) (i32.const 42)) - (global (export "b") (mut i32) (i32.const 42)) - (memory (export "c") 1 2 shared) - )"#; - let wasm = wat::parse_str(wat).unwrap(); - assert_eq!( - list_export_names(&wasm), - vec!["a".to_string(), "b".to_string(), "c".to_string()], - ); - } +#[test] +fn smoke() { + crate::oracles::engine::smoke_test_engine(|config| WasmiEngine::new(&config.module_config)) } diff --git a/crates/fuzzing/src/oracles/diff_wasmtime.rs b/crates/fuzzing/src/oracles/diff_wasmtime.rs index 559e837f29..3c86138f9e 100644 --- a/crates/fuzzing/src/oracles/diff_wasmtime.rs +++ b/crates/fuzzing/src/oracles/diff_wasmtime.rs @@ -1,12 +1,11 @@ //! Evaluate an exported Wasm function using Wasmtime. -use crate::generators::{self, DiffValue}; +use crate::generators::{self, DiffValue, DiffValueType}; +use crate::oracles::dummy; use crate::oracles::engine::DiffInstance; -use crate::oracles::{compile_module, engine::DiffEngine, instantiate_with_dummy, StoreLimits}; -use anyhow::{Context, Result}; -use std::hash::Hash; -use std::slice; -use wasmtime::{AsContextMut, Extern, FuncType, Instance, Module, Store, Val}; +use crate::oracles::{compile_module, engine::DiffEngine, StoreLimits}; +use anyhow::{Context, Error, Result}; +use wasmtime::{Extern, FuncType, Instance, Module, Store, Trap, Val}; /// A wrapper for using Wasmtime as a [`DiffEngine`]. pub struct WasmtimeEngine { @@ -18,10 +17,8 @@ impl WasmtimeEngine { /// later. Ideally the store and engine could be built here but /// `compile_module` takes a [`generators::Config`]; TODO re-factor this if /// that ever changes. - pub fn new(config: &generators::Config) -> Result> { - Ok(Box::new(Self { - config: config.clone(), - })) + pub fn new(config: generators::Config) -> Result { + Ok(Self { config }) } } @@ -30,12 +27,23 @@ impl DiffEngine for WasmtimeEngine { "wasmtime" } - fn instantiate(&self, wasm: &[u8]) -> Result> { + fn instantiate(&mut self, wasm: &[u8]) -> Result> { let store = self.config.to_store(); let module = compile_module(store.engine(), wasm, true, &self.config).unwrap(); let instance = WasmtimeInstance::new(store, module)?; Ok(Box::new(instance)) } + + fn assert_error_match(&self, trap: &Trap, err: Error) { + let trap2 = err.downcast::().unwrap(); + assert_eq!( + trap.trap_code(), + trap2.trap_code(), + "{}\nis not equal to\n{}", + trap, + trap2 + ); + } } /// A wrapper around a Wasmtime instance. @@ -50,7 +58,8 @@ pub struct WasmtimeInstance { impl WasmtimeInstance { /// Instantiate a new Wasmtime instance. pub fn new(mut store: Store, module: Module) -> Result { - let instance = instantiate_with_dummy(&mut store, &module) + let instance = dummy::dummy_linker(&mut store, &module) + .and_then(|l| l.instantiate(&mut store, &module)) .context("unable to instantiate module in wasmtime")?; Ok(Self { store, instance }) } @@ -73,6 +82,44 @@ impl WasmtimeInstance { .map(|(n, f)| (n, f.ty(&self.store))) .collect() } + + /// Returns the list of globals and their types exported from this instance. + pub fn exported_globals(&mut self) -> Vec<(String, DiffValueType)> { + let globals = self + .instance + .exports(&mut self.store) + .filter_map(|e| { + let name = e.name(); + e.into_global().map(|g| (name.to_string(), g)) + }) + .collect::>(); + + globals + .into_iter() + .map(|(name, global)| { + ( + name, + global.ty(&self.store).content().clone().try_into().unwrap(), + ) + }) + .collect() + } + + /// Returns the list of exported memories and whether or not it's a shared + /// memory. + pub fn exported_memories(&mut self) -> Vec<(String, bool)> { + self.instance + .exports(&mut self.store) + .filter_map(|e| { + let name = e.name(); + match e.into_extern() { + Extern::Memory(_) => Some((name.to_string(), false)), + Extern::SharedMemory(_) => Some((name.to_string(), true)), + _ => None, + } + }) + .collect() + } } impl DiffInstance for WasmtimeInstance { @@ -80,7 +127,12 @@ impl DiffInstance for WasmtimeInstance { "wasmtime" } - fn evaluate(&mut self, function_name: &str, arguments: &[DiffValue]) -> Result> { + fn evaluate( + &mut self, + function_name: &str, + arguments: &[DiffValue], + _results: &[DiffValueType], + ) -> Result>> { let arguments: Vec<_> = arguments.iter().map(Val::from).collect(); let function = self @@ -92,43 +144,34 @@ impl DiffInstance for WasmtimeInstance { function.call(&mut self.store, &arguments, &mut results)?; let results = results.into_iter().map(Val::into).collect(); - Ok(results) + Ok(Some(results)) } - fn is_hashable(&self) -> bool { - true + fn get_global(&mut self, name: &str, _ty: DiffValueType) -> Option { + Some( + self.instance + .get_global(&mut self.store, name) + .unwrap() + .get(&mut self.store) + .into(), + ) } - fn hash(&mut self, state: &mut std::collections::hash_map::DefaultHasher) -> Result<()> { - let exports: Vec<_> = self - .instance - .exports(self.store.as_context_mut()) - .map(|e| e.into_extern()) - .collect(); - for e in exports { - match e { - Extern::Global(g) => { - let val: DiffValue = g.get(&mut self.store).into(); - val.hash(state) - } - Extern::Memory(m) => { - let data = m.data(&mut self.store); - data.hash(state) - } - Extern::SharedMemory(m) => { - let data = unsafe { slice::from_raw_parts(m.data() as *mut u8, m.data_size()) }; - data.hash(state) - } - Extern::Table(_) => { - // TODO: it's unclear whether it is worth it to iterate - // through the table and hash the values. - } - Extern::Func(_) => { - // Note: no need to hash exported functions. - } - } - } - Ok(()) + fn get_memory(&mut self, name: &str, shared: bool) -> Option> { + Some(if shared { + let data = self + .instance + .get_shared_memory(&mut self.store, name) + .unwrap() + .data(); + unsafe { (*data).to_vec() } + } else { + self.instance + .get_memory(&mut self.store, name) + .unwrap() + .data(&self.store) + .to_vec() + }) } } @@ -140,6 +183,14 @@ impl From<&DiffValue> for Val { DiffValue::F32(n) => Val::F32(n), DiffValue::F64(n) => Val::F64(n), DiffValue::V128(n) => Val::V128(n), + DiffValue::FuncRef { null } => { + assert!(null); + Val::FuncRef(None) + } + DiffValue::ExternRef { null } => { + assert!(null); + Val::ExternRef(None) + } } } } @@ -152,8 +203,13 @@ impl Into for Val { Val::F32(n) => DiffValue::F32(n), Val::F64(n) => DiffValue::F64(n), Val::V128(n) => DiffValue::V128(n), - Val::FuncRef(_) => unimplemented!(), - Val::ExternRef(_) => unimplemented!(), + Val::FuncRef(f) => DiffValue::FuncRef { null: f.is_none() }, + Val::ExternRef(e) => DiffValue::ExternRef { null: e.is_none() }, } } } + +#[test] +fn smoke() { + crate::oracles::engine::smoke_test_engine(|config| WasmtimeEngine::new(config)) +} diff --git a/crates/fuzzing/src/oracles/engine.rs b/crates/fuzzing/src/oracles/engine.rs index 4c7c274ce4..686858ea66 100644 --- a/crates/fuzzing/src/oracles/engine.rs +++ b/crates/fuzzing/src/oracles/engine.rs @@ -1,9 +1,10 @@ //! Define the interface for differential evaluation of Wasm functions. -use crate::generators::{Config, DiffValue}; +use crate::generators::{Config, DiffValue, DiffValueType, WasmtimeConfig}; use crate::oracles::{diff_wasmi::WasmiEngine, diff_wasmtime::WasmtimeEngine}; +use anyhow::Error; use arbitrary::Unstructured; -use std::collections::hash_map::DefaultHasher; +use wasmtime::Trap; /// Pick one of the engines implemented in this module that is compatible with /// the Wasm features passed in `features` and, when fuzzing Wasmtime against @@ -14,31 +15,36 @@ pub fn choose( ) -> arbitrary::Result> { // Filter out any engines that cannot match the given configuration. let mut engines: Vec> = vec![]; - let mut config: Config = u.arbitrary()?; // TODO change to WasmtimeConfig - config.make_compatible_with(&existing_config); - if let Result::Ok(e) = WasmtimeEngine::new(&config) { - engines.push(e) + let mut config2: WasmtimeConfig = u.arbitrary()?; // TODO change to WasmtimeConfig + config2.make_compatible_with(&existing_config.wasmtime); + let config2 = Config { + wasmtime: config2, + module_config: existing_config.module_config.clone(), + }; + if let Result::Ok(e) = WasmtimeEngine::new(config2) { + engines.push(Box::new(e)) } if let Result::Ok(e) = WasmiEngine::new(&existing_config.module_config) { - engines.push(e) + engines.push(Box::new(e)) } #[cfg(feature = "fuzz-spec-interpreter")] if let Result::Ok(e) = crate::oracles::diff_spec::SpecInterpreter::new(&existing_config.module_config) { - engines.push(e) + engines.push(Box::new(e)) } - - // Choose one of the remaining engines. - if !engines.is_empty() { - let index: usize = u.int_in_range(0..=engines.len() - 1)?; - let engine = engines.swap_remove(index); - log::debug!("selected engine: {}", engine.name()); - Ok(engine) - } else { - panic!("no engines to pick from"); - // Err(arbitrary::Error::EmptyChoose) + #[cfg(not(any(windows, target_arch = "s390x")))] + if let Result::Ok(e) = crate::oracles::diff_v8::V8Engine::new(&existing_config.module_config) { + engines.push(Box::new(e)) } + + // Use the input of the fuzzer to pick an engine that we'll be fuzzing + // Wasmtime against. + assert!(!engines.is_empty()); + let index: usize = u.int_in_range(0..=engines.len() - 1)?; + let engine = engines.swap_remove(index); + log::debug!("selected engine: {}", engine.name()); + Ok(engine) } /// Provide a way to instantiate Wasm modules. @@ -47,7 +53,11 @@ pub trait DiffEngine { fn name(&self) -> &'static str; /// Create a new instance with the given engine. - fn instantiate(&self, wasm: &[u8]) -> anyhow::Result>; + fn instantiate(&mut self, wasm: &[u8]) -> anyhow::Result>; + + /// Tests that the wasmtime-originating `trap` matches the error this engine + /// generated. + fn assert_error_match(&self, trap: &Trap, err: Error); } /// Provide a way to evaluate Wasm functions--a Wasm instance implemented by a @@ -57,19 +67,111 @@ pub trait DiffInstance { fn name(&self) -> &'static str; /// Evaluate an exported function with the given values. + /// + /// Any error, such as a trap, should be returned through an `Err`. If this + /// engine cannot invoke the function signature then `None` should be + /// returned and this invocation will be skipped. fn evaluate( &mut self, function_name: &str, arguments: &[DiffValue], - ) -> anyhow::Result>; + results: &[DiffValueType], + ) -> anyhow::Result>>; - /// Check if instances of this kind are actually hashable--not all engines - /// support this. - fn is_hashable(&self) -> bool; + /// Attempts to return the value of the specified global, returning `None` + /// if this engine doesn't support retrieving globals at this time. + fn get_global(&mut self, name: &str, ty: DiffValueType) -> Option; - /// If the instance `is_hashable()`, this method will try to hash the - /// following exported items in the instance: globals, memory. - /// - /// TODO allow more types of hashers. - fn hash(&mut self, state: &mut DefaultHasher) -> anyhow::Result<()>; + /// Same as `get_global` but for memory. + fn get_memory(&mut self, name: &str, shared: bool) -> Option>; +} + +/// Initialize any global state associated with runtimes that may be +/// differentially executed against. +pub fn setup_engine_runtimes() { + #[cfg(feature = "fuzz-spec-interpreter")] + crate::oracles::diff_spec::setup_ocaml_runtime(); +} + +#[cfg(test)] +pub fn smoke_test_engine(mk_engine: impl Fn(Config) -> anyhow::Result) +where + T: DiffEngine, +{ + use arbitrary::Arbitrary; + use rand::prelude::*; + + let mut rng = SmallRng::seed_from_u64(0); + let mut buf = vec![0; 2048]; + let n = 100; + for _ in 0..n { + rng.fill_bytes(&mut buf); + let u = Unstructured::new(&buf); + let mut config = match Config::arbitrary_take_rest(u) { + Ok(config) => config, + Err(_) => continue, + }; + // This will ensure that wasmtime, which uses this configuration + // settings, can guaranteed instantiate a module. + config.set_differential_config(); + + // Configure settings to ensure that any filters in engine constructors + // try not to filter out this `Config`. + config.module_config.config.reference_types_enabled = false; + config.module_config.config.bulk_memory_enabled = false; + config.module_config.config.memory64_enabled = false; + config.module_config.config.threads_enabled = false; + config.module_config.config.simd_enabled = false; + config.module_config.config.min_funcs = 1; + config.module_config.config.max_funcs = 1; + config.module_config.config.min_tables = 0; + config.module_config.config.max_tables = 0; + + let mut engine = match mk_engine(config) { + Ok(engine) => engine, + Err(e) => { + println!("skip {:?}", e); + continue; + } + }; + + let wasm = wat::parse_str( + r#" + (module + (func (export "add") (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.add) + + (global (export "global") i32 i32.const 1) + (memory (export "memory") 1) + ) + "#, + ) + .unwrap(); + let mut instance = engine.instantiate(&wasm).unwrap(); + let results = instance + .evaluate( + "add", + &[DiffValue::I32(1), DiffValue::I32(2)], + &[DiffValueType::I32], + ) + .unwrap(); + assert_eq!(results, Some(vec![DiffValue::I32(3)])); + + if let Some(val) = instance.get_global("global", DiffValueType::I32) { + assert_eq!(val, DiffValue::I32(1)); + } + + if let Some(val) = instance.get_memory("memory", false) { + assert_eq!(val.len(), 65536); + for i in val.iter() { + assert_eq!(*i, 0); + } + } + + return; + } + + panic!("after {n} runs nothing ever ran, something is probably wrong"); } diff --git a/crates/fuzzing/src/oracles/v8.rs b/crates/fuzzing/src/oracles/v8.rs deleted file mode 100644 index 6621511ec4..0000000000 --- a/crates/fuzzing/src/oracles/v8.rs +++ /dev/null @@ -1,372 +0,0 @@ -use super::{compile_module, log_wasm}; -use crate::generators; -use std::convert::TryFrom; -use std::sync::Once; -use wasmtime::*; - -/// Performs differential execution between Wasmtime and V8. -/// -/// This will instantiate the `wasm` provided, which should have no host -/// imports, and then run it in Wasmtime with the `config` specified and V8 with -/// default settings. The first export is executed and if memory is exported -/// it's compared as well. -/// -/// Note that it's the caller's responsibility to ensure that the `wasm` -/// doesn't infinitely loop as no protections are done in v8 to prevent this -/// from happening. -pub fn differential_v8_execution(wasm: &[u8], config: &crate::generators::Config) -> Option<()> { - // Wasmtime setup - log_wasm(wasm); - let (wasmtime_module, mut wasmtime_store) = differential_store(wasm, config); - let wasmtime_module = wasmtime_module?; - log::trace!("compiled module with wasmtime"); - - // V8 setup - let mut isolate = isolate(); - let mut scope = v8::HandleScope::new(&mut *isolate); - let context = v8::Context::new(&mut scope); - let global = context.global(&mut scope); - let mut scope = v8::ContextScope::new(&mut scope, context); - - // V8: compile module - let buf = v8::ArrayBuffer::new_backing_store_from_boxed_slice(wasm.into()); - let buf = v8::SharedRef::from(buf); - let name = v8::String::new(&mut scope, "WASM_BINARY").unwrap(); - let buf = v8::ArrayBuffer::with_backing_store(&mut scope, &buf); - global.set(&mut scope, name.into(), buf.into()); - let v8_module = eval(&mut scope, "new WebAssembly.Module(WASM_BINARY)").unwrap(); - let name = v8::String::new(&mut scope, "WASM_MODULE").unwrap(); - global.set(&mut scope, name.into(), v8_module); - log::trace!("compiled module with v8"); - - // Wasmtime: instantiate - let wasmtime_instance = wasmtime::Instance::new(&mut wasmtime_store, &wasmtime_module, &[]); - log::trace!("instantiated with wasmtime"); - - // V8: instantiate - let v8_instance = eval(&mut scope, "new WebAssembly.Instance(WASM_MODULE)"); - log::trace!("instantiated with v8"); - - // Verify V8 and wasmtime match - let (wasmtime_instance, v8_instance) = match (wasmtime_instance, v8_instance) { - (Ok(i1), Ok(i2)) => (i1, i2), - (Ok(_), Err(msg)) => { - panic!("wasmtime succeeded at instantiation, v8 failed: {}", msg) - } - (Err(err), Ok(_)) => { - panic!("v8 succeeded at instantiation, wasmtime failed: {:?}", err) - } - (Err(err), Err(msg)) => { - log::trace!("instantiations failed"); - assert_error_matches(&err, &msg); - return None; - } - }; - log::trace!("instantiations were successful"); - - let (func, ty) = first_exported_function(&wasmtime_module)?; - - // not supported yet in V8 - if ty.params().chain(ty.results()).any(|t| t == ValType::V128) { - log::trace!("exported function uses v128, skipping"); - return None; - } - - let mut wasmtime_params = Vec::new(); - let mut v8_params = Vec::new(); - for param in ty.params() { - wasmtime_params.push(match param { - ValType::I32 => Val::I32(0), - ValType::I64 => Val::I64(0), - ValType::F32 => Val::F32(0), - ValType::F64 => Val::F64(0), - ValType::FuncRef => Val::FuncRef(None), - ValType::ExternRef => Val::ExternRef(None), - _ => unimplemented!(), - }); - // See https://webassembly.github.io/spec/js-api/index.html#tojsvalue - // for how the Wasm-to-JS conversions are done. - v8_params.push(match param { - ValType::I32 | ValType::F32 | ValType::F64 => v8::Number::new(&mut scope, 0.0).into(), - ValType::I64 => v8::BigInt::new_from_i64(&mut scope, 0).into(), - ValType::FuncRef => v8::null(&mut scope).into(), - ValType::ExternRef => v8::null(&mut scope).into(), - _ => unimplemented!(), - }); - } - - // Wasmtime: call the first exported func - let wasmtime_main = wasmtime_instance - .get_func(&mut wasmtime_store, func) - .expect("function export is present"); - let mut wasmtime_vals = vec![Val::I32(0); ty.results().len()]; - let wasmtime_result = - wasmtime_main.call(&mut wasmtime_store, &wasmtime_params, &mut wasmtime_vals); - log::trace!("finished wasmtime invocation"); - - // V8: call the first exported func - let name = v8::String::new(&mut scope, "WASM_INSTANCE").unwrap(); - global.set(&mut scope, name.into(), v8_instance); - let name = v8::String::new(&mut scope, "EXPORT_NAME").unwrap(); - let func_name = v8::String::new(&mut scope, func).unwrap(); - global.set(&mut scope, name.into(), func_name.into()); - let name = v8::String::new(&mut scope, "ARGS").unwrap(); - let v8_params = v8::Array::new_with_elements(&mut scope, &v8_params); - global.set(&mut scope, name.into(), v8_params.into()); - let v8_vals = eval( - &mut scope, - &format!("WASM_INSTANCE.exports[EXPORT_NAME](...ARGS)"), - ); - log::trace!("finished v8 invocation"); - - // Verify V8 and wasmtime match - match (wasmtime_result, v8_vals) { - (Ok(()), Ok(v8)) => { - log::trace!("both executed successfully"); - match wasmtime_vals.len() { - 0 => assert!(v8.is_undefined()), - 1 => assert_val_match(&wasmtime_vals[0], &v8, &mut scope), - _ => { - let array = v8::Local::<'_, v8::Array>::try_from(v8).unwrap(); - for (i, wasmtime) in wasmtime_vals.iter().enumerate() { - let v8 = array.get_index(&mut scope, i as u32).unwrap(); - assert_val_match(wasmtime, &v8, &mut scope); - // .. - } - } - } - } - (Ok(()), Err(msg)) => { - panic!("wasmtime succeeded at invocation, v8 failed: {}", msg) - } - (Err(err), Ok(_)) => { - panic!("v8 succeeded at invocation, wasmtime failed: {:?}", err) - } - (Err(err), Err(msg)) => { - log::trace!("got two traps"); - assert_error_matches(&err, &msg); - return Some(()); - } - }; - - // Verify V8 and wasmtime match memories - if let Some(mem) = first_exported_memory(&wasmtime_module) { - log::trace!("comparing memories"); - let wasmtime = wasmtime_instance - .get_memory(&mut wasmtime_store, mem) - .unwrap(); - - let name = v8::String::new(&mut scope, "MEMORY_NAME").unwrap(); - let func_name = v8::String::new(&mut scope, mem).unwrap(); - global.set(&mut scope, name.into(), func_name.into()); - let v8 = eval( - &mut scope, - &format!("WASM_INSTANCE.exports[MEMORY_NAME].buffer"), - ) - .unwrap(); - let v8 = v8::Local::<'_, v8::ArrayBuffer>::try_from(v8).unwrap(); - let v8_data = v8.get_backing_store(); - let wasmtime_data = wasmtime.data(&wasmtime_store); - assert_eq!(wasmtime_data.len(), v8_data.len()); - for i in 0..v8_data.len() { - if wasmtime_data[i] != v8_data[i].get() { - panic!("memories differ"); - } - } - } - - Some(()) -} - -/// Manufactures a new V8 Isolate to run within. -fn isolate() -> v8::OwnedIsolate { - static INIT: Once = Once::new(); - - INIT.call_once(|| { - let platform = v8::new_default_platform(0, false).make_shared(); - v8::V8::initialize_platform(platform); - v8::V8::initialize(); - }); - - v8::Isolate::new(Default::default()) -} - -/// Evaluates the JS `code` within `scope`, returning either the result of the -/// computation or the stringified exception if one happened. -fn eval<'s>( - scope: &mut v8::HandleScope<'s>, - code: &str, -) -> Result, String> { - let mut tc = v8::TryCatch::new(scope); - let mut scope = v8::EscapableHandleScope::new(&mut tc); - let source = v8::String::new(&mut scope, code).unwrap(); - let script = v8::Script::compile(&mut scope, source, None).unwrap(); - match script.run(&mut scope) { - Some(val) => Ok(scope.escape(val)), - None => { - drop(scope); - assert!(tc.has_caught()); - Err(tc - .message() - .unwrap() - .get(&mut tc) - .to_rust_string_lossy(&mut tc)) - } - } -} - -/// Asserts that the wasmtime value `a` matches the v8 value `b`. -/// -/// For NaN values simply just asserts that they're both NaN. -/// -/// See https://webassembly.github.io/spec/js-api/index.html#towebassemblyvalue -/// for how the JS-to-Wasm conversions are done. -fn assert_val_match(a: &Val, b: &v8::Local<'_, v8::Value>, scope: &mut v8::HandleScope<'_>) { - match *a { - Val::I32(wasmtime) => { - assert_eq!(i64::from(wasmtime), b.to_int32(scope).unwrap().value()); - } - Val::I64(wasmtime) => { - assert_eq!((wasmtime, true), b.to_big_int(scope).unwrap().i64_value()); - } - Val::F32(wasmtime) => { - same_float( - f64::from(f32::from_bits(wasmtime)), - b.to_number(scope).unwrap().value(), - ); - } - Val::F64(wasmtime) => { - same_float( - f64::from_bits(wasmtime), - b.to_number(scope).unwrap().value(), - ); - } - - // Externref values can only come from us, the embedder, and we only - // give wasm null, so these values should always be null. - Val::ExternRef(ref wasmtime) => { - assert!(wasmtime.is_none()); - assert!(b.is_null()); - } - - // In general we can't equate function references since wasm modules can - // create references to internal functions via `func.ref`, so we don't - // equate values here. - Val::FuncRef(_) => {} - - _ => panic!("unsupported match {:?}", a), - } - - fn same_float(a: f64, b: f64) { - assert!(a == b || (a.is_nan() && b.is_nan()), "{} != {}", a, b); - } -} - -/// Attempts to assert that the `wasmtime` error matches the `v8` error string. -/// -/// This is not a precise function. This will likely need updates over time as -/// v8 and/or wasmtime changes. The goal here is to generally make sure that -/// both engines fail for basically the same reason. -fn assert_error_matches(wasmtime: &anyhow::Error, v8: &str) { - let wasmtime_msg = match wasmtime.downcast_ref::() { - Some(trap) => trap.display_reason().to_string(), - None => format!("{:?}", wasmtime), - }; - let verify_wasmtime = |msg: &str| { - assert!(wasmtime_msg.contains(msg), "{}\n!=\n{}", wasmtime_msg, v8); - }; - let verify_v8 = |msg: &[&str]| { - assert!( - msg.iter().any(|msg| v8.contains(msg)), - "{:?}\n\t!=\n{}", - wasmtime_msg, - v8 - ); - }; - if let Some(code) = wasmtime.downcast_ref::().and_then(|t| t.trap_code()) { - match code { - TrapCode::MemoryOutOfBounds => { - return verify_v8(&[ - "memory access out of bounds", - "data segment is out of bounds", - ]) - } - TrapCode::UnreachableCodeReached => { - return verify_v8(&[ - "unreachable", - // All the wasms we test use wasm-smith's - // `ensure_termination` option which will `unreachable` when - // "fuel" runs out within the wasm module itself. This - // sometimes manifests as a call stack size exceeded in v8, - // however, since v8 sometimes has different limits on the - // call-stack especially when it's run multiple times. To - // get these error messages to line up allow v8 to say the - // call stack size exceeded when wasmtime says we hit - // unreachable. - "Maximum call stack size exceeded", - ]); - } - TrapCode::IntegerDivisionByZero => { - return verify_v8(&["divide by zero", "remainder by zero"]) - } - TrapCode::StackOverflow => { - return verify_v8(&[ - "call stack size exceeded", - // Similar to the above comment in `UnreachableCodeReached` - // if wasmtime hits a stack overflow but v8 ran all the way - // to when the `unreachable` instruction was hit then that's - // ok. This just means that wasmtime either has less optimal - // codegen or different limits on the stack than v8 does, - // which isn't an issue per-se. - "unreachable", - ]); - } - TrapCode::IndirectCallToNull => return verify_v8(&["null function"]), - TrapCode::TableOutOfBounds => { - return verify_v8(&[ - "table initializer is out of bounds", - "table index is out of bounds", - ]) - } - TrapCode::BadSignature => return verify_v8(&["function signature mismatch"]), - TrapCode::IntegerOverflow | TrapCode::BadConversionToInteger => { - return verify_v8(&[ - "float unrepresentable in integer range", - "divide result unrepresentable", - ]) - } - other => log::debug!("unknown code {:?}", other), - } - } - verify_wasmtime("not possibly present in an error, just panic please"); -} - -fn differential_store( - wasm: &[u8], - fuzz_config: &generators::Config, -) -> (Option, Store) { - let store = fuzz_config.to_store(); - let module = compile_module(store.engine(), wasm, true, fuzz_config); - (module, store) -} - -// Introspect wasmtime module to find the name of the first exported function. -fn first_exported_function(module: &wasmtime::Module) -> Option<(&str, FuncType)> { - for e in module.exports() { - match e.ty() { - wasmtime::ExternType::Func(ty) => return Some((e.name(), ty)), - _ => {} - } - } - None -} - -fn first_exported_memory(module: &Module) -> Option<&str> { - for e in module.exports() { - match e.ty() { - wasmtime::ExternType::Memory(..) => return Some(e.name()), - _ => {} - } - } - None -} diff --git a/crates/fuzzing/wasm-spec-interpreter/src/lib.rs b/crates/fuzzing/wasm-spec-interpreter/src/lib.rs index 6a977f7f50..1d8008c84d 100644 --- a/crates/fuzzing/wasm-spec-interpreter/src/lib.rs +++ b/crates/fuzzing/wasm-spec-interpreter/src/lib.rs @@ -32,3 +32,7 @@ pub use without_library::*; // If the user is fuzzing`, we expect the OCaml library to have been built. #[cfg(all(fuzzing, not(feature = "has-libinterpret")))] compile_error!("The OCaml library was not built."); + +pub fn support_compiled_in() -> bool { + cfg!(feature = "has-libinterpret") +} diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 52ab67fb78..33a0181e71 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -60,12 +60,6 @@ path = "fuzz_targets/differential.rs" test = false doc = false -[[bin]] -name = "differential_v8" -path = "fuzz_targets/differential_v8.rs" -test = false -doc = false - [[bin]] name = "spectests" path = "fuzz_targets/spectests.rs" diff --git a/fuzz/README.md b/fuzz/README.md index ec2b77acab..f52c2c71b9 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -37,10 +37,8 @@ At the time of writing, we have the following fuzz targets: * `differential`: Generate a Wasm module, evaluate each exported function with random inputs, and check that Wasmtime returns the same results as a choice of another engine: the Wasm spec interpreter (see the - `wasm-spec-interpreter` crate), the `wasmi` interpreter, or Wasmtime itself - run with a different configuration. -* `differential_v8`: Generate a Wasm module and check that Wasmtime returns - the same results as V8. + `wasm-spec-interpreter` crate), the `wasmi` interpreter, V8 (through the `v8` + crate), or Wasmtime itself run with a different configuration. * `instantiate`: Generate a Wasm module and Wasmtime configuration and attempt to compile and instantiate with them. * `instantiate-many`: Generate many Wasm modules and attempt to compile and diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs index 9119981123..c80883543b 100644 --- a/fuzz/fuzz_targets/differential.rs +++ b/fuzz/fuzz_targets/differential.rs @@ -5,8 +5,8 @@ use libfuzzer_sys::fuzz_target; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering::SeqCst; use std::sync::Once; -use wasmtime_fuzzing::generators::{Config, DiffValue, SingleInstModule}; -use wasmtime_fuzzing::oracles::diff_spec; +use wasmtime::Trap; +use wasmtime_fuzzing::generators::{Config, DiffValue, DiffValueType, SingleInstModule}; use wasmtime_fuzzing::oracles::diff_wasmtime::WasmtimeInstance; use wasmtime_fuzzing::oracles::{differential, engine, log_wasm}; @@ -14,11 +14,8 @@ use wasmtime_fuzzing::oracles::{differential, engine, log_wasm}; // executed by this fuzz target. const NUM_INVOCATIONS: usize = 5; -// Keep track of how many WebAssembly modules we actually executed (i.e. ran to -// completion) versus how many were tried. -static TOTAL_INVOCATIONS: AtomicUsize = AtomicUsize::new(0); -static TOTAL_SUCCESSES: AtomicUsize = AtomicUsize::new(0); -static TOTAL_ATTEMPTED: AtomicUsize = AtomicUsize::new(0); +// Statistics about what's actually getting executed during fuzzing +static STATS: RuntimeStats = RuntimeStats::new(); // The spec interpreter requires special one-time setup. static SETUP: Once = Once::new(); @@ -26,7 +23,7 @@ static SETUP: Once = Once::new(); fuzz_target!(|data: &[u8]| { // To avoid a uncaught `SIGSEGV` due to signal handlers; see comments on // `setup_ocaml_runtime`. - SETUP.call_once(|| diff_spec::setup_ocaml_runtime()); + SETUP.call_once(|| engine::setup_engine_runtimes()); // Errors in `run` have to do with not enough input in `data`, which we // ignore here since it doesn't affect how we'd like to fuzz. @@ -34,16 +31,7 @@ fuzz_target!(|data: &[u8]| { }); fn run(data: &[u8]) -> Result<()> { - let successes = TOTAL_SUCCESSES.load(SeqCst); - let attempts = TOTAL_ATTEMPTED.fetch_add(1, SeqCst); - if attempts > 1 && attempts % 1_000 == 0 { - println!("=== Execution rate ({} successes / {} attempted modules): {}% (total invocations: {}) ===", - successes, - attempts, - successes as f64 / attempts as f64 * 100f64, - TOTAL_INVOCATIONS.load(SeqCst) - ); - } + STATS.bump_attempts(); let mut u = Unstructured::new(data); let mut config: Config = u.arbitrary()?; @@ -51,19 +39,20 @@ fn run(data: &[u8]) -> Result<()> { // Generate the Wasm module. let wasm = if u.arbitrary()? { - // TODO figure out if this always eats up the rest of the unstructured; - // can we limit the number of instructions/functions. + STATS.wasm_smith_modules.fetch_add(1, SeqCst); let module = config.generate(&mut u, Some(1000))?; module.to_bytes() } else { + STATS.single_instruction_modules.fetch_add(1, SeqCst); let module = SingleInstModule::new(&mut u, &mut config.module_config)?; module.to_bytes() }; log_wasm(&wasm); // Choose a left-hand side Wasm engine. - let lhs = engine::choose(&mut u, &config)?; + let mut lhs = engine::choose(&mut u, &config)?; let lhs_instance = lhs.instantiate(&wasm); + STATS.bump_engine(lhs.name()); // Choose a right-hand side Wasm engine--this will always be Wasmtime. let rhs_store = config.to_store(); @@ -73,7 +62,11 @@ fn run(data: &[u8]) -> Result<()> { // If we fail to instantiate, check that both sides do. let (mut lhs_instance, mut rhs_instance) = match (lhs_instance, rhs_instance) { (Ok(l), Ok(r)) => (l, r), - (Err(_), Err(_)) => return Ok(()), // TODO match the error messages. + (Err(l), Err(r)) => { + let err = r.downcast::().expect("not a trap"); + lhs.assert_error_match(&err, l); + return Ok(()); + } (l, r) => panic!( "failed to instantiate only one side: {:?} != {:?}", l.err(), @@ -89,21 +82,116 @@ fn run(data: &[u8]) -> Result<()> { .params() .map(|t| DiffValue::arbitrary_of_type(&mut u, t.try_into().unwrap())) .collect::>>()?; - differential(lhs_instance.as_mut(), &mut rhs_instance, &name, &arguments) - .expect("failed to run differential evaluation"); + let result_tys = signature + .results() + .map(|t| DiffValueType::try_from(t).unwrap()) + .collect::>(); + differential( + lhs_instance.as_mut(), + &mut rhs_instance, + &name, + &arguments, + &result_tys, + ) + .expect("failed to run differential evaluation"); // We evaluate the same function with different arguments until we // hit a predetermined limit or we run out of unstructured data--it // does not make sense to re-evaluate the same arguments over and // over. invocations += 1; - TOTAL_INVOCATIONS.fetch_add(1, SeqCst); + STATS.total_invocations.fetch_add(1, SeqCst); if invocations > NUM_INVOCATIONS || u.is_empty() { break; } } } - TOTAL_SUCCESSES.fetch_add(1, SeqCst); + STATS.successes.fetch_add(1, SeqCst); Ok(()) } + +#[derive(Default)] +struct RuntimeStats { + /// Total number of fuzz inputs processed + attempts: AtomicUsize, + + /// Number of times we've invoked engines + total_invocations: AtomicUsize, + + /// Number of times a fuzz input finished all the way to the end without any + /// sort of error (including `Arbitrary` errors) + successes: AtomicUsize, + + // Counters for which engine was chosen + wasmi: AtomicUsize, + v8: AtomicUsize, + spec: AtomicUsize, + wasmtime: AtomicUsize, + + // Counters for which style of module is chosen + wasm_smith_modules: AtomicUsize, + single_instruction_modules: AtomicUsize, +} + +impl RuntimeStats { + const fn new() -> RuntimeStats { + RuntimeStats { + attempts: AtomicUsize::new(0), + total_invocations: AtomicUsize::new(0), + successes: AtomicUsize::new(0), + wasmi: AtomicUsize::new(0), + v8: AtomicUsize::new(0), + spec: AtomicUsize::new(0), + wasmtime: AtomicUsize::new(0), + wasm_smith_modules: AtomicUsize::new(0), + single_instruction_modules: AtomicUsize::new(0), + } + } + + fn bump_attempts(&self) { + let attempts = self.attempts.fetch_add(1, SeqCst); + if attempts == 0 || attempts % 1_000 != 0 { + return; + } + let successes = self.successes.load(SeqCst); + println!( + "=== Execution rate ({} successes / {} attempted modules): {:.02}% ===", + successes, + attempts, + successes as f64 / attempts as f64 * 100f64, + ); + + let v8 = self.v8.load(SeqCst); + let spec = self.spec.load(SeqCst); + let wasmi = self.wasmi.load(SeqCst); + let wasmtime = self.wasmtime.load(SeqCst); + let total = v8 + spec + wasmi + wasmtime; + println!( + "\twasmi: {:.02}%, spec: {:.02}%, wasmtime: {:.02}%, v8: {:.02}%", + wasmi as f64 / total as f64 * 100f64, + spec as f64 / total as f64 * 100f64, + wasmtime as f64 / total as f64 * 100f64, + v8 as f64 / total as f64 * 100f64, + ); + + let wasm_smith = self.wasm_smith_modules.load(SeqCst); + let single_inst = self.single_instruction_modules.load(SeqCst); + let total = wasm_smith + single_inst; + println!( + "\twasm-smith: {:.02}%, single-inst: {:.02}%", + wasm_smith as f64 / total as f64 * 100f64, + single_inst as f64 / total as f64 * 100f64, + ); + } + + fn bump_engine(&self, name: &str) { + match name { + "wasmi" => self.wasmi.fetch_add(1, SeqCst), + "wasmtime" => self.wasmtime.fetch_add(1, SeqCst), + "spec" => self.spec.fetch_add(1, SeqCst), + "v8" => self.v8.fetch_add(1, SeqCst), + _ => return, + }; + } +} diff --git a/fuzz/fuzz_targets/differential_v8.rs b/fuzz/fuzz_targets/differential_v8.rs deleted file mode 100644 index e4546ea8c1..0000000000 --- a/fuzz/fuzz_targets/differential_v8.rs +++ /dev/null @@ -1,50 +0,0 @@ -#![no_main] - -use libfuzzer_sys::arbitrary::{Result, Unstructured}; -use libfuzzer_sys::fuzz_target; -use wasmtime_fuzzing::generators::InstanceAllocationStrategy; -use wasmtime_fuzzing::{generators, oracles}; - -fuzz_target!(|data: &[u8]| { - // errors in `run` have to do with not enough input in `data`, which we - // ignore here since it doesn't affect how we'd like to fuzz. - drop(run(data)); -}); - -fn run(data: &[u8]) -> Result<()> { - let mut u = Unstructured::new(data); - let mut config: generators::Config = u.arbitrary()?; - config.set_differential_config(); - - // Enable features that v8 has implemented - config.module_config.config.simd_enabled = u.arbitrary()?; - config.module_config.config.bulk_memory_enabled = u.arbitrary()?; - - // FIXME: reference types are disabled for now as we seemingly keep finding - // a segfault in v8. This is found relatively quickly locally and keeps - // getting found by oss-fuzz and currently we don't think that there's - // really much we can do about it. For the time being disable reference - // types entirely. An example bug is - // https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=45662 - // - // config.module_config.config.reference_types_enabled = u.arbitrary()?; - - // FIXME: to enable fuzzing with the threads proposal, see - // https://github.com/bytecodealliance/wasmtime/issues/4268. - // config.module_config.config.threads_enabled = u.arbitrary()?; - - // Allow multiple tables, as set_differential_config() assumes reference - // types are disabled and therefore sets max_tables to 1 - config.module_config.config.max_tables = 4; - if let InstanceAllocationStrategy::Pooling { - instance_limits: limits, - .. - } = &mut config.wasmtime.strategy - { - limits.tables = 4; - } - - let module = config.generate(&mut u, Some(1000))?; - oracles::differential_v8_execution(&module.to_bytes(), &config); - Ok(()) -}