Remove C++ dependency from wasmtime (#1365)
* Remove C++ dependency from `wasmtime` This commit removes the last wads of C++ that we have in wasmtime, meaning that building wasmtime no longer requires a C++ compiler. It still does require a C toolchain for some minor purposes, but hopefully we can remove that over time too! The motivation for doing this is to consolidate all our signal-handling code into one location in one language so you don't have to keep crossing back and forth when understanding what's going on. This also allows us to remove some extra cruft that wasn't necessary from the C++ original implementation. Additionally this should also make building wasmtime a bit more portable since it's often easier to acquire a C toolchain than it is to acquire a C++ toolchain. (e.g. if you're cross-compiling to a musl target) * Typos
This commit is contained in:
28
crates/runtime/src/helpers.c
Normal file
28
crates/runtime/src/helpers.c
Normal file
@@ -0,0 +1,28 @@
|
||||
#include <setjmp.h>
|
||||
|
||||
int RegisterSetjmp(
|
||||
void **buf_storage,
|
||||
void (*body)(void*),
|
||||
void *payload) {
|
||||
jmp_buf buf;
|
||||
if (setjmp(buf) != 0) {
|
||||
return 0;
|
||||
}
|
||||
*buf_storage = &buf;
|
||||
body(payload);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Unwind(void *JmpBuf) {
|
||||
jmp_buf *buf = (jmp_buf*) JmpBuf;
|
||||
longjmp(*buf, 1);
|
||||
}
|
||||
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <sys/ucontext.h>
|
||||
|
||||
void* GetPcFromUContext(ucontext_t *cx) {
|
||||
return (void*) cx->uc_mcontext->__ss.__rip;
|
||||
}
|
||||
#endif
|
||||
@@ -6,8 +6,8 @@ use crate::export::Export;
|
||||
use crate::imports::Imports;
|
||||
use crate::jit_int::GdbJitImageRegistration;
|
||||
use crate::memory::LinearMemory;
|
||||
use crate::signalhandlers;
|
||||
use crate::table::Table;
|
||||
use crate::traphandlers;
|
||||
use crate::traphandlers::{catch_traps, Trap};
|
||||
use crate::vmcontext::{
|
||||
VMBuiltinFunctionsArray, VMCallerCheckedAnyfunc, VMContext, VMFunctionBody, VMFunctionImport,
|
||||
@@ -980,7 +980,7 @@ impl InstanceHandle {
|
||||
|
||||
// Ensure that our signal handlers are ready for action.
|
||||
// TODO: Move these calls out of `InstanceHandle`.
|
||||
signalhandlers::init();
|
||||
traphandlers::init();
|
||||
|
||||
// The WebAssembly spec specifies that the start function is
|
||||
// invoked automatically at instantiation time.
|
||||
|
||||
@@ -28,7 +28,6 @@ mod jit_int;
|
||||
mod memory;
|
||||
mod mmap;
|
||||
mod sig_registry;
|
||||
mod signalhandlers;
|
||||
mod table;
|
||||
mod trap_registry;
|
||||
mod traphandlers;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
//! Interface to low-level signal-handling mechanisms.
|
||||
|
||||
use std::sync::Once;
|
||||
|
||||
extern "C" {
|
||||
fn EnsureEagerSignalHandlers() -> libc::c_int;
|
||||
}
|
||||
|
||||
/// This function performs the low-overhead signal handler initialization that
|
||||
/// we want to do eagerly to ensure a more-deterministic global process state.
|
||||
///
|
||||
/// This is especially relevant for signal handlers since handler ordering
|
||||
/// depends on installation order: the wasm signal handler must run *before*
|
||||
/// the other crash handlers and since POSIX signal handlers work LIFO, this
|
||||
/// function needs to be called at the end of the startup process, after other
|
||||
/// handlers have been installed. This function can thus be called multiple
|
||||
/// times, having no effect after the first call.
|
||||
pub fn init() {
|
||||
static INIT: Once = Once::new();
|
||||
INIT.call_once(real_init);
|
||||
}
|
||||
|
||||
fn real_init() {
|
||||
// This is a really weird and unfortunate function call. For all the gory
|
||||
// details see #829, but the tl;dr; is that in a trap handler we have 2
|
||||
// pages of stack space on Linux, and calling into libunwind which triggers
|
||||
// the dynamic loader blows the stack.
|
||||
//
|
||||
// This is a dumb hack to work around this system-specific issue by
|
||||
// capturing a backtrace once in the lifetime of a process to ensure that
|
||||
// when we capture a backtrace in the trap handler all caches are primed,
|
||||
// aka the dynamic loader has resolved all the relevant symbols.
|
||||
drop(backtrace::Backtrace::new_unresolved());
|
||||
|
||||
if unsafe { EnsureEagerSignalHandlers() == 0 } {
|
||||
panic!("failed to install signal handlers");
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ use std::any::Any;
|
||||
use std::cell::Cell;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::ptr;
|
||||
use std::sync::Once;
|
||||
use wasmtime_environ::ir;
|
||||
|
||||
extern "C" {
|
||||
@@ -23,40 +25,248 @@ extern "C" {
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(unix)] {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn HandleTrap(
|
||||
pc: *mut u8,
|
||||
use std::mem::{self, MaybeUninit};
|
||||
|
||||
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();
|
||||
|
||||
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.
|
||||
// On Darwin, guard page accesses are raised as SIGBUS.
|
||||
if cfg!(target_arch = "arm") || cfg!(target_os = "macos") {
|
||||
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,
|
||||
) -> *const u8 {
|
||||
tls::with(|info| {
|
||||
match info {
|
||||
Some(info) => info.handle_trap(pc, false, |handler| handler(signum, siginfo, context)),
|
||||
None => ptr::null(),
|
||||
) {
|
||||
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 jmp_buf = info.handle_trap(
|
||||
get_pc(context),
|
||||
false,
|
||||
|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;
|
||||
} else if jmp_buf as usize == 1 {
|
||||
return true;
|
||||
} else {
|
||||
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(target_os = "macos")] {
|
||||
// FIXME(rust-lang/libc#1702) - once that lands and is
|
||||
// released we should inline the definition here
|
||||
extern "C" {
|
||||
fn GetPcFromUContext(cx: *mut libc::c_void) -> *const u8;
|
||||
}
|
||||
GetPcFromUContext(cx)
|
||||
} else {
|
||||
compile_error!("unsupported platform");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if #[cfg(target_os = "windows")] {
|
||||
use winapi::um::winnt::PEXCEPTION_POINTERS;
|
||||
use winapi::um::minwinbase::EXCEPTION_STACK_OVERFLOW;
|
||||
use winapi::um::errhandlingapi::*;
|
||||
use winapi::um::winnt::*;
|
||||
use winapi::um::minwinbase::*;
|
||||
use winapi::vc::excpt::*;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn HandleTrap(
|
||||
pc: *mut u8,
|
||||
unsafe fn platform_init() {
|
||||
// our trap handler needs to go first, so that we can recover from
|
||||
// wasm faults and continue execution, so pass `1` as a true value
|
||||
// here.
|
||||
if AddVectoredExceptionHandler(1, Some(exception_handler)).is_null() {
|
||||
panic!("failed to add exception handler: {}", io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "system" fn exception_handler(
|
||||
exception_info: PEXCEPTION_POINTERS
|
||||
) -> *const u8 {
|
||||
) -> LONG {
|
||||
// Check the kind of exception, since we only handle a subset within
|
||||
// wasm code. If anything else happens we want to defer to whatever
|
||||
// the rest of the system wants to do for this exception.
|
||||
let record = &*(*exception_info).ExceptionRecord;
|
||||
if record.ExceptionCode != EXCEPTION_ACCESS_VIOLATION &&
|
||||
record.ExceptionCode != EXCEPTION_ILLEGAL_INSTRUCTION &&
|
||||
record.ExceptionCode != EXCEPTION_STACK_OVERFLOW &&
|
||||
record.ExceptionCode != EXCEPTION_INT_DIVIDE_BY_ZERO &&
|
||||
record.ExceptionCode != EXCEPTION_INT_OVERFLOW
|
||||
{
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
// FIXME: this is what the previous C++ did to make sure that TLS
|
||||
// works by the time we execute this trap handling code. This isn't
|
||||
// exactly super easy to call from Rust though and it's not clear we
|
||||
// necessarily need to do so. Leaving this here in case we need this
|
||||
// in the future, but for now we can probably wait until we see a
|
||||
// strange fault before figuring out how to reimplement this in
|
||||
// Rust.
|
||||
//
|
||||
// if (!NtCurrentTeb()->Reserved1[sThreadLocalArrayPointerIndex]) {
|
||||
// return EXCEPTION_CONTINUE_SEARCH;
|
||||
// }
|
||||
|
||||
// This is basically the same as the unix version above, only with a
|
||||
// few parameters tweaked here and there.
|
||||
tls::with(|info| {
|
||||
let reset_guard_page = (*(*exception_info).ExceptionRecord).ExceptionCode == EXCEPTION_STACK_OVERFLOW;
|
||||
match info {
|
||||
Some(info) => info.handle_trap(pc, reset_guard_page, |handler| handler(exception_info)),
|
||||
None => ptr::null(),
|
||||
let info = match info {
|
||||
Some(info) => info,
|
||||
None => return EXCEPTION_CONTINUE_SEARCH,
|
||||
};
|
||||
let jmp_buf = info.handle_trap(
|
||||
(*(*exception_info).ContextRecord).Rip as *const u8,
|
||||
record.ExceptionCode == EXCEPTION_STACK_OVERFLOW,
|
||||
|handler| handler(exception_info),
|
||||
);
|
||||
if jmp_buf.is_null() {
|
||||
EXCEPTION_CONTINUE_SEARCH
|
||||
} else if jmp_buf as usize == 1 {
|
||||
EXCEPTION_CONTINUE_EXECUTION
|
||||
} else {
|
||||
Unwind(jmp_buf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function performs the low-overhead signal handler initialization that
|
||||
/// we want to do eagerly to ensure a more-deterministic global process state.
|
||||
///
|
||||
/// This is especially relevant for signal handlers since handler ordering
|
||||
/// depends on installation order: the wasm signal handler must run *before*
|
||||
/// the other crash handlers and since POSIX signal handlers work LIFO, this
|
||||
/// function needs to be called at the end of the startup process, after other
|
||||
/// handlers have been installed. This function can thus be called multiple
|
||||
/// times, having no effect after the first call.
|
||||
pub fn init() {
|
||||
static INIT: Once = Once::new();
|
||||
INIT.call_once(real_init);
|
||||
}
|
||||
|
||||
fn real_init() {
|
||||
// This is a really weird and unfortunate function call. For all the gory
|
||||
// details see #829, but the tl;dr; is that in a trap handler we have 2
|
||||
// pages of stack space on Linux, and calling into libunwind which triggers
|
||||
// the dynamic loader blows the stack.
|
||||
//
|
||||
// This is a dumb hack to work around this system-specific issue by
|
||||
// capturing a backtrace once in the lifetime of a process to ensure that
|
||||
// when we capture a backtrace in the trap handler all caches are primed,
|
||||
// aka the dynamic loader has resolved all the relevant symbols.
|
||||
drop(backtrace::Backtrace::new_unresolved());
|
||||
unsafe {
|
||||
platform_init();
|
||||
}
|
||||
}
|
||||
|
||||
/// Raises a user-defined trap immediately.
|
||||
///
|
||||
/// This function performs as-if a wasm trap was just executed, only the trap
|
||||
@@ -186,6 +396,7 @@ pub struct CallThreadState {
|
||||
reset_guard_page: Cell<bool>,
|
||||
prev: Option<*const CallThreadState>,
|
||||
vmctx: *mut VMContext,
|
||||
handling_trap: Cell<bool>,
|
||||
}
|
||||
|
||||
enum UnwindReason {
|
||||
@@ -204,6 +415,7 @@ impl CallThreadState {
|
||||
jmp_buf: Cell::new(ptr::null()),
|
||||
reset_guard_page: Cell::new(false),
|
||||
prev: None,
|
||||
handling_trap: Cell::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +499,15 @@ impl CallThreadState {
|
||||
reset_guard_page: bool,
|
||||
call_handler: impl Fn(&SignalHandler) -> bool,
|
||||
) -> *const u8 {
|
||||
// If we hit a fault 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.
|
||||
if self.handling_trap.replace(true) {
|
||||
return ptr::null();
|
||||
}
|
||||
|
||||
// First up see if any instance registered has a custom trap handler,
|
||||
// in which case run them all. If anything handles the trap then we
|
||||
// return that the trap was handled.
|
||||
@@ -299,6 +520,7 @@ impl CallThreadState {
|
||||
i.instance().signal_handler.set(Some(handler));
|
||||
return result;
|
||||
}) {
|
||||
self.handling_trap.set(false);
|
||||
return 1 as *const _;
|
||||
}
|
||||
|
||||
@@ -310,6 +532,7 @@ impl CallThreadState {
|
||||
// doesn't trap. Then, if we have called some WebAssembly code, it
|
||||
// means the trap is stack overflow.
|
||||
if self.jmp_buf.get().is_null() {
|
||||
self.handling_trap.set(false);
|
||||
return ptr::null();
|
||||
}
|
||||
let backtrace = Backtrace::new_unresolved();
|
||||
@@ -318,6 +541,7 @@ impl CallThreadState {
|
||||
backtrace,
|
||||
pc: pc as usize,
|
||||
});
|
||||
self.handling_trap.set(false);
|
||||
self.jmp_buf.get()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user