Browse Source

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
pull/4743/head
Alex Crichton 2 years ago
committed by GitHub
parent
commit
fd98814b96
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 77
      crates/fuzzing/src/generators/config.rs
  2. 45
      crates/fuzzing/src/generators/value.rs
  3. 59
      crates/fuzzing/src/oracles.rs
  4. 61
      crates/fuzzing/src/oracles/diff_spec.rs
  5. 321
      crates/fuzzing/src/oracles/diff_v8.rs
  6. 127
      crates/fuzzing/src/oracles/diff_wasmi.rs
  7. 152
      crates/fuzzing/src/oracles/diff_wasmtime.rs
  8. 158
      crates/fuzzing/src/oracles/engine.rs
  9. 372
      crates/fuzzing/src/oracles/v8.rs
  10. 4
      crates/fuzzing/wasm-spec-interpreter/src/lib.rs
  11. 6
      fuzz/Cargo.toml
  12. 6
      fuzz/README.md
  13. 140
      fuzz/fuzz_targets/differential.rs
  14. 50
      fuzz/fuzz_targets/differential_v8.rs

77
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,

45
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<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),
}
}
}

59
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<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(())

61
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<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

@ -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, &params);
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))
}

127
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<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)
}
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!(),
}
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_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))
}

152
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<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))
}

158
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<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))
}
// 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<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");
}

372
crates/fuzzing/src/oracles/v8.rs

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

4
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")
}

6
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"

6
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

140
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::<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,
};
}
}

50
fuzz/fuzz_targets/differential_v8.rs

@ -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(())
}
Loading…
Cancel
Save