Add support for a custom, per-instance signal handler (#620)

* Per Instance signal handler

* add custom signal handler test

* add instance signal handling to callable.rs

* extend signal handler test to test callable.rs

* test multiple instances, multiple signal handlers

* support more than one current instance

import_calling_export.rs is a good example of why this is needed:
execution switches from one instance to another before the first one has
finished running

* add another custom signal handler test case

* move and update custom signal handler tests

* fmt

* fix libc version to 0.2

* call the correct instance signal handler

We keep a stack of instances so should call last() not first().

* move custom signal handler test to top level dir

* windows/mac signal handling wip

* os-specific signal handling wip

* disable custom signal handler test on windows

* fmt

* unify signal handling on mac and linux
This commit is contained in:
Maciej Woś
2020-01-08 17:09:12 -08:00
committed by Dan Gohman
parent 1fe76ef9e3
commit 61f9b8ade8
13 changed files with 510 additions and 13 deletions

View File

@@ -20,6 +20,11 @@ wasmparser = { version = "0.45.1", default-features = false }
target-lexicon = { version = "0.9.0", default-features = false } target-lexicon = { version = "0.9.0", default-features = false }
anyhow = "1.0.19" anyhow = "1.0.19"
region = "2.0.0" region = "2.0.0"
libc = "0.2"
cfg-if = "0.1.9"
[target.'cfg(target_os = "windows")'.dependencies]
winapi = "0.3.7"
[dev-dependencies] [dev-dependencies]
# for wasmtime.rs # for wasmtime.rs

View File

@@ -151,11 +151,13 @@ impl WrappedCallable for WasmtimeFn {
// Call the trampoline. // Call the trampoline.
if let Err(message) = unsafe { if let Err(message) = unsafe {
self.instance.with_signals_on(|| {
wasmtime_runtime::wasmtime_call_trampoline( wasmtime_runtime::wasmtime_call_trampoline(
vmctx, vmctx,
exec_code_buf, exec_code_buf,
values_vec.as_mut_ptr() as *mut u8, values_vec.as_mut_ptr() as *mut u8,
) )
})
} { } {
let trap = let trap =
take_api_trap().unwrap_or_else(|| Trap::new(format!("call error: {}", message))); take_api_trap().unwrap_or_else(|| Trap::new(format!("call error: {}", message)));

View File

@@ -161,3 +161,37 @@ impl Instance {
instance_handle.lookup("memory") instance_handle.lookup("memory")
} }
} }
// OS-specific signal handling
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
impl Instance {
/// The signal handler must be
/// [async-signal-safe](http://man7.org/linux/man-pages/man7/signal-safety.7.html).
pub fn set_signal_handler<H>(&mut self, handler: H)
where
H: 'static + Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool,
{
self.instance_handle.set_signal_handler(handler);
}
}
} else if #[cfg(target_os = "windows")] {
impl Instance {
pub fn set_signal_handler<H>(&mut self, handler: H)
where
H: 'static + Fn(winapi::um::winnt::EXCEPTION_POINTERS) -> bool,
{
self.instance_handle.set_signal_handler(handler);
}
}
} else if #[cfg(target_os = "macos")] {
impl Instance {
pub fn set_signal_handler<H>(&mut self, handler: H)
where
H: 'static + Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool,
{
self.instance_handle.set_signal_handler(handler);
}
}
}
}

View File

