Repository: bytecodealliance/wasmtime
Pull request #9304 (2024-09-24 20:18:32)

Print an error on async stack overflow
This commit updates Wasmtime's handling of traps on Unix platforms to print an error message on stack overflow when the guard page is hit. This is distinct from stack overflow in WebAssembly which raises a normal trap and can be caught. This is instead to be used on misconfigured hosts where the async stack is too small or wasm was allowed to take up too much of the async stack. Currently no error message is printed and the program simply aborts with a core dump which can be difficult to debug.

This instead registers the range of the async guard page with the trap handling infrastructure to test the faulting address and if it lies within this range. If so then a small message is printed and then the program is aborted with `libc::abort()`.

This does not impact the safety of any prior embedding or fix any issues. It's instead intended purely as a diagnostic tool to help users more quickly understand that stack size configuration settings are the likely culprit.

<!--
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
-->


alexcrichton: This is inspired by https://github.com/bytecodealliance/wasmtime/issues/9298 where stack overflows on the CLI in async fibers might be more common now.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index da732e316..8f1cd97f5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -446,7 +446,16 @@ dependencies = [
  "anstyle",
  "clap_lex",
  "strsim",
- "terminal_size 0.3.0",
+ "terminal_size",
+]
+
+[[package]]
+name = "clap_complete"
+version = "4.5.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b378c786d3bde9442d2c6dd7e6080b2a818db2b96e30d6e7f1b6d224eb617d3"
+dependencies = [
+ "clap",
 ]
 
 [[package]]
