Browse Source
* 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 READMEpull/4743/head
Alex Crichton
2 years ago
committed by
GitHub
14 changed files with 887 additions and 691 deletions
@ -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,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 |
|||
} |
@ -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…
Reference in new issue