diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs index 155895a78895..de264add21e5 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -28,7 +28,10 @@ pub use codegen_settings::CodegenSettings; pub use config::CompilerStrategy; pub use config::{Config, WasmtimeConfig}; pub use instance_allocation_strategy::InstanceAllocationStrategy; -pub use memory::{MemoryConfig, NormalMemoryConfig, UnalignedMemory, UnalignedMemoryCreator}; +pub use memory::{ + HeapImage, MemoryAccesses, MemoryConfig, NormalMemoryConfig, UnalignedMemory, + UnalignedMemoryCreator, +}; pub use module::ModuleConfig; pub use pooling_config::PoolingAllocationConfig; pub use single_inst_module::SingleInstModule; diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs index c42c5f8d249d..69f321e86142 100644 --- a/crates/fuzzing/src/generators/config.rs +++ b/crates/fuzzing/src/generators/config.rs @@ -172,7 +172,6 @@ impl Config { .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. @@ -259,7 +258,9 @@ impl Config { static_memory_maximum_size: Some(4 << 30), // 4 GiB static_memory_guard_size: Some(2 << 30), // 2 GiB dynamic_memory_guard_size: Some(0), + dynamic_memory_reserved_for_growth: Some(0), guard_before_linear_memory: false, + memory_init_cow: true, }) } else { self.wasmtime.memory_config.clone() @@ -267,19 +268,16 @@ impl Config { match &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); + memory_config.apply_to(&mut cfg); } MemoryConfig::CustomUnaligned => { cfg.with_host_memory(Arc::new(UnalignedMemoryCreator)) .static_memory_maximum_size(0) .dynamic_memory_guard_size(0) + .dynamic_memory_reserved_for_growth(0) .static_memory_guard_size(0) - .guard_before_linear_memory(false); + .guard_before_linear_memory(false) + .memory_init_cow(false); } } } diff --git a/crates/fuzzing/src/generators/memory.rs b/crates/fuzzing/src/generators/memory.rs index bdf679b58319..bb419b0f2440 100644 --- a/crates/fuzzing/src/generators/memory.rs +++ b/crates/fuzzing/src/generators/memory.rs @@ -5,6 +5,75 @@ use arbitrary::{Arbitrary, Unstructured}; use std::ops::Range; use wasmtime::{LinearMemory, MemoryCreator, MemoryType}; +/// A description of a memory config, image, etc... that can be used to test +/// memory accesses. +#[derive(Debug)] +#[allow(missing_docs)] +pub struct MemoryAccesses { + pub memory_config: NormalMemoryConfig, + pub image: HeapImage, + pub offset: u32, + pub growth: u32, +} + +impl<'a> Arbitrary<'a> for MemoryAccesses { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + Ok(MemoryAccesses { + memory_config: u.arbitrary()?, + image: u.arbitrary()?, + offset: u.arbitrary()?, + growth: u.arbitrary()?, + }) + } +} + +/// A memory heap image. +#[allow(missing_docs)] +pub struct HeapImage { + pub minimum: u32, + pub maximum: Option, + pub segments: Vec<(u32, Vec)>, +} + +impl std::fmt::Debug for HeapImage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HeapImage") + .field("minimum", &self.minimum) + .field("maximum", &self.maximum) + .field("segments", &"..") + .finish() + } +} + +impl<'a> Arbitrary<'a> for HeapImage { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let minimum = u.int_in_range(0..=4)?; + let maximum = if u.arbitrary()? { + Some(u.int_in_range(minimum..=10)?) + } else { + None + }; + let mut segments = vec![]; + if minimum > 0 { + for _ in 0..u.int_in_range(0..=4)? { + const WASM_PAGE_SIZE: u32 = 65536; + let last_addressable = WASM_PAGE_SIZE * minimum - 1; + let offset = u.int_in_range(0..=last_addressable)?; + let max_len = + std::cmp::min(u.len(), usize::try_from(last_addressable - offset).unwrap()); + let len = u.int_in_range(0..=max_len)?; + let data = u.bytes(len)?.to_vec(); + segments.push((offset, data)); + } + } + Ok(HeapImage { + minimum, + maximum, + segments, + }) + } +} + /// Configuration for linear memories in Wasmtime. #[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)] pub enum MemoryConfig { @@ -27,7 +96,9 @@ pub struct NormalMemoryConfig { pub static_memory_maximum_size: Option, pub static_memory_guard_size: Option, pub dynamic_memory_guard_size: Option, + pub dynamic_memory_reserved_for_growth: Option, pub guard_before_linear_memory: bool, + pub memory_init_cow: bool, } impl<'a> Arbitrary<'a> for NormalMemoryConfig { @@ -38,17 +109,36 @@ impl<'a> Arbitrary<'a> for NormalMemoryConfig { 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), + dynamic_memory_reserved_for_growth: as Arbitrary>::arbitrary(u)? + .map(Into::into), guard_before_linear_memory: u.arbitrary()?, + memory_init_cow: 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 NormalMemoryConfig { + /// Apply this memory configuration to the given `wasmtime::Config`. + pub fn apply_to(&self, config: &mut wasmtime::Config) { + config + .static_memory_maximum_size(self.static_memory_maximum_size.unwrap_or(0)) + .static_memory_guard_size(self.static_memory_guard_size.unwrap_or(0)) + .dynamic_memory_guard_size(self.dynamic_memory_guard_size.unwrap_or(0)) + .dynamic_memory_reserved_for_growth( + self.dynamic_memory_reserved_for_growth.unwrap_or(0), + ) + .guard_before_linear_memory(self.guard_before_linear_memory) + .memory_init_cow(self.memory_init_cow); + } +} + /// A custom "linear memory allocator" for wasm which only works with the /// "dynamic" mode of configuration where wasm always does explicit bounds /// checks. diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index 686e22fdb829..38cef3d17fac 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -16,6 +16,7 @@ pub mod diff_wasmi; pub mod diff_wasmtime; pub mod dummy; pub mod engine; +pub mod memory; mod stacks; use self::diff_wasmtime::WasmtimeInstance; diff --git a/crates/fuzzing/src/oracles/memory.rs b/crates/fuzzing/src/oracles/memory.rs new file mode 100644 index 000000000000..341add4857ab --- /dev/null +++ b/crates/fuzzing/src/oracles/memory.rs @@ -0,0 +1,316 @@ +//! Oracles related to memory. + +use crate::generators::{HeapImage, MemoryAccesses}; +use wasmtime::*; + +/// Oracle to perform the described memory accesses and check that they are all +/// in- or out-of-bounds as expected +pub fn check_memory_accesses(input: MemoryAccesses) { + crate::init_fuzzing(); + log::info!("Testing memory accesses: {input:#x?}"); + + let MemoryAccesses { + memory_config, + image, + offset, + growth, + } = input; + + let wasm = build_wasm(&image, offset); + crate::oracles::log_wasm(&wasm); + + let mut config = wasmtime::Config::new(); + memory_config.apply_to(&mut config); + + let engine = Engine::new(&config).unwrap(); + let module = Module::new(&engine, &wasm).unwrap(); + + let mut store = Store::new(&engine, ()); + let instance = match Instance::new(&mut store, &module, &[]) { + Ok(x) => x, + Err(e) => { + log::info!("Failed to instantiate: {e}"); + assert!(format!("{e:?}").contains("Cannot allocate memory")); + return; + } + }; + + let memory = instance.get_memory(&mut store, "memory").unwrap(); + let load8 = instance + .get_typed_func::(&mut store, "load8") + .unwrap(); + let load16 = instance + .get_typed_func::(&mut store, "load16") + .unwrap(); + let load32 = instance + .get_typed_func::(&mut store, "load32") + .unwrap(); + let load64 = instance + .get_typed_func::(&mut store, "load64") + .unwrap(); + + let do_accesses = |store: &mut Store<()>, msg: &str| { + let len = memory.data_size(&mut *store); + + if let Some(n) = len + .checked_sub(8) + .and_then(|n| n.checked_sub(usize::try_from(offset).unwrap())) + .and_then(|n| { + // Only convert to a `u32` after the `checked_sub` above, + // because `len` can validly be `2**32` while `u32` can only + // hold up to `2**32 - 1`. + u32::try_from(n).ok() + }) + { + // Test various in-bounds accesses near the bound. + for i in 0..=7 { + let addr = n + i; + assert!( + load8.call(&mut *store, addr).is_ok(), + "{msg}: len={len:#x}, offset={offset:#x}, load8({n} + {i:#x} = {addr:#x}) is in bounds" + ); + } + for i in 0..=6 { + let addr = n + i; + assert!( + load16.call(&mut *store, n + i).is_ok(), + "{msg}: len={len:#x}, offset={offset:#x}, load16({n} + {i:#x} = {addr:#x}) is in bounds" + ); + } + for i in 0..=4 { + let addr = n + i; + assert!( + load32.call(&mut *store, n + i).is_ok(), + "{msg}: len={len:#x}, offset={offset:#x}, load32({n} + {i:#x} = {addr:#x}) is in bounds" + ); + } + assert!( + load64.call(&mut *store, n).is_ok(), + "{msg}: len={len:#x}, offset={offset:#x}, load64({n}) is in bounds" + ); + + // Test various out-of-bounds accesses overlapping the memory bound. + for i in 7..=7 { + let addr = n + i; + assert!( + load16.call(&mut *store, n + i).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load16({n} + {i:#x} = {addr:#x}) traps" + ); + } + for i in 5..=7 { + let addr = n + i; + assert!( + load32.call(&mut *store, n + i).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load32({n} + {i:#x} = {addr:#x}) traps" + ); + } + for i in 8..=7 { + let addr = n + i; + assert!( + load64.call(&mut *store, n + i).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load64({n} + {i:#x} = {addr:#x}) traps" + ); + } + } + + // Test that out-of-bounds accesses just after the memory bound trap. + if let Some(n) = len + .checked_sub(usize::try_from(offset).unwrap()) + .and_then(|n| + // Only convert to a `u32` after the `checked_sub` above, because + // `len` can validly be `2**32` while `u32` can only hold up to + // `2**32 - 1`. + u32::try_from(n).ok()) + { + for i in 0..=1 { + let addr = n + i; + assert!( + load8.call(&mut *store, addr).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load8({n} + {i:#x} = {addr:#x}) traps" + ); + assert!( + load16.call(&mut *store, addr).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load16({n} + {i:#x} = {addr:#x}) traps" + ); + assert!( + load32.call(&mut *store, addr).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load32({n} + {i:#x} = {addr:#x}) traps" + ); + assert!( + load64.call(&mut *store, addr).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load64({n} + {i:#x} = {addr:#x}) traps" + ); + } + } + + // Test out-of-bounds accesses near the end of the index type's range to + // double check our overflow handling inside the bounds checks. + let len_is_4gib = u64::try_from(len).unwrap() == u64::from(u32::MAX) + 1; + let end_delta = len_is_4gib as u32; + let max = u32::MAX; + for i in 0..(1 - end_delta) { + let addr = max - i; + assert!( + load8.call(&mut *store, addr).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load8({max:#x} - {i:#x} = {addr:#x}) traps" + ); + } + for i in 0..(2 - end_delta) { + let addr = max - i; + assert!( + load16.call(&mut *store, addr).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load16({max:#x} - {i:#x} = {addr:#x}) traps" + ); + } + for i in 0..(4 - end_delta) { + let addr = max - i; + assert!( + load32.call(&mut *store, addr).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load32({max:#x} - {i:#x} = {addr:#x}) traps" + ); + } + for i in 0..(8 - end_delta) { + let addr = max - i; + assert!( + load64.call(&mut *store, addr).is_err(), + "{msg}: len={len:#x}, offset={offset:#x}, load64({max:#x} - {i:#x} = {addr:#x}) traps" + ); + } + }; + + do_accesses(&mut store, "initial size"); + let _ = memory.grow(&mut store, u64::from(growth)); + do_accesses(&mut store, "after growing"); +} + +fn build_wasm(image: &HeapImage, offset: u32) -> Vec { + let mut module = wasm_encoder::Module::new(); + + { + let mut types = wasm_encoder::TypeSection::new(); + types.function([wasm_encoder::ValType::I32], [wasm_encoder::ValType::I32]); + types.function([wasm_encoder::ValType::I32], [wasm_encoder::ValType::I64]); + module.section(&types); + } + + { + let mut funcs = wasm_encoder::FunctionSection::new(); + funcs.function(0); + funcs.function(0); + funcs.function(0); + funcs.function(1); + module.section(&funcs); + } + + { + let mut memories = wasm_encoder::MemorySection::new(); + memories.memory(wasm_encoder::MemoryType { + minimum: u64::from(image.minimum), + maximum: image.maximum.map(Into::into), + memory64: false, + shared: false, + page_size_log2: None, + }); + module.section(&memories); + } + + { + let mut exports = wasm_encoder::ExportSection::new(); + exports.export("memory", wasm_encoder::ExportKind::Memory, 0); + exports.export("load8", wasm_encoder::ExportKind::Func, 0); + exports.export("load16", wasm_encoder::ExportKind::Func, 1); + exports.export("load32", wasm_encoder::ExportKind::Func, 2); + exports.export("load64", wasm_encoder::ExportKind::Func, 3); + module.section(&exports); + } + + { + let mut code = wasm_encoder::CodeSection::new(); + { + let mut func = wasm_encoder::Function::new([]); + func.instruction(&wasm_encoder::Instruction::LocalGet(0)); + func.instruction(&wasm_encoder::Instruction::I32Load8U( + wasm_encoder::MemArg { + offset: u64::from(offset), + align: 0, + memory_index: 0, + }, + )); + func.instruction(&wasm_encoder::Instruction::End); + code.function(&func); + } + { + let mut func = wasm_encoder::Function::new([]); + func.instruction(&wasm_encoder::Instruction::LocalGet(0)); + func.instruction(&wasm_encoder::Instruction::I32Load16U( + wasm_encoder::MemArg { + offset: u64::from(offset), + align: 0, + memory_index: 0, + }, + )); + func.instruction(&wasm_encoder::Instruction::End); + code.function(&func); + } + { + let mut func = wasm_encoder::Function::new([]); + func.instruction(&wasm_encoder::Instruction::LocalGet(0)); + func.instruction(&wasm_encoder::Instruction::I32Load(wasm_encoder::MemArg { + offset: u64::from(offset), + align: 0, + memory_index: 0, + })); + func.instruction(&wasm_encoder::Instruction::End); + code.function(&func); + } + { + let mut func = wasm_encoder::Function::new([]); + func.instruction(&wasm_encoder::Instruction::LocalGet(0)); + func.instruction(&wasm_encoder::Instruction::I64Load(wasm_encoder::MemArg { + offset: u64::from(offset), + align: 0, + memory_index: 0, + })); + func.instruction(&wasm_encoder::Instruction::End); + code.function(&func); + } + module.section(&code); + } + + { + let mut datas = wasm_encoder::DataSection::new(); + for (offset, data) in image.segments.iter() { + datas.segment(wasm_encoder::DataSegment { + mode: wasm_encoder::DataSegmentMode::Active { + memory_index: 0, + offset: &wasm_encoder::ConstExpr::i32_const(*offset as i32), + }, + data: data.iter().copied(), + }); + } + module.section(&datas); + } + + module.finish() +} + +#[cfg(test)] +mod tests { + use super::*; + use arbitrary::{Arbitrary, Unstructured}; + use rand::prelude::*; + + #[test] + fn smoke_test_memory_access() { + let mut rng = SmallRng::seed_from_u64(0); + let mut buf = vec![0; 1024]; + + for _ in 0..1024 { + rng.fill_bytes(&mut buf); + let u = Unstructured::new(&buf); + if let Ok(input) = MemoryAccesses::arbitrary_take_rest(u) { + check_memory_accesses(input); + } + } + } +} diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 1f8e2cee1ae8..f424c424c48b 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -114,3 +114,10 @@ name = "call_async" path = "fuzz_targets/call_async.rs" test = false doc = false + +[[bin]] +name = "memory_accesses" +path = "fuzz_targets/memory_accesses.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/memory_accesses.rs b/fuzz/fuzz_targets/memory_accesses.rs new file mode 100644 index 000000000000..1386a069b8e7 --- /dev/null +++ b/fuzz/fuzz_targets/memory_accesses.rs @@ -0,0 +1,8 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use wasmtime_fuzzing::{generators::MemoryAccesses, oracles::memory::check_memory_accesses}; + +fuzz_target!(|input: MemoryAccesses| { + check_memory_accesses(input); +});