@@ -192,11 +192,13 @@ pub fn invoke(
// Call the trampoline. // Call the trampoline.
if let Err(message) = unsafe { if let Err(message) = unsafe {
instance.with_signals_on(|| {
wasmtime_call_trampoline( wasmtime_call_trampoline(
callee_vmctx, callee_vmctx,
exec_code_buf, exec_code_buf,
values_vec.as_mut_ptr() as *mut u8, values_vec.as_mut_ptr() as *mut u8,
) )
})
} { } {
return Ok(ActionOutcome::Trapped { message }); return Ok(ActionOutcome::Trapped { message });
} }

View File

@@ -19,6 +19,7 @@ memoffset = "0.5.3"
indexmap = "1.0.2" indexmap = "1.0.2"
thiserror = "1.0.4" thiserror = "1.0.4"
more-asserts = "0.2.1" more-asserts = "0.2.1"
cfg-if = "0.1.9"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3.7", features = ["winbase", "memoryapi"] } winapi = { version = "0.3.7", features = ["winbase", "memoryapi"] }

View File

@@ -467,10 +467,14 @@ WasmTrapHandler(LPEXCEPTION_POINTERS exception)
return EXCEPTION_CONTINUE_SEARCH; return EXCEPTION_CONTINUE_SEARCH;
} }
bool handled = InstanceSignalHandler(exception);
if (!handled) {
if (!HandleTrap(exception->ContextRecord, if (!HandleTrap(exception->ContextRecord,
record->ExceptionCode == EXCEPTION_STACK_OVERFLOW)) { record->ExceptionCode == EXCEPTION_STACK_OVERFLOW)) {
return EXCEPTION_CONTINUE_SEARCH; return EXCEPTION_CONTINUE_SEARCH;
} }
}
return EXCEPTION_CONTINUE_EXECUTION; return EXCEPTION_CONTINUE_EXECUTION;
} }
@@ -633,11 +637,17 @@ WasmTrapHandler(int signum, siginfo_t* info, void* context)
if (!sAlreadyHandlingTrap) { if (!sAlreadyHandlingTrap) {
AutoHandlingTrap aht; AutoHandlingTrap aht;
assert(signum == SIGSEGV || signum == SIGBUS || signum == SIGFPE || signum == SIGILL); assert(signum == SIGSEGV || signum == SIGBUS || signum == SIGFPE || signum == SIGILL);
if (InstanceSignalHandler(signum, info, (ucontext_t*) context)) {
return;
}
if (HandleTrap(static_cast<CONTEXT*>(context), false)) { if (HandleTrap(static_cast<CONTEXT*>(context), false)) {
return; return;
} }
} }
struct sigaction* previousSignal = nullptr; struct sigaction* previousSignal = nullptr;
switch (signum) { switch (signum) {
case SIGSEGV: previousSignal = &sPrevSIGSEGVHandler; break; case SIGSEGV: previousSignal = &sPrevSIGSEGVHandler; break;

View File

@@ -7,6 +7,8 @@
#include <stdbool.h> #include <stdbool.h>
#endif #endif
#include <signal.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@@ -15,6 +17,17 @@ int8_t CheckIfTrapAtAddress(const uint8_t* pc);
// Record the Trap code and wasm bytecode offset in TLS somewhere // Record the Trap code and wasm bytecode offset in TLS somewhere
void RecordTrap(const uint8_t* pc, bool reset_guard_page); void RecordTrap(const uint8_t* pc, bool reset_guard_page);
#if defined(_WIN32)
#include <windows.h>
#include <winternl.h>
bool InstanceSignalHandler(LPEXCEPTION_POINTERS);
#elif defined(USE_APPLE_MACH_PORTS)
bool InstanceSignalHandler(int, siginfo_t *, void *);
#else
#include <sys/ucontext.h>
bool InstanceSignalHandler(int, siginfo_t *, ucontext_t *);
#endif
void* EnterScope(void*); void* EnterScope(void*);
void LeaveScope(void*); void LeaveScope(void*);
void* GetScope(void); void* GetScope(void);

View File

@@ -22,6 +22,7 @@ use std::borrow::Borrow;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::ptr::NonNull;
use std::rc::Rc; use std::rc::Rc;
use std::{mem, ptr, slice}; use std::{mem, ptr, slice};
use thiserror::Error; use thiserror::Error;
@@ -32,6 +33,11 @@ use wasmtime_environ::wasm::{
}; };
use wasmtime_environ::{DataInitializer, Module, TableElements, VMOffsets}; use wasmtime_environ::{DataInitializer, Module, TableElements, VMOffsets};
thread_local! {
/// A stack of currently-running `Instance`s, if any.
pub(crate) static CURRENT_INSTANCE: RefCell<Vec<NonNull<Instance>>> = RefCell::new(Vec::new());
}
fn signature_id( fn signature_id(
vmctx: &VMContext, vmctx: &VMContext,
offsets: &VMOffsets, offsets: &VMOffsets,
@@ -175,6 +181,97 @@ fn global_mut<'vmctx>(
} }
} }
cfg_if::cfg_if! {
if #[cfg(any(target_os = "linux", target_os = "macos"))] {
pub type SignalHandler = dyn Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool;
pub fn signal_handler_none(
_signum: libc::c_int,
_siginfo: *const libc::siginfo_t,
_context: *const libc::c_void,
) -> bool {
false
}
#[no_mangle]
pub extern "C" fn InstanceSignalHandler(
signum: libc::c_int,
siginfo: *mut libc::siginfo_t,
context: *mut libc::c_void,
) -> bool {
CURRENT_INSTANCE.with(|current_instance| {
let current_instance = current_instance
.try_borrow()
.expect("borrow current instance");
if current_instance.is_empty() {
return false;
} else {
unsafe {
let f = &current_instance
.last()
.expect("current instance not none")
.as_ref()
.signal_handler;
f(signum, siginfo, context)
}
}
})
}
impl InstanceHandle {
/// Set a custom signal handler
pub fn set_signal_handler<H>(&mut self, handler: H)
where
H: 'static + Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool,
{
self.instance_mut().signal_handler = Box::new(handler) as Box<SignalHandler>;
}
}
} else if #[cfg(target_os = "windows")] {
pub type SignalHandler = dyn Fn(winapi::um::winnt::EXCEPTION_POINTERS) -> bool;
pub fn signal_handler_none(
_exception_info: winapi::um::winnt::EXCEPTION_POINTERS
) -> bool {
false
}
#[no_mangle]
pub extern "C" fn InstanceSignalHandler(
exception_info: winapi::um::winnt::EXCEPTION_POINTERS
) -> bool {
CURRENT_INSTANCE.with(|current_instance| {
let current_instance = current_instance
.try_borrow()
.expect("borrow current instance");
if current_instance.is_empty() {
return false;
} else {
unsafe {
let f = &current_instance
.last()
.expect("current instance not none")
.as_ref()
.signal_handler;
f(exception_info)
}
}
})
}
impl InstanceHandle {
/// Set a custom signal handler
pub fn set_signal_handler<H>(&mut self, handler: H)
where
H: 'static + Fn(winapi::um::winnt::EXCEPTION_POINTERS) -> bool,
{
self.instance_mut().signal_handler = Box::new(handler) as Box<SignalHandler>;
}
}
}
}
/// A WebAssembly instance. /// A WebAssembly instance.
/// ///
/// This is repr(C) to ensure that the vmctx field is last. /// This is repr(C) to ensure that the vmctx field is last.
@@ -217,6 +314,9 @@ pub(crate) struct Instance {
/// Optional image of JIT'ed code for debugger registration. /// Optional image of JIT'ed code for debugger registration.
dbg_jit_registration: Option<Rc<GdbJitImageRegistration>>, dbg_jit_registration: Option<Rc<GdbJitImageRegistration>>,
/// Handler run when `SIGBUS`, `SIGFPE`, `SIGILL`, or `SIGSEGV` are caught by the instance thread.
signal_handler: Box<SignalHandler>,
/// Additional context used by compiled wasm code. This field is last, and /// Additional context used by compiled wasm code. This field is last, and
/// represents a dynamically-sized array that extends beyond the nominal /// represents a dynamically-sized array that extends beyond the nominal
/// end of the struct (similar to a flexible array member). /// end of the struct (similar to a flexible array member).
@@ -647,6 +747,28 @@ pub struct InstanceHandle {
} }
impl InstanceHandle { impl InstanceHandle {
#[doc(hidden)]
pub fn with_signals_on<F, R>(&self, action: F) -> R
where
F: FnOnce() -> R,
{
CURRENT_INSTANCE.with(|current_instance| {
current_instance
.borrow_mut()
.push(unsafe { NonNull::new_unchecked(self.instance) });
});
let result = action();
CURRENT_INSTANCE.with(|current_instance| {
let mut current_instance = current_instance.borrow_mut();
assert!(!current_instance.is_empty());
current_instance.pop();
});
result
}
/// Create a new `InstanceHandle` pointing at a new `Instance`. /// Create a new `InstanceHandle` pointing at a new `Instance`.
pub fn new( pub fn new(
module: Rc<Module>, module: Rc<Module>,
@@ -699,6 +821,7 @@ impl InstanceHandle {
finished_functions, finished_functions,
dbg_jit_registration, dbg_jit_registration,
host_state, host_state,
signal_handler: Box::new(signal_handler_none) as Box<SignalHandler>,
vmctx: VMContext {}, vmctx: VMContext {},
}; };
unsafe { unsafe {

View File

@@ -0,0 +1,282 @@
#[cfg(not(target_os = "windows"))]
mod tests {
use core::cell::Ref;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use wasmtime::*;
use wasmtime_interface_types::{ModuleData, Value};
fn invoke_export(
instance: &HostRef<Instance>,
data: &[u8],
func_name: &str,
) -> Result<Vec<Value>, anyhow::Error> {
ModuleData::new(&data)
.expect("module data")
.invoke_export(instance, func_name, &[])
}
// Locate "memory" export, get base address and size and set memory protection to PROT_NONE
fn set_up_memory(instance: &HostRef<Instance>) -> (*mut u8, usize) {
let mem_export = instance.borrow().get_wasmtime_memory().expect("memory");
let (base, length) = if let wasmtime_runtime::Export::Memory {
definition,
vmctx: _,
memory: _,
} = mem_export
{
unsafe {
let definition = std::ptr::read(definition);
(definition.base, definition.current_length)
}
} else {
panic!("expected memory");
};
// So we can later trigger SIGSEGV by performing a read
unsafe {
libc::mprotect(base as *mut libc::c_void, length, libc::PROT_NONE);
}
println!("memory: base={:?}, length={}", base, length);
(base, length)
}
fn handle_sigsegv(
base: *mut u8,
length: usize,
signum: libc::c_int,
siginfo: *const libc::siginfo_t,
) -> bool {
println!("Hello from instance signal handler!");
// SIGSEGV on Linux, SIGBUS on Mac
if libc::SIGSEGV == signum || libc::SIGBUS == signum {
let si_addr: *mut libc::c_void = unsafe { (*siginfo).si_addr() };
// Any signal from within module's memory we handle ourselves
let result = (si_addr as u64) < (base as u64) + (length as u64);
// Remove protections so the execution may resume
unsafe {
libc::mprotect(
base as *mut libc::c_void,
length,
libc::PROT_READ | libc::PROT_WRITE,
);
}
println!("signal handled: {}", result);
result
} else {
// Otherwise, we forward to wasmtime's signal handler.
false
}
}
#[test]
fn test_custom_signal_handler_single_instance() {
let engine = HostRef::new(Engine::new(&Config::default()));
let store = HostRef::new(Store::new(&engine));
let data =
std::fs::read("tests/custom_signal_handler.wasm").expect("failed to read wasm file");
let module = HostRef::new(Module::new(&store, &data).expect("failed to create module"));
let instance = HostRef::new(
Instance::new(&store, &module, &[]).expect("failed to instantiate module"),
);
let (base, length) = set_up_memory(&instance);
instance
.borrow_mut()
.set_signal_handler(move |signum, siginfo, _| {
handle_sigsegv(base, length, signum, siginfo)
});
let exports = Ref::map(instance.borrow(), |instance| instance.exports());
assert!(!exports.is_empty());
// these invoke wasmtime_call_trampoline from action.rs
{
println!("calling read...");
let result = invoke_export(&instance, &data, "read").expect("read succeeded");
assert_eq!("123", result[0].clone().to_string());
}
{
println!("calling read_out_of_bounds...");
let trap = invoke_export(&instance, &data, "read_out_of_bounds").unwrap_err();
assert!(trap.root_cause().to_string().starts_with(
"trapped: Ref(Trap { message: \"wasm trap: out of bounds memory access"
));
}
// these invoke wasmtime_call_trampoline from callable.rs
{
let read_func = exports[0]
.func()
.expect("expected a 'read' func in the module");
println!("calling read...");
let result = read_func
.borrow()
.call(&[])
.expect("expected function not to trap");
assert_eq!(123i32, result[0].clone().unwrap_i32());
}
{
let read_out_of_bounds_func = exports[1]
.func()
.expect("expected a 'read_out_of_bounds' func in the module");
println!("calling read_out_of_bounds...");
let trap = read_out_of_bounds_func.borrow().call(&[]).unwrap_err();
assert!(trap
.borrow()
.message()
.starts_with("wasm trap: out of bounds memory access"));
}
}
#[test]
fn test_custom_signal_handler_multiple_instances() {
let engine = HostRef::new(Engine::new(&Config::default()));
let store = HostRef::new(Store::new(&engine));
let data =
std::fs::read("tests/custom_signal_handler.wasm").expect("failed to read wasm file");
let module = HostRef::new(Module::new(&store, &data).expect("failed to create module"));
// Set up multiple instances
let instance1 = HostRef::new(
Instance::new(&store, &module, &[]).expect("failed to instantiate module"),
);
let instance1_handler_triggered = Rc::new(AtomicBool::new(false));
{
let (base1, length1) = set_up_memory(&instance1);
instance1.borrow_mut().set_signal_handler({
let instance1_handler_triggered = instance1_handler_triggered.clone();
move |_signum, _siginfo, _context| {
// Remove protections so the execution may resume
unsafe {
libc::mprotect(
base1 as *mut libc::c_void,
length1,
libc::PROT_READ | libc::PROT_WRITE,
);
}
instance1_handler_triggered.store(true, Ordering::SeqCst);
println!(
"Hello from instance1 signal handler! {}",
instance1_handler_triggered.load(Ordering::SeqCst)
);
true
}
});
}
let instance2 = HostRef::new(
Instance::new(&store, &module, &[]).expect("failed to instantiate module"),
);
let instance2_handler_triggered = Rc::new(AtomicBool::new(false));
{
let (base2, length2) = set_up_memory(&instance2);
instance2.borrow_mut().set_signal_handler({
let instance2_handler_triggered = instance2_handler_triggered.clone();
move |_signum, _siginfo, _context| {
// Remove protections so the execution may resume
unsafe {
libc::mprotect(
base2 as *mut libc::c_void,
length2,
libc::PROT_READ | libc::PROT_WRITE,
);
}
instance2_handler_triggered.store(true, Ordering::SeqCst);
println!(
"Hello from instance2 signal handler! {}",
instance2_handler_triggered.load(Ordering::SeqCst)
);
true
}
});
}
// Invoke both instances and trigger both signal handlers
// First instance1
{
let exports1 = Ref::map(instance1.borrow(), |i| i.exports());
assert!(!exports1.is_empty());
println!("calling instance1.read...");
let result = invoke_export(&instance1, &data, "read").expect("read succeeded");
assert_eq!("123", result[0].clone().to_string());
assert_eq!(
instance1_handler_triggered.load(Ordering::SeqCst),
true,
"instance1 signal handler has been triggered"
);
}
// And then instance2
{
let exports2 = Ref::map(instance2.borrow(), |i| i.exports());
assert!(!exports2.is_empty());
println!("calling instance2.read...");
let result = invoke_export(&instance2, &data, "read").expect("read succeeded");
assert_eq!("123", result[0].clone().to_string());
assert_eq!(
instance2_handler_triggered.load(Ordering::SeqCst),
true,
"instance1 signal handler has been triggered"
);
}
}
#[test]
fn test_custom_signal_handler_instance_calling_another_instance() {
let engine = HostRef::new(Engine::new(&Config::default()));
let store = HostRef::new(Store::new(&engine));
// instance1 which defines 'read'
let data1 =
std::fs::read("tests/custom_signal_handler.wasm").expect("failed to read wasm file");
let module1 = HostRef::new(Module::new(&store, &data1).expect("failed to create module"));
let instance1: HostRef<Instance> = HostRef::new(
Instance::new(&store, &module1, &[]).expect("failed to instantiate module"),
);
let (base1, length1) = set_up_memory(&instance1);
instance1
.borrow_mut()
.set_signal_handler(move |signum, siginfo, _| {
println!("instance1");
handle_sigsegv(base1, length1, signum, siginfo)
});
let instance1_exports = Ref::map(instance1.borrow(), |i| i.exports());
assert!(!instance1_exports.is_empty());
let instance1_read = instance1_exports[0].clone();
// instance2 wich calls 'instance1.read'
let data2 =
std::fs::read("tests/custom_signal_handler_2.wasm").expect("failed to read wasm file");
let module2 = HostRef::new(Module::new(&store, &data2).expect("failed to create module"));
let instance2 = HostRef::new(
Instance::new(&store, &module2, &[instance1_read])
.expect("failed to instantiate module"),
);
// since 'instance2.run' calls 'instance1.read' we need to set up the signal handler to handle
// SIGSEGV originating from within the memory of instance1
instance2
.borrow_mut()
.set_signal_handler(move |signum, siginfo, _| {
handle_sigsegv(base1, length1, signum, siginfo)
});
println!("calling instance2.run");
let result = invoke_export(&instance2, &data2, "run").expect("instance2.run succeeded");
assert_eq!("123", result[0].clone().to_string());
}
}

Binary file not shown.

View File

@@ -0,0 +1,20 @@
(module
(func $read (export "read") (result i32)
(i32.load (i32.const 0))
)
(func $read_out_of_bounds (export "read_out_of_bounds") (result i32)
(i32.load
(i32.mul
;; memory size in Wasm pages
(memory.size)
;; Wasm page size
(i32.const 65536)
)
)
)
(func $start
(i32.store (i32.const 0) (i32.const 123))
)
(start $start)
(memory (export "memory") 1 4)
)

Binary file not shown.

View File

@@ -0,0 +1,5 @@
(module
(import "other_module" "read" (func $other_module.read (result i32)))
(func $run (export "run") (result i32)
call $other_module.read)
)