diff --git a/Cargo.lock b/Cargo.lock index 390fd7e8f8..d2f6763725 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "fslock" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc585f4fe7227b37ef0216444c87ca8ab6051622e4e2bc75d4bed4ea5106148" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "fst" version = "0.4.6" @@ -2649,6 +2659,19 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rusty_v8" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fc062fb861b82fa7ac4e1a009da873279a10180d2133574e4219d870038c1c" +dependencies = [ + "bitflags", + "fslock", + "lazy_static", + "libc", + "which", +] + [[package]] name = "ryu" version = "1.0.5" @@ -3739,6 +3762,7 @@ dependencies = [ "env_logger 0.8.3", "log", "rayon", + "rusty_v8", "wasm-encoder", "wasm-smith", "wasm-spec-interpreter", @@ -3905,6 +3929,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +dependencies = [ + "either", + "lazy_static", + "libc", +] + [[package]] name = "wiggle" version = "0.29.0" diff --git a/crates/fuzzing/Cargo.toml b/crates/fuzzing/Cargo.toml index 34e641ccdc..250c7daa64 100644 --- a/crates/fuzzing/Cargo.toml +++ b/crates/fuzzing/Cargo.toml @@ -22,6 +22,13 @@ wasm-smith = "0.7.0" wasm-spec-interpreter = { path = "./wasm-spec-interpreter" } wasmi = "0.7.0" +# We rely on precompiled v8 binaries, but rusty-v8 doesn't have a precompiled +# binary for MinGW which is built on our CI. It does have one for Windows-msvc, +# though, so we could use that if we wanted. For now though just simplify a bit +# and don't depend on this on Windows. +[target.'cfg(not(windows))'.dependencies] +rusty_v8 = "0.27" + [dev-dependencies] wat = "1.0.37" diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index 0aecde02ce..ba7ab3b278 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -21,6 +21,11 @@ use std::time::{Duration, Instant}; use wasmtime::*; use wasmtime_wast::WastContext; +#[cfg(not(windows))] +pub use v8::*; +#[cfg(not(windows))] +mod v8; + static CNT: AtomicUsize = AtomicUsize::new(0); fn log_wasm(wasm: &[u8]) { @@ -563,9 +568,11 @@ pub fn table_ops( /// conform to certain specifications: one exported function, one exported /// memory. #[derive(Default, Debug, Arbitrary, Clone)] -pub struct SingleFunctionModuleConfig; +pub struct SingleFunctionModuleConfig; -impl wasm_smith::Config for SingleFunctionModuleConfig { +impl wasm_smith::Config + for SingleFunctionModuleConfig +{ fn allow_start_export(&self) -> bool { false } @@ -611,6 +618,14 @@ impl wasm_smith::Config for SingleFunctionModuleConfig { fn canonicalize_nans(&self) -> bool { true } + + fn simd_enabled(&self) -> bool { + SIMD + } + + fn bulk_memory_enabled(&self) -> bool { + BULK + } } /// Perform differential execution between Cranelift and wasmi, diffing the @@ -638,7 +653,7 @@ pub fn differential_wasmi_execution(wasm: &[u8], config: &crate::generators::Con // Introspect wasmtime module to find name of an exported function and of an // exported memory. - let func_name = first_exported_function(&wasmtime_module)?; + let (func_name, _ty) = first_exported_function(&wasmtime_module)?; let memory_name = first_exported_memory(&wasmtime_module)?; let wasmi_mem_export = wasmi_instance.export_by_name(memory_name).unwrap(); @@ -816,7 +831,7 @@ fn run_in_wasmtime( .context("Wasmtime cannot instantiate module")?; // Find the first exported function. - let func_name = + let (func_name, _ty) = first_exported_function(&wasmtime_module).context("Cannot find exported function")?; let wasmtime_main = wasmtime_instance .get_func(&mut wasmtime_store, &func_name[..]) @@ -828,10 +843,10 @@ fn run_in_wasmtime( } // Introspect wasmtime module to find the name of the first exported function. -fn first_exported_function(module: &wasmtime::Module) -> Option<&str> { +fn first_exported_function(module: &wasmtime::Module) -> Option<(&str, FuncType)> { for e in module.exports() { match e.ty() { - wasmtime::ExternType::Func(..) => return Some(e.name()), + wasmtime::ExternType::Func(ty) => return Some((e.name(), ty)), _ => {} } } diff --git a/crates/fuzzing/src/oracles/v8.rs b/crates/fuzzing/src/oracles/v8.rs new file mode 100644 index 0000000000..59da9d3742 --- /dev/null +++ b/crates/fuzzing/src/oracles/v8.rs @@ -0,0 +1,293 @@ +use super::{first_exported_function, first_exported_memory, log_wasm}; +use rusty_v8 as v8; +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 + crate::init_fuzzing(); + 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), + _ => 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(), + _ => unimplemented!(), + }); + } + + // Wasmtime: call the first exported func + let wasmtime_main = wasmtime_instance + .get_func(&mut wasmtime_store, func) + .expect("function export is present"); + let wasmtime_vals = wasmtime_main.call(&mut wasmtime_store, &wasmtime_params); + 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_vals, v8_vals) { + (Ok(wasmtime), Ok(v8)) => { + log::trace!("both executed successfully"); + match wasmtime.len() { + 0 => assert!(v8.is_undefined()), + 1 => assert_val_match(&wasmtime[0], &v8, &mut scope), + _ => { + let array = v8::Local::<'_, v8::Array>::try_from(v8).unwrap(); + for (i, wasmtime) in wasmtime.iter().enumerate() { + let v8 = array.get_index(&mut scope, i as u32).unwrap(); + assert_val_match(wasmtime, &v8, &mut scope); + // .. + } + } + } + } + (Ok(_), Err(msg)) => { + panic!("wasmtime succeeded at invocation, v8 failed: {}", msg) + } + (Err(err), Ok(_)) => { + panic!("v8 succeeded at invocation, wasmtime failed: {:?}", err) + } + (Err(err), Err(msg)) => { + log::trace!("got two traps"); + assert_error_matches(&err, &msg); + return Some(()); + } + }; + + // Verify V8 and wasmtime match memories + if let Some(mem) = first_exported_memory(&wasmtime_module) { + log::trace!("comparing memories"); + let wasmtime = wasmtime_instance + .get_memory(&mut wasmtime_store, mem) + .unwrap(); + + let name = v8::String::new(&mut scope, "MEMORY_NAME").unwrap(); + let func_name = v8::String::new(&mut scope, mem).unwrap(); + global.set(&mut scope, name.into(), func_name.into()); + let v8 = eval( + &mut scope, + &format!("WASM_INSTANCE.exports[MEMORY_NAME].buffer"), + ) + .unwrap(); + let v8 = v8::Local::<'_, v8::ArrayBuffer>::try_from(v8).unwrap(); + let v8_data = v8.get_backing_store(); + let wasmtime_data = wasmtime.data(&wasmtime_store); + assert_eq!(wasmtime_data.len(), v8_data.len()); + for i in 0..v8_data.len() { + if wasmtime_data[i] != v8_data[i].get() { + panic!("memories differ"); + } + } + } + + Some(()) +} + +/// Manufactures a new V8 Isolate to run within. +fn isolate() -> v8::OwnedIsolate { + static INIT: Once = Once::new(); + + INIT.call_once(|| { + let platform = v8::new_default_platform(0, false).make_shared(); + v8::V8::initialize_platform(platform); + v8::V8::initialize(); + }); + + v8::Isolate::new(Default::default()) +} + +/// Evaluates the JS `code` within `scope`, returning either the result of the +/// computation or the stringified exception if one happened. +fn eval<'s>( + scope: &mut v8::HandleScope<'s>, + code: &str, +) -> Result, String> { + let mut tc = v8::TryCatch::new(scope); + let mut scope = v8::EscapableHandleScope::new(&mut tc); + let source = v8::String::new(&mut scope, code).unwrap(); + let script = v8::Script::compile(&mut scope, source, None).unwrap(); + match script.run(&mut scope) { + Some(val) => Ok(scope.escape(val)), + None => { + drop(scope); + assert!(tc.has_caught()); + Err(tc + .message() + .unwrap() + .get(&mut tc) + .to_rust_string_lossy(&mut tc)) + } + } +} + +/// Asserts that the wasmtime value `a` matches the v8 value `b`. +/// +/// For NaN values simply just asserts that they're both NaN. +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(), + ); + } + _ => panic!("unsupported match {:?}", a), + } + + fn same_float(a: f64, b: f64) { + assert!(a == b || (a.is_nan() && b.is_nan()), "{} != {}", a, b); + } +} + +/// Attempts to assert that the `wasmtime` error matches the `v8` error string. +/// +/// This is not a precise function. This will likely need updates over time as +/// v8 and/or wasmtime changes. The goal here is to generally make sure that +/// both engines fail for basically the same reason. +fn assert_error_matches(wasmtime: &anyhow::Error, v8: &str) { + let wasmtime_msg = match wasmtime.downcast_ref::() { + Some(trap) => trap.display_reason().to_string(), + None => format!("{:?}", wasmtime), + }; + let verify_wasmtime = |msg: &str| { + assert!(wasmtime_msg.contains(msg), "{}\n!=\n{}", wasmtime_msg, v8); + }; + let verify_v8 = |msg: &[&str]| { + assert!( + msg.iter().any(|msg| v8.contains(msg)), + "{:?}\n\t!=\n{}", + wasmtime_msg, + v8 + ); + }; + if let Some(code) = wasmtime.downcast_ref::().and_then(|t| t.trap_code()) { + match code { + TrapCode::MemoryOutOfBounds => { + return verify_v8(&[ + "memory access out of bounds", + "data segment is out of bounds", + ]) + } + TrapCode::UnreachableCodeReached => return verify_v8(&["unreachable"]), + TrapCode::IntegerDivisionByZero => { + return verify_v8(&["divide by zero", "remainder by zero"]) + } + TrapCode::StackOverflow => return verify_v8(&["call stack size exceeded"]), + 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"); +} diff --git a/crates/runtime/src/helpers.c b/crates/runtime/src/helpers.c index 8605844057..4865928a18 100644 --- a/crates/runtime/src/helpers.c +++ b/crates/runtime/src/helpers.c @@ -1,4 +1,6 @@ #include +#include +#include // Note that `sigsetjmp` and `siglongjmp` are used here where possible to // explicitly pass a 0 argument to `sigsetjmp` that we don't need to preserve @@ -32,3 +34,38 @@ void wasmtime_longjmp(void *JmpBuf) { platform_jmp_buf *buf = (platform_jmp_buf*) JmpBuf; platform_longjmp(*buf, 1); } + +// Just in case cross-language LTO is enabled we set the `noinline` attribute +// and also try to have some sort of side effect in this function with a dummy +// `asm` statement. +// +// Note the `weak` linkage here, though, which is intended to let other code +// override this symbol if it's defined elsewhere, since this definition doesn't +// matter. +#ifndef CFG_TARGET_OS_windows +__attribute__((weak, noinline)) +#endif +void __jit_debug_register_code() { +#ifndef CFG_TARGET_OS_windows + asm(""); +#endif +} + +struct JITDescriptor { + uint32_t version_; + uint32_t action_flag_; + void* relevant_entry_; + void* first_entry_; +}; + +// Note the `weak` linkage here which is the same purpose as above. We want to +// let other runtimes be able to override this since our own definition isn't +// important. +#ifndef CFG_TARGET_OS_windows +__attribute__((weak)) +#endif +struct JITDescriptor __jit_debug_descriptor = {1, 0, NULL, NULL}; + +struct JITDescriptor* wasmtime_jit_debug_descriptor() { + return &__jit_debug_descriptor; +} diff --git a/crates/runtime/src/jit_int.rs b/crates/runtime/src/jit_int.rs index 4fd1e7f88e..3a0f58d9cc 100644 --- a/crates/runtime/src/jit_int.rs +++ b/crates/runtime/src/jit_int.rs @@ -27,23 +27,9 @@ struct JITDescriptor { first_entry: *mut JITCodeEntry, } -#[no_mangle] -#[used] -static mut __jit_debug_descriptor: JITDescriptor = JITDescriptor { - version: 1, - action_flag: JIT_NOACTION, - relevant_entry: ptr::null_mut(), - first_entry: ptr::null_mut(), -}; - -#[no_mangle] -#[inline(never)] -extern "C" fn __jit_debug_register_code() { - // Hack to not allow inlining even when Rust wants to do it in release mode. - let x = 3; - unsafe { - std::ptr::read_volatile(&x); - } +extern "C" { + fn wasmtime_jit_debug_descriptor() -> *mut JITDescriptor; + fn __jit_debug_register_code(); } lazy_static! { @@ -102,41 +88,43 @@ unsafe impl Sync for GdbJitImageRegistration {} unsafe fn register_gdb_jit_image(entry: *mut JITCodeEntry) { let _lock = GDB_REGISTRATION.lock().unwrap(); + let desc = &mut *wasmtime_jit_debug_descriptor(); // Add it to the linked list in the JIT descriptor. - (*entry).next_entry = __jit_debug_descriptor.first_entry; - if !__jit_debug_descriptor.first_entry.is_null() { - (*__jit_debug_descriptor.first_entry).prev_entry = entry; + (*entry).next_entry = desc.first_entry; + if !desc.first_entry.is_null() { + (*desc.first_entry).prev_entry = entry; } - __jit_debug_descriptor.first_entry = entry; + desc.first_entry = entry; // Point the relevant_entry field of the descriptor at the entry. - __jit_debug_descriptor.relevant_entry = entry; + desc.relevant_entry = entry; // Set action_flag to JIT_REGISTER and call __jit_debug_register_code. - __jit_debug_descriptor.action_flag = JIT_REGISTER_FN; + desc.action_flag = JIT_REGISTER_FN; __jit_debug_register_code(); - __jit_debug_descriptor.action_flag = JIT_NOACTION; - __jit_debug_descriptor.relevant_entry = ptr::null_mut(); + desc.action_flag = JIT_NOACTION; + desc.relevant_entry = ptr::null_mut(); } unsafe fn unregister_gdb_jit_image(entry: *mut JITCodeEntry) { let _lock = GDB_REGISTRATION.lock().unwrap(); + let desc = &mut *wasmtime_jit_debug_descriptor(); // Remove the code entry corresponding to the code from the linked list. if !(*entry).prev_entry.is_null() { (*(*entry).prev_entry).next_entry = (*entry).next_entry; } else { - __jit_debug_descriptor.first_entry = (*entry).next_entry; + desc.first_entry = (*entry).next_entry; } if !(*entry).next_entry.is_null() { (*(*entry).next_entry).prev_entry = (*entry).prev_entry; } // Point the relevant_entry field of the descriptor at the code entry. - __jit_debug_descriptor.relevant_entry = entry; + desc.relevant_entry = entry; // Set action_flag to JIT_UNREGISTER and call __jit_debug_register_code. - __jit_debug_descriptor.action_flag = JIT_UNREGISTER_FN; + desc.action_flag = JIT_UNREGISTER_FN; __jit_debug_register_code(); - __jit_debug_descriptor.action_flag = JIT_NOACTION; - __jit_debug_descriptor.relevant_entry = ptr::null_mut(); + desc.action_flag = JIT_NOACTION; + desc.relevant_entry = ptr::null_mut(); } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index dc8315a04e..9f30a38326 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -64,6 +64,12 @@ path = "fuzz_targets/differential_wasmi.rs" test = false doc = false +[[bin]] +name = "differential_v8" +path = "fuzz_targets/differential_v8.rs" +test = false +doc = false + [[bin]] name = "spectests" path = "fuzz_targets/spectests.rs" diff --git a/fuzz/fuzz_targets/differential_spec.rs b/fuzz/fuzz_targets/differential_spec.rs index 70b52b3490..3116d24a0a 100644 --- a/fuzz/fuzz_targets/differential_spec.rs +++ b/fuzz/fuzz_targets/differential_spec.rs @@ -11,7 +11,7 @@ static EXECUTED: AtomicUsize = AtomicUsize::new(0); fuzz_target!(|data: ( generators::Config, - wasm_smith::ConfiguredModule + wasm_smith::ConfiguredModule> )| { let (config, mut wasm) = data; wasm.module.ensure_termination(1000); diff --git a/fuzz/fuzz_targets/differential_v8.rs b/fuzz/fuzz_targets/differential_v8.rs new file mode 100644 index 0000000000..3870e09759 --- /dev/null +++ b/fuzz/fuzz_targets/differential_v8.rs @@ -0,0 +1,13 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use wasmtime_fuzzing::{generators, oracles}; + +fuzz_target!(|data: ( + generators::Config, + wasm_smith::ConfiguredModule> +)| { + let (config, mut wasm) = data; + wasm.module.ensure_termination(1000); + oracles::differential_v8_execution(&wasm.module.to_bytes(), &config); +}); diff --git a/fuzz/fuzz_targets/differential_wasmi.rs b/fuzz/fuzz_targets/differential_wasmi.rs index 25091abd49..29674ee449 100644 --- a/fuzz/fuzz_targets/differential_wasmi.rs +++ b/fuzz/fuzz_targets/differential_wasmi.rs @@ -5,7 +5,7 @@ use wasmtime_fuzzing::{generators, oracles}; fuzz_target!(|data: ( generators::Config, - wasm_smith::ConfiguredModule + wasm_smith::ConfiguredModule> )| { let (config, mut wasm) = data; wasm.module.ensure_termination(1000);