Repository: bytecodealliance/wasmtime
Pull request #9158 (2024-08-20 20:03:08)

Update handling of wasm features and `*.wast` testing
This commit is an attempt at making it easier to manage the set of variables we have in play with `*.wast` testing and wasm features. The Cranelift and Winch backends support different sets of wasm features at this time and historically Cranelift has also had architecture-specific support for wasm features. This is hoped to help simplify management of all of this in the future in addition to making `*.wast` testing more robust. Notable changes here are:

* A `Config` no longer tracks a `WasmFeatures` but instead only tracks explicitly disabled/enabled features. The final `WasmFeatures` is then computed given the result of compiler configuration.
* Default-enabled features now start from nothing instead of `wasmparser`'s defaults to avoid future breakage there.
* Each compiler configuration and/or backend now has a listed set of "this feature may panic" wasm features. These are all disabled by default and if explicitly enabled a `Config::validate` error is returned.
* All `*.wast` tests are now run through both Cranelift and Winch. Tests are now asserted to either fail or pass depending on configuration and the whole test will fail if the result doesn't match the expectation.
* Many `gc` proposal tests which are now passing are flagged as passing now. Failing `winch` tests are now explicitly listed instead of using a litany of patterns.

<!--
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/ci/docker/aarch64-linux/Dockerfile b/ci/docker/aarch64-linux/Dockerfile
index 4d1660d08..4573e830e 100644
--- a/ci/docker/aarch64-linux/Dockerfile
+++ b/ci/docker/aarch64-linux/Dockerfile
@@ -1,6 +1,7 @@
 FROM ubuntu:16.04
 
 RUN apt-get update -y && apt-get install -y gcc gcc-aarch64-linux-gnu ca-certificates curl make git
+RUN git config --global --add safe.directory '*'
 
 # The CMake in Ubuntu 16.04 was a bit too old for us to use so download one from
 # CMake's own releases and use that instead.
diff --git a/ci/docker/riscv64gc-linux/Dockerfile b/ci/docker/riscv64gc-linux/Dockerfile
index ca72564ea..c10524d86 100644
--- a/ci/docker/riscv64gc-linux/Dockerfile
+++ b/ci/docker/riscv64gc-linux/Dockerfile
@@ -1,5 +1,6 @@
 FROM ubuntu:22.04
 
 RUN apt-get update -y && apt-get install -y gcc gcc-riscv64-linux-gnu ca-certificates cmake git
+RUN git config --global --add safe.directory '*'
 
 ENV CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER=riscv64-linux-gnu-gcc
diff --git a/ci/docker/s390x-linux/Dockerfile b/ci/docker/s390x-linux/Dockerfile
index efb6e982b..6c97b174d 100644
--- a/ci/docker/s390x-linux/Dockerfile
+++ b/ci/docker/s390x-linux/Dockerfile
@@ -1,6 +1,7 @@
 FROM ubuntu:16.04
 
 RUN apt-get update -y && apt-get install -y gcc gcc-s390x-linux-gnu ca-certificates curl make git
+RUN git config --global --add safe.directory '*'
 
 # The CMake in Ubuntu 16.04 was a bit too old for us to use so download one from
 # CMake's own releases and use that instead.
diff --git a/ci/docker/x86_64-linux/Dockerfile b/ci/docker/x86_64-linux/Dockerfile
index 784a548db..add422150 100644
--- a/ci/docker/x86_64-linux/Dockerfile
+++ b/ci/docker/x86_64-linux/Dockerfile
@@ -1,3 +1,4 @@
 FROM almalinux:8
 
 RUN dnf install -y git gcc make cmake git
+RUN git config --global --add safe.directory '*'
diff --git a/ci/docker/x86_64-musl/Dockerfile b/ci/docker/x86_64-musl/Dockerfile
index d823bafb9..acd6dd88d 100644
--- a/ci/docker/x86_64-musl/Dockerfile
+++ b/ci/docker/x86_64-musl/Dockerfile
@@ -8,6 +8,7 @@ RUN apk add libgcc
 FROM ubuntu:24.04
 RUN apt-get update -y && apt-get install -y cmake musl-tools git
 COPY --from=libgcc_s_src /usr/lib/libgcc_s.so.1 /usr/lib/x86_64-linux-musl
+RUN git config --global --add safe.directory '*'
 
 # Note that `-crt-feature` is passed here to specifically disable static linking
 # with musl. We want a `*.so` to pop out so static linking isn't what we want.
diff --git a/cranelift/codegen/meta/src/shared/instructions.rs b/cranelift/codegen/meta/src/shared/instructions.rs
index 862ae8271..c0f62954f 100644
--- a/cranelift/codegen/meta/src/shared/instructions.rs
+++ b/cranelift/codegen/meta/src/shared/instructions.rs
@@ -915,6 +915,66 @@ pub(crate) fn define(
         ])
         .can_store(),
     );
