diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs index bf8b647e6e..e4d110f50c 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -185,7 +185,7 @@ impl InstanceAllocationStrategy { /// 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(Debug)] +#[derive(Debug, Clone)] pub struct Config { /// Configuration related to the `wasmtime::Config`. pub wasmtime: WasmtimeConfig, @@ -210,9 +210,17 @@ impl<'a> Arbitrary<'a> for Config { // 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); + config.wasmtime.memory_config = match config.wasmtime.memory_config { + MemoryConfig::Normal(mut config) => { + config.static_memory_maximum_size = Some(limits.memory_pages * 0x10000); + MemoryConfig::Normal(config) + } + MemoryConfig::CustomUnaligned => { + let mut config: NormalMemoryConfig = u.arbitrary()?; + config.static_memory_maximum_size = Some(limits.memory_pages * 0x10000); + MemoryConfig::Normal(config) + } + }; let cfg = &mut config.module_config.config; cfg.max_imports = limits.imported_functions.min( @@ -245,7 +253,8 @@ pub struct WasmtimeConfig { canonicalize_nans: bool, interruptable: bool, pub(crate) consume_fuel: bool, - memory_config: MemoryConfig, + /// The Wasmtime memory configuration to use. + pub memory_config: MemoryConfig, force_jump_veneers: bool, memfd: bool, use_precompiled_cwasm: bool, @@ -254,8 +263,9 @@ pub struct WasmtimeConfig { codegen: CodegenSettings, } +/// Configuration for linear memories in Wasmtime. #[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)] -enum MemoryConfig { +pub enum MemoryConfig { /// Configuration for linear memories which correspond to normal /// configuration settings in `wasmtime` itself. This will tweak various /// parameters about static/dynamic memories. @@ -267,8 +277,10 @@ enum MemoryConfig { CustomUnaligned, } +/// Represents a normal memory configuration for Wasmtime with the given +/// static and dynamic memory sizes. #[derive(Clone, Debug, Eq, Hash, PartialEq)] -struct NormalMemoryConfig { +pub struct NormalMemoryConfig { static_memory_maximum_size: Option, static_memory_guard_size: Option, dynamic_memory_guard_size: Option, @@ -289,6 +301,117 @@ impl<'a> Arbitrary<'a> for NormalMemoryConfig { } impl Config { + /// Indicates that this configuration is being used for differential + /// execution so only a single function should be generated since that's all + /// that's going to be exercised. + pub fn set_differential_config(&mut self) { + let config = &mut self.module_config.config; + + config.allow_start_export = false; + // Make sure there's a type available for the function. + config.min_types = 1; + config.max_types = 1; + + // Generate one and only one function + config.min_funcs = 1; + config.max_funcs = 1; + + // Give the function a memory, but keep it small + config.min_memories = 1; + config.max_memories = 1; + config.max_memory_pages = 1; + config.memory_max_size_required = true; + + // Don't allow any imports + config.max_imports = 0; + + // Try to get the function and the memory exported + config.min_exports = 2; + config.max_exports = 4; + + // NaN is canonicalized at the wasm level for differential fuzzing so we + // can paper over NaN differences between engines. + config.canonicalize_nans = true; + + // When diffing against a non-wasmtime engine then disable wasm + // features to get selectively re-enabled against each differential + // engine. + config.bulk_memory_enabled = false; + config.reference_types_enabled = false; + config.simd_enabled = false; + config.memory64_enabled = false; + + // If using the pooling allocator, update the module limits too + if let InstanceAllocationStrategy::Pooling { + module_limits: limits, + .. + } = &mut self.wasmtime.strategy + { + // No imports + limits.imported_functions = 0; + limits.imported_tables = 0; + limits.imported_memories = 0; + limits.imported_globals = 0; + + // One type, one function, and one single-page memory + limits.types = 1; + limits.functions = 1; + limits.memories = 1; + limits.memory_pages = 1; + + match &mut self.wasmtime.memory_config { + MemoryConfig::Normal(config) => { + config.static_memory_maximum_size = Some(limits.memory_pages * 0x10000); + } + MemoryConfig::CustomUnaligned => unreachable!(), // Arbitrary impl for `Config` should have prevented this + } + } + } + + /// Uses this configuration and the supplied source of data to generate + /// a wasm module. + /// + /// If a `default_fuel` is provided, the resulting module will be configured + /// to ensure termination; as doing so will add an additional global to the module, + /// the pooling allocator, if configured, will also have its globals limit updated. + pub fn generate( + &mut self, + input: &mut Unstructured<'_>, + default_fuel: Option, + ) -> arbitrary::Result { + let mut module = wasm_smith::Module::new(self.module_config.config.clone(), input)?; + + if let Some(default_fuel) = default_fuel { + module.ensure_termination(default_fuel); + + // Bump the allowed global count by 1 + if let InstanceAllocationStrategy::Pooling { module_limits, .. } = + &mut self.wasmtime.strategy + { + module_limits.globals += 1; + } + } + + Ok(module) + } + + /// Indicates that this configuration should be spec-test-compliant, + /// disabling various features the spec tests assert are disabled. + pub fn set_spectest_compliant(&mut self) { + let config = &mut self.module_config.config; + config.memory64_enabled = false; + config.simd_enabled = false; + config.bulk_memory_enabled = true; + config.reference_types_enabled = true; + config.max_memories = 1; + + if let InstanceAllocationStrategy::Pooling { module_limits, .. } = + &mut self.wasmtime.strategy + { + module_limits.memories = 1; + } + } + /// Converts this to a `wasmtime::Config` object pub fn to_wasmtime(&self) -> wasmtime::Config { crate::init_fuzzing(); @@ -496,63 +619,6 @@ pub struct ModuleConfig { pub config: SwarmConfig, } -impl ModuleConfig { - /// Uses this configuration and the supplied source of data to generate - /// a wasm module. - pub fn generate(&self, input: &mut Unstructured<'_>) -> arbitrary::Result { - wasm_smith::Module::new(self.config.clone(), input) - } - - /// Indicates that this configuration should be spec-test-compliant, - /// disabling various features the spec tests assert are disabled. - pub fn set_spectest_compliant(&mut self) { - self.config.memory64_enabled = false; - self.config.simd_enabled = false; - self.config.bulk_memory_enabled = true; - self.config.reference_types_enabled = true; - self.config.max_memories = 1; - } - - /// Indicates that this configuration is being used for differential - /// execution so only a single function should be generated since that's all - /// that's going to be exercised. - pub fn set_differential_config(&mut self) { - self.config.allow_start_export = false; - // Make sure there's a type available for the function. - self.config.min_types = 1; - self.config.max_types = 1; - - // Generate one and only one function - self.config.min_funcs = 1; - self.config.max_funcs = 1; - - // Give the function a memory, but keep it small - self.config.min_memories = 1; - self.config.max_memories = 1; - self.config.max_memory_pages = 1; - self.config.memory_max_size_required = true; - - // Don't allow any imports - self.config.max_imports = 0; - - // Try to get the function and the memory exported - self.config.min_exports = 2; - self.config.max_exports = 4; - - // NaN is canonicalized at the wasm level for differential fuzzing so we - // can paper over NaN differences between engines. - self.config.canonicalize_nans = true; - - // When diffing against a non-wasmtime engine then disable wasm - // features to get selectively re-enabled against each differential - // engine. - self.config.bulk_memory_enabled = false; - self.config.reference_types_enabled = false; - self.config.simd_enabled = false; - self.config.memory64_enabled = false; - } -} - impl<'a> Arbitrary<'a> for ModuleConfig { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { let mut config = SwarmConfig::arbitrary(u)?; diff --git a/crates/fuzzing/src/generators/api.rs b/crates/fuzzing/src/generators/api.rs index b27313b459..6707b23fec 100644 --- a/crates/fuzzing/src/generators/api.rs +++ b/crates/fuzzing/src/generators/api.rs @@ -14,7 +14,7 @@ //! //! [swarm testing]: https://www.cs.utah.edu/~regehr/papers/swarm12.pdf -use crate::generators::{Config, ModuleConfig}; +use crate::generators::Config; use arbitrary::{Arbitrary, Unstructured}; use std::collections::BTreeSet; @@ -44,7 +44,7 @@ struct Scope { id_counter: usize, modules: BTreeSet, instances: BTreeSet, - module_config: ModuleConfig, + config: Config, } impl Scope { @@ -70,14 +70,13 @@ impl<'a> Arbitrary<'a> for ApiCalls { let mut calls = vec![]; let config = Config::arbitrary(input)?; - let module_config = config.module_config.clone(); - calls.push(StoreNew(config)); + calls.push(StoreNew(config.clone())); let mut scope = Scope { id_counter: 0, modules: BTreeSet::default(), instances: BTreeSet::default(), - module_config, + config, }; // Total limit on number of API calls we'll generate. This exists to @@ -94,8 +93,7 @@ impl<'a> Arbitrary<'a> for ApiCalls { if swarm.module_new { choices.push(|input, scope| { let id = scope.next_id(); - let mut wasm = scope.module_config.generate(input)?; - wasm.ensure_termination(1000); + let wasm = scope.config.generate(input, Some(1000))?; scope.modules.insert(id); Ok(ModuleNew { id, diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index dd8ef9a036..55a18ad690 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -295,11 +295,13 @@ fn instantiate_with_dummy(store: &mut Store, module: &Module) -> Op /// exports. Modulo OOM, non-canonical NaNs, and usage of Wasm features that are /// or aren't enabled for different configs, we should get the same results when /// we call the exported functions for all of our different configs. +/// +/// Returns `None` if a fuzz configuration was rejected (should happen rarely). pub fn differential_execution( wasm: &[u8], module_config: &generators::ModuleConfig, configs: &[generators::WasmtimeConfig], -) { +) -> Option<()> { use std::collections::{HashMap, HashSet}; // We need at least two configs. @@ -307,7 +309,7 @@ pub fn differential_execution( // And all the configs should be unique. || configs.iter().collect::>().len() != configs.len() { - return; + return None; } let mut export_func_results: HashMap, Trap>> = Default::default(); @@ -321,7 +323,7 @@ pub fn differential_execution( log::debug!("fuzz config: {:?}", fuzz_config); let mut store = fuzz_config.to_store(); - let module = fuzz_config.compile(store.engine(), &wasm).unwrap(); + let module = compile_module(store.engine(), &wasm, true, &fuzz_config)?; // TODO: we should implement tracing versions of these dummy imports // that record a trace of the order that imported functions were called @@ -357,6 +359,8 @@ pub fn differential_execution( } } + return Some(()); + fn assert_same_export_func_result( lhs: &Result, Trap>, rhs: &Result, Trap>, @@ -505,7 +509,7 @@ pub fn make_api_calls(api: generators::api::ApiCalls) { /// Ensures that spec tests pass regardless of the `Config`. pub fn spectest(mut fuzz_config: generators::Config, test: generators::SpecTest) { crate::init_fuzzing(); - fuzz_config.module_config.set_spectest_compliant(); + fuzz_config.set_spectest_compliant(); log::debug!("running {:?} with {:?}", test.file, fuzz_config); let mut wast_context = WastContext::new(fuzz_config.to_store()); wast_context.register_spectest().unwrap(); @@ -531,9 +535,9 @@ pub fn table_ops(mut fuzz_config: generators::Config, ops: generators::table_ops let wasm = ops.to_wasm_binary(); log_wasm(&wasm); - let module = match fuzz_config.compile(store.engine(), &wasm) { - Ok(m) => m, - Err(_) => return, + let module = match compile_module(store.engine(), &wasm, false, &fuzz_config) { + Some(m) => m, + None => return, }; let mut linker = Linker::new(store.engine()); @@ -677,6 +681,7 @@ pub fn differential_wasmi_execution(wasm: &[u8], config: &generators::Config) -> // If wasmi succeeded then we assert that wasmtime will also succeed. let (wasmtime_module, mut wasmtime_store) = differential_store(wasm, config); + let wasmtime_module = wasmtime_module?; let wasmtime_instance = Instance::new(&mut wasmtime_store, &wasmtime_module, &[]) .expect("Wasmtime can instantiate module"); @@ -790,7 +795,7 @@ pub fn differential_spec_execution(wasm: &[u8], config: &generators::Config) -> match (&spec_vals, &wasmtime_vals) { // Compare the returned values, failing if they do not match. - (Ok(spec_vals), Ok(wasmtime_vals)) => { + (Ok(spec_vals), Ok(Some(wasmtime_vals))) => { let all_match = spec_vals .iter() .zip(wasmtime_vals) @@ -802,6 +807,10 @@ pub fn differential_spec_execution(wasm: &[u8], config: &generators::Config) -> ); } } + (_, Ok(None)) => { + // `run_in_wasmtime` rejected the config + return None; + } // If both sides fail, skip this fuzz execution. (Err(spec_error), Err(wasmtime_error)) => { // The `None` value returned here indicates that both sides @@ -833,11 +842,9 @@ pub fn differential_spec_execution(wasm: &[u8], config: &generators::Config) -> fn differential_store( wasm: &[u8], fuzz_config: &generators::Config, -) -> (Module, Store) { +) -> (Option, Store) { let store = fuzz_config.to_store(); - let module = fuzz_config - .compile(store.engine(), wasm) - .expect("Wasmtime can compile module"); + let module = compile_module(store.engine(), wasm, true, fuzz_config); (module, store) } @@ -847,9 +854,14 @@ fn run_in_wasmtime( wasm: &[u8], config: &generators::Config, params: &[Val], -) -> anyhow::Result> { +) -> anyhow::Result>> { // Instantiate wasmtime module and instance. let (wasmtime_module, mut wasmtime_store) = differential_store(wasm, config); + let wasmtime_module = match wasmtime_module { + Some(m) => m, + None => return Ok(None), + }; + let wasmtime_instance = Instance::new(&mut wasmtime_store, &wasmtime_module, &[]) .context("Wasmtime cannot instantiate module")?; @@ -864,7 +876,7 @@ fn run_in_wasmtime( let mut results = vec![Val::I32(0); ty.results().len()]; wasmtime_main .call(&mut wasmtime_store, params, &mut results) - .map(|()| results) + .map(|()| Some(results)) } // Introspect wasmtime module to find the name of the first exported function. diff --git a/crates/fuzzing/src/oracles/v8.rs b/crates/fuzzing/src/oracles/v8.rs index 5da046b051..bf8b683ab7 100644 --- a/crates/fuzzing/src/oracles/v8.rs +++ b/crates/fuzzing/src/oracles/v8.rs @@ -17,6 +17,7 @@ pub fn differential_v8_execution(wasm: &[u8], config: &crate::generators::Config // Wasmtime setup log_wasm(wasm); let (wasmtime_module, mut wasmtime_store) = super::differential_store(wasm, config); + let wasmtime_module = wasmtime_module?; log::trace!("compiled module with wasmtime"); // V8 setup diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs index cdd9835249..e3e868ea00 100644 --- a/fuzz/fuzz_targets/differential.rs +++ b/fuzz/fuzz_targets/differential.rs @@ -2,6 +2,7 @@ use libfuzzer_sys::arbitrary::{Result, Unstructured}; use libfuzzer_sys::fuzz_target; +use wasmtime_fuzzing::generators::InstanceAllocationStrategy; use wasmtime_fuzzing::{generators, oracles}; fuzz_target!(|data: &[u8]| { @@ -13,12 +14,28 @@ fuzz_target!(|data: &[u8]| { fn run(data: &[u8]) -> Result<()> { let mut u = Unstructured::new(data); - let lhs: generators::WasmtimeConfig = u.arbitrary()?; - let rhs: generators::WasmtimeConfig = u.arbitrary()?; - let config: generators::ModuleConfig = u.arbitrary()?; - let mut module = config.generate(&mut u)?; - module.ensure_termination(1000); + let mut config: generators::Config = u.arbitrary()?; + let module = config.generate(&mut u, Some(1000))?; - oracles::differential_execution(&module.to_bytes(), &config, &[lhs, rhs]); + let lhs = config.wasmtime; + let mut rhs: generators::WasmtimeConfig = u.arbitrary()?; + + // Use the same allocation strategy between the two configs. + // + // Ideally this wouldn't be necessary, but if the lhs is using ondemand + // and the rhs is using the pooling allocator (or vice versa), then + // the module may have been generated in such a way that is incompatible + // with the other allocation strategy. + // + // We can remove this in the future when it's possible to access the + // fields of `wasm_smith::Module` to constrain the pooling allocator + // based on what was actually generated. + rhs.strategy = lhs.strategy.clone(); + if let InstanceAllocationStrategy::Pooling { .. } = &rhs.strategy { + // Also use the same memory configuration when using the pooling allocator + rhs.memory_config = lhs.memory_config.clone(); + } + + oracles::differential_execution(&module.to_bytes(), &config.module_config, &[lhs, rhs]); Ok(()) } diff --git a/fuzz/fuzz_targets/differential_v8.rs b/fuzz/fuzz_targets/differential_v8.rs index 9304fdec5c..fa4c8b6df6 100644 --- a/fuzz/fuzz_targets/differential_v8.rs +++ b/fuzz/fuzz_targets/differential_v8.rs @@ -13,15 +13,14 @@ fuzz_target!(|data: &[u8]| { fn run(data: &[u8]) -> Result<()> { let mut u = Unstructured::new(data); let mut config: generators::Config = u.arbitrary()?; - config.module_config.set_differential_config(); + config.set_differential_config(); // Enable features that v8 has implemented config.module_config.config.simd_enabled = true; config.module_config.config.bulk_memory_enabled = true; config.module_config.config.reference_types_enabled = true; - let mut module = config.module_config.generate(&mut u)?; - module.ensure_termination(1000); + let module = config.generate(&mut u, Some(1000))?; oracles::differential_v8_execution(&module.to_bytes(), &config); Ok(()) } diff --git a/fuzz/fuzz_targets/differential_wasmi.rs b/fuzz/fuzz_targets/differential_wasmi.rs index fa6931b6fb..fe02ac2509 100644 --- a/fuzz/fuzz_targets/differential_wasmi.rs +++ b/fuzz/fuzz_targets/differential_wasmi.rs @@ -13,9 +13,8 @@ fuzz_target!(|data: &[u8]| { fn run(data: &[u8]) -> Result<()> { let mut u = Unstructured::new(data); let mut config: generators::Config = u.arbitrary()?; - config.module_config.set_differential_config(); - let mut module = config.module_config.generate(&mut u)?; - module.ensure_termination(1000); + config.set_differential_config(); + let module = config.generate(&mut u, Some(1000))?; oracles::differential_wasmi_execution(&module.to_bytes(), &config); Ok(()) } diff --git a/fuzz/fuzz_targets/instantiate-many.rs b/fuzz/fuzz_targets/instantiate-many.rs index 8845413e5d..351da977ed 100644 --- a/fuzz/fuzz_targets/instantiate-many.rs +++ b/fuzz/fuzz_targets/instantiate-many.rs @@ -26,7 +26,7 @@ fn run(data: &[u8]) -> Result<()> { // 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())) + .map(|_| Ok(config.generate(&mut u, None)?.to_bytes())) .collect::>>()?; let max_instances = match &config.wasmtime.strategy { diff --git a/fuzz/fuzz_targets/instantiate.rs b/fuzz/fuzz_targets/instantiate.rs index 11c5f79a25..ec8744b8fe 100644 --- a/fuzz/fuzz_targets/instantiate.rs +++ b/fuzz/fuzz_targets/instantiate.rs @@ -2,7 +2,6 @@ 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}; @@ -28,21 +27,15 @@ 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 module = config.generate( + &mut u, + if let Timeout::None = timeout { + Some(1000) + } else { + None + }, + )?; - let mut module = config.module_config.generate(&mut u)?; - if let Timeout::None = timeout { - module.ensure_termination(1000); - } oracles::instantiate(&module.to_bytes(), true, &config, timeout); Ok(()) }