@@ -529,17 +538,15 @@ dependencies = [
 
 [[package]]
 name = "console"
-version = "0.15.0"
+version = "0.15.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
+checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
 dependencies = [
  "encode_unicode",
+ "lazy_static",
  "libc",
- "once_cell",
- "regex",
- "terminal_size 0.1.17",
  "unicode-width",
- "winapi",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -919,26 +926,18 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.15"
+version = "0.9.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
 dependencies = [
- "autocfg",
- "cfg-if",
  "crossbeam-utils",
- "memoffset",
- "scopeguard",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.10"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83"
-dependencies = [
- "cfg-if",
- "once_cell",
-]
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
 
 [[package]]
 name = "crypto-common"
@@ -1862,15 +1861,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "memoffset"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
-dependencies = [
- "autocfg",
-]
-
 [[package]]
 name = "min-platform-host"
 version = "26.0.0"
@@ -2488,12 +2478,6 @@ dependencies = [
  "winapi-util",
 ]
 
-[[package]]
-name = "scopeguard"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
-
 [[package]]
 name = "semver"
 version = "1.0.17"
@@ -2770,16 +2754,6 @@ dependencies = [
  "winapi-util",
 ]
 
-[[package]]
-name = "terminal_size"
-version = "0.1.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
-dependencies = [
- "libc",
- "winapi",
-]
-
 [[package]]
 name = "terminal_size"
 version = "0.3.0"
@@ -3582,6 +3556,7 @@ dependencies = [
  "capstone",
  "cfg-if",
  "clap",
+ "clap_complete",
  "component-macro-test",
  "component-test-util",
  "cranelift-codegen",
diff --git a/Cargo.toml b/Cargo.toml
index 74ee00055..0bf32b204 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -58,6 +58,7 @@ wasmtime-wasi-keyvalue = { workspace = true, optional = true }
 wasmtime-wasi-threads = { workspace = true, optional = true }
 wasmtime-wasi-http = { workspace = true, optional = true }
 clap = { workspace = true }
+clap_complete = { workspace = true, optional = true }
 anyhow = { workspace = true, features = ['std'] }
 target-lexicon = { workspace = true }
 once_cell = { workspace = true }
@@ -288,6 +289,7 @@ windows-sys = "0.59.0"
 env_logger = "0.11.5"
 log = { version = "0.4.8", default-features = false }
 clap = { version = "4.5.17", default-features = false, features = ["std", "derive"] }
+clap_complete = "4.4.7"
 hashbrown = { version = "0.14", default-features = false }
 capstone = "0.12.0"
 once_cell = { version = "1.12.0", default-features = false }
@@ -360,6 +362,7 @@ default = [
   "serve",
   "wast",
   "config",
+  "completion",
 
   # On-by-default WASI features
   "wasi-nn",
@@ -456,6 +459,7 @@ run = [
   "dep:tokio",
   "wasmtime-cli-flags/async",
 ]
+completion = ["dep:clap_complete"]
 
 [[test]]
 name = "host_segfault"
diff --git a/crates/c-api/src/async.rs b/crates/c-api/src/async.rs
index ef577d87c..c6066cf54 100644
--- a/crates/c-api/src/async.rs
+++ b/crates/c-api/src/async.rs
@@ -389,6 +389,9 @@ unsafe impl StackMemory for CHostStackMemory {
         let base = unsafe { top.sub(len) as usize };
         base..base + len
     }
+    fn guard_range(&self) -> Range<*mut u8> {
+        std::ptr::null_mut()..std::ptr::null_mut()
+    }
 }
 
 pub type wasmtime_new_stack_memory_callback_t = extern "C" fn(
diff --git a/crates/fiber/src/lib.rs b/crates/fiber/src/lib.rs
index 16fb10bd2..d17f92d6b 100644
--- a/crates/fiber/src/lib.rs
+++ b/crates/fiber/src/lib.rs
@@ -33,10 +33,11 @@ impl FiberStack {
     }
 
     /// Creates a new fiber stack with the given pointer to the bottom of the
-    /// stack plus the byte length of the stack.
+    /// stack plus how large the guard size and stack size are.
     ///
-    /// The `bottom` pointer should be addressable for `len` bytes. The page
-    /// beneath `bottom` should be unmapped as a guard page.
+    /// The bytes from `bottom` to `bottom.add(guard_size)` should all be
+    /// guaranteed to be unmapped. The bytes from `bottom.add(guard_size)` to
+    /// `bottom.add(guard_size + len)` should be addressable.
     ///
     /// # Safety
     ///
@@ -44,8 +45,14 @@ impl FiberStack {
     ///
     /// The caller must properly allocate the stack space with a guard page and
     /// make the pages accessible for correct behavior.
-    pub unsafe fn from_raw_parts(bottom: *mut u8, len: usize) -> io::Result<Self> {
-        Ok(Self(imp::FiberStack::from_raw_parts(bottom, len)?))
+    pub unsafe fn from_raw_parts(
+        bottom: *mut u8,
+        guard_size: usize,
+        len: usize,
+    ) -> io::Result<Self> {
+        Ok(Self(imp::FiberStack::from_raw_parts(
+            bottom, guard_size, len,
+        )?))
     }
 
     /// Gets the top of the stack.
@@ -67,6 +74,11 @@ impl FiberStack {
     pub fn is_from_raw_parts(&self) -> bool {
         self.0.is_from_raw_parts()
     }
+
+    /// Returns the range of memory that the guard page(s) reside in.
+    pub fn guard_range(&self) -> Option<Range<*mut u8>> {
+        self.0.guard_range()
+    }
 }
 
 /// A creator of RuntimeFiberStacks.
@@ -85,6 +97,8 @@ pub unsafe trait RuntimeFiberStack: Send + Sync {
     fn top(&self) -> *mut u8;
     /// The valid range of the stack without guard pages.
     fn range(&self) -> Range<usize>;
+    /// The range of the guard page(s)
+    fn guard_range(&self) -> Range<*mut u8>;
 }
 
 pub struct Fiber<'a, Resume, Yield, Return> {
diff --git a/crates/fiber/src/unix.rs b/crates/fiber/src/unix.rs
index b16006543..632188351 100644
--- a/crates/fiber/src/unix.rs
+++ b/crates/fiber/src/unix.rs
@@ -46,7 +46,7 @@ pub struct FiberStack {
 
 enum FiberStackStorage {
     Mmap(#[allow(dead_code)] MmapFiberStack),
-    Unmanaged,
+    Unmanaged(usize),
     Custom(#[allow(dead_code)] Box<dyn RuntimeFiberStack>),
 }
 
@@ -70,21 +70,21 @@ impl FiberStack {
         })
     }
 
-    pub unsafe fn from_raw_parts(base: *mut u8, len: usize) -> io::Result<Self> {
+    pub unsafe fn from_raw_parts(base: *mut u8, guard_size: usize, len: usize) -> io::Result<Self> {
         // See comments in `mod asan` below for why asan has a different stack
         // allocation strategy.
         if cfg!(asan) {
             return Self::from_custom(asan::new_fiber_stack(len)?);
         }
         Ok(FiberStack {
-            base,
+            base: base.add(guard_size),
             len,
-            storage: FiberStackStorage::Unmanaged,
+            storage: FiberStackStorage::Unmanaged(guard_size),
         })
     }
 
     pub fn is_from_raw_parts(&self) -> bool {
-        matches!(self.storage, FiberStackStorage::Unmanaged)
+        matches!(self.storage, FiberStackStorage::Unmanaged(_))
     }
 
     pub fn from_custom(custom: Box<dyn RuntimeFiberStack>) -> io::Result<Self> {
@@ -115,6 +115,17 @@ impl FiberStack {
         let base = self.base as usize;
         Some(base..base + self.len)
     }
+
+    pub fn guard_range(&self) -> Option<Range<*mut u8>> {
+        match &self.storage {
+            FiberStackStorage::Unmanaged(guard_size) => unsafe {
+                let start = self.base.sub(*guard_size);
+                Some(start..self.base)
+            },
+            FiberStackStorage::Mmap(mmap) => Some(mmap.mapping_base..self.base),
+            FiberStackStorage::Custom(custom) => Some(custom.guard_range()),
+        }
+    }
 }
 
 struct MmapFiberStack {
diff --git a/crates/fiber/src/windows.rs b/crates/fiber/src/windows.rs
index e15d1bb2b..16203bd5c 100644
--- a/crates/fiber/src/windows.rs
+++ b/crates/fiber/src/windows.rs
@@ -15,7 +15,11 @@ impl FiberStack {
         Ok(Self(size))
     }
 
-    pub unsafe fn from_raw_parts(_base: *mut u8, _len: usize) -> io::Result<Self> {
+    pub unsafe fn from_raw_parts(
+        _base: *mut u8,
+        _guard_size: usize,
+        _len: usize,
+    ) -> io::Result<Self> {
         Err(io::Error::from_raw_os_error(ERROR_NOT_SUPPORTED as i32))
     }
 
@@ -34,6 +38,10 @@ impl FiberStack {
     pub fn range(&self) -> Option<Range<usize>> {
         None
     }
+
+    pub fn guard_range(&self) -> Option<Range<*mut u8>> {
+        None
+    }
 }
 
 pub struct Fiber {
diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs
index 753799e43..fd45ba84d 100644
--- a/crates/wasmtime/src/runtime/func.rs
+++ b/crates/wasmtime/src/runtime/func.rs
@@ -1610,6 +1610,7 @@ pub(crate) fn invoke_wasm_and_catch_traps<T>(
             store.0.signal_handler(),
             store.0.engine().config().wasm_backtrace,
             store.0.engine().config().coredump_on_trap,
+            store.0.async_guard_range(),
             store.0.default_caller(),
             closure,
         );
diff --git a/crates/wasmtime/src/runtime/stack.rs b/crates/wasmtime/src/runtime/stack.rs
index 703c03691..94f5732ec 100644
--- a/crates/wasmtime/src/runtime/stack.rs
+++ b/crates/wasmtime/src/runtime/stack.rs
@@ -57,6 +57,8 @@ pub unsafe trait StackMemory: Send + Sync {
     fn top(&self) -> *mut u8;
     /// The range of where this stack resides in memory, excluding guard pages.
     fn range(&self) -> Range<usize>;
+    /// The range of memory where the guard region of this stack resides.
+    fn guard_range(&self) -> Range<*mut u8>;
 }
 
 pub(crate) struct FiberStackProxy(pub Box<dyn StackMemory>);
@@ -69,4 +71,8 @@ unsafe impl RuntimeFiberStack for FiberStackProxy {
     fn range(&self) -> Range<usize> {
         self.0.range()
     }
+
+    fn guard_range(&self) -> Range<*mut u8> {
+        self.0.guard_range()
+    }
 }
diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs
index 10ba85308..bf21c4a65 100644
--- a/crates/wasmtime/src/runtime/store.rs
+++ b/crates/wasmtime/src/runtime/store.rs
@@ -99,7 +99,7 @@ use core::future::Future;
 use core::marker;
 use core::mem::{self, ManuallyDrop};
 use core::num::NonZeroU64;
-use core::ops::{Deref, DerefMut};
+use core::ops::{Deref, DerefMut, Range};
 use core::pin::Pin;
 use core::ptr;
 use core::task::{Context, Poll};
@@ -391,7 +391,26 @@ pub struct StoreOpaque {
 #[cfg(feature = "async")]
 struct AsyncState {
     current_suspend: UnsafeCell<*mut wasmtime_fiber::Suspend<Result<()>, (), Result<()>>>,
-    current_poll_cx: UnsafeCell<*mut Context<'static>>,
+    current_poll_cx: UnsafeCell<PollContext>,
+}
+
+#[cfg(feature = "async")]
+#[derive(Clone, Copy)]
+struct PollContext {
+    future_context: *mut Context<'static>,
+    guard_range_start: *mut u8,
+    guard_range_end: *mut u8,
+}
+
+#[cfg(feature = "async")]
+impl Default for PollContext {
+    fn default() -> PollContext {
+        PollContext {
+            future_context: core::ptr::null_mut(),
+            guard_range_start: core::ptr::null_mut(),
+            guard_range_end: core::ptr::null_mut(),
+        }
+    }
 }
 
 // Lots of pesky unsafe cells and pointers in this structure. This means we need
@@ -536,7 +555,7 @@ impl<T> Store<T> {
                 #[cfg(feature = "async")]
                 async_state: AsyncState {
                     current_suspend: UnsafeCell::new(ptr::null_mut()),
-                    current_poll_cx: UnsafeCell::new(ptr::null_mut()),
+                    current_poll_cx: UnsafeCell::new(PollContext::default()),
                 },
                 fuel_reserve: 0,
                 fuel_yield_interval: None,
@@ -1779,13 +1798,13 @@ impl StoreOpaque {
         }
 
         let poll_cx_inner_ptr = unsafe { *poll_cx_box_ptr };
-        if poll_cx_inner_ptr.is_null() {
+        if poll_cx_inner_ptr.future_context.is_null() {
             return None;
         }
 
         Some(AsyncCx {
             current_suspend: self.async_state.current_suspend.get(),
-            current_poll_cx: poll_cx_box_ptr,
+            current_poll_cx: unsafe { core::ptr::addr_of_mut!((*poll_cx_box_ptr).future_context) },
             track_pkey_context_switch: self.pkey.is_some(),
         })
     }
@@ -2053,6 +2072,18 @@ at https://bytecodealliance.org/security.
 
         self.num_component_instances += 1;
     }
+
+    pub(crate) fn async_guard_range(&self) -> Range<*mut u8> {
+        #[cfg(feature = "async")]
+        unsafe {
+            let ptr = self.async_state.current_poll_cx.get();
+            (*ptr).guard_range_start..(*ptr).guard_range_end
+        }
+        #[cfg(not(feature = "async"))]
+        {
+            core::ptr::null_mut()..core::ptr::null_mut()
+        }
+    }
 }
 
 impl<T> StoreContextMut<'_, T> {
@@ -2123,7 +2154,7 @@ impl<T> StoreContextMut<'_, T> {
 
         struct FiberFuture<'a> {
             fiber: Option<wasmtime_fiber::Fiber<'a, Result<()>, (), Result<()>>>,
-            current_poll_cx: *mut *mut Context<'static>,
+            current_poll_cx: *mut PollContext,
             engine: Engine,
             // See comments in `FiberFuture::resume` for this
             state: Option<crate::runtime::vm::AsyncWasmCallState>,
@@ -2259,10 +2290,21 @@ impl<T> StoreContextMut<'_, T> {
                 // On exit from this function, though, we reset the polling
                 // context back to what it was to signify that `Store` no longer
                 // has access to this pointer.
+                let guard = self
+                    .fiber()
+                    .stack()
+                    .guard_range()
+                    .unwrap_or(core::ptr::null_mut()..core::ptr::null_mut());
                 unsafe {
                     let _reset = Reset(self.current_poll_cx, *self.current_poll_cx);
-                    *self.current_poll_cx =
-                        core::mem::transmute::<&mut Context<'_>, *mut Context<'static>>(cx);
+                    *self.current_poll_cx = PollContext {
+                        future_context: core::mem::transmute::<
+                            &mut Context<'_>,
+                            *mut Context<'static>,
+                        >(cx),
+                        guard_range_start: guard.start,
+                        guard_range_end: guard.end,
+                    };
 
                     // After that's set up we resume execution of the fiber, which
                     // may also start the fiber for the first time. This either
diff --git a/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/unix_stack_pool.rs b/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/unix_stack_pool.rs
index 76dcdd999..f274e51df 100644
--- a/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/unix_stack_pool.rs
+++ b/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/unix_stack_pool.rs
@@ -105,13 +105,16 @@ impl StackPool {
             let bottom_of_stack = self
                 .mapping
                 .as_ptr()
-                .add((index * self.stack_size) + self.page_size)
+                .add(index * self.stack_size)
                 .cast_mut();
 
             commit_pages(bottom_of_stack, size_without_guard)?;
 
-            let stack =
-                wasmtime_fiber::FiberStack::from_raw_parts(bottom_of_stack, size_without_guard)?;
+            let stack = wasmtime_fiber::FiberStack::from_raw_parts(
+                bottom_of_stack,
+                self.page_size,
+                size_without_guard,
+            )?;
             Ok(stack)
         }
     }
diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs b/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs
index 63872692b..3d65b25d2 100644
--- a/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs
+++ b/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs
@@ -47,7 +47,8 @@ use mach2::port::*;
 use mach2::thread_act::*;
 use mach2::thread_status::*;
 use mach2::traps::*;
-use std::mem;
+use std::io;
+use std::mem::{self, MaybeUninit};
 use std::ptr::addr_of_mut;
 use std::thread;
 use wasmtime_environ::Trap;
@@ -61,6 +62,8 @@ pub struct TrapHandler {
     thread: Option<thread::JoinHandle<()>>,
 }
 
+static mut PREV_SIGBUS: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
+
 impl TrapHandler {
     pub unsafe fn new() -> TrapHandler {
         // Mach ports do not currently work across forks, so configure Wasmtime to
@@ -86,6 +89,21 @@ impl TrapHandler {
         // we're not very interested in so it's detached here.
         let thread = thread::spawn(|| handler_thread());
 
+        // Setup a SIGBUS handler which is used for printing that the stack was
+        // overflowed when a host overflows its fiber stack.
+        unsafe {
+            let mut handler: libc::sigaction = mem::zeroed();
+            handler.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK;
+            handler.sa_sigaction = sigbus_handler as usize;
+            libc::sigemptyset(&mut handler.sa_mask);
+            if libc::sigaction(libc::SIGBUS, &handler, PREV_SIGBUS.as_mut_ptr()) != 0 {
+                panic!(
+                    "unable to install signal handler: {}",
+                    io::Error::last_os_error(),
+                );
+            }
+        }
+
         TrapHandler {
             thread: Some(thread),
         }
@@ -102,6 +120,35 @@ impl Drop for TrapHandler {
     }
 }
 
+unsafe extern "C" fn sigbus_handler(
+    signum: libc::c_int,
+    siginfo: *mut libc::siginfo_t,
+    context: *mut libc::c_void,
+) {
+    // If this is a faulting address within the async guard range listed within
+    // our tls storage then print a helpful message about it and abort.
+    // Otherwise forward to the previous SIGBUS handler, if any.
+    tls::with(|info| {
+        let info = match info {
+            Some(info) => info,
+            None => return,
+        };
+        let faulting_addr = (*siginfo).si_addr() as usize;
+        let start = info.async_guard_range.start;
+        let end = info.async_guard_range.end;
+        if start as usize <= faulting_addr && faulting_addr < end as usize {
+            super::signals::abort_stack_overflow();
+        }
+    });
+
+    super::signals::delegate_signal_to_previous_handler(
+        PREV_SIGBUS.as_ptr(),
+        signum,
+        siginfo,
+        context,
+    )
+}
+
 // Note that this is copied from Gecko at
 //
 // https://searchfox.org/mozilla-central/rev/ed93119be4818da1509bbcb7b28e245853eeedd5/js/src/wasm/WasmSignalHandlers.cpp#583-601
diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs b/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs
index 0736c52e0..5198d6b05 100644
--- a/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs
+++ b/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs
@@ -180,7 +180,16 @@ unsafe extern "C" fn trap_handler(
         // exception was handled by a custom exception handler, so we
         // keep executing.
         let jmp_buf = match test {
-            TrapTest::NotWasm => return false,
+            TrapTest::NotWasm => {
+                if let Some(faulting_addr) = faulting_addr {
+                    let start = info.async_guard_range.start;
+                    let end = info.async_guard_range.end;
+                    if start as usize <= faulting_addr && faulting_addr < end as usize {
+                        abort_stack_overflow();
+                    }
+                }
+                return false;
+            }
             TrapTest::HandledByEmbedder => return true,
             TrapTest::Trap { jmp_buf } => jmp_buf,
         };
@@ -227,6 +236,15 @@ unsafe extern "C" fn trap_handler(
         return;
     }
 
+    delegate_signal_to_previous_handler(previous, signum, siginfo, context)
+}
+
+pub unsafe fn delegate_signal_to_previous_handler(
+    previous: *const libc::sigaction,
+    signum: libc::c_int,
+    siginfo: *mut libc::siginfo_t,
+    context: *mut libc::c_void,
+) {
     // This signal is not for any compiled wasm code we expect, so we
     // need to forward the signal to the next handler. If there is no
     // next handler (SIG_IGN or SIG_DFL), then it's time to crash. To do
@@ -248,6 +266,14 @@ unsafe extern "C" fn trap_handler(
     }
 }
 
+pub fn abort_stack_overflow() -> ! {
+    unsafe {
+        let msg = "execution on async fiber has overflowed its stack";
+        libc::write(libc::STDERR_FILENO, msg.as_ptr().cast(), msg.len());
+        libc::abort();
+    }
+}
+
 #[allow(clippy::cast_possible_truncation)] // too fiddly to handle and wouldn't
                                            // help much anyway
 unsafe fn get_trap_registers(cx: *mut libc::c_void, _signum: libc::c_int) -> TrapRegisters {
diff --git a/crates/wasmtime/src/runtime/vm/traphandlers.rs b/crates/wasmtime/src/runtime/vm/traphandlers.rs
index 087039bfe..0f405c03e 100644
--- a/crates/wasmtime/src/runtime/vm/traphandlers.rs
+++ b/crates/wasmtime/src/runtime/vm/traphandlers.rs
@@ -17,6 +17,7 @@ use crate::runtime::vm::{Instance, VMContext, VMRuntimeLimits};
 use crate::sync::RwLock;
 use core::cell::{Cell, UnsafeCell};
 use core::mem::MaybeUninit;
+use core::ops::Range;
 use core::ptr;
 
 pub use self::backtrace::Backtrace;
@@ -246,6 +247,7 @@ pub unsafe fn catch_traps<F>(
     signal_handler: Option<*const SignalHandler<'static>>,
     capture_backtrace: bool,
     capture_coredump: bool,
+    async_guard_range: Range<*mut u8>,
     caller: *mut VMContext,
     mut closure: F,
 ) -> Result<(), Box<Trap>>
@@ -254,15 +256,21 @@ where
 {
     let limits = Instance::from_vmctx(caller, |i| i.runtime_limits());
 
-    let result = CallThreadState::new(signal_handler, capture_backtrace, capture_coredump, *limits)
-        .with(|cx| {
-            traphandlers::wasmtime_setjmp(
-                cx.jmp_buf.as_ptr(),
-                call_closure::<F>,
-                &mut closure as *mut F as *mut u8,
-                caller,
-            )
-        });
+    let result = CallThreadState::new(
+        signal_handler,
+        capture_backtrace,
+        capture_coredump,
+        *limits,
+        async_guard_range,
+    )
+    .with(|cx| {
+        traphandlers::wasmtime_setjmp(
+            cx.jmp_buf.as_ptr(),
+            call_closure::<F>,
+            &mut closure as *mut F as *mut u8,
+            caller,
+        )
+    });
 
     return match result {
         Ok(x) => Ok(x),
@@ -302,6 +310,8 @@ mod call_thread_state {
         pub(crate) limits: *const VMRuntimeLimits,
 
         pub(super) prev: Cell<tls::Ptr>,
+        #[cfg_attr(any(windows, miri), allow(dead_code))]
+        pub(crate) async_guard_range: Range<*mut u8>,
 
         // The values of `VMRuntimeLimits::last_wasm_{exit_{pc,fp},entry_sp}`
         // for the *previous* `CallThreadState` for this same store/limits. Our
@@ -333,6 +343,7 @@ mod call_thread_state {
             capture_backtrace: bool,
             capture_coredump: bool,
             limits: *const VMRuntimeLimits,
+            async_guard_range: Range<*mut u8>,
         ) -> CallThreadState {
             let _ = capture_coredump;
 
@@ -344,6 +355,7 @@ mod call_thread_state {
                 #[cfg(feature = "coredump")]
                 capture_coredump,
                 limits,
+                async_guard_range,
                 prev: Cell::new(ptr::null()),
                 old_last_wasm_exit_fp: Cell::new(unsafe { *(*limits).last_wasm_exit_fp.get() }),
                 old_last_wasm_exit_pc: Cell::new(unsafe { *(*limits).last_wasm_exit_pc.get() }),
diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs
index fb64c0d76..ba3c1fb77 100644
--- a/src/bin/wasmtime.rs
+++ b/src/bin/wasmtime.rs
@@ -7,7 +7,7 @@ use anyhow::Result;
 use clap::Parser;
 
 /// Wasmtime WebAssembly Runtime
-#[derive(Parser, PartialEq)]
+#[derive(Parser)]
 #[command(
     name = "wasmtime",
     version = version(),
@@ -52,7 +52,7 @@ fn version() -> &'static str {
     option_env!("WASMTIME_VERSION_INFO").unwrap_or(env!("CARGO_PKG_VERSION"))
 }
 
-#[derive(Parser, PartialEq)]
+#[derive(Parser)]
 enum Subcommand {
     /// Runs a WebAssembly module
     #[cfg(feature = "run")]
@@ -81,6 +81,10 @@ enum Subcommand {
     /// Runs a WebAssembly test script file
     #[cfg(feature = "wast")]
     Wast(wasmtime_cli::commands::WastCommand),
+
+    /// Generate shell completions for the `wasmtime` CLI
+    #[cfg(feature = "completion")]
+    Completion(CompletionCommand),
 }
 
 impl Wasmtime {
@@ -112,10 +116,44 @@ impl Wasmtime {
 
             #[cfg(feature = "wast")]
             Subcommand::Wast(c) => c.execute(),
+
+            #[cfg(feature = "completion")]
+            Subcommand::Completion(c) => c.execute(),
         }
     }
 }
 
+/// Generate shell completion scripts for this CLI.
+///
+/// Shells have different paths for their completion scripts. Please refer to
+/// their documentation. For example, to generate completions for the fish
+/// shell, run the following command below:
+///
+///     wasmtime completion fish > ~/.config/fish/completions/wasmtime.fish
+///
+/// For a shell like zsh you can add this to your .zshrc or startup scripts:
+///
+///     eval "$(wasmtime completion zsh)"
+#[derive(Parser)]
+#[cfg(feature = "completion")]
+pub struct CompletionCommand {
+    /// The shell to generate completions for.
+    shell: clap_complete::Shell,
+}
+
+#[cfg(feature = "completion")]
+impl CompletionCommand {
+    pub fn execute(&self) -> Result<()> {
+        use clap::CommandFactory;
+
+        let mut cmd = Wasmtime::command();
+        let cli_name = cmd.get_name().to_owned();
+
+        clap_complete::generate(self.shell, &mut cmd, cli_name, &mut std::io::stdout());
+        Ok(())
+    }
+}
+
 fn main() -> Result<()> {
     return Wasmtime::parse().execute();
 }
diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml
index 90308e3df..b2cab043e 100644
--- a/supply-chain/audits.toml
+++ b/supply-chain/audits.toml
@@ -1329,6 +1329,12 @@ criteria = "safe-to-deploy"
 version = "0.7.1"
 notes = "This library has many uses of unsafe, but contains extensive fuzzing and miri testing."
 
+[[audits.console]]
+who = "Alex Crichton <alex@alexcrichton.com>"
+criteria = "safe-to-deploy"
+delta = "0.15.0 -> 0.15.8"
+notes = "Lots of internal refactorings for new features and such, nothing major out of place."
+
 [[audits.constant_time_eq]]
 who = "Nick Fitzgerald <fitzgen@gmail.com>"
 criteria = "safe-to-deploy"
@@ -1415,6 +1421,18 @@ other words everything is as one would expect from a minor update for this
 crate.
 """
 
+[[audits.crossbeam-epoch]]
+who = "Alex Crichton <alex@alexcrichton.com>"
+criteria = "safe-to-deploy"
+delta = "0.9.15 -> 0.9.18"
+notes = "Nontrivial update but mostly around dependencies and how `unsafe` code is managed. Everything looks the same shape as before."
+
+[[audits.crossbeam-utils]]
+who = "Alex Crichton <alex@alexcrichton.com>"
+criteria = "safe-to-deploy"
+delta = "0.8.10 -> 0.8.11"
+notes = "No changes of substance, only minor updates around how `unsafe` code is managed but it's all trivially the same as before"
+
 [[audits.crypto-common]]
 who = "Benjamin Bouvier <public@benj.me>"
 criteria = "safe-to-deploy"
@@ -2695,6 +2713,18 @@ who = "Pat Hickey <phickey@fastly.com>"
 criteria = "safe-to-deploy"
 version = "0.2.11"
 
+[[audits.test-log]]
+who = "Alex Crichton <alex@alexcrichton.com>"
+criteria = "safe-to-run"
+delta = "0.2.11 -> 0.2.16"
+notes = "Crate implementation was moved to a `*-macros` crate, crate is very small as a result."
+
+[[audits.test-log-macros]]
+who = "Alex Crichton <alex@alexcrichton.com>"
+criteria = "safe-to-run"
+version = "0.2.16"
+notes = "Simple procedural macro copied from its previous source."
+
 [[audits.thread_local]]
 who = "Pat Hickey <phickey@fastly.com>"
 criteria = "safe-to-deploy"
@@ -3784,6 +3814,12 @@ user-id = 6743 # Ed Page (epage)
 start = "2023-03-28"
 end = "2025-09-20"
 
+[[trusted.clap_complete]]
+criteria = "safe-to-deploy"
+user-id = 6743 # Ed Page (epage)
+start = "2021-12-31"
+end = "2025-09-25"
+
 [[trusted.clap_derive]]
 criteria = "safe-to-deploy"
 user-id = 6743 # Ed Page (epage)
diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock
index 6886cf2aa..11d30ed2c 100644
--- a/supply-chain/imports.lock
+++ b/supply-chain/imports.lock
@@ -815,6 +815,13 @@ user-id = 6743
 user-login = "epage"
 user-name = "Ed Page"
 
+[[publisher.clap_complete]]
+version = "4.5.28"
+when = "2024-09-17"
+user-id = 6743
+user-login = "epage"
+user-name = "Ed Page"
+
 [[publisher.clap_derive]]
 version = "4.3.12"
 when = "2023-07-14"
@@ -1387,13 +1394,6 @@ user-id = 189
 user-login = "BurntSushi"
 user-name = "Andrew Gallant"
 
-[[publisher.scopeguard]]
-version = "1.1.0"
-when = "2020-02-16"
-user-id = 2915
-user-login = "Amanieu"
-user-name = "Amanieu d'Antras"
-
 [[publisher.serde]]
 version = "1.0.188"
 when = "2023-08-26"
@@ -2810,6 +2810,25 @@ criteria = "safe-to-deploy"
 delta = "1.0.78 -> 1.0.83"
 aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml"
 
+[[audits.mozilla.audits.crossbeam-utils]]
+who = "Mike Hommey <mh+mozilla@glandium.org>"
+criteria = "safe-to-deploy"
+delta = "0.8.11 -> 0.8.14"
+aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
+
+[[audits.mozilla.audits.crossbeam-utils]]
+who = "Jan-Erik Rediger <jrediger@mozilla.com>"
+criteria = "safe-to-deploy"
+delta = "0.8.14 -> 0.8.19"
+aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml"
+
+[[audits.mozilla.audits.crossbeam-utils]]
+who = "Alex Franchuk <afranchuk@mozilla.com>"
+criteria = "safe-to-deploy"
+delta = "0.8.19 -> 0.8.20"
+notes = "Minor changes."
+aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
+
 [[audits.mozilla.audits.crypto-common]]
 who = "Mike Hommey <mh+mozilla@glandium.org>"
 criteria = "safe-to-deploy"


Test patch:
diff --git a/tests/all/stack_creator.rs b/tests/all/stack_creator.rs
index 9545908d1..e05629aee 100644
--- a/tests/all/stack_creator.rs
+++ b/tests/all/stack_creator.rs
@@ -31,6 +31,9 @@ unsafe impl StackMemory for CustomStack {
         let base = self.base.as_ptr() as usize;
         base..base + self.len
     }
+    fn guard_range(&self) -> Range<*mut u8> {
+        std::ptr::null_mut()..std::ptr::null_mut()
+    }
 }
 
 // A creator that allocates stacks on the heap instead of mmap'ing.
diff --git a/tests/host_segfault.rs b/tests/host_segfault.rs
index 1880e1039..9ea913083 100644
--- a/tests/host_segfault.rs
+++ b/tests/host_segfault.rs
@@ -95,6 +95,23 @@ fn main() {
         return;
     }
 
+    // Disable core dumps on Unix to avoid spamming the system core dump utility
+    // and having this test take longer.
+    #[cfg(unix)]
+    unsafe {
+        let zero = libc::rlimit {
+            rlim_cur: 0,
+            rlim_max: 0,
+        };
+        let rc = libc::setrlimit(libc::RLIMIT_CORE, &zero);
+        assert_eq!(
+            rc,
+            0,
+            "failed to disable core dumps: {}",
+            std::io::Error::last_os_error()
+        );
+    }
+
     let tests: &[(&str, fn(), bool)] = &[
         ("normal segfault", || segfault(), false),
         (
@@ -263,12 +280,16 @@ fn is_segfault(status: &ExitStatus) -> bool {
 fn is_stack_overflow(status: &ExitStatus, stderr: &str) -> bool {
     use std::os::unix::prelude::*;
 
-    // The main thread might overflow or it might be from a fiber stack (SIGSEGV/SIGBUS)
+    // The exit status should always be SIGABRT, not SIGSEGV. Something, be it
+    // the standard library or Wasmtime, should catch the original SIGSEGV or
+    // SIGBUS and abort instead.
+    match status.signal() {
+        Some(libc::SIGABRT) => {}
+        _ => return false,
+    }
+
+    // A helpful message should additionally be printed at all times.
     stderr.contains("has overflowed its stack")
-        || match status.signal() {
-            Some(libc::SIGSEGV) | Some(libc::SIGBUS) => true,
-            _ => false,
-        }
 }
 
 #[cfg(windows)]