+    ig.push(
+        Inst::new(
+            "stack_switch",
+            r#"
+        Suspends execution of the current stack and resumes execution of another
+        one.
+
+        The target stack to switch to is identified by the data stored at
+        ``load_context_ptr``. Before switching, this instruction stores
+        analogous information about the
+        current (i.e., original) stack at ``store_context_ptr``, to
+        enabled switching back to the original stack at a later point.
+
+        The size, alignment and layout of the information stored at
+        ``load_context_ptr`` and ``store_context_ptr`` is platform-dependent.
+        The instruction assumes that ``load_context_ptr`` and
+        ``store_context_ptr`` are valid pointers to memory with said layout and
+        alignment, and does not perform any checks on these pointers or the data
+        stored there.
+
+        The instruction is experimental and only supported on x64 Linux at the
+        moment.
+
+        When switching from a stack A to a stack B, one of the following cases
+        must apply:
+        1. Stack B was previously suspended using a ``stack_switch`` instruction.
+        2. Stack B is a newly initialized stack. The necessary initialization is
+        platform-dependent and will generally involve running some kind of
+        trampoline to start execution of a function on the new stack.
+
+        In both cases, the ``in_payload`` argument of the ``stack_switch``
+        instruction executed on A is passed to stack B. In the first case above,
+        it will be the result value of the earlier ``stack_switch`` instruction
+        executed on stack B. In the second case, the value will be accessible to
+        the trampoline in a platform-dependent register.
+
+        The pointers ``load_context_ptr`` and ``store_context_ptr`` are allowed
+        to be equal; the instruction ensures that all data is loaded from the
+        former before writing to the latter.
+
+        Stack switching is one-shot in the sense that each ``stack_switch``
+        operation effectively consumes the context identified by
+        ``load_context_ptr``. In other words, performing two ``stack_switches``
+        using the same ``load_context_ptr`` causes undefined behavior, unless
+        the context at ``load_context_ptr`` is overwritten by another
+        `stack_switch` in between.
+        "#,
+            &formats.ternary,
+        )
+        .operands_in(vec![
+            Operand::new("store_context_ptr", iAddr),
+            Operand::new("load_context_ptr", iAddr),
+            Operand::new("in_payload0", iAddr),
+        ])
+        .operands_out(vec![Operand::new("out_payload0", iAddr)])
+        .other_side_effects()
+        .can_load()
+        .can_store()
+        .call(),
+    );
 
     let I16x8 = &TypeVar::new(
         "I16x8",
diff --git a/cranelift/codegen/meta/src/shared/settings.rs b/cranelift/codegen/meta/src/shared/settings.rs
index 65ff05be8..b6aa2c02d 100644
--- a/cranelift/codegen/meta/src/shared/settings.rs
+++ b/cranelift/codegen/meta/src/shared/settings.rs
@@ -154,6 +154,19 @@ pub(crate) fn define() -> SettingGroup {
         vec!["none", "elf_gd", "macho", "coff"],
     );
 
+    settings.add_enum(
+        "stack_switch_model",
+        "Defines the model used to performing stack switching.",
+        r#"
+           This determines the compilation of `stack_switch` instructions. If
+           set to `basic`, we simply save all registers, update stack pointer
+           and frame pointer (if needed), and jump to the target IP.
+           If set to `update_windows_tib`, we *additionally* update information
+           about the active stack in Windows' Thread Information Block.
+        "#,
+        vec!["none", "basic", "update_windows_tib"],
+    );
+
     settings.add_enum(
         "libcall_call_conv",
         "Defines the calling convention to use for LibCalls call expansion.",
diff --git a/cranelift/codegen/src/binemit/mod.rs b/cranelift/codegen/src/binemit/mod.rs
index 498459026..edd4d7d7c 100644
--- a/cranelift/codegen/src/binemit/mod.rs
+++ b/cranelift/codegen/src/binemit/mod.rs
@@ -3,9 +3,6 @@
 //! The `binemit` module contains code for translating Cranelift's intermediate representation into
 //! binary machine code.
 
-mod stack_map;
-
-pub use self::stack_map::StackMap;
 use core::fmt;
 #[cfg(feature = "enable-serde")]
 use serde_derive::{Deserialize, Serialize};
diff --git a/cranelift/codegen/src/binemit/stack_map.rs b/cranelift/codegen/src/binemit/stack_map.rs
deleted file mode 100644
index b5d1b694d..000000000
--- a/cranelift/codegen/src/binemit/stack_map.rs
+++ /dev/null
@@ -1,141 +0,0 @@
-use cranelift_bitset::CompoundBitSet;
-
-/// Stack maps record which words in a stack frame contain live GC references at
-/// a given instruction pointer.
-///
-/// Logically, a set of stack maps for a function record a table of the form:
-///
-/// ```text
-/// +---------------------+-------------------------------------------+
-/// | Instruction Pointer | SP-Relative Offsets of Live GC References |
-/// +---------------------+-------------------------------------------+
-/// | 0x12345678          | 2, 6, 12                                  |
-/// | 0x1234abcd          | 2, 6                                      |
-/// | ...                 | ...                                       |
-/// +---------------------+-------------------------------------------+
-/// ```
-///
-/// Where "instruction pointer" is an instruction pointer within the function,
-/// and "offsets of live GC references" contains the offsets (in units of words)
-/// from the frame's stack pointer where live GC references are stored on the
-/// stack. Instruction pointers within the function that do not have an entry in
-/// this table are not GC safepoints.
-///
-/// Because
-///
-/// * offsets of live GC references are relative from the stack pointer, and
-/// * stack frames grow down from higher addresses to lower addresses,
-///
-/// to get a pointer to a live reference at offset `x` within a stack frame, you
-/// add `x` from the frame's stack pointer.
-///
-/// For example, to calculate the pointer to the live GC reference inside "frame
-/// 1" below, you would do `frame_1_sp + x`:
-///
-/// ```text
-///           Stack
-///         +-------------------+
-///         | Frame 0           |
-///         |                   |
-///    |    |                   |
-///    |    +-------------------+ <--- Frame 0's SP
-///    |    | Frame 1           |
-///  Grows  |                   |
-///  down   |                   |
-///    |    | Live GC reference | --+--
-///    |    |                   |   |
-///    |    |                   |   |
-///    V    |                   |   x = offset of live GC reference
-///         |                   |   |
-///         |                   |   |
-///         +-------------------+ --+--  <--- Frame 1's SP
-///         | Frame 2           |
-///         | ...               |
-/// ```
-///
-/// An individual `StackMap` is associated with just one instruction pointer
-/// within the function, contains the size of the stack frame, and represents
-/// the stack frame as a bitmap. There is one bit per word in the stack frame,
-/// and if the bit is set, then the word contains a live GC reference.
-///
-/// Note that a caller's `OutgoingArg` stack slots and callee's `IncomingArg`
-/// stack slots overlap, so we must choose which function's stack maps record
-/// live GC references in these slots. We record the `IncomingArg`s in the
-/// callee's stack map.
-#[derive(Clone, Debug, PartialEq, Eq)]
-#[cfg_attr(
-    feature = "enable-serde",
-    derive(serde_derive::Deserialize, serde_derive::Serialize)
-)]
-pub struct StackMap {
-    bitset: CompoundBitSet,
-    mapped_words: u32,
-}
-
-impl StackMap {
-    /// Create a stack map from a slice of booleans.
-    pub fn from_slice(bools: &[bool]) -> Self {
-        let mut bitset = CompoundBitSet::with_capacity(bools.len());
-        for (i, b) in bools.iter().enumerate() {
-            if *b {
-                bitset.insert(i);
-            }
-        }
-        Self {
-            mapped_words: u32::try_from(bools.len()).unwrap(),
-            bitset,
-        }
-    }
-
-    /// Returns a specified bit.
-    pub fn get_bit(&self, bit_index: usize) -> bool {
-        self.bitset.contains(bit_index)
-    }
-
-    /// Returns the raw bitmap that represents this stack map.
-    pub fn into_bitset(self) -> CompoundBitSet {
-        self.bitset
-    }
-
-    /// Returns the number of words represented by this stack map.
-    pub fn mapped_words(&self) -> u32 {
-        self.mapped_words
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use alloc::vec::Vec;
-
-    type Num = u32;
-    const NUM_BITS: usize = core::mem::size_of::<Num>() * 8;
-
-    #[test]
-    fn stack_maps() {
-        let vec: Vec<bool> = Vec::new();
-        assert!(StackMap::from_slice(&vec).bitset.is_empty());
-
-        let mut vec: [bool; NUM_BITS] = Default::default();
-        let set_true_idx = [5, 7, 24, 31];
-
-        for &idx in &set_true_idx {
-            vec[idx] = true;
-        }
-
-        let mut vec = vec.to_vec();
-        let stack_map = StackMap::from_slice(&vec);
-        for idx in 0..32 {
-            assert_eq!(stack_map.get_bit(idx), set_true_idx.contains(&idx));
-        }
-
-        vec.push(false);
-        vec.push(true);
-        let res = StackMap::from_slice(&vec);
-        for idx in 0..32 {
-            assert_eq!(stack_map.get_bit(idx), set_true_idx.contains(&idx));
-        }
-        assert!(!res.get_bit(32));
-        assert!(res.get_bit(33));
-    }
-}
diff --git a/cranelift/codegen/src/ir/instructions.rs b/cranelift/codegen/src/ir/instructions.rs
index 068b3e1e2..069f2adc3 100644
--- a/cranelift/codegen/src/ir/instructions.rs
+++ b/cranelift/codegen/src/ir/instructions.rs
@@ -450,6 +450,14 @@ impl InstructionData {
             Self::CallIndirect {
                 sig_ref, ref args, ..
             } => CallInfo::Indirect(sig_ref, &args.as_slice(pool)[1..]),
+            Self::Ternary {
+                opcode: Opcode::StackSwitch,
+                ..
+            } => {
+                // `StackSwitch` is not actually a call, but has the .call() side
+                // effect as it continues execution elsewhere.
+                CallInfo::NotACall
+            }
             _ => {
                 debug_assert!(!self.opcode().is_call());
                 CallInfo::NotACall
diff --git a/cranelift/codegen/src/ir/user_stack_maps.rs b/cranelift/codegen/src/ir/user_stack_maps.rs
index 7378c3747..fe495a814 100644
--- a/cranelift/codegen/src/ir/user_stack_maps.rs
+++ b/cranelift/codegen/src/ir/user_stack_maps.rs
@@ -27,6 +27,68 @@
 //! `cranelift_codegen::ir::DataFlowGraph::append_user_stack_map_entry`). Cranelift
 //! will not insert spills and record these stack map entries automatically (in
 //! contrast to the old system and its `r64` values).
+//!
+//! Logically, a set of stack maps for a function record a table of the form:
+//!
+//! ```text
+//! +---------------------+-------------------------------------------+
+//! | Instruction Pointer | SP-Relative Offsets of Live GC References |
+//! +---------------------+-------------------------------------------+
+//! | 0x12345678          | 2, 6, 12                                  |
+//! | 0x1234abcd          | 2, 6                                      |
+//! | ...                 | ...                                       |
+//! +---------------------+-------------------------------------------+
+//! ```
+//!
+//! Where "instruction pointer" is an instruction pointer within the function,
+//! and "offsets of live GC references" contains the offsets (in units of words)
+//! from the frame's stack pointer where live GC references are stored on the
+//! stack. Instruction pointers within the function that do not have an entry in
+//! this table are not GC safepoints.
+//!
+//! Because
+//!
+//! * offsets of live GC references are relative from the stack pointer, and
+//! * stack frames grow down from higher addresses to lower addresses,
+//!
+//! to get a pointer to a live reference at offset `x` within a stack frame, you
+//! add `x` to the frame's stack pointer.
+//!
+//! For example, to calculate the pointer to the live GC reference inside "frame
+//! 1" below, you would do `frame_1_sp + x`:
+//!
+//! ```text
+//!           Stack
+//!         +-------------------+
+//!         | Frame 0           |
+//!         |                   |
+//!    |    |                   |
+//!    |    +-------------------+ <--- Frame 0's SP
+//!    |    | Frame 1           |
+//!  Grows  |                   |
+//!  down   |                   |
+//!    |    | Live GC reference | --+--
+//!    |    |                   |   |
+//!    |    |                   |   |
+//!    V    |                   |   x = offset of live GC reference
+//!         |                   |   |
+//!         |                   |   |
+//!         +-------------------+ --+--  <--- Frame 1's SP
+//!         | Frame 2           |
+//!         | ...               |
+//! ```
+//!
+//! An individual `UserStackMap` is associated with just one instruction pointer
+//! within the function, contains the size of the stack frame, and represents
+//! the stack frame as a bitmap. There is one bit per word in the stack frame,
+//! and if the bit is set, then the word contains a live GC reference.
+//!
+//! Note that a caller's outgoing argument stack slots (if any) and callee's
+//! incoming argument stack slots (if any) overlap, so we must choose which
+//! function's stack maps record live GC references in these slots. We record
+//! the incoming arguments in the callee's stack map. This choice plays nice
+//! with tail calls, where by the time we transfer control to the callee, the
+//! caller no longer exists.
 
 use crate::ir;
 use cranelift_bitset::CompoundBitSet;
diff --git a/cranelift/codegen/src/isa/aarch64/inst/emit.rs b/cranelift/codegen/src/isa/aarch64/inst/emit.rs
index d458f0d18..8f76dd56a 100644
--- a/cranelift/codegen/src/isa/aarch64/inst/emit.rs
+++ b/cranelift/codegen/src/isa/aarch64/inst/emit.rs
@@ -2,7 +2,6 @@
 
 use cranelift_control::ControlPlane;
 
-use crate::binemit::StackMap;
 use crate::ir::{self, types::*};
 use crate::isa::aarch64::inst::*;
 use crate::trace;
@@ -651,10 +650,6 @@ fn enc_asimd_mod_imm(rd: Writable<Reg>, q_op: u32, cmode: u32, imm: u8) -> u32 {
 /// State carried between emissions of a sequence of instructions.
 #[derive(Default, Clone, Debug)]
 pub struct EmitState {
-    /// Safepoint stack map for upcoming instruction, as provided to
-    /// `pre_safepoint()`.
-    stack_map: Option<StackMap>,
-
     /// The user stack map for the upcoming instruction, as provided to
     /// `pre_safepoint()`.
     user_stack_map: Option<ir::UserStackMap>,
@@ -669,19 +664,13 @@ pub struct EmitState {
 impl MachInstEmitState<Inst> for EmitState {
     fn new(abi: &Callee<AArch64MachineDeps>, ctrl_plane: ControlPlane) -> Self {
         EmitState {
-            stack_map: None,
             user_stack_map: None,
             ctrl_plane,
             frame_layout: abi.frame_layout().clone(),
         }
     }
 
-    fn pre_safepoint(
-        &mut self,
-        stack_map: Option<StackMap>,
-        user_stack_map: Option<ir::UserStackMap>,
-    ) {
-        self.stack_map = stack_map;
+    fn pre_safepoint(&mut self, user_stack_map: Option<ir::UserStackMap>) {
         self.user_stack_map = user_stack_map;
     }
 
@@ -699,12 +688,12 @@ impl MachInstEmitState<Inst> for EmitState {
 }
 
 impl EmitState {
-    fn take_stack_map(&mut self) -> (Option<StackMap>, Option<ir::UserStackMap>) {
-        (self.stack_map.take(), self.user_stack_map.take())
+    fn take_stack_map(&mut self) -> Option<ir::UserStackMap> {
+        self.user_stack_map.take()
     }
 
     fn clear_post_insn(&mut self) {
-        self.stack_map = None;
+        self.user_stack_map = None;
     }
 }
 
@@ -2935,10 +2924,7 @@ impl MachInstEmit for Inst {
                 }
             }
             &Inst::Call { ref info } => {
-                let (stack_map, user_stack_map) = state.take_stack_map();
-                if let Some(s) = stack_map {
-                    sink.add_stack_map(StackMapExtent::UpcomingBytes(4), s);
-                }
+                let user_stack_map = state.take_stack_map();
                 sink.add_reloc(Reloc::Arm64Call, &info.dest, 0);
                 sink.put4(enc_jump26(0b100101, 0));
                 if let Some(s) = user_stack_map {
@@ -2956,10 +2942,7 @@ impl MachInstEmit for Inst {
                 }
             }
             &Inst::CallInd { ref info } => {
-                let (stack_map, user_stack_map) = state.take_stack_map();
-                if let Some(s) = stack_map {
-                    sink.add_stack_map(StackMapExtent::UpcomingBytes(4), s);
-                }
+                let user_stack_map = state.take_stack_map();
                 let rn = info.rn;
                 sink.put4(0b1101011_0001_11111_000000_00000_00000 | (machreg_to_gpr(rn) << 5));
                 if let Some(s) = user_stack_map {
diff --git a/cranelift/codegen/src/isa/pulley_shared/inst/emit.rs b/cranelift/codegen/src/isa/pulley_shared/inst/emit.rs
index c94ee8226..7470bcad6 100644
--- a/cranelift/codegen/src/isa/pulley_shared/inst/emit.rs
+++ b/cranelift/codegen/src/isa/pulley_shared/inst/emit.rs
@@ -1,7 +1,6 @@
 //! Pulley binary code emission.
 
 use super::*;
-use crate::binemit::StackMap;
 use crate::ir;
 use crate::isa::pulley_shared::abi::PulleyMachineDeps;
 use crate::isa::pulley_shared::PointerWidth;
@@ -40,7 +39,6 @@ where
 {
     _phantom: PhantomData<P>,
     ctrl_plane: ControlPlane,
-    stack_map: Option<StackMap>,
     user_stack_map: Option<ir::UserStackMap>,
     pub virtual_sp_offset: i64,
     frame_layout: FrameLayout,
@@ -50,8 +48,8 @@ impl<P> EmitState<P>
 where
     P: PulleyTargetKind,
 {
-    fn take_stack_map(&mut self) -> (Option<StackMap>, Option<ir::UserStackMap>) {
-        (self.stack_map.take(), self.user_stack_map.take())
+    fn take_stack_map(&mut self) -> Option<ir::UserStackMap> {
+        self.user_stack_map.take()
     }
 
     pub(crate) fn adjust_virtual_sp_offset(&mut self, amount: i64) {
@@ -70,19 +68,13 @@ where
         EmitState {
             _phantom: PhantomData,
             ctrl_plane,
-            stack_map: None,
             user_stack_map: None,
             virtual_sp_offset: 0,
             frame_layout: abi.frame_layout().clone(),
         }
     }
 
-    fn pre_safepoint(
-        &mut self,
-        stack_map: Option<StackMap>,
-        user_stack_map: Option<ir::UserStackMap>,
-    ) {
-        self.stack_map = stack_map;
+    fn pre_safepoint(&mut self, user_stack_map: Option<ir::UserStackMap>) {
         self.user_stack_map = user_stack_map;
     }
 
@@ -156,10 +148,6 @@ fn pulley_emit<P>(
         Inst::LoadExtName { .. } => todo!(),
 
         Inst::Call { callee, info } => {
-            let (stack_map, user_stack_map) = state.take_stack_map();
-            if let Some(s) = stack_map {
-                sink.add_stack_map(StackMapExtent::UpcomingBytes(5), s);
-            }
             sink.put1(pulley_interpreter::Opcode::Call as u8);
             sink.add_reloc(
                 // TODO: is it actually okay to reuse this reloc here?
@@ -170,7 +158,7 @@ fn pulley_emit<P>(
                 -1,
             );
             sink.put4(0);
-            if let Some(s) = user_stack_map {
+            if let Some(s) = state.take_stack_map() {
                 let offset = sink.cur_offset();
                 sink.push_user_stack_map(state, offset, s);
             }
diff --git a/cranelift/codegen/src/isa/riscv64/inst/emit.rs b/cranelift/codegen/src/isa/riscv64/inst/emit.rs
index 8093f8509..bbce4ec90 100644
--- a/cranelift/codegen/src/isa/riscv64/inst/emit.rs
+++ b/cranelift/codegen/src/isa/riscv64/inst/emit.rs
@@ -1,6 +1,5 @@
 //! Riscv64 ISA: binary code emission.
 
-use crate::binemit::StackMap;
 use crate::ir::{self, LibCall, TrapCode};
 use crate::isa::riscv64::inst::*;
 use crate::isa::riscv64::lower::isle::generated_code::{
@@ -46,10 +45,6 @@ pub enum EmitVState {
 /// State carried between emissions of a sequence of instructions.
 #[derive(Default, Clone, Debug)]
 pub struct EmitState {
-    /// Safepoint stack map for upcoming instruction, as provided to
-    /// `pre_safepoint()`.
-    stack_map: Option<StackMap>,
-
     /// The user stack map for the upcoming instruction, as provided to
     /// `pre_safepoint()`.
     user_stack_map: Option<ir::UserStackMap>,
@@ -66,8 +61,8 @@ pub struct EmitState {
 }
 
 impl EmitState {
-    fn take_stack_map(&mut self) -> (Option<StackMap>, Option<ir::UserStackMap>) {
-        (self.stack_map.take(), self.user_stack_map.take())
+    fn take_stack_map(&mut self) -> Option<ir::UserStackMap> {
+        self.user_stack_map.take()
     }
 }
 
@@ -77,7 +72,6 @@ impl MachInstEmitState<Inst> for EmitState {
         ctrl_plane: ControlPlane,
     ) -> Self {
         EmitState {
-            stack_map: None,
             user_stack_map: None,
             ctrl_plane,
             vstate: EmitVState::Unknown,
@@ -85,12 +79,7 @@ impl MachInstEmitState<Inst> for EmitState {
         }
     }
 
-    fn pre_safepoint(
-        &mut self,
-        stack_map: Option<StackMap>,
-        user_stack_map: Option<ir::UserStackMap>,
-    ) {
-        self.stack_map = stack_map;
+    fn pre_safepoint(&mut self, user_stack_map: Option<ir::UserStackMap>) {
         self.user_stack_map = user_stack_map;
     }
 
@@ -1129,16 +1118,11 @@ impl Inst {
                 sink.add_call_site();
                 sink.add_reloc(Reloc::RiscvCallPlt, &info.dest, 0);
 
-                let (stack_map, user_stack_map) = state.take_stack_map();
-                if let Some(s) = stack_map {
-                    sink.add_stack_map(StackMapExtent::UpcomingBytes(8), s);
-                }
-
                 Inst::construct_auipc_and_jalr(Some(writable_link_reg()), writable_link_reg(), 0)
                     .into_iter()
                     .for_each(|i| i.emit_uncompressed(sink, emit_info, state, start_off));
 
-                if let Some(s) = user_stack_map {
+                if let Some(s) = state.take_stack_map() {
                     let offset = sink.cur_offset();
                     sink.push_user_stack_map(state, offset, s);
                 }
@@ -1151,8 +1135,6 @@ impl Inst {
                 }
             }
             &Inst::CallInd { ref info } => {
-                let start_offset = sink.cur_offset();
-
                 Inst::Jalr {
                     rd: writable_link_reg(),
                     base: info.rn,
@@ -1160,11 +1142,7 @@ impl Inst {
                 }
                 .emit(sink, emit_info, state);
 
-                let (stack_map, user_stack_map) = state.take_stack_map();
-                if let Some(s) = stack_map {
-                    sink.add_stack_map(StackMapExtent::StartedAtOffset(start_offset), s);
-                }
-                if let Some(s) = user_stack_map {
+                if let Some(s) = state.take_stack_map() {
                     let offset = sink.cur_offset();
                     sink.push_user_stack_map(state, offset, s);
                 }
diff --git a/cranelift/codegen/src/isa/s390x/inst/emit.rs b/cranelift/codegen/src/isa/s390x/inst/emit.rs
index 5f559ace1..7d9fcc3e7 100644
--- a/cranelift/codegen/src/isa/s390x/inst/emit.rs
+++ b/cranelift/codegen/src/isa/s390x/inst/emit.rs
@@ -1,6 +1,5 @@
 //! S390x ISA: binary code emission.
 
-use crate::binemit::StackMap;
 use crate::ir::{self, MemFlags, TrapCode};
 use crate::isa::s390x::inst::*;
 use crate::isa::s390x::settings as s390x_settings;
@@ -1320,10 +1319,6 @@ pub struct EmitState {
     /// ABI, between the AllocateArgs and the actual call instruction.
     pub(crate) nominal_sp_offset: u32,
 
-    /// Safepoint stack map for upcoming instruction, as provided to
-    /// `pre_safepoint()`.
-    stack_map: Option<StackMap>,
-
     /// The user stack map for the upcoming instruction, as provided to
     /// `pre_safepoint()`.
     user_stack_map: Option<ir::UserStackMap>,
@@ -1339,19 +1334,13 @@ impl MachInstEmitState<Inst> for EmitState {
     fn new(abi: &Callee<S390xMachineDeps>, ctrl_plane: ControlPlane) -> Self {
         EmitState {
             nominal_sp_offset: 0,
-            stack_map: None,
             user_stack_map: None,
             ctrl_plane,
             frame_layout: abi.frame_layout().clone(),
         }
     }
 
-    fn pre_safepoint(
-        &mut self,
-        stack_map: Option<StackMap>,
-        user_stack_map: Option<ir::UserStackMap>,
-    ) {
-        self.stack_map = stack_map;
+    fn pre_safepoint(&mut self, user_stack_map: Option<ir::UserStackMap>) {
         self.user_stack_map = user_stack_map;
     }
 
@@ -1369,12 +1358,12 @@ impl MachInstEmitState<Inst> for EmitState {
 }
 
 impl EmitState {
-    fn take_stack_map(&mut self) -> (Option<StackMap>, Option<ir::UserStackMap>) {
-        (self.stack_map.take(), self.user_stack_map.take())
+    fn take_stack_map(&mut self) -> Option<ir::UserStackMap> {
+        self.user_stack_map.take()
     }
 
     fn clear_post_insn(&mut self) {
-        self.stack_map = None;
+        self.user_stack_map = None;
     }
 }
 
@@ -3287,11 +3276,7 @@ impl Inst {
                     _ => unreachable!(),
                 }
 
-                let (stack_map, user_stack_map) = state.take_stack_map();
-                if let Some(s) = stack_map {
-                    sink.add_stack_map(StackMapExtent::UpcomingBytes(6), s);
-                }
-                if let Some(s) = user_stack_map {
+                if let Some(s) = state.take_stack_map() {
                     let offset = sink.cur_offset() + 6;
                     sink.push_user_stack_map(state, offset, s);
                 }
@@ -3305,11 +3290,7 @@ impl Inst {
                 debug_assert_eq!(link.to_reg(), gpr(14));
                 let rn = info.rn;
 
-                let (stack_map, user_stack_map) = state.take_stack_map();
-                if let Some(s) = stack_map {
-                    sink.add_stack_map(StackMapExtent::UpcomingBytes(2), s);
-                }
-                if let Some(s) = user_stack_map {
+                if let Some(s) = state.take_stack_map() {
                     let offset = sink.cur_offset() + 2;
                     sink.push_user_stack_map(state, offset, s);
                 }
diff --git a/cranelift/codegen/src/isa/x64/abi.rs b/cranelift/codegen/src/isa/x64/abi.rs
index 211934a03..9d3c9fd55 100644
--- a/cranelift/codegen/src/isa/x64/abi.rs
+++ b/cranelift/codegen/src/isa/x64/abi.rs
@@ -1257,7 +1257,7 @@ fn compute_clobber_size(clobbers: &[Writable<RealReg>]) -> u32 {
 
 const WINDOWS_CLOBBERS: PRegSet = windows_clobbers();
 const SYSV_CLOBBERS: PRegSet = sysv_clobbers();
-const ALL_CLOBBERS: PRegSet = all_clobbers();
+pub(crate) const ALL_CLOBBERS: PRegSet = all_clobbers();
 
 const fn windows_clobbers() -> PRegSet {
     PRegSet::empty()
diff --git a/cranelift/codegen/src/isa/x64/inst.isle b/cranelift/codegen/src/isa/x64/inst.isle
index e104548a4..30377633e 100644
--- a/cranelift/codegen/src/isa/x64/inst.isle
+++ b/cranelift/codegen/src/isa/x64/inst.isle
@@ -557,6 +557,12 @@
        ;; Return.
        (Ret (stack_bytes_to_pop u32))
 
+       ;; Stack switching
+       (StackSwitchBasic (store_context_ptr Gpr)
+                         (load_context_ptr Gpr)
+                         (in_payload0 Gpr)
+                         (out_payload0 WritableGpr))
+
        ;; Jump to a known target: jmp simm32.
        (JmpKnown (dst MachLabel))
 
@@ -2288,6 +2294,17 @@
 (decl gen_call_indirect (SigRef Value ValueSlice) InstOutput)
 (extern constructor gen_call_indirect gen_call_indirect)
 
+;;;; Helpers for emitting stack switches ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(decl x64_stack_switch_basic (Gpr Gpr Gpr) Gpr)
+(rule (x64_stack_switch_basic store_context_ptr load_context_ptr in_payload0)
+      (let ((out_payload0 WritableGpr (temp_writable_gpr))
+            (_ Unit (emit (MInst.StackSwitchBasic store_context_ptr
+                                                  load_context_ptr
+                                                  in_payload0
+                                                  out_payload0))))
+        out_payload0))
+
 ;;;; Helpers for Emitting Loads ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
 ;; Load a value into a register.
diff --git a/cranelift/codegen/src/isa/x64/inst/emit.rs b/cranelift/codegen/src/isa/x64/inst/emit.rs
index 0cc127724..9bc374ce5 100644
--- a/cranelift/codegen/src/isa/x64/inst/emit.rs
+++ b/cranelift/codegen/src/isa/x64/inst/emit.rs
@@ -1598,11 +1598,7 @@ pub(crate) fn emit(
             info: call_info,
             ..
         } => {
-            let (stack_map, user_stack_map) = state.take_stack_map();
-            if let Some(s) = stack_map {
-                sink.add_stack_map(StackMapExtent::UpcomingBytes(5), s);
-            }
-            if let Some(s) = user_stack_map {
+            if let Some(s) = state.take_stack_map() {
                 let offset = sink.cur_offset() + 5;
                 sink.push_user_stack_map(state, offset, s);
             }
@@ -1670,7 +1666,6 @@ pub(crate) fn emit(
         } => {
             let dest = dest.clone();
 
-            let start_offset = sink.cur_offset();
             match dest {
                 RegMem::Reg { reg } => {
                     let reg_enc = int_reg_enc(reg);
@@ -1700,11 +1695,7 @@ pub(crate) fn emit(
                 }
             }
 
-            let (stack_map, user_stack_map) = state.take_stack_map();
-            if let Some(s) = stack_map {
-                sink.add_stack_map(StackMapExtent::StartedAtOffset(start_offset), s);
-            }
-            if let Some(s) = user_stack_map {
+            if let Some(s) = state.take_stack_map() {
                 let offset = sink.cur_offset();
                 sink.push_user_stack_map(state, offset, s);
             }
@@ -1738,6 +1729,123 @@ pub(crate) fn emit(
             sink.put2(u16::try_from(*stack_bytes_to_pop).unwrap());
         }
 
+        Inst::StackSwitchBasic {
+            store_context_ptr,
+            load_context_ptr,
+            in_payload0,
+            out_payload0,
+        } => {
+            // Note that we do not emit anything for preserving and restoring
+            // ordinary registers here: That's taken care of by regalloc for us,
+            // since we marked this instruction as clobbering all registers.
+            //
+            // Also note that we do nothing about passing the single payload
+            // value: We've informed regalloc that it is sent and received via
+            // the fixed register given by [stack_switch::payload_register]
+
+            let (tmp1, tmp2) = {
+                // Ideally we would just ask regalloc for two temporary registers.
+                // However, adding any early defs to the constraints on StackSwitch
+                // causes TooManyLiveRegs. Fortunately, we can manually find tmp
+                // registers without regalloc: Since our instruction clobbers all
+                // registers, we can simply pick any register that is not assigned
+                // to the operands.
+
+                let all = crate::isa::x64::abi::ALL_CLOBBERS;
+
+                let used_regs = [
+                    **load_context_ptr,
+                    **store_context_ptr,
+                    **in_payload0,
+                    *out_payload0.to_reg(),
+                ];
+
+                let mut tmps = all.into_iter().filter_map(|preg| {
+                    let reg: Reg = preg.into();
+                    if !used_regs.contains(&reg) {
+                        WritableGpr::from_writable_reg(isle::WritableReg::from_reg(reg))
+                    } else {
+                        None
+                    }
+                });
+                (tmps.next().unwrap(), tmps.next().unwrap())
+            };
+
+            let layout = stack_switch::control_context_layout();
+            let rsp_offset = layout.stack_pointer_offset as i32;
+            let pc_offset = layout.ip_offset as i32;
+            let rbp_offset = layout.frame_pointer_offset as i32;
+
+            // Location to which someone switch-ing back to this stack will jump
+            // to: Right behind the `StackSwitch` instruction
+            let resume = sink.get_label();
+
+            //
+            // For RBP and RSP we do the following:
+            // - Load new value for register from `load_context_ptr` +
+            // corresponding offset.
+            // - Store previous (!) value of register at `store_context_ptr` +
+            // corresponding offset.
+            //
+            // Since `load_context_ptr` and `store_context_ptr` are allowed to be
+            // equal, we need to use a temporary register here.
+            //
+
+            let mut exchange = |offset, reg| {
+                let inst = Inst::Mov64MR {
+                    src: Amode::imm_reg(offset, **load_context_ptr).into(),
+                    dst: tmp1,
+                };
+                emit(&inst, sink, info, state);
+
+                let inst = Inst::MovRM {
+                    size: OperandSize::Size64,
+                    src: Gpr::new(reg).unwrap(),
+                    dst: Amode::imm_reg(offset, **store_context_ptr).into(),
+                };
+                emit(&inst, sink, info, state);
+
+                let dst = Writable::from_reg(reg.into());
+                let inst = Inst::MovRR {
+                    size: OperandSize::Size64,
+                    src: tmp1.to_reg(),
+                    dst: WritableGpr::from_writable_reg(dst.into()).unwrap(),
+                };
+                emit(&inst, sink, info, state);
+            };
+
+            exchange(rsp_offset, regs::rsp());
+            exchange(rbp_offset, regs::rbp());
+
+            //
+            // Load target PC, store resume PC, jump to target PC
+            //
+
+            let inst = Inst::Mov64MR {
+                src: Amode::imm_reg(pc_offset, **load_context_ptr).into(),
+                dst: tmp1,
+            };
+            emit(&inst, sink, info, state);
+
+            let amode = Amode::RipRelative { target: resume };
+            let inst = Inst::lea(amode, tmp2.map(Reg::from));
+            inst.emit(sink, info, state);
+
+            let inst = Inst::MovRM {
+                size: OperandSize::Size64,
+                src: tmp2.to_reg(),
+                dst: Amode::imm_reg(pc_offset, **store_context_ptr).into(),
+            };
+            emit(&inst, sink, info, state);
+
+            let inst = Inst::JmpUnknown {
+                target: RegMem::reg(tmp1.to_reg().into()),
+            };
+            emit(&inst, sink, info, state);
+
+            sink.bind_label(resume, state.ctrl_plane_mut());
+        }
+
         Inst::JmpKnown { dst } => {
             let br_start = sink.cur_offset();
             let br_disp_off = br_start + 1;
diff --git a/cranelift/codegen/src/isa/x64/inst/emit_state.rs b/cranelift/codegen/src/isa/x64/inst/emit_state.rs
index 9cca7d789..c28776b90 100644
--- a/cranelift/codegen/src/isa/x64/inst/emit_state.rs
+++ b/cranelift/codegen/src/isa/x64/inst/emit_state.rs
@@ -5,10 +5,6 @@ use cranelift_control::ControlPlane;
 /// State carried between emissions of a sequence of instructions.
 #[derive(Default, Clone, Debug)]
 pub struct EmitState {
-    /// Safepoint stack map for upcoming instruction, as provided to
-    /// `pre_safepoint()`.
-    stack_map: Option<StackMap>,
-
     /// The user stack map for the upcoming instruction, as provided to
     /// `pre_safepoint()`.
     user_stack_map: Option<ir::UserStackMap>,
@@ -25,19 +21,13 @@ pub struct EmitState {
 impl MachInstEmitState<Inst> for EmitState {
     fn new(abi: &Callee<X64ABIMachineSpec>, ctrl_plane: ControlPlane) -> Self {
         EmitState {
-            stack_map: None,
             user_stack_map: None,
             ctrl_plane,
             frame_layout: abi.frame_layout().clone(),
         }
     }
 
-    fn pre_safepoint(
-        &mut self,
-        stack_map: Option<StackMap>,
-        user_stack_map: Option<ir::UserStackMap>,
-    ) {
-        self.stack_map = stack_map;
+    fn pre_safepoint(&mut self, user_stack_map: Option<ir::UserStackMap>) {
         self.user_stack_map = user_stack_map;
     }
 
@@ -55,11 +45,11 @@ impl MachInstEmitState<Inst> for EmitState {
 }
 
 impl EmitState {
-    pub(crate) fn take_stack_map(&mut self) -> (Option<StackMap>, Option<ir::UserStackMap>) {
-        (self.stack_map.take(), self.user_stack_map.take())
+    pub(crate) fn take_stack_map(&mut self) -> Option<ir::UserStackMap> {
+        self.user_stack_map.take()
     }
 
     pub(crate) fn clear_post_insn(&mut self) {
-        self.stack_map = None;
+        self.user_stack_map = None;
     }
 }
diff --git a/cranelift/codegen/src/isa/x64/inst/mod.rs b/cranelift/codegen/src/isa/x64/inst/mod.rs
index 2d68c5d4b..b255e12ef 100644
--- a/cranelift/codegen/src/isa/x64/inst/mod.rs
+++ b/cranelift/codegen/src/isa/x64/inst/mod.rs
@@ -2,7 +2,7 @@
 
 pub use emit_state::EmitState;
 
-use crate::binemit::{Addend, CodeOffset, Reloc, StackMap};
+use crate::binemit::{Addend, CodeOffset, Reloc};
 use crate::ir::{types, ExternalName, LibCall, TrapCode, Type};
 use crate::isa::x64::abi::X64ABIMachineSpec;
 use crate::isa::x64::inst::regs::{pretty_print_reg, show_ireg_sized};
@@ -22,6 +22,7 @@ mod emit_state;
 #[cfg(test)]
 mod emit_tests;
 pub mod regs;
+mod stack_switch;
 pub mod unwind;
 
 use args::*;
@@ -137,6 +138,7 @@ impl Inst {
             | Inst::Setcc { .. }
             | Inst::ShiftR { .. }
             | Inst::SignExtendData { .. }
+            | Inst::StackSwitchBasic { .. }
             | Inst::TrapIf { .. }
             | Inst::TrapIfAnd { .. }
             | Inst::TrapIfOr { .. }
@@ -1732,6 +1734,19 @@ impl PrettyPrint for Inst {
                 s
             }
 
+            Inst::StackSwitchBasic {
+                store_context_ptr,
+                load_context_ptr,
+                in_payload0,
+                out_payload0,
+            } => {
+                let store_context_ptr = pretty_print_reg(**store_context_ptr, 8);
+                let load_context_ptr = pretty_print_reg(**load_context_ptr, 8);
+                let in_payload0 = pretty_print_reg(**in_payload0, 8);
+                let out_payload0 = pretty_print_reg(*out_payload0.to_reg(), 8);
+                format!("{out_payload0} = stack_switch_basic {store_context_ptr}, {load_context_ptr}, {in_payload0}")
+            }
+
             Inst::JmpKnown { dst } => {
                 let op = ljustify("jmp".to_string());
                 let dst = dst.to_string();
@@ -2371,6 +2386,27 @@ fn x64_get_operands(inst: &mut Inst, collector: &mut impl OperandVisitor) {
             }
             collector.reg_clobbers(*clobbers);
         }
+        Inst::StackSwitchBasic {
+            store_context_ptr,
+            load_context_ptr,
+            in_payload0,
+            out_payload0,
+        } => {
+            collector.reg_use(load_context_ptr);
+            collector.reg_use(store_context_ptr);
+            collector.reg_fixed_use(in_payload0, stack_switch::payload_register());
+            collector.reg_fixed_def(out_payload0, stack_switch::payload_register());
+
+            let mut clobbers = crate::isa::x64::abi::ALL_CLOBBERS;
+            // The return/payload reg must not be included in the clobber set
+            clobbers.remove(
+                stack_switch::payload_register()
+                    .to_real_reg()
+                    .unwrap()
+                    .into(),
+            );
+            collector.reg_clobbers(clobbers);
+        }
 
         Inst::ReturnCallKnown { callee, info } => {
             let ReturnCallInfo { uses, tmp, .. } = &mut **info;
diff --git a/cranelift/codegen/src/isa/x64/inst/stack_switch.rs b/cranelift/codegen/src/isa/x64/inst/stack_switch.rs
new file mode 100644
index 000000000..b96eb9362
--- /dev/null
+++ b/cranelift/codegen/src/isa/x64/inst/stack_switch.rs
@@ -0,0 +1,52 @@
+use crate::{isa::x64::inst::regs, machinst::Reg};
+
+/// The `stack_switch` instruction loads information about the stack to switch
+/// to and stores information about the current stack by receiving pointers to
+/// memory laid out as in the struct `ControlContext` below.
+///
+/// The instruction is only supported on x64 Linux at the moment.
+///
+/// ```
+/// #[repr(C)]
+/// pub struct ControlContext {
+///     pub stack_pointer: *mut u8,
+///     pub frame_pointer: *mut u8,
+///     pub instruction_pointer: *mut u8,
+/// }
+/// ```
+///
+/// Note that this layout is deliberately chosen to make frame pointer walking
+/// possible, if desired: The layout enables stack layouts where a
+/// `ControlContext` is part of a frame pointer chain, putting the frame pointer
+/// next to the corresponding IP.
+///
+/// We never actually interact with values of that type in Cranelift, but are
+/// only interested in its layout for the purposes of generating code.
+#[allow(dead_code)]
+pub struct ControlContextLayout {
+    pub size: usize,
+    pub stack_pointer_offset: usize,
+    pub frame_pointer_offset: usize,
+    pub ip_offset: usize,
+}
+
+pub fn control_context_layout() -> ControlContextLayout {
+    ControlContextLayout {
+        size: 24,
+        stack_pointer_offset: 0,
+        frame_pointer_offset: 8,
+        ip_offset: 16,
+    }
+}
+
+/// The register used for handing over the payload when switching stacks.
+///
+/// We must use a fixed register for sending and receiving the payload: When
+/// switching from one stack to another using two matching``stack_switch``
+/// instructions, they must agree on the register where the payload is, similar
+/// to a calling convention. The same holds when `stack_switch`-ing to a newly
+/// initialized stack, where the entry trampoline must know which register the
+/// payload is in.
+pub fn payload_register() -> Reg {
+    regs::rdi()
+}
diff --git a/cranelift/codegen/src/isa/x64/lower.isle b/cranelift/codegen/src/isa/x64/lower.isle
index b0bf08f35..fea56b48d 100644
--- a/cranelift/codegen/src/isa/x64/lower.isle
+++ b/cranelift/codegen/src/isa/x64/lower.isle
@@ -3409,6 +3409,16 @@
 (rule (lower (return_call_indirect sig_ref callee args))
       (gen_return_call_indirect sig_ref callee args))
 
+;; Rules for `stack_switch` ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; currently, only the Basic model is supported
+(rule (lower (stack_switch store_context_ptr load_context_ptr in_payload0))
+      (if-let (StackSwitchModel.Basic) (stack_switch_model))
+      (let ((store_context_ptr Gpr (put_in_gpr store_context_ptr))
+            (load_context_ptr Gpr (put_in_gpr load_context_ptr))
+            (in_payload0 Gpr (put_in_gpr in_payload0)))
+        (x64_stack_switch_basic store_context_ptr load_context_ptr in_payload0)))
+
 ;;;; Rules for `get_{frame,stack}_pointer` and `get_return_address` ;;;;;;;;;;;;
 
 (rule (lower (get_frame_pointer))
diff --git a/cranelift/codegen/src/isa/x64/pcc.rs b/cranelift/codegen/src/isa/x64/pcc.rs
index 01732dd73..402c16301 100644
--- a/cranelift/codegen/src/isa/x64/pcc.rs
+++ b/cranelift/codegen/src/isa/x64/pcc.rs
@@ -901,6 +901,8 @@ pub(crate) fn check(
         }
 
         Inst::Unwind { .. } | Inst::DummyUse { .. } => Ok(()),
+
+        Inst::StackSwitchBasic { .. } => Err(PccError::UnimplementedInst),
     }
 }
 
diff --git a/cranelift/codegen/src/lib.rs b/cranelift/codegen/src/lib.rs
index a5b8a1051..1c78ac9a3 100644
--- a/cranelift/codegen/src/lib.rs
+++ b/cranelift/codegen/src/lib.rs
@@ -56,8 +56,8 @@ pub mod write;
 
 pub use crate::entity::packed_option;
 pub use crate::machinst::buffer::{
-    FinalizedMachReloc, FinalizedRelocTarget, MachCallSite, MachSrcLoc, MachStackMap,
-    MachTextSectionBuilder, MachTrap, OpenPatchRegion, PatchRegion,
+    FinalizedMachReloc, FinalizedRelocTarget, MachCallSite, MachSrcLoc, MachTextSectionBuilder,
+    MachTrap, OpenPatchRegion, PatchRegion,
 };
 pub use crate::machinst::{
     CompiledCode, Final, MachBuffer, MachBufferFinalized, MachInst, MachInstEmit,
diff --git a/cranelift/codegen/src/machinst/abi.rs b/cranelift/codegen/src/machinst/abi.rs
index 904e3f4b7..bd2c211fb 100644
--- a/cranelift/codegen/src/machinst/abi.rs
+++ b/cranelift/codegen/src/machinst/abi.rs
@@ -1664,38 +1664,6 @@ impl<M: ABIMachineSpec> Callee<M> {
 /// These methods of `Callee` may only be called after
 /// regalloc.
 impl<M: ABIMachineSpec> Callee<M> {
-    /// Generate a stack map, given a list of spillslots and the emission state
-    /// at a given program point (prior to emission of the safepointing
-    /// instruction).
-    pub fn spillslots_to_stack_map(
-        &self,
-        slots: &[SpillSlot],
-        state: &<M::I as MachInstEmit>::State,
-    ) -> StackMap {
-        let frame_layout = state.frame_layout();
-        let outgoing_args_size = frame_layout.outgoing_args_size;
-        let clobbers_and_slots = frame_layout.fixed_frame_storage_size + frame_layout.clobber_size;
-        trace!(
-            "spillslots_to_stackmap: slots = {:?}, state = {:?}",
-            slots,
-            state
-        );
-        let map_size = outgoing_args_size + clobbers_and_slots;
-        let bytes = M::word_bytes();
-        let map_words = (map_size + bytes - 1) / bytes;
-        let mut bits = std::iter::repeat(false)
-            .take(map_words as usize)
-            .collect::<Vec<bool>>();
-
-        let first_spillslot_word = ((self.stackslots_size + outgoing_args_size) / bytes) as usize;
-        for &slot in slots {
-            let slot = slot.index();
-            bits[first_spillslot_word + slot] = true;
-        }
-
-        StackMap::from_slice(&bits[..])
-    }
-
     /// Compute the final frame layout, post-regalloc.
     ///
     /// This must be called before gen_prologue or gen_epilogue.
diff --git a/cranelift/codegen/src/machinst/buffer.rs b/cranelift/codegen/src/machinst/buffer.rs
index 82a6e6add..67cd6c629 100644
--- a/cranelift/codegen/src/machinst/buffer.rs
+++ b/cranelift/codegen/src/machinst/buffer.rs
@@ -170,7 +170,7 @@
 //!   only process those which are required during island emission, deferring
 //!   all longer-range fixups to later.
 
-use crate::binemit::{Addend, CodeOffset, Reloc, StackMap};
+use crate::binemit::{Addend, CodeOffset, Reloc};
 use crate::ir::function::FunctionParameters;
 use crate::ir::{ExternalName, RelSourceLoc, SourceLoc, TrapCode};
 use crate::isa::unwind::UnwindInst;
@@ -249,8 +249,6 @@ pub struct MachBuffer<I: VCodeInst> {
     call_sites: SmallVec<[MachCallSite; 16]>,
     /// Any source location mappings referring to this code.
     srclocs: SmallVec<[MachSrcLoc<Stencil>; 64]>,
-    /// Any stack maps referring to this code.
-    stack_maps: SmallVec<[MachStackMap; 8]>,
     /// Any user stack maps for this code.
     ///
     /// Each entry is an `(offset, span, stack_map)` triple. Entries are sorted
@@ -334,7 +332,6 @@ impl MachBufferFinalized<Stencil> {
                 .into_iter()
                 .map(|srcloc| srcloc.apply_base_srcloc(base_srcloc))
                 .collect(),
-            stack_maps: self.stack_maps,
             user_stack_maps: self.user_stack_maps,
             unwind_info: self.unwind_info,
             alignment: self.alignment,
@@ -362,8 +359,6 @@ pub struct MachBufferFinalized<T: CompilePhase> {
     pub(crate) call_sites: SmallVec<[MachCallSite; 16]>,
     /// Any source location mappings referring to this code.
     pub(crate) srclocs: SmallVec<[T::MachSrcLocType; 64]>,
-    /// Any stack maps referring to this code.
-    pub(crate) stack_maps: SmallVec<[MachStackMap; 8]>,
     /// Any user stack maps for this code.
     ///
     /// Each entry is an `(offset, span, stack_map)` triple. Entries are sorted
@@ -414,17 +409,6 @@ impl Default for MachLabel {
     }
 }
 
-/// A stack map extent, when creating a stack map.
-pub enum StackMapExtent {
-    /// The stack map starts at this instruction, and ends after the number of upcoming bytes
-    /// (note: this is a code offset diff).
-    UpcomingBytes(CodeOffset),
-
-    /// The stack map started at the given offset and ends at the current one. This helps
-    /// architectures where the instruction size has not a fixed length.
-    StartedAtOffset(CodeOffset),
-}
-
 /// Represents the beginning of an editable region in the [`MachBuffer`], while code emission is
 /// still occurring. An [`OpenPatchRegion`] is closed by [`MachBuffer::end_patchable`], consuming
 /// the [`OpenPatchRegion`] token in the process.
@@ -458,7 +442,6 @@ impl<I: VCodeInst> MachBuffer<I> {
             traps: SmallVec::new(),
             call_sites: SmallVec::new(),
             srclocs: SmallVec::new(),
-            stack_maps: SmallVec::new(),
             user_stack_maps: SmallVec::new(),
             unwind_info: SmallVec::new(),
             cur_srcloc: None,
@@ -1547,7 +1530,6 @@ impl<I: VCodeInst> MachBuffer<I> {
             traps: self.traps,
             call_sites: self.call_sites,
             srclocs,
-            stack_maps: self.stack_maps,
             user_stack_maps: self.user_stack_maps,
             unwind_info: self.unwind_info,
             alignment,
@@ -1658,28 +1640,6 @@ impl<I: VCodeInst> MachBuffer<I> {
         }
     }
 
-    /// Add stack map metadata for this program point: a set of stack offsets
-    /// (from SP upward) that contain live references.
-    pub fn add_stack_map(&mut self, extent: StackMapExtent, stack_map: StackMap) {
-        let (start, end) = match extent {
-            StackMapExtent::UpcomingBytes(insn_len) => {
-                let start_offset = self.cur_offset();
-                (start_offset, start_offset + insn_len)
-            }
-            StackMapExtent::StartedAtOffset(start_offset) => {
-                let end_offset = self.cur_offset();
-                debug_assert!(end_offset >= start_offset);
-                (start_offset, end_offset)
-            }
-        };
-        trace!("Adding stack map for offsets {start:#x}..{end:#x}: {stack_map:?}");
-        self.stack_maps.push(MachStackMap {
-            offset: start,
-            offset_end: end,
-            stack_map,
-        });
-    }
-
     /// Push a user stack map onto this buffer.
     ///
     /// The stack map is associated with the given `return_addr` code
@@ -1768,17 +1728,7 @@ impl<T: CompilePhase> MachBufferFinalized<T> {
         &self.traps[..]
     }
 
-    /// Get the stack map metadata for this code.
-    pub fn stack_maps(&self) -> &[MachStackMap] {
-        &self.stack_maps[..]
-    }
-
-    /// Take this buffer's stack map metadata.
-    pub fn take_stack_maps(&mut self) -> SmallVec<[MachStackMap; 8]> {
-        mem::take(&mut self.stack_maps)
-    }
-
-    /// Ge tthe user stack map metadata for this code.
+    /// Get the user stack map metadata for this code.
     pub fn user_stack_maps(&self) -> &[(CodeOffset, u32, ir::UserStackMap)] {
         &self.user_stack_maps
     }
@@ -1987,23 +1937,6 @@ impl MachSrcLoc<Stencil> {
     }
 }
 
-/// Record of stack map metadata: stack offsets containing references.
-#[derive(Clone, Debug, PartialEq)]
-#[cfg_attr(
-    feature = "enable-serde",
-    derive(serde_derive::Serialize, serde_derive::Deserialize)
-)]
-pub struct MachStackMap {
-    /// The code offset at which this stack map applies.
-    pub offset: CodeOffset,
-    /// The code offset just past the "end" of the instruction: that is, the
-    /// offset of the first byte of the following instruction, or equivalently,
-    /// the start offset plus the instruction length.
-    pub offset_end: CodeOffset,
-    /// The stack map itself.
-    pub stack_map: StackMap,
-}
-
 /// Record of branch instruction in the buffer, to facilitate editing.
 #[derive(Clone, Debug)]
 struct MachBranch {
diff --git a/cranelift/codegen/src/machinst/isle.rs b/cranelift/codegen/src/machinst/isle.rs
index aca90549a..3c1388ab1 100644
--- a/cranelift/codegen/src/machinst/isle.rs
+++ b/cranelift/codegen/src/machinst/isle.rs
@@ -15,7 +15,7 @@ pub use crate::machinst::{
     ABIArg, ABIArgSlot, ABIMachineSpec, CallSite, InputSourceInst, Lower, LowerBackend, RealReg,
     Reg, RelocDistance, Sig, VCodeInst, Writable,
 };
-pub use crate::settings::TlsModel;
+pub use crate::settings::{StackSwitchModel, TlsModel};
 
 pub type Unit = ();
 pub type ValueSlice = (ValueList, usize);
@@ -343,6 +343,11 @@ macro_rules! isle_lower_prelude_methods {
             }
         }
 
+        #[inline]
+        fn stack_switch_model(&mut self) -> Option<StackSwitchModel> {
+            Some(self.backend.flags().stack_switch_model())
+        }
+
         #[inline]
         fn func_ref_data(&mut self, func_ref: FuncRef) -> (SigRef, ExternalName, RelocDistance) {
             let funcdata = &self.lower_ctx.dfg().ext_funcs[func_ref];
diff --git a/cranelift/codegen/src/machinst/mod.rs b/cranelift/codegen/src/machinst/mod.rs
index 434b3df7a..6a6872882 100644
--- a/cranelift/codegen/src/machinst/mod.rs
+++ b/cranelift/codegen/src/machinst/mod.rs
@@ -44,7 +44,7 @@
 //!
 //! ```
 
-use crate::binemit::{Addend, CodeInfo, CodeOffset, Reloc, StackMap};
+use crate::binemit::{Addend, CodeInfo, CodeOffset, Reloc};
 use crate::ir::{
     self, function::FunctionParameters, DynamicStackSlot, RelSourceLoc, StackSlot, Type,
 };
@@ -304,11 +304,7 @@ pub trait MachInstEmitState<I: VCodeInst>: Default + Clone + Debug {
 
     /// Update the emission state before emitting an instruction that is a
     /// safepoint.
-    fn pre_safepoint(
-        &mut self,
-        stack_map: Option<StackMap>,
-        user_stack_map: Option<ir::UserStackMap>,
-    );
+    fn pre_safepoint(&mut self, user_stack_map: Option<ir::UserStackMap>);
 
     /// The emission state holds ownership of a control plane, so it doesn't
     /// have to be passed around explicitly too much. `ctrl_plane_mut` may
diff --git a/cranelift/codegen/src/machinst/vcode.rs b/cranelift/codegen/src/machinst/vcode.rs
index 98ed209ed..0ea503bec 100644
--- a/cranelift/codegen/src/machinst/vcode.rs
+++ b/cranelift/codegen/src/machinst/vcode.rs
@@ -935,15 +935,6 @@ impl<I: VCodeInst> VCode<I> {
                                 safepoint_slots.push(slot);
                             }
 
-                            let stack_map = if safepoint_slots.is_empty() {
-                                None
-                            } else {
-                                Some(
-                                    self.abi
-                                        .spillslots_to_stack_map(&safepoint_slots[..], &state),
-                                )
-                            };
-
                             let (user_stack_map, user_stack_map_disasm) = {
                                 // The `user_stack_maps` is keyed by reverse
                                 // instruction index, so we must flip the
@@ -958,7 +949,7 @@ impl<I: VCodeInst> VCode<I> {
                                 (user_stack_map, user_stack_map_disasm)
                             };
 
-                            state.pre_safepoint(stack_map, user_stack_map);
+                            state.pre_safepoint(user_stack_map);
 
                             user_stack_map_disasm
                         } else {
diff --git a/cranelift/codegen/src/prelude_lower.isle b/cranelift/codegen/src/prelude_lower.isle
index 124b952cc..00c444576 100644
--- a/cranelift/codegen/src/prelude_lower.isle
+++ b/cranelift/codegen/src/prelude_lower.isle
@@ -870,6 +870,13 @@
 (decl pure partial preserve_frame_pointers () Unit)
 (extern constructor preserve_frame_pointers preserve_frame_pointers)
 
+;; This definition should be kept up to date with the values defined in
+;; cranelift/codegen/meta/src/shared/settings.rs
+(type StackSwitchModel extern (enum (None) (Basic) (UpdateWindowsTib)))
+
+(decl pure partial stack_switch_model () StackSwitchModel)
+(extern constructor stack_switch_model stack_switch_model)
+
 ;;;; Helpers for accessing instruction data ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
 (decl box_external_name (ExternalName) BoxExternalName)
diff --git a/cranelift/codegen/src/settings.rs b/cranelift/codegen/src/settings.rs
index da1e05e67..39446528d 100644
--- a/cranelift/codegen/src/settings.rs
+++ b/cranelift/codegen/src/settings.rs
@@ -511,6 +511,7 @@ mod tests {
         let expected = r#"[shared]
 opt_level = "none"
 tls_model = "none"
+stack_switch_model = "none"
 libcall_call_conv = "isa_default"
 probestack_size_log2 = 12
 probestack_strategy = "outline"
diff --git a/cranelift/interpreter/src/step.rs b/cranelift/interpreter/src/step.rs
index 3e1cfd7ab..7497e866a 100644
--- a/cranelift/interpreter/src/step.rs
+++ b/cranelift/interpreter/src/step.rs
@@ -1286,6 +1286,7 @@ where
         Opcode::X86Pmulhrsw => unimplemented!("X86Pmulhrsw"),
         Opcode::X86Pmaddubsw => unimplemented!("X86Pmaddubsw"),
         Opcode::X86Cvtt2dq => unimplemented!("X86Cvtt2dq"),
+        Opcode::StackSwitch => unimplemented!("StackSwitch"),
     })
 }
 
diff --git a/cranelift/src/compile.rs b/cranelift/src/compile.rs
index 2364cd0f5..65aa223d2 100644
--- a/cranelift/src/compile.rs
+++ b/cranelift/src/compile.rs
@@ -134,7 +134,6 @@ fn handle_module(
                 options.print,
                 result.buffer.relocs(),
                 result.buffer.traps(),
-                result.buffer.stack_maps(),
             )?;
         }
     }
diff --git a/cranelift/src/disasm.rs b/cranelift/src/disasm.rs
index 2cbc4b739..6947a5b97 100644
--- a/cranelift/src/disasm.rs
+++ b/cranelift/src/disasm.rs
@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
 use cranelift_codegen::ir::function::FunctionParameters;
 use cranelift_codegen::ir::Function;
 use cranelift_codegen::isa::TargetIsa;
-use cranelift_codegen::{FinalizedMachReloc, MachStackMap, MachTrap};
+use cranelift_codegen::{FinalizedMachReloc, MachTrap};
 use std::fmt::Write;
 
 fn print_relocs(func_params: &FunctionParameters, relocs: &[FinalizedMachReloc]) -> String {
@@ -36,38 +36,6 @@ pub fn print_traps(traps: &[MachTrap]) -> String {
     text
 }
 
-pub fn print_stack_maps(traps: &[MachStackMap]) -> String {
-    let mut text = String::new();
-    for MachStackMap {
-        offset,
-        offset_end,
-        stack_map,
-    } in traps
-    {
-        writeln!(
-            text,
-            "add_stack_map at {offset:#x}-{offset_end:#x} mapped_words={}",
-            stack_map.mapped_words()
-        )
-        .unwrap();
-
-        write!(text, "    entries: ").unwrap();
-        let mut first = true;
-        for i in 0..stack_map.mapped_words() {
-            if !stack_map.get_bit(i as usize) {
-                continue;
-            }
-            if !first {
-                write!(text, ", ").unwrap();
-            } else {
-                first = false;
-            }
-            write!(text, "{i}").unwrap();
-        }
-    }
-    text
-}
-
 cfg_if! {
     if #[cfg(feature = "disas")] {
         pub fn print_disassembly(func: &Function, isa: &dyn TargetIsa, mem: &[u8]) -> Result<()> {
@@ -124,16 +92,14 @@ pub fn print_all(
     print: bool,
     relocs: &[FinalizedMachReloc],
     traps: &[MachTrap],
-    stack_maps: &[MachStackMap],
 ) -> Result<()> {
     print_bytes(&mem);
     print_disassembly(func, isa, &mem[0..code_size as usize])?;
     if print {
         println!(
-            "\n{}\n{}\n{}",
+            "\n{}\n{}",
             print_relocs(&func.params, relocs),
             print_traps(traps),
-            print_stack_maps(stack_maps)
         );
     }
     Ok(())
diff --git a/cranelift/wasm/src/code_translator.rs b/cranelift/wasm/src/code_translator.rs
index 7bb017563..5506c6672 100644
--- a/cranelift/wasm/src/code_translator.rs
+++ b/cranelift/wasm/src/code_translator.rs
@@ -2506,7 +2506,9 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
         }
 
         Operator::TryTable { .. } | Operator::ThrowRef => {
-            unimplemented!("exception operators not yet implemented")
+            return Err(wasm_unsupported!(
+                "exception operators are not yet implemented"
+            ));
         }
 
         Operator::RefEq
@@ -2538,7 +2540,7 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
         | Operator::StructGetU { .. }
         | Operator::StructSet { .. }
         | Operator::StructGet { .. } => {
-            unimplemented!("GC operators not yet implemented")
+            return Err(wasm_unsupported!("GC operators are not yet implemented"));
         }
 
         Operator::GlobalAtomicGet { .. }
@@ -2577,7 +2579,9 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
         | Operator::ArrayAtomicRmwXchg { .. }
         | Operator::ArrayAtomicRmwCmpxchg { .. }
         | Operator::RefI31Shared { .. } => {
-            unimplemented!("shared-everything-threads not yet implemented")
+            return Err(wasm_unsupported!(
+                "shared-everything-threads operators are not yet implemented"
+            ));
         }
     };
     Ok(())
diff --git a/crates/wasmtime/src/compile.rs b/crates/wasmtime/src/compile.rs
index 1e4ef3d6d..35877008e 100644
--- a/crates/wasmtime/src/compile.rs
+++ b/crates/wasmtime/src/compile.rs
@@ -70,7 +70,7 @@ pub(crate) fn build_artifacts<T: FinishedObject>(
     // validated. Afterwards `types` will have all the type information for
     // this module.
     let mut parser = wasmparser::Parser::new(0);
-    let mut validator = wasmparser::Validator::new_with_features(engine.config().features);
+    let mut validator = wasmparser::Validator::new_with_features(engine.features());
     parser.set_features(*validator.features());
     let mut types = ModuleTypesBuilder::new(&validator);
     let mut translation = ModuleEnvironment::new(tunables, &mut validator, &mut types)
@@ -136,7 +136,7 @@ pub(crate) fn build_component_artifacts<T: FinishedObject>(
     let compiler = engine.compiler();
 
     let scope = ScopeVec::new();
-    let mut validator = wasmparser::Validator::new_with_features(engine.config().features);
+    let mut validator = wasmparser::Validator::new_with_features(engine.features());
     let mut types = ComponentTypesBuilder::new(&validator);
     let (component, mut module_translations) =
         Translator::new(tunables, &mut validator, &mut types, &scope)
diff --git a/crates/wasmtime/src/compile/code_builder.rs b/crates/wasmtime/src/compile/code_builder.rs
index 0d35886c9..ec8b54329 100644
--- a/crates/wasmtime/src/compile/code_builder.rs
+++ b/crates/wasmtime/src/compile/code_builder.rs
@@ -308,7 +308,7 @@ impl std::hash::Hash for HashedEngineCompileEnv<'_> {
         // Hash configuration state read for compilation
         let config = self.0.config();
         self.0.tunables().hash(hasher);
-        config.features.hash(hasher);
+        self.0.features().hash(hasher);
         config.wmemcheck.hash(hasher);
 
         // Catch accidental bugs of reusing across crate versions.
diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs
index a25cfd35c..d3e731d28 100644
--- a/crates/wasmtime/src/config.rs
+++ b/crates/wasmtime/src/config.rs
@@ -1,5 +1,6 @@
 use crate::prelude::*;
 use alloc::sync::Arc;
+use bitflags::Flags;
 use core::fmt;
 use core::str::FromStr;
 use hashbrown::{HashMap, HashSet};
@@ -114,7 +115,15 @@ pub struct Config {
     pub(crate) mem_creator: Option<Arc<dyn RuntimeMemoryCreator>>,
     pub(crate) allocation_strategy: InstanceAllocationStrategy,
     pub(crate) max_wasm_stack: usize,
-    pub(crate) features: WasmFeatures,
+    /// Explicitly enabled features via `Config::wasm_*` methods. This is a
+    /// signal that the embedder specifically wants something turned on
+    /// regardless of the defaults that Wasmtime might otherwise have enabled.
+    ///
+    /// Note that this, and `disabled_features` below, start as the empty set of
+    /// features to only track explicit user requests.
+    pub(crate) enabled_features: WasmFeatures,
+    /// Same as `enabled_features`, but for those that are explicitly disabled.
+    pub(crate) disabled_features: WasmFeatures,
     pub(crate) wasm_backtrace: bool,
     pub(crate) wasm_backtrace_details_env_used: bool,
     pub(crate) native_unwind_info: Option<bool>,
@@ -234,7 +243,8 @@ impl Config {
             wasm_backtrace: true,
             wasm_backtrace_details_env_used: false,
             native_unwind_info: None,
-            features: WasmFeatures::default(),
+            enabled_features: WasmFeatures::empty(),
+            disabled_features: WasmFeatures::empty(),
             #[cfg(feature = "async")]
             async_stack_size: 2 << 20,
             #[cfg(feature = "async")]
@@ -259,30 +269,6 @@ impl Config {
             ret.cranelift_opt_level(OptLevel::Speed);
         }
 
-        // Conditionally enabled features depending on compile-time crate
-        // features. Note that if these features are disabled then `Config` has
-        // no way of re-enabling them.
-        ret.features
-            .set(WasmFeatures::REFERENCE_TYPES, cfg!(feature = "gc"));
-        ret.features
-            .set(WasmFeatures::THREADS, cfg!(feature = "threads"));
-        ret.features.set(
-            WasmFeatures::COMPONENT_MODEL,
-            cfg!(feature = "component-model"),
-        );
-
-        // If GC is disabled at compile time also disable it in features
-        // forcibly irrespective of `wasmparser` defaults. Note that these also
-        // aren't yet fully implemented in Wasmtime.
-        if !cfg!(feature = "gc") {
-            ret.features.set(WasmFeatures::FUNCTION_REFERENCES, false);
-            ret.features.set(WasmFeatures::GC, false);
-        }
-
-        ret.wasm_multi_value(true);
-        ret.wasm_bulk_memory(true);
-        ret.wasm_simd(true);
-        ret.wasm_extended_const(true);
         ret.wasm_backtrace_details(WasmBacktraceDetails::Environment);
 
         ret
@@ -701,6 +687,12 @@ impl Config {
         self
     }
 
+    fn wasm_feature(&mut self, flag: WasmFeatures, enable: bool) -> &mut Self {
+        self.enabled_features.set(flag, enable);
+        self.disabled_features.set(flag, !enable);
+        self
+    }
+
     /// Configures whether the WebAssembly tail calls proposal will be enabled
     /// for compilation or not.
     ///
@@ -713,7 +705,7 @@ impl Config {
     ///
     /// [WebAssembly tail calls proposal]: https://github.com/WebAssembly/tail-call
     pub fn wasm_tail_call(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::TAIL_CALL, enable);
+        self.wasm_feature(WasmFeatures::TAIL_CALL, enable);
         self
     }
 
@@ -738,7 +730,7 @@ impl Config {
     ///
     /// [WebAssembly custom-page-sizes proposal]: https://github.com/WebAssembly/custom-page-sizes
     pub fn wasm_custom_page_sizes(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::CUSTOM_PAGE_SIZES, enable);
+        self.wasm_feature(WasmFeatures::CUSTOM_PAGE_SIZES, enable);
         self
     }
 
@@ -761,7 +753,7 @@ impl Config {
     /// [wasi-threads]: https://github.com/webassembly/wasi-threads
     #[cfg(feature = "threads")]
     pub fn wasm_threads(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::THREADS, enable);
+        self.wasm_feature(WasmFeatures::THREADS, enable);
         self
     }
 
@@ -783,7 +775,7 @@ impl Config {
     /// [proposal]: https://github.com/webassembly/reference-types
     #[cfg(feature = "gc")]
     pub fn wasm_reference_types(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::REFERENCE_TYPES, enable);
+        self.wasm_feature(WasmFeatures::REFERENCE_TYPES, enable);
         self
     }
 
@@ -802,7 +794,7 @@ impl Config {
     /// [proposal]: https://github.com/WebAssembly/function-references
     #[cfg(feature = "gc")]
     pub fn wasm_function_references(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::FUNCTION_REFERENCES, enable);
+        self.wasm_feature(WasmFeatures::FUNCTION_REFERENCES, enable);
         self
     }
 
@@ -823,7 +815,7 @@ impl Config {
     /// [proposal]: https://github.com/WebAssembly/gc
     #[cfg(feature = "gc")]
     pub fn wasm_gc(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::GC, enable);
+        self.wasm_feature(WasmFeatures::GC, enable);
         self
     }
 
@@ -843,7 +835,7 @@ impl Config {
     /// [proposal]: https://github.com/webassembly/simd
     /// [relaxed simd proposal]: https://github.com/WebAssembly/relaxed-simd
     pub fn wasm_simd(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::SIMD, enable);
+        self.wasm_feature(WasmFeatures::SIMD, enable);
         self
     }
 
@@ -870,7 +862,7 @@ impl Config {
     ///
     /// [proposal]: https://github.com/webassembly/relaxed-simd
     pub fn wasm_relaxed_simd(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::RELAXED_SIMD, enable);
+        self.wasm_feature(WasmFeatures::RELAXED_SIMD, enable);
         self
     }
 
@@ -914,7 +906,7 @@ impl Config {
     ///
     /// [proposal]: https://github.com/webassembly/bulk-memory-operations
     pub fn wasm_bulk_memory(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::BULK_MEMORY, enable);
+        self.wasm_feature(WasmFeatures::BULK_MEMORY, enable);
         self
     }
 
@@ -928,7 +920,7 @@ impl Config {
     ///
     /// [proposal]: https://github.com/webassembly/multi-value
     pub fn wasm_multi_value(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::MULTI_VALUE, enable);
+        self.wasm_feature(WasmFeatures::MULTI_VALUE, enable);
         self
     }
 
@@ -942,7 +934,7 @@ impl Config {
     ///
     /// [proposal]: https://github.com/webassembly/multi-memory
     pub fn wasm_multi_memory(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::MULTI_MEMORY, enable);
+        self.wasm_feature(WasmFeatures::MULTI_MEMORY, enable);
         self
     }
 
@@ -957,7 +949,7 @@ impl Config {
     ///
     /// [proposal]: https://github.com/webassembly/memory64
     pub fn wasm_memory64(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::MEMORY64, enable);
+        self.wasm_feature(WasmFeatures::MEMORY64, enable);
         self
     }
 
@@ -968,7 +960,7 @@ impl Config {
     ///
     /// [proposal]: https://github.com/webassembly/extended-const
     pub fn wasm_extended_const(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::EXTENDED_CONST, enable);
+        self.wasm_feature(WasmFeatures::EXTENDED_CONST, enable);
         self
     }
 
@@ -982,7 +974,7 @@ impl Config {
     /// [proposal]: https://github.com/webassembly/component-model
     #[cfg(feature = "component-model")]
     pub fn wasm_component_model(&mut self, enable: bool) -> &mut Self {
-        self.features.set(WasmFeatures::COMPONENT_MODEL, enable);
+        self.wasm_feature(WasmFeatures::COMPONENT_MODEL, enable);
         self
     }
 
@@ -993,8 +985,7 @@ impl Config {
     /// https://github.com/WebAssembly/component-model/issues/370.
     #[cfg(feature = "component-model")]
     pub fn wasm_component_model_more_flags(&mut self, enable: bool) -> &mut Self {
-        self.features
-            .set(WasmFeatures::COMPONENT_MODEL_MORE_FLAGS, enable);
+        self.wasm_feature(WasmFeatures::COMPONENT_MODEL_MORE_FLAGS, enable);
         self
     }
 
@@ -1004,8 +995,7 @@ impl Config {
     /// https://github.com/WebAssembly/component-model/pull/368.
     #[cfg(feature = "component-model")]
     pub fn wasm_component_model_multiple_returns(&mut self, enable: bool) -> &mut Self {
-        self.features
-            .set(WasmFeatures::COMPONENT_MODEL_MULTIPLE_RETURNS, enable);
+        self.wasm_feature(WasmFeatures::COMPONENT_MODEL_MULTIPLE_RETURNS, enable);
         self
     }
 
@@ -1732,24 +1722,164 @@ impl Config {
         self
     }
 
-    pub(crate) fn validate(&self) -> Result<Tunables> {
-        if self.features.contains(WasmFeatures::REFERENCE_TYPES)
-            && !self.features.contains(WasmFeatures::BULK_MEMORY)
+    /// Returns the set of features that the currently selected compiler backend
+    /// does not support at all and may panic on.
+    ///
+    /// Wasmtime strives to reject unknown modules or unsupported modules with
+    /// first-class errors instead of panics. Not all compiler backends have the
+    /// same level of feature support on all platforms as well. This method
+    /// returns a set of features that the currently selected compiler
+    /// configuration is known to not support and may panic on. This acts as a
+    /// first-level filter on incoming wasm modules/configuration to fail-fast
+    /// instead of panicking later on.
+    ///
+    /// Note that if a feature is not listed here it does not mean that the
+    /// backend fully supports the proposal. Instead that means that the backend
+    /// doesn't ever panic on the proposal, but errors during compilation may
+    /// still be returned. This means that features listed here are definitely
+    /// not supported at all, but features not listed here may still be
+    /// partially supported. For example at the time of this writing the Winch
+    /// backend partially supports simd so it's not listed here. Winch doesn't
+    /// fully support simd but unimplemented instructions just return errors.
+    fn compiler_panicking_wasm_features(&self) -> WasmFeatures {
+        #[cfg(any(feature = "cranelift", feature = "winch"))]
+        match self.compiler_config.strategy {
+            None | Some(Strategy::Cranelift) => WasmFeatures::empty(),
+            Some(Strategy::Winch) => {
+                let mut unsupported = WasmFeatures::GC
+                    | WasmFeatures::FUNCTION_REFERENCES
+                    | WasmFeatures::THREADS
+                    | WasmFeatures::RELAXED_SIMD
+                    | WasmFeatures::TAIL_CALL;
+                match self.compiler_target().architecture {
+                    target_lexicon::Architecture::Aarch64(_) => {
+                        // no support for simd on aarch64
+                        unsupported |= WasmFeatures::SIMD;
+                        // technically this is mostly supported in the sense of
+                        // multi-tables work well enough but enough of MVP wasm
+                        // currently panics that this is used here instead to
+                        // disable most spec tests which require reference
+                        // types.
+                        unsupported |= WasmFeatures::REFERENCE_TYPES;
+                    }
+
+                    // Winch doesn't support other non-x64 architectures at this
+                    // time either but will return an first-class error for
+                    // them.
+                    _ => {}
+                }
+                unsupported
+            }
+            Some(Strategy::Auto) => unreachable!(),
+        }
+        #[cfg(not(any(feature = "cranelift", feature = "winch")))]
+        return WasmFeatures::empty();
+    }
+
+    /// Calculates the set of features that are enabled for this `Config`.
+    ///
+    /// This method internally will start with the an empty set of features to
+    /// avoid being tied to wasmparser's defaults. Next Wasmtime's set of
+    /// default features are added to this set, some of which are conditional
+    /// depending on crate features. Finally explicitly requested features via
+    /// `wasm_*` methods on `Config` are applied. Everything is then validated
+    /// later in `Config::validate`.
+    fn features(&self) -> WasmFeatures {
+        let mut features = WasmFeatures::empty();
+
+        // On-by-default features that wasmtime has. Note that these are all
+        // subject to the criteria at
+        // https://docs.wasmtime.dev/contributing-implementing-wasm-proposals.html
+        features |= WasmFeatures::FLOATS;
+        features |= WasmFeatures::MULTI_VALUE;
+        features |= WasmFeatures::BULK_MEMORY;
+        features |= WasmFeatures::SIGN_EXTENSION;
+        features |= WasmFeatures::MUTABLE_GLOBAL;
+        features |= WasmFeatures::SATURATING_FLOAT_TO_INT;
+        features |= WasmFeatures::MULTI_MEMORY;
+        features |= WasmFeatures::SIMD;
+        features |= WasmFeatures::RELAXED_SIMD;
+        features |= WasmFeatures::TAIL_CALL;
+        features |= WasmFeatures::EXTENDED_CONST;
+        if cfg!(feature = "gc") {
+            features |= WasmFeatures::REFERENCE_TYPES;
+        }
+        if cfg!(feature = "threads") {
+            features |= WasmFeatures::THREADS;
+        }
+        if cfg!(feature = "component-model") {
+            features |= WasmFeatures::COMPONENT_MODEL;
+        }
+
+        // From the default set of proposals remove any that the current
+        // compiler backend may panic on if the module contains them.
+        features = features & !self.compiler_panicking_wasm_features();
+
+        // After wasmtime's defaults are configured then factor in user requests
+        // and disable/enable features. Note that the enable/disable sets should
+        // be disjoint.
+        debug_assert!((self.enabled_features & self.disabled_features).is_empty());
+        features &= !self.disabled_features;
+        features |= self.enabled_features;
+
+        features
+    }
+
+    fn compiler_target(&self) -> target_lexicon::Triple {
+        #[cfg(any(feature = "cranelift", feature = "winch"))]
+        {
+            let host = target_lexicon::Triple::host();
+
+            self.compiler_config
+                .target
+                .as_ref()
+                .unwrap_or(&host)
+                .clone()
+        }
+        #[cfg(not(any(feature = "cranelift", feature = "winch")))]
+        {
+            target_lexicon::Triple::host()
+        }
+    }
+
+    pub(crate) fn validate(&self) -> Result<(Tunables, WasmFeatures)> {
+        let features = self.features();
+
+        // First validate that the selected compiler backend and configuration
+        // supports the set of `features` that are enabled. This will help
+        // provide more first class errors instead of panics about unsupported
+        // features and configurations.
+        let unsupported = features & self.compiler_panicking_wasm_features();
+        if !unsupported.is_empty() {
+            for flag in WasmFeatures::FLAGS.iter() {
+                if !unsupported.contains(*flag.value()) {
+                    continue;
+                }
+                bail!(
+                    "the wasm_{} feature is not supported on this compiler configuration",
+                    flag.name().to_lowercase()
+                );
+            }
+
+            panic!("should have returned an error by now")
+        }
+
+        if features.contains(WasmFeatures::REFERENCE_TYPES)
+            && !features.contains(WasmFeatures::BULK_MEMORY)
         {
             bail!("feature 'reference_types' requires 'bulk_memory' to be enabled");
         }
-        if self.features.contains(WasmFeatures::THREADS)
-            && !self.features.contains(WasmFeatures::BULK_MEMORY)
+        if features.contains(WasmFeatures::THREADS) && !features.contains(WasmFeatures::BULK_MEMORY)
         {
             bail!("feature 'threads' requires 'bulk_memory' to be enabled");
         }
-        if self.features.contains(WasmFeatures::FUNCTION_REFERENCES)
-            && !self.features.contains(WasmFeatures::REFERENCE_TYPES)
+        if features.contains(WasmFeatures::FUNCTION_REFERENCES)
+            && !features.contains(WasmFeatures::REFERENCE_TYPES)
         {
             bail!("feature 'function_references' requires 'reference_types' to be enabled");
         }
-        if self.features.contains(WasmFeatures::GC)
-            && !self.features.contains(WasmFeatures::FUNCTION_REFERENCES)
+        if features.contains(WasmFeatures::GC)
+            && !features.contains(WasmFeatures::FUNCTION_REFERENCES)
         {
             bail!("feature 'gc' requires 'function_references' to be enabled");
         }
@@ -1818,7 +1948,7 @@ impl Config {
             bail!("static memory guard size cannot be smaller than dynamic memory guard size");
         }
 
-        Ok(tunables)
+        Ok((tunables, features))
     }
 
     #[cfg(feature = "runtime")]
@@ -1877,6 +2007,7 @@ impl Config {
     pub(crate) fn build_compiler(
         mut self,
         tunables: &Tunables,
+        features: WasmFeatures,
     ) -> Result<(Self, Box<dyn wasmtime_environ::Compiler>)> {
         let target = self.compiler_config.target.clone();
 
@@ -1904,13 +2035,7 @@ impl Config {
             .settings
             .insert("probestack_strategy".into(), "inline".into());
 
-        let host = target_lexicon::Triple::host();
-        let target = self
-            .compiler_config
-            .target
-            .as_ref()
-            .unwrap_or(&host)
-            .clone();
+        let target = self.compiler_target();
 
         // On supported targets, we enable stack probing by default.
         // This is required on Windows because of the way Windows
@@ -1949,7 +2074,7 @@ impl Config {
             .insert("preserve_frame_pointers".into(), "true".into());
 
         // check for incompatible compiler options and set required values
-        if self.features.contains(WasmFeatures::REFERENCE_TYPES) {
+        if features.contains(WasmFeatures::REFERENCE_TYPES) {
             if !self
                 .compiler_config
                 .ensure_setting_unset_or_given("enable_safepoints", "true")
@@ -1958,9 +2083,7 @@ impl Config {
             }
         }
 
-        if self.features.contains(WasmFeatures::RELAXED_SIMD)
-            && !self.features.contains(WasmFeatures::SIMD)
-        {
+        if features.contains(WasmFeatures::RELAXED_SIMD) && !features.contains(WasmFeatures::SIMD) {
             bail!("cannot disable the simd proposal but enable the relaxed simd proposal");
         }
 
@@ -2085,11 +2208,11 @@ impl fmt::Debug for Config {
         // enabled, and doesn't require maintenance by hand (which has become out
         // of date in the past), at the cost of possible confusion for why
         // a flag in this set doesn't have a Config setter.
-        use bitflags::Flags;
+        let features = self.features();
         for flag in WasmFeatures::FLAGS.iter() {
             f.field(
                 &format!("wasm_{}", flag.name().to_lowercase()),
-                &self.features.contains(*flag.value()),
+                &features.contains(*flag.value()),
             );
         }
 
diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs
index 6765c3cb4..8fa179770 100644
--- a/crates/wasmtime/src/engine.rs
+++ b/crates/wasmtime/src/engine.rs
@@ -47,6 +47,7 @@ pub struct Engine {
 
 struct EngineInner {
     config: Config,
+    features: WasmFeatures,
     tunables: Tunables,
     #[cfg(any(feature = "cranelift", feature = "winch"))]
     compiler: Box<dyn wasmtime_environ::Compiler>,
@@ -99,10 +100,10 @@ impl Engine {
         }
 
         let config = config.clone();
-        let tunables = config.validate()?;
+        let (tunables, features) = config.validate()?;
 
         #[cfg(any(feature = "cranelift", feature = "winch"))]
-        let (config, compiler) = config.build_compiler(&tunables)?;
+        let (config, compiler) = config.build_compiler(&tunables, features)?;
 
         Ok(Engine {
             inner: Arc::new(EngineInner {
@@ -122,6 +123,7 @@ impl Engine {
                 compatible_with_native_host: OnceLock::new(),
                 config,
                 tunables,
+                features,
             }),
         })
     }
@@ -132,6 +134,11 @@ impl Engine {
         &self.inner.config
     }
 
+    #[inline]
+    pub(crate) fn features(&self) -> WasmFeatures {
+        self.inner.features
+    }
+
     pub(crate) fn run_maybe_parallel<
         A: Send,
         B: Send,
@@ -307,7 +314,7 @@ impl Engine {
             // like typed function references and GC) are enabled this must be
             // enabled, otherwise this setting can have any value.
             "enable_safepoints" => {
-                if self.config().features.contains(WasmFeatures::REFERENCE_TYPES) {
+                if self.features().contains(WasmFeatures::REFERENCE_TYPES) {
                     *value == FlagValue::Bool(true)
                 } else {
                     return Ok(())
@@ -339,6 +346,7 @@ impl Engine {
             | "bb_padding_log2_minus_one"
             | "machine_code_cfg_info"
             | "tls_model" // wasmtime doesn't use tls right now
+            | "stack_switch_model" // wasmtime doesn't use stack switching right now
             | "opt_level" // opt level doesn't change semantics
             | "enable_alias_analysis" // alias analysis-based opts don't change semantics
             | "probestack_size_log2" // probestack above asserted disabled
diff --git a/crates/wasmtime/src/engine/serialization.rs b/crates/wasmtime/src/engine/serialization.rs
index 13f6d07f1..665b7569a 100644
--- a/crates/wasmtime/src/engine/serialization.rs
+++ b/crates/wasmtime/src/engine/serialization.rs
@@ -238,7 +238,7 @@ impl Metadata<'_> {
             saturating_float_to_int: _,
             sign_extension: _,
             floats: _,
-        } = engine.config().features.inflate();
+        } = engine.features().inflate();
 
         // These features are not implemented in Wasmtime yet. We match on them
         // above so that once we do implement support for them, we won't
@@ -281,7 +281,7 @@ impl Metadata<'_> {
         self.check_shared_flags(engine)?;
         self.check_isa_flags(engine)?;
         self.check_tunables(&engine.tunables())?;
-        self.check_features(&engine.config().features)?;
+        self.check_features(&engine.features())?;
         Ok(())
     }
 
diff --git a/crates/wasmtime/src/runtime/instance.rs b/crates/wasmtime/src/runtime/instance.rs
index 022ee6a18..06c3e66ff 100644
--- a/crates/wasmtime/src/runtime/instance.rs
+++ b/crates/wasmtime/src/runtime/instance.rs
@@ -341,8 +341,7 @@ impl Instance {
             compiled_module.module(),
             store
                 .engine()
-                .config()
-                .features
+                .features()
                 .contains(WasmFeatures::BULK_MEMORY),
         )?;
 
diff --git a/crates/wasmtime/src/runtime/module.rs b/crates/wasmtime/src/runtime/module.rs
index abb96227e..2b277e254 100644
--- a/crates/wasmtime/src/runtime/module.rs
+++ b/crates/wasmtime/src/runtime/module.rs
@@ -513,7 +513,7 @@ impl Module {
     ///
     /// [binary]: https://webassembly.github.io/spec/core/binary/index.html
     pub fn validate(engine: &Engine, binary: &[u8]) -> Result<()> {
-        let mut validator = Validator::new_with_features(engine.config().features);
+        let mut validator = Validator::new_with_features(engine.features());
 
         let mut functions = Vec::new();
         for payload in Parser::new(0).parse_all(binary) {
diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs
index 356fe5610..d1f238fa5 100644
--- a/crates/wasmtime/src/runtime/store.rs
+++ b/crates/wasmtime/src/runtime/store.rs
@@ -1520,8 +1520,7 @@ impl StoreOpaque {
         #[cfg(feature = "gc")]
         fn allocate_gc_store(engine: &Engine) -> Result<GcStore> {
             let (index, heap) = if engine
-                .config()
-                .features
+                .features()
                 .contains(wasmparser::WasmFeatures::REFERENCE_TYPES)
             {
                 engine
diff --git a/crates/wasmtime/src/runtime/vm/gc/enabled.rs b/crates/wasmtime/src/runtime/vm/gc/enabled.rs
index 200d081c7..ca8ab8859 100644
--- a/crates/wasmtime/src/runtime/vm/gc/enabled.rs
+++ b/crates/wasmtime/src/runtime/vm/gc/enabled.rs
@@ -21,4 +21,9 @@ pub fn default_gc_runtime() -> impl GcRuntime {
 }
 
 /// The default GC heap capacity: 512KiB.
+#[cfg(not(miri))]
 const DEFAULT_GC_HEAP_CAPACITY: usize = 1 << 19;
+
+/// The default GC heap capacity for miri: 64KiB.
+#[cfg(miri)]
+const DEFAULT_GC_HEAP_CAPACITY: usize = 1 << 16;
diff --git a/winch/codegen/src/codegen/context.rs b/winch/codegen/src/codegen/context.rs
index c7e34fb4d..f61b7749a 100644
--- a/winch/codegen/src/codegen/context.rs
+++ b/winch/codegen/src/codegen/context.rs
@@ -73,7 +73,9 @@ impl<'a> CodeGenContext<'a> {
             // All of our supported architectures use the float registers for vector operations.
             V128 => self.reg_for_class(RegClass::Float, masm),
             Ref(rt) => match rt.heap_type {
-                WasmHeapType::Func => self.reg_for_class(RegClass::Int, masm),
+                WasmHeapType::Func | WasmHeapType::Extern => {
+                    self.reg_for_class(RegClass::Int, masm)
+                }
                 ht => unimplemented!("Support for WasmHeapType: {ht}"),
             },
         }
diff --git a/winch/codegen/src/codegen/mod.rs b/winch/codegen/src/codegen/mod.rs
index 6d052982e..0edcd7fc4 100644
--- a/winch/codegen/src/codegen/mod.rs
+++ b/winch/codegen/src/codegen/mod.rs
@@ -419,7 +419,9 @@ where
                 match &ty {
                     I32 | I64 | F32 | F64 | V128 => self.masm.store(src.into(), addr, ty.into()),
                     Ref(rt) => match rt.heap_type {
-                        WasmHeapType::Func => self.masm.store_ptr(src.into(), addr),
+                        WasmHeapType::Func | WasmHeapType::Extern => {
+                            self.masm.store_ptr(src.into(), addr)
+                        }
                         ht => unimplemented!("Support for WasmHeapType: {ht}"),
                     },
                 }
diff --git a/winch/codegen/src/isa/x64/abi.rs b/winch/codegen/src/isa/x64/abi.rs
index 02829f28d..294e484fa 100644
--- a/winch/codegen/src/isa/x64/abi.rs
+++ b/winch/codegen/src/isa/x64/abi.rs
@@ -170,7 +170,7 @@ impl ABI for X64ABI {
     fn sizeof(ty: &WasmValType) -> u8 {
         match ty {
             WasmValType::Ref(rt) => match rt.heap_type {
-                WasmHeapType::Func => Self::word_bytes(),
+                WasmHeapType::Func | WasmHeapType::Extern => Self::word_bytes(),
                 ht => unimplemented!("Support for WasmHeapType: {ht}"),
             },
             WasmValType::F64 | WasmValType::I64 => Self::word_bytes(),
@@ -190,7 +190,7 @@ impl X64ABI {
     ) -> (ABIOperand, u32) {
         let (reg, ty) = match wasm_arg {
             ty @ WasmValType::Ref(rt) => match rt.heap_type {
-                WasmHeapType::Func => (
+                WasmHeapType::Func | WasmHeapType::Extern => (
                     Self::int_reg_for(index_env.next_gpr(), call_conv, params_or_returns),
                     ty,
                 ),
diff --git a/winch/codegen/src/isa/x64/asm.rs b/winch/codegen/src/isa/x64/asm.rs
index 19d9328fc..79eab2a80 100644
--- a/winch/codegen/src/isa/x64/asm.rs
+++ b/winch/codegen/src/isa/x64/asm.rs
@@ -490,7 +490,7 @@ impl Assembler {
             OperandSize::S32 => types::F32,
             OperandSize::S64 => types::F64,
             // Move the entire 128 bits via movdqa.
-            OperandSize::S128 => types::I128,
+            OperandSize::S128 => types::I32X4,
             OperandSize::S8 | OperandSize::S16 => unreachable!(),
         };
 
diff --git a/winch/codegen/src/visitor.rs b/winch/codegen/src/visitor.rs
index c36f9324f..e313300af 100644
--- a/winch/codegen/src/visitor.rs
+++ b/winch/codegen/src/visitor.rs
@@ -1338,6 +1338,10 @@ where
             I32 | I64 | F32 | F64 | V128 => context.stack.push(Val::local(index, slot.ty)),
             Ref(rt) => match rt.heap_type {
                 WasmHeapType::Func => context.stack.push(Val::local(index, slot.ty)),
+                WasmHeapType::Extern => {
+                    self.found_unsupported_instruction =
+                        Some("unsupported local.get of externref local");
+                }
                 ht => unimplemented!("Support for WasmHeapType: {ht}"),
             },
         }
@@ -1430,7 +1434,13 @@ where
                 }
                 _ => unimplemented!("Support for eager table init"),
             },
-            t => unimplemented!("Support for WasmHeapType: {t}"),
+            WasmHeapType::Extern => {
+                self.found_unsupported_instruction =
+                    Some("unsupported table.get of externref table");
+            }
+            t => {
+                unimplemented!("Support for WasmHeapType: {t}")
+            }
         }
     }
 
@@ -2150,6 +2160,7 @@ impl From<WasmValType> for OperandSize {
                     // to be updated in such a way that the calculation of the
                     // OperandSize will depend on the target's  pointer size.
                     WasmHeapType::Func => OperandSize::S64,
+                    WasmHeapType::Extern => OperandSize::S64,
                     t => unimplemented!("Support for WasmHeapType: {t}"),
                 }
             }


Test patch:
diff --git a/cranelift/filetests/filetests/isa/x64/stack_switch.clif b/cranelift/filetests/filetests/isa/x64/stack_switch.clif
new file mode 100644
index 000000000..333e8668b
--- /dev/null
+++ b/cranelift/filetests/filetests/isa/x64/stack_switch.clif
@@ -0,0 +1,605 @@
+test compile precise-output
+set opt_level=speed
+set stack_switch_model=basic
+target x86_64
+
+;; Test code emitted for stack switch itself
+function %switch(i64, i64, i64) -> i64 {
+block0(v0: i64, v1: i64, v2: i64):
+  v3 = stack_switch v0, v1, v2
+  return v3
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+;   subq    %rsp, $48, %rsp
+;   movq    %rbx, 0(%rsp)
+;   movq    %r12, 8(%rsp)
+;   movq    %r13, 16(%rsp)
+;   movq    %r14, 24(%rsp)
+;   movq    %r15, 32(%rsp)
+; block0:
+;   movq    %rdi, %r10
+;   movq    %rdx, %rdi
+;   %rdi = stack_switch_basic %r10, %rsi, %rdi
+;   movq    %rdi, %rax
+;   movq    0(%rsp), %rbx
+;   movq    8(%rsp), %r12
+;   movq    16(%rsp), %r13
+;   movq    24(%rsp), %r14
+;   movq    32(%rsp), %r15
+;   addq    %rsp, $48, %rsp
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+;   subq $0x30, %rsp
+;   movq %rbx, (%rsp)
+;   movq %r12, 8(%rsp)
+;   movq %r13, 0x10(%rsp)
+;   movq %r14, 0x18(%rsp)
+;   movq %r15, 0x20(%rsp)
+; block1: ; offset 0x20
+;   movq %rdi, %r10
+;   movq %rdx, %rdi
+;   movq (%rsi), %rax
+;   movq %rsp, (%r10)
+;   movq %rax, %rsp
+;   movq 8(%rsi), %rax
+;   movq %rbp, 8(%r10)
+;   movq %rax, %rbp
+;   movq 0x10(%rsi), %rax
+;   leaq 6(%rip), %rcx
+;   movq %rcx, 0x10(%r10)
+;   jmpq *%rax
+;   movq %rdi, %rax
+;   movq (%rsp), %rbx
+;   movq 8(%rsp), %r12
+;   movq 0x10(%rsp), %r13
+;   movq 0x18(%rsp), %r14
+;   movq 0x20(%rsp), %r15
+;   addq $0x30, %rsp
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+;; Test clobbering of all 14 GPRs used by Cranelift
+function %switch_int_clobber(i64, i64) -> i64 {
+block0(v0: i64, v1: i64):
+  v2 = iconst.i64 0
+
+;; We create values v100 to v114 before the stack_switch and want to use them
+;; afterwards. Thus, they must all be spilled on the stack.
+;; Given that they are loads, they cannot be moved behind the stack_switch.
+
+  v100 = load.i64 v0+0
+  v101 = load.i64 v0+8
+  v102 = load.i64 v0+16
+  v103 = load.i64 v0+24
+  v104 = load.i64 v0+32
+  v105 = load.i64 v0+40
+  v106 = load.i64 v0+48
+  v107 = load.i64 v0+56
+  v108 = load.i64 v0+64
+  v109 = load.i64 v0+72
+  v110 = load.i64 v0+80
+  v111 = load.i64 v0+88
+  v112 = load.i64 v0+96
+  v113 = load.i64 v0+104
+  v114 = load.i64 v0+112
+
+  v299 = stack_switch v1, v1, v2
+
+;; We use v100 - v114 after the switch: We load from each v(100+i) into
+;; v(200+i), and use v(300+i) as an accumulator of all values so far.
+;; These are loads and therefore they cannot be moved before the stack_switch.
+
+  v200 = load.i64 v100
+  v300 = iadd.i64 v299, v200
+  v201 = load.i64 v101
+  v301 = iadd.i64 v300, v201
+  v202 = load.i64 v102
+  v302 = iadd.i64 v301, v202
+  v203 = load.i64 v103
+  v303 = iadd.i64 v302, v203
+  v204 = load.i64 v104
+  v304 = iadd.i64 v303, v204
+  v205 = load.i64 v105
+  v305 = iadd.i64 v304, v205
+  v206 = load.i64 v106
+  v306 = iadd.i64 v305, v206
+  v207 = load.i64 v107
+  v307 = iadd.i64 v306, v207
+  v208 = load.i64 v108
+  v308 = iadd.i64 v307, v208
+  v209 = load.i64 v109
+  v309 = iadd.i64 v308, v209
+  v210 = load.i64 v110
+  v310 = iadd.i64 v309, v210
+  v211 = load.i64 v111
+  v311 = iadd.i64 v310, v211
+  v212 = load.i64 v112
+  v312 = iadd.i64 v311, v212
+  v213 = load.i64 v113
+  v313 = iadd.i64 v312, v213
+  v214 = load.i64 v114
+  v314 = iadd.i64 v313, v214
+
+;; Let's also use v0 again
+  v400 = iadd.i64 v314, v0
+
+;; We cannot use v1 again: That causes TooManyLiveRegs, as this usage is an
+;; instance of https://github.com/bytecodealliance/regalloc2/issues/145
+;; v401 = iadd.i64 v400, v1
+
+  return v400
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+;   subq    %rsp, $192, %rsp
+;   movq    %rbx, 144(%rsp)
+;   movq    %r12, 152(%rsp)
+;   movq    %r13, 160(%rsp)
+;   movq    %r14, 168(%rsp)
+;   movq    %r15, 176(%rsp)
+; block0:
+;   movq    0(%rdi), %rdx
+;   movq    %rdx, rsp(128 + virtual offset)
+;   movq    8(%rdi), %rdx
+;   movq    %rdx, rsp(8 + virtual offset)
+;   movq    16(%rdi), %rdx
+;   movq    %rdx, rsp(120 + virtual offset)
+;   movq    24(%rdi), %rdx
+;   movq    %rdx, rsp(112 + virtual offset)
+;   movq    32(%rdi), %rdx
+;   movq    %rdx, rsp(104 + virtual offset)
+;   movq    40(%rdi), %rdx
+;   movq    %rdx, rsp(96 + virtual offset)
+;   movq    48(%rdi), %rdx
+;   movq    %rdx, rsp(88 + virtual offset)
+;   movq    56(%rdi), %rdx
+;   movq    %rdx, rsp(80 + virtual offset)
+;   movq    64(%rdi), %rdx
+;   movq    %rdx, rsp(72 + virtual offset)
+;   movq    72(%rdi), %rdx
+;   movq    %rdx, rsp(64 + virtual offset)
+;   movq    80(%rdi), %rdx
+;   movq    %rdx, rsp(56 + virtual offset)
+;   movq    88(%rdi), %rdx
+;   movq    %rdx, rsp(48 + virtual offset)
+;   movq    96(%rdi), %rdx
+;   movq    %rdx, rsp(40 + virtual offset)
+;   movq    104(%rdi), %rdx
+;   movq    %rdx, rsp(32 + virtual offset)
+;   movq    112(%rdi), %rdx
+;   movq    %rdi, rsp(0 + virtual offset)
+;   movq    %rdx, rsp(24 + virtual offset)
+;   xorq    %rdi, %rdi, %rdi
+;   %rdi = stack_switch_basic %rsi, %rsi, %rdi
+;   movq    rsp(128 + virtual offset), %rdx
+;   movq    %rdi, rsp(16 + virtual offset)
+;   movq    0(%rdx), %r8
+;   movq    rsp(8 + virtual offset), %rdx
+;   movq    0(%rdx), %r9
+;   movq    rsp(120 + virtual offset), %rdx
+;   movq    %r9, rsp(8 + virtual offset)
+;   movq    0(%rdx), %r10
+;   movq    rsp(112 + virtual offset), %rdx
+;   movq    0(%rdx), %r9
+;   movq    rsp(104 + virtual offset), %rdx
+;   movq    0(%rdx), %rsi
+;   movq    rsp(96 + virtual offset), %rdx
+;   movq    0(%rdx), %rax
+;   movq    rsp(88 + virtual offset), %rdx
+;   movq    0(%rdx), %rcx
+;   movq    rsp(80 + virtual offset), %rdx
+;   movq    0(%rdx), %r15
+;   movq    rsp(72 + virtual offset), %rdx
+;   movq    0(%rdx), %r13
+;   movq    rsp(64 + virtual offset), %rdx
+;   movq    0(%rdx), %rbx
+;   movq    rsp(56 + virtual offset), %rdx
+;   movq    0(%rdx), %r14
+;   movq    rsp(48 + virtual offset), %rdx
+;   movq    0(%rdx), %r12
+;   movq    rsp(40 + virtual offset), %rdx
+;   movq    0(%rdx), %rdx
+;   movq    rsp(32 + virtual offset), %rdi
+;   movq    0(%rdi), %r11
+;   movq    rsp(16 + virtual offset), %rdi
+;   lea     0(%rdi,%r8,1), %r8
+;   movq    rsp(8 + virtual offset), %rdi
+;   lea     0(%rdi,%r10,1), %r10
+;   lea     0(%r8,%r10,1), %r8
+;   lea     0(%r9,%rsi,1), %r9
+;   lea     0(%rax,%rcx,1), %r10
+;   lea     0(%r15,%r13,1), %rsi
+;   lea     0(%r10,%rsi,1), %r10
+;   lea     0(%rbx,%r14,1), %rsi
+;   lea     0(%r10,%rsi,1), %r10
+;   lea     0(%r12,%rdx,1), %rdx
+;   lea     0(%r10,%rdx,1), %rdx
+;   lea     0(%r9,%rdx,1), %rdx
+;   lea     0(%r8,%rdx,1), %rdx
+;   movq    rsp(24 + virtual offset), %r8
+;   addq    %r11, 0(%r8), %r11
+;   movq    rsp(0 + virtual offset), %rdi
+;   lea     0(%r11,%rdi,1), %r8
+;   lea     0(%rdx,%r8,1), %rax
+;   movq    144(%rsp), %rbx
+;   movq    152(%rsp), %r12
+;   movq    160(%rsp), %r13
+;   movq    168(%rsp), %r14
+;   movq    176(%rsp), %r15
+;   addq    %rsp, $192, %rsp
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+;   subq $0xc0, %rsp
+;   movq %rbx, 0x90(%rsp)
+;   movq %r12, 0x98(%rsp)
+;   movq %r13, 0xa0(%rsp)
+;   movq %r14, 0xa8(%rsp)
+;   movq %r15, 0xb0(%rsp)
+; block1: ; offset 0x33
+;   movq (%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x80(%rsp)
+;   movq 8(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 8(%rsp)
+;   movq 0x10(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x78(%rsp)
+;   movq 0x18(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x70(%rsp)
+;   movq 0x20(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x68(%rsp)
+;   movq 0x28(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x60(%rsp)
+;   movq 0x30(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x58(%rsp)
+;   movq 0x38(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x50(%rsp)
+;   movq 0x40(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x48(%rsp)
+;   movq 0x48(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x40(%rsp)
+;   movq 0x50(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x38(%rsp)
+;   movq 0x58(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x30(%rsp)
+;   movq 0x60(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x28(%rsp)
+;   movq 0x68(%rdi), %rdx ; trap: heap_oob
+;   movq %rdx, 0x20(%rsp)
+;   movq 0x70(%rdi), %rdx ; trap: heap_oob
+;   movq %rdi, (%rsp)
+;   movq %rdx, 0x18(%rsp)
+;   xorq %rdi, %rdi
+;   movq (%rsi), %rax
+;   movq %rsp, (%rsi)
+;   movq %rax, %rsp
+;   movq 8(%rsi), %rax
+;   movq %rbp, 8(%rsi)
+;   movq %rax, %rbp
+;   movq 0x10(%rsi), %rax
+;   leaq 6(%rip), %rcx
+;   movq %rcx, 0x10(%rsi)
+;   jmpq *%rax
+;   movq 0x80(%rsp), %rdx
+;   movq %rdi, 0x10(%rsp)
+;   movq (%rdx), %r8 ; trap: heap_oob
+;   movq 8(%rsp), %rdx
+;   movq (%rdx), %r9 ; trap: heap_oob
+;   movq 0x78(%rsp), %rdx
+;   movq %r9, 8(%rsp)
+;   movq (%rdx), %r10 ; trap: heap_oob
+;   movq 0x70(%rsp), %rdx
+;   movq (%rdx), %r9 ; trap: heap_oob
+;   movq 0x68(%rsp), %rdx
+;   movq (%rdx), %rsi ; trap: heap_oob
+;   movq 0x60(%rsp), %rdx
+;   movq (%rdx), %rax ; trap: heap_oob
+;   movq 0x58(%rsp), %rdx
+;   movq (%rdx), %rcx ; trap: heap_oob
+;   movq 0x50(%rsp), %rdx
+;   movq (%rdx), %r15 ; trap: heap_oob
+;   movq 0x48(%rsp), %rdx
+;   movq (%rdx), %r13 ; trap: heap_oob
+;   movq 0x40(%rsp), %rdx
+;   movq (%rdx), %rbx ; trap: heap_oob
+;   movq 0x38(%rsp), %rdx
+;   movq (%rdx), %r14 ; trap: heap_oob
+;   movq 0x30(%rsp), %rdx
+;   movq (%rdx), %r12 ; trap: heap_oob
+;   movq 0x28(%rsp), %rdx
+;   movq (%rdx), %rdx ; trap: heap_oob
+;   movq 0x20(%rsp), %rdi
+;   movq (%rdi), %r11 ; trap: heap_oob
+;   movq 0x10(%rsp), %rdi
+;   addq %rdi, %r8
+;   movq 8(%rsp), %rdi
+;   addq %rdi, %r10
+;   addq %r10, %r8
+;   addq %rsi, %r9
+;   leaq (%rax, %rcx), %r10
+;   leaq (%r15, %r13), %rsi
+;   addq %rsi, %r10
+;   leaq (%rbx, %r14), %rsi
+;   addq %rsi, %r10
+;   addq %r12, %rdx
+;   addq %r10, %rdx
+;   addq %r9, %rdx
+;   addq %r8, %rdx
+;   movq 0x18(%rsp), %r8
+;   addq (%r8), %r11 ; trap: heap_oob
+;   movq (%rsp), %rdi
+;   leaq (%r11, %rdi), %r8
+;   leaq (%rdx, %r8), %rax
+;   movq 0x90(%rsp), %rbx
+;   movq 0x98(%rsp), %r12
+;   movq 0xa0(%rsp), %r13
+;   movq 0xa8(%rsp), %r14
+;   movq 0xb0(%rsp), %r15
+;   addq $0xc0, %rsp
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+;; Test clobbering of all 16 float registers
+function %switch_float_clobber(i64, i64) -> f64 {
+block0(v0: i64, v1: i64):
+  v2 = iconst.i64 0
+
+;; We create values v100 to v115 before the stack_switch and want to use them
+;; afterwards. Thus, they must all be spilled on the stack.
+;; Given that they are loads, they cannot be moved behind the stack_switch.
+
+  v100 = load.f64 v0+0
+  v101 = load.f64 v0+8
+  v102 = load.f64 v0+16
+  v103 = load.f64 v0+24
+  v104 = load.f64 v0+32
+  v105 = load.f64 v0+40
+  v106 = load.f64 v0+48
+  v107 = load.f64 v0+56
+  v108 = load.f64 v0+64
+  v109 = load.f64 v0+72
+  v110 = load.f64 v0+80
+  v111 = load.f64 v0+88
+  v112 = load.f64 v0+96
+  v113 = load.f64 v0+104
+  v114 = load.f64 v0+112
+  v115 = load.f64 v0+120
+
+  v198 = stack_switch v1, v1, v2
+  v199 = fcvt_from_uint.f64 v198
+
+;; We add v100 to v115, using v200 to v215 as an accumulator
+;; We make v199 part of the result to prevent summation of v100 to v115 before
+;; the switch.
+
+  v200 = fadd v199, v100
+  v201 = fadd v200, v101
+  v202 = fadd v201, v102
+  v203 = fadd v202, v103
+  v204 = fadd v203, v104
+  v205 = fadd v204, v105
+  v206 = fadd v205, v106
+  v207 = fadd v206, v107
+  v208 = fadd v207, v108
+  v209 = fadd v208, v109
+  v210 = fadd v209, v110
+  v211 = fadd v210, v111
+  v212 = fadd v211, v112
+  v213 = fadd v212, v113
+  v214 = fadd v213, v114
+  v215 = fadd v214, v115
+
+  return v215
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+;   subq    %rsp, $304, %rsp
+;   movq    %rbx, 256(%rsp)
+;   movq    %r12, 264(%rsp)
+;   movq    %r13, 272(%rsp)
+;   movq    %r14, 280(%rsp)
+;   movq    %r15, 288(%rsp)
+; block0:
+;   movsd   0(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(240 + virtual offset)
+;   movsd   8(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(224 + virtual offset)
+;   movsd   16(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(208 + virtual offset)
+;   movsd   24(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(192 + virtual offset)
+;   movsd   32(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(176 + virtual offset)
+;   movsd   40(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(160 + virtual offset)
+;   movsd   48(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(144 + virtual offset)
+;   movsd   56(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(128 + virtual offset)
+;   movsd   64(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(112 + virtual offset)
+;   movsd   72(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(96 + virtual offset)
+;   movsd   80(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(80 + virtual offset)
+;   movsd   88(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(64 + virtual offset)
+;   movsd   96(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(48 + virtual offset)
+;   movsd   104(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(32 + virtual offset)
+;   movsd   112(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(16 + virtual offset)
+;   movsd   120(%rdi), %xmm7
+;   movdqu  %xmm7, rsp(0 + virtual offset)
+;   xorq    %rdi, %rdi, %rdi
+;   %rdi = stack_switch_basic %rsi, %rsi, %rdi
+;   u64_to_f64_seq %rdi, %xmm0, %rax, %rcx
+;   movdqu  rsp(240 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(224 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(208 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(192 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(176 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(160 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(144 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(128 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(112 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(96 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(80 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(64 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(48 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(32 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(16 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movdqu  rsp(0 + virtual offset), %xmm7
+;   addsd   %xmm0, %xmm7, %xmm0
+;   movq    256(%rsp), %rbx
+;   movq    264(%rsp), %r12
+;   movq    272(%rsp), %r13
+;   movq    280(%rsp), %r14
+;   movq    288(%rsp), %r15
+;   addq    %rsp, $304, %rsp
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+;   subq $0x130, %rsp
+;   movq %rbx, 0x100(%rsp)
+;   movq %r12, 0x108(%rsp)
+;   movq %r13, 0x110(%rsp)
+;   movq %r14, 0x118(%rsp)
+;   movq %r15, 0x120(%rsp)
+; block1: ; offset 0x33
+;   movsd (%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0xf0(%rsp)
+;   movsd 8(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0xe0(%rsp)
+;   movsd 0x10(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0xd0(%rsp)
+;   movsd 0x18(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0xc0(%rsp)
+;   movsd 0x20(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0xb0(%rsp)
+;   movsd 0x28(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0xa0(%rsp)
+;   movsd 0x30(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0x90(%rsp)
+;   movsd 0x38(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0x80(%rsp)
+;   movsd 0x40(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0x70(%rsp)
+;   movsd 0x48(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0x60(%rsp)
+;   movsd 0x50(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0x50(%rsp)
+;   movsd 0x58(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0x40(%rsp)
+;   movsd 0x60(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0x30(%rsp)
+;   movsd 0x68(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0x20(%rsp)
+;   movsd 0x70(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, 0x10(%rsp)
+;   movsd 0x78(%rdi), %xmm7 ; trap: heap_oob
+;   movdqu %xmm7, (%rsp)
+;   xorq %rdi, %rdi
+;   movq (%rsi), %rax
+;   movq %rsp, (%rsi)
+;   movq %rax, %rsp
+;   movq 8(%rsi), %rax
+;   movq %rbp, 8(%rsi)
+;   movq %rax, %rbp
+;   movq 0x10(%rsi), %rax
+;   leaq 6(%rip), %rcx
+;   movq %rcx, 0x10(%rsi)
+;   jmpq *%rax
+;   cmpq $0, %rdi
+;   jl 0x135
+;   cvtsi2sdq %rdi, %xmm0
+;   jmp 0x14f
+;   movq %rdi, %rax
+;   shrq $1, %rax
+;   movq %rdi, %rcx
+;   andq $1, %rcx
+;   orq %rax, %rcx
+;   cvtsi2sdq %rcx, %xmm0
+;   addsd %xmm0, %xmm0
+;   movdqu 0xf0(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0xe0(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0xd0(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0xc0(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0xb0(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0xa0(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0x90(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0x80(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0x70(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0x60(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0x50(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0x40(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0x30(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0x20(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu 0x10(%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movdqu (%rsp), %xmm7
+;   addsd %xmm7, %xmm0
+;   movq 0x100(%rsp), %rbx
+;   movq 0x108(%rsp), %r12
+;   movq 0x110(%rsp), %r13
+;   movq 0x118(%rsp), %r14
+;   movq 0x120(%rsp), %r15
+;   addq $0x130, %rsp
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
diff --git a/tests/wast.rs b/tests/wast.rs
index e4983d7d3..875303c67 100644
--- a/tests/wast.rs
+++ b/tests/wast.rs
@@ -1,4 +1,4 @@
-use anyhow::Context;
+use anyhow::{bail, Context};
 use bstr::ByteSlice;
 use libtest_mimic::{Arguments, FormatSetting, Trial};
 use once_cell::sync::Lazy;
@@ -57,23 +57,147 @@ fn add_tests(trials: &mut Vec<Trial>, path: &Path) {
                         }
                     },
                 );
-                trials.push(trial.with_ignored_flag(ignore(&path, strategy)));
+                trials.push(trial);
             }
         }
     }
 }
 
-fn ignore(test: &Path, strategy: Strategy) -> bool {
+fn should_fail(test: &Path, strategy: Strategy) -> bool {
     // Winch only supports x86_64 at this time.
     if strategy == Strategy::Winch && !cfg!(target_arch = "x86_64") {
         return true;
     }
 
+    // Disable spec tests for proposals that Winch does not implement yet.
+    if strategy == Strategy::Winch {
+        let unsupported = [
+            // externref/reference-types related
+            "memory64/threads.wast",
+            "misc_testsuite/externref-id-function.wast",
+            "misc_testsuite/externref-segments.wast",
+            "misc_testsuite/many_table_gets_lead_to_gc.wast",
+            "misc_testsuite/mutable_externref_globals.wast",
+            "misc_testsuite/no-mixup-stack-maps.wast",
+            "misc_testsuite/no-panic.wast",
+            "misc_testsuite/simple_ref_is_null.wast",
+            "misc_testsuite/table_grow_with_funcref.wast",
+            "spec_testsuite/br_table.wast",
+            "spec_testsuite/data-invalid.wast",
+            "spec_testsuite/elem.wast",
+            "spec_testsuite/global.wast",
+            "spec_testsuite/ref_func.wast",
+            "spec_testsuite/ref_is_null.wast",
+            "spec_testsuite/ref_null.wast",
+            "spec_testsuite/select.wast",
+            "spec_testsuite/table_fill.wast",
+            "spec_testsuite/table_get.wast",
+            "spec_testsuite/table_grow.wast",
+            "spec_testsuite/table_set.wast",
+            "spec_testsuite/table_size.wast",
+            "spec_testsuite/unreached-invalid.wast",
+            "extended-const/elem.wast",
+            "extended-const/global.wast",
+            // simd-related failures
+            "annotations/simd_lane.wast",
+            "memory64/simd.wast",
+            "misc_testsuite/int-to-float-splat.wast",
+            "misc_testsuite/issue6562.wast",
+            "misc_testsuite/simd/almost-extmul.wast",
+            "misc_testsuite/simd/canonicalize-nan.wast",
+            "misc_testsuite/simd/cvt-from-uint.wast",
+            "misc_testsuite/simd/issue4807.wast",
+            "misc_testsuite/simd/issue6725-no-egraph-panic.wast",
+            "misc_testsuite/simd/issue_3327_bnot_lowering.wast",
+            "misc_testsuite/simd/load_splat_out_of_bounds.wast",
+            "misc_testsuite/simd/replace-lane-preserve.wast",
+            "misc_testsuite/simd/spillslot-size-fuzzbug.wast",
+            "misc_testsuite/simd/unaligned-load.wast",
+            "multi-memory/simd_memory-multi.wast",
+            "spec_testsuite/simd_align.wast",
+            "spec_testsuite/simd_bit_shift.wast",
+            "spec_testsuite/simd_bitwise.wast",
+            "spec_testsuite/simd_boolean.wast",
+            "spec_testsuite/simd_const.wast",
+            "spec_testsuite/simd_conversions.wast",
+            "spec_testsuite/simd_f32x4.wast",
+            "spec_testsuite/simd_f32x4_arith.wast",
+            "spec_testsuite/simd_f32x4_cmp.wast",
+            "spec_testsuite/simd_f32x4_pmin_pmax.wast",
+            "spec_testsuite/simd_f32x4_rounding.wast",
+            "spec_testsuite/simd_f64x2.wast",
+            "spec_testsuite/simd_f64x2_arith.wast",
+            "spec_testsuite/simd_f64x2_cmp.wast",
+            "spec_testsuite/simd_f64x2_pmin_pmax.wast",
+            "spec_testsuite/simd_f64x2_rounding.wast",
+            "spec_testsuite/simd_i16x8_arith.wast",
+            "spec_testsuite/simd_i16x8_arith2.wast",
+            "spec_testsuite/simd_i16x8_cmp.wast",
+            "spec_testsuite/simd_i16x8_extadd_pairwise_i8x16.wast",
+            "spec_testsuite/simd_i16x8_extmul_i8x16.wast",
+            "spec_testsuite/simd_i16x8_q15mulr_sat_s.wast",
+            "spec_testsuite/simd_i16x8_sat_arith.wast",
+            "spec_testsuite/simd_i32x4_arith.wast",
+            "spec_testsuite/simd_i32x4_arith2.wast",
+            "spec_testsuite/simd_i32x4_cmp.wast",
+            "spec_testsuite/simd_i32x4_dot_i16x8.wast",
+            "spec_testsuite/simd_i32x4_extadd_pairwise_i16x8.wast",
+            "spec_testsuite/simd_i32x4_extmul_i16x8.wast",
+            "spec_testsuite/simd_i32x4_trunc_sat_f32x4.wast",
+            "spec_testsuite/simd_i32x4_trunc_sat_f64x2.wast",
+            "spec_testsuite/simd_i64x2_arith.wast",
+            "spec_testsuite/simd_i64x2_arith2.wast",
+            "spec_testsuite/simd_i64x2_cmp.wast",
+            "spec_testsuite/simd_i64x2_extmul_i32x4.wast",
+            "spec_testsuite/simd_i8x16_arith.wast",
+            "spec_testsuite/simd_i8x16_arith2.wast",
+            "spec_testsuite/simd_i8x16_cmp.wast",
+            "spec_testsuite/simd_i8x16_sat_arith.wast",
+            "spec_testsuite/simd_int_to_int_extend.wast",
+            "spec_testsuite/simd_lane.wast",
+            "spec_testsuite/simd_load.wast",
+            "spec_testsuite/simd_load16_lane.wast",
+            "spec_testsuite/simd_load32_lane.wast",
+            "spec_testsuite/simd_load64_lane.wast",
+            "spec_testsuite/simd_load8_lane.wast",
+            "spec_testsuite/simd_load_extend.wast",
+            "spec_testsuite/simd_load_splat.wast",
+            "spec_testsuite/simd_load_zero.wast",
+            "spec_testsuite/simd_splat.wast",
+            "spec_testsuite/simd_store16_lane.wast",
+            "spec_testsuite/simd_store32_lane.wast",
+            "spec_testsuite/simd_store64_lane.wast",
+            "spec_testsuite/simd_store8_lane.wast",
+        ];
+
+        if unsupported.iter().any(|part| test.ends_with(part)) {
+            return true;
+        }
+
+        // A few proposals that winch has no support for.
+        let unsupported_proposals = [
+            "function-references",
+            "gc",
+            "tail-call",
+            "relaxed-simd",
+            "threads",
+        ];
+        if let Some(parent) = test.parent() {
+            if unsupported_proposals
+                .iter()
+                .any(|part| parent.ends_with(part))
+            {
+                return true;
+            }
+        }
+    }
+
     for part in test.iter() {
         // Not implemented in Wasmtime yet
         if part == "exception-handling" {
-            return true;
+            return !test.ends_with("binary.wast");
         }
+
         // Wasmtime doesn't implement the table64 extension yet.
         if part == "memory64" {
             if [
@@ -95,48 +219,6 @@ fn ignore(test: &Path, strategy: Strategy) -> bool {
             }
         }
 
-        // Disable spec tests for proposals that Winch does not implement yet.
-        if strategy == Strategy::Winch {
-            let part = part.to_str().unwrap();
-            let unsupported = [
-                // wasm proposals that Winch doesn't support,
-                "references",
-                "tail-call",
-                "gc",
-                "threads",
-                "multi-memory",
-                "relaxed-simd",
-                "function-references",
-                // tests in misc_testsuite that Winch doesn't support
-                "no-panic.wast",
-                "externref-id-function.wast",
-                "int-to-float-splat.wast",
-                "issue6562.wast",
-                "many_table_gets_lead_to_gc.wast",
-                "mutable_externref_globals.wast",
-                "no-mixup-stack-maps.wast",
-                "simple_ref_is_null.wast",
-                "table_grow_with_funcref.wast",
-                // Tests in the spec test suite Winch doesn't support
-                "threads.wast",
-                "br_table.wast",
-                "global.wast",
-                "table_fill.wast",
-                "table_get.wast",
-                "table_set.wast",
-                "table_grow.wast",
-                "table_size.wast",
-                "elem.wast",
-                "select.wast",
-                "unreached-invalid.wast",
-                "linking.wast",
-            ];
-
-            if unsupported.contains(&part) || part.starts_with("simd") || part.starts_with("ref_") {
-                return true;
-            }
-        }
-
         // Implementation of the GC proposal is a work-in-progress, this is
         // a list of all currently known-to-fail tests.
         if part == "gc" {
@@ -147,41 +229,20 @@ fn ignore(test: &Path, strategy: Strategy) -> bool {
                 "array_init_elem.wast",
                 "array.wast",
                 "binary_gc.wast",
-                "binary.wast",
                 "br_on_cast_fail.wast",
                 "br_on_cast.wast",
-                "br_on_non_null.wast",
-                "br_on_null.wast",
-                "br_table.wast",
-                "call_ref.wast",
-                "data.wast",
-                "elem.wast",
                 "extern.wast",
-                "func.wast",
-                "global.wast",
-                "if.wast",
-                "linking.wast",
-                "local_get.wast",
-                "local_init.wast",
-                "ref_as_non_null.wast",
                 "ref_cast.wast",
                 "ref_eq.wast",
-                "ref_is_null.wast",
-                "ref_null.wast",
                 "ref_test.wast",
-                "ref.wast",
                 "return_call_indirect.wast",
-                "return_call_ref.wast",
                 "return_call.wast",
-                "select.wast",
                 "struct.wast",
                 "table_sub.wast",
-                "table.wast",
                 "type_canon.wast",
                 "type_equivalence.wast",
                 "type-rec.wast",
                 "type-subtyping.wast",
-                "unreached-invalid.wast",
                 "unreached_valid.wast",
                 "i31.wast",
             ]
@@ -197,6 +258,7 @@ fn ignore(test: &Path, strategy: Strategy) -> bool {
 // function which actually executes the `wast` test suite given the `strategy`
 // to compile it.
 fn run_wast(wast: &Path, strategy: Strategy, pooling: bool) -> anyhow::Result<()> {
+    let should_fail = should_fail(wast, strategy);
     let wast_bytes =
         std::fs::read(wast).with_context(|| format!("failed to read `{}`", wast.display()))?;
 
@@ -340,28 +402,38 @@ fn run_wast(wast: &Path, strategy: Strategy, pooling: bool) -> anyhow::Result<()
         None
     };
 
-    let mut engines = vec![(Engine::new(&cfg)?, "default")];
+    let mut engines = vec![(Engine::new(&cfg), "default")];
 
     // For tests that use relaxed-simd test both the default engine and the
     // guaranteed-deterministic engine to ensure that both the 'native'
     // semantics of the instructions plus the canonical semantics work.
     if relaxed_simd {
         engines.push((
-            Engine::new(cfg.relaxed_simd_deterministic(true))?,
+            Engine::new(cfg.relaxed_simd_deterministic(true)),
             "deterministic",
         ));
     }
 
     for (engine, desc) in engines {
-        let store = Store::new(&engine, ());
-        let mut wast_context = WastContext::new(store);
-        wast_context.register_spectest(&SpectestConfig {
-            use_shared_memory,
-            suppress_prints: true,
-        })?;
-        wast_context
-            .run_buffer(wast.to_str().unwrap(), &wast_bytes)
-            .with_context(|| format!("failed to run spec test with {desc} engine"))?;
+        let result = engine.and_then(|engine| {
+            let store = Store::new(&engine, ());
+            let mut wast_context = WastContext::new(store);
+            wast_context.register_spectest(&SpectestConfig {
+                use_shared_memory,
+                suppress_prints: true,
+            })?;
+            wast_context
+                .run_buffer(wast.to_str().unwrap(), &wast_bytes)
+                .with_context(|| format!("failed to run spec test with {desc} engine"))
+        });
+
+        if should_fail {
+            if result.is_ok() {
+                bail!("this test is flagged as should-fail but it succeeded")
+            }
+        } else {
+            result?;
+        }
     }
 
     Ok(())