Port v8 fuzzer to the new framework (#4739)
* Port v8 fuzzer to the new framework This commit aims to improve the support for the new "meta" differential fuzzer added in #4515 by ensuring that all existing differential fuzzing is migrated to this new fuzzer. This PR includes features such as: * The V8 differential execution is migrated to the new framework. * `Config::set_differential_config` no longer force-disables wasm features, instead allowing them to be enabled as per the fuzz input. * `DiffInstance::{hash, hash}` was replaced with `DiffInstance::get_{memory,global}` to allow more fine-grained assertions. * Support for `FuncRef` and `ExternRef` have been added to `DiffValue` and `DiffValueType`. For now though generating an arbitrary `ExternRef` and `FuncRef` simply generates a null value. * Arbitrary `DiffValue::{F32,F64}` values are guaranteed to use canonical NaN representations to fix an issue with v8 where with the v8 engine we can't communicate non-canonical NaN values through JS. * `DiffEngine::evaluate` allows "successful failure" for cases where engines can't support that particular invocation, for example v8 can't support `v128` arguments or return values. * Smoke tests were added for each engine to ensure that a simple wasm module works at PR-time. * Statistics printed from the main fuzzer now include percentage-rates for chosen engines as well as percentage rates for styles-of-module. There's also a few small refactorings here and there but mostly just things I saw along the way. * Update the fuzzing README
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<wasmtime::ValType> for DiffValueType {
|
||||
@@ -170,8 +205,8 @@ impl TryFrom<wasmtime::ValType> 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<u64> {
|
||||
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(())
|
||||
|
||||
@@ -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<Box<Self>> {
|
||||
pub fn new(config: &ModuleConfig) -> Result<Self> {
|
||||
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<Box<dyn DiffInstance>> {
|
||||
fn instantiate(&mut self, wasm: &[u8]) -> Result<Box<dyn DiffInstance>> {
|
||||
// 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<Vec<DiffValue>> {
|
||||
_results: &[DiffValueType],
|
||||
) -> Result<Option<Vec<DiffValue>>> {
|
||||
// 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<DiffValue> {
|
||||
// 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<Vec<u8>> {
|
||||
// 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<DiffValue> 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))
|
||||
}
|
||||
|
||||
321
crates/fuzzing/src/oracles/diff_v8.rs
Normal file
321
crates/fuzzing/src/oracles/diff_v8.rs
Normal file
@@ -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<RefCell<v8::OwnedIsolate>>,
|
||||
}
|
||||
|
||||
impl V8Engine {
|
||||
pub fn new(config: &ModuleConfig) -> Result<V8Engine> {
|
||||
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<Box<dyn DiffInstance>> {
|
||||
// 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<RefCell<v8::OwnedIsolate>>,
|
||||
context: v8::Global<v8::Context>,
|
||||
instance: v8::Global<v8::Value>,
|
||||
}
|
||||
|
||||
impl DiffInstance for V8Instance {
|
||||
fn name(&self) -> &'static str {
|
||||
"v8"
|
||||
}
|
||||
|
||||
fn evaluate(
|
||||
&mut self,
|
||||
function_name: &str,
|
||||
arguments: &[DiffValue],
|
||||
result_tys: &[DiffValueType],
|
||||
) -> Result<Option<Vec<DiffValue>>> {
|
||||
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<DiffValue> {
|
||||
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<Vec<u8>> {
|
||||
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<v8::Local<'s, v8::Value>> {
|
||||
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))
|
||||
}
|
||||
@@ -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<Box<Self>> {
|
||||
pub fn new(config: &ModuleConfig) -> Result<Self> {
|
||||
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<Box<dyn DiffInstance>> {
|
||||
fn instantiate(&mut self, wasm: &[u8]) -> Result<Box<dyn DiffInstance>> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
impl DiffInstance for WasmiInstance {
|
||||
@@ -65,7 +71,12 @@ impl DiffInstance for WasmiInstance {
|
||||
"wasmi"
|
||||
}
|
||||
|
||||
fn evaluate(&mut self, function_name: &str, arguments: &[DiffValue]) -> Result<Vec<DiffValue>> {
|
||||
fn evaluate(
|
||||
&mut self,
|
||||
function_name: &str,
|
||||
arguments: &[DiffValue],
|
||||
_results: &[DiffValueType],
|
||||
) -> Result<Option<Vec<DiffValue>>> {
|
||||
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<u8> = 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)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// List the names of all exported items in a binary Wasm module.
|
||||
fn list_export_names(wasm: &[u8]) -> Vec<String> {
|
||||
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_global(&mut self, name: &str, _ty: DiffValueType) -> Option<DiffValue> {
|
||||
match self.instance.export_by_name(name) {
|
||||
Some(wasmi::ExternVal::Global(g)) => Some(g.get().into()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_memory(&mut self, name: &str, shared: bool) -> Option<Vec<u8>> {
|
||||
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<u8> = 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<DiffValue> 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))
|
||||
}
|
||||
|
||||
@@ -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<Box<Self>> {
|
||||
Ok(Box::new(Self {
|
||||
config: config.clone(),
|
||||
}))
|
||||
pub fn new(config: generators::Config) -> Result<Self> {
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +27,23 @@ impl DiffEngine for WasmtimeEngine {
|
||||
"wasmtime"
|
||||
}
|
||||
|
||||
fn instantiate(&self, wasm: &[u8]) -> Result<Box<dyn DiffInstance>> {
|
||||
fn instantiate(&mut self, wasm: &[u8]) -> Result<Box<dyn DiffInstance>> {
|
||||
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::<Trap>().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<StoreLimits>, module: Module) -> Result<Self> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<Vec<DiffValue>> {
|
||||
fn evaluate(
|
||||
&mut self,
|
||||
function_name: &str,
|
||||
arguments: &[DiffValue],
|
||||
_results: &[DiffValueType],
|
||||
) -> Result<Option<Vec<DiffValue>>> {
|
||||
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<DiffValue> {
|
||||
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<Vec<u8>> {
|
||||
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<DiffValue> 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))
|
||||
}
|
||||
|
||||
@@ -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<Box<dyn DiffEngine>> {
|
||||
// Filter out any engines that cannot match the given configuration.
|
||||
let mut engines: Vec<Box<dyn DiffEngine>> = 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))
|
||||
}
|
||||
#[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))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// 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<Box<dyn DiffInstance>>;
|
||||
fn instantiate(&mut self, wasm: &[u8]) -> anyhow::Result<Box<dyn DiffInstance>>;
|
||||
|
||||
/// 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<Vec<DiffValue>>;
|
||||
results: &[DiffValueType],
|
||||
) -> anyhow::Result<Option<Vec<DiffValue>>>;
|
||||
|
||||
/// 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<DiffValue>;
|
||||
|
||||
/// 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<Vec<u8>>;
|
||||
}
|
||||
|
||||
/// 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<T>(mk_engine: impl Fn(Config) -> anyhow::Result<T>)
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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<v8::Local<'s, v8::Value>, 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::<Trap>() {
|
||||
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::<Trap>().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<Module>, Store<super::StoreLimits>) {
|
||||
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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::<Trap>().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::<Result<Vec<_>>>()?;
|
||||
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::<Vec<_>>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user