diff --git a/Cargo.toml b/Cargo.toml index 36f80cc666b9..7625c634c62a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ wasi-crypto = ["wasmtime-wasi-crypto"] wasi-nn = ["wasmtime-wasi-nn"] uffd = ["wasmtime/uffd"] all-arch = ["wasmtime/all-arch"] +posix-signals-on-macos = ["wasmtime/posix-signals-on-macos"] # Stub feature that does nothing, for Cargo-features compatibility: the new # backend is the default now. diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 8e5174b32a27..94072a4daf82 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -49,3 +49,8 @@ async = ["wasmtime-fiber"] # Enables support for userfaultfd in the pooling allocator when building on Linux uffd = ["userfaultfd"] + +# Enables trap handling using POSIX signals instead of Mach exceptions on MacOS. +# It is useful for applications that do not bind their own exception ports and +# need portable signal handling. +posix-signals-on-macos = [] diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index 75f7a7d6d471..04496d3bc5bb 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -24,7 +24,7 @@ extern "C" { } cfg_if::cfg_if! { - if #[cfg(target_os = "macos")] { + if #[cfg(all(target_os = "macos", not(feature = "posix-signals-on-macos")))] { mod macos; use macos as sys; } else if #[cfg(unix)] { diff --git a/crates/runtime/src/traphandlers/unix.rs b/crates/runtime/src/traphandlers/unix.rs index b4046f094429..95ca473fa467 100644 --- a/crates/runtime/src/traphandlers/unix.rs +++ b/crates/runtime/src/traphandlers/unix.rs @@ -53,7 +53,8 @@ pub unsafe fn platform_init() { } // On ARM, handle Unaligned Accesses. - if cfg!(target_arch = "arm") || cfg!(target_os = "freebsd") { + // On Darwin, guard page accesses are raised as SIGBUS. + if cfg!(target_arch = "arm") || cfg!(target_os = "macos") || cfg!(target_os = "freebsd") { register(&mut PREV_SIGBUS, libc::SIGBUS); } } @@ -99,7 +100,44 @@ unsafe extern "C" fn trap_handler( return true; } info.capture_backtrace(pc); - Unwind(jmp_buf) + // On macOS this is a bit special, unfortunately. If we were to + // `siglongjmp` out of the signal handler that notably does + // *not* reset the sigaltstack state of our signal handler. This + // seems to trick the kernel into thinking that the sigaltstack + // is still in use upon delivery of the next signal, meaning + // that the sigaltstack is not ever used again if we immediately + // call `Unwind` here. + // + // Note that if we use `longjmp` instead of `siglongjmp` then + // the problem is fixed. The problem with that, however, is that + // `setjmp` is much slower than `sigsetjmp` due to the + // preservation of the proceses signal mask. The reason + // `longjmp` appears to work is that it seems to call a function + // (according to published macOS sources) called + // `_sigunaltstack` which updates the kernel to say the + // sigaltstack is no longer in use. We ideally want to call that + // here but I don't think there's a stable way for us to call + // that. + // + // Given all that, on macOS only, we do the next best thing. We + // return from the signal handler after updating the register + // context. This will cause control to return to our + // `unwind_shim` function defined here which will perform the + // `Unwind` (`siglongjmp`) for us. The reason this works is that + // by returning from the signal handler we'll trigger all the + // normal machinery for "the signal handler is done running" + // which will clear the sigaltstack flag and allow reusing it + // for the next signal. Then upon resuming in our custom code we + // blow away the stack anyway with a longjmp. + if cfg!(target_os = "macos") { + unsafe extern "C" fn unwind_shim(jmp_buf: *const u8) { + Unwind(jmp_buf) + } + set_pc(context, unwind_shim as usize, jmp_buf as usize); + return true; + } else { + Unwind(jmp_buf) + } }); if handled { @@ -155,6 +193,15 @@ unsafe fn get_pc(cx: *mut libc::c_void, _signum: libc::c_int) -> *const u8 { }; let cx = &*(cx as *const libc::ucontext_t); (cx.uc_mcontext.psw.addr - trap_offset) as *const u8 + } else if #[cfg(all(target_os = "macos", target_arch = "x86_64"))] { + let cx = &*(cx as *const libc::ucontext_t); + (*cx.uc_mcontext).__ss.__rip as *const u8 + } else if #[cfg(all(target_os = "macos", target_arch = "x86"))] { + let cx = &*(cx as *const libc::ucontext_t); + (*cx.uc_mcontext).__ss.__eip as *const u8 + } else if #[cfg(all(target_os = "macos", target_arch = "aarch64"))] { + let cx = &*(cx as *const libc::ucontext_t); + (*cx.uc_mcontext).__ss.__pc as *const u8 } else if #[cfg(all(target_os = "freebsd", target_arch = "x86_64"))] { let cx = &*(cx as *const libc::ucontext_t); cx.uc_mcontext.mc_rip as *const u8 @@ -164,6 +211,41 @@ unsafe fn get_pc(cx: *mut libc::c_void, _signum: libc::c_int) -> *const u8 { } } +// This is only used on macOS targets for calling an unwinding shim +// function to ensure that we return from the signal handler. +// +// See more comments above where this is called for what it's doing. +unsafe fn set_pc(cx: *mut libc::c_void, pc: usize, arg1: usize) { + cfg_if::cfg_if! { + if #[cfg(not(target_os = "macos"))] { + drop((cx, pc, arg1)); + unreachable!(); // not used on these platforms + } else if #[cfg(target_arch = "x86_64")] { + let cx = &mut *(cx as *mut libc::ucontext_t); + (*cx.uc_mcontext).__ss.__rip = pc as u64; + (*cx.uc_mcontext).__ss.__rdi = arg1 as u64; + // We're simulating a "pseudo-call" so we need to ensure + // stack alignment is properly respected, notably that on a + // `call` instruction the stack is 8/16-byte aligned, then + // the function adjusts itself to be 16-byte aligned. + // + // Most of the time the stack pointer is 16-byte aligned at + // the time of the trap but for more robust-ness with JIT + // code where it may ud2 in a prologue check before the + // stack is aligned we double-check here. + if (*cx.uc_mcontext).__ss.__rsp % 16 == 0 { + (*cx.uc_mcontext).__ss.__rsp -= 8; + } + } else if #[cfg(target_arch = "aarch64")] { + let cx = &mut *(cx as *mut libc::ucontext_t); + (*cx.uc_mcontext).__ss.__pc = pc as u64; + (*cx.uc_mcontext).__ss.__x[0] = arg1 as u64; + } else { + compile_error!("unsupported macos target architecture"); + } + } +} + /// A function for registering a custom alternate signal stack (sigaltstack). /// /// Rust's libstd installs an alternate stack with size `SIGSTKSZ`, which is not diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index e681a7fdfe42..e00a09b681cf 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -82,3 +82,8 @@ uffd = ["wasmtime-runtime/uffd"] # Enables support for all architectures in JIT and the `wasmtime compile` CLI command. all-arch = ["wasmtime-jit/all-arch"] + +# Enables trap handling using POSIX signals instead of Mach exceptions on MacOS. +# It is useful for applications that do not bind their own exception ports and +# need portable signal handling. +posix-signals-on-macos = ["wasmtime-runtime/posix-signals-on-macos"] diff --git a/crates/wasmtime/src/lib.rs b/crates/wasmtime/src/lib.rs index 59d175a488ed..8ffd44e0ef51 100644 --- a/crates/wasmtime/src/lib.rs +++ b/crates/wasmtime/src/lib.rs @@ -311,7 +311,7 @@ pub use crate::types::*; pub use crate::values::*; cfg_if::cfg_if! { - if #[cfg(target_os = "macos")] { + if #[cfg(all(target_os = "macos", not(feature = "posix-signals-on-macos")))] { // no extensions for macOS at this time } else if #[cfg(unix)] { pub mod unix; diff --git a/tests/all/custom_signal_handler.rs b/tests/all/custom_signal_handler.rs index 2d4243123eff..f1fa3bddae40 100644 --- a/tests/all/custom_signal_handler.rs +++ b/tests/all/custom_signal_handler.rs @@ -1,4 +1,7 @@ -#[cfg(target_os = "linux")] +#[cfg(any( + target_os = "linux", + all(target_os = "macos", feature = "posix-signals-on-macos") +))] mod tests { use anyhow::Result; use std::rc::Rc;