Path: blob/main/crates/fuzzing/src/oracles.rs
1693 views
//! Oracles.1//!2//! Oracles take a test case and determine whether we have a bug. For example,3//! one of the simplest oracles is to take a Wasm binary as our input test case,4//! validate and instantiate it, and (implicitly) check that no assertions5//! failed or segfaults happened. A more complicated oracle might compare the6//! result of executing a Wasm file with and without optimizations enabled, and7//! make sure that the two executions are observably identical.8//!9//! When an oracle finds a bug, it should report it to the fuzzing engine by10//! panicking.1112#[cfg(feature = "fuzz-spec-interpreter")]13pub mod diff_spec;14pub mod diff_wasmi;15pub mod diff_wasmtime;16pub mod dummy;17pub mod engine;18pub mod memory;19mod stacks;2021use self::diff_wasmtime::WasmtimeInstance;22use self::engine::{DiffEngine, DiffInstance};23use crate::generators::{self, CompilerStrategy, DiffValue, DiffValueType};24use crate::single_module_fuzzer::KnownValid;25use arbitrary::Arbitrary;26pub use stacks::check_stacks;27use std::future::Future;28use std::pin::Pin;29use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst};30use std::sync::{Arc, Condvar, Mutex};31use std::task::{Context, Poll, Waker};32use std::time::{Duration, Instant};33use wasmtime::*;34use wasmtime_wast::WastContext;3536#[cfg(not(any(windows, target_arch = "s390x", target_arch = "riscv64")))]37mod diff_v8;3839static CNT: AtomicUsize = AtomicUsize::new(0);4041/// Logs a wasm file to the filesystem to make it easy to figure out what wasm42/// was used when debugging.43pub fn log_wasm(wasm: &[u8]) {44super::init_fuzzing();4546if !log::log_enabled!(log::Level::Debug) {47return;48}4950let i = CNT.fetch_add(1, SeqCst);51let name = format!("testcase{i}.wasm");52std::fs::write(&name, wasm).expect("failed to write wasm file");53log::debug!("wrote wasm file to `{name}`");54let wat = format!("testcase{i}.wat");55match wasmprinter::print_bytes(wasm) {56Ok(s) => std::fs::write(&wat, s).expect("failed to write wat file"),57// If wasmprinter failed remove a `*.wat` file, if any, to avoid58// confusing a preexisting one with this wasm which failed to get59// printed.60Err(_) => drop(std::fs::remove_file(&wat)),61}62}6364/// The `T` in `Store<T>` for fuzzing stores, used to limit resource65/// consumption during fuzzing.66#[derive(Clone)]67pub struct StoreLimits(Arc<LimitsState>);6869struct LimitsState {70/// Remaining memory, in bytes, left to allocate71remaining_memory: AtomicUsize,72/// Remaining amount of memory that's allowed to be copied via a growth.73remaining_copy_allowance: AtomicUsize,74/// Whether or not an allocation request has been denied75oom: AtomicBool,76}7778/// Allow up to 1G which is well below the 2G limit on OSS-Fuzz and should allow79/// most interesting behavior.80const MAX_MEMORY: usize = 1 << 30;8182/// Allow up to 4G of bytes to be copied (conservatively) which should enable83/// growth up to `MAX_MEMORY` or at least up to a relatively large amount.84const MAX_MEMORY_MOVED: usize = 4 << 30;8586impl StoreLimits {87/// Creates the default set of limits for all fuzzing stores.88pub fn new() -> StoreLimits {89StoreLimits(Arc::new(LimitsState {90remaining_memory: AtomicUsize::new(MAX_MEMORY),91remaining_copy_allowance: AtomicUsize::new(MAX_MEMORY_MOVED),92oom: AtomicBool::new(false),93}))94}9596fn alloc(&mut self, amt: usize) -> bool {97log::trace!("alloc {amt:#x} bytes");9899// Assume that on each allocation of memory that all previous100// allocations of memory are moved. This is pretty coarse but is used to101// help prevent against fuzz test cases that just move tons of bytes102// around continuously. This assumes that all previous memory was103// allocated in a single linear memory and growing by `amt` will require104// moving all the bytes to a new location. This isn't actually required105// all the time nor does it accurately reflect what happens all the106// time, but it's a coarse approximation that should be "good enough"107// for allowing interesting fuzz behaviors to happen while not timing108// out just copying bytes around.109let prev_size = MAX_MEMORY - self.0.remaining_memory.load(SeqCst);110if self111.0112.remaining_copy_allowance113.fetch_update(SeqCst, SeqCst, |remaining| remaining.checked_sub(prev_size))114.is_err()115{116self.0.oom.store(true, SeqCst);117log::debug!("-> too many bytes moved, rejecting allocation");118return false;119}120121// If we're allowed to move the bytes, then also check if we're allowed122// to actually have this much residence at once.123match self124.0125.remaining_memory126.fetch_update(SeqCst, SeqCst, |remaining| remaining.checked_sub(amt))127{128Ok(_) => true,129Err(_) => {130self.0.oom.store(true, SeqCst);131log::debug!("-> OOM hit");132false133}134}135}136137fn is_oom(&self) -> bool {138self.0.oom.load(SeqCst)139}140}141142impl ResourceLimiter for StoreLimits {143fn memory_growing(144&mut self,145current: usize,146desired: usize,147_maximum: Option<usize>,148) -> Result<bool> {149Ok(self.alloc(desired - current))150}151152fn table_growing(153&mut self,154current: usize,155desired: usize,156_maximum: Option<usize>,157) -> Result<bool> {158let delta = (desired - current).saturating_mul(std::mem::size_of::<usize>());159Ok(self.alloc(delta))160}161}162163/// Methods of timing out execution of a WebAssembly module164#[derive(Clone, Debug)]165pub enum Timeout {166/// No timeout is used, it should be guaranteed via some other means that167/// the input does not infinite loop.168None,169/// Fuel-based timeouts are used where the specified fuel is all that the170/// provided wasm module is allowed to consume.171Fuel(u64),172/// An epoch-interruption-based timeout is used with a sleeping173/// thread bumping the epoch counter after the specified duration.174Epoch(Duration),175}176177/// Instantiate the Wasm buffer, and implicitly fail if we have an unexpected178/// panic or segfault or anything else that can be detected "passively".179///180/// The engine will be configured using provided config.181pub fn instantiate(182wasm: &[u8],183known_valid: KnownValid,184config: &generators::Config,185timeout: Timeout,186) {187let mut store = config.to_store();188189let module = match compile_module(store.engine(), wasm, known_valid, config) {190Some(module) => module,191None => return,192};193194let mut timeout_state = HelperThread::default();195match timeout {196Timeout::Fuel(fuel) => store.set_fuel(fuel).unwrap(),197198// If a timeout is requested then we spawn a helper thread to wait for199// the requested time and then send us a signal to get interrupted. We200// also arrange for the thread's sleep to get interrupted if we return201// early (or the wasm returns within the time limit), which allows the202// thread to get torn down.203//204// This prevents us from creating a huge number of sleeping threads if205// this function is executed in a loop, like it does on nightly fuzzing206// infrastructure.207Timeout::Epoch(timeout) => {208let engine = store.engine().clone();209timeout_state.run_periodically(timeout, move || engine.increment_epoch());210}211Timeout::None => {}212}213214instantiate_with_dummy(&mut store, &module);215}216217/// Represents supported commands to the `instantiate_many` function.218#[derive(Arbitrary, Debug)]219pub enum Command {220/// Instantiates a module.221///222/// The value is the index of the module to instantiate.223///224/// The module instantiated will be this value modulo the number of modules provided to `instantiate_many`.225Instantiate(usize),226/// Terminates a "running" instance.227///228/// The value is the index of the instance to terminate.229///230/// The instance terminated will be this value modulo the number of currently running231/// instances.232///233/// If no instances are running, the command will be ignored.234Terminate(usize),235}236237/// Instantiates many instances from the given modules.238///239/// The engine will be configured using the provided config.240///241/// The modules are expected to *not* have start functions as no timeouts are configured.242pub fn instantiate_many(243modules: &[Vec<u8>],244known_valid: KnownValid,245config: &generators::Config,246commands: &[Command],247) {248log::debug!("instantiate_many: {commands:#?}");249250assert!(!config.module_config.config.allow_start_export);251252let engine = Engine::new(&config.to_wasmtime()).unwrap();253254let modules = modules255.iter()256.enumerate()257.filter_map(258|(i, bytes)| match compile_module(&engine, bytes, known_valid, config) {259Some(m) => {260log::debug!("successfully compiled module {i}");261Some(m)262}263None => {264log::debug!("failed to compile module {i}");265None266}267},268)269.collect::<Vec<_>>();270271// If no modules were valid, we're done272if modules.is_empty() {273return;274}275276// This stores every `Store` where a successful instantiation takes place277let mut stores = Vec::new();278let limits = StoreLimits::new();279280for command in commands {281match command {282Command::Instantiate(index) => {283let index = *index % modules.len();284log::info!("instantiating {index}");285let module = &modules[index];286let mut store = Store::new(&engine, limits.clone());287config.configure_store(&mut store);288289if instantiate_with_dummy(&mut store, module).is_some() {290stores.push(Some(store));291} else {292log::warn!("instantiation failed");293}294}295Command::Terminate(index) => {296if stores.is_empty() {297continue;298}299let index = *index % stores.len();300301log::info!("dropping {index}");302stores.swap_remove(index);303}304}305}306}307308fn compile_module(309engine: &Engine,310bytes: &[u8],311known_valid: KnownValid,312config: &generators::Config,313) -> Option<Module> {314log_wasm(bytes);315316fn is_pcc_error(e: &anyhow::Error) -> bool {317// NOTE: please keep this predicate in sync with the display format of CodegenError,318// defined in `wasmtime/cranelift/codegen/src/result.rs`319e.to_string().to_lowercase().contains("proof-carrying-code")320}321322match config.compile(engine, bytes) {323Ok(module) => Some(module),324Err(e) if is_pcc_error(&e) => {325panic!("pcc error in input: {e:#?}");326}327Err(_) if known_valid == KnownValid::No => None,328Err(e) => {329if let generators::InstanceAllocationStrategy::Pooling(c) = &config.wasmtime.strategy {330// When using the pooling allocator, accept failures to compile331// when arbitrary table element limits have been exceeded as332// there is currently no way to constrain the generated module333// table types.334let string = format!("{e:?}");335if string.contains("minimum element size") {336return None;337}338339// Allow modules-failing-to-compile which exceed the requested340// size for each instance. This is something that is difficult341// to control and ensure it always succeeds, so we simply have a342// "random" instance size limit and if a module doesn't fit we343// move on to the next fuzz input.344if string.contains("instance allocation for this module requires") {345return None;346}347348// If the pooling allocator is more restrictive on the number of349// tables and memories than we allowed wasm-smith to generate350// then allow compilation errors along those lines.351if c.max_tables_per_module < (config.module_config.config.max_tables as u32)352&& string.contains("defined tables count")353&& string.contains("exceeds the per-instance limit")354{355return None;356}357358if c.max_memories_per_module < (config.module_config.config.max_memories as u32)359&& string.contains("defined memories count")360&& string.contains("exceeds the per-instance limit")361{362return None;363}364}365366panic!("failed to compile module: {e:?}");367}368}369}370371/// Create a Wasmtime [`Instance`] from a [`Module`] and fill in all imports372/// with dummy values (e.g., zeroed values, immediately-trapping functions).373/// Also, this function catches certain fuzz-related instantiation failures and374/// returns `None` instead of panicking.375///376/// TODO: we should implement tracing versions of these dummy imports that377/// record a trace of the order that imported functions were called in and with378/// what values. Like the results of exported functions, calls to imports should379/// also yield the same values for each configuration, and we should assert380/// that.381pub fn instantiate_with_dummy(store: &mut Store<StoreLimits>, module: &Module) -> Option<Instance> {382// Creation of imports can fail due to resource limit constraints, and then383// instantiation can naturally fail for a number of reasons as well. Bundle384// the two steps together to match on the error below.385let linker = dummy::dummy_linker(store, module);386if let Err(e) = &linker {387log::warn!("failed to create dummy linker: {e:?}");388}389let instance = linker.and_then(|l| l.instantiate(&mut *store, module));390unwrap_instance(store, instance)391}392393fn unwrap_instance(394store: &Store<StoreLimits>,395instance: anyhow::Result<Instance>,396) -> Option<Instance> {397let e = match instance {398Ok(i) => return Some(i),399Err(e) => e,400};401402log::debug!("failed to instantiate: {e:?}");403404// If the instantiation hit OOM for some reason then that's ok, it's405// expected that fuzz-generated programs try to allocate lots of406// stuff.407if store.data().is_oom() {408return None;409}410411// Allow traps which can happen normally with `unreachable` or a timeout or412// such.413if e.is::<Trap>()414// Also allow failures to instantiate as a result of hitting pooling415// limits.416|| e.is::<wasmtime::PoolConcurrencyLimitError>()417// And GC heap OOMs.418|| e.is::<wasmtime::GcHeapOutOfMemory<()>>()419// And thrown exceptions.420|| e.is::<wasmtime::ThrownException>()421{422return None;423}424425let string = e.to_string();426427// Currently we instantiate with a `Linker` which can't instantiate428// every single module under the sun due to using name-based resolution429// rather than positional-based resolution430if string.contains("incompatible import type") {431return None;432}433434// Everything else should be a bug in the fuzzer or a bug in wasmtime435panic!("failed to instantiate: {e:?}");436}437438/// Evaluate the function identified by `name` in two different engine439/// instances--`lhs` and `rhs`.440///441/// Returns `Ok(true)` if more evaluations can happen or `Ok(false)` if the442/// instances may have drifted apart and no more evaluations can happen.443///444/// # Panics445///446/// This will panic if the evaluation is different between engines (e.g.,447/// results are different, hashed instance is different, one side traps, etc.).448pub fn differential(449lhs: &mut dyn DiffInstance,450lhs_engine: &dyn DiffEngine,451rhs: &mut WasmtimeInstance,452name: &str,453args: &[DiffValue],454result_tys: &[DiffValueType],455) -> anyhow::Result<bool> {456log::debug!("Evaluating: `{name}` with {args:?}");457let lhs_results = match lhs.evaluate(name, args, result_tys) {458Ok(Some(results)) => Ok(results),459Err(e) => Err(e),460// this engine couldn't execute this type signature, so discard this461// execution by returning success.462Ok(None) => return Ok(true),463};464log::debug!(" -> lhs results on {}: {:?}", lhs.name(), &lhs_results);465466let rhs_results = rhs467.evaluate(name, args, result_tys)468// wasmtime should be able to invoke any signature, so unwrap this result469.map(|results| results.unwrap());470log::debug!(" -> rhs results on {}: {:?}", rhs.name(), &rhs_results);471472// If Wasmtime hit its OOM condition, which is possible since it's set473// somewhat low while fuzzing, then don't return an error but return474// `false` indicating that differential fuzzing must stop. There's no475// guarantee the other engine has the same OOM limits as Wasmtime, and476// it's assumed that Wasmtime is configured to have a more conservative477// limit than the other engine.478if rhs.is_oom() {479return Ok(false);480}481482match DiffEqResult::new(lhs_engine, lhs_results, rhs_results) {483DiffEqResult::Success(lhs, rhs) => assert_eq!(lhs, rhs),484DiffEqResult::Poisoned => return Ok(false),485DiffEqResult::Failed => {}486}487488for (global, ty) in rhs.exported_globals() {489log::debug!("Comparing global `{global}`");490let lhs = match lhs.get_global(&global, ty) {491Some(val) => val,492None => continue,493};494let rhs = rhs.get_global(&global, ty).unwrap();495assert_eq!(lhs, rhs);496}497for (memory, shared) in rhs.exported_memories() {498log::debug!("Comparing memory `{memory}`");499let lhs = match lhs.get_memory(&memory, shared) {500Some(val) => val,501None => continue,502};503let rhs = rhs.get_memory(&memory, shared).unwrap();504if lhs == rhs {505continue;506}507eprintln!("differential memory is {} bytes long", lhs.len());508eprintln!("wasmtime memory is {} bytes long", rhs.len());509panic!("memories have differing values");510}511512Ok(true)513}514515/// Result of comparing the result of two operations during differential516/// execution.517pub enum DiffEqResult<T, U> {518/// Both engines succeeded.519Success(T, U),520/// The result has reached the state where engines may have diverged and521/// results can no longer be compared.522Poisoned,523/// Both engines failed with the same error message, and internal state524/// should still match between the two engines.525Failed,526}527528fn wasmtime_trap_is_non_deterministic(trap: &Trap) -> bool {529match trap {530// Allocations being too large for the GC are531// implementation-defined.532Trap::AllocationTooLarge |533// Stack size, and therefore when overflow happens, is534// implementation-defined.535Trap::StackOverflow => true,536_ => false,537}538}539540fn wasmtime_error_is_non_deterministic(error: &wasmtime::Error) -> bool {541match error.downcast_ref::<Trap>() {542Some(trap) => wasmtime_trap_is_non_deterministic(trap),543544// For general, unknown errors, we can't rely on this being545// a deterministic Wasm failure that both engines handled546// identically, leaving Wasm in identical states. We could547// just as easily be hitting engine-specific failures, like548// different implementation-defined limits. So simply poison549// this execution and move on to the next test.550None => true,551}552}553554impl<T, U> DiffEqResult<T, U> {555/// Computes the differential result from executing in two different556/// engines.557pub fn new(558lhs_engine: &dyn DiffEngine,559lhs_result: Result<T>,560rhs_result: Result<U>,561) -> DiffEqResult<T, U> {562match (lhs_result, rhs_result) {563(Ok(lhs_result), Ok(rhs_result)) => DiffEqResult::Success(lhs_result, rhs_result),564565// Handle all non-deterministic errors by poisoning this execution's566// state, so that we simply move on to the next test.567(Err(lhs), _) if lhs_engine.is_non_deterministic_error(&lhs) => {568log::debug!("lhs failed non-deterministically: {lhs:?}");569DiffEqResult::Poisoned570}571(_, Err(rhs)) if wasmtime_error_is_non_deterministic(&rhs) => {572log::debug!("rhs failed non-deterministically: {rhs:?}");573DiffEqResult::Poisoned574}575576// Both sides failed deterministically. Check that the trap and577// state at the time of failure is the same.578(Err(lhs), Err(rhs)) => {579let rhs = rhs580.downcast::<Trap>()581.expect("non-traps handled in earlier match arm");582583debug_assert!(584!lhs_engine.is_non_deterministic_error(&lhs),585"non-deterministic traps handled in earlier match arm",586);587debug_assert!(588!wasmtime_trap_is_non_deterministic(&rhs),589"non-deterministic traps handled in earlier match arm",590);591592lhs_engine.assert_error_match(&lhs, &rhs);593DiffEqResult::Failed594}595596// A real bug is found if only one side fails.597(Ok(_), Err(err)) => panic!("only the `rhs` failed for this input: {err:?}"),598(Err(err), Ok(_)) => panic!("only the `lhs` failed for this input: {err:?}"),599}600}601}602603/// Invoke the given API calls.604pub fn make_api_calls(api: generators::api::ApiCalls) {605use crate::generators::api::ApiCall;606use std::collections::HashMap;607608let mut store: Option<Store<StoreLimits>> = None;609let mut modules: HashMap<usize, Module> = Default::default();610let mut instances: HashMap<usize, Instance> = Default::default();611612for call in api.calls {613match call {614ApiCall::StoreNew(config) => {615log::trace!("creating store");616assert!(store.is_none());617store = Some(config.to_store());618}619620ApiCall::ModuleNew { id, wasm } => {621log::debug!("creating module: {id}");622log_wasm(&wasm);623let module = match Module::new(store.as_ref().unwrap().engine(), &wasm) {624Ok(m) => m,625Err(_) => continue,626};627let old = modules.insert(id, module);628assert!(old.is_none());629}630631ApiCall::ModuleDrop { id } => {632log::trace!("dropping module: {id}");633drop(modules.remove(&id));634}635636ApiCall::InstanceNew { id, module } => {637log::trace!("instantiating module {module} as {id}");638let module = match modules.get(&module) {639Some(m) => m,640None => continue,641};642643let store = store.as_mut().unwrap();644if let Some(instance) = instantiate_with_dummy(store, module) {645instances.insert(id, instance);646}647}648649ApiCall::InstanceDrop { id } => {650log::trace!("dropping instance {id}");651instances.remove(&id);652}653654ApiCall::CallExportedFunc { instance, nth } => {655log::trace!("calling instance export {instance} / {nth}");656let instance = match instances.get(&instance) {657Some(i) => i,658None => {659// Note that we aren't guaranteed to instantiate valid660// modules, see comments in `InstanceNew` for details on661// that. But the API call generator can't know if662// instantiation failed, so we might not actually have663// this instance. When that's the case, just skip the664// API call and keep going.665continue;666}667};668let store = store.as_mut().unwrap();669670let funcs = instance671.exports(&mut *store)672.filter_map(|e| match e.into_extern() {673Extern::Func(f) => Some(f),674_ => None,675})676.collect::<Vec<_>>();677678if funcs.is_empty() {679continue;680}681682let nth = nth % funcs.len();683let f = &funcs[nth];684let ty = f.ty(&store);685if let Some(params) = ty686.params()687.map(|p| p.default_value())688.collect::<Option<Vec<_>>>()689{690let mut results = vec![Val::I32(0); ty.results().len()];691let _ = f.call(store, ¶ms, &mut results);692}693}694}695}696}697698/// Executes the wast `test` with the `config` specified.699///700/// Ensures that wast tests pass regardless of the `Config`.701pub fn wast_test(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {702crate::init_fuzzing();703704let mut fuzz_config: generators::Config = u.arbitrary()?;705let test: generators::WastTest = u.arbitrary()?;706707let test = &test.test;708709if test.config.component_model_async() || u.arbitrary()? {710fuzz_config.enable_async(u)?;711}712713// Discard tests that allocate a lot of memory as we don't want to OOM the714// fuzzer and we also limit memory growth which would cause the test to715// fail.716if test.config.hogs_memory.unwrap_or(false) {717return Err(arbitrary::Error::IncorrectFormat);718}719720// Transform `fuzz_config` to be valid for `test` and make sure that this721// test is supposed to pass.722let wast_config = fuzz_config.make_wast_test_compliant(test);723if test.should_fail(&wast_config) {724return Err(arbitrary::Error::IncorrectFormat);725}726727// Winch requires AVX and AVX2 for SIMD tests to pass so don't run the test728// if either isn't enabled.729if fuzz_config.wasmtime.compiler_strategy == CompilerStrategy::Winch730&& test.config.simd()731&& (fuzz_config732.wasmtime733.codegen_flag("has_avx")734.is_some_and(|value| value == "false")735|| fuzz_config736.wasmtime737.codegen_flag("has_avx2")738.is_some_and(|value| value == "false"))739{740log::warn!(741"Skipping Wast test because Winch doesn't support SIMD tests with AVX or AVX2 disabled"742);743return Err(arbitrary::Error::IncorrectFormat);744}745746// Fuel and epochs don't play well with threads right now, so exclude any747// thread-spawning test if it looks like threads are spawned in that case.748if fuzz_config.wasmtime.consume_fuel || fuzz_config.wasmtime.epoch_interruption {749if test.contents.contains("(thread") {750return Err(arbitrary::Error::IncorrectFormat);751}752}753754log::debug!("running {:?}", test.path);755let async_ = if fuzz_config.wasmtime.async_config == generators::AsyncConfig::Disabled {756wasmtime_wast::Async::No757} else {758wasmtime_wast::Async::Yes759};760let mut wast_context = WastContext::new(fuzz_config.to_store(), async_);761wast_context762.register_spectest(&wasmtime_wast::SpectestConfig {763use_shared_memory: true,764suppress_prints: true,765})766.unwrap();767wast_context768.run_wast(test.path.to_str().unwrap(), test.contents.as_bytes())769.unwrap();770Ok(())771}772773/// Execute a series of `table.get` and `table.set` operations.774///775/// Returns the number of `gc` operations which occurred throughout the test776/// case -- used to test below that gc happens reasonably soon and eventually.777pub fn table_ops(778mut fuzz_config: generators::Config,779mut ops: generators::table_ops::TableOps,780) -> Result<usize> {781let expected_drops = Arc::new(AtomicUsize::new(0));782let num_dropped = Arc::new(AtomicUsize::new(0));783784let num_gcs = Arc::new(AtomicUsize::new(0));785{786fuzz_config.wasmtime.consume_fuel = true;787let mut store = fuzz_config.to_store();788store.set_fuel(1_000).unwrap();789790let wasm = ops.to_wasm_binary();791log_wasm(&wasm);792let module = match compile_module(store.engine(), &wasm, KnownValid::No, &fuzz_config) {793Some(m) => m,794None => return Ok(0),795};796797let mut linker = Linker::new(store.engine());798799// To avoid timeouts, limit the number of explicit GCs we perform per800// test case.801const MAX_GCS: usize = 5;802803let func_ty = FuncType::new(804store.engine(),805vec![],806vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],807);808let func = Func::new(&mut store, func_ty, {809let num_dropped = num_dropped.clone();810let expected_drops = expected_drops.clone();811let num_gcs = num_gcs.clone();812move |mut caller: Caller<'_, StoreLimits>, _params, results| {813log::info!("table_ops: GC");814if num_gcs.fetch_add(1, SeqCst) < MAX_GCS {815caller.gc(None);816}817818let a = ExternRef::new(819&mut caller,820CountDrops::new(&expected_drops, num_dropped.clone()),821)?;822let b = ExternRef::new(823&mut caller,824CountDrops::new(&expected_drops, num_dropped.clone()),825)?;826let c = ExternRef::new(827&mut caller,828CountDrops::new(&expected_drops, num_dropped.clone()),829)?;830831log::info!("table_ops: gc() -> ({a:?}, {b:?}, {c:?})");832results[0] = Some(a).into();833results[1] = Some(b).into();834results[2] = Some(c).into();835Ok(())836}837});838linker.define(&store, "", "gc", func).unwrap();839840linker841.func_wrap("", "take_refs", {842let expected_drops = expected_drops.clone();843move |caller: Caller<'_, StoreLimits>,844a: Option<Rooted<ExternRef>>,845b: Option<Rooted<ExternRef>>,846c: Option<Rooted<ExternRef>>|847-> Result<()> {848log::info!("table_ops: take_refs({a:?}, {b:?}, {c:?})",);849850// Do the assertion on each ref's inner data, even though it851// all points to the same atomic, so that if we happen to852// run into a use-after-free bug with one of these refs we853// are more likely to trigger a segfault.854if let Some(a) = a {855let a = a856.data(&caller)?857.unwrap()858.downcast_ref::<CountDrops>()859.unwrap();860assert!(a.0.load(SeqCst) <= expected_drops.load(SeqCst));861}862if let Some(b) = b {863let b = b864.data(&caller)?865.unwrap()866.downcast_ref::<CountDrops>()867.unwrap();868assert!(b.0.load(SeqCst) <= expected_drops.load(SeqCst));869}870if let Some(c) = c {871let c = c872.data(&caller)?873.unwrap()874.downcast_ref::<CountDrops>()875.unwrap();876assert!(c.0.load(SeqCst) <= expected_drops.load(SeqCst));877}878Ok(())879}880})881.unwrap();882883let func_ty = FuncType::new(884store.engine(),885vec![],886vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],887);888let func = Func::new(&mut store, func_ty, {889let num_dropped = num_dropped.clone();890let expected_drops = expected_drops.clone();891move |mut caller, _params, results| {892log::info!("table_ops: make_refs");893894let a = ExternRef::new(895&mut caller,896CountDrops::new(&expected_drops, num_dropped.clone()),897)?;898let b = ExternRef::new(899&mut caller,900CountDrops::new(&expected_drops, num_dropped.clone()),901)?;902let c = ExternRef::new(903&mut caller,904CountDrops::new(&expected_drops, num_dropped.clone()),905)?;906907log::info!("table_ops: make_refs() -> ({a:?}, {b:?}, {c:?})");908909results[0] = Some(a).into();910results[1] = Some(b).into();911results[2] = Some(c).into();912913Ok(())914}915});916linker.define(&store, "", "make_refs", func).unwrap();917918let instance = linker.instantiate(&mut store, &module).unwrap();919let run = instance.get_func(&mut store, "run").unwrap();920921{922let mut scope = RootScope::new(&mut store);923924log::info!(925"table_ops: begin allocating {} externref arguments",926ops.limits.num_globals927);928let args: Vec<_> = (0..ops.limits.num_params)929.map(|_| {930Ok(Val::ExternRef(Some(ExternRef::new(931&mut scope,932CountDrops::new(&expected_drops, num_dropped.clone()),933)?)))934})935.collect::<Result<_>>()?;936log::info!(937"table_ops: end allocating {} externref arguments",938ops.limits.num_globals939);940941// The generated function should always return a trap. The only two942// valid traps are table-out-of-bounds which happens through `table.get`943// and `table.set` generated or an out-of-fuel trap. Otherwise any other944// error is unexpected and should fail fuzzing.945log::info!("table_ops: calling into Wasm `run` function");946let err = run.call(&mut scope, &args, &mut []).unwrap_err();947match err.downcast::<GcHeapOutOfMemory<CountDrops>>() {948Ok(_oom) => {}949Err(err) => {950let trap = err951.downcast::<Trap>()952.expect("if not GC oom, error should be a Wasm trap");953match trap {954Trap::TableOutOfBounds | Trap::OutOfFuel => {}955_ => panic!("unexpected trap: {trap}"),956}957}958}959}960961// Do a final GC after running the Wasm.962store.gc(None);963}964965assert_eq!(num_dropped.load(SeqCst), expected_drops.load(SeqCst));966return Ok(num_gcs.load(SeqCst));967968struct CountDrops(Arc<AtomicUsize>);969970impl CountDrops {971fn new(expected_drops: &AtomicUsize, num_dropped: Arc<AtomicUsize>) -> Self {972let expected = expected_drops.fetch_add(1, SeqCst);973log::info!(974"CountDrops::new: expected drops: {expected} -> {}",975expected + 1976);977Self(num_dropped)978}979}980981impl Drop for CountDrops {982fn drop(&mut self) {983let drops = self.0.fetch_add(1, SeqCst);984log::info!("CountDrops::drop: actual drops: {drops} -> {}", drops + 1);985}986}987}988989#[derive(Default)]990struct HelperThread {991state: Arc<HelperThreadState>,992thread: Option<std::thread::JoinHandle<()>>,993}994995#[derive(Default)]996struct HelperThreadState {997should_exit: Mutex<bool>,998should_exit_cvar: Condvar,999}10001001impl HelperThread {1002fn run_periodically(&mut self, dur: Duration, mut closure: impl FnMut() + Send + 'static) {1003let state = self.state.clone();1004self.thread = Some(std::thread::spawn(move || {1005// Using our mutex/condvar we wait here for the first of `dur` to1006// pass or the `HelperThread` instance to get dropped.1007let mut should_exit = state.should_exit.lock().unwrap();1008while !*should_exit {1009let (lock, result) = state1010.should_exit_cvar1011.wait_timeout(should_exit, dur)1012.unwrap();1013should_exit = lock;1014// If we timed out for sure then there's no need to continue1015// since we'll just abort on the next `checked_sub` anyway.1016if result.timed_out() {1017closure();1018}1019}1020}));1021}1022}10231024impl Drop for HelperThread {1025fn drop(&mut self) {1026let thread = match self.thread.take() {1027Some(thread) => thread,1028None => return,1029};1030// Signal our thread that it should exit and wake it up in case it's1031// sleeping.1032*self.state.should_exit.lock().unwrap() = true;1033self.state.should_exit_cvar.notify_one();10341035// ... and then wait for the thread to exit to ensure we clean up1036// after ourselves.1037thread.join().unwrap();1038}1039}10401041/// Generate and execute a `crate::generators::component_types::TestCase` using the specified `input` to create1042/// arbitrary types and values.1043pub fn dynamic_component_api_target(input: &mut arbitrary::Unstructured) -> arbitrary::Result<()> {1044use crate::generators::component_types;1045use wasmtime::component::{Component, Linker, Val};1046use wasmtime_test_util::component::FuncExt;1047use wasmtime_test_util::component_fuzz::{1048EXPORT_FUNCTION, IMPORT_FUNCTION, MAX_TYPE_DEPTH, TestCase, Type,1049};10501051crate::init_fuzzing();10521053let mut types = Vec::new();1054let mut type_fuel = 500;10551056for _ in 0..5 {1057types.push(Type::generate(input, MAX_TYPE_DEPTH, &mut type_fuel)?);1058}1059let params = (0..input.int_in_range(0..=5)?)1060.map(|_| input.choose(&types))1061.collect::<arbitrary::Result<Vec<_>>>()?;1062let result = if input.arbitrary()? {1063Some(input.choose(&types)?)1064} else {1065None1066};10671068let case = TestCase {1069params,1070result,1071encoding1: input.arbitrary()?,1072encoding2: input.arbitrary()?,1073};10741075let mut config = wasmtime_test_util::component::config();1076config.debug_adapter_modules(input.arbitrary()?);1077let engine = Engine::new(&config).unwrap();1078let mut store = Store::new(&engine, (Vec::new(), None));1079let wat = case.declarations().make_component();1080let wat = wat.as_bytes();1081log_wasm(wat);1082let component = Component::new(&engine, wat).unwrap();1083let mut linker = Linker::new(&engine);10841085linker1086.root()1087.func_new(IMPORT_FUNCTION, {1088move |mut cx: StoreContextMut<'_, (Vec<Val>, Option<Vec<Val>>)>,1089params: &[Val],1090results: &mut [Val]|1091-> Result<()> {1092log::trace!("received params {params:?}");1093let (expected_args, expected_results) = cx.data_mut();1094assert_eq!(params.len(), expected_args.len());1095for (expected, actual) in expected_args.iter().zip(params) {1096assert_eq!(expected, actual);1097}1098results.clone_from_slice(&expected_results.take().unwrap());1099log::trace!("returning results {results:?}");1100Ok(())1101}1102})1103.unwrap();11041105let instance = linker.instantiate(&mut store, &component).unwrap();1106let func = instance.get_func(&mut store, EXPORT_FUNCTION).unwrap();1107let param_tys = func.params(&store);1108let result_tys = func.results(&store);11091110while input.arbitrary()? {1111let params = param_tys1112.iter()1113.map(|(_, ty)| component_types::arbitrary_val(ty, input))1114.collect::<arbitrary::Result<Vec<_>>>()?;1115let results = result_tys1116.iter()1117.map(|ty| component_types::arbitrary_val(ty, input))1118.collect::<arbitrary::Result<Vec<_>>>()?;11191120*store.data_mut() = (params.clone(), Some(results.clone()));11211122log::trace!("passing params {params:?}");1123let mut actual = vec![Val::Bool(false); results.len()];1124func.call_and_post_return(&mut store, ¶ms, &mut actual)1125.unwrap();1126log::trace!("received results {actual:?}");1127assert_eq!(actual, results);1128}11291130Ok(())1131}11321133/// Instantiates a wasm module and runs its exports with dummy values, all in1134/// an async fashion.1135///1136/// Attempts to stress yields in host functions to ensure that exiting and1137/// resuming a wasm function call works.1138pub fn call_async(wasm: &[u8], config: &generators::Config, mut poll_amts: &[u32]) {1139let mut store = config.to_store();1140let module = match compile_module(store.engine(), wasm, KnownValid::Yes, config) {1141Some(module) => module,1142None => return,1143};11441145// Configure a helper thread to periodically increment the epoch to1146// forcibly enable yields-via-epochs if epochs are in use. Note that this1147// is required because the wasm isn't otherwise guaranteed to necessarily1148// call any imports which will also increment the epoch.1149let mut helper_thread = HelperThread::default();1150if let generators::AsyncConfig::YieldWithEpochs { dur, .. } = &config.wasmtime.async_config {1151let engine = store.engine().clone();1152helper_thread.run_periodically(*dur, move || engine.increment_epoch());1153}11541155// Generate a `Linker` where all function imports are custom-built to yield1156// periodically and additionally increment the epoch.1157let mut imports = Vec::new();1158for import in module.imports() {1159let item = match import.ty() {1160ExternType::Func(ty) => {1161let poll_amt = take_poll_amt(&mut poll_amts);1162Func::new_async(&mut store, ty.clone(), move |caller, _, results| {1163let ty = ty.clone();1164Box::new(async move {1165caller.engine().increment_epoch();1166log::info!("yielding {poll_amt} times in import");1167YieldN(poll_amt).await;1168for (ret_ty, result) in ty.results().zip(results) {1169*result = ret_ty.default_value().unwrap();1170}1171Ok(())1172})1173})1174.into()1175}1176other_ty => match other_ty.default_value(&mut store) {1177Ok(item) => item,1178Err(e) => {1179log::warn!("couldn't create import for {import:?}: {e:?}");1180return;1181}1182},1183};1184imports.push(item);1185}11861187// Run the instantiation process, asynchronously, and if everything1188// succeeds then pull out the instance.1189// log::info!("starting instantiation");1190let instance = run(Timeout {1191future: Instance::new_async(&mut store, &module, &imports),1192polls: take_poll_amt(&mut poll_amts),1193end: Instant::now() + Duration::from_millis(2_000),1194});1195let instance = match instance {1196Ok(instantiation_result) => match unwrap_instance(&store, instantiation_result) {1197Some(instance) => instance,1198None => {1199log::info!("instantiation hit a nominal error");1200return; // resource exhaustion or limits met1201}1202},1203Err(_) => {1204log::info!("instantiation failed to complete");1205return; // Timed out or ran out of polls1206}1207};12081209// Run each export of the instance in the same manner as instantiation1210// above. Dummy values are passed in for argument values here:1211//1212// TODO: this should probably be more clever about passing in arguments for1213// example they might be used as pointers or something and always using 01214// isn't too interesting.1215let funcs = instance1216.exports(&mut store)1217.filter_map(|e| {1218let name = e.name().to_string();1219let func = e.into_extern().into_func()?;1220Some((name, func))1221})1222.collect::<Vec<_>>();1223for (name, func) in funcs {1224let ty = func.ty(&store);1225let params = ty1226.params()1227.map(|ty| ty.default_value().unwrap())1228.collect::<Vec<_>>();1229let mut results = ty1230.results()1231.map(|ty| ty.default_value().unwrap())1232.collect::<Vec<_>>();12331234log::info!("invoking export {name:?}");1235let future = func.call_async(&mut store, ¶ms, &mut results);1236match run(Timeout {1237future,1238polls: take_poll_amt(&mut poll_amts),1239end: Instant::now() + Duration::from_millis(2_000),1240}) {1241// On success or too many polls, try the next export.1242Ok(_) | Err(Exhausted::Polls) => {}12431244// If time ran out then stop the current test case as we might have1245// already sucked up a lot of time for this fuzz test case so don't1246// keep it going.1247Err(Exhausted::Time) => return,1248}1249}12501251fn take_poll_amt(polls: &mut &[u32]) -> u32 {1252match polls.split_first() {1253Some((a, rest)) => {1254*polls = rest;1255*a1256}1257None => 0,1258}1259}12601261/// Helper future to yield N times before resolving.1262struct YieldN(u32);12631264impl Future for YieldN {1265type Output = ();12661267fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {1268if self.0 == 0 {1269Poll::Ready(())1270} else {1271self.0 -= 1;1272cx.waker().wake_by_ref();1273Poll::Pending1274}1275}1276}12771278/// Helper future for applying a timeout to `future` up to either when `end`1279/// is the current time or `polls` polls happen.1280///1281/// Note that this helps to time out infinite loops in wasm, for example.1282struct Timeout<F> {1283future: F,1284/// If the future isn't ready by this time then the `Timeout<F>` future1285/// will return `None`.1286end: Instant,1287/// If the future doesn't resolve itself in this many calls to `poll`1288/// then the `Timeout<F>` future will return `None`.1289polls: u32,1290}12911292enum Exhausted {1293Time,1294Polls,1295}12961297impl<F: Future> Future for Timeout<F> {1298type Output = Result<F::Output, Exhausted>;12991300fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {1301let (end, polls, future) = unsafe {1302let me = self.get_unchecked_mut();1303(me.end, &mut me.polls, Pin::new_unchecked(&mut me.future))1304};1305match future.poll(cx) {1306Poll::Ready(val) => Poll::Ready(Ok(val)),1307Poll::Pending => {1308if Instant::now() >= end {1309log::warn!("future operation timed out");1310return Poll::Ready(Err(Exhausted::Time));1311}1312if *polls == 0 {1313log::warn!("future operation ran out of polls");1314return Poll::Ready(Err(Exhausted::Polls));1315}1316*polls -= 1;1317Poll::Pending1318}1319}1320}1321}13221323fn run<F: Future>(future: F) -> F::Output {1324let mut f = Box::pin(future);1325let mut cx = Context::from_waker(Waker::noop());1326loop {1327match f.as_mut().poll(&mut cx) {1328Poll::Ready(val) => break val,1329Poll::Pending => {}1330}1331}1332}1333}13341335#[cfg(test)]1336mod tests {1337use super::*;1338use arbitrary::Unstructured;1339use rand::prelude::*;1340use wasmparser::{Validator, WasmFeatures};13411342fn gen_until_pass<T: for<'a> Arbitrary<'a>>(1343mut f: impl FnMut(T, &mut Unstructured<'_>) -> Result<bool>,1344) -> bool {1345let mut rng = SmallRng::seed_from_u64(0);1346let mut buf = vec![0; 2048];1347let n = 3000;1348for _ in 0..n {1349rng.fill_bytes(&mut buf);1350let mut u = Unstructured::new(&buf);13511352if let Ok(config) = u.arbitrary() {1353if f(config, &mut u).unwrap() {1354return true;1355}1356}1357}1358false1359}13601361/// Runs `f` with random data until it returns `Ok(())` `iters` times.1362fn test_n_times<T: for<'a> Arbitrary<'a>>(1363iters: u32,1364mut f: impl FnMut(T, &mut Unstructured<'_>) -> arbitrary::Result<()>,1365) {1366let mut to_test = 0..iters;1367let ok = gen_until_pass(|a, b| {1368if f(a, b).is_ok() {1369Ok(to_test.next().is_none())1370} else {1371Ok(false)1372}1373});1374assert!(ok);1375}13761377// Test that the `table_ops` fuzzer eventually runs the gc function in the host.1378// We've historically had issues where this fuzzer accidentally wasn't fuzzing1379// anything for a long time so this is an attempt to prevent that from happening1380// again.1381#[test]1382fn table_ops_eventually_gcs() {1383// Skip if we're under emulation because some fuzz configurations will do1384// large address space reservations that QEMU doesn't handle well.1385if std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok() {1386return;1387}13881389let ok = gen_until_pass(|(config, test), _| {1390let result = table_ops(config, test)?;1391Ok(result > 0)1392});13931394if !ok {1395panic!("gc was never found");1396}1397}13981399#[test]1400fn module_generation_uses_expected_proposals() {1401// Proposals that Wasmtime supports. Eventually a module should be1402// generated that needs these proposals.1403let mut expected = WasmFeatures::MUTABLE_GLOBAL1404| WasmFeatures::FLOATS1405| WasmFeatures::SIGN_EXTENSION1406| WasmFeatures::SATURATING_FLOAT_TO_INT1407| WasmFeatures::MULTI_VALUE1408| WasmFeatures::BULK_MEMORY1409| WasmFeatures::REFERENCE_TYPES1410| WasmFeatures::SIMD1411| WasmFeatures::MULTI_MEMORY1412| WasmFeatures::RELAXED_SIMD1413| WasmFeatures::THREADS1414| WasmFeatures::TAIL_CALL1415| WasmFeatures::WIDE_ARITHMETIC1416| WasmFeatures::MEMORY641417| WasmFeatures::FUNCTION_REFERENCES1418| WasmFeatures::GC1419| WasmFeatures::GC_TYPES1420| WasmFeatures::CUSTOM_PAGE_SIZES1421| WasmFeatures::EXTENDED_CONST1422| WasmFeatures::EXCEPTIONS;14231424// All other features that wasmparser supports, which is presumably a1425// superset of the features that wasm-smith supports, are listed here as1426// unexpected. This means, for example, that if wasm-smith updates to1427// include a new proposal by default that wasmtime implements then it1428// will be required to be listed above.1429let unexpected = WasmFeatures::all() ^ expected;14301431let ok = gen_until_pass(|config: generators::Config, u| {1432let wasm = config.generate(u, None)?.to_bytes();14331434// Double-check the module is valid1435Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?;14361437// If any of the unexpected features are removed then this module1438// should always be valid, otherwise something went wrong.1439for feature in unexpected.iter() {1440let ok =1441Validator::new_with_features(WasmFeatures::all() ^ feature).validate_all(&wasm);1442if ok.is_err() {1443anyhow::bail!("generated a module with {feature:?} but that wasn't expected");1444}1445}14461447// If any of `expected` is removed and the module fails to validate,1448// then that means the module requires that feature. Remove that1449// from the set of features we're then expecting.1450for feature in expected.iter() {1451let ok =1452Validator::new_with_features(WasmFeatures::all() ^ feature).validate_all(&wasm);1453if ok.is_err() {1454expected ^= feature;1455}1456}14571458Ok(expected.is_empty())1459});14601461if !ok {1462panic!("never generated wasm module using {expected:?}");1463}1464}14651466#[test]1467fn wast_smoke_test() {1468test_n_times(50, |(), u| super::wast_test(u));1469}1470}147114721473