diff --git a/crates/fuzzing/src/generators/memory.rs b/crates/fuzzing/src/generators/memory.rs index 57ad4dd415..bdf679b583 100644 --- a/crates/fuzzing/src/generators/memory.rs +++ b/crates/fuzzing/src/generators/memory.rs @@ -2,6 +2,7 @@ use anyhow::Result; use arbitrary::{Arbitrary, Unstructured}; +use std::ops::Range; use wasmtime::{LinearMemory, MemoryCreator, MemoryType}; /// Configuration for linear memories in Wasmtime. @@ -86,6 +87,12 @@ unsafe impl LinearMemory for UnalignedMemory { // of memory is always unaligned. self.src[1..].as_ptr() as *mut _ } + + fn wasm_accessible(&self) -> Range { + let base = self.as_ptr() as usize; + let len = self.byte_size(); + base..base + len + } } /// A mechanism to generate [`UnalignedMemory`] at runtime. diff --git a/crates/runtime/src/instance.rs b/crates/runtime/src/instance.rs index eabe277940..c3c6d6378a 100644 --- a/crates/runtime/src/instance.rs +++ b/crates/runtime/src/instance.rs @@ -13,7 +13,7 @@ use crate::vmcontext::{ }; use crate::{ ExportFunction, ExportGlobal, ExportMemory, ExportTable, Imports, ModuleRuntimeInfo, Store, - VMFunctionBody, VMSharedSignatureIndex, + VMFunctionBody, VMSharedSignatureIndex, WasmFault, }; use anyhow::Error; use anyhow::Result; @@ -1046,6 +1046,23 @@ impl Instance { } } } + + fn wasm_fault(&self, addr: usize) -> Option { + let mut fault = None; + for (_, memory) in self.memories.iter() { + let accessible = memory.wasm_accessible(); + if accessible.start <= addr && addr < accessible.end { + // All linear memories should be disjoint so assert that no + // prior fault has been found. + assert!(fault.is_none()); + fault = Some(WasmFault { + memory_size: memory.byte_size(), + wasm_address: u64::try_from(addr - accessible.start).unwrap(), + }); + } + } + fault + } } impl Drop for Instance { @@ -1231,4 +1248,15 @@ impl InstanceHandle { pub fn initialize(&mut self, module: &Module, is_bulk_memory: bool) -> Result<()> { allocator::initialize_instance(self.instance_mut(), module, is_bulk_memory) } + + /// Attempts to convert from the host `addr` specified to a WebAssembly + /// based address recorded in `WasmFault`. + /// + /// This method will check all linear memories that this instance contains + /// to see if any of them contain `addr`. If one does then `Some` is + /// returned with metadata about the wasm fault. Otherwise `None` is + /// returned and `addr` doesn't belong to this instance. + pub fn wasm_fault(&self, addr: usize) -> Option { + self.instance().wasm_fault(addr) + } } diff --git a/crates/runtime/src/instance/allocator/pooling.rs b/crates/runtime/src/instance/allocator/pooling.rs index 05fe1a352c..c89d749068 100644 --- a/crates/runtime/src/instance/allocator/pooling.rs +++ b/crates/runtime/src/instance/allocator/pooling.rs @@ -817,9 +817,13 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator { // else to come in and map something. slot.instantiate(initial_size as usize, image, &plan.style)?; - memories.push(Memory::new_static(plan, memory, slot, unsafe { - &mut *req.store.get().unwrap() - })?); + memories.push(Memory::new_static( + plan, + memory, + slot, + self.memories.memory_and_guard_size, + unsafe { &mut *req.store.get().unwrap() }, + )?); } Ok(()) diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 31b0137862..3428a0ffb0 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -21,6 +21,7 @@ )] use anyhow::Error; +use std::fmt; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::sync::Arc; use wasmtime_environ::{DefinedFuncIndex, DefinedMemoryIndex, HostPtr, VMOffsets}; @@ -237,3 +238,22 @@ pub enum WaitResult { /// original value matched as expected but nothing ever called `notify`. TimedOut = 2, } + +/// Description about a fault that occurred in WebAssembly. +#[derive(Debug)] +pub struct WasmFault { + /// The size of memory, in bytes, at the time of the fault. + pub memory_size: usize, + /// The WebAssembly address at which the fault occurred. + pub wasm_address: u64, +} + +impl fmt::Display for WasmFault { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "memory fault at wasm address 0x{:x} in linear memory of size 0x{:x}", + self.wasm_address, self.memory_size, + ) + } +} diff --git a/crates/runtime/src/memory.rs b/crates/runtime/src/memory.rs index 3cbcd125da..e3f3d2c564 100644 --- a/crates/runtime/src/memory.rs +++ b/crates/runtime/src/memory.rs @@ -9,6 +9,7 @@ use crate::{MemoryImage, MemoryImageSlot, Store, WaitResult}; use anyhow::Error; use anyhow::{bail, format_err, Result}; use std::convert::TryFrom; +use std::ops::Range; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::sync::{Arc, RwLock}; use std::time::Instant; @@ -152,6 +153,12 @@ pub trait RuntimeLinearMemory: Send + Sync { /// Used for optional dynamic downcasting. fn as_any_mut(&mut self) -> &mut dyn std::any::Any; + + /// Returns the range of addresses that may be reached by WebAssembly. + /// + /// This starts at the base of linear memory and ends at the end of the + /// guard pages, if any. + fn wasm_accessible(&self) -> Range; } /// A linear memory instance. @@ -338,6 +345,12 @@ impl RuntimeLinearMemory for MmapMemory { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn wasm_accessible(&self) -> Range { + let base = self.mmap.as_mut_ptr() as usize + self.pre_guard_size; + let end = base + (self.mmap.len() - self.pre_guard_size); + base..end + } } /// A "static" memory where the lifetime of the backing memory is managed @@ -350,6 +363,10 @@ struct StaticMemory { /// The current size, in bytes, of this memory. size: usize, + /// The size, in bytes, of the virtual address allocation starting at `base` + /// and going to the end of the guard pages at the end of the linear memory. + memory_and_guard_size: usize, + /// The image management, if any, for this memory. Owned here and /// returned to the pooling allocator when termination occurs. memory_image: MemoryImageSlot, @@ -361,6 +378,7 @@ impl StaticMemory { initial_size: usize, maximum_size: Option, memory_image: MemoryImageSlot, + memory_and_guard_size: usize, ) -> Result { if base.len() < initial_size { bail!( @@ -381,6 +399,7 @@ impl StaticMemory { base, size: initial_size, memory_image, + memory_and_guard_size, }) } } @@ -420,6 +439,12 @@ impl RuntimeLinearMemory for StaticMemory { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn wasm_accessible(&self) -> Range { + let base = self.base.as_ptr() as usize; + let end = base + self.memory_and_guard_size; + base..end + } } /// For shared memory (and only for shared memory), this lock-version restricts @@ -620,6 +645,10 @@ impl RuntimeLinearMemory for SharedMemory { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn wasm_accessible(&self) -> Range { + self.0.memory.read().unwrap().wasm_accessible() + } } /// Representation of a runtime wasm linear memory. @@ -648,10 +677,12 @@ impl Memory { plan: &MemoryPlan, base: &'static mut [u8], memory_image: MemoryImageSlot, + memory_and_guard_size: usize, store: &mut dyn Store, ) -> Result { let (minimum, maximum) = Self::limit_new(plan, Some(store))?; - let pooled_memory = StaticMemory::new(base, minimum, maximum, memory_image)?; + let pooled_memory = + StaticMemory::new(base, minimum, maximum, memory_image, memory_and_guard_size)?; let allocation = Box::new(pooled_memory); let allocation: Box = if plan.memory.shared { // FIXME: since the pooling allocator owns the memory allocation @@ -874,6 +905,13 @@ impl Memory { } } } + + /// Returns the range of bytes that WebAssembly should be able to address in + /// this linear memory. Note that this includes guard pages which wasm can + /// hit. + pub fn wasm_accessible(&self) -> Range { + self.0.wasm_accessible() + } } /// In the configurations where bounds checks were elided in JIT code (because diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index 215bfddc06..618b6de504 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -147,9 +147,25 @@ pub enum TrapReason { needs_backtrace: bool, }, - /// A trap raised from Cranelift-generated code with the pc listed of where - /// the trap came from. - Jit(usize), + /// A trap raised from Cranelift-generated code. + Jit { + /// The program counter where this trap originated. + /// + /// This is later used with side tables from compilation to translate + /// the trapping address to a trap code. + pc: usize, + + /// If the trap was a memory-related trap such as SIGSEGV then this + /// field will contain the address of the inaccessible data. + /// + /// Note that wasm loads/stores are not guaranteed to fill in this + /// information. Dynamically-bounds-checked memories, for example, will + /// not access an invalid address but may instead load from NULL or may + /// explicitly jump to a `ud2` instruction. This is only available for + /// fault-based traps which are one of the main ways, but not the only + /// way, to run wasm. + faulting_addr: Option, + }, /// A trap raised from a wasm libcall Wasm(wasmtime_environ::Trap), @@ -174,7 +190,7 @@ impl TrapReason { /// Is this a JIT trap? pub fn is_jit(&self) -> bool { - matches!(self, TrapReason::Jit(_)) + matches!(self, TrapReason::Jit { .. }) } } @@ -470,12 +486,16 @@ impl CallThreadState { self.jmp_buf.replace(ptr::null()) } - fn set_jit_trap(&self, pc: *const u8, fp: usize) { + fn set_jit_trap(&self, pc: *const u8, fp: usize, faulting_addr: Option) { let backtrace = self.capture_backtrace(Some((pc as usize, fp))); unsafe { - (*self.unwind.get()) - .as_mut_ptr() - .write((UnwindReason::Trap(TrapReason::Jit(pc as usize)), backtrace)); + (*self.unwind.get()).as_mut_ptr().write(( + UnwindReason::Trap(TrapReason::Jit { + pc: pc as usize, + faulting_addr, + }), + backtrace, + )); } } diff --git a/crates/runtime/src/traphandlers/macos.rs b/crates/runtime/src/traphandlers/macos.rs index da2cdac91a..092be819ec 100644 --- a/crates/runtime/src/traphandlers/macos.rs +++ b/crates/runtime/src/traphandlers/macos.rs @@ -75,7 +75,18 @@ mod mach_addons { pub static NDR_record: NDR_record_t; } - #[repr(C)] + // Note that this is copied from Gecko at + // + // https://searchfox.org/mozilla-central/rev/ed93119be4818da1509bbcb7b28e245853eeedd5/js/src/wasm/WasmSignalHandlers.cpp#583-601 + // + // which distinctly diverges from the actual version of this in the header + // files provided by macOS, notably in the `code` field which uses `i64` + // instead of `i32`. + // + // Also note the `packed(4)` here which forcibly decreases alignment to 4 to + // additionally match what mach expects (apparently, I wish I had a better + // reference for this). + #[repr(C, packed(4))] #[allow(dead_code)] #[derive(Copy, Clone, Debug)] pub struct __Request__exception_raise_t { @@ -88,6 +99,11 @@ mod mach_addons { pub NDR: NDR_record_t, pub exception: exception_type_t, pub codeCnt: mach_msg_type_number_t, + + // Note that this is a divergence from the C headers which use + // `integer_t` here for this field which is a `c_int`. That isn't + // actually reflecting reality apparently though because if `c_int` is + // used here then the structure is too small to receive a message. pub code: [i64; 2], } @@ -172,6 +188,7 @@ pub unsafe fn platform_init() { // This is largely just copied from SpiderMonkey. #[repr(C)] #[allow(dead_code)] +#[derive(Debug)] struct ExceptionRequest { body: __Request__exception_raise_t, trailer: mach_msg_trailer_t, @@ -248,6 +265,16 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool { _ => return false, } + // For `EXC_BAD_ACCESS` the faulting address is listed as the "subcode" in + // the second `code` field. If we're ever interested in it the first code + // field has a `kern_return_t` describing the kind of failure (e.g. SIGSEGV + // vs SIGBUS), but we're not interested in that right now. + let (fault1, fault2) = if request.body.exception as u32 == EXC_BAD_ACCESS { + (1, request.body.code[1] as usize) + } else { + (0, 0) + }; + // Depending on the current architecture various bits and pieces of this // will change. This is expected to get filled out for other macos // platforms as necessary. @@ -279,7 +306,7 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool { state.__rbp as usize, ); - let resume = |state: &mut ThreadState, pc: usize, fp: usize| { + let resume = |state: &mut ThreadState, pc: usize, fp: usize, fault1: usize, fault2: usize| { // The x86_64 ABI requires a 16-byte stack alignment for // functions, so typically we'll be 16-byte aligned. In this // case we simulate a `call` instruction by decrementing the @@ -306,6 +333,8 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool { state.__rip = unwind as u64; state.__rdi = pc as u64; state.__rsi = fp as u64; + state.__rdx = fault1 as u64; + state.__rcx = fault2 as u64; }; let mut thread_state = ThreadState::new(); } else if #[cfg(target_arch = "aarch64")] { @@ -318,7 +347,7 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool { state.__fp as usize, ); - let resume = |state: &mut ThreadState, pc: usize, fp: usize| { + let resume = |state: &mut ThreadState, pc: usize, fp: usize, fault1: usize, fault2: usize| { // Clobber LR with the faulting PC, so unwinding resumes at the // faulting instruction. The previous value of LR has been saved // by the callee (in Cranelift generated code), so no need to @@ -329,6 +358,8 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool { // it looks like a call to unwind. state.__x[0] = pc as u64; state.__x[1] = fp as u64; + state.__x[2] = fault1 as u64; + state.__x[3] = fault2 as u64; state.__pc = unwind as u64; }; let mut thread_state = mem::zeroed::(); @@ -373,7 +404,7 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool { // force the thread itself to trap. The thread's register state is // configured to resume in the `unwind` function below, we update the // thread's register state, and then we're off to the races. - resume(&mut thread_state, pc as usize, fp); + resume(&mut thread_state, pc as usize, fp, fault1, fault2); let kret = thread_set_state( origin_thread, thread_state_flavor, @@ -390,10 +421,20 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool { /// a native backtrace once we've switched back to the thread itself. After /// the backtrace is captured we can do the usual `longjmp` back to the source /// of the wasm code. -unsafe extern "C" fn unwind(wasm_pc: *const u8, wasm_fp: usize) -> ! { +unsafe extern "C" fn unwind( + wasm_pc: *const u8, + wasm_fp: usize, + has_faulting_addr: usize, + faulting_addr: usize, +) -> ! { let jmp_buf = tls::with(|state| { let state = state.unwrap(); - state.set_jit_trap(wasm_pc, wasm_fp); + let faulting_addr = if has_faulting_addr != 0 { + Some(faulting_addr) + } else { + None + }; + state.set_jit_trap(wasm_pc, wasm_fp, faulting_addr); state.jmp_buf.get() }); debug_assert!(!jmp_buf.is_null()); diff --git a/crates/runtime/src/traphandlers/unix.rs b/crates/runtime/src/traphandlers/unix.rs index 889867ba1b..1837733706 100644 --- a/crates/runtime/src/traphandlers/unix.rs +++ b/crates/runtime/src/traphandlers/unix.rs @@ -101,7 +101,11 @@ unsafe extern "C" fn trap_handler( if jmp_buf as usize == 1 { return true; } - info.set_jit_trap(pc, fp); + let faulting_addr = match signum { + libc::SIGSEGV | libc::SIGBUS => Some((*siginfo).si_addr() as usize), + _ => None, + }; + info.set_jit_trap(pc, fp, faulting_addr); // On macOS this is a bit special, unfortunately. If we were to // `siglongjmp` out of the signal handler that notably does // *not* reset the sigaltstack state of our signal handler. This diff --git a/crates/runtime/src/traphandlers/windows.rs b/crates/runtime/src/traphandlers/windows.rs index a9c1b4f044..5ad7295f5e 100644 --- a/crates/runtime/src/traphandlers/windows.rs +++ b/crates/runtime/src/traphandlers/windows.rs @@ -62,13 +62,23 @@ unsafe extern "system" fn exception_handler(exception_info: *mut EXCEPTION_POINT compile_error!("unsupported platform"); } } + // For access violations the first element in `ExceptionInformation` is + // an indicator as to whether the fault was a read/write. The second + // element is the address of the inaccessible data causing this + // violation. + let faulting_addr = if record.ExceptionCode == EXCEPTION_ACCESS_VIOLATION { + assert!(record.NumberParameters >= 2); + Some(record.ExceptionInformation[1]) + } else { + None + }; let jmp_buf = info.take_jmp_buf_if_trap(ip, |handler| handler(exception_info)); if jmp_buf.is_null() { ExceptionContinueSearch } else if jmp_buf as usize == 1 { ExceptionContinueExecution } else { - info.set_jit_trap(ip, fp); + info.set_jit_trap(ip, fp, faulting_addr); wasmtime_longjmp(jmp_buf) } }) diff --git a/crates/wasmtime/src/memory.rs b/crates/wasmtime/src/memory.rs index 7dbf8d2f37..c3d17784fc 100644 --- a/crates/wasmtime/src/memory.rs +++ b/crates/wasmtime/src/memory.rs @@ -5,6 +5,7 @@ use crate::{AsContext, AsContextMut, Engine, MemoryType, StoreContext, StoreCont use anyhow::{bail, Result}; use std::cell::UnsafeCell; use std::convert::TryFrom; +use std::ops::Range; use std::slice; use std::time::Instant; use wasmtime_environ::MemoryPlan; @@ -611,6 +612,10 @@ pub unsafe trait LinearMemory: Send + Sync + 'static { /// Return the allocated memory as a mutable pointer to u8. fn as_ptr(&self) -> *mut u8; + + /// Returns the range of native addresses that WebAssembly can natively + /// access from this linear memory, including guard pages. + fn wasm_accessible(&self) -> Range; } /// A memory creator. Can be used to provide a memory creator diff --git a/crates/wasmtime/src/store.rs b/crates/wasmtime/src/store.rs index 866aa39f15..9a322fd6a5 100644 --- a/crates/wasmtime/src/store.rs +++ b/crates/wasmtime/src/store.rs @@ -97,7 +97,7 @@ use wasmtime_runtime::{ InstanceAllocationRequest, InstanceAllocator, InstanceHandle, ModuleInfo, OnDemandInstanceAllocator, SignalHandler, StorePtr, VMCallerCheckedFuncRef, VMContext, VMExternRef, VMExternRefActivationsTable, VMRuntimeLimits, VMSharedSignatureIndex, - VMTrampoline, + VMTrampoline, WasmFault, }; mod context; @@ -1501,6 +1501,68 @@ impl StoreOpaque { pub(crate) fn push_rooted_funcs(&mut self, funcs: Arc<[Definition]>) { self.rooted_host_funcs.push(funcs); } + + /// Translates a WebAssembly fault at the native `pc` and native `addr` to a + /// WebAssembly-relative fault. + /// + /// This function may abort the process if `addr` is not found to actually + /// reside in any linear memory. In such a situation it means that the + /// segfault was erroneously caught by Wasmtime and is possibly indicative + /// of a code generator bug. + /// + /// This function returns `None` for dynamically-bounds-checked-memories + /// with spectre mitigations enabled since the hardware fault address is + /// always zero in these situations which means that the trapping context + /// doesn't have enough information to report the fault address. + pub(crate) fn wasm_fault(&self, pc: usize, addr: usize) -> Option { + // Explicitly bounds-checked memories with spectre-guards enabled will + // cause out-of-bounds accesses to get routed to address 0, so allow + // wasm instructions to fault on the null address. + if addr == 0 { + return None; + } + + // Search all known instances in this store for this address. Note that + // this is probably not the speediest way to do this. Traps, however, + // are generally not expected to be super fast and additionally stores + // probably don't have all that many instances or memories. + // + // If this loop becomes hot in the future, however, it should be + // possible to precompute maps about linear memories in a store and have + // a quicker lookup. + let mut fault = None; + for instance in self.instances.iter() { + if let Some(f) = instance.handle.wasm_fault(addr) { + assert!(fault.is_none()); + fault = Some(f); + } + } + if fault.is_some() { + return fault; + } + + eprintln!( + "\ +Wasmtime caught a segfault for a wasm program because the faulting instruction +is allowed to segfault due to how linear memories are implemented. The address +that was accessed, however, is not known to any linear memory in use within this +Store. This may be indicative of a critical bug in Wasmtime's code generation +because all addresses which are known to be reachable from wasm won't reach this +message. + + pc: 0x{pc:x} + address: 0x{addr:x} + +This is a possible security issue because WebAssembly has accessed something it +shouldn't have been able to. Other accesses may have succeeded and this one just +happened to be caught. The process will now be aborted to prevent this damage +from going any further and to alert what's going on. If this is a security +issue please reach out to the Wasmtime team via its security policy +at https://bytecodealliance.org/security. +" + ); + std::process::abort(); + } } impl StoreContextMut<'_, T> { diff --git a/crates/wasmtime/src/trampoline/memory.rs b/crates/wasmtime/src/trampoline/memory.rs index b0de46a90d..e10e26bccf 100644 --- a/crates/wasmtime/src/trampoline/memory.rs +++ b/crates/wasmtime/src/trampoline/memory.rs @@ -4,6 +4,7 @@ use crate::store::{InstanceId, StoreOpaque}; use crate::MemoryType; use anyhow::{anyhow, Result}; use std::convert::TryFrom; +use std::ops::Range; use std::sync::Arc; use wasmtime_environ::{ DefinedMemoryIndex, DefinedTableIndex, EntityIndex, MemoryPlan, MemoryStyle, Module, @@ -99,6 +100,10 @@ impl RuntimeLinearMemory for LinearMemoryProxy { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn wasm_accessible(&self) -> Range { + self.mem.wasm_accessible() + } } #[derive(Clone)] diff --git a/crates/wasmtime/src/trap.rs b/crates/wasmtime/src/trap.rs index e1e3fdd9ad..80d367d8a6 100644 --- a/crates/wasmtime/src/trap.rs +++ b/crates/wasmtime/src/trap.rs @@ -103,12 +103,21 @@ pub(crate) fn from_runtime_box( ); (error, None) } - wasmtime_runtime::TrapReason::Jit(pc) => { + wasmtime_runtime::TrapReason::Jit { pc, faulting_addr } => { let code = store .modules() .lookup_trap_code(pc) .unwrap_or(Trap::StackOverflow); - (code.into(), Some(pc)) + let mut err: Error = code.into(); + + // If a fault address was present, for example with segfaults, + // then simultaneously assert that it's within a known linear memory + // and additionally translate it to a wasm-local address to be added + // as context to the error. + if let Some(fault) = faulting_addr.and_then(|addr| store.wasm_fault(pc, addr)) { + err = err.context(fault); + } + (err, Some(pc)) } wasmtime_runtime::TrapReason::Wasm(trap_code) => (trap_code.into(), None), }; diff --git a/tests/all/memory_creator.rs b/tests/all/memory_creator.rs index 01cc0fabfb..26950ca616 100644 --- a/tests/all/memory_creator.rs +++ b/tests/all/memory_creator.rs @@ -6,6 +6,7 @@ mod not_for_windows { use rustix::mm::{mmap_anonymous, mprotect, munmap, MapFlags, MprotectFlags, ProtFlags}; use std::convert::TryFrom; + use std::ops::Range; use std::ptr::null_mut; use std::sync::{Arc, Mutex}; @@ -74,6 +75,12 @@ mod not_for_windows { fn as_ptr(&self) -> *mut u8 { self.mem as *mut u8 } + + fn wasm_accessible(&self) -> Range { + let base = self.mem as usize; + let end = base + self.size; + base..end + } } struct CustomMemoryCreator { diff --git a/tests/all/traps.rs b/tests/all/traps.rs index ccceb71163..e6785979af 100644 --- a/tests/all/traps.rs +++ b/tests/all/traps.rs @@ -1279,3 +1279,44 @@ fn div_plus_load_reported_right() -> Result<()> { } } } + +#[test] +fn wasm_fault_address_reported_by_default() -> Result<()> { + let engine = Engine::default(); + let mut store = Store::new(&engine, ()); + let module = Module::new( + &engine, + r#" + (module + (memory 1) + (func $start + i32.const 0xdeadbeef + i32.load + drop) + (start $start) + ) + "#, + )?; + let err = Instance::new(&mut store, &module, &[]).unwrap_err(); + + // On s390x faulting addressess are rounded to the nearest page boundary + // instead of having the precise address reported. + let mut expected_addr = 0xdeadbeef_u32; + if cfg!(target_arch = "s390x") { + expected_addr &= 0xfffff000; + } + + // NB: at this time there's no programmatic access to the fault address + // because it's not always available for load/store traps. Only static + // memories on 32-bit have this information, but bounds-checked memories + // use manual trapping instructions and otherwise don't have a means of + // communicating the faulting address at this time. + let err = format!("{err:?}"); + assert!( + err.contains(&format!( + "memory fault at wasm address 0x{expected_addr:x} in linear memory of size 0x10000" + )), + "bad error: {err}" + ); + Ok(()) +}