Repository: bytecodealliance/wasmtime
Pull request #9243 (2024-09-13 16:46:00)

Remove some job dependencies in CI
All of the `cargo check`-style jobs are always run on both PRs and merge so there's no need to gate on the `determine` step. That's only needed when the output of `determine` conditionally decides to run a job or not, but these are always unconditionally run. This will help a tiny bit with CI time where the `determine` job doesn't need to run, but otherwise cuts down a bit on the amount of configuration.

<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->



Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index eb7dc5e51..e1b9b0466 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -338,7 +338,6 @@ jobs:
   # here isn't to run tests, mostly just serve as a double-check that Rust code
   # compiles and is likely to work everywhere else.
   micro_checks:
-    needs: determine
     name: Check ${{matrix.name}}
     strategy:
       fail-fast: true
@@ -421,7 +420,6 @@ jobs:
 
   # Checks for no_std support, ensure that crates can build on a no_std target
   no_std_checks:
-    needs: determine
     name: no_std checks
     runs-on: ubuntu-latest
     env:
@@ -445,7 +443,6 @@ jobs:
 
   # Check that Clippy lints are passing.
   clippy:
-    needs: determine
     name: Clippy
     runs-on: ubuntu-latest
     env:
@@ -469,7 +466,6 @@ jobs:
   # (e.g. Android NDK) and we haven't factored support for those things out into
   # a parallel jobs yet.
   monolith_checks:
-    needs: determine
     name: Monolith Checks
     runs-on: ubuntu-latest
     env:
diff --git a/cranelift/wasm/src/code_translator.rs b/cranelift/wasm/src/code_translator.rs
index 5506c6672..333b5d692 100644
--- a/cranelift/wasm/src/code_translator.rs
+++ b/cranelift/wasm/src/code_translator.rs
@@ -245,7 +245,7 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
             // We do nothing
         }
         Operator::Unreachable => {
-            builder.ins().trap(ir::TrapCode::UnreachableCodeReached);
+            environ.trap(builder, ir::TrapCode::UnreachableCodeReached);
             state.reachable = false;
         }
         /***************************** Control flow blocks **********************************
@@ -1016,19 +1016,19 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
         }
         Operator::I64TruncF64S | Operator::I64TruncF32S => {
             let val = state.pop1();
-            state.push1(builder.ins().fcvt_to_sint(I64, val));
+            state.push1(environ.translate_fcvt_to_sint(builder, I64, val));
         }
         Operator::I32TruncF64S | Operator::I32TruncF32S => {
             let val = state.pop1();
-            state.push1(builder.ins().fcvt_to_sint(I32, val));
+            state.push1(environ.translate_fcvt_to_sint(builder, I32, val));
         }
         Operator::I64TruncF64U | Operator::I64TruncF32U => {
             let val = state.pop1();
-            state.push1(builder.ins().fcvt_to_uint(I64, val));
+            state.push1(environ.translate_fcvt_to_uint(builder, I64, val));
         }
         Operator::I32TruncF64U | Operator::I32TruncF32U => {
             let val = state.pop1();
-            state.push1(builder.ins().fcvt_to_uint(I32, val));
+            state.push1(environ.translate_fcvt_to_uint(builder, I32, val));
         }
         Operator::I64TruncSatF64S | Operator::I64TruncSatF32S => {
             let val = state.pop1();
@@ -1155,19 +1155,19 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
         }
         Operator::I32DivS | Operator::I64DivS => {
             let (arg1, arg2) = state.pop2();
-            state.push1(builder.ins().sdiv(arg1, arg2));
+            state.push1(environ.translate_sdiv(builder, arg1, arg2));
         }
         Operator::I32DivU | Operator::I64DivU => {
             let (arg1, arg2) = state.pop2();
-            state.push1(builder.ins().udiv(arg1, arg2));
+            state.push1(environ.translate_udiv(builder, arg1, arg2));
         }
         Operator::I32RemS | Operator::I64RemS => {
             let (arg1, arg2) = state.pop2();
-            state.push1(builder.ins().srem(arg1, arg2));
+            state.push1(environ.translate_srem(builder, arg1, arg2));
         }
         Operator::I32RemU | Operator::I64RemU => {
             let (arg1, arg2) = state.pop2();
-            state.push1(builder.ins().urem(arg1, arg2));
+            state.push1(environ.translate_urem(builder, arg1, arg2));
         }
         Operator::F32Min | Operator::F64Min => {
             let (arg1, arg2) = state.pop2();
@@ -1255,9 +1255,7 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
             } else {
                 let index_type = environ.heaps()[heap].index_type;
                 let offset = builder.ins().iconst(index_type, memarg.offset as i64);
-                builder
-                    .ins()
-                    .uadd_overflow_trap(addr, offset, ir::TrapCode::HeapOutOfBounds)
+                environ.uadd_overflow_trap(builder, addr, offset, ir::TrapCode::HeapOutOfBounds)
             };
             // `fn translate_atomic_wait` can inspect the type of `expected` to figure out what
             // code it needs to generate, if it wants.
@@ -1281,9 +1279,7 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
             } else {
                 let index_type = environ.heaps()[heap].index_type;
                 let offset = builder.ins().iconst(index_type, memarg.offset as i64);
-                builder
-                    .ins()
-                    .uadd_overflow_trap(addr, offset, ir::TrapCode::HeapOutOfBounds)
+                environ.uadd_overflow_trap(builder, addr, offset, ir::TrapCode::HeapOutOfBounds)
             };
             let res = environ.translate_atomic_notify(
                 builder.cursor(),
@@ -2485,7 +2481,7 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
         Operator::RefAsNonNull => {
             let r = state.pop1();
             let is_null = environ.translate_ref_is_null(builder.cursor(), r)?;
-            builder.ins().trapnz(is_null, ir::TrapCode::NullReference);
+            environ.trapnz(builder, is_null, ir::TrapCode::NullReference);
             state.push1(r);
         }
 
@@ -2496,12 +2492,12 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
         }
         Operator::I31GetS => {
             let i31ref = state.pop1();
-            let val = environ.translate_i31_get_s(builder.cursor(), i31ref)?;
+            let val = environ.translate_i31_get_s(builder, i31ref)?;
             state.push1(val);
         }
         Operator::I31GetU => {
             let i31ref = state.pop1();
-            let val = environ.translate_i31_get_u(builder.cursor(), i31ref)?;
+            let val = environ.translate_i31_get_u(builder, i31ref)?;
             state.push1(val);
         }
 
@@ -2863,9 +2859,7 @@ where
         Err(_) => {
             let offset = builder.ins().iconst(heap.index_type, memarg.offset as i64);
             let adjusted_index =
-                builder
-                    .ins()
-                    .uadd_overflow_trap(index, offset, ir::TrapCode::HeapOutOfBounds);
+                environ.uadd_overflow_trap(builder, index, offset, ir::TrapCode::HeapOutOfBounds);
             bounds_checks::bounds_check_and_compute_addr(
                 builder,
                 environ,
@@ -2902,11 +2896,12 @@ where
     Ok(Reachability::Reachable((flags, index, addr)))
 }
 
-fn align_atomic_addr(
+fn align_atomic_addr<FE: FuncEnvironment + ?Sized>(
     memarg: &MemArg,
     loaded_bytes: u8,
     builder: &mut FunctionBuilder,
     state: &mut FuncTranslationState,
+    environ: &mut FE,
 ) {
     // Atomic addresses must all be aligned correctly, and for now we check
     // alignment before we check out-of-bounds-ness. The order of this check may
@@ -2933,7 +2928,7 @@ fn align_atomic_addr(
             .ins()
             .band_imm(effective_addr, i64::from(loaded_bytes - 1));
         let f = builder.ins().icmp_imm(IntCC::NotEqual, misalignment, 0);
-        builder.ins().trapnz(f, ir::TrapCode::HeapMisaligned);
+        environ.trapnz(builder, f, ir::TrapCode::HeapMisaligned);
     }
 }
 
@@ -2947,7 +2942,7 @@ fn prepare_atomic_addr<FE: FuncEnvironment + ?Sized>(
     state: &mut FuncTranslationState,
     environ: &mut FE,
 ) -> WasmResult<Reachability<(MemFlags, Value, Value)>> {
-    align_atomic_addr(memarg, loaded_bytes, builder, state);
+    align_atomic_addr(memarg, loaded_bytes, builder, state, environ);
     prepare_addr(memarg, loaded_bytes, builder, state, environ)
 }
 
diff --git a/cranelift/wasm/src/code_translator/bounds_checks.rs b/cranelift/wasm/src/code_translator/bounds_checks.rs
index c66eb8001..cd1ca3282 100644
--- a/cranelift/wasm/src/code_translator/bounds_checks.rs
+++ b/cranelift/wasm/src/code_translator/bounds_checks.rs
@@ -63,7 +63,8 @@ where
     let pcc = env.proof_carrying_code();
 
     let host_page_size_log2 = env.target_config().page_size_align_log2;
-    let can_use_virtual_memory = heap.page_size_log2 >= host_page_size_log2;
+    let can_use_virtual_memory =
+        heap.page_size_log2 >= host_page_size_log2 && env.signals_based_traps();
 
     let make_compare = |builder: &mut FunctionBuilder,
                         compare_kind: IntCC,
@@ -154,9 +155,9 @@ where
                 Some(0),
             );
             Reachable(explicit_check_oob_condition_and_compute_addr(
-                &mut builder.cursor(),
+                env,
+                builder,
                 heap,
-                env.pointer_type(),
                 index,
                 offset,
                 access_size,
@@ -204,9 +205,9 @@ where
                 Some(0),
             );
             Reachable(explicit_check_oob_condition_and_compute_addr(
-                &mut builder.cursor(),
+                env,
+                builder,
                 heap,
-                env.pointer_type(),
                 index,
                 offset,
                 access_size,
@@ -248,9 +249,9 @@ where
                 Some(adjustment),
             );
             Reachable(explicit_check_oob_condition_and_compute_addr(
-                &mut builder.cursor(),
+                env,
+                builder,
                 heap,
-                env.pointer_type(),
                 index,
                 offset,
                 access_size,
@@ -275,7 +276,8 @@ where
                 builder.func.dfg.facts[access_size_val] =
                     Some(Fact::constant(pointer_bit_width, offset_and_size));
             }
-            let adjusted_index = builder.ins().uadd_overflow_trap(
+            let adjusted_index = env.uadd_overflow_trap(
+                builder,
                 index,
                 access_size_val,
                 ir::TrapCode::HeapOutOfBounds,
@@ -297,9 +299,9 @@ where
                 Some(0),
             );
             Reachable(explicit_check_oob_condition_and_compute_addr(
-                &mut builder.cursor(),
+                env,
+                builder,
                 heap,
-                env.pointer_type(),
                 index,
                 offset,
                 access_size,
@@ -323,7 +325,7 @@ where
                 "static memories require the ability to use virtual memory"
             );
             env.before_unconditionally_trapping_memory_access(builder)?;
-            builder.ins().trap(ir::TrapCode::HeapOutOfBounds);
+            env.trap(builder, ir::TrapCode::HeapOutOfBounds);
             Unreachable
         }
 
@@ -423,9 +425,9 @@ where
                 Some(0),
             );
             Reachable(explicit_check_oob_condition_and_compute_addr(
-                &mut builder.cursor(),
+                env,
+                builder,
                 heap,
-                env.pointer_type(),
                 index,
                 offset,
                 access_size,
@@ -550,10 +552,10 @@ impl AddrPcc {
 ///
 /// This function deduplicates explicit bounds checks and Spectre mitigations
 /// that inherently also implement bounds checking.
-fn explicit_check_oob_condition_and_compute_addr(
-    pos: &mut FuncCursor,
+fn explicit_check_oob_condition_and_compute_addr<FE: FuncEnvironment + ?Sized>(
+    env: &mut FE,
+    builder: &mut FunctionBuilder,
     heap: &HeapData,
-    addr_ty: ir::Type,
     index: ir::Value,
     offset: u32,
     access_size: u8,
@@ -567,22 +569,27 @@ fn explicit_check_oob_condition_and_compute_addr(
     oob_condition: ir::Value,
 ) -> ir::Value {
     if !spectre_mitigations_enabled {
-        pos.ins()
-            .trapnz(oob_condition, ir::TrapCode::HeapOutOfBounds);
+        env.trapnz(builder, oob_condition, ir::TrapCode::HeapOutOfBounds);
     }
+    let addr_ty = env.pointer_type();
 
-    let mut addr = compute_addr(pos, heap, addr_ty, index, offset, pcc);
+    let mut addr = compute_addr(&mut builder.cursor(), heap, addr_ty, index, offset, pcc);
 
     if spectre_mitigations_enabled {
-        let null = pos.ins().iconst(addr_ty, 0);
-        addr = pos.ins().select_spectre_guard(oob_condition, null, addr);
+        // These mitigations rely on trapping when loading from NULL so
+        // signals-based traps must be allowed for this to be generated.
+        assert!(env.signals_based_traps());
+        let null = builder.ins().iconst(addr_ty, 0);
+        addr = builder
+            .ins()
+            .select_spectre_guard(oob_condition, null, addr);
 
         match pcc {
             None => {}
             Some(AddrPcc::Static32(ty, size)) => {
-                pos.func.dfg.facts[null] =
+                builder.func.dfg.facts[null] =
                     Some(Fact::constant(u16::try_from(addr_ty.bits()).unwrap(), 0));
-                pos.func.dfg.facts[addr] = Some(Fact::Mem {
+                builder.func.dfg.facts[addr] = Some(Fact::Mem {
                     ty,
                     min_offset: 0,
                     max_offset: size.checked_sub(u64::from(access_size)).unwrap(),
@@ -590,9 +597,9 @@ fn explicit_check_oob_condition_and_compute_addr(
                 });
             }
             Some(AddrPcc::Dynamic(ty, gv)) => {
-                pos.func.dfg.facts[null] =
+                builder.func.dfg.facts[null] =
                     Some(Fact::constant(u16::try_from(addr_ty.bits()).unwrap(), 0));
-                pos.func.dfg.facts[addr] = Some(Fact::DynamicMem {
+                builder.func.dfg.facts[addr] = Some(Fact::DynamicMem {
                     ty,
                     min: Expr::constant(0),
                     max: Expr::offset(
diff --git a/cranelift/wasm/src/environ/spec.rs b/cranelift/wasm/src/environ/spec.rs
index a673894e1..7a561953a 100644
--- a/cranelift/wasm/src/environ/spec.rs
+++ b/cranelift/wasm/src/environ/spec.rs
@@ -15,7 +15,7 @@ use crate::{
 use cranelift_codegen::cursor::FuncCursor;
 use cranelift_codegen::ir::immediates::Offset32;
 use cranelift_codegen::ir::{self, InstBuilder, Type};
-use cranelift_codegen::isa::TargetFrontendConfig;
+use cranelift_codegen::isa::{TargetFrontendConfig, TargetIsa};
 use cranelift_entity::PrimaryMap;
 use cranelift_frontend::FunctionBuilder;
 use std::boxed::Box;
@@ -503,10 +503,18 @@ pub trait FuncEnvironment: TargetEnvironment {
     fn translate_ref_i31(&mut self, pos: FuncCursor, val: ir::Value) -> WasmResult<ir::Value>;
 
     /// Sign-extend an `i31ref` into an `i32`.
-    fn translate_i31_get_s(&mut self, pos: FuncCursor, i31ref: ir::Value) -> WasmResult<ir::Value>;
+    fn translate_i31_get_s(
+        &mut self,
+        pos: &mut FunctionBuilder,
+        i31ref: ir::Value,
+    ) -> WasmResult<ir::Value>;
 
     /// Zero-extend an `i31ref` into an `i32`.
-    fn translate_i31_get_u(&mut self, pos: FuncCursor, i31ref: ir::Value) -> WasmResult<ir::Value>;
+    fn translate_i31_get_u(
+        &mut self,
+        pos: &mut FunctionBuilder,
+        i31ref: ir::Value,
+    ) -> WasmResult<ir::Value>;
 
     /// Emit code at the beginning of every wasm loop.
     ///
@@ -657,6 +665,101 @@ pub trait FuncEnvironment: TargetEnvironment {
         _mem_index: MemoryIndex,
     ) {
     }
+
+    /// Optional hook for customizing how `trap` is lowered.
+    fn trap(&mut self, builder: &mut FunctionBuilder, code: ir::TrapCode) {
+        builder.ins().trap(code);
+    }
+
+    /// Optional hook for customizing how `trapz` is lowered.
+    fn trapz(&mut self, builder: &mut FunctionBuilder, value: ir::Value, code: ir::TrapCode) {
+        builder.ins().trapz(value, code);
+    }
+
+    /// Optional hook for customizing how `trapnz` is lowered.
+    fn trapnz(&mut self, builder: &mut FunctionBuilder, value: ir::Value, code: ir::TrapCode) {
+        builder.ins().trapnz(value, code);
+    }
+
+    /// Optional hook for customizing how `uadd_overflow_trap` is lowered.
+    fn uadd_overflow_trap(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+        code: ir::TrapCode,
+    ) -> ir::Value {
+        builder.ins().uadd_overflow_trap(lhs, rhs, code)
+    }
+
+    /// Accesses the ISA that is being compiled for.
+    fn isa(&self) -> &dyn TargetIsa;
+
+    /// Embedder-defined hook for indicating whether signals can be used to
+    /// indicate traps.
+    fn signals_based_traps(&self) -> bool {
+        true
+    }
+
+    /// Optional hook for customizing `sdiv` instruction lowering.
+    fn translate_sdiv(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+    ) -> ir::Value {
+        builder.ins().sdiv(lhs, rhs)
+    }
+
+    /// Optional hook for customizing `udiv` instruction lowering.
+    fn translate_udiv(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+    ) -> ir::Value {
+        builder.ins().udiv(lhs, rhs)
+    }
+
+    /// Optional hook for customizing `srem` instruction lowering.
+    fn translate_srem(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+    ) -> ir::Value {
+        builder.ins().srem(lhs, rhs)
+    }
+
+    /// Optional hook for customizing `urem` instruction lowering.
+    fn translate_urem(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+    ) -> ir::Value {
+        builder.ins().urem(lhs, rhs)
+    }
+
+    /// Optional hook for customizing `fcvt_to_sint` instruction lowering.
+    fn translate_fcvt_to_sint(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        ty: ir::Type,
+        val: ir::Value,
+    ) -> ir::Value {
+        builder.ins().fcvt_to_sint(ty, val)
+    }
+
+    /// Optional hook for customizing `fcvt_to_uint` instruction lowering.
+    fn translate_fcvt_to_uint(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        ty: ir::Type,
+        val: ir::Value,
+    ) -> ir::Value {
+        builder.ins().fcvt_to_uint(ty, val)
+    }
 }
 
 /// An object satisfying the `ModuleEnvironment` trait can be passed as argument to the
diff --git a/cranelift/wasm/src/table.rs b/cranelift/wasm/src/table.rs
index 64d92b1d2..7d38924a0 100644
--- a/cranelift/wasm/src/table.rs
+++ b/cranelift/wasm/src/table.rs
@@ -1,3 +1,4 @@
+use crate::FuncEnvironment;
 use cranelift_codegen::cursor::FuncCursor;
 use cranelift_codegen::ir::{self, condcodes::IntCC, immediates::Imm64, InstBuilder};
 use cranelift_codegen::isa::TargetIsa;
@@ -58,23 +59,23 @@ impl TableData {
     /// given index within this table.
     pub fn prepare_table_addr(
         &self,
-        isa: &dyn TargetIsa,
+        env: &mut dyn FuncEnvironment,
         pos: &mut FunctionBuilder,
         mut index: ir::Value,
     ) -> (ir::Value, ir::MemFlags) {
         let index_ty = pos.func.dfg.value_type(index);
-        let addr_ty = isa.pointer_type();
+        let addr_ty = env.pointer_type();
 
         // Start with the bounds check. Trap if `index + 1 > bound`.
-        let bound = self.bound.bound(isa, pos.cursor(), index_ty);
+        let bound = self.bound.bound(env.isa(), pos.cursor(), index_ty);
 
         // `index > bound - 1` is the same as `index >= bound`.
         let oob = pos
             .ins()
             .icmp(IntCC::UnsignedGreaterThanOrEqual, index, bound);
 
-        if !isa.flags().enable_table_access_spectre_mitigation() {
-            pos.ins().trapnz(oob, ir::TrapCode::TableOutOfBounds);
+        if !env.isa().flags().enable_table_access_spectre_mitigation() {
+            env.trapnz(pos, oob, ir::TrapCode::TableOutOfBounds);
         }
 
         // Convert `index` to `addr_ty`.
@@ -100,7 +101,7 @@ impl TableData {
         let base_flags = ir::MemFlags::new()
             .with_aligned()
             .with_alias_region(Some(ir::AliasRegion::Table));
-        if isa.flags().enable_table_access_spectre_mitigation() {
+        if env.isa().flags().enable_table_access_spectre_mitigation() {
             // Short-circuit the computed table element address to a null pointer
             // when out-of-bounds. The consumer of this address will trap when
             // trying to access it.
diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs
index 0f92417cf..ece1e20bc 100644
--- a/crates/cli-flags/src/lib.rs
+++ b/crates/cli-flags/src/lib.rs
@@ -123,6 +123,9 @@ wasmtime_option_group! {
         /// The maximum size, in bytes, allocated for a core instance's metadata
         /// when using the pooling allocator.
         pub pooling_max_core_instance_size: Option<usize>,
+
+        /// Enable or disable the use of host signal handlers for traps.
+        pub signals_based_traps: Option<bool>,
     }
 
     enum Optimize {
@@ -593,6 +596,9 @@ impl CommonOptions {
         if let Some(enable) = self.opts.memory_init_cow {
             config.memory_init_cow(enable);
         }
+        if let Some(enable) = self.opts.signals_based_traps {
+            config.signals_based_traps(enable);
+        }
 
         match_feature! {
             ["pooling-allocator" : self.opts.pooling_allocator.or(pooling_allocator_default)]
diff --git a/crates/cranelift/src/compiler.rs b/crates/cranelift/src/compiler.rs
index a25679b6e..328aa940f 100644
--- a/crates/cranelift/src/compiler.rs
+++ b/crates/cranelift/src/compiler.rs
@@ -15,7 +15,9 @@ use cranelift_codegen::print_errors::pretty_error;
 use cranelift_codegen::{CompiledCode, Context};
 use cranelift_entity::PrimaryMap;
 use cranelift_frontend::FunctionBuilder;
-use cranelift_wasm::{DefinedFuncIndex, FuncTranslator, WasmFuncType, WasmValType};
+use cranelift_wasm::{
+    DefinedFuncIndex, FuncEnvironment as _, FuncTranslator, WasmFuncType, WasmValType,
+};
 use object::write::{Object, StandardSegment, SymbolId};
 use object::{RelocationEncoding, RelocationFlags, RelocationKind, SectionKind};
 use std::any::Any;
@@ -206,7 +208,11 @@ impl wasmtime_environ::Compiler for Compiler {
             global_type: isa.pointer_type(),
             flags: MemFlags::trusted(),
         });
-        context.func.stack_limit = Some(stack_limit);
+        if func_env.signals_based_traps() {
+            context.func.stack_limit = Some(stack_limit);
+        } else {
+            func_env.stack_limit_at_function_entry = Some(stack_limit);
+        }
         let FunctionBodyData { validator, body } = input;
         let mut validator =
             validator.into_validator(mem::take(&mut compiler.cx.validator_allocations));
diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs
index 774eeb43f..7223029e2 100644
--- a/crates/cranelift/src/compiler/component.rs
+++ b/crates/cranelift/src/compiler/component.rs
@@ -8,7 +8,7 @@ use cranelift_frontend::FunctionBuilder;
 use cranelift_wasm::ModuleInternedTypeIndex;
 use std::any::Any;
 use wasmtime_environ::component::*;
-use wasmtime_environ::{PtrSize, WasmValType};
+use wasmtime_environ::{PtrSize, Tunables, WasmValType};
 
 struct TrampolineCompiler<'a> {
     compiler: &'a Compiler,
@@ -20,6 +20,7 @@ struct TrampolineCompiler<'a> {
     abi: Abi,
     block0: ir::Block,
     signature: ModuleInternedTypeIndex,
+    tunables: &'a Tunables,
 }
 
 #[derive(Copy, Clone)]
@@ -36,6 +37,7 @@ impl<'a> TrampolineCompiler<'a> {
         types: &'a ComponentTypesBuilder,
         index: TrampolineIndex,
         abi: Abi,
+        tunables: &'a Tunables,
     ) -> TrampolineCompiler<'a> {
         let isa = &*compiler.isa;
         let signature = component.trampolines[index];
@@ -58,6 +60,7 @@ impl<'a> TrampolineCompiler<'a> {
             abi,
             block0,
             signature,
+            tunables,
         }
     }
 
@@ -91,9 +94,7 @@ impl<'a> TrampolineCompiler<'a> {
                 self.translate_lower_import(*index, options, *lower_ty);
             }
             Trampoline::AlwaysTrap => {
-                self.builder
-                    .ins()
-                    .trap(ir::TrapCode::User(ALWAYS_TRAP_CODE));
+                self.translate_always_trap();
             }
             Trampoline::ResourceNew(ty) => self.translate_resource_new(*ty),
             Trampoline::ResourceRep(ty) => self.translate_resource_rep(*ty),
@@ -261,6 +262,34 @@ impl<'a> TrampolineCompiler<'a> {
         }
     }
 
+    fn translate_always_trap(&mut self) {
+        if self.tunables.signals_based_traps {
+            self.builder
+                .ins()
+                .trap(ir::TrapCode::User(ALWAYS_TRAP_CODE));
+            return;
+        }
+
+        let args = self.abi_load_params();
+        let vmctx = args[0];
+
+        let (host_sig, offset) = host::trap(self.isa, &mut self.builder.func);
+        let host_fn = self.load_libcall(vmctx, offset);
+
+        let code = self.builder.ins().iconst(
+            ir::types::I8,
+            i64::from(wasmtime_environ::Trap::AlwaysTrapAdapter as u8),
+        );
+        self.builder
+            .ins()
+            .call_indirect(host_sig, host_fn, &[vmctx, code]);
+        // debug trap in case execution actually falls through, but this
+        // shouldn't ever get hit at runtime.
+        self.builder
+            .ins()
+            .trap(ir::TrapCode::User(crate::DEBUG_ASSERT_TRAP_CODE));
+    }
+
     fn translate_resource_new(&mut self, resource: TypeResourceTableIndex) {
         let args = self.abi_load_params();
         let vmctx = args[0];
@@ -625,6 +654,7 @@ impl ComponentCompiler for Compiler {
         component: &ComponentTranslation,
         types: &ComponentTypesBuilder,
         index: TrampolineIndex,
+        tunables: &Tunables,
     ) -> Result<AllCallFunc<Box<dyn Any + Send>>> {
         let compile = |abi: Abi| -> Result<_> {
             let mut compiler = self.function_compiler();
@@ -635,6 +665,7 @@ impl ComponentCompiler for Compiler {
                 types,
                 index,
                 abi,
+                tunables,
             );
 
             // If we are crossing the Wasm-to-native boundary, we need to save the
@@ -907,6 +938,7 @@ mod host {
         (@ty $ptr:ident ptr_u8) => ($ptr);
         (@ty $ptr:ident ptr_u16) => ($ptr);
         (@ty $ptr:ident ptr_size) => ($ptr);
+        (@ty $ptr:ident u8) => (ir::types::I8);
         (@ty $ptr:ident u32) => (ir::types::I32);
         (@ty $ptr:ident u64) => (ir::types::I64);
         (@ty $ptr:ident vmctx) => ($ptr);
diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs
index 16b32a7b6..ceb8eddbb 100644
--- a/crates/cranelift/src/func_environ.rs
+++ b/crates/cranelift/src/func_environ.rs
@@ -142,6 +142,15 @@ pub struct FuncEnvironment<'module_environment> {
 
     #[cfg(feature = "wmemcheck")]
     wmemcheck: bool,
+
+    /// A `GlobalValue` in CLIF which represents the stack limit.
+    ///
+    /// Typically this resides in the `stack_limit` value of `ir::Function` but
+    /// that requires signal handlers on the host and when that's disabled this
+    /// is here with an explicit check instead. Note that the explicit check is
+    /// always present even if this is a "leaf" function, as we have to call
+    /// into the host to trap when signal handlers are disabled.
+    pub(crate) stack_limit_at_function_entry: Option<ir::GlobalValue>,
 }
 
 impl<'module_environment> FuncEnvironment<'module_environment> {
@@ -186,6 +195,8 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
             wmemcheck,
             #[cfg(feature = "wmemcheck")]
             translation,
+
+            stack_limit_at_function_entry: None,
         }
     }
 
@@ -852,13 +863,13 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
     ) -> ir::Value {
         let pointer_type = self.pointer_type();
         self.ensure_table_exists(builder.func, table_index);
-        let table_data = self.tables[table_index].as_ref().unwrap();
+        let table_data = self.tables[table_index].clone().unwrap();
 
         // To support lazy initialization of table
         // contents, we check for a null entry here, and
         // if null, we take a slow-path that invokes a
         // libcall.
-        let (table_entry_addr, flags) = table_data.prepare_table_addr(&*self.isa, builder, index);
+        let (table_entry_addr, flags) = table_data.prepare_table_addr(self, builder, index);
         let value = builder.ins().load(pointer_type, flags, table_entry_addr, 0);
 
         if !lazy_init {
@@ -1015,6 +1026,99 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
         });
         (pointee, mt)
     }
+
+    /// Helper to emit a conditional trap based on `trap_cond`.
+    ///
+    /// This should only be used if `self.signals_based_traps()` is false,
+    /// otherwise native CLIF instructions should be used instead.
+    pub fn conditionally_trap(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        trap_cond: ir::Value,
+        trap: ir::TrapCode,
+    ) {
+        assert!(!self.signals_based_traps());
+
+        let trap_block = builder.create_block();
+        builder.set_cold_block(trap_block);
+        let continuation_block = builder.create_block();
+
+        builder
+            .ins()
+            .brif(trap_cond, trap_block, &[], continuation_block, &[]);
+
+        builder.seal_block(trap_block);
+        builder.seal_block(continuation_block);
+
+        builder.switch_to_block(trap_block);
+        self.trap(builder, trap);
+        builder.switch_to_block(continuation_block);
+    }
+
+    /// Helper used when `!self.signals_based_traps()` is enabled to test
+    /// whether the divisor is zero.
+    fn guard_zero_divisor(&mut self, builder: &mut FunctionBuilder, rhs: ir::Value) {
+        if self.signals_based_traps() {
+            return;
+        }
+        self.trapz(builder, rhs, ir::TrapCode::IntegerDivisionByZero);
+    }
+
+    /// Helper used when `!self.signals_based_traps()` is enabled to test
+    /// whether a signed division operation will raise a trap.
+    fn guard_signed_divide(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+    ) {
+        if self.signals_based_traps() {
+            return;
+        }
+        self.trapz(builder, rhs, ir::TrapCode::IntegerDivisionByZero);
+
+        let ty = builder.func.dfg.value_type(rhs);
+        let minus_one = builder.ins().iconst(ty, -1);
+        let rhs_is_minus_one = builder.ins().icmp(IntCC::Equal, rhs, minus_one);
+        let int_min = builder.ins().iconst(
+            ty,
+            match ty {
+                I32 => i64::from(i32::MIN),
+                I64 => i64::MIN,
+                _ => unreachable!(),
+            },
+        );
+        let lhs_is_int_min = builder.ins().icmp(IntCC::Equal, lhs, int_min);
+        let is_integer_overflow = builder.ins().band(rhs_is_minus_one, lhs_is_int_min);
+        self.conditionally_trap(builder, is_integer_overflow, ir::TrapCode::IntegerOverflow);
+    }
+
+    /// Helper used when `!self.signals_based_traps()` is enabled to perform
+    /// trapping float-to-int conversions.
+    fn fcvt_to_int(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        ty: ir::Type,
+        val: ir::Value,
+        i32: fn(&mut Self, &mut Function) -> ir::FuncRef,
+        i64: fn(&mut Self, &mut Function) -> ir::FuncRef,
+    ) -> ir::Value {
+        assert!(!self.signals_based_traps());
+        let val_ty = builder.func.dfg.value_type(val);
+        let val = if val_ty == F64 {
+            val
+        } else {
+            builder.ins().fpromote(F64, val)
+        };
+        let libcall = match ty {
+            I32 => i32(self, &mut builder.func),
+            I64 => i64(self, &mut builder.func),
+            _ => unreachable!(),
+        };
+        let vmctx = self.vmctx_val(&mut builder.cursor());
+        let call = builder.ins().call(libcall, &[vmctx, val]);
+        *builder.func.dfg.inst_results(call).first().unwrap()
+    }
 }
 
 struct Call<'a, 'func, 'module_env> {
@@ -1247,15 +1351,20 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
                 // null pointer, then this was a call to null. Otherwise if it
                 // succeeds then we know it won't match, so trap anyway.
                 if table.table.ref_type.nullable {
-                    let mem_flags = ir::MemFlags::trusted().with_readonly();
-                    self.builder.ins().load(
-                        sig_id_type,
-                        mem_flags.with_trap_code(Some(ir::TrapCode::IndirectCallToNull)),
-                        funcref_ptr,
-                        i32::from(self.env.offsets.ptr.vm_func_ref_type_index()),
-                    );
+                    if self.env.signals_based_traps() {
+                        let mem_flags = ir::MemFlags::trusted().with_readonly();
+                        self.builder.ins().load(
+                            sig_id_type,
+                            mem_flags.with_trap_code(Some(ir::TrapCode::IndirectCallToNull)),
+                            funcref_ptr,
+                            i32::from(self.env.offsets.ptr.vm_func_ref_type_index()),
+                        );
+                    } else {
+                        self.env
+                            .trapz(self.builder, funcref_ptr, ir::TrapCode::IndirectCallToNull);
+                    }
                 }
-                self.builder.ins().trap(ir::TrapCode::BadSignature);
+                self.env.trap(self.builder, ir::TrapCode::BadSignature);
                 return CheckIndirectCallTypeSignature::StaticTrap;
             }
 
@@ -1263,7 +1372,8 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
             // trap with that.
             WasmHeapType::NoFunc => {
                 assert!(table.table.ref_type.nullable);
-                self.builder.ins().trap(ir::TrapCode::IndirectCallToNull);
+                self.env
+                    .trap(self.builder, ir::TrapCode::IndirectCallToNull);
                 return CheckIndirectCallTypeSignature::StaticTrap;
             }
 
@@ -1311,10 +1421,16 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
         //
         // Note that the callee may be null in which case this load may
         // trap. If so use the `IndirectCallToNull` trap code.
-        let mem_flags = ir::MemFlags::trusted().with_readonly();
+        let mut mem_flags = ir::MemFlags::trusted().with_readonly();
+        if self.env.signals_based_traps() {
+            mem_flags = mem_flags.with_trap_code(Some(ir::TrapCode::IndirectCallToNull));
+        } else {
+            self.env
+                .trapz(self.builder, funcref_ptr, ir::TrapCode::IndirectCallToNull);
+        }
         let callee_sig_id = self.builder.ins().load(
             sig_id_type,
-            mem_flags.with_trap_code(Some(ir::TrapCode::IndirectCallToNull)),
+            mem_flags,
             funcref_ptr,
             i32::from(self.env.offsets.ptr.vm_func_ref_type_index()),
         );
@@ -1324,7 +1440,8 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
             .builder
             .ins()
             .icmp(IntCC::Equal, callee_sig_id, caller_sig_id);
-        self.builder.ins().trapz(cmp, ir::TrapCode::BadSignature);
+        self.env
+            .trapz(self.builder, cmp, ir::TrapCode::BadSignature);
         CheckIndirectCallTypeSignature::Runtime
     }
 
@@ -1377,9 +1494,17 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
         // will handle the case where this is either already known to be
         // non-null or may trap.
         let mem_flags = ir::MemFlags::trusted().with_readonly();
+        let mut callee_flags = mem_flags;
+        if self.env.signals_based_traps() {
+            callee_flags = callee_flags.with_trap_code(callee_load_trap_code);
+        } else {
+            if let Some(trap) = callee_load_trap_code {
+                self.env.trapz(self.builder, callee, trap);
+            }
+        }
         let func_addr = self.builder.ins().load(
             pointer_type,
-            mem_flags.with_trap_code(callee_load_trap_code),
+            callee_flags,
             callee,
             i32::from(self.env.offsets.ptr.vm_func_ref_wasm_call()),
         );
@@ -1588,19 +1713,19 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
         let plan = &self.module.table_plans[table_index];
         let table = plan.table;
         self.ensure_table_exists(builder.func, table_index);
-        let table_data = self.tables[table_index].as_ref().unwrap();
+        let table_data = self.tables[table_index].clone().unwrap();
         let heap_ty = table.ref_type.heap_type;
         match heap_ty.top() {
             // `i31ref`s never need barriers, and therefore don't need to go
             // through the GC compiler.
             WasmHeapTopType::Any if heap_ty == WasmHeapType::I31 => {
-                let (src, flags) = table_data.prepare_table_addr(&*self.isa, builder, index);
+                let (src, flags) = table_data.prepare_table_addr(self, builder, index);
                 gc::unbarriered_load_gc_ref(self, builder, WasmHeapType::I31, src, flags)
             }
 
             // GC-managed types.
             WasmHeapTopType::Any | WasmHeapTopType::Extern => {
-                let (src, flags) = table_data.prepare_table_addr(&*self.isa, builder, index);
+                let (src, flags) = table_data.prepare_table_addr(self, builder, index);
                 gc::gc_compiler(self).translate_read_gc_reference(
                     self,
                     builder,
@@ -1634,19 +1759,19 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
         let plan = &self.module.table_plans[table_index];
         let table = plan.table;
         self.ensure_table_exists(builder.func, table_index);
-        let table_data = self.tables[table_index].as_ref().unwrap();
+        let table_data = self.tables[table_index].clone().unwrap();
         let heap_ty = table.ref_type.heap_type;
         match heap_ty.top() {
             // `i31ref`s never need GC barriers, and therefore don't need to go
             // through the GC compiler.
             WasmHeapTopType::Any if heap_ty == WasmHeapType::I31 => {
-                let (addr, flags) = table_data.prepare_table_addr(&*self.isa, builder, index);
+                let (addr, flags) = table_data.prepare_table_addr(self, builder, index);
                 gc::unbarriered_store_gc_ref(self, builder, WasmHeapType::I31, addr, value, flags)
             }
 
             // GC-managed types.
             WasmHeapTopType::Any | WasmHeapTopType::Extern => {
-                let (dst, flags) = table_data.prepare_table_addr(&*self.isa, builder, index);
+                let (dst, flags) = table_data.prepare_table_addr(self, builder, index);
                 gc::gc_compiler(self).translate_write_gc_reference(
                     self,
                     builder,
@@ -1662,7 +1787,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
                 match plan.style {
                     TableStyle::CallerChecksSignature { lazy_init } => {
                         let (elem_addr, flags) =
-                            table_data.prepare_table_addr(&*self.isa, builder, index);
+                            table_data.prepare_table_addr(self, builder, index);
                         // Set the "initialized bit". See doc-comment on
                         // `FUNCREF_INIT_BIT` in
                         // crates/environ/src/ref_bits.rs for details.
@@ -1725,20 +1850,20 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
 
     fn translate_i31_get_s(
         &mut self,
-        mut pos: FuncCursor,
+        builder: &mut FunctionBuilder,
         i31ref: ir::Value,
     ) -> WasmResult<ir::Value> {
-        pos.ins().trapz(i31ref, ir::TrapCode::NullI31Ref);
-        Ok(pos.ins().sshr_imm(i31ref, 1))
+        self.trapz(builder, i31ref, ir::TrapCode::NullI31Ref);
+        Ok(builder.ins().sshr_imm(i31ref, 1))
     }
 
     fn translate_i31_get_u(
         &mut self,
-        mut pos: FuncCursor,
+        builder: &mut FunctionBuilder,
         i31ref: ir::Value,
     ) -> WasmResult<ir::Value> {
-        pos.ins().trapz(i31ref, ir::TrapCode::NullI31Ref);
-        Ok(pos.ins().ushr_imm(i31ref, 1))
+        self.trapz(builder, i31ref, ir::TrapCode::NullI31Ref);
+        Ok(builder.ins().ushr_imm(i31ref, 1))
     }
 
     fn translate_ref_null(
@@ -2631,6 +2756,15 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
         builder: &mut FunctionBuilder,
         _state: &FuncTranslationState,
     ) -> WasmResult<()> {
+        // If an explicit stack limit is requested, emit one here at the start
+        // of the function.
+        if let Some(gv) = self.stack_limit_at_function_entry {
+            let limit = builder.ins().global_value(self.pointer_type(), gv);
+            let sp = builder.ins().get_stack_pointer(self.pointer_type());
+            let overflow = builder.ins().icmp(IntCC::UnsignedLessThan, sp, limit);
+            self.conditionally_trap(builder, overflow, ir::TrapCode::StackOverflow);
+        }
+
         // If the `vmruntime_limits_ptr` variable will get used then we initialize
         // it here.
         if self.tunables.consume_fuel || self.tunables.epoch_interruption {
@@ -2778,6 +2912,160 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
             builder.ins().call(update_mem_size, &[vmctx, num_pages]);
         }
     }
+
+    fn isa(&self) -> &dyn TargetIsa {
+        &*self.isa
+    }
+
+    fn trap(&mut self, builder: &mut FunctionBuilder, trap: ir::TrapCode) {
+        match (
+            self.signals_based_traps(),
+            crate::clif_trap_to_env_trap(trap),
+        ) {
+            // If libcall traps are disabled or there's no wasmtime-defined trap
+            // code for this, then emit a native trap instruction.
+            (true, _) | (_, None) => {
+                builder.ins().trap(trap);
+            }
+            // ... otherwise with libcall traps explicitly enabled and a
+            // wasmtime-based trap code invoke the libcall to raise a trap and
+            // pass in our trap code. Leave a debug `unreachable` in place
+            // afterwards as a defense-in-depth measure.
+            (false, Some(trap)) => {
+                let libcall = self.builtin_functions.trap(&mut builder.func);
+                let vmctx = self.vmctx_val(&mut builder.cursor());
+                let trap_code = builder.ins().iconst(I8, i64::from(trap as u8));
+                builder.ins().call(libcall, &[vmctx, trap_code]);
+                builder
+                    .ins()
+                    .trap(ir::TrapCode::User(crate::DEBUG_ASSERT_TRAP_CODE));
+            }
+        }
+    }
+
+    fn trapz(&mut self, builder: &mut FunctionBuilder, value: ir::Value, trap: ir::TrapCode) {
+        if self.signals_based_traps() {
+            builder.ins().trapz(value, trap);
+        } else {
+            let ty = builder.func.dfg.value_type(value);
+            let zero = builder.ins().iconst(ty, 0);
+            let cmp = builder.ins().icmp(IntCC::Equal, value, zero);
+            self.conditionally_trap(builder, cmp, trap);
+        }
+    }
+
+    fn trapnz(&mut self, builder: &mut FunctionBuilder, value: ir::Value, trap: ir::TrapCode) {
+        if self.signals_based_traps() {
+            builder.ins().trapnz(value, trap);
+        } else {
+            let ty = builder.func.dfg.value_type(value);
+            let zero = builder.ins().iconst(ty, 0);
+            let cmp = builder.ins().icmp(IntCC::NotEqual, value, zero);
+            self.conditionally_trap(builder, cmp, trap);
+        }
+    }
+
+    fn uadd_overflow_trap(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+        trap: ir::TrapCode,
+    ) -> ir::Value {
+        if self.signals_based_traps() {
+            builder.ins().uadd_overflow_trap(lhs, rhs, trap)
+        } else {
+            let (ret, overflow) = builder.ins().uadd_overflow(lhs, rhs);
+            self.conditionally_trap(builder, overflow, trap);
+            ret
+        }
+    }
+
+    fn signals_based_traps(&self) -> bool {
+        self.tunables.signals_based_traps
+    }
+
+    fn translate_sdiv(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+    ) -> ir::Value {
+        self.guard_signed_divide(builder, lhs, rhs);
+        builder.ins().sdiv(lhs, rhs)
+    }
+
+    fn translate_udiv(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+    ) -> ir::Value {
+        self.guard_zero_divisor(builder, rhs);
+        builder.ins().udiv(lhs, rhs)
+    }
+
+    fn translate_srem(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+    ) -> ir::Value {
+        self.guard_zero_divisor(builder, rhs);
+        builder.ins().srem(lhs, rhs)
+    }
+
+    fn translate_urem(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        lhs: ir::Value,
+        rhs: ir::Value,
+    ) -> ir::Value {
+        self.guard_zero_divisor(builder, rhs);
+        builder.ins().urem(lhs, rhs)
+    }
+
+    fn translate_fcvt_to_sint(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        ty: ir::Type,
+        val: ir::Value,
+    ) -> ir::Value {
+        // NB: for now avoid translating this entire instruction to CLIF and
+        // just do it in a libcall.
+        if self.signals_based_traps() {
+            builder.ins().fcvt_to_sint(ty, val)
+        } else {
+            self.fcvt_to_int(
+                builder,
+                ty,
+                val,
+                |me, func| me.builtin_functions.f64_to_i32(func),
+                |me, func| me.builtin_functions.f64_to_i64(func),
+            )
+        }
+    }
+
+    fn translate_fcvt_to_uint(
+        &mut self,
+        builder: &mut FunctionBuilder,
+        ty: ir::Type,
+        val: ir::Value,
+    ) -> ir::Value {
+        // NB: for now avoid translating this entire instruction to CLIF and
+        // just do it in a libcall.
+        if self.signals_based_traps() {
+            builder.ins().fcvt_to_uint(ty, val)
+        } else {
+            self.fcvt_to_int(
+                builder,
+                ty,
+                val,
+                |me, func| me.builtin_functions.f64_to_u32(func),
+                |me, func| me.builtin_functions.f64_to_u64(func),
+            )
+        }
+    }
 }
 
 // Helper function to convert an `IndexType` to an `ir::Type`.
diff --git a/crates/cranelift/src/lib.rs b/crates/cranelift/src/lib.rs
index 1f1a97daa..cbfcc7a5b 100644
--- a/crates/cranelift/src/lib.rs
+++ b/crates/cranelift/src/lib.rs
@@ -228,31 +228,35 @@ pub fn mach_trap_to_trap(trap: &MachTrap) -> Option<TrapInformation> {
     let &MachTrap { offset, code } = trap;
     Some(TrapInformation {
         code_offset: offset,
-        trap_code: match code {
-            ir::TrapCode::StackOverflow => Trap::StackOverflow,
-            ir::TrapCode::HeapOutOfBounds => Trap::MemoryOutOfBounds,
-            ir::TrapCode::HeapMisaligned => Trap::HeapMisaligned,
-            ir::TrapCode::TableOutOfBounds => Trap::TableOutOfBounds,
-            ir::TrapCode::IndirectCallToNull => Trap::IndirectCallToNull,
-            ir::TrapCode::BadSignature => Trap::BadSignature,
-            ir::TrapCode::IntegerOverflow => Trap::IntegerOverflow,
-            ir::TrapCode::IntegerDivisionByZero => Trap::IntegerDivisionByZero,
-            ir::TrapCode::BadConversionToInteger => Trap::BadConversionToInteger,
-            ir::TrapCode::UnreachableCodeReached => Trap::UnreachableCodeReached,
-            ir::TrapCode::Interrupt => Trap::Interrupt,
-            ir::TrapCode::User(ALWAYS_TRAP_CODE) => Trap::AlwaysTrapAdapter,
-            ir::TrapCode::User(CANNOT_ENTER_CODE) => Trap::CannotEnterComponent,
-            ir::TrapCode::NullReference => Trap::NullReference,
-            ir::TrapCode::NullI31Ref => Trap::NullI31Ref,
-
-            // These do not get converted to wasmtime traps, since they
-            // shouldn't ever be hit in theory. Instead of catching and handling
-            // these, we let the signal crash the process.
-            ir::TrapCode::User(DEBUG_ASSERT_TRAP_CODE) => return None,
-
-            // these should never be emitted by wasmtime-cranelift
-            ir::TrapCode::User(_) => unreachable!(),
-        },
+        trap_code: clif_trap_to_env_trap(code)?,
+    })
+}
+
+fn clif_trap_to_env_trap(trap: ir::TrapCode) -> Option<Trap> {
+    Some(match trap {
+        ir::TrapCode::StackOverflow => Trap::StackOverflow,
+        ir::TrapCode::HeapOutOfBounds => Trap::MemoryOutOfBounds,
+        ir::TrapCode::HeapMisaligned => Trap::HeapMisaligned,
+        ir::TrapCode::TableOutOfBounds => Trap::TableOutOfBounds,
+        ir::TrapCode::IndirectCallToNull => Trap::IndirectCallToNull,
+        ir::TrapCode::BadSignature => Trap::BadSignature,
+        ir::TrapCode::IntegerOverflow => Trap::IntegerOverflow,
+        ir::TrapCode::IntegerDivisionByZero => Trap::IntegerDivisionByZero,
+        ir::TrapCode::BadConversionToInteger => Trap::BadConversionToInteger,
+        ir::TrapCode::UnreachableCodeReached => Trap::UnreachableCodeReached,
+        ir::TrapCode::Interrupt => Trap::Interrupt,
+        ir::TrapCode::User(ALWAYS_TRAP_CODE) => Trap::AlwaysTrapAdapter,
+        ir::TrapCode::User(CANNOT_ENTER_CODE) => Trap::CannotEnterComponent,
+        ir::TrapCode::NullReference => Trap::NullReference,
+        ir::TrapCode::NullI31Ref => Trap::NullI31Ref,
+
+        // These do not get converted to wasmtime traps, since they
+        // shouldn't ever be hit in theory. Instead of catching and handling
+        // these, we let the signal crash the process.
+        ir::TrapCode::User(DEBUG_ASSERT_TRAP_CODE) => return None,
+
+        // these should never be emitted by wasmtime-cranelift
+        ir::TrapCode::User(_) => unreachable!(),
     })
 }
 
@@ -362,6 +366,14 @@ impl BuiltinFunctionSignatures {
         AbiParam::new(ir::types::I64)
     }
 
+    fn f64(&self) -> AbiParam {
+        AbiParam::new(ir::types::F64)
+    }
+
+    fn u8(&self) -> AbiParam {
+        AbiParam::new(ir::types::I8)
+    }
+
     fn signature(&self, builtin: BuiltinFunctionIndex) -> Signature {
         let mut _cur = 0;
         macro_rules! iter {
diff --git a/crates/environ/src/builtin.rs b/crates/environ/src/builtin.rs
index 79fdf8e73..f42b0b5dc 100644
--- a/crates/environ/src/builtin.rs
+++ b/crates/environ/src/builtin.rs
@@ -96,6 +96,19 @@ macro_rules! foreach_builtin_function {
             // Returns an index for Wasm's `table.fill` instruction for GC references.
             #[cfg(feature = "gc")]
             table_fill_gc_ref(vmctx: vmctx, table: i32, dst: i64, val: reference, len: i64);
+
+            // Raises an unconditional trap.
+            trap(vmctx: vmctx, code: u8);
+
+            // Implementation of `i{32,64}.trunc_f{32,64}_{u,s}` when host trap
+            // handlers are disabled. These will raise a trap if necessary. Note
+            // that f32 inputs are always converted to f64 as the argument. Also
+            // note that the signed-ness of the result is not reflected in the
+            // type here.
+            f64_to_i64(vmctx: vmctx, float: f64) -> i64;
+            f64_to_u64(vmctx: vmctx, float: f64) -> i64;
+            f64_to_i32(vmctx: vmctx, float: f64) -> i32;
+            f64_to_u32(vmctx: vmctx, float: f64) -> i32;
         }
     };
 }
diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs
index 10741a59c..4b72baa71 100644
--- a/crates/environ/src/component.rs
+++ b/crates/environ/src/component.rs
@@ -104,6 +104,8 @@ macro_rules! foreach_builtin_component_function {
             resource_transfer_borrow(vmctx: vmctx, src_idx: u32, src_table: u32, dst_table: u32) -> u32;
             resource_enter_call(vmctx: vmctx);
             resource_exit_call(vmctx: vmctx);
+
+            trap(vmctx: vmctx, code: u8);
         }
     };
 }
diff --git a/crates/environ/src/component/compiler.rs b/crates/environ/src/component/compiler.rs
index aec715c9d..b4320ec40 100644
--- a/crates/environ/src/component/compiler.rs
+++ b/crates/environ/src/component/compiler.rs
@@ -1,5 +1,6 @@
 use crate::component::{AllCallFunc, ComponentTranslation, ComponentTypesBuilder, TrampolineIndex};
 use crate::prelude::*;
+use crate::Tunables;
 use anyhow::Result;
 use std::any::Any;
 
@@ -16,5 +17,6 @@ pub trait ComponentCompiler: Send + Sync {
         component: &ComponentTranslation,
         types: &ComponentTypesBuilder,
         trampoline: TrampolineIndex,
+        tunables: &Tunables,
     ) -> Result<AllCallFunc<Box<dyn Any + Send>>>;
 }
diff --git a/crates/environ/src/module.rs b/crates/environ/src/module.rs
index 578f3a6bd..16507387a 100644
--- a/crates/environ/src/module.rs
+++ b/crates/environ/src/module.rs
@@ -33,6 +33,7 @@ impl MemoryStyle {
             // page size, but unfortunately that is a little hard to plumb
             // through here.
             memory.page_size_log2 >= Memory::DEFAULT_PAGE_SIZE_LOG2
+            && tunables.signals_based_traps
             && match memory.maximum_byte_size() {
                 Ok(mut maximum) => {
                     if tunables.static_memory_bound_is_maximum {
diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs
index 22b96c4ab..ddf317124 100644
--- a/crates/environ/src/tunables.rs
+++ b/crates/environ/src/tunables.rs
@@ -62,6 +62,10 @@ pub struct Tunables {
 
     /// Whether or not Wasm functions target the winch abi.
     pub winch_callable: bool,
+
+    /// Whether or not the host will be using native signals (e.g. SIGILL,
+    /// SIGSEGV, etc) to implement traps.
+    pub signals_based_traps: bool,
 }
 
 impl Tunables {
@@ -113,6 +117,7 @@ impl Tunables {
             debug_adapter_modules: false,
             relaxed_simd_deterministic: false,
             winch_callable: false,
+            signals_based_traps: true,
         }
     }
 
diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs
index 66b4abdb8..aaf12ac49 100644
--- a/crates/fuzzing/src/generators/config.rs
+++ b/crates/fuzzing/src/generators/config.rs
@@ -180,7 +180,8 @@ impl Config {
                 self.wasmtime.memory_guaranteed_dense_image_size,
             ))
             .allocation_strategy(self.wasmtime.strategy.to_wasmtime())
-            .generate_address_map(self.wasmtime.generate_address_map);
+            .generate_address_map(self.wasmtime.generate_address_map)
+            .signals_based_traps(self.wasmtime.signals_based_traps);
 
         if !self.module_config.config.simd_enabled {
             cfg.wasm_relaxed_simd(false);
@@ -361,20 +362,6 @@ impl Config {
         unsafe { Ok(Module::deserialize_file(engine, &file).unwrap()) }
     }
 
-    /// Winch doesn't support the same set of wasm proposal as Cranelift at
-    /// this time, so if winch is selected be sure to disable wasm proposals
-    /// in `Config` to ensure that Winch can compile the module that
-    /// wasm-smith generates.
-    pub fn disable_unimplemented_winch_proposals(&mut self) {
-        self.module_config.config.simd_enabled = false;
-        self.module_config.config.relaxed_simd_enabled = false;
-        self.module_config.config.gc_enabled = false;
-        self.module_config.config.threads_enabled = false;
-        self.module_config.config.tail_call_enabled = false;
-        self.module_config.config.exceptions_enabled = false;
-        self.module_config.config.reference_types_enabled = false;
-    }
-
     /// Updates this configuration to forcibly enable async support. Only useful
     /// in fuzzers which do async calls.
     pub fn enable_async(&mut self, u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
@@ -400,71 +387,9 @@ impl<'a> Arbitrary<'a> for Config {
             module_config: u.arbitrary()?,
         };
 
-        if let CompilerStrategy::Winch = config.wasmtime.compiler_strategy {
-            config.disable_unimplemented_winch_proposals();
-        }
-
-        // If using the pooling allocator, constrain the memory and module configurations
-        // to the module limits.
-        if let InstanceAllocationStrategy::Pooling(pooling) = &mut config.wasmtime.strategy {
-            // Forcibly don't use the `CustomUnaligned` memory configuration
-            // with the pooling allocator active.
-            if let MemoryConfig::CustomUnaligned = config.wasmtime.memory_config {
-                config.wasmtime.memory_config = MemoryConfig::Normal(u.arbitrary()?);
-            }
-
-            let cfg = &mut config.module_config.config;
-            // If the pooling allocator is used, do not allow shared memory to
-            // be created. FIXME: see
-            // https://github.com/bytecodealliance/wasmtime/issues/4244.
-            cfg.threads_enabled = false;
-
-            // Ensure the pooling allocator can support the maximal size of
-            // memory, picking the smaller of the two to win.
-            let min_bytes = cfg
-                .max_memory32_bytes
-                // memory64_bytes is a u128, but since we are taking the min
-                // we can truncate it down to a u64.
-                .min(cfg.max_memory64_bytes.try_into().unwrap_or(u64::MAX));
-            let mut min = min_bytes.min(pooling.max_memory_size as u64);
-            if let MemoryConfig::Normal(cfg) = &config.wasmtime.memory_config {
-                min = min.min(cfg.static_memory_maximum_size.unwrap_or(0));
-            }
-            pooling.max_memory_size = min as usize;
-            cfg.max_memory32_bytes = min;
-            cfg.max_memory64_bytes = min as u128;
-
-            // If traps are disallowed then memories must have at least one page
-            // of memory so if we still are only allowing 0 pages of memory then
-            // increase that to one here.
-            if cfg.disallow_traps {
-                if pooling.max_memory_size < (1 << 16) {
-                    pooling.max_memory_size = 1 << 16;
-                    cfg.max_memory32_bytes = 1 << 16;
-                    cfg.max_memory64_bytes = 1 << 16;
-                    if let MemoryConfig::Normal(cfg) = &mut config.wasmtime.memory_config {
-                        match &mut cfg.static_memory_maximum_size {
-                            Some(size) => *size = (*size).max(pooling.max_memory_size as u64),
-                            size @ None => *size = Some(pooling.max_memory_size as u64),
-                        }
-                    }
-                }
-                // .. additionally update tables
-                if pooling.table_elements == 0 {
-                    pooling.table_elements = 1;
-                }
-            }
-
-            // Don't allow too many linear memories per instance since massive
-            // virtual mappings can fail to get allocated.
-            cfg.min_memories = cfg.min_memories.min(10);
-            cfg.max_memories = cfg.max_memories.min(10);
-
-            // Force this pooling allocator to always be able to accommodate the
-            // module that may be generated.
-            pooling.total_memories = cfg.max_memories as u32;
-            pooling.total_tables = cfg.max_tables as u32;
-        }
+        config
+            .wasmtime
+            .update_module_config(&mut config.module_config.config, u)?;
 
         Ok(config)
     }
@@ -502,6 +427,10 @@ pub struct WasmtimeConfig {
     /// Configuration for whether wasm is invoked in an async fashion and how
     /// it's cooperatively time-sliced.
     pub async_config: AsyncConfig,
+
+    /// Whether or not host signal handlers are enabled for this configuration,
+    /// aka whether signal handlers are supported.
+    signals_based_traps: bool,
 }
 
 impl WasmtimeConfig {
@@ -527,6 +456,109 @@ impl WasmtimeConfig {
             self.memory_config = other.memory_config.clone();
         }
     }
+
+    /// Updates `config` to be compatible with `self` and the other way around
+    /// too.
+    pub fn update_module_config(
+        &mut self,
+        config: &mut wasm_smith::Config,
+        u: &mut Unstructured<'_>,
+    ) -> arbitrary::Result<()> {
+        // Winch doesn't support the same set of wasm proposal as Cranelift at
+        // this time, so if winch is selected be sure to disable wasm proposals
+        // in `Config` to ensure that Winch can compile the module that
+        // wasm-smith generates.
+        if let CompilerStrategy::Winch = self.compiler_strategy {
+            config.simd_enabled = false;
+            config.relaxed_simd_enabled = false;
+            config.gc_enabled = false;
+            config.threads_enabled = false;
+            config.tail_call_enabled = false;
+            config.exceptions_enabled = false;
+            config.reference_types_enabled = false;
+
+            // Winch requires host trap handlers to be enabled at this time.
+            self.signals_based_traps = true;
+        }
+
+        // If using the pooling allocator, constrain the memory and module configurations
+        // to the module limits.
+        if let InstanceAllocationStrategy::Pooling(pooling) = &mut self.strategy {
+            // Forcibly don't use the `CustomUnaligned` memory configuration
+            // with the pooling allocator active.
+            if let MemoryConfig::CustomUnaligned = self.memory_config {
+                self.memory_config = MemoryConfig::Normal(u.arbitrary()?);
+            }
+
+            // If the pooling allocator is used, do not allow shared memory to
+            // be created. FIXME: see
+            // https://github.com/bytecodealliance/wasmtime/issues/4244.
+            config.threads_enabled = false;
+
+            // Ensure the pooling allocator can support the maximal size of
+            // memory, picking the smaller of the two to win.
+            let min_bytes = config
+                .max_memory32_bytes
+                // memory64_bytes is a u128, but since we are taking the min
+                // we can truncate it down to a u64.
+                .min(config.max_memory64_bytes.try_into().unwrap_or(u64::MAX));
+            let mut min = min_bytes.min(pooling.max_memory_size as u64);
+            if let MemoryConfig::Normal(cfg) = &self.memory_config {
+                min = min.min(cfg.static_memory_maximum_size.unwrap_or(0));
+            }
+            pooling.max_memory_size = min as usize;
+            config.max_memory32_bytes = min;
+            config.max_memory64_bytes = min as u128;
+
+            // If traps are disallowed then memories must have at least one page
+            // of memory so if we still are only allowing 0 pages of memory then
+            // increase that to one here.
+            if config.disallow_traps {
+                if pooling.max_memory_size < (1 << 16) {
+                    pooling.max_memory_size = 1 << 16;
+                    config.max_memory32_bytes = 1 << 16;
+                    config.max_memory64_bytes = 1 << 16;
+                    if let MemoryConfig::Normal(cfg) = &mut self.memory_config {
+                        match &mut cfg.static_memory_maximum_size {
+                            Some(size) => *size = (*size).max(pooling.max_memory_size as u64),
+                            size @ None => *size = Some(pooling.max_memory_size as u64),
+                        }
+                    }
+                }
+                // .. additionally update tables
+                if pooling.table_elements == 0 {
+                    pooling.table_elements = 1;
+                }
+            }
+
+            // Don't allow too many linear memories per instance since massive
+            // virtual mappings can fail to get allocated.
+            config.min_memories = config.min_memories.min(10);
+            config.max_memories = config.max_memories.min(10);
+
+            // Force this pooling allocator to always be able to accommodate the
+            // module that may be generated.
+            pooling.total_memories = config.max_memories as u32;
+            pooling.total_tables = config.max_tables as u32;
+        }
+
+        if !self.signals_based_traps {
+            // At this time shared memories require a "static" memory
+            // configuration but when signals-based traps are disabled all
+            // memories are forced to the "dynamic" configuration. This is
+            // fixable with some more work on the bounds-checks side of things
+            // to do a full bounds check even on static memories, but that's
+            // left for a future PR.
+            config.threads_enabled = false;
+
+            // Spectre-based heap mitigations require signal handlers so this
+            // must always be disabled if signals-based traps are disabled.
+            if let MemoryConfig::Normal(cfg) = &mut self.memory_config {
+                cfg.cranelift_enable_heap_access_spectre_mitigations = None;
+            }
+        }
+        Ok(())
+    }
 }
 
 #[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)]
diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs
index 9b60e0da3..6cc1e586c 100644
--- a/crates/fuzzing/src/oracles.rs
+++ b/crates/fuzzing/src/oracles.rs
@@ -489,7 +489,13 @@ impl<T, U> DiffEqResult<T, U> {
             // of the two instances, so `None` is returned and nothing else is
             // compared.
             (Err(lhs), Err(rhs)) => {
-                let err = rhs.downcast::<Trap>().expect("not a trap");
+                let err = match rhs.downcast::<Trap>() {
+                    Ok(trap) => trap,
+                    Err(err) => {
+                        log::debug!("rhs failed: {err:?}");
+                        return DiffEqResult::Failed;
+                    }
+                };
                 let poisoned = err == Trap::StackOverflow || lhs_engine.is_stack_overflow(&lhs);
 
                 if poisoned {
@@ -499,8 +505,8 @@ impl<T, U> DiffEqResult<T, U> {
                 DiffEqResult::Failed
             }
             // A real bug is found if only one side fails.
-            (Ok(_), Err(_)) => panic!("only the `rhs` failed for this input"),
-            (Err(_), Ok(_)) => panic!("only the `lhs` failed for this input"),
+            (Ok(_), Err(err)) => panic!("only the `rhs` failed for this input: {err:?}"),
+            (Err(err), Ok(_)) => panic!("only the `lhs` failed for this input: {err:?}"),
         }
     }
 }
diff --git a/crates/fuzzing/src/oracles/diff_wasmtime.rs b/crates/fuzzing/src/oracles/diff_wasmtime.rs
index b232b0e8d..538cb1fdd 100644
--- a/crates/fuzzing/src/oracles/diff_wasmtime.rs
+++ b/crates/fuzzing/src/oracles/diff_wasmtime.rs
@@ -24,11 +24,9 @@ impl WasmtimeEngine {
         config: &mut generators::Config,
         compiler_strategy: CompilerStrategy,
     ) -> arbitrary::Result<Self> {
-        if let CompilerStrategy::Winch = compiler_strategy {
-            config.disable_unimplemented_winch_proposals();
-        }
         let mut new_config = u.arbitrary::<WasmtimeConfig>()?;
         new_config.compiler_strategy = compiler_strategy;
+        new_config.update_module_config(&mut config.module_config.config, u)?;
         new_config.make_compatible_with(&config.wasmtime);
 
         let config = generators::Config {
diff --git a/crates/wasmtime/src/compile.rs b/crates/wasmtime/src/compile.rs
index 0469b4e09..6021c627f 100644
--- a/crates/wasmtime/src/compile.rs
+++ b/crates/wasmtime/src/compile.rs
@@ -145,7 +145,7 @@ pub(crate) fn build_component_artifacts<T: FinishedObject>(
             .context("failed to parse WebAssembly module")?;
 
     let compile_inputs = CompileInputs::for_component(
-        engine.compiler().triple(),
+        engine,
         &types,
         &component,
         module_translations.iter_mut().map(|(i, translation)| {
@@ -357,7 +357,7 @@ impl<'a> CompileInputs<'a> {
     /// Create a `CompileInputs` for a component.
     #[cfg(feature = "component-model")]
     fn for_component(
-        triple: &target_lexicon::Triple,
+        engine: &'a Engine,
         types: &'a wasmtime_environ::component::ComponentTypesBuilder,
         component: &'a wasmtime_environ::component::ComponentTranslation,
         module_translations: impl IntoIterator<
@@ -368,6 +368,7 @@ impl<'a> CompileInputs<'a> {
             ),
         >,
     ) -> Self {
+        let triple = engine.compiler().triple();
         let mut ret = CompileInputs {
             need_wasm_to_array_trampolines: !matches!(
                 triple.architecture,
@@ -377,6 +378,7 @@ impl<'a> CompileInputs<'a> {
         };
 
         ret.collect_inputs_in_translations(types.module_types_builder(), module_translations);
+        let tunables = engine.tunables();
 
         for (idx, trampoline) in component.trampolines.iter() {
             ret.push_input(move |compiler| {
@@ -385,7 +387,7 @@ impl<'a> CompileInputs<'a> {
                     symbol: trampoline.symbol_name(),
                     function: compiler
                         .component_compiler()
-                        .compile_trampoline(component, types, idx)?
+                        .compile_trampoline(component, types, idx, tunables)?
                         .into(),
                     info: None,
                 })
diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs
index 929f64e57..8df4db982 100644
--- a/crates/wasmtime/src/config.rs
+++ b/crates/wasmtime/src/config.rs
@@ -159,6 +159,7 @@ struct ConfigTunables {
     generate_address_map: Option<bool>,
     debug_adapter_modules: Option<bool>,
     relaxed_simd_deterministic: Option<bool>,
+    signals_based_traps: Option<bool>,
 }
 
 /// User-provided configuration for the compiler.
@@ -1945,6 +1946,7 @@ impl Config {
             generate_address_map
             debug_adapter_modules
             relaxed_simd_deterministic
+            signals_based_traps
         }
 
         // If we're going to compile with winch, we must use the winch calling convention.
@@ -1955,6 +1957,10 @@ impl Config {
             if tunables.winch_callable && !tunables.table_lazy_init {
                 bail!("Winch requires the table-lazy-init configuration option");
             }
+
+            if tunables.winch_callable && !tunables.signals_based_traps {
+                bail!("Winch requires signals-based traps to be enabled");
+            }
         }
 
         if tunables.static_memory_offset_guard_size < tunables.dynamic_memory_offset_guard_size {
@@ -2086,6 +2092,29 @@ impl Config {
             .settings
             .insert("preserve_frame_pointers".into(), "true".into());
 
+        if !tunables.signals_based_traps {
+            let mut ok = self.compiler_config.ensure_setting_unset_or_given(
+                "enable_table_access_spectre_mitigation".into(),
+                "false".into(),
+            );
+            ok = ok
+                && self.compiler_config.ensure_setting_unset_or_given(
+                    "enable_heap_access_spectre_mitigation".into(),
+                    "false".into(),
+                );
+
+            // Right now spectre-mitigated bounds checks will load from zero so
+            // if host-based signal handlers are disabled then that's a mismatch
+            // and doesn't work right now. Fixing this will require more thought
+            // of how to implement the bounds check in spectre-only mode.
+            if !ok {
+                bail!(
+                    "when signals-based traps are disabled then spectre \
+                     mitigations must also be disabled"
+                );
+            }
+        }
+
         // check for incompatible compiler options and set required values
         if features.contains(WasmFeatures::REFERENCE_TYPES) {
             if !self
@@ -2203,6 +2232,43 @@ impl Config {
         self.detect_host_feature = Some(detect);
         self
     }
+
+    /// Configures Wasmtime to not use signals-based trap handlers, for example
+    /// disables `SIGILL` and `SIGSEGV` handler registration on Unix platforms.
+    ///
+    /// Wasmtime will by default leverage signals-based trap handlers (or the
+    /// platform equivalent, for example "vectored exception handlers" on
+    /// Windows) to make generated code more efficient. For example an
+    /// out-of-bounds load in WebAssembly will result in a `SIGSEGV` on Unix
+    /// that is caught by a signal handler in Wasmtime by default. Another
+    /// example is divide-by-zero is reported by hardware rather than
+    /// explicitly checked and Wasmtime turns that into a trap.
+    ///
+    /// Some environments however may not have easy access to signal handlers.
+    /// For example embedded scenarios may not support virtual memory. Other
+    /// environments where Wasmtime is embedded within the surrounding
+    /// environment may require that new signal handlers aren't registered due
+    /// to the global nature of signal handlers. This option exists to disable
+    /// the signal handler registration when required.
+    ///
+    /// When signals-based trap handlers are disabled then generated code will
+    /// never rely on segfaults or other signals. Generated code will be slower
+    /// because bounds checks must be explicit along with other operations like
+    /// integer division which must check for zero.
+    ///
+    /// When this option is disable it additionally requires that the
+    /// `enable_heap_access_spectre_mitigation` and
+    /// `enable_table_access_spectre_mitigation` Cranelift settings are
+    /// disabled. This means that generated code must have spectre mitigations
+    /// disabled. This is because spectre mitigations rely on faults from
+    /// loading from the null address to implement bounds checks.
+    ///
+    /// This option defaults to `true` meaning that signals-based trap handlers
+    /// are enabled by default.
+    pub fn signals_based_traps(&mut self, enable: bool) -> &mut Self {
+        self.tunables.signals_based_traps = Some(enable);
+        self
+    }
 }
 
 impl Default for Config {
diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs
index fbd6d8aac..e62b991b3 100644
--- a/crates/wasmtime/src/engine.rs
+++ b/crates/wasmtime/src/engine.rs
@@ -88,8 +88,11 @@ impl Engine {
     /// to `true`, but explicitly disable these two compiler settings
     /// will cause errors.
     pub fn new(config: &Config) -> Result<Engine> {
+        let config = config.clone();
+        let (tunables, features) = config.validate()?;
+
         #[cfg(feature = "runtime")]
-        {
+        if tunables.signals_based_traps {
             // Ensure that crate::runtime::vm's signal handlers are
             // configured. This is the per-program initialization required for
             // handling traps, such as configuring signals, vectored exception
@@ -99,9 +102,6 @@ impl Engine {
             crate::runtime::vm::debug_builtins::ensure_exported();
         }
 
-        let config = config.clone();
-        let (tunables, features) = config.validate()?;
-
         #[cfg(any(feature = "cranelift", feature = "winch"))]
         let (config, compiler) = config.build_compiler(&tunables, features)?;
 
diff --git a/crates/wasmtime/src/engine/serialization.rs b/crates/wasmtime/src/engine/serialization.rs
index 9b1da2bcb..ced555df7 100644
--- a/crates/wasmtime/src/engine/serialization.rs
+++ b/crates/wasmtime/src/engine/serialization.rs
@@ -370,7 +370,7 @@ impl Metadata<'_> {
             table_lazy_init,
             relaxed_simd_deterministic,
             winch_callable,
-
+            signals_based_traps,
             // This doesn't affect compilation, it's just a runtime setting.
             dynamic_memory_growth_reserve: _,
 
@@ -436,6 +436,11 @@ impl Metadata<'_> {
             other.winch_callable,
             "Winch calling convention",
         )?;
+        Self::check_bool(
+            signals_based_traps,
+            other.signals_based_traps,
+            "Signals-based traps",
+        )?;
 
         Ok(())
     }
diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs
index 34d2a0c18..69ff4d628 100644
--- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs
+++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs
@@ -27,6 +27,7 @@ macro_rules! signature {
     (@ty ptr_u8) => (*mut u8);
     (@ty ptr_u16) => (*mut u16);
     (@ty ptr_size) => (*mut usize);
+    (@ty u8) => (u8);
     (@ty u32) => (u32);
     (@ty u64) => (u64);
     (@ty vmctx) => (*mut VMComponentContext);
@@ -569,3 +570,7 @@ unsafe fn resource_enter_call(vmctx: *mut VMComponentContext) -> Result<()> {
 unsafe fn resource_exit_call(vmctx: *mut VMComponentContext) -> Result<()> {
     ComponentInstance::from_vmctx(vmctx, |instance| instance.resource_exit_call())
 }
+
+unsafe fn trap(_vmctx: *mut VMComponentContext, code: u8) -> Result<()> {
+    Err(wasmtime_environ::Trap::from_u8(code).unwrap()).err2anyhow()
+}
diff --git a/crates/wasmtime/src/runtime/vm/gc/i31.rs b/crates/wasmtime/src/runtime/vm/gc/i31.rs
index e79ece0c3..98e3e3c07 100644
--- a/crates/wasmtime/src/runtime/vm/gc/i31.rs
+++ b/crates/wasmtime/src/runtime/vm/gc/i31.rs
@@ -2,6 +2,7 @@
 
 use super::VMGcRef;
 use core::fmt;
+use wasmtime_environ::Unsigned;
 
 /// A 31-bit integer for use with `i31ref`.
 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
@@ -68,9 +69,8 @@ impl I31 {
     /// If the value doesn't fit in the bottom 31 bits, it is wrapped such that
     /// the wrapped value does.
     #[inline]
-    #[allow(clippy::cast_sign_loss)]
     pub fn wrapping_i32(value: i32) -> Self {
-        Self::wrapping_u32(value as u32)
+        Self::wrapping_u32(value.unsigned())
     }
 
     /// Get this `I31`'s value as an unsigned integer.
diff --git a/crates/wasmtime/src/runtime/vm/libcalls.rs b/crates/wasmtime/src/runtime/vm/libcalls.rs
index d80fb39d2..bd8638459 100644
--- a/crates/wasmtime/src/runtime/vm/libcalls.rs
+++ b/crates/wasmtime/src/runtime/vm/libcalls.rs
@@ -60,6 +60,7 @@ use crate::runtime::vm::vmcontext::VMFuncRef;
 use crate::runtime::vm::{Instance, TrapReason, VMGcRef};
 #[cfg(feature = "threads")]
 use core::time::Duration;
+use wasmtime_environ::Unsigned;
 use wasmtime_environ::{DataIndex, ElemIndex, FuncIndex, MemoryIndex, TableIndex, Trap};
 #[cfg(feature = "wmemcheck")]
 use wasmtime_wmemcheck::AccessError::{
@@ -139,6 +140,8 @@ pub mod raw {
 
         (@ty i32) => (u32);
         (@ty i64) => (u64);
+        (@ty f64) => (f64);
+        (@ty u8) => (u8);
         (@ty reference) => (u32);
         (@ty pointer) => (*mut u8);
         (@ty vmctx) => (*mut VMContext);
@@ -655,6 +658,60 @@ fn update_mem_size(instance: &mut Instance, num_pages: u32) {
     }
 }
 
+fn trap(_instance: &mut Instance, code: u8) -> Result<(), TrapReason> {
+    Err(TrapReason::Wasm(
+        wasmtime_environ::Trap::from_u8(code).unwrap(),
+    ))
+}
+
+fn f64_to_i64(_instance: &mut Instance, val: f64) -> Result<u64, TrapReason> {
+    if val.is_nan() {
+        return Err(TrapReason::Wasm(Trap::BadConversionToInteger));
+    }
+    let val = relocs::truncf64(val);
+    if val <= -9223372036854777856.0 || val >= 9223372036854775808.0 {
+        return Err(TrapReason::Wasm(Trap::IntegerOverflow));
+    }
+    #[allow(clippy::cast_possible_truncation)]
+    return Ok((val as i64).unsigned());
+}
+
+fn f64_to_u64(_instance: &mut Instance, val: f64) -> Result<u64, TrapReason> {
+    if val.is_nan() {
+        return Err(TrapReason::Wasm(Trap::BadConversionToInteger));
+    }
+    let val = relocs::truncf64(val);
+    if val <= -1.0 || val >= 18446744073709551616.0 {
+        return Err(TrapReason::Wasm(Trap::IntegerOverflow));
+    }
+    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
+    return Ok(val as u64);
+}
+
+fn f64_to_i32(_instance: &mut Instance, val: f64) -> Result<u32, TrapReason> {
+    if val.is_nan() {
+        return Err(TrapReason::Wasm(Trap::BadConversionToInteger));
+    }
+    let val = relocs::truncf64(val);
+    if val <= -2147483649.0 || val >= 2147483648.0 {
+        return Err(TrapReason::Wasm(Trap::IntegerOverflow));
+    }
+    #[allow(clippy::cast_possible_truncation)]
+    return Ok((val as i32).unsigned());
+}
+
+fn f64_to_u32(_instance: &mut Instance, val: f64) -> Result<u32, TrapReason> {
+    if val.is_nan() {
+        return Err(TrapReason::Wasm(Trap::BadConversionToInteger));
+    }
+    let val = relocs::truncf64(val);
+    if val <= -1.0 || val >= 4294967296.0 {
+        return Err(TrapReason::Wasm(Trap::IntegerOverflow));
+    }
+    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
+    return Ok(val as u32);
+}
+
 /// This module contains functions which are used for resolving relocations at
 /// runtime if necessary.
 ///
diff --git a/crates/wasmtime/src/runtime/vm/vmcontext.rs b/crates/wasmtime/src/runtime/vm/vmcontext.rs
index ca032b758..26e72a84f 100644
--- a/crates/wasmtime/src/runtime/vm/vmcontext.rs
+++ b/crates/wasmtime/src/runtime/vm/vmcontext.rs
@@ -732,6 +732,8 @@ macro_rules! define_builtin_array {
 
     (@ty i32) => (u32);
     (@ty i64) => (u64);
+    (@ty f64) => (f64);
+    (@ty u8) => (u8);
     (@ty reference) => (u32);
     (@ty pointer) => (*mut u8);
     (@ty vmctx) => (*mut VMContext);
diff --git a/winch/codegen/src/codegen/builtin.rs b/winch/codegen/src/codegen/builtin.rs
index eb0ad65a2..a7a7de748 100644
--- a/winch/codegen/src/codegen/builtin.rs
+++ b/winch/codegen/src/codegen/builtin.rs
@@ -127,6 +127,10 @@ macro_rules! declare_function_sig {
                 WasmValType::I32
             }
 
+            fn u8(&self) -> WasmValType {
+                WasmValType::I32
+            }
+
             fn f32(&self) -> WasmValType {
                 WasmValType::F32
             }


Test patch:
diff --git a/tests/disable_host_trap_handlers.rs b/tests/disable_host_trap_handlers.rs
new file mode 100644
index 000000000..da2830d80
--- /dev/null
+++ b/tests/disable_host_trap_handlers.rs
@@ -0,0 +1,92 @@
+//! A standalone test to assert that Wasmtime can operate in "no signal handlers
+//! mode"
+//!
+//! This is a test for `Config::signals_based_traps(false)` which resides in its
+//! own binary to assert properties about signal handlers that Wasmtime uses.
+//! Due to the global nature of signals no other tests can be in this binary.
+//! This will ensure that various trapping scenarios all work and additionally
+//! signal handlers are not registered.
+
+use anyhow::Result;
+use wasmtime::{Config, Engine, Instance, Module, Store, Trap};
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn no_host_trap_handlers() -> Result<()> {
+    let mut config = Config::new();
+    config.signals_based_traps(false);
+    let engine = Engine::new(&config)?;
+    let module = Module::new(
+        &engine,
+        r#"
+            (module
+                (memory 1)
+
+                (func (export "load") (param i32) (result i32)
+                    (i32.load (local.get 0)))
+
+                (func (export "div") (param i32 i32) (result i32)
+                    (i32.div_s (local.get 0) (local.get 1)))
+
+                (func (export "unreachable") unreachable)
+                (func $oflow (export "overflow") call $oflow)
+            )
+        "#,
+    )?;
+
+    let mut store = Store::new(&engine, ());
+    let instance = Instance::new(&mut store, &module, &[])?;
+    let load = instance.get_typed_func::<i32, i32>(&mut store, "load")?;
+    let div = instance.get_typed_func::<(i32, i32), i32>(&mut store, "div")?;
+    let unreachable = instance.get_typed_func::<(), ()>(&mut store, "unreachable")?;
+    let overflow = instance.get_typed_func::<(), ()>(&mut store, "overflow")?;
+
+    let trap = load
+        .call(&mut store, 1 << 20)
+        .unwrap_err()
+        .downcast::<Trap>()?;
+    assert_eq!(trap, Trap::MemoryOutOfBounds);
+
+    let trap = div
+        .call(&mut store, (1, 0))
+        .unwrap_err()
+        .downcast::<Trap>()?;
+    assert_eq!(trap, Trap::IntegerDivisionByZero);
+
+    let trap = unreachable
+        .call(&mut store, ())
+        .unwrap_err()
+        .downcast::<Trap>()?;
+    assert_eq!(trap, Trap::UnreachableCodeReached);
+
+    let trap = overflow
+        .call(&mut store, ())
+        .unwrap_err()
+        .downcast::<Trap>()?;
+    assert_eq!(trap, Trap::StackOverflow);
+
+    assert_host_signal_handlers_are_unset();
+
+    Ok(())
+}
+
+fn assert_host_signal_handlers_are_unset() {
+    #[cfg(unix)]
+    unsafe {
+        let mut prev = std::mem::zeroed::<libc::sigaction>();
+        let rc = libc::sigaction(libc::SIGILL, std::ptr::null(), &mut prev);
+        assert_eq!(rc, 0);
+        assert_eq!(
+            prev.sa_sigaction,
+            libc::SIG_DFL,
+            "fault handler was installed when it shouldn't have been"
+        );
+    }
+    #[cfg(windows)]
+    {
+        // Note that this can't be checked on Windows because vectored exception
+        // handlers work a bit differently and aren't as "global" as a signal
+        // handler. For now rely on the check above on unix to also guarantee
+        // that on Windows we don't register any vectored exception handlers.
+    }
+}