diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index 77b35bfd3ba1..49582751404a 100644 --- a/crates/wasmtime/src/engine.rs +++ b/crates/wasmtime/src/engine.rs @@ -699,6 +699,60 @@ impl Engine { code.publish()?; Ok(Arc::new(code)) } + + /// Unload process-related trap/signal handlers and destroy this engine. + /// + /// This method is not safe and is not widely applicable. It is not required + /// to be called and is intended for use cases such as unloading a dynamic + /// library from a process. It is difficult to invoke this method correctly + /// and it requires careful coordination to do so. + /// + /// # Panics + /// + /// This method will panic if this `Engine` handle is not the last remaining + /// engine handle. + /// + /// # Aborts + /// + /// This method will abort the process on some platforms in some situations + /// where unloading the handler cannot be performed and an unrecoverable + /// state is reached. For example on Unix platforms with signal handling + /// the process will be aborted if the current signal handlers are not + /// Wasmtime's. + /// + /// # Unsafety + /// + /// This method is not generally safe to call and has a number of + /// preconditions that must be met to even possibly be safe. Even with these + /// known preconditions met there may be other unknown invariants to uphold + /// as well. + /// + /// * There must be no other instances of `Engine` elsewhere in the process. + /// Note that this isn't just copies of this `Engine` but it's any other + /// `Engine` at all. This unloads global state that is used by all + /// `Engine`s so this instance must be the last. + /// + /// * On Unix platforms no other signal handlers could have been installed + /// for signals that Wasmtime catches. In this situation Wasmtime won't + /// know how to restore signal handlers that Wasmtime possibly overwrote + /// when Wasmtime was initially loaded. If possible initialize other + /// libraries first and then initialize Wasmtime last (e.g. defer creating + /// an `Engine`). + /// + /// * All existing threads which have used this DLL or copy of Wasmtime may + /// no longer use this copy of Wasmtime. Per-thread state is not iterated + /// and destroyed. Only future threads may use future instances of this + /// Wasmtime itself. + /// + /// If other crashes are seen from using this method please feel free to + /// file an issue to update the documentation here with more preconditions + /// that must be met. + pub unsafe fn unload_process_handlers(self) { + assert_eq!(Arc::weak_count(&self.inner), 0); + assert_eq!(Arc::strong_count(&self.inner), 1); + + crate::runtime::vm::deinit_traps(); + } } /// A weak reference to an [`Engine`]. diff --git a/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs index c5964491e179..46cfe310aca3 100644 --- a/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs +++ b/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs @@ -20,8 +20,15 @@ pub unsafe fn wasmtime_setjmp( capi::wasmtime_setjmp(jmp_buf, callback, payload, callee.cast()) } -pub unsafe fn platform_init(_macos_use_mach_ports: bool) { - capi::wasmtime_init_traps(handle_trap); +pub struct TrapHandler; + +impl TrapHandler { + pub unsafe fn new(_macos_use_mach_ports: bool) -> TrapHandler { + capi::wasmtime_init_traps(handle_trap); + TrapHandler + } + + pub fn validate_config(&self, _macos_use_mach_ports: bool) {} } extern "C" fn handle_trap(ip: usize, fp: usize, has_faulting_addr: bool, faulting_addr: usize) { diff --git a/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs index 320a178618c3..0059e4b0654c 100644 --- a/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs +++ b/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs @@ -30,7 +30,15 @@ pub fn wasmtime_longjmp(_jmp_buf: *const u8) -> ! { #[allow(missing_docs)] pub type SignalHandler<'a> = dyn Fn() + Send + Sync + 'a; -pub unsafe fn platform_init(_macos_use_mach_ports: bool) {} +pub struct TrapHandler; + +impl TrapHandler { + pub unsafe fn new(_macos_use_mach_ports: bool) -> TrapHandler { + TrapHandler + } + + pub fn validate_config(&self, _macos_use_mach_ports: bool) {} +} pub fn lazy_per_thread_init() {} diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs b/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs index 83a4b948d047..9ab83046d821 100644 --- a/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs +++ b/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs @@ -57,28 +57,49 @@ static mut WASMTIME_PORT: mach_port_name_t = MACH_PORT_NULL; static mut CHILD_OF_FORKED_PROCESS: bool = false; -pub unsafe fn platform_init() { - // Mach ports do not currently work across forks, so configure Wasmtime to - // panic in `lazy_per_thread_init` if the child attempts to invoke - // WebAssembly. - unsafe extern "C" fn child() { - CHILD_OF_FORKED_PROCESS = true; +pub struct TrapHandler { + thread: Option>, +} + +impl TrapHandler { + pub unsafe fn new() -> TrapHandler { + // Mach ports do not currently work across forks, so configure Wasmtime to + // panic in `lazy_per_thread_init` if the child attempts to invoke + // WebAssembly. + unsafe extern "C" fn child() { + CHILD_OF_FORKED_PROCESS = true; + } + let rc = libc::pthread_atfork(None, None, Some(child)); + assert_eq!(rc, 0, "failed to configure `pthread_atfork` handler"); + + // Allocate our WASMTIME_PORT and make sure that it can be sent to so we + // can receive exceptions. + let me = mach_task_self(); + let kret = mach_port_allocate(me, MACH_PORT_RIGHT_RECEIVE, addr_of_mut!(WASMTIME_PORT)); + assert_eq!(kret, KERN_SUCCESS, "failed to allocate port"); + let kret = + mach_port_insert_right(me, WASMTIME_PORT, WASMTIME_PORT, MACH_MSG_TYPE_MAKE_SEND); + assert_eq!(kret, KERN_SUCCESS, "failed to insert right"); + + // Spin up our handler thread which will solely exist to service exceptions + // generated by other threads. Note that this is a background thread that + // we're not very interested in so it's detached here. + let thread = thread::spawn(|| handler_thread()); + + TrapHandler { + thread: Some(thread), + } + } +} + +impl Drop for TrapHandler { + fn drop(&mut self) { + unsafe { + let kret = mach_port_destroy(mach_task_self(), WASMTIME_PORT); + assert_eq!(kret, KERN_SUCCESS, "failed to destroy port"); + self.thread.take().unwrap().join().unwrap(); + } } - let rc = libc::pthread_atfork(None, None, Some(child)); - assert_eq!(rc, 0, "failed to configure `pthread_atfork` handler"); - - // Allocate our WASMTIME_PORT and make sure that it can be sent to so we - // can receive exceptions. - let me = mach_task_self(); - let kret = mach_port_allocate(me, MACH_PORT_RIGHT_RECEIVE, addr_of_mut!(WASMTIME_PORT)); - assert_eq!(kret, KERN_SUCCESS, "failed to allocate port"); - let kret = mach_port_insert_right(me, WASMTIME_PORT, WASMTIME_PORT, MACH_MSG_TYPE_MAKE_SEND); - assert_eq!(kret, KERN_SUCCESS, "failed to insert right"); - - // Spin up our handler thread which will solely exist to service exceptions - // generated by other threads. Note that this is a background thread that - // we're not very interested in so it's detached here. - thread::spawn(|| handler_thread()); } // Note that this is copied from Gecko at @@ -133,13 +154,17 @@ unsafe fn handler_thread() { let mut request: ExceptionRequest = mem::zeroed(); let kret = mach_msg( &mut request.body.Head, - MACH_RCV_MSG, + MACH_RCV_MSG | MACH_RCV_INTERRUPT, 0, mem::size_of_val(&request) as u32, WASMTIME_PORT, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL, ); + if kret == MACH_RCV_PORT_CHANGED { + // this port has been closed; + break; + } if kret != KERN_SUCCESS { eprintln!("mach_msg failed with {} ({0:x})", kret); libc::abort(); diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs index fd0d607530fc..862b3eb44dd7 100644 --- a/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs +++ b/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs @@ -4,17 +4,28 @@ static mut USE_MACH_PORTS: bool = false; pub use super::signals::{wasmtime_longjmp, wasmtime_setjmp, SignalHandler}; -pub unsafe fn platform_init(macos_use_mach_ports: bool) { - USE_MACH_PORTS = macos_use_mach_ports; - if macos_use_mach_ports { - super::machports::platform_init(); - } else { - super::signals::platform_init(false); - } +pub enum TrapHandler { + Signals(super::signals::TrapHandler), + #[allow(dead_code)] // used for its drop + MachPorts(super::machports::TrapHandler), } -pub fn using_mach_ports() -> bool { - unsafe { USE_MACH_PORTS } +impl TrapHandler { + pub unsafe fn new(macos_use_mach_ports: bool) -> TrapHandler { + USE_MACH_PORTS = macos_use_mach_ports; + if macos_use_mach_ports { + TrapHandler::MachPorts(super::machports::TrapHandler::new()) + } else { + TrapHandler::Signals(super::signals::TrapHandler::new(false)) + } + } + + pub fn validate_config(&self, macos_use_mach_ports: bool) { + match self { + TrapHandler::Signals(t) => t.validate_config(macos_use_mach_ports), + TrapHandler::MachPorts(_) => assert!(macos_use_mach_ports), + } + } } pub fn lazy_per_thread_init() { diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs b/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs index 9a12bd460843..91db01bab07e 100644 --- a/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs +++ b/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs @@ -31,58 +31,114 @@ static mut PREV_SIGBUS: MaybeUninit = MaybeUninit::uninit(); static mut PREV_SIGILL: MaybeUninit = MaybeUninit::uninit(); static mut PREV_SIGFPE: MaybeUninit = MaybeUninit::uninit(); -pub unsafe fn platform_init(macos_use_mach_ports: bool) { - // Either mach ports shouldn't be in use or we shouldn't be on macOS, - // otherwise the `machports.rs` module should be used instead. - assert!(!macos_use_mach_ports || !cfg!(target_os = "macos")); - - let register = |slot: *mut 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) != 0 { - panic!( - "unable to install signal handler: {}", - io::Error::last_os_error(), - ); - } - }; +pub struct TrapHandler; + +impl TrapHandler { + /// Installs all trap handlers. + /// + /// # Unsafety + /// + /// This function is unsafe because it's not safe to call concurrently and + /// it's not safe to call if the trap handlers have already been initialized + /// for this process. + pub unsafe fn new(macos_use_mach_ports: bool) -> TrapHandler { + // Either mach ports shouldn't be in use or we shouldn't be on macOS, + // otherwise the `machports.rs` module should be used instead. + assert!(!macos_use_mach_ports || !cfg!(target_os = "macos")); + + foreach_handler(|slot, signal| { + 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) != 0 { + panic!( + "unable to install signal handler: {}", + io::Error::last_os_error(), + ); + } + }); + + TrapHandler + } + + pub fn validate_config(&self, macos_use_mach_ports: bool) { + assert!(!macos_use_mach_ports || !cfg!(target_os = "macos")); + } +} +unsafe fn foreach_handler(mut f: impl FnMut(*mut libc::sigaction, i32)) { // Allow handling OOB with signals on all architectures - register(PREV_SIGSEGV.as_mut_ptr(), libc::SIGSEGV); + f(PREV_SIGSEGV.as_mut_ptr(), libc::SIGSEGV); // Handle `unreachable` instructions which execute `ud2` right now - register(PREV_SIGILL.as_mut_ptr(), libc::SIGILL); + f(PREV_SIGILL.as_mut_ptr(), libc::SIGILL); // x86 and s390x use SIGFPE to report division by zero if cfg!(target_arch = "x86_64") || cfg!(target_arch = "s390x") { - register(PREV_SIGFPE.as_mut_ptr(), libc::SIGFPE); + f(PREV_SIGFPE.as_mut_ptr(), libc::SIGFPE); } // Sometimes we need to handle SIGBUS too: // - On Darwin, guard page accesses are raised as SIGBUS. if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") { - register(PREV_SIGBUS.as_mut_ptr(), libc::SIGBUS); + f(PREV_SIGBUS.as_mut_ptr(), libc::SIGBUS); } // TODO(#1980): x86-32, if we support it, will also need a SIGFPE handler. // TODO(#1173): ARM32, if we support it, will also need a SIGBUS handler. } +impl Drop for TrapHandler { + fn drop(&mut self) { + unsafe { + foreach_handler(|slot, signal| { + let mut prev: libc::sigaction = mem::zeroed(); + + // Restore the previous handler that this signal had. + if libc::sigaction(signal, slot, &mut prev) != 0 { + eprintln!( + "unable to reinstall signal handler: {}", + io::Error::last_os_error(), + ); + libc::abort(); + } + + // If our trap handler wasn't currently listed for this process + // then that's a problem because we have just corrupted the + // signal handler state and don't know how to remove ourselves + // from the signal handling state. Inform the user of this and + // abort the process. + if prev.sa_sigaction != trap_handler as usize { + eprintln!( + " +Wasmtime's signal handler was not the last signal handler to be installed +in the process so it's not certain how to unload signal handlers. In this +situation the Engine::unload_process_handlers API is not applicable and requires +perhaps initializing libraries in a different order. The process will be aborted +now. +" + ); + libc::abort(); + } + }); + } + } +} + unsafe extern "C" fn trap_handler( signum: libc::c_int, siginfo: *mut libc::siginfo_t, diff --git a/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs index 87159e0652b4..26298e905179 100644 --- a/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs +++ b/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs @@ -1,5 +1,6 @@ use crate::runtime::vm::traphandlers::{tls, TrapTest}; use crate::runtime::vm::VMContext; +use std::ffi::c_void; use std::io; use windows_sys::Win32::Foundation::*; use windows_sys::Win32::System::Diagnostics::Debug::*; @@ -23,15 +24,43 @@ extern "C" { /// Function which may handle custom signals while processing traps. pub type SignalHandler<'a> = dyn Fn(*mut EXCEPTION_POINTERS) -> bool + Send + Sync + 'a; -pub unsafe fn platform_init(_macos_use_mach_ports: bool) { - // 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() - ); +pub struct TrapHandler { + handle: *mut c_void, +} + +unsafe impl Send for TrapHandler {} +unsafe impl Sync for TrapHandler {} + +impl TrapHandler { + pub unsafe fn new(_macos_use_mach_ports: bool) -> TrapHandler { + // 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. + let handle = AddVectoredExceptionHandler(1, Some(exception_handler)); + if handle.is_null() { + panic!( + "failed to add exception handler: {}", + io::Error::last_os_error() + ); + } + TrapHandler { handle } + } + + pub fn validate_config(&self, _macos_use_mach_ports: bool) {} +} + +impl Drop for TrapHandler { + fn drop(&mut self) { + unsafe { + let rc = RemoveVectoredExceptionHandler(self.handle); + if rc == 0 { + eprintln!( + "failed to remove exception handler: {}", + io::Error::last_os_error() + ); + libc::abort(); + } + } } } diff --git a/crates/wasmtime/src/runtime/vm/traphandlers.rs b/crates/wasmtime/src/runtime/vm/traphandlers.rs index 365354e421d1..8bec876847b6 100644 --- a/crates/wasmtime/src/runtime/vm/traphandlers.rs +++ b/crates/wasmtime/src/runtime/vm/traphandlers.rs @@ -14,7 +14,7 @@ use crate::prelude::*; use crate::runtime::module::lookup_code; use crate::runtime::vm::sys::traphandlers; use crate::runtime::vm::{Instance, VMContext, VMRuntimeLimits}; -use crate::sync::OnceLock; +use crate::sync::RwLock; use core::cell::{Cell, UnsafeCell}; use core::mem::MaybeUninit; use core::ptr; @@ -25,6 +25,16 @@ pub use self::tls::{tls_eager_initialize, AsyncWasmCallState, PreviousAsyncWasmC pub use traphandlers::SignalHandler; +/// Platform-specific trap-handler state. +/// +/// This state is protected by a lock to synchronize access to it. Right now +/// it's a `RwLock` but it could be a `Mutex`, and `RwLock` is just chosen for +/// convenience as it's what's implemented in no_std. The performance here +/// should not be of consequence. +/// +/// This is initialized to `None` and then set as part of `init_traps`. +static TRAP_HANDLER: RwLock> = RwLock::new(None); + /// This function is required to be called before any WebAssembly is entered. /// This will configure global state such as signal handlers to prepare the /// process to receive wasm traps. @@ -37,18 +47,36 @@ pub use traphandlers::SignalHandler; /// This function will also panic if the `std` feature is disabled and it's /// called concurrently. pub fn init_traps(macos_use_mach_ports: bool) { - static INIT: OnceLock<()> = OnceLock::new(); - - INIT.get_or_init(|| unsafe { - traphandlers::platform_init(macos_use_mach_ports); - }); - - #[cfg(target_os = "macos")] - assert_eq!( - traphandlers::using_mach_ports(), - macos_use_mach_ports, - "cannot configure two different methods of signal handling in the same process" - ); + let mut lock = TRAP_HANDLER.write(); + match lock.as_mut() { + Some(state) => state.validate_config(macos_use_mach_ports), + None => *lock = Some(unsafe { traphandlers::TrapHandler::new(macos_use_mach_ports) }), + } +} + +/// De-initializes platform-specific state for trap handling. +/// +/// # Panics +/// +/// This function will also panic if the `std` feature is disabled and it's +/// called concurrently. +/// +/// # Aborts +/// +/// This may abort the process on some platforms where trap handling state +/// cannot be unloaded. +/// +/// # Unsafety +/// +/// This is not safe to be called unless all wasm code is unloaded. This is not +/// safe to be called on some platforms, like Unix, when other libraries +/// installed their own signal handlers after `init_traps` was called. +/// +/// There's more reasons for unsafety here than those articulated above, +/// generally this can only be called "if you know what you're doing". +pub unsafe fn deinit_traps() { + let mut lock = TRAP_HANDLER.write(); + let _ = lock.take(); } fn lazy_per_thread_init() { diff --git a/tests/unload-engine.rs b/tests/unload-engine.rs new file mode 100644 index 000000000000..c59af4f9ad31 --- /dev/null +++ b/tests/unload-engine.rs @@ -0,0 +1,29 @@ +//! A single-test executable which only tests `Engine::unload_process_handlers` +//! is possible. +//! +//! It's not safe for this binary to contain any other tests. + +use wasmtime::*; + +#[test] +#[cfg_attr(miri, ignore)] +fn test_unload_engine() { + for _ in 0..3 { + std::thread::spawn(|| { + let engine = Engine::default(); + { + let module = + Module::new(&engine, r#"(module (func (export "x") unreachable))"#).unwrap(); + let mut store = Store::new(&engine, ()); + let instance = Instance::new(&mut store, &module, &[]).unwrap(); + let func = instance.get_typed_func::<(), ()>(&mut store, "x").unwrap(); + assert!(func.call(&mut store, ()).unwrap_err().is::()); + } + unsafe { + engine.unload_process_handlers(); + } + }) + .join() + .unwrap(); + } +}