Repository: bytecodealliance/wasmtime
Pull request #6032 (2023-03-16 20:00:12)

cranelift: simplify `x-x` to `0`

Kmeakin: > Thanks!
> 
> Out of curiosity, is this something you've seen in the wild, or is it just for completeness?

Just for completeness
alexcrichton: Apologies for the merge queue frobbing here GitHub has been acting up today and I'm not really sure what's going on...

Patch:
diff --git a/cranelift/codegen/src/opts/algebraic.isle b/cranelift/codegen/src/opts/algebraic.isle
index ce04c2716..3efa05dfe 100644
--- a/cranelift/codegen/src/opts/algebraic.isle
+++ b/cranelift/codegen/src/opts/algebraic.isle
@@ -35,6 +35,9 @@
 ;; ineg(ineg(x)) == x.
 (rule (simplify (ineg ty (ineg ty x))) (subsume x))
 
+;; x-x == 0.
+(rule (simplify (isub (fits_in_64 (ty_int ty)) x x)) (subsume (iconst ty (imm64 0))))
+
 ;; x*1 == 1*x == x.
 (rule (simplify (imul ty
                       x
diff --git a/crates/fuzzing/src/generators/memory.rs b/crates/fuzzing/src/generators/memory.rs
index 57ad4dd41..bdf679b58 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<usize> {
+        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 eabe27794..c3c6d6378 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<WasmFault> {
+        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<WasmFault> {
+        self.instance().wasm_fault(addr)
+    }
 }
diff --git a/crates/runtime/src/instance/allocator/pooling.rs b/crates/runtime/src/instance/allocator/pooling.rs
index 05fe1a352..c89d74906 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 31b013786..3428a0ffb 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 3cbcd125d..e3f3d2c56 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<usize>;
 }
 
 /// 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<usize> {
+        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<usize>,
         memory_image: MemoryImageSlot,
+        memory_and_guard_size: usize,
     ) -> Result<Self> {
         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<usize> {
+        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<usize> {
+        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<Self> {
         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<dyn RuntimeLinearMemory> = 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<usize> {
+        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 215bfddc0..618b6de50 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<usize>,
+    },
 
     /// 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<usize>) {
         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 da2cdac91..092be819e 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::<ThreadState>();
@@ -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 889867ba1..183773370 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 a9c1b4f04..5ad7295f5 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 7dbf8d2f3..c3d17784f 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<usize>;
 }
 
 /// 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 866aa39f1..9a322fd6a 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<WasmFault> {
+        // 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<T> StoreContextMut<'_, T> {
diff --git a/crates/wasmtime/src/trampoline/memory.rs b/crates/wasmtime/src/trampoline/memory.rs
index b0de46a90..e10e26bcc 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<usize> {
+        self.mem.wasm_accessible()
+    }
 }
 
 #[derive(Clone)]
diff --git a/crates/wasmtime/src/trap.rs b/crates/wasmtime/src/trap.rs
index e1e3fdd9a..80d367d8a 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),
     };


Test patch:
diff --git a/cranelift/filetests/filetests/egraph/algebraic.clif b/cranelift/filetests/filetests/egraph/algebraic.clif
index 02d38cdd9..faae822fe 100644
--- a/cranelift/filetests/filetests/egraph/algebraic.clif
+++ b/cranelift/filetests/filetests/egraph/algebraic.clif
@@ -232,6 +232,14 @@ block0(v0: i32):
     ; check: return v0
 }
 
+function %isub_self(i32) -> i32 {
+block0(v0: i32):
+    v1 = isub v0, v0
+    return v1
+    ; check: v2 = iconst.i32 0
+    ; check: return v2
+}
+
 function %or_and_y_with_not_y_i8(i8, i8) -> i8 {
 block0(v0: i8, v1: i8):
     v2 = band v0, v1
diff --git a/tests/all/memory_creator.rs b/tests/all/memory_creator.rs
index 01cc0fabf..26950ca61 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<usize> {
+            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 ccceb7116..e6785979a 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(())
+}