//! Oracles. //! //! Oracles take a test case and determine whether we have a bug. For example, //! one of the simplest oracles is to take a Wasm binary as our input test case, //! validate and instantiate it, and (implicitly) check that no assertions //! failed or segfaults happened. A more complicated oracle might compare the //! result of executing a Wasm file with and without optimizations enabled, and //! make sure that the two executions are observably identical. //! //! When an oracle finds a bug, it should report it to the fuzzing engine by //! panicking. pub mod component_api; pub mod component_async; #[cfg(feature = "fuzz-spec-interpreter")] pub mod diff_spec; pub mod diff_wasmi; pub mod diff_wasmtime; pub mod dummy; pub mod engine; pub mod memory; mod stacks; use self::diff_wasmtime::WasmtimeInstance; use self::engine::{DiffEngine, DiffInstance}; use crate::generators::ExceptionOps; use crate::generators::GcOps; use crate::generators::{self, CompilerStrategy, DiffValue, DiffValueType}; use crate::single_module_fuzzer::KnownValid; use crate::{YieldN, block_on}; use arbitrary::Arbitrary; pub use stacks::check_stacks; use std::future::Future; use std::pin::Pin; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}; use std::sync::{Arc, Condvar, Mutex}; use std::task::{Context, Poll}; use std::time::{Duration, Instant}; use wasmtime::*; use wasmtime_wast::WastContext; #[cfg(not(any(windows, target_arch = "s390x", target_arch = "riscv64")))] mod diff_v8; static CNT: AtomicUsize = AtomicUsize::new(0); /// Logs a wasm file to the filesystem to make it easy to figure out what wasm /// was used when debugging. pub fn log_wasm(wasm: &[u8]) { super::init_fuzzing(); if !log::log_enabled!(log::Level::Debug) { return; } let i = CNT.fetch_add(1, SeqCst); let name = format!("testcase{i}.wasm"); std::fs::write(&name, wasm).expect("failed to write wasm file"); log::debug!("wrote wasm file to `{name}`"); let wat = format!("testcase{i}.wat"); match wasmprinter::print_bytes(wasm) { Ok(s) => { std::fs::write(&wat, s).expect("failed to write wat file"); log::debug!("wrote wat file to `{wat}`"); } // If wasmprinter failed remove a `*.wat` file, if any, to avoid // confusing a preexisting one with this wasm which failed to get // printed. Err(e) => { log::debug!("failed to print to wat: {e}"); drop(std::fs::remove_file(&wat)); } } } /// The `T` in `Store` for fuzzing stores, used to limit resource /// consumption during fuzzing. #[derive(Clone)] pub struct StoreLimits(Arc); struct LimitsState { /// Remaining memory, in bytes, left to allocate remaining_memory: AtomicUsize, /// Remaining amount of memory that's allowed to be copied via a growth. remaining_copy_allowance: AtomicUsize, /// Whether or not an allocation request has been denied oom: AtomicBool, } /// Allow up to 1G which is well below the 2G limit on OSS-Fuzz and should allow /// most interesting behavior. const MAX_MEMORY: usize = 1 << 30; /// Allow up to 4G of bytes to be copied (conservatively) which should enable /// growth up to `MAX_MEMORY` or at least up to a relatively large amount. const MAX_MEMORY_MOVED: usize = 4 << 30; impl StoreLimits { /// Creates the default set of limits for all fuzzing stores. pub fn new() -> StoreLimits { StoreLimits(Arc::new(LimitsState { remaining_memory: AtomicUsize::new(MAX_MEMORY), remaining_copy_allowance: AtomicUsize::new(MAX_MEMORY_MOVED), oom: AtomicBool::new(false), })) } fn alloc(&mut self, amt: usize) -> bool { log::trace!("alloc {amt:#x} bytes"); // Assume that on each allocation of memory that all previous // allocations of memory are moved. This is pretty coarse but is used to // help prevent against fuzz test cases that just move tons of bytes // around continuously. This assumes that all previous memory was // allocated in a single linear memory and growing by `amt` will require // moving all the bytes to a new location. This isn't actually required // all the time nor does it accurately reflect what happens all the // time, but it's a coarse approximation that should be "good enough" // for allowing interesting fuzz behaviors to happen while not timing // out just copying bytes around. let prev_size = MAX_MEMORY - self.0.remaining_memory.load(SeqCst); if self .0 .remaining_copy_allowance .fetch_update(SeqCst, SeqCst, |remaining| remaining.checked_sub(prev_size)) .is_err() { self.0.oom.store(true, SeqCst); log::debug!("-> too many bytes moved, rejecting allocation"); return false; } // If we're allowed to move the bytes, then also check if we're allowed // to actually have this much residence at once. match self .0 .remaining_memory .fetch_update(SeqCst, SeqCst, |remaining| remaining.checked_sub(amt)) { Ok(_) => true, Err(_) => { self.0.oom.store(true, SeqCst); log::debug!("-> OOM hit"); false } } } fn is_oom(&self) -> bool { self.0.oom.load(SeqCst) } } impl ResourceLimiter for StoreLimits { fn memory_growing( &mut self, current: usize, desired: usize, _maximum: Option, ) -> Result { Ok(self.alloc(desired - current)) } fn table_growing( &mut self, current: usize, desired: usize, _maximum: Option, ) -> Result { let delta = (desired - current).saturating_mul(std::mem::size_of::()); Ok(self.alloc(delta)) } } /// Methods of timing out execution of a WebAssembly module #[derive(Clone, Debug)] pub enum Timeout { /// No timeout is used, it should be guaranteed via some other means that /// the input does not infinite loop. None, /// Fuel-based timeouts are used where the specified fuel is all that the /// provided wasm module is allowed to consume. Fuel(u64), /// An epoch-interruption-based timeout is used with a sleeping /// thread bumping the epoch counter after the specified duration. Epoch(Duration), } /// Instantiate the Wasm buffer, and implicitly fail if we have an unexpected /// panic or segfault or anything else that can be detected "passively". /// /// The engine will be configured using provided config. pub fn instantiate( wasm: &[u8], known_valid: KnownValid, config: &generators::Config, timeout: Timeout, ) { let mut store = config.to_store(); let module = match compile_module(store.engine(), wasm, known_valid, config) { Some(module) => module, None => return, }; let mut timeout_state = HelperThread::default(); match timeout { Timeout::Fuel(fuel) => store.set_fuel(fuel).unwrap(), // If a timeout is requested then we spawn a helper thread to wait for // the requested time and then send us a signal to get interrupted. We // also arrange for the thread's sleep to get interrupted if we return // early (or the wasm returns within the time limit), which allows the // thread to get torn down. // // This prevents us from creating a huge number of sleeping threads if // this function is executed in a loop, like it does on nightly fuzzing // infrastructure. Timeout::Epoch(timeout) => { let engine = store.engine().clone(); timeout_state.run_periodically(timeout, move || engine.increment_epoch()); } Timeout::None => {} } instantiate_with_dummy(&mut store, &module); } /// Represents supported commands to the `instantiate_many` function. #[derive(Arbitrary, Debug)] pub enum Command { /// Instantiates a module. /// /// The value is the index of the module to instantiate. /// /// The module instantiated will be this value modulo the number of modules provided to `instantiate_many`. Instantiate(usize), /// Terminates a "running" instance. /// /// The value is the index of the instance to terminate. /// /// The instance terminated will be this value modulo the number of currently running /// instances. /// /// If no instances are running, the command will be ignored. Terminate(usize), } /// Instantiates many instances from the given modules. /// /// The engine will be configured using the provided config. /// /// The modules are expected to *not* have start functions as no timeouts are configured. pub fn instantiate_many( modules: &[Vec], known_valid: KnownValid, config: &generators::Config, commands: &[Command], ) { log::debug!("instantiate_many: {commands:#?}"); assert!(!config.module_config.config.allow_start_export); let engine = Engine::new(&config.to_wasmtime()).unwrap(); let modules = modules .iter() .enumerate() .filter_map( |(i, bytes)| match compile_module(&engine, bytes, known_valid, config) { Some(m) => { log::debug!("successfully compiled module {i}"); Some(m) } None => { log::debug!("failed to compile module {i}"); None } }, ) .collect::>(); // If no modules were valid, we're done if modules.is_empty() { return; } // This stores every `Store` where a successful instantiation takes place let mut stores = Vec::new(); let limits = StoreLimits::new(); for command in commands { match command { Command::Instantiate(index) => { let index = *index % modules.len(); log::info!("instantiating {index}"); let module = &modules[index]; let mut store = Store::new(&engine, limits.clone()); config.configure_store(&mut store); if instantiate_with_dummy(&mut store, module).is_some() { stores.push(Some(store)); } else { log::warn!("instantiation failed"); } } Command::Terminate(index) => { if stores.is_empty() { continue; } let index = *index % stores.len(); log::info!("dropping {index}"); stores.swap_remove(index); } } } } fn compile_module( engine: &Engine, bytes: &[u8], known_valid: KnownValid, config: &generators::Config, ) -> Option { log_wasm(bytes); match config.compile(engine, bytes) { Ok(module) => Some(module), Err(_) if known_valid == KnownValid::No => None, Err(e) => { if let generators::InstanceAllocationStrategy::Pooling(c) = &config.wasmtime.strategy { // When using the pooling allocator, accept failures to compile // when arbitrary table element limits have been exceeded as // there is currently no way to constrain the generated module // table types. let string = format!("{e:?}"); if string.contains("minimum element size") { return None; } // Allow modules-failing-to-compile which exceed the requested // size for each instance. This is something that is difficult // to control and ensure it always succeeds, so we simply have a // "random" instance size limit and if a module doesn't fit we // move on to the next fuzz input. if string.contains("instance allocation for this module requires") { return None; } // If the pooling allocator is more restrictive on the number of // tables and memories than we allowed wasm-smith to generate // then allow compilation errors along those lines. if c.max_tables_per_module < (config.module_config.config.max_tables as u32) && string.contains("defined tables count") && string.contains("exceeds the per-instance limit") { return None; } if c.max_memories_per_module < (config.module_config.config.max_memories as u32) && string.contains("defined memories count") && string.contains("exceeds the per-instance limit") { return None; } } panic!("failed to compile module: {e:?}"); } } } /// Create a Wasmtime [`Instance`] from a [`Module`] and fill in all imports /// with dummy values (e.g., zeroed values, immediately-trapping functions). /// Also, this function catches certain fuzz-related instantiation failures and /// returns `None` instead of panicking. /// /// TODO: we should implement tracing versions of these dummy imports that /// record a trace of the order that imported functions were called in and with /// what values. Like the results of exported functions, calls to imports should /// also yield the same values for each configuration, and we should assert /// that. pub fn instantiate_with_dummy(store: &mut Store, module: &Module) -> Option { // Creation of imports can fail due to resource limit constraints, and then // instantiation can naturally fail for a number of reasons as well. Bundle // the two steps together to match on the error below. let linker = dummy::dummy_linker(store, module); if let Err(e) = &linker { log::warn!("failed to create dummy linker: {e:?}"); } let instance = linker.and_then(|l| l.instantiate(&mut *store, module)); unwrap_instance(store, instance) } fn unwrap_instance( store: &Store, instance: wasmtime::Result, ) -> Option { let e = match instance { Ok(i) => return Some(i), Err(e) => e, }; log::debug!("failed to instantiate: {e:?}"); // If the instantiation hit OOM for some reason then that's ok, it's // expected that fuzz-generated programs try to allocate lots of // stuff. if store.data().is_oom() { return None; } // Allow traps which can happen normally with `unreachable` or a timeout or // such. if e.is::() // Also allow failures to instantiate as a result of hitting pooling // limits. || e.is::() // And GC heap OOMs. || e.is::>() // And thrown exceptions. || e.is::() { return None; } let string = e.to_string(); // Currently we instantiate with a `Linker` which can't instantiate // every single module under the sun due to using name-based resolution // rather than positional-based resolution if string.contains("incompatible import type") { return None; } // Everything else should be a bug in the fuzzer or a bug in wasmtime panic!("failed to instantiate: {e:?}"); } /// Evaluate the function identified by `name` in two different engine /// instances--`lhs` and `rhs`. /// /// Returns `Ok(true)` if more evaluations can happen or `Ok(false)` if the /// instances may have drifted apart and no more evaluations can happen. /// /// # Panics /// /// This will panic if the evaluation is different between engines (e.g., /// results are different, hashed instance is different, one side traps, etc.). pub fn differential( lhs: &mut dyn DiffInstance, lhs_engine: &dyn DiffEngine, rhs: &mut WasmtimeInstance, name: &str, args: &[DiffValue], result_tys: &[DiffValueType], ) -> wasmtime::Result { log::debug!("Evaluating: `{name}` with {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(true), }; log::debug!(" -> lhs results on {}: {:?}", lhs.name(), &lhs_results); 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!(" -> rhs results on {}: {:?}", rhs.name(), &rhs_results); // If Wasmtime hit its OOM condition, which is possible since it's set // somewhat low while fuzzing, then don't return an error but return // `false` indicating that differential fuzzing must stop. There's no // guarantee the other engine has the same OOM limits as Wasmtime, and // it's assumed that Wasmtime is configured to have a more conservative // limit than the other engine. if rhs.is_oom() { return Ok(false); } match DiffEqResult::new(lhs_engine, lhs_results, rhs_results) { DiffEqResult::Success(lhs, rhs) => assert_eq!(lhs, rhs), DiffEqResult::Poisoned => return Ok(false), DiffEqResult::Failed => {} } 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; } eprintln!("differential memory is {} bytes long", lhs.len()); eprintln!("wasmtime memory is {} bytes long", rhs.len()); panic!("memories have differing values"); } Ok(true) } /// Result of comparing the result of two operations during differential /// execution. pub enum DiffEqResult { /// Both engines succeeded. Success(T, U), /// The result has reached the state where engines may have diverged and /// results can no longer be compared. Poisoned, /// Both engines failed with the same error message, and internal state /// should still match between the two engines. Failed, } fn wasmtime_trap_is_non_deterministic(trap: &Trap) -> bool { match trap { // Allocations being too large for the GC are // implementation-defined. Trap::AllocationTooLarge | // Stack size, and therefore when overflow happens, is // implementation-defined. Trap::StackOverflow => true, _ => false, } } fn wasmtime_error_is_non_deterministic(error: &wasmtime::Error) -> bool { match error.downcast_ref::() { Some(trap) => wasmtime_trap_is_non_deterministic(trap), // For general, unknown errors, we can't rely on this being // a deterministic Wasm failure that both engines handled // identically, leaving Wasm in identical states. We could // just as easily be hitting engine-specific failures, like // different implementation-defined limits. So simply poison // this execution and move on to the next test. None => true, } } impl DiffEqResult { /// Computes the differential result from executing in two different /// engines. pub fn new( lhs_engine: &dyn DiffEngine, lhs_result: Result, rhs_result: Result, ) -> DiffEqResult { match (lhs_result, rhs_result) { (Ok(lhs_result), Ok(rhs_result)) => DiffEqResult::Success(lhs_result, rhs_result), // Handle all non-deterministic errors by poisoning this execution's // state, so that we simply move on to the next test. (Err(lhs), _) if lhs_engine.is_non_deterministic_error(&lhs) => { log::debug!("lhs failed non-deterministically: {lhs:?}"); DiffEqResult::Poisoned } (_, Err(rhs)) if wasmtime_error_is_non_deterministic(&rhs) => { log::debug!("rhs failed non-deterministically: {rhs:?}"); DiffEqResult::Poisoned } // Both sides failed deterministically. Check that the trap and // state at the time of failure is the same. (Err(lhs), Err(rhs)) => { let rhs = rhs .downcast::() .expect("non-traps handled in earlier match arm"); debug_assert!( !lhs_engine.is_non_deterministic_error(&lhs), "non-deterministic traps handled in earlier match arm", ); debug_assert!( !wasmtime_trap_is_non_deterministic(&rhs), "non-deterministic traps handled in earlier match arm", ); lhs_engine.assert_error_match(&lhs, &rhs); DiffEqResult::Failed } // A real bug is found if only one side fails. (Ok(_), Err(err)) => panic!("only the `rhs` failed for this input: {err:?}"), (Err(err), Ok(_)) => panic!("only the `lhs` failed for this input: {err:?}"), } } } /// Invoke the given API calls. pub fn make_api_calls(api: generators::api::ApiCalls) { use crate::generators::api::ApiCall; use std::collections::HashMap; let mut store: Option> = None; let mut modules: HashMap = Default::default(); let mut instances: HashMap = Default::default(); for call in api.calls { match call { ApiCall::StoreNew(config) => { log::trace!("creating store"); assert!(store.is_none()); store = Some(config.to_store()); } ApiCall::ModuleNew { id, wasm } => { log::debug!("creating module: {id}"); log_wasm(&wasm); let module = match Module::new(store.as_ref().unwrap().engine(), &wasm) { Ok(m) => m, Err(_) => continue, }; let old = modules.insert(id, module); assert!(old.is_none()); } ApiCall::ModuleDrop { id } => { log::trace!("dropping module: {id}"); drop(modules.remove(&id)); } ApiCall::InstanceNew { id, module } => { log::trace!("instantiating module {module} as {id}"); let module = match modules.get(&module) { Some(m) => m, None => continue, }; let store = store.as_mut().unwrap(); if let Some(instance) = instantiate_with_dummy(store, module) { instances.insert(id, instance); } } ApiCall::InstanceDrop { id } => { log::trace!("dropping instance {id}"); instances.remove(&id); } ApiCall::CallExportedFunc { instance, nth } => { log::trace!("calling instance export {instance} / {nth}"); let instance = match instances.get(&instance) { Some(i) => i, None => { // Note that we aren't guaranteed to instantiate valid // modules, see comments in `InstanceNew` for details on // that. But the API call generator can't know if // instantiation failed, so we might not actually have // this instance. When that's the case, just skip the // API call and keep going. continue; } }; let store = store.as_mut().unwrap(); let funcs = instance .exports(&mut *store) .filter_map(|e| match e.into_extern() { Extern::Func(f) => Some(f), _ => None, }) .collect::>(); if funcs.is_empty() { continue; } let nth = nth % funcs.len(); let f = &funcs[nth]; let ty = f.ty(&store); if let Some(params) = ty .params() .map(|p| p.default_value()) .collect::>>() { let mut results = vec![Val::I32(0); ty.results().len()]; let _ = f.call(store, ¶ms, &mut results); } } } } } /// Executes the wast `test` with the `config` specified. /// /// Ensures that wast tests pass regardless of the `Config`. pub fn wast_test(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> { crate::init_fuzzing(); let mut fuzz_config: generators::Config = u.arbitrary()?; fuzz_config.module_config.shared_memory = true; let test: generators::WastTest = u.arbitrary()?; let test = &test.test; if test.config.component_model_async() || u.arbitrary()? { fuzz_config.enable_async(u)?; } // Discard tests that allocate a lot of memory as we don't want to OOM the // fuzzer and we also limit memory growth which would cause the test to // fail. if test.config.hogs_memory.unwrap_or(false) { return Err(arbitrary::Error::IncorrectFormat); } // Transform `fuzz_config` to be valid for `test` and make sure that this // test is supposed to pass. let wast_config = fuzz_config.make_wast_test_compliant(test); if test.should_fail(&wast_config) { return Err(arbitrary::Error::IncorrectFormat); } // Winch requires AVX and AVX2 for SIMD tests to pass so don't run the test // if either isn't enabled. if fuzz_config.wasmtime.compiler_strategy == CompilerStrategy::Winch && test.config.simd() && (fuzz_config .wasmtime .codegen_flag("has_avx") .is_some_and(|value| value == "false") || fuzz_config .wasmtime .codegen_flag("has_avx2") .is_some_and(|value| value == "false")) { log::warn!( "Skipping Wast test because Winch doesn't support SIMD tests with AVX or AVX2 disabled" ); return Err(arbitrary::Error::IncorrectFormat); } // Fuel and epochs don't play well with threads right now, so exclude any // thread-spawning test if it looks like threads are spawned in that case. if fuzz_config.wasmtime.consume_fuel || fuzz_config.wasmtime.epoch_interruption { if test.contents.contains("(thread") { return Err(arbitrary::Error::IncorrectFormat); } } log::debug!("running {:?}", test.path); let async_ = if fuzz_config.wasmtime.async_config == generators::AsyncConfig::Disabled { wasmtime_wast::Async::No } else { wasmtime_wast::Async::Yes }; log::debug!("async: {async_:?}"); let engine = Engine::new(&fuzz_config.to_wasmtime()).unwrap(); let mut wast_context = WastContext::new(&engine, async_, move |store| { fuzz_config.configure_store_epoch_and_fuel(store); }); wast_context .register_spectest(&wasmtime_wast::SpectestConfig { use_shared_memory: true, suppress_prints: true, }) .unwrap(); wast_context.register_wasmtime().unwrap(); wast_context .run_wast(test.path.to_str().unwrap(), test.contents.as_bytes()) .unwrap(); Ok(()) } /// Execute a series of `gc` operations. /// /// Returns the number of `gc` operations which occurred throughout the test /// case -- used to test below that gc happens reasonably soon and eventually. pub fn gc_ops(mut fuzz_config: generators::Config, mut ops: GcOps) -> Result { let expected_drops = Arc::new(AtomicUsize::new(0)); let num_dropped = Arc::new(AtomicUsize::new(0)); let num_gcs = Arc::new(AtomicUsize::new(0)); { fuzz_config.wasmtime.consume_fuel = true; let mut store = fuzz_config.to_store(); store.set_fuel(1_000).unwrap(); let wasm = ops.to_wasm_binary(); log_wasm(&wasm); let module = match compile_module(store.engine(), &wasm, KnownValid::No, &fuzz_config) { Some(m) => m, None => return Ok(0), }; let mut linker = Linker::new(store.engine()); // To avoid timeouts, limit the number of explicit GCs we perform per // test case. const MAX_GCS: usize = 5; let func_ty = FuncType::new( store.engine(), vec![], vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF], ); let func = Func::new(&mut store, func_ty, { let num_dropped = num_dropped.clone(); let expected_drops = expected_drops.clone(); let num_gcs = num_gcs.clone(); move |mut caller: Caller<'_, StoreLimits>, _params, results| { log::info!("gc_ops: GC"); if num_gcs.fetch_add(1, SeqCst) < MAX_GCS { caller.gc(None)?; } let a = ExternRef::new( &mut caller, CountDrops::new(&expected_drops, num_dropped.clone()), )?; let b = ExternRef::new( &mut caller, CountDrops::new(&expected_drops, num_dropped.clone()), )?; let c = ExternRef::new( &mut caller, CountDrops::new(&expected_drops, num_dropped.clone()), )?; log::info!("gc_ops: gc() -> ({a:?}, {b:?}, {c:?})"); results[0] = Some(a).into(); results[1] = Some(b).into(); results[2] = Some(c).into(); Ok(()) } }); linker.define(&store, "", "gc", func).unwrap(); linker .func_wrap("", "take_refs", { let expected_drops = expected_drops.clone(); move |caller: Caller<'_, StoreLimits>, a: Option>, b: Option>, c: Option>| -> Result<()> { log::info!("gc_ops: take_refs({a:?}, {b:?}, {c:?})",); // Do the assertion on each ref's inner data, even though it // all points to the same atomic, so that if we happen to // run into a use-after-free bug with one of these refs we // are more likely to trigger a segfault. if let Some(a) = a { let a = a .data(&caller)? .unwrap() .downcast_ref::() .unwrap(); assert!(a.0.load(SeqCst) <= expected_drops.load(SeqCst)); } if let Some(b) = b { let b = b .data(&caller)? .unwrap() .downcast_ref::() .unwrap(); assert!(b.0.load(SeqCst) <= expected_drops.load(SeqCst)); } if let Some(c) = c { let c = c .data(&caller)? .unwrap() .downcast_ref::() .unwrap(); assert!(c.0.load(SeqCst) <= expected_drops.load(SeqCst)); } Ok(()) } }) .unwrap(); let func_ty = FuncType::new( store.engine(), vec![], vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF], ); let func = Func::new(&mut store, func_ty, { let num_dropped = num_dropped.clone(); let expected_drops = expected_drops.clone(); move |mut caller, _params, results| { log::info!("gc_ops: make_refs"); let a = ExternRef::new( &mut caller, CountDrops::new(&expected_drops, num_dropped.clone()), )?; let b = ExternRef::new( &mut caller, CountDrops::new(&expected_drops, num_dropped.clone()), )?; let c = ExternRef::new( &mut caller, CountDrops::new(&expected_drops, num_dropped.clone()), )?; log::info!("gc_ops: make_refs() -> ({a:?}, {b:?}, {c:?})"); results[0] = Some(a).into(); results[1] = Some(b).into(); results[2] = Some(c).into(); Ok(()) } }); linker.define(&store, "", "make_refs", func).unwrap(); let func_ty = FuncType::new( store.engine(), vec![ValType::Ref(RefType::new(true, HeapType::Struct))], vec![], ); let func = Func::new(&mut store, func_ty, { move |_caller: Caller<'_, StoreLimits>, _params, _results| { log::info!("gc_ops: take_struct()"); Ok(()) } }); linker.define(&store, "", "take_struct", func).unwrap(); for imp in module.imports() { if imp.module() == "" { let name = imp.name(); if name.starts_with("take_struct_") { if let wasmtime::ExternType::Func(ft) = imp.ty() { let imp_name = name.to_string(); let func = Func::new(&mut store, ft.clone(), move |_caller, _params, _results| { log::info!("gc_ops: {imp_name}()"); Ok(()) }); linker.define(&store, "", name, func).unwrap(); } } } } let instance = linker.instantiate(&mut store, &module).unwrap(); let run = instance.get_func(&mut store, "run").unwrap(); { let mut scope = RootScope::new(&mut store); log::info!( "gc_ops: begin allocating {} externref arguments", ops.limits.num_globals ); let args: Vec<_> = (0..ops.limits.num_params) .map(|_| { Ok(Val::ExternRef(Some(ExternRef::new( &mut scope, CountDrops::new(&expected_drops, num_dropped.clone()), )?))) }) .collect::>()?; log::info!( "gc_ops: end allocating {} externref arguments", ops.limits.num_globals ); // The generated function should always return a trap. The only two // valid traps are table-out-of-bounds which happens through `table.get` // and `table.set` generated or an out-of-fuel trap. Otherwise any other // error is unexpected and should fail fuzzing. log::info!("gc_ops: calling into Wasm `run` function"); let err = run.call(&mut scope, &args, &mut []).unwrap_err(); if err.is::>() || err.is::>() { // Accept GC OOM as an allowed outcome for this fuzzer. } else { let trap = err .downcast::() .expect("if not GC oom, error should be a Wasm trap"); match trap { Trap::TableOutOfBounds | Trap::OutOfFuel | Trap::AllocationTooLarge => {} _ => panic!("unexpected trap: {trap}"), } } } // Do a final GC after running the Wasm. store.gc(None)?; } assert_eq!(num_dropped.load(SeqCst), expected_drops.load(SeqCst)); return Ok(num_gcs.load(SeqCst)); struct CountDrops(Arc); impl CountDrops { fn new(expected_drops: &AtomicUsize, num_dropped: Arc) -> Self { let expected = expected_drops.fetch_add(1, SeqCst); log::info!( "CountDrops::new: expected drops: {expected} -> {}", expected + 1 ); Self(num_dropped) } } impl Drop for CountDrops { fn drop(&mut self) { let drops = self.0.fetch_add(1, SeqCst); log::info!("CountDrops::drop: actual drops: {drops} -> {}", drops + 1); } } } /// Execute a series of exception-related operations. pub fn exception_ops(mut fuzz_config: generators::Config, mut ops: ExceptionOps) -> Result<()> { match fuzz_config.wasmtime.compiler_strategy { // Winch doesn't support exceptions; force to Cranelift. CompilerStrategy::Winch => { fuzz_config.wasmtime.compiler_strategy = CompilerStrategy::CraneliftNative; } CompilerStrategy::CraneliftNative | CompilerStrategy::CraneliftPulley => {} } let module_cfg = &mut fuzz_config.module_config.config; // Force exceptions + GC on (exceptions require GC). module_cfg.gc_enabled = true; module_cfg.exceptions_enabled = true; module_cfg.reference_types_enabled = true; let expected = ops.expected_result(); let wasm = ops.to_wasm_binary(); log_wasm(&wasm); let mut store = fuzz_config.to_store(); let module = compile_module(store.engine(), &wasm, KnownValid::No, &fuzz_config) .ok_or_else(|| wasmtime::format_err!("Compilation failed"))?; let mut linker = Linker::new(store.engine()); let check_ty = FuncType::new(store.engine(), [ValType::I32, ValType::I32], []); let check_func = Func::new(&mut store, check_ty, |_caller, params, _results| { let actual = params[0].unwrap_i32(); let expected = params[1].unwrap_i32(); assert_eq!(actual, expected, "check_i32 mismatch"); Ok(()) }); linker.define(&store, "", "check_i32", check_func).unwrap(); let instance = linker.instantiate(&mut store, &module).unwrap(); let run = instance.get_func(&mut store, "run").unwrap(); let mut results = [Val::I32(0)]; match run.call(&mut store, &[], &mut results) { Ok(()) => { let actual = results[0].unwrap_i32(); assert_eq!( actual, expected, "exception_ops: run returned {actual}, expected {expected} \ (one catch per scenario)" ); } Err(e) => { // AllocationTooLarge / GcHeapOutOfMemory are acceptable resource-limit traps. if let Some(trap) = e.downcast_ref::() { match trap { Trap::AllocationTooLarge => return Ok(()), _ => {} } } if e.is::>() { return Ok(()); } // Any other error (including ThrownException) is unexpected. panic!("exception_ops: unexpected error during execution: {e:?}"); } } Ok(()) } #[derive(Default)] struct HelperThread { state: Arc, thread: Option>, } #[derive(Default)] struct HelperThreadState { should_exit: Mutex, should_exit_cvar: Condvar, } impl HelperThread { fn run_periodically(&mut self, dur: Duration, mut closure: impl FnMut() + Send + 'static) { let state = self.state.clone(); self.thread = Some(std::thread::spawn(move || { // Using our mutex/condvar we wait here for the first of `dur` to // pass or the `HelperThread` instance to get dropped. let mut should_exit = state.should_exit.lock().unwrap(); while !*should_exit { let (lock, result) = state .should_exit_cvar .wait_timeout(should_exit, dur) .unwrap(); should_exit = lock; // If we timed out for sure then there's no need to continue // since we'll just abort on the next `checked_sub` anyway. if result.timed_out() { closure(); } } })); } } impl Drop for HelperThread { fn drop(&mut self) { let thread = match self.thread.take() { Some(thread) => thread, None => return, }; // Signal our thread that it should exit and wake it up in case it's // sleeping. *self.state.should_exit.lock().unwrap() = true; self.state.should_exit_cvar.notify_one(); // ... and then wait for the thread to exit to ensure we clean up // after ourselves. thread.join().unwrap(); } } /// Instantiates a wasm module and runs its exports with dummy values, all in /// an async fashion. /// /// Attempts to stress yields in host functions to ensure that exiting and /// resuming a wasm function call works. pub fn call_async(wasm: &[u8], config: &generators::Config, mut poll_amts: &[u32]) { let mut store = config.to_store(); let module = match compile_module(store.engine(), wasm, KnownValid::Yes, config) { Some(module) => module, None => return, }; // Configure a helper thread to periodically increment the epoch to // forcibly enable yields-via-epochs if epochs are in use. Note that this // is required because the wasm isn't otherwise guaranteed to necessarily // call any imports which will also increment the epoch. let mut helper_thread = HelperThread::default(); if let generators::AsyncConfig::YieldWithEpochs { dur, .. } = &config.wasmtime.async_config { let engine = store.engine().clone(); helper_thread.run_periodically(*dur, move || engine.increment_epoch()); } // Generate a `Linker` where all function imports are custom-built to yield // periodically and additionally increment the epoch. let mut imports = Vec::new(); for import in module.imports() { let item = match import.ty() { ExternType::Func(ty) => { let poll_amt = take_poll_amt(&mut poll_amts); Func::new_async(&mut store, ty.clone(), move |caller, _, results| { let ty = ty.clone(); Box::new(async move { caller.engine().increment_epoch(); log::info!("yielding {poll_amt} times in import"); YieldN(poll_amt).await; for (ret_ty, result) in ty.results().zip(results) { *result = ret_ty.default_value().unwrap(); } Ok(()) }) }) .into() } other_ty => match other_ty.default_value(&mut store) { Ok(item) => item, Err(e) => { log::warn!("couldn't create import for {import:?}: {e:?}"); return; } }, }; imports.push(item); } // Run the instantiation process, asynchronously, and if everything // succeeds then pull out the instance. // log::info!("starting instantiation"); let instance = block_on(Timeout { future: Instance::new_async(&mut store, &module, &imports), polls: take_poll_amt(&mut poll_amts), end: Instant::now() + Duration::from_millis(2_000), }); let instance = match instance { Ok(instantiation_result) => match unwrap_instance(&store, instantiation_result) { Some(instance) => instance, None => { log::info!("instantiation hit a nominal error"); return; // resource exhaustion or limits met } }, Err(_) => { log::info!("instantiation failed to complete"); return; // Timed out or ran out of polls } }; // Run each export of the instance in the same manner as instantiation // above. Dummy values are passed in for argument values here: // // TODO: this should probably be more clever about passing in arguments for // example they might be used as pointers or something and always using 0 // isn't too interesting. let funcs = instance .exports(&mut store) .filter_map(|e| { let name = e.name().to_string(); let func = e.into_extern().into_func()?; Some((name, func)) }) .collect::>(); for (name, func) in funcs { let ty = func.ty(&store); let params = ty .params() .map(|ty| ty.default_value().unwrap()) .collect::>(); let mut results = ty .results() .map(|ty| ty.default_value().unwrap()) .collect::>(); log::info!("invoking export {name:?}"); let future = func.call_async(&mut store, ¶ms, &mut results); match block_on(Timeout { future, polls: take_poll_amt(&mut poll_amts), end: Instant::now() + Duration::from_millis(2_000), }) { // On success or too many polls, try the next export. Ok(_) | Err(Exhausted::Polls) => {} // If time ran out then stop the current test case as we might have // already sucked up a lot of time for this fuzz test case so don't // keep it going. Err(Exhausted::Time) => return, } } fn take_poll_amt(polls: &mut &[u32]) -> u32 { match polls.split_first() { Some((a, rest)) => { *polls = rest; *a } None => 0, } } /// Helper future for applying a timeout to `future` up to either when `end` /// is the current time or `polls` polls happen. /// /// Note that this helps to time out infinite loops in wasm, for example. struct Timeout { future: F, /// If the future isn't ready by this time then the `Timeout` future /// will return `None`. end: Instant, /// If the future doesn't resolve itself in this many calls to `poll` /// then the `Timeout` future will return `None`. polls: u32, } enum Exhausted { Time, Polls, } impl Future for Timeout { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let (end, polls, future) = unsafe { let me = self.get_unchecked_mut(); (me.end, &mut me.polls, Pin::new_unchecked(&mut me.future)) }; match future.poll(cx) { Poll::Ready(val) => Poll::Ready(Ok(val)), Poll::Pending => { if Instant::now() >= end { log::warn!("future operation timed out"); return Poll::Ready(Err(Exhausted::Time)); } if *polls == 0 { log::warn!("future operation ran out of polls"); return Poll::Ready(Err(Exhausted::Polls)); } *polls -= 1; Poll::Pending } } } } } #[cfg(test)] mod tests { use super::*; use crate::test::{gen_until_pass, test_n_times}; use wasmparser::{Validator, WasmFeatures}; // Test that the `gc_ops` fuzzer eventually runs the gc function in the host. // We've historically had issues where this fuzzer accidentally wasn't fuzzing // anything for a long time so this is an attempt to prevent that from happening // again. #[test] fn gc_ops_eventually_gcs() { // Skip if we're under emulation because some fuzz configurations will do // large address space reservations that QEMU doesn't handle well. if std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok() { return; } let ok = gen_until_pass(|(config, test), _| { let result = gc_ops(config, test)?; Ok(result > 0) }); if !ok { panic!("gc was never found"); } } #[test] fn module_generation_uses_expected_proposals() { // Proposals that Wasmtime supports. Eventually a module should be // generated that needs these proposals. let mut expected = WasmFeatures::MUTABLE_GLOBAL | WasmFeatures::FLOATS | WasmFeatures::SIGN_EXTENSION | WasmFeatures::SATURATING_FLOAT_TO_INT | WasmFeatures::MULTI_VALUE | WasmFeatures::BULK_MEMORY | WasmFeatures::REFERENCE_TYPES | WasmFeatures::SIMD | WasmFeatures::MULTI_MEMORY | WasmFeatures::RELAXED_SIMD | WasmFeatures::TAIL_CALL | WasmFeatures::WIDE_ARITHMETIC | WasmFeatures::MEMORY64 | WasmFeatures::FUNCTION_REFERENCES | WasmFeatures::GC | WasmFeatures::GC_TYPES | WasmFeatures::CUSTOM_PAGE_SIZES | WasmFeatures::EXTENDED_CONST | WasmFeatures::EXCEPTIONS; // All other features that wasmparser supports, which is presumably a // superset of the features that wasm-smith supports, are listed here as // unexpected. This means, for example, that if wasm-smith updates to // include a new proposal by default that wasmtime implements then it // will be required to be listed above. let unexpected = WasmFeatures::all() ^ expected; let ok = gen_until_pass(|config: generators::Config, u| { let wasm = config.generate(u, None)?.to_bytes(); // Double-check the module is valid Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; // If any of the unexpected features are removed then this module // should always be valid, otherwise something went wrong. for feature in unexpected.iter() { let ok = Validator::new_with_features(WasmFeatures::all() ^ feature).validate_all(&wasm); if ok.is_err() { wasmtime::bail!("generated a module with {feature:?} but that wasn't expected"); } } // If any of `expected` is removed and the module fails to validate, // then that means the module requires that feature. Remove that // from the set of features we're then expecting. for feature in expected.iter() { let ok = Validator::new_with_features(WasmFeatures::all() ^ feature).validate_all(&wasm); if ok.is_err() { expected ^= feature; } } Ok(expected.is_empty()) }); if !ok { panic!("never generated wasm module using {expected:?}"); } } #[test] fn wast_smoke_test() { test_n_times(50, |(), u| super::wast_test(u)); } }