diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs index e91d3f3f97..d92f10ad33 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -36,13 +36,156 @@ impl OptLevel { } } +/// Configuration for `wasmtime::PoolingAllocationStrategy`. +#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)] +pub enum PoolingAllocationStrategy { + /// Use next available instance slot. + NextAvailable, + /// Use random instance slot. + Random, + /// Use an affinity-based strategy. + ReuseAffinity, +} + +impl PoolingAllocationStrategy { + fn to_wasmtime(&self) -> wasmtime::PoolingAllocationStrategy { + match self { + PoolingAllocationStrategy::NextAvailable => { + wasmtime::PoolingAllocationStrategy::NextAvailable + } + PoolingAllocationStrategy::Random => wasmtime::PoolingAllocationStrategy::Random, + PoolingAllocationStrategy::ReuseAffinity => { + wasmtime::PoolingAllocationStrategy::ReuseAffinity + } + } + } +} + +/// Configuration for `wasmtime::ModuleLimits`. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct ModuleLimits { + imported_functions: u32, + imported_tables: u32, + imported_memories: u32, + imported_globals: u32, + types: u32, + functions: u32, + tables: u32, + memories: u32, + /// The maximum number of globals that can be defined in a module. + pub globals: u32, + table_elements: u32, + memory_pages: u64, +} + +impl ModuleLimits { + fn to_wasmtime(&self) -> wasmtime::ModuleLimits { + wasmtime::ModuleLimits { + imported_functions: self.imported_functions, + imported_tables: self.imported_tables, + imported_memories: self.imported_memories, + imported_globals: self.imported_globals, + types: self.types, + functions: self.functions, + tables: self.tables, + memories: self.memories, + globals: self.globals, + table_elements: self.table_elements, + memory_pages: self.memory_pages, + } + } +} + +impl<'a> Arbitrary<'a> for ModuleLimits { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + const MAX_IMPORTS: u32 = 1000; + const MAX_TYPES: u32 = 1000; + const MAX_FUNCTIONS: u32 = 1000; + const MAX_TABLES: u32 = 10; + const MAX_MEMORIES: u32 = 10; + const MAX_GLOBALS: u32 = 1000; + const MAX_ELEMENTS: u32 = 1000; + const MAX_MEMORY_PAGES: u64 = 0x10000; + + Ok(Self { + imported_functions: u.int_in_range(0..=MAX_IMPORTS)?, + imported_tables: u.int_in_range(0..=MAX_IMPORTS)?, + imported_memories: u.int_in_range(0..=MAX_IMPORTS)?, + imported_globals: u.int_in_range(0..=MAX_IMPORTS)?, + types: u.int_in_range(0..=MAX_TYPES)?, + functions: u.int_in_range(0..=MAX_FUNCTIONS)?, + tables: u.int_in_range(0..=MAX_TABLES)?, + memories: u.int_in_range(0..=MAX_MEMORIES)?, + globals: u.int_in_range(0..=MAX_GLOBALS)?, + table_elements: u.int_in_range(0..=MAX_ELEMENTS)?, + memory_pages: u.int_in_range(0..=MAX_MEMORY_PAGES)?, + }) + } +} + +/// Configuration for `wasmtime::PoolingAllocationStrategy`. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct InstanceLimits { + /// The maximum number of instances that can be instantiated in the pool at a time. + pub count: u32, +} + +impl InstanceLimits { + fn to_wasmtime(&self) -> wasmtime::InstanceLimits { + wasmtime::InstanceLimits { count: self.count } + } +} + +impl<'a> Arbitrary<'a> for InstanceLimits { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + const MAX_COUNT: u32 = 100; + + Ok(Self { + count: u.int_in_range(1..=MAX_COUNT)?, + }) + } +} + +/// Configuration for `wasmtime::InstanceAllocationStrategy`. +#[derive(Arbitrary, Clone, Debug, Eq, PartialEq, Hash)] +pub enum InstanceAllocationStrategy { + /// Use the on-demand instance allocation strategy. + OnDemand, + /// Use the pooling instance allocation strategy. + Pooling { + /// The pooling strategy to use. + strategy: PoolingAllocationStrategy, + /// The module limits. + module_limits: ModuleLimits, + /// The instance limits. + instance_limits: InstanceLimits, + }, +} + +impl InstanceAllocationStrategy { + fn to_wasmtime(&self) -> wasmtime::InstanceAllocationStrategy { + match self { + InstanceAllocationStrategy::OnDemand => wasmtime::InstanceAllocationStrategy::OnDemand, + InstanceAllocationStrategy::Pooling { + strategy, + module_limits, + instance_limits, + } => wasmtime::InstanceAllocationStrategy::Pooling { + strategy: strategy.to_wasmtime(), + module_limits: module_limits.to_wasmtime(), + instance_limits: instance_limits.to_wasmtime(), + }, + } + } +} + /// Configuration for `wasmtime::Config` and generated modules for a session of /// fuzzing. /// /// This configuration guides what modules are generated, how wasmtime /// configuration is generated, and is typically itself generated through a call /// to `Arbitrary` which allows for a form of "swarm testing". -#[derive(Arbitrary, Debug)] +#[derive(Debug)] pub struct Config { /// Configuration related to the `wasmtime::Config`. pub wasmtime: WasmtimeConfig, @@ -50,6 +193,49 @@ pub struct Config { pub module_config: ModuleConfig, } +impl<'a> Arbitrary<'a> for Config { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let mut config = Self { + wasmtime: u.arbitrary()?, + module_config: u.arbitrary()?, + }; + + // If using the pooling allocator, constrain the memory and module configurations + // to the module limits. + if let InstanceAllocationStrategy::Pooling { + module_limits: limits, + .. + } = &config.wasmtime.strategy + { + // Force the use of a normal memory config when using the pooling allocator and + // limit the static memory maximum to be the same as the pooling allocator's memory + // page limit. + let mut memory_config: NormalMemoryConfig = u.arbitrary()?; + memory_config.static_memory_maximum_size = Some(limits.memory_pages * 0x10000); + config.wasmtime.memory_config = MemoryConfig::Normal(memory_config); + + let cfg = &mut config.module_config.config; + cfg.max_imports = limits.imported_functions.min( + limits + .imported_globals + .min(limits.imported_memories.min(limits.imported_tables)), + ) as usize; + cfg.max_types = limits.types as usize; + cfg.max_funcs = limits.functions as usize; + cfg.max_globals = limits.globals as usize; + cfg.max_memories = limits.memories as usize; + cfg.max_tables = limits.tables as usize; + cfg.max_memory_pages = limits.memory_pages; + + // Force no aliases in any generated modules as they might count against the + // import limits above. + cfg.max_aliases = 0; + } + + Ok(config) + } +} + /// Configuration related to `wasmtime::Config` and the various settings which /// can be tweaked from within. #[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)] @@ -63,6 +249,8 @@ pub struct WasmtimeConfig { force_jump_veneers: bool, memfd: bool, use_precompiled_cwasm: bool, + /// Configuration for the instance allocation strategy to use. + pub strategy: InstanceAllocationStrategy, } #[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)] @@ -70,15 +258,7 @@ enum MemoryConfig { /// Configuration for linear memories which correspond to normal /// configuration settings in `wasmtime` itself. This will tweak various /// parameters about static/dynamic memories. - /// - /// Note that we use 32-bit values here to avoid blowing the 64-bit address - /// space by requesting ungodly-large sizes/guards. - Normal { - static_memory_maximum_size: Option, - static_memory_guard_size: Option, - dynamic_memory_guard_size: Option, - guard_before_linear_memory: bool, - }, + Normal(NormalMemoryConfig), /// Configuration to force use of a linear memory that's unaligned at its /// base address to force all wasm addresses to be unaligned at the hardware @@ -86,6 +266,27 @@ enum MemoryConfig { CustomUnaligned, } +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct NormalMemoryConfig { + static_memory_maximum_size: Option, + static_memory_guard_size: Option, + dynamic_memory_guard_size: Option, + guard_before_linear_memory: bool, +} + +impl<'a> Arbitrary<'a> for NormalMemoryConfig { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + // This attempts to limit memory and guard sizes to 32-bit ranges so + // we don't exhaust a 64-bit address space easily. + Ok(Self { + static_memory_maximum_size: as Arbitrary>::arbitrary(u)?.map(Into::into), + static_memory_guard_size: as Arbitrary>::arbitrary(u)?.map(Into::into), + dynamic_memory_guard_size: as Arbitrary>::arbitrary(u)?.map(Into::into), + guard_before_linear_memory: u.arbitrary()?, + }) + } +} + impl Config { /// Converts this to a `wasmtime::Config` object pub fn to_wasmtime(&self) -> wasmtime::Config { @@ -102,7 +303,8 @@ impl Config { .cranelift_opt_level(self.wasmtime.opt_level.to_wasmtime()) .interruptable(self.wasmtime.interruptable) .consume_fuel(self.wasmtime.consume_fuel) - .memfd(self.wasmtime.memfd); + .memfd(self.wasmtime.memfd) + .allocation_strategy(self.wasmtime.strategy.to_wasmtime()); // If the wasm-smith-generated module use nan canonicalization then we // don't need to enable it, but if it doesn't enable it already then we @@ -126,16 +328,13 @@ impl Config { } match &self.wasmtime.memory_config { - MemoryConfig::Normal { - static_memory_maximum_size, - static_memory_guard_size, - dynamic_memory_guard_size, - guard_before_linear_memory, - } => { - cfg.static_memory_maximum_size(static_memory_maximum_size.unwrap_or(0).into()) - .static_memory_guard_size(static_memory_guard_size.unwrap_or(0).into()) - .dynamic_memory_guard_size(dynamic_memory_guard_size.unwrap_or(0).into()) - .guard_before_linear_memory(*guard_before_linear_memory); + MemoryConfig::Normal(memory_config) => { + cfg.static_memory_maximum_size( + memory_config.static_memory_maximum_size.unwrap_or(0), + ) + .static_memory_guard_size(memory_config.static_memory_guard_size.unwrap_or(0)) + .dynamic_memory_guard_size(memory_config.dynamic_memory_guard_size.unwrap_or(0)) + .guard_before_linear_memory(memory_config.guard_before_linear_memory); } MemoryConfig::CustomUnaligned => { cfg.with_host_memory(Arc::new(UnalignedMemoryCreator)) @@ -145,6 +344,7 @@ impl Config { .guard_before_linear_memory(false); } } + return cfg; } @@ -153,11 +353,16 @@ impl Config { pub fn to_store(&self) -> Store { let engine = Engine::new(&self.to_wasmtime()).unwrap(); let mut store = Store::new(&engine, StoreLimits::new()); + self.configure_store(&mut store); + store + } + + /// Configures a store based on this configuration. + pub fn configure_store(&self, store: &mut Store) { store.limiter(|s| s as &mut dyn wasmtime::ResourceLimiter); if self.wasmtime.consume_fuel { store.add_fuel(u64::max_value()).unwrap(); } - return store; } /// Generates an arbitrary method of timing out an instance, ensuring that diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index 9d5a647d9f..dd8ef9a036 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -14,6 +14,7 @@ pub mod dummy; use crate::generators; use anyhow::Context; +use arbitrary::Arbitrary; use log::{debug, warn}; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; use std::sync::{Arc, Condvar, Mutex}; @@ -142,14 +143,107 @@ pub fn instantiate(wasm: &[u8], known_valid: bool, config: &generators::Config, Timeout::None => {} } - log_wasm(wasm); - let module = match config.compile(store.engine(), wasm) { - Ok(module) => module, - Err(_) if !known_valid => return, - Err(e) => panic!("failed to compile module: {:?}", e), - }; + if let Some(module) = compile_module(store.engine(), wasm, known_valid, config) { + instantiate_with_dummy(&mut store, &module); + } +} - 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: bool, + config: &generators::Config, + commands: &[Command], +) { + assert!(!config.module_config.config.allow_start_export); + + let engine = Engine::new(&config.to_wasmtime()).unwrap(); + + let modules = modules + .iter() + .filter_map(|bytes| compile_module(&engine, bytes, known_valid, config)) + .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(); + + for command in commands { + match command { + Command::Instantiate(index) => { + let module = &modules[*index % modules.len()]; + let mut store = Store::new(&engine, StoreLimits::new()); + config.configure_store(&mut store); + + if instantiate_with_dummy(&mut store, module).is_some() { + stores.push(Some(store)); + } + } + Command::Terminate(index) => { + if stores.is_empty() { + continue; + } + + stores.swap_remove(*index % stores.len()); + } + } + } +} + +fn compile_module( + engine: &Engine, + bytes: &[u8], + known_valid: bool, + config: &generators::Config, +) -> Option { + log_wasm(bytes); + match config.compile(engine, bytes) { + Ok(module) => Some(module), + Err(_) if !known_valid => None, + Err(e) => { + if let generators::InstanceAllocationStrategy::Pooling { .. } = + &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 = e.to_string(); + if string.contains("minimum element size") { + return None; + } + } + + panic!("failed to compile module: {:?}", e); + } + } } fn instantiate_with_dummy(store: &mut Store, module: &Module) -> Option { @@ -188,8 +282,13 @@ fn instantiate_with_dummy(store: &mut Store, module: &Module) -> Op return None; } + // Also allow failures to instantiate as a result of hitting instance limits + if string.contains("concurrent instances has been reached") { + return None; + } + // Everything else should be a bug in the fuzzer or a bug in wasmtime - panic!("failed to instantiate {:?}", e); + panic!("failed to instantiate: {:?}", e); } /// Instantiate the given Wasm module with each `Config` and call all of its diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 39f2adf11e..94947abba8 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -91,3 +91,9 @@ name = "cranelift-fuzzgen-verify" path = "fuzz_targets/cranelift-fuzzgen-verify.rs" test = false doc = false + +[[bin]] +name = "instantiate-many" +path = "fuzz_targets/instantiate-many.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/instantiate-many.rs b/fuzz/fuzz_targets/instantiate-many.rs new file mode 100644 index 0000000000..8845413e5d --- /dev/null +++ b/fuzz/fuzz_targets/instantiate-many.rs @@ -0,0 +1,54 @@ +//! This fuzz target is used to test multiple concurrent instantiations from +//! multiple modules. + +#![no_main] + +use libfuzzer_sys::arbitrary::{Result, Unstructured}; +use libfuzzer_sys::fuzz_target; +use wasmtime_fuzzing::{generators, oracles}; + +const MAX_MODULES: usize = 5; + +fuzz_target!(|data: &[u8]| { + // errors in `run` have to do with not enough input in `data`, which we + // ignore here since it doesn't affect how we'd like to fuzz. + drop(run(data)); +}); + +fn run(data: &[u8]) -> Result<()> { + let mut u = Unstructured::new(data); + let mut config: generators::Config = u.arbitrary()?; + + // Don't generate start functions + // No wasm code execution is necessary for this fuzz target and thus we don't + // use timeouts or ensure that the generated wasm code will terminate. + config.module_config.config.allow_start_export = false; + + // Create the modules to instantiate + let modules = (0..u.int_in_range(1..=MAX_MODULES)?) + .map(|_| Ok(config.module_config.generate(&mut u)?.to_bytes())) + .collect::>>()?; + + let max_instances = match &config.wasmtime.strategy { + generators::InstanceAllocationStrategy::OnDemand => u.int_in_range(1..=100)?, + generators::InstanceAllocationStrategy::Pooling { + instance_limits, .. + } => instance_limits.count, + }; + + // Front-load with instantiation commands + let mut commands: Vec = (0..u.int_in_range(1..=max_instances)?) + .map(|_| Ok(oracles::Command::Instantiate(u.arbitrary()?))) + .collect::>()?; + + // Then add some more arbitrary commands + commands.extend( + (0..u.int_in_range(0..=2 * max_instances)?) + .map(|_| u.arbitrary()) + .collect::>>()?, + ); + + oracles::instantiate_many(&modules, true, &config, &commands); + + Ok(()) +} diff --git a/fuzz/fuzz_targets/instantiate.rs b/fuzz/fuzz_targets/instantiate.rs index 0247d9f21d..11c5f79a25 100644 --- a/fuzz/fuzz_targets/instantiate.rs +++ b/fuzz/fuzz_targets/instantiate.rs @@ -2,6 +2,7 @@ use libfuzzer_sys::arbitrary::{Result, Unstructured}; use libfuzzer_sys::fuzz_target; +use wasmtime_fuzzing::generators::InstanceAllocationStrategy; use wasmtime_fuzzing::oracles::Timeout; use wasmtime_fuzzing::{generators, oracles}; @@ -27,6 +28,17 @@ fn run(data: &[u8]) -> Result<()> { // Enable module linking for this fuzz target specifically config.module_config.config.module_linking_enabled = u.arbitrary()?; + // When using the pooling allocator without a timeout, we must + // allow at least 1 more global because the `ensure_termination` call below + // will define one. + if let Timeout::None = timeout { + if let InstanceAllocationStrategy::Pooling { module_limits, .. } = + &mut config.wasmtime.strategy + { + module_limits.globals += 1; + } + } + let mut module = config.module_config.generate(&mut u)?; if let Timeout::None = timeout { module.ensure_termination(1000);