Mach ports continued + support aarch64-apple unwinding (#2723)
* Switch macOS to using mach ports for trap handling This commit moves macOS to using mach ports instead of signals for handling traps. The motivation for this is listed in #2456, namely that once mach ports are used in a process that means traditional UNIX signal handlers won't get used. This means that if Wasmtime is integrated with Breakpad, for example, then Wasmtime's trap handler never fires and traps don't work. The `traphandlers` module is refactored as part of this commit to split the platform-specific bits into their own files (it was growing quite a lot for one inline `cfg_if!`). The `unix.rs` and `windows.rs` files remain the same as they were before with a few minor tweaks for some refactored interfaces. The `macos.rs` file is brand new and lifts almost its entire implementation from SpiderMonkey, adapted for Wasmtime though. The main gotcha with mach ports is that a separate thread is what services the exception. Some unsafe magic allows this separate thread to read non-`Send` and temporary state from other threads, but is hoped to be safe in this context. The unfortunate downside is that calling wasm on macOS now involves taking a global lock and modifying a global hash map twice-per-call. I'm not entirely sure how to get out of this cost for now, but hopefully for any embeddings on macOS it's not the end of the world. Closes #2456 * Add a sketch of arm64 apple support * store: maintain CallThreadState mapping when switching fibers * cranelift/aarch64: generate unwind directives to disable pointer auth Aarch64 post ARMv8.3 has a feature called pointer authentication, designed to fight ROP/JOP attacks: some pointers may be signed using new instructions, adding payloads to the high (previously unused) bits of the pointers. More on this here: https://lwn.net/Articles/718888/ Unwinders on aarch64 need to know if some pointers contained on the call frame contain an authentication code or not, to be able to properly authenticate them or use them directly. Since native code may have enabled it by default (as is the case on the Mac M1), and the default is that this configuration value is inherited, we need to explicitly disable it, for the only kind of supported pointers (return addresses). To do so, we set the value of a non-existing dwarf pseudo register (34) to 0, as documented in https://github.com/ARM-software/abi-aa/blob/master/aadwarf64/aadwarf64.rst#note-8. This is done at the function granularity, in the spirit of Cranelift compilation model. Alternatively, a single directive could be generated in the CIE, generating less information per module. * Make exception handling work on Mac aarch64 too * fibers: use a breakpoint instruction after the final call in wasmtime_fiber_start Co-authored-by: Alex Crichton <alex@alexcrichton.com>
This commit is contained in:
257
crates/runtime/src/traphandlers/unix.rs
Normal file
257
crates/runtime/src/traphandlers/unix.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
use crate::traphandlers::{tls, CallThreadState, Trap, Unwind};
|
||||
use std::cell::RefCell;
|
||||
use std::convert::TryInto;
|
||||
use std::io;
|
||||
use std::mem::{self, MaybeUninit};
|
||||
use std::ptr::{self, null_mut};
|
||||
|
||||
/// Function which may handle custom signals while processing traps.
|
||||
pub type SignalHandler<'a> =
|
||||
dyn Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool + 'a;
|
||||
|
||||
static mut PREV_SIGSEGV: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
|
||||
static mut PREV_SIGBUS: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
|
||||
static mut PREV_SIGILL: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
|
||||
static mut PREV_SIGFPE: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
|
||||
|
||||
pub unsafe fn platform_init() {
|
||||
let register = |slot: &mut MaybeUninit<libc::sigaction>, signal: i32| {
|
||||
let mut handler: libc::sigaction = mem::zeroed();
|
||||
// The flags here are relatively careful, and they are...
|
||||
//
|
||||
// SA_SIGINFO gives us access to information like the program
|
||||
// counter from where the fault happened.
|
||||
//
|
||||
// SA_ONSTACK allows us to handle signals on an alternate stack,
|
||||
// so that the handler can run in response to running out of
|
||||
// stack space on the main stack. Rust installs an alternate
|
||||
// stack with sigaltstack, so we rely on that.
|
||||
//
|
||||
// SA_NODEFER allows us to reenter the signal handler if we
|
||||
// crash while handling the signal, and fall through to the
|
||||
// Breakpad handler by testing handlingSegFault.
|
||||
handler.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER | libc::SA_ONSTACK;
|
||||
handler.sa_sigaction = trap_handler as usize;
|
||||
libc::sigemptyset(&mut handler.sa_mask);
|
||||
if libc::sigaction(signal, &handler, slot.as_mut_ptr()) != 0 {
|
||||
panic!(
|
||||
"unable to install signal handler: {}",
|
||||
io::Error::last_os_error(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Allow handling OOB with signals on all architectures
|
||||
register(&mut PREV_SIGSEGV, libc::SIGSEGV);
|
||||
|
||||
// Handle `unreachable` instructions which execute `ud2` right now
|
||||
register(&mut PREV_SIGILL, libc::SIGILL);
|
||||
|
||||
// x86 uses SIGFPE to report division by zero
|
||||
if cfg!(target_arch = "x86") || cfg!(target_arch = "x86_64") {
|
||||
register(&mut PREV_SIGFPE, libc::SIGFPE);
|
||||
}
|
||||
|
||||
// On ARM, handle Unaligned Accesses.
|
||||
if cfg!(target_arch = "arm") || cfg!(target_os = "freebsd") {
|
||||
register(&mut PREV_SIGBUS, libc::SIGBUS);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn trap_handler(
|
||||
signum: libc::c_int,
|
||||
siginfo: *mut libc::siginfo_t,
|
||||
context: *mut libc::c_void,
|
||||
) {
|
||||
let previous = match signum {
|
||||
libc::SIGSEGV => &PREV_SIGSEGV,
|
||||
libc::SIGBUS => &PREV_SIGBUS,
|
||||
libc::SIGFPE => &PREV_SIGFPE,
|
||||
libc::SIGILL => &PREV_SIGILL,
|
||||
_ => panic!("unknown signal: {}", signum),
|
||||
};
|
||||
let handled = tls::with(|info| {
|
||||
// If no wasm code is executing, we don't handle this as a wasm
|
||||
// trap.
|
||||
let info = match info {
|
||||
Some(info) => info,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
// If we hit an exception while handling a previous trap, that's
|
||||
// quite bad, so bail out and let the system handle this
|
||||
// recursive segfault.
|
||||
//
|
||||
// Otherwise flag ourselves as handling a trap, do the trap
|
||||
// handling, and reset our trap handling flag. Then we figure
|
||||
// out what to do based on the result of the trap handling.
|
||||
let pc = get_pc(context);
|
||||
let jmp_buf =
|
||||
info.jmp_buf_if_trap(pc, |handler| handler(signum, siginfo, context));
|
||||
|
||||
// Figure out what to do based on the result of this handling of
|
||||
// the trap. Note that our sentinel value of 1 means that the
|
||||
// exception was handled by a custom exception handler, so we
|
||||
// keep executing.
|
||||
if jmp_buf.is_null() {
|
||||
return false;
|
||||
}
|
||||
if jmp_buf as usize == 1 {
|
||||
return true;
|
||||
}
|
||||
info.capture_backtrace(pc);
|
||||
Unwind(jmp_buf)
|
||||
});
|
||||
|
||||
if handled {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
// this, we set the signal back to its original disposition and
|
||||
// return. This will cause the faulting op to be re-executed which
|
||||
// will crash in the normal way. If there is a next handler, call
|
||||
// it. It will either crash synchronously, fix up the instruction
|
||||
// so that execution can continue and return, or trigger a crash by
|
||||
// returning the signal to it's original disposition and returning.
|
||||
let previous = &*previous.as_ptr();
|
||||
if previous.sa_flags & libc::SA_SIGINFO != 0 {
|
||||
mem::transmute::<usize, extern "C" fn(libc::c_int, *mut libc::siginfo_t, *mut libc::c_void)>(
|
||||
previous.sa_sigaction,
|
||||
)(signum, siginfo, context)
|
||||
} else if previous.sa_sigaction == libc::SIG_DFL || previous.sa_sigaction == libc::SIG_IGN {
|
||||
libc::sigaction(signum, previous, ptr::null_mut());
|
||||
} else {
|
||||
mem::transmute::<usize, extern "C" fn(libc::c_int)>(previous.sa_sigaction)(signum)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_pc(cx: *mut libc::c_void) -> *const u8 {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(target_os = "linux", target_arch = "x86_64"))] {
|
||||
let cx = &*(cx as *const libc::ucontext_t);
|
||||
cx.uc_mcontext.gregs[libc::REG_RIP as usize] as *const u8
|
||||
} else if #[cfg(all(target_os = "linux", target_arch = "x86"))] {
|
||||
let cx = &*(cx as *const libc::ucontext_t);
|
||||
cx.uc_mcontext.gregs[libc::REG_EIP as usize] as *const u8
|
||||
} else if #[cfg(all(any(target_os = "linux", target_os = "android"), target_arch = "aarch64"))] {
|
||||
let cx = &*(cx as *const libc::ucontext_t);
|
||||
cx.uc_mcontext.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
|
||||
} else {
|
||||
compile_error!("unsupported platform");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A function for registering a custom alternate signal stack (sigaltstack).
|
||||
///
|
||||
/// Rust's libstd installs an alternate stack with size `SIGSTKSZ`, which is not
|
||||
/// always large enough for our signal handling code. Override it by creating
|
||||
/// and registering our own alternate stack that is large enough and has a guard
|
||||
/// page.
|
||||
pub fn lazy_per_thread_init() -> Result<(), Trap> {
|
||||
thread_local! {
|
||||
/// Thread-local state is lazy-initialized on the first time it's used,
|
||||
/// and dropped when the thread exits.
|
||||
static TLS: RefCell<Tls> = RefCell::new(Tls::None);
|
||||
}
|
||||
|
||||
/// The size of the sigaltstack (not including the guard, which will be
|
||||
/// added). Make this large enough to run our signal handlers.
|
||||
const MIN_STACK_SIZE: usize = 16 * 4096;
|
||||
|
||||
enum Tls {
|
||||
None,
|
||||
Allocated {
|
||||
mmap_ptr: *mut libc::c_void,
|
||||
mmap_size: usize,
|
||||
},
|
||||
BigEnough,
|
||||
}
|
||||
|
||||
return TLS.with(|slot| unsafe {
|
||||
let mut slot = slot.borrow_mut();
|
||||
match *slot {
|
||||
Tls::None => {}
|
||||
// already checked
|
||||
_ => return Ok(()),
|
||||
}
|
||||
|
||||
// Check to see if the existing sigaltstack, if it exists, is big
|
||||
// enough. If so we don't need to allocate our own.
|
||||
let mut old_stack = mem::zeroed();
|
||||
let r = libc::sigaltstack(ptr::null(), &mut old_stack);
|
||||
assert_eq!(r, 0, "learning about sigaltstack failed");
|
||||
if old_stack.ss_flags & libc::SS_DISABLE == 0 && old_stack.ss_size >= MIN_STACK_SIZE {
|
||||
*slot = Tls::BigEnough;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// ... but failing that we need to allocate our own, so do all that
|
||||
// here.
|
||||
let page_size: usize = libc::sysconf(libc::_SC_PAGESIZE).try_into().unwrap();
|
||||
let guard_size = page_size;
|
||||
let alloc_size = guard_size + MIN_STACK_SIZE;
|
||||
|
||||
let ptr = libc::mmap(
|
||||
null_mut(),
|
||||
alloc_size,
|
||||
libc::PROT_NONE,
|
||||
libc::MAP_PRIVATE | libc::MAP_ANON,
|
||||
-1,
|
||||
0,
|
||||
);
|
||||
if ptr == libc::MAP_FAILED {
|
||||
return Err(Trap::oom());
|
||||
}
|
||||
|
||||
// Prepare the stack with readable/writable memory and then register it
|
||||
// with `sigaltstack`.
|
||||
let stack_ptr = (ptr as usize + guard_size) as *mut libc::c_void;
|
||||
let r = libc::mprotect(
|
||||
stack_ptr,
|
||||
MIN_STACK_SIZE,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
);
|
||||
assert_eq!(r, 0, "mprotect to configure memory for sigaltstack failed");
|
||||
let new_stack = libc::stack_t {
|
||||
ss_sp: stack_ptr,
|
||||
ss_flags: 0,
|
||||
ss_size: MIN_STACK_SIZE,
|
||||
};
|
||||
let r = libc::sigaltstack(&new_stack, ptr::null_mut());
|
||||
assert_eq!(r, 0, "registering new sigaltstack failed");
|
||||
|
||||
*slot = Tls::Allocated {
|
||||
mmap_ptr: ptr,
|
||||
mmap_size: alloc_size,
|
||||
};
|
||||
Ok(())
|
||||
});
|
||||
|
||||
impl Drop for Tls {
|
||||
fn drop(&mut self) {
|
||||
let (ptr, size) = match self {
|
||||
Tls::Allocated {
|
||||
mmap_ptr,
|
||||
mmap_size,
|
||||
} => (*mmap_ptr, *mmap_size),
|
||||
_ => return,
|
||||
};
|
||||
unsafe {
|
||||
// Deallocate the stack memory.
|
||||
let r = libc::munmap(ptr, size);
|
||||
debug_assert_eq!(r, 0, "munmap failed during thread shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_tls(_: *const CallThreadState<'static>) {
|
||||
// Unused on unix
|
||||
}
|
||||
Reference in New Issue
Block a user