diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index f5cd6089d4..75850b0503 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -20,6 +20,11 @@ wasmparser = { version = "0.45.1", default-features = false } target-lexicon = { version = "0.9.0", default-features = false } anyhow = "1.0.19" region = "2.0.0" +libc = "0.2" +cfg-if = "0.1.9" + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = "0.3.7" [dev-dependencies] # for wasmtime.rs diff --git a/crates/api/src/callable.rs b/crates/api/src/callable.rs index c035252ae0..317875a7fd 100644 --- a/crates/api/src/callable.rs +++ b/crates/api/src/callable.rs @@ -151,11 +151,13 @@ impl WrappedCallable for WasmtimeFn { // Call the trampoline. if let Err(message) = unsafe { - wasmtime_runtime::wasmtime_call_trampoline( - vmctx, - exec_code_buf, - values_vec.as_mut_ptr() as *mut u8, - ) + self.instance.with_signals_on(|| { + wasmtime_runtime::wasmtime_call_trampoline( + vmctx, + exec_code_buf, + values_vec.as_mut_ptr() as *mut u8, + ) + }) } { let trap = take_api_trap().unwrap_or_else(|| Trap::new(format!("call error: {}", message))); diff --git a/crates/api/src/instance.rs b/crates/api/src/instance.rs index 0398ec3dd7..dc7a89390b 100644 --- a/crates/api/src/instance.rs +++ b/crates/api/src/instance.rs @@ -161,3 +161,37 @@ impl Instance { 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(&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(&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(&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); + } + } + } +} diff --git a/crates/jit/src/action.rs b/crates/jit/src/action.rs index 994d653922..d3668b0887 100644 --- a/crates/jit/src/action.rs +++ b/crates/jit/src/action.rs @@ -192,11 +192,13 @@ pub fn invoke( // Call the trampoline. if let Err(message) = unsafe { - wasmtime_call_trampoline( - callee_vmctx, - exec_code_buf, - values_vec.as_mut_ptr() as *mut u8, - ) + instance.with_signals_on(|| { + wasmtime_call_trampoline( + callee_vmctx, + exec_code_buf, + values_vec.as_mut_ptr() as *mut u8, + ) + }) } { return Ok(ActionOutcome::Trapped { message }); } diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 01775882c9..f613d42e27 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -19,6 +19,7 @@ memoffset = "0.5.3" indexmap = "1.0.2" thiserror = "1.0.4" more-asserts = "0.2.1" +cfg-if = "0.1.9" [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3.7", features = ["winbase", "memoryapi"] } diff --git a/crates/runtime/signalhandlers/SignalHandlers.cpp b/crates/runtime/signalhandlers/SignalHandlers.cpp index ef9200a440..40fb8c1f5f 100644 --- a/crates/runtime/signalhandlers/SignalHandlers.cpp +++ b/crates/runtime/signalhandlers/SignalHandlers.cpp @@ -467,9 +467,13 @@ WasmTrapHandler(LPEXCEPTION_POINTERS exception) return EXCEPTION_CONTINUE_SEARCH; } - if (!HandleTrap(exception->ContextRecord, - record->ExceptionCode == EXCEPTION_STACK_OVERFLOW)) { - return EXCEPTION_CONTINUE_SEARCH; + bool handled = InstanceSignalHandler(exception); + + if (!handled) { + if (!HandleTrap(exception->ContextRecord, + record->ExceptionCode == EXCEPTION_STACK_OVERFLOW)) { + return EXCEPTION_CONTINUE_SEARCH; + } } return EXCEPTION_CONTINUE_EXECUTION; @@ -633,11 +637,17 @@ WasmTrapHandler(int signum, siginfo_t* info, void* context) if (!sAlreadyHandlingTrap) { AutoHandlingTrap aht; assert(signum == SIGSEGV || signum == SIGBUS || signum == SIGFPE || signum == SIGILL); + + if (InstanceSignalHandler(signum, info, (ucontext_t*) context)) { + return; + } + if (HandleTrap(static_cast(context), false)) { return; } } + struct sigaction* previousSignal = nullptr; switch (signum) { case SIGSEGV: previousSignal = &sPrevSIGSEGVHandler; break; diff --git a/crates/runtime/signalhandlers/SignalHandlers.hpp b/crates/runtime/signalhandlers/SignalHandlers.hpp index cd0c9e4cdb..5bccd03e5d 100644 --- a/crates/runtime/signalhandlers/SignalHandlers.hpp +++ b/crates/runtime/signalhandlers/SignalHandlers.hpp @@ -7,6 +7,8 @@ #include #endif +#include + #ifdef __cplusplus extern "C" { #endif @@ -15,6 +17,17 @@ int8_t CheckIfTrapAtAddress(const uint8_t* pc); // Record the Trap code and wasm bytecode offset in TLS somewhere void RecordTrap(const uint8_t* pc, bool reset_guard_page); +#if defined(_WIN32) +#include +#include +bool InstanceSignalHandler(LPEXCEPTION_POINTERS); +#elif defined(USE_APPLE_MACH_PORTS) +bool InstanceSignalHandler(int, siginfo_t *, void *); +#else +#include +bool InstanceSignalHandler(int, siginfo_t *, ucontext_t *); +#endif + void* EnterScope(void*); void LeaveScope(void*); void* GetScope(void); diff --git a/crates/runtime/src/instance.rs b/crates/runtime/src/instance.rs index 8002ff7c20..4fe112281d 100644 --- a/crates/runtime/src/instance.rs +++ b/crates/runtime/src/instance.rs @@ -22,6 +22,7 @@ use std::borrow::Borrow; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; +use std::ptr::NonNull; use std::rc::Rc; use std::{mem, ptr, slice}; use thiserror::Error; @@ -32,6 +33,11 @@ use wasmtime_environ::wasm::{ }; use wasmtime_environ::{DataInitializer, Module, TableElements, VMOffsets}; +thread_local! { + /// A stack of currently-running `Instance`s, if any. + pub(crate) static CURRENT_INSTANCE: RefCell>> = RefCell::new(Vec::new()); +} + fn signature_id( vmctx: &VMContext, 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 = ¤t_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(&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; + } + } + } 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 = ¤t_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(&mut self, handler: H) + where + H: 'static + Fn(winapi::um::winnt::EXCEPTION_POINTERS) -> bool, + { + self.instance_mut().signal_handler = Box::new(handler) as Box; + } + } + } +} + /// A WebAssembly instance. /// /// 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. dbg_jit_registration: Option>, + /// Handler run when `SIGBUS`, `SIGFPE`, `SIGILL`, or `SIGSEGV` are caught by the instance thread. + signal_handler: Box, + /// Additional context used by compiled wasm code. This field is last, and /// represents a dynamically-sized array that extends beyond the nominal /// end of the struct (similar to a flexible array member). @@ -647,6 +747,28 @@ pub struct InstanceHandle { } impl InstanceHandle { + #[doc(hidden)] + pub fn with_signals_on(&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`. pub fn new( module: Rc, @@ -699,6 +821,7 @@ impl InstanceHandle { finished_functions, dbg_jit_registration, host_state, + signal_handler: Box::new(signal_handler_none) as Box, vmctx: VMContext {}, }; unsafe { diff --git a/tests/custom_signal_handler.rs b/tests/custom_signal_handler.rs new file mode 100644 index 0000000000..253d62d49d --- /dev/null +++ b/tests/custom_signal_handler.rs @@ -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, + data: &[u8], + func_name: &str, + ) -> Result, 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) -> (*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 = 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()); + } +} diff --git a/tests/custom_signal_handler.wasm b/tests/custom_signal_handler.wasm new file mode 100644 index 0000000000..9348e1a61e Binary files /dev/null and b/tests/custom_signal_handler.wasm differ diff --git a/tests/custom_signal_handler.wat b/tests/custom_signal_handler.wat new file mode 100644 index 0000000000..e80533f7e4 --- /dev/null +++ b/tests/custom_signal_handler.wat @@ -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) +) diff --git a/tests/custom_signal_handler_2.wasm b/tests/custom_signal_handler_2.wasm new file mode 100644 index 0000000000..f0c4da5ae9 Binary files /dev/null and b/tests/custom_signal_handler_2.wasm differ diff --git a/tests/custom_signal_handler_2.wat b/tests/custom_signal_handler_2.wat new file mode 100644 index 0000000000..a93c5db8d0 --- /dev/null +++ b/tests/custom_signal_handler_2.wat @@ -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) +)