* fuzz: Refactor Wasmtime's fuzz targets
A recent fuzz bug found is related to timing out when compiling a
module. This timeout, however, is predominately because Cranelift's
debug verifier is enabled and taking up over half the compilation time.
I wanted to fix this by disabling the verifier when input modules might
have a lot of functions, but this was pretty difficult to implement.
Over time we've grown a number of various fuzzers. Most are
`wasm-smith`-based at this point but there's various entry points for
configuring the wasm-smith module, the wasmtime configuration, etc. I've
historically gotten quite lost in trying to change defaults and feeling
like I have to touch a lot of different places. This is the motivation
for this commit, simplifying fuzzer default configuration.
This commit removes the ability to create a default `Config` for
fuzzing, instead only supporting generating a configuration via
`Arbitrary`. This then involved refactoring all targets and fuzzers to
ensure that configuration is generated through `Arbitrary`. This should
actually expand the coverage of some existing fuzz targets since
`Arbitrary for Config` will tweak options that don't affect runtime,
such as memory configuration or jump veneers.
All existing fuzz targets are refactored to use this new method of
configuration. Some fuzz targets were also shuffled around or
reimplemented:
* `compile` - this now directly calls `Module::new` to skip all the
fuzzing infrastructure. This is mostly done because this fuzz target
isn't too interesting and is largely just seeing what happens when
things are thrown at the wall for Wasmtime.
* `instantiate-maybe-invalid` - this fuzz target now skips instantiation
and instead simply goes into `Module::new` like the `compile` target.
The rationale behind this is that most modules won't instantiate
anyway and this fuzz target is primarily fuzzing the compiler. This
skips having to generate arbitrary configuration since
wasm-smith-generated-modules (or valid ones at least) aren't used
here.
* `instantiate` - this fuzz target was removed. In general this fuzz
target isn't too interesting in isolation. Almost everything it deals
with likely won't pass compilation and is covered by the `compile`
fuzz target, and otherwise interesting modules being instantiated can
all theoretically be created by `wasm-smith` anyway.
* `instantiate-wasm-smith` and `instantiate-swarm` - these were both merged
into a new `instantiate` target (replacing the old one from above).
There wasn't really much need to keep these separate since they really
only differed at this point in methods of timeout. Otherwise we much
more heavily use `SwarmConfig` than wasm-smith's built-in options.
The intention is that we should still have basically the same coverage
of fuzzing as before, if not better because configuration is now
possible on some targets. Additionally there is one centralized point of
configuration for fuzzing for wasmtime, `Arbitrary for ModuleConfig`.
This internally creates an arbitrary `SwarmConfig` from `wasm-smith` and
then further tweaks it for Wasmtime's needs, such as enabling various
wasm proposals by default. In the future enabling a wasm proposal on
fuzzing should largely just be modifying this one trait implementation.
* fuzz: Sometimes disable the cranelift debug verifier
This commit disables the cranelift debug verifier if the input wasm
module might be "large" for the definition of "more than 10 functions".
While fuzzing we disable threads (set them to 1) and enable the
cranelift debug verifier. Coupled with a 20-30x slowdown this means that
a module with the maximum number of functions, 100, gives:
60x / 100 functions / 30x slowdown = 20ms
With only 20 milliseconds per function this is even further halved by
the `differential` fuzz target compiling a module twice, which means
that, when compiling with a normal release mode Wasmtime, if any
function takes more than 10ms to compile then it's a candidate for
timing out while fuzzing. Given that the cranelift debug verifier can
more than double compilation time in fuzzing mode this actually means
that the real time budget for function compilation is more like 4ms.
The `wasm-smith` crate can pretty easily generate a large function that
takes 4ms to compile, and then when that function is multiplied 100x in
the `differential` fuzz target we trivially time out the fuzz target.
The hope of this commit is to buy back half our budget by disabling the
debug verifier for modules that may have many functions. Further
refinements can be implemented in the future such as limiting functions
for just the differential target as well.
* Fix the single-function-module fuzz configuration
* Tweak how features work in differential fuzzing
* Disable everything for baseline differential fuzzing
* Enable selectively for each engine afterwards
* Also forcibly enable reference types and bulk memory for spec tests
* Log wasms when compiling
* Add reference types support to v8 fuzzer
* Fix timeouts via fuel
The default store has "infinite" fuel so that needs to be consumed
before fuel is added back in.
* Remove fuzzing-specific tests
These no longer compile and also haven't been added to in a long time.
Most of the time a reduced form of original the fuzz test case is added
when a fuzz bug is fixed.
336 lines
13 KiB
Rust
336 lines
13 KiB
Rust
use super::{first_exported_function, first_exported_memory, log_wasm};
|
|
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) = super::differential_store(wasm, config);
|
|
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!(),
|
|
});
|
|
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.
|
|
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");
|
|
}
|