diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs index 7242519087..9e945d5507 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -9,801 +9,19 @@ //! `Arbitrary` trait for the wrapped external tool. pub mod api; +mod codegen_settings; +mod config; +mod instance_allocation_strategy; +mod instance_limits; +mod memory; +mod module_config; +mod spec_test; pub mod table_ops; -use crate::oracles::{StoreLimits, Timeout}; -use anyhow::Result; -use arbitrary::{Arbitrary, Unstructured}; -use std::sync::Arc; -use std::time::Duration; -use wasm_smith::SwarmConfig; -use wasmtime::{Engine, LinearMemory, MemoryCreator, MemoryType, Module, Store}; - -#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)] -enum OptLevel { - None, - Speed, - SpeedAndSize, -} - -impl OptLevel { - fn to_wasmtime(&self) -> wasmtime::OptLevel { - match self { - OptLevel::None => wasmtime::OptLevel::None, - OptLevel::Speed => wasmtime::OptLevel::Speed, - OptLevel::SpeedAndSize => wasmtime::OptLevel::SpeedAndSize, - } - } -} - -/// 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::PoolingAllocationStrategy`. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -#[allow(missing_docs)] -pub struct InstanceLimits { - pub count: u32, - pub memories: u32, - pub tables: u32, - pub memory_pages: u64, - pub table_elements: u32, - pub size: usize, -} - -impl InstanceLimits { - fn to_wasmtime(&self) -> wasmtime::InstanceLimits { - wasmtime::InstanceLimits { - count: self.count, - memories: self.memories, - tables: self.tables, - memory_pages: self.memory_pages, - table_elements: self.table_elements, - size: self.size, - } - } -} - -impl<'a> Arbitrary<'a> for InstanceLimits { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - const MAX_COUNT: u32 = 100; - - const MAX_TABLES: u32 = 10; - const MAX_MEMORIES: u32 = 10; - const MAX_ELEMENTS: u32 = 1000; - const MAX_MEMORY_PAGES: u64 = 160; // 10 MiB - const MAX_SIZE: usize = 1 << 20; // 1 MiB - - Ok(Self { - tables: u.int_in_range(0..=MAX_TABLES)?, - memories: u.int_in_range(0..=MAX_MEMORIES)?, - table_elements: u.int_in_range(0..=MAX_ELEMENTS)?, - memory_pages: u.int_in_range(0..=MAX_MEMORY_PAGES)?, - count: u.int_in_range(1..=MAX_COUNT)?, - size: u.int_in_range(0..=MAX_SIZE)?, - }) - } -} - -/// 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 instance limits. - instance_limits: InstanceLimits, - }, -} - -impl InstanceAllocationStrategy { - fn to_wasmtime(&self) -> wasmtime::InstanceAllocationStrategy { - match self { - InstanceAllocationStrategy::OnDemand => wasmtime::InstanceAllocationStrategy::OnDemand, - InstanceAllocationStrategy::Pooling { - strategy, - instance_limits, - } => wasmtime::InstanceAllocationStrategy::Pooling { - strategy: strategy.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(Debug, Clone)] -pub struct Config { - /// Configuration related to the `wasmtime::Config`. - pub wasmtime: WasmtimeConfig, - /// Configuration related to generated modules. - 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 { - instance_limits: limits, - .. - } = &config.wasmtime.strategy - { - // If the pooling allocator is used, do not allow shared memory to - // be created. FIXME: see - // https://github.com/bytecodealliance/wasmtime/issues/4244. - config.module_config.config.threads_enabled = false; - - // 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. - 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_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)] -pub struct WasmtimeConfig { - opt_level: OptLevel, - debug_info: bool, - canonicalize_nans: bool, - interruptable: bool, - pub(crate) consume_fuel: bool, - epoch_interruption: bool, - /// The Wasmtime memory configuration to use. - pub memory_config: MemoryConfig, - force_jump_veneers: bool, - memory_init_cow: bool, - memory_guaranteed_dense_image_size: u64, - use_precompiled_cwasm: bool, - /// Configuration for the instance allocation strategy to use. - pub strategy: InstanceAllocationStrategy, - codegen: CodegenSettings, - padding_between_functions: Option, - generate_address_map: bool, - wasm_backtraces: bool, -} - -/// Configuration for linear memories in Wasmtime. -#[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)] -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. - 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 - /// level, even if the wasm itself correctly aligns everything internally. - CustomUnaligned, -} - -/// Represents a normal memory configuration for Wasmtime with the given -/// static and dynamic memory sizes. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub 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. - let mut ret = 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()?, - }; - - if let Some(dynamic) = ret.dynamic_memory_guard_size { - let statik = ret.static_memory_guard_size.unwrap_or(2 << 30); - ret.static_memory_guard_size = Some(statik.max(dynamic)); - } - Ok(ret) - } -} - -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 = config.max_types.max(1); - - // Generate at least one function - config.min_funcs = 1; - config.max_funcs = config.max_funcs.max(1); - - // Allow a memory to be generated, but don't let it get too large. - // Additionally require the maximum size to guarantee that the growth - // behavior is consistent across engines. - config.max_memories = 1; - config.max_memory_pages = 10; - config.memory_max_size_required = true; - - // If tables are generated make sure they don't get too large to avoid - // hitting any engine-specific limit. Additionally ensure that the - // maximum size is required to guarantee consistent growth across - // engines. - // - // Note that while reference types are disabled below, only allow one - // table. - config.max_tables = 1; - config.max_table_elements = 1_000; - config.table_max_size_required = true; - - // Don't allow any imports - config.max_imports = 0; - - // Try to get the function and the memory exported - config.export_everything = true; - - // 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; - config.threads_enabled = false; - - // If using the pooling allocator, update the instance limits too - if let InstanceAllocationStrategy::Pooling { - instance_limits: limits, - .. - } = &mut self.wasmtime.strategy - { - // One single-page memory - limits.memories = 1; - limits.memory_pages = 10; - - limits.tables = 1; - limits.table_elements = 1_000; - - 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); - } - - 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.bulk_memory_enabled = true; - config.reference_types_enabled = true; - config.multi_value_enabled = true; - config.simd_enabled = true; - config.threads_enabled = false; - config.max_memories = 1; - config.max_tables = 5; - - if let InstanceAllocationStrategy::Pooling { - instance_limits: limits, - .. - } = &mut self.wasmtime.strategy - { - // Configure the lower bound of a number of limits to what's - // required to actually run the spec tests. Fuzz-generated inputs - // may have limits less than these thresholds which would cause the - // spec tests to fail which isn't particularly interesting. - limits.memories = limits.memories.max(1); - limits.tables = limits.memories.max(5); - limits.table_elements = limits.memories.max(1_000); - limits.memory_pages = limits.memory_pages.max(900); - limits.count = limits.count.max(500); - limits.size = limits.size.max(64 * 1024); - - 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 - } - } - } - - /// Converts this to a `wasmtime::Config` object - pub fn to_wasmtime(&self) -> wasmtime::Config { - crate::init_fuzzing(); - log::debug!("creating wasmtime config with {:#?}", self.wasmtime); - - let mut cfg = wasmtime::Config::new(); - cfg.wasm_bulk_memory(true) - .wasm_reference_types(true) - .wasm_multi_value(self.module_config.config.multi_value_enabled) - .wasm_multi_memory(self.module_config.config.max_memories > 1) - .wasm_simd(self.module_config.config.simd_enabled) - .wasm_memory64(self.module_config.config.memory64_enabled) - .wasm_threads(self.module_config.config.threads_enabled) - .wasm_backtrace(self.wasmtime.wasm_backtraces) - .cranelift_nan_canonicalization(self.wasmtime.canonicalize_nans) - .cranelift_opt_level(self.wasmtime.opt_level.to_wasmtime()) - .consume_fuel(self.wasmtime.consume_fuel) - .epoch_interruption(self.wasmtime.epoch_interruption) - .memory_init_cow(self.wasmtime.memory_init_cow) - .memory_guaranteed_dense_image_size(std::cmp::min( - // Clamp this at 16MiB so we don't get huge in-memory - // images during fuzzing. - 16 << 20, - self.wasmtime.memory_guaranteed_dense_image_size, - )) - .allocation_strategy(self.wasmtime.strategy.to_wasmtime()) - .generate_address_map(self.wasmtime.generate_address_map); - - self.wasmtime.codegen.configure(&mut cfg); - - // 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 - // enable this codegen option. - cfg.cranelift_nan_canonicalization(!self.module_config.config.canonicalize_nans); - - // Enabling the verifier will at-least-double compilation time, which - // with a 20-30x slowdown in fuzzing can cause issues related to - // timeouts. If generated modules can have more than a small handful of - // functions then disable the verifier when fuzzing to try to lessen the - // impact of timeouts. - if self.module_config.config.max_funcs > 10 { - cfg.cranelift_debug_verifier(false); - } - - if self.wasmtime.force_jump_veneers { - unsafe { - cfg.cranelift_flag_set("wasmtime_linkopt_force_jump_veneer", "true"); - } - } - - if let Some(pad) = self.wasmtime.padding_between_functions { - unsafe { - cfg.cranelift_flag_set( - "wasmtime_linkopt_padding_between_functions", - &pad.to_string(), - ); - } - } - - // Vary the memory configuration, but only if threads are not enabled. - // When the threads proposal is enabled we might generate shared memory, - // which is less amenable to different memory configurations: - // - shared memories are required to be "static" so fuzzing the various - // memory configurations will mostly result in uninteresting errors. - // The interesting part about shared memories is the runtime so we - // don't fuzz non-default settings. - // - shared memories are required to be aligned which means that the - // `CustomUnaligned` variant isn't actually safe to use with a shared - // memory. - if !self.module_config.config.threads_enabled { - match &self.wasmtime.memory_config { - 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)) - .static_memory_maximum_size(0) - .dynamic_memory_guard_size(0) - .static_memory_guard_size(0) - .guard_before_linear_memory(false); - } - } - } - - return cfg; - } - - /// Convenience function for generating a `Store` using this - /// configuration. - 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(); - } - if self.wasmtime.epoch_interruption { - // Without fuzzing of async execution, we can't test the - // "update deadline and continue" behavior, but we can at - // least test the codegen paths and checks with the - // trapping behavior, which works synchronously too. We'll - // set the deadline one epoch tick in the future; then - // this works exactly like an interrupt flag. We expect no - // traps/interrupts unless we bump the epoch, which we do - // as one particular Timeout mode (`Timeout::Epoch`). - store.epoch_deadline_trap(); - store.set_epoch_deadline(1); - } - } - - /// Generates an arbitrary method of timing out an instance, ensuring that - /// this configuration supports the returned timeout. - pub fn generate_timeout(&mut self, u: &mut Unstructured<'_>) -> arbitrary::Result { - let time_duration = Duration::from_secs(20); - let timeout = u - .choose(&[Timeout::Fuel(100_000), Timeout::Epoch(time_duration)])? - .clone(); - match &timeout { - Timeout::Fuel(..) => { - self.wasmtime.consume_fuel = true; - } - Timeout::Epoch(..) => { - self.wasmtime.epoch_interruption = true; - } - Timeout::None => unreachable!("Not an option given to choose()"), - } - Ok(timeout) - } - - /// Compiles the `wasm` within the `engine` provided. - /// - /// This notably will use `Module::{serialize,deserialize_file}` to - /// round-trip if configured in the fuzzer. - pub fn compile(&self, engine: &Engine, wasm: &[u8]) -> Result { - // Propagate this error in case the caller wants to handle - // valid-vs-invalid wasm. - let module = Module::new(engine, wasm)?; - if !self.wasmtime.use_precompiled_cwasm { - return Ok(module); - } - - // Don't propagate these errors to prevent them from accidentally being - // interpreted as invalid wasm, these should never fail on a - // well-behaved host system. - let file = tempfile::NamedTempFile::new().unwrap(); - std::fs::write(file.path(), module.serialize().unwrap()).unwrap(); - unsafe { Ok(Module::deserialize_file(engine, file.path()).unwrap()) } - } -} - -struct UnalignedMemoryCreator; - -unsafe impl MemoryCreator for UnalignedMemoryCreator { - fn new_memory( - &self, - _ty: MemoryType, - minimum: usize, - maximum: Option, - reserved_size_in_bytes: Option, - guard_size_in_bytes: usize, - ) -> Result, String> { - assert_eq!(guard_size_in_bytes, 0); - assert!(reserved_size_in_bytes.is_none() || reserved_size_in_bytes == Some(0)); - Ok(Box::new(UnalignedMemory { - src: vec![0; minimum + 1], - maximum, - })) - } -} - -/// A custom "linear memory allocator" for wasm which only works with the -/// "dynamic" mode of configuration where wasm always does explicit bounds -/// checks. -/// -/// This memory attempts to always use unaligned host addresses for the base -/// address of linear memory with wasm. This means that all jit loads/stores -/// should be unaligned, which is a "big hammer way" of testing that all our JIT -/// code works with unaligned addresses since alignment is not required for -/// correctness in wasm itself. -struct UnalignedMemory { - /// This memory is always one byte larger than the actual size of linear - /// memory. - src: Vec, - maximum: Option, -} - -unsafe impl LinearMemory for UnalignedMemory { - fn byte_size(&self) -> usize { - // Chop off the extra byte reserved for the true byte size of this - // linear memory. - self.src.len() - 1 - } - - fn maximum_byte_size(&self) -> Option { - self.maximum - } - - fn grow_to(&mut self, new_size: usize) -> Result<()> { - // Make sure to allocate an extra byte for our "unalignment" - self.src.resize(new_size + 1, 0); - Ok(()) - } - - fn as_ptr(&self) -> *mut u8 { - // Return our allocated memory, offset by one, so that the base address - // of memory is always unaligned. - self.src[1..].as_ptr() as *mut _ - } -} - -include!(concat!(env!("OUT_DIR"), "/spectests.rs")); - -/// A spec test from the upstream wast testsuite, arbitrarily chosen from the -/// list of known spec tests. -#[derive(Debug)] -pub struct SpecTest { - /// The filename of the spec test - pub file: &'static str, - /// The `*.wast` contents of the spec test - pub contents: &'static str, -} - -impl<'a> Arbitrary<'a> for SpecTest { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - // NB: this does get a uniform value in the provided range. - let i = u.int_in_range(0..=FILES.len() - 1)?; - let (file, contents) = FILES[i]; - Ok(SpecTest { file, contents }) - } - - fn size_hint(_depth: usize) -> (usize, Option) { - (1, Some(std::mem::size_of::())) - } -} - -/// Default module-level configuration for fuzzing Wasmtime. -/// -/// Internally this uses `wasm-smith`'s own `SwarmConfig` but we further refine -/// the defaults here as well. -#[derive(Debug, Clone)] -pub struct ModuleConfig { - #[allow(missing_docs)] - pub config: SwarmConfig, -} - -impl<'a> Arbitrary<'a> for ModuleConfig { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - let mut config = SwarmConfig::arbitrary(u)?; - - // Allow multi-memory by default. - config.max_memories = config.max_memories.max(2); - - // Allow multi-table by default. - config.max_tables = config.max_tables.max(4); - - // Allow enabling some various wasm proposals by default. Note that - // these are all unconditionally turned off even with - // `SwarmConfig::arbitrary`. - config.memory64_enabled = u.arbitrary()?; - - // Allow the threads proposal if memory64 is not already enabled. FIXME: - // to allow threads and memory64 to coexist, see - // https://github.com/bytecodealliance/wasmtime/issues/4267. - config.threads_enabled = !config.memory64_enabled && u.arbitrary()?; - - Ok(ModuleConfig { config }) - } -} - -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -enum CodegenSettings { - Native, - #[allow(dead_code)] - Target { - target: String, - flags: Vec<(String, String)>, - }, -} - -impl CodegenSettings { - fn configure(&self, config: &mut wasmtime::Config) { - match self { - CodegenSettings::Native => {} - CodegenSettings::Target { target, flags } => { - config.target(target).unwrap(); - for (key, value) in flags { - unsafe { - config.cranelift_flag_set(key, value); - } - } - } - } - } -} - -impl<'a> Arbitrary<'a> for CodegenSettings { - #[allow(unused_macros, unused_variables)] - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - // Helper macro to enable clif features based on what the native host - // supports. If the input says to enable a feature and the host doesn't - // support it then that test case is rejected with a warning. - // - // Note that this specifically consumes bytes from the fuzz input for - // features for all targets, discarding anything which isn't applicable - // to the current target. The theory behind this is that most fuzz bugs - // won't be related to this feature selection so by consistently - // consuming input irrespective of the current platform reproducing fuzz - // bugs should be easier between different architectures. - macro_rules! target_features { - ( - $( - $arch:tt => { - test:$test:ident, - $(std: $std:tt => clif: $clif:tt $(ratio: $a:tt in $b:tt)?,)* - }, - )* - ) => ({ - let mut flags = Vec::new(); - $( // for each `$arch` - $( // for each `$std`/`$clif` pair - // Use the input to generate whether `$clif` will be - // enabled. By default this is a 1 in 2 chance but each - // feature supports a custom ratio as well which shadows - // the (low, hi) - let (low, hi) = (1, 2); - $(let (low, hi) = ($a, $b);)? - let enable = u.ratio(low, hi)?; - - // If we're actually on the relevant platform and the - // feature is enabled be sure to check that this host - // supports it. If the host doesn't support it then - // print a warning and return an error because this fuzz - // input must be discarded. - #[cfg(target_arch = $arch)] - if enable && !std::arch::$test!($std) { - log::warn!("want to enable clif `{}` but host doesn't support it", - $clif); - return Err(arbitrary::Error::EmptyChoose) - } - - // And finally actually push the feature into the set of - // flags to enable, but only if we're on the right - // architecture. - if cfg!(target_arch = $arch) { - flags.push(( - $clif.to_string(), - enable.to_string(), - )); - } - )* - )* - flags - }) - } - if u.ratio(1, 10)? { - let flags = target_features! { - "x86_64" => { - test: is_x86_feature_detected, - - // These features are considered to be baseline required by - // Wasmtime. Currently some SIMD code generation will - // fail if these features are disabled, so unconditionally - // enable them as we're not interested in fuzzing without - // them. - std:"sse3" => clif:"has_sse3" ratio: 1 in 1, - std:"ssse3" => clif:"has_ssse3" ratio: 1 in 1, - std:"sse4.1" => clif:"has_sse41" ratio: 1 in 1, - std:"sse4.2" => clif:"has_sse42" ratio: 1 in 1, - - std:"popcnt" => clif:"has_popcnt", - std:"avx" => clif:"has_avx", - std:"avx2" => clif:"has_avx2", - std:"bmi1" => clif:"has_bmi1", - std:"bmi2" => clif:"has_bmi2", - std:"lzcnt" => clif:"has_lzcnt", - - // not a lot of of cpus support avx512 so these are weighted - // to get enabled much less frequently. - std:"avx512bitalg" => clif:"has_avx512bitalg" ratio:1 in 1000, - std:"avx512dq" => clif:"has_avx512dq" ratio: 1 in 1000, - std:"avx512f" => clif:"has_avx512f" ratio: 1 in 1000, - std:"avx512vl" => clif:"has_avx512vl" ratio: 1 in 1000, - std:"avx512vbmi" => clif:"has_avx512vbmi" ratio: 1 in 1000, - }, - "aarch64" => { - test: is_aarch64_feature_detected, - - std: "lse" => clif: "has_lse", - }, - }; - return Ok(CodegenSettings::Target { - target: target_lexicon::Triple::host().to_string(), - flags, - }); - } - Ok(CodegenSettings::Native) - } -} +pub use codegen_settings::CodegenSettings; +pub use config::{Config, WasmtimeConfig}; +pub use instance_allocation_strategy::InstanceAllocationStrategy; +pub use instance_limits::InstanceLimits; +pub use memory::{MemoryConfig, NormalMemoryConfig, UnalignedMemory, UnalignedMemoryCreator}; +pub use module_config::ModuleConfig; +pub use spec_test::SpecTest; diff --git a/crates/fuzzing/src/generators/codegen_settings.rs b/crates/fuzzing/src/generators/codegen_settings.rs new file mode 100644 index 0000000000..f7a305a729 --- /dev/null +++ b/crates/fuzzing/src/generators/codegen_settings.rs @@ -0,0 +1,139 @@ +//! Generate Cranelift compiler settings. + +use arbitrary::{Arbitrary, Unstructured}; + +/// Choose between matching the host architecture or a cross-compilation target. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum CodegenSettings { + /// Use the host's feature set. + Native, + /// Generate a modified flag set for the current host. + #[allow(dead_code)] + Target { + /// The target triple of the host. + target: String, + /// A list of CPU features to enable, e.g., `("has_avx", "false")`. + flags: Vec<(String, String)>, + }, +} + +impl CodegenSettings { + /// Configure Wasmtime with these codegen settings. + pub fn configure(&self, config: &mut wasmtime::Config) { + match self { + CodegenSettings::Native => {} + CodegenSettings::Target { target, flags } => { + config.target(target).unwrap(); + for (key, value) in flags { + unsafe { + config.cranelift_flag_set(key, value); + } + } + } + } + } +} + +impl<'a> Arbitrary<'a> for CodegenSettings { + #[allow(unused_macros, unused_variables)] + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + // Helper macro to enable clif features based on what the native host + // supports. If the input says to enable a feature and the host doesn't + // support it then that test case is rejected with a warning. + // + // Note that this specifically consumes bytes from the fuzz input for + // features for all targets, discarding anything which isn't applicable + // to the current target. The theory behind this is that most fuzz bugs + // won't be related to this feature selection so by consistently + // consuming input irrespective of the current platform reproducing fuzz + // bugs should be easier between different architectures. + macro_rules! target_features { + ( + $( + $arch:tt => { + test:$test:ident, + $(std: $std:tt => clif: $clif:tt $(ratio: $a:tt in $b:tt)?,)* + }, + )* + ) => ({ + let mut flags = Vec::new(); + $( // for each `$arch` + $( // for each `$std`/`$clif` pair + // Use the input to generate whether `$clif` will be + // enabled. By default this is a 1 in 2 chance but each + // feature supports a custom ratio as well which shadows + // the (low, hi) + let (low, hi) = (1, 2); + $(let (low, hi) = ($a, $b);)? + let enable = u.ratio(low, hi)?; + + // If we're actually on the relevant platform and the + // feature is enabled be sure to check that this host + // supports it. If the host doesn't support it then + // print a warning and return an error because this fuzz + // input must be discarded. + #[cfg(target_arch = $arch)] + if enable && !std::arch::$test!($std) { + log::warn!("want to enable clif `{}` but host doesn't support it", + $clif); + return Err(arbitrary::Error::EmptyChoose) + } + + // And finally actually push the feature into the set of + // flags to enable, but only if we're on the right + // architecture. + if cfg!(target_arch = $arch) { + flags.push(( + $clif.to_string(), + enable.to_string(), + )); + } + )* + )* + flags + }) + } + if u.ratio(1, 10)? { + let flags = target_features! { + "x86_64" => { + test: is_x86_feature_detected, + + // These features are considered to be baseline required by + // Wasmtime. Currently some SIMD code generation will + // fail if these features are disabled, so unconditionally + // enable them as we're not interested in fuzzing without + // them. + std:"sse3" => clif:"has_sse3" ratio: 1 in 1, + std:"ssse3" => clif:"has_ssse3" ratio: 1 in 1, + std:"sse4.1" => clif:"has_sse41" ratio: 1 in 1, + std:"sse4.2" => clif:"has_sse42" ratio: 1 in 1, + + std:"popcnt" => clif:"has_popcnt", + std:"avx" => clif:"has_avx", + std:"avx2" => clif:"has_avx2", + std:"bmi1" => clif:"has_bmi1", + std:"bmi2" => clif:"has_bmi2", + std:"lzcnt" => clif:"has_lzcnt", + + // not a lot of of cpus support avx512 so these are weighted + // to get enabled much less frequently. + std:"avx512bitalg" => clif:"has_avx512bitalg" ratio:1 in 1000, + std:"avx512dq" => clif:"has_avx512dq" ratio: 1 in 1000, + std:"avx512f" => clif:"has_avx512f" ratio: 1 in 1000, + std:"avx512vl" => clif:"has_avx512vl" ratio: 1 in 1000, + std:"avx512vbmi" => clif:"has_avx512vbmi" ratio: 1 in 1000, + }, + "aarch64" => { + test: is_aarch64_feature_detected, + + std: "lse" => clif: "has_lse", + }, + }; + return Ok(CodegenSettings::Target { + target: target_lexicon::Triple::host().to_string(), + flags, + }); + } + Ok(CodegenSettings::Native) + } +} diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs new file mode 100644 index 0000000000..0358d70757 --- /dev/null +++ b/crates/fuzzing/src/generators/config.rs @@ -0,0 +1,410 @@ +//! Generate a configuration for both Wasmtime and the Wasm module to execute. + +use super::{ + CodegenSettings, InstanceAllocationStrategy, MemoryConfig, ModuleConfig, NormalMemoryConfig, + UnalignedMemoryCreator, +}; +use crate::oracles::{StoreLimits, Timeout}; +use anyhow::Result; +use arbitrary::{Arbitrary, Unstructured}; +use std::sync::Arc; +use std::time::Duration; +use wasmtime::{Engine, Module, Store}; + +/// 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(Debug, Clone)] +pub struct Config { + /// Configuration related to the `wasmtime::Config`. + pub wasmtime: WasmtimeConfig, + /// Configuration related to generated modules. + pub module_config: ModuleConfig, +} + +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 = config.max_types.max(1); + + // Generate at least one function + config.min_funcs = 1; + config.max_funcs = config.max_funcs.max(1); + + // Allow a memory to be generated, but don't let it get too large. + // Additionally require the maximum size to guarantee that the growth + // behavior is consistent across engines. + config.max_memories = 1; + config.max_memory_pages = 10; + config.memory_max_size_required = true; + + // If tables are generated make sure they don't get too large to avoid + // hitting any engine-specific limit. Additionally ensure that the + // maximum size is required to guarantee consistent growth across + // engines. + // + // Note that while reference types are disabled below, only allow one + // table. + config.max_tables = 1; + config.max_table_elements = 1_000; + config.table_max_size_required = true; + + // Don't allow any imports + config.max_imports = 0; + + // Try to get the function and the memory exported + config.export_everything = true; + + // 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; + config.threads_enabled = false; + + // If using the pooling allocator, update the instance limits too + if let InstanceAllocationStrategy::Pooling { + instance_limits: limits, + .. + } = &mut self.wasmtime.strategy + { + // One single-page memory + limits.memories = 1; + limits.memory_pages = 10; + + limits.tables = 1; + limits.table_elements = 1_000; + + 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); + } + + 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.bulk_memory_enabled = true; + config.reference_types_enabled = true; + config.multi_value_enabled = true; + config.simd_enabled = true; + config.threads_enabled = false; + config.max_memories = 1; + config.max_tables = 5; + + if let InstanceAllocationStrategy::Pooling { + instance_limits: limits, + .. + } = &mut self.wasmtime.strategy + { + // Configure the lower bound of a number of limits to what's + // required to actually run the spec tests. Fuzz-generated inputs + // may have limits less than these thresholds which would cause the + // spec tests to fail which isn't particularly interesting. + limits.memories = limits.memories.max(1); + limits.tables = limits.memories.max(5); + limits.table_elements = limits.memories.max(1_000); + limits.memory_pages = limits.memory_pages.max(900); + limits.count = limits.count.max(500); + limits.size = limits.size.max(64 * 1024); + + 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 + } + } + } + + /// Converts this to a `wasmtime::Config` object + pub fn to_wasmtime(&self) -> wasmtime::Config { + crate::init_fuzzing(); + log::debug!("creating wasmtime config with {:#?}", self.wasmtime); + + let mut cfg = wasmtime::Config::new(); + cfg.wasm_bulk_memory(true) + .wasm_reference_types(true) + .wasm_multi_value(self.module_config.config.multi_value_enabled) + .wasm_multi_memory(self.module_config.config.max_memories > 1) + .wasm_simd(self.module_config.config.simd_enabled) + .wasm_memory64(self.module_config.config.memory64_enabled) + .wasm_threads(self.module_config.config.threads_enabled) + .wasm_backtrace(self.wasmtime.wasm_backtraces) + .cranelift_nan_canonicalization(self.wasmtime.canonicalize_nans) + .cranelift_opt_level(self.wasmtime.opt_level.to_wasmtime()) + .consume_fuel(self.wasmtime.consume_fuel) + .epoch_interruption(self.wasmtime.epoch_interruption) + .memory_init_cow(self.wasmtime.memory_init_cow) + .memory_guaranteed_dense_image_size(std::cmp::min( + // Clamp this at 16MiB so we don't get huge in-memory + // images during fuzzing. + 16 << 20, + self.wasmtime.memory_guaranteed_dense_image_size, + )) + .allocation_strategy(self.wasmtime.strategy.to_wasmtime()) + .generate_address_map(self.wasmtime.generate_address_map); + + self.wasmtime.codegen.configure(&mut cfg); + + // 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 + // enable this codegen option. + cfg.cranelift_nan_canonicalization(!self.module_config.config.canonicalize_nans); + + // Enabling the verifier will at-least-double compilation time, which + // with a 20-30x slowdown in fuzzing can cause issues related to + // timeouts. If generated modules can have more than a small handful of + // functions then disable the verifier when fuzzing to try to lessen the + // impact of timeouts. + if self.module_config.config.max_funcs > 10 { + cfg.cranelift_debug_verifier(false); + } + + if self.wasmtime.force_jump_veneers { + unsafe { + cfg.cranelift_flag_set("wasmtime_linkopt_force_jump_veneer", "true"); + } + } + + if let Some(pad) = self.wasmtime.padding_between_functions { + unsafe { + cfg.cranelift_flag_set( + "wasmtime_linkopt_padding_between_functions", + &pad.to_string(), + ); + } + } + + // Vary the memory configuration, but only if threads are not enabled. + // When the threads proposal is enabled we might generate shared memory, + // which is less amenable to different memory configurations: + // - shared memories are required to be "static" so fuzzing the various + // memory configurations will mostly result in uninteresting errors. + // The interesting part about shared memories is the runtime so we + // don't fuzz non-default settings. + // - shared memories are required to be aligned which means that the + // `CustomUnaligned` variant isn't actually safe to use with a shared + // memory. + if !self.module_config.config.threads_enabled { + match &self.wasmtime.memory_config { + 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)) + .static_memory_maximum_size(0) + .dynamic_memory_guard_size(0) + .static_memory_guard_size(0) + .guard_before_linear_memory(false); + } + } + } + + return cfg; + } + + /// Convenience function for generating a `Store` using this + /// configuration. + 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(); + } + if self.wasmtime.epoch_interruption { + // Without fuzzing of async execution, we can't test the + // "update deadline and continue" behavior, but we can at + // least test the codegen paths and checks with the + // trapping behavior, which works synchronously too. We'll + // set the deadline one epoch tick in the future; then + // this works exactly like an interrupt flag. We expect no + // traps/interrupts unless we bump the epoch, which we do + // as one particular Timeout mode (`Timeout::Epoch`). + store.epoch_deadline_trap(); + store.set_epoch_deadline(1); + } + } + + /// Generates an arbitrary method of timing out an instance, ensuring that + /// this configuration supports the returned timeout. + pub fn generate_timeout(&mut self, u: &mut Unstructured<'_>) -> arbitrary::Result { + let time_duration = Duration::from_secs(20); + let timeout = u + .choose(&[Timeout::Fuel(100_000), Timeout::Epoch(time_duration)])? + .clone(); + match &timeout { + Timeout::Fuel(..) => { + self.wasmtime.consume_fuel = true; + } + Timeout::Epoch(..) => { + self.wasmtime.epoch_interruption = true; + } + Timeout::None => unreachable!("Not an option given to choose()"), + } + Ok(timeout) + } + + /// Compiles the `wasm` within the `engine` provided. + /// + /// This notably will use `Module::{serialize,deserialize_file}` to + /// round-trip if configured in the fuzzer. + pub fn compile(&self, engine: &Engine, wasm: &[u8]) -> Result { + // Propagate this error in case the caller wants to handle + // valid-vs-invalid wasm. + let module = Module::new(engine, wasm)?; + if !self.wasmtime.use_precompiled_cwasm { + return Ok(module); + } + + // Don't propagate these errors to prevent them from accidentally being + // interpreted as invalid wasm, these should never fail on a + // well-behaved host system. + let file = tempfile::NamedTempFile::new().unwrap(); + std::fs::write(file.path(), module.serialize().unwrap()).unwrap(); + unsafe { Ok(Module::deserialize_file(engine, file.path()).unwrap()) } + } +} + +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 { + instance_limits: limits, + .. + } = &config.wasmtime.strategy + { + // If the pooling allocator is used, do not allow shared memory to + // be created. FIXME: see + // https://github.com/bytecodealliance/wasmtime/issues/4244. + config.module_config.config.threads_enabled = false; + + // 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. + 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_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)] +pub struct WasmtimeConfig { + opt_level: OptLevel, + debug_info: bool, + canonicalize_nans: bool, + interruptable: bool, + pub(crate) consume_fuel: bool, + epoch_interruption: bool, + /// The Wasmtime memory configuration to use. + pub memory_config: MemoryConfig, + force_jump_veneers: bool, + memory_init_cow: bool, + memory_guaranteed_dense_image_size: u64, + use_precompiled_cwasm: bool, + /// Configuration for the instance allocation strategy to use. + pub strategy: InstanceAllocationStrategy, + codegen: CodegenSettings, + padding_between_functions: Option, + generate_address_map: bool, + wasm_backtraces: bool, +} + +#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)] +enum OptLevel { + None, + Speed, + SpeedAndSize, +} + +impl OptLevel { + fn to_wasmtime(&self) -> wasmtime::OptLevel { + match self { + OptLevel::None => wasmtime::OptLevel::None, + OptLevel::Speed => wasmtime::OptLevel::Speed, + OptLevel::SpeedAndSize => wasmtime::OptLevel::SpeedAndSize, + } + } +} diff --git a/crates/fuzzing/src/generators/instance_allocation_strategy.rs b/crates/fuzzing/src/generators/instance_allocation_strategy.rs new file mode 100644 index 0000000000..f5aabeb58e --- /dev/null +++ b/crates/fuzzing/src/generators/instance_allocation_strategy.rs @@ -0,0 +1,58 @@ +use arbitrary::Arbitrary; + +use super::InstanceLimits; + +/// 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 instance limits. + instance_limits: InstanceLimits, + }, +} + +impl InstanceAllocationStrategy { + /// Convert this generated strategy a Wasmtime strategy. + pub fn to_wasmtime(&self) -> wasmtime::InstanceAllocationStrategy { + match self { + InstanceAllocationStrategy::OnDemand => wasmtime::InstanceAllocationStrategy::OnDemand, + InstanceAllocationStrategy::Pooling { + strategy, + instance_limits, + } => wasmtime::InstanceAllocationStrategy::Pooling { + strategy: strategy.to_wasmtime(), + instance_limits: instance_limits.to_wasmtime(), + }, + } + } +} + +/// 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 + } + } + } +} diff --git a/crates/fuzzing/src/generators/instance_limits.rs b/crates/fuzzing/src/generators/instance_limits.rs new file mode 100644 index 0000000000..7176d2ba65 --- /dev/null +++ b/crates/fuzzing/src/generators/instance_limits.rs @@ -0,0 +1,49 @@ +//! Generate instance limits for the pooling allocation strategy. + +use arbitrary::{Arbitrary, Unstructured}; + +/// Configuration for `wasmtime::PoolingAllocationStrategy`. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[allow(missing_docs)] +pub struct InstanceLimits { + pub count: u32, + pub memories: u32, + pub tables: u32, + pub memory_pages: u64, + pub table_elements: u32, + pub size: usize, +} + +impl InstanceLimits { + /// Convert the generated limits to Wasmtime limits. + pub fn to_wasmtime(&self) -> wasmtime::InstanceLimits { + wasmtime::InstanceLimits { + count: self.count, + memories: self.memories, + tables: self.tables, + memory_pages: self.memory_pages, + table_elements: self.table_elements, + size: self.size, + } + } +} + +impl<'a> Arbitrary<'a> for InstanceLimits { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + const MAX_COUNT: u32 = 100; + const MAX_TABLES: u32 = 10; + const MAX_MEMORIES: u32 = 10; + const MAX_ELEMENTS: u32 = 1000; + const MAX_MEMORY_PAGES: u64 = 160; // 10 MiB + const MAX_SIZE: usize = 1 << 20; // 1 MiB + + Ok(Self { + tables: u.int_in_range(0..=MAX_TABLES)?, + memories: u.int_in_range(0..=MAX_MEMORIES)?, + table_elements: u.int_in_range(0..=MAX_ELEMENTS)?, + memory_pages: u.int_in_range(0..=MAX_MEMORY_PAGES)?, + count: u.int_in_range(1..=MAX_COUNT)?, + size: u.int_in_range(0..=MAX_SIZE)?, + }) + } +} diff --git a/crates/fuzzing/src/generators/memory.rs b/crates/fuzzing/src/generators/memory.rs new file mode 100644 index 0000000000..57ad4dd415 --- /dev/null +++ b/crates/fuzzing/src/generators/memory.rs @@ -0,0 +1,110 @@ +//! Generate various kinds of Wasm memory. + +use anyhow::Result; +use arbitrary::{Arbitrary, Unstructured}; +use wasmtime::{LinearMemory, MemoryCreator, MemoryType}; + +/// Configuration for linear memories in Wasmtime. +#[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)] +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. + 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 + /// level, even if the wasm itself correctly aligns everything internally. + CustomUnaligned, +} + +/// Represents a normal memory configuration for Wasmtime with the given +/// static and dynamic memory sizes. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[allow(missing_docs)] +pub struct NormalMemoryConfig { + pub static_memory_maximum_size: Option, + pub static_memory_guard_size: Option, + pub dynamic_memory_guard_size: Option, + pub 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. + let mut ret = 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()?, + }; + + if let Some(dynamic) = ret.dynamic_memory_guard_size { + let statik = ret.static_memory_guard_size.unwrap_or(2 << 30); + ret.static_memory_guard_size = Some(statik.max(dynamic)); + } + Ok(ret) + } +} + +/// A custom "linear memory allocator" for wasm which only works with the +/// "dynamic" mode of configuration where wasm always does explicit bounds +/// checks. +/// +/// This memory attempts to always use unaligned host addresses for the base +/// address of linear memory with wasm. This means that all jit loads/stores +/// should be unaligned, which is a "big hammer way" of testing that all our JIT +/// code works with unaligned addresses since alignment is not required for +/// correctness in wasm itself. +pub struct UnalignedMemory { + /// This memory is always one byte larger than the actual size of linear + /// memory. + src: Vec, + maximum: Option, +} + +unsafe impl LinearMemory for UnalignedMemory { + fn byte_size(&self) -> usize { + // Chop off the extra byte reserved for the true byte size of this + // linear memory. + self.src.len() - 1 + } + + fn maximum_byte_size(&self) -> Option { + self.maximum + } + + fn grow_to(&mut self, new_size: usize) -> Result<()> { + // Make sure to allocate an extra byte for our "unalignment" + self.src.resize(new_size + 1, 0); + Ok(()) + } + + fn as_ptr(&self) -> *mut u8 { + // Return our allocated memory, offset by one, so that the base address + // of memory is always unaligned. + self.src[1..].as_ptr() as *mut _ + } +} + +/// A mechanism to generate [`UnalignedMemory`] at runtime. +pub struct UnalignedMemoryCreator; + +unsafe impl MemoryCreator for UnalignedMemoryCreator { + fn new_memory( + &self, + _ty: MemoryType, + minimum: usize, + maximum: Option, + reserved_size_in_bytes: Option, + guard_size_in_bytes: usize, + ) -> Result, String> { + assert_eq!(guard_size_in_bytes, 0); + assert!(reserved_size_in_bytes.is_none() || reserved_size_in_bytes == Some(0)); + Ok(Box::new(UnalignedMemory { + src: vec![0; minimum + 1], + maximum, + })) + } +} diff --git a/crates/fuzzing/src/generators/module_config.rs b/crates/fuzzing/src/generators/module_config.rs new file mode 100644 index 0000000000..190b41e3d4 --- /dev/null +++ b/crates/fuzzing/src/generators/module_config.rs @@ -0,0 +1,38 @@ +//! Generate a configuration for generating a Wasm module. + +use arbitrary::{Arbitrary, Unstructured}; +use wasm_smith::SwarmConfig; + +/// Default module-level configuration for fuzzing Wasmtime. +/// +/// Internally this uses `wasm-smith`'s own `SwarmConfig` but we further refine +/// the defaults here as well. +#[derive(Debug, Clone)] +pub struct ModuleConfig { + #[allow(missing_docs)] + pub config: SwarmConfig, +} + +impl<'a> Arbitrary<'a> for ModuleConfig { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let mut config = SwarmConfig::arbitrary(u)?; + + // Allow multi-memory by default. + config.max_memories = config.max_memories.max(2); + + // Allow multi-table by default. + config.max_tables = config.max_tables.max(4); + + // Allow enabling some various wasm proposals by default. Note that + // these are all unconditionally turned off even with + // `SwarmConfig::arbitrary`. + config.memory64_enabled = u.arbitrary()?; + + // Allow the threads proposal if memory64 is not already enabled. FIXME: + // to allow threads and memory64 to coexist, see + // https://github.com/bytecodealliance/wasmtime/issues/4267. + config.threads_enabled = !config.memory64_enabled && u.arbitrary()?; + + Ok(ModuleConfig { config }) + } +} diff --git a/crates/fuzzing/src/generators/spec_test.rs b/crates/fuzzing/src/generators/spec_test.rs new file mode 100644 index 0000000000..2e11e46a1e --- /dev/null +++ b/crates/fuzzing/src/generators/spec_test.rs @@ -0,0 +1,29 @@ +//! Arbitrarily choose a spec test from the list of known spec tests. + +use arbitrary::{Arbitrary, Unstructured}; + +// See `build.rs` for how the `FILES` array is generated. +include!(concat!(env!("OUT_DIR"), "/spectests.rs")); + +/// A spec test from the upstream wast testsuite, arbitrarily chosen from the +/// list of known spec tests. +#[derive(Debug)] +pub struct SpecTest { + /// The filename of the spec test + pub file: &'static str, + /// The `*.wast` contents of the spec test + pub contents: &'static str, +} + +impl<'a> Arbitrary<'a> for SpecTest { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + // NB: this does get a uniform value in the provided range. + let i = u.int_in_range(0..=FILES.len() - 1)?; + let (file, contents) = FILES[i]; + Ok(SpecTest { file, contents }) + } + + fn size_hint(_depth: usize) -> (usize, Option) { + (1, Some(std::mem::size_of::())) + } +}