diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/rust/lucet-runtime-internals-wasmsbx/src/instance | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/lucet-runtime-internals-wasmsbx/src/instance')
4 files changed, 646 insertions, 0 deletions
diff --git a/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/siginfo_ext.c b/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/siginfo_ext.c new file mode 100644 index 0000000000..4dd9b58b4c --- /dev/null +++ b/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/siginfo_ext.c @@ -0,0 +1,6 @@ +#include <signal.h> + +void *siginfo_si_addr(siginfo_t *si) +{ + return si->si_addr; +} diff --git a/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/siginfo_ext.rs b/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/siginfo_ext.rs new file mode 100644 index 0000000000..863d8a8684 --- /dev/null +++ b/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/siginfo_ext.rs @@ -0,0 +1,15 @@ +use libc::{c_void, siginfo_t}; + +extern "C" { + fn siginfo_si_addr(si: *const siginfo_t) -> *const c_void; +} + +pub trait SiginfoExt { + fn si_addr_ext(&self) -> *const c_void; +} + +impl SiginfoExt for siginfo_t { + fn si_addr_ext(&self) -> *const c_void { + unsafe { siginfo_si_addr(self as *const siginfo_t) } + } +} diff --git a/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/signals.rs b/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/signals.rs new file mode 100644 index 0000000000..d52be927c7 --- /dev/null +++ b/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/signals.rs @@ -0,0 +1,447 @@ +use crate::context::Context; +use crate::error::Error; +use crate::instance::{ + siginfo_ext::SiginfoExt, FaultDetails, Instance, State, TerminationDetails, CURRENT_INSTANCE, + HOST_CTX, +}; +use crate::sysdeps::UContextPtr; +use lazy_static::lazy_static; +use libc::{c_int, c_void, siginfo_t, SIGBUS, SIGSEGV}; +use lucet_module::TrapCode; +use nix::sys::signal::{ + pthread_sigmask, raise, sigaction, SaFlags, SigAction, SigHandler, SigSet, SigmaskHow, Signal, +}; +use std::mem::MaybeUninit; +use std::panic; +use std::sync::{Arc, Mutex}; + +lazy_static! { + // TODO: work out an alternative to this that is signal-safe for `reraise_host_signal_in_handler` + static ref LUCET_SIGNAL_STATE: Mutex<Option<SignalState>> = Mutex::new(None); +} + +/// The value returned by +/// [`Instance.signal_handler`](struct.Instance.html#structfield.signal_handler) to determine the +/// outcome of a handled signal. +pub enum SignalBehavior { + /// Use default behavior, which switches back to the host with `State::Fault` populated. + Default, + /// Override default behavior and cause the instance to continue. + Continue, + /// Override default behavior and cause the instance to terminate. + Terminate, +} + +pub type SignalHandler = dyn Fn( + &Instance, + &Option<TrapCode>, + libc::c_int, + *const siginfo_t, + *const c_void, +) -> SignalBehavior; + +pub fn signal_handler_none( + _inst: &Instance, + _trapcode: &Option<TrapCode>, + _signum: libc::c_int, + _siginfo_ptr: *const siginfo_t, + _ucontext_ptr: *const c_void, +) -> SignalBehavior { + SignalBehavior::Default +} + +impl Instance { + pub(crate) fn with_signals_on<F, R>(&mut self, f: F) -> Result<R, Error> + where + F: FnOnce(&mut Instance) -> Result<R, Error>, + { + // Set up the signal stack for this thread. Note that because signal stacks are per-thread, + // rather than per-process, we do this for every run, while the signal handler is installed + // only once per process. + let guest_sigstack = SigStack::new( + self.alloc.slot().sigstack, + SigStackFlags::empty(), + self.alloc.slot().limits.signal_stack_size, + ); + let previous_sigstack = unsafe { sigaltstack(Some(guest_sigstack)) } + .expect("enabling or changing the signal stack succeeds"); + if let Some(previous_sigstack) = previous_sigstack { + assert!( + !previous_sigstack + .flags() + .contains(SigStackFlags::SS_ONSTACK), + "an instance was created with a signal stack" + ); + } + let mut ostate = LUCET_SIGNAL_STATE.lock().unwrap(); + if let Some(ref mut state) = *ostate { + state.counter += 1; + } else { + unsafe { + setup_guest_signal_state(&mut ostate); + } + } + drop(ostate); + + // run the body + let res = f(self); + + let mut ostate = LUCET_SIGNAL_STATE.lock().unwrap(); + let counter_zero = if let Some(ref mut state) = *ostate { + state.counter -= 1; + if state.counter == 0 { + unsafe { + restore_host_signal_state(state); + } + true + } else { + false + } + } else { + panic!("signal handlers weren't installed at instance exit"); + }; + if counter_zero { + *ostate = None; + } + + unsafe { + // restore the host signal stack for this thread + if !altstack_flags() + .expect("the current stack flags could be retrieved") + .contains(SigStackFlags::SS_ONSTACK) + { + sigaltstack(previous_sigstack).expect("sigaltstack restoration succeeds"); + } + } + + res + } +} + +/// Signal handler installed during instance execution. +/// +/// This function is only designed to handle signals that are the direct result of execution of a +/// hardware instruction from the faulting WASM thread. It thus safely assumes the signal is +/// directed specifically at this thread (i.e. not a different thread or the process as a whole). +extern "C" fn handle_signal(signum: c_int, siginfo_ptr: *mut siginfo_t, ucontext_ptr: *mut c_void) { + let signal = Signal::from_c_int(signum).expect("signum is a valid signal"); + if !(signal == Signal::SIGBUS + || signal == Signal::SIGSEGV + || signal == Signal::SIGILL + || signal == Signal::SIGFPE) + { + panic!("unexpected signal in guest signal handler: {:?}", signal); + } + assert!(!siginfo_ptr.is_null(), "siginfo must not be null"); + + // Safety: when using a SA_SIGINFO sigaction, the third argument can be cast to a `ucontext_t` + // pointer per the manpage + assert!(!ucontext_ptr.is_null(), "ucontext_ptr must not be null"); + let ctx = UContextPtr::new(ucontext_ptr); + let rip = ctx.get_ip(); + + let switch_to_host = CURRENT_INSTANCE.with(|current_instance| { + let mut current_instance = current_instance.borrow_mut(); + + if current_instance.is_none() { + // If there is no current instance, we've caught a signal raised by a thread that's not + // running a lucet instance. Restore the host signal handler and reraise the signal, + // then return if the host handler returns + unsafe { + reraise_host_signal_in_handler(signal, signum, siginfo_ptr, ucontext_ptr); + } + // don't try context-switching + return false; + } + + // Safety: the memory pointed to by CURRENT_INSTANCE should be a valid instance. This is not + // a trivial property, but relies on the compiler not emitting guest programs that can + // overwrite the instance. + let inst = unsafe { + current_instance + .as_mut() + .expect("current instance exists") + .as_mut() + }; + + let trapcode = inst.module.lookup_trapcode(rip); + + let behavior = (inst.signal_handler)(inst, &trapcode, signum, siginfo_ptr, ucontext_ptr); + match behavior { + SignalBehavior::Continue => { + // return to the guest context without making any modifications to the instance + false + } + SignalBehavior::Terminate => { + // set the state before jumping back to the host context + inst.state = State::Terminating { + details: TerminationDetails::Signal, + }; + true + } + SignalBehavior::Default => { + // safety: pointer is checked for null at the top of the function, and the + // manpage guarantees that a siginfo_t will be passed as the second argument + let siginfo = unsafe { *siginfo_ptr }; + let rip_addr = rip as usize; + // If the trap table lookup returned unknown, it is a fatal error + let unknown_fault = trapcode.is_none(); + + // If the trap was a segv or bus fault and the addressed memory was outside the + // guard pages, it is also a fatal error + let outside_guard = (siginfo.si_signo == SIGSEGV || siginfo.si_signo == SIGBUS) + && !inst.alloc.addr_in_guard_page(siginfo.si_addr_ext()); + + // record the fault and jump back to the host context + inst.state = State::Faulted { + details: FaultDetails { + fatal: unknown_fault || outside_guard, + trapcode: trapcode, + rip_addr, + // Details set to `None` here: have to wait until `verify_trap_safety` to + // fill in these details, because access may not be signal safe. + rip_addr_details: None, + }, + siginfo, + context: ctx.into(), + }; + true + } + } + }); + + if switch_to_host { + HOST_CTX.with(|host_ctx| unsafe { + Context::set_from_signal(&*host_ctx.get()) + .expect("can successfully switch back to the host context"); + }); + unreachable!() + } +} + +struct SignalState { + counter: usize, + saved_sigbus: SigAction, + saved_sigfpe: SigAction, + saved_sigill: SigAction, + saved_sigsegv: SigAction, + saved_panic_hook: Option<Arc<Box<dyn Fn(&panic::PanicInfo<'_>) + Sync + Send + 'static>>>, +} + +// raw pointers in the saved types +unsafe impl Send for SignalState {} + +unsafe fn setup_guest_signal_state(ostate: &mut Option<SignalState>) { + let mut masked_signals = SigSet::empty(); + masked_signals.add(Signal::SIGBUS); + masked_signals.add(Signal::SIGFPE); + masked_signals.add(Signal::SIGILL); + masked_signals.add(Signal::SIGSEGV); + + // setup signal handlers + let sa = SigAction::new( + SigHandler::SigAction(handle_signal), + SaFlags::SA_RESTART | SaFlags::SA_SIGINFO | SaFlags::SA_ONSTACK, + masked_signals, + ); + let saved_sigbus = sigaction(Signal::SIGBUS, &sa).expect("sigaction succeeds"); + let saved_sigfpe = sigaction(Signal::SIGFPE, &sa).expect("sigaction succeeds"); + let saved_sigill = sigaction(Signal::SIGILL, &sa).expect("sigaction succeeds"); + let saved_sigsegv = sigaction(Signal::SIGSEGV, &sa).expect("sigaction succeeds"); + + let saved_panic_hook = Some(setup_guest_panic_hook()); + + *ostate = Some(SignalState { + counter: 1, + saved_sigbus, + saved_sigfpe, + saved_sigill, + saved_sigsegv, + saved_panic_hook, + }); +} + +fn setup_guest_panic_hook() -> Arc<Box<dyn Fn(&panic::PanicInfo<'_>) + Sync + Send + 'static>> { + let saved_panic_hook = Arc::new(panic::take_hook()); + let closure_saved_panic_hook = saved_panic_hook.clone(); + std::panic::set_hook(Box::new(move |panic_info| { + if panic_info + .payload() + .downcast_ref::<TerminationDetails>() + .is_none() + { + closure_saved_panic_hook(panic_info); + } else { + // this is a panic used to implement instance termination (such as + // `lucet_hostcall_terminate!`), so we don't want to print a backtrace; instead, we do + // nothing + } + })); + saved_panic_hook +} + +unsafe fn restore_host_signal_state(state: &mut SignalState) { + // restore signal handlers + sigaction(Signal::SIGBUS, &state.saved_sigbus).expect("sigaction succeeds"); + sigaction(Signal::SIGFPE, &state.saved_sigfpe).expect("sigaction succeeds"); + sigaction(Signal::SIGILL, &state.saved_sigill).expect("sigaction succeeds"); + sigaction(Signal::SIGSEGV, &state.saved_sigsegv).expect("sigaction succeeds"); + + // restore panic hook + drop(panic::take_hook()); + state + .saved_panic_hook + .take() + .map(|hook| Arc::try_unwrap(hook).map(|hook| panic::set_hook(hook))); +} + +unsafe fn reraise_host_signal_in_handler( + sig: Signal, + signum: libc::c_int, + siginfo_ptr: *mut libc::siginfo_t, + ucontext_ptr: *mut c_void, +) { + let saved_handler = { + // TODO: avoid taking a mutex here, probably by having some static muts just for this + // function + if let Some(ref state) = *LUCET_SIGNAL_STATE.lock().unwrap() { + match sig { + Signal::SIGBUS => state.saved_sigbus.clone(), + Signal::SIGFPE => state.saved_sigfpe.clone(), + Signal::SIGILL => state.saved_sigill.clone(), + Signal::SIGSEGV => state.saved_sigsegv.clone(), + sig => panic!( + "unexpected signal in reraise_host_signal_in_handler: {:?}", + sig + ), + } + } else { + // this case is very fishy; it can arise when the last lucet instance spins down and + // uninstalls the lucet handlers while a signal handler is running on this thread, but + // before taking the mutex above. The theory is that if this has happened, the host + // handler has been reinstalled, so we shouldn't end up back here if we reraise + + // unmask the signal to reraise; we don't have to restore it because the handler will return + // after this. If it signals again between here and now, that's a double fault and the + // process is going to die anyway + let mut unmask = SigSet::empty(); + unmask.add(sig); + pthread_sigmask(SigmaskHow::SIG_UNBLOCK, Some(&unmask), None) + .expect("pthread_sigmask succeeds"); + // if there's no current signal state, just re-raise and hope for the best + raise(sig).expect("raise succeeds"); + return; + } + }; + + match saved_handler.handler() { + SigHandler::SigDfl => { + // reinstall default signal handler and reraise the signal; this should terminate the + // program + sigaction(sig, &saved_handler).expect("sigaction succeeds"); + let mut unmask = SigSet::empty(); + unmask.add(sig); + pthread_sigmask(SigmaskHow::SIG_UNBLOCK, Some(&unmask), None) + .expect("pthread_sigmask succeeds"); + raise(sig).expect("raise succeeds"); + } + SigHandler::SigIgn => { + // don't do anything; if we hit this case, whatever program is hosting us is almost + // certainly doing something wrong, because our set of signals requires intervention to + // proceed + return; + } + SigHandler::Handler(f) => { + // call the saved handler directly so there is no altstack confusion + f(signum) + } + SigHandler::SigAction(f) => { + // call the saved handler directly so there is no altstack confusion + f(signum, siginfo_ptr, ucontext_ptr) + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// A collection of wrappers that will be upstreamed to the `nix` crate eventually. +//////////////////////////////////////////////////////////////////////////////////////////////////// + +use bitflags::bitflags; + +#[derive(Copy, Clone)] +pub struct SigStack { + stack: libc::stack_t, +} + +impl SigStack { + pub fn new(sp: *mut libc::c_void, flags: SigStackFlags, size: libc::size_t) -> SigStack { + let stack = libc::stack_t { + ss_sp: sp, + ss_flags: flags.bits(), + ss_size: size, + }; + SigStack { stack } + } + + pub fn disabled() -> SigStack { + let stack = libc::stack_t { + ss_sp: std::ptr::null_mut(), + ss_flags: SigStackFlags::SS_DISABLE.bits(), + ss_size: libc::SIGSTKSZ, + }; + SigStack { stack } + } + + pub fn flags(&self) -> SigStackFlags { + SigStackFlags::from_bits_truncate(self.stack.ss_flags) + } +} + +impl AsRef<libc::stack_t> for SigStack { + fn as_ref(&self) -> &libc::stack_t { + &self.stack + } +} + +impl AsMut<libc::stack_t> for SigStack { + fn as_mut(&mut self) -> &mut libc::stack_t { + &mut self.stack + } +} + +bitflags! { + pub struct SigStackFlags: libc::c_int { + const SS_ONSTACK = libc::SS_ONSTACK; + const SS_DISABLE = libc::SS_DISABLE; + } +} + +pub unsafe fn sigaltstack(new_sigstack: Option<SigStack>) -> nix::Result<Option<SigStack>> { + let mut previous_stack = MaybeUninit::<libc::stack_t>::uninit(); + let disabled_sigstack = SigStack::disabled(); + let new_stack = match new_sigstack { + None => &disabled_sigstack.stack, + Some(ref new_stack) => &new_stack.stack, + }; + let res = libc::sigaltstack( + new_stack as *const libc::stack_t, + previous_stack.as_mut_ptr(), + ); + nix::errno::Errno::result(res).map(|_| { + let sigstack = SigStack { + stack: previous_stack.assume_init(), + }; + if sigstack.flags().contains(SigStackFlags::SS_DISABLE) { + None + } else { + Some(sigstack) + } + }) +} + +pub unsafe fn altstack_flags() -> nix::Result<SigStackFlags> { + let mut current_stack = MaybeUninit::<libc::stack_t>::uninit(); + let res = libc::sigaltstack(std::ptr::null_mut(), current_stack.as_mut_ptr()); + nix::errno::Errno::result(res) + .map(|_| SigStackFlags::from_bits_truncate(current_stack.assume_init().ss_flags)) +} diff --git a/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/state.rs b/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/state.rs new file mode 100644 index 0000000000..1be0346675 --- /dev/null +++ b/third_party/rust/lucet-runtime-internals-wasmsbx/src/instance/state.rs @@ -0,0 +1,178 @@ +use crate::instance::siginfo_ext::SiginfoExt; +use crate::instance::{FaultDetails, TerminationDetails, YieldedVal}; +use crate::sysdeps::UContext; +use libc::{SIGBUS, SIGSEGV}; +use std::any::Any; +use std::ffi::{CStr, CString}; + +/// The representation of a Lucet instance's state machine. +pub enum State { + /// The instance is ready to run. + /// + /// Transitions to `Running` when the instance is run, or to `Ready` when it's reset. + Ready, + + /// The instance is running. + /// + /// Transitions to `Ready` when the guest function returns normally, or to `Faulted`, + /// `Terminating`, or `Yielding` if the instance faults, terminates, or yields. + Running, + + /// The instance has faulted, potentially fatally. + /// + /// Transitions to `Faulted` when filling in additional fault details, to `Running` if + /// re-running a non-fatally faulted instance, or to `Ready` when the instance is reset. + Faulted { + details: FaultDetails, + siginfo: libc::siginfo_t, + context: UContext, + }, + + /// The instance is in the process of terminating. + /// + /// Transitions only to `Terminated`; the `TerminationDetails` are always extracted into a + /// `RunResult` before anything else happens to the instance. + Terminating { details: TerminationDetails }, + + /// The instance has terminated, and must be reset before running again. + /// + /// Transitions to `Ready` if the instance is reset. + Terminated, + + /// The instance is in the process of yielding. + /// + /// Transitions only to `Yielded`; the `YieldedVal` is always extracted into a + /// `RunResult` before anything else happens to the instance. + Yielding { + val: YieldedVal, + /// A phantom value carrying the type of the expected resumption value. + /// + /// Concretely, this should only ever be `Box<PhantomData<R>>` where `R` is the type + /// the guest expects upon resumption. + expecting: Box<dyn Any>, + }, + + /// The instance has yielded. + /// + /// Transitions to `Running` if the instance is resumed, or to `Ready` if the instance is reset. + Yielded { + /// A phantom value carrying the type of the expected resumption value. + /// + /// Concretely, this should only ever be `Box<PhantomData<R>>` where `R` is the type + /// the guest expects upon resumption. + expecting: Box<dyn Any>, + }, + + /// A placeholder state used with `std::mem::replace()` when a new state must be constructed by + /// moving values out of an old state. + /// + /// This is used so that we do not need a `Clone` impl for this type, which would add + /// unnecessary constraints to the types of values instances could yield or terminate with. + /// + /// It is an error for this state to appear outside of a transition between other states. + Transitioning, +} + +impl std::fmt::Display for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + State::Ready => write!(f, "ready"), + State::Running => write!(f, "running"), + State::Faulted { + details, siginfo, .. + } => { + write!(f, "{}", details)?; + write!( + f, + " triggered by {}: ", + strsignal_wrapper(siginfo.si_signo) + .into_string() + .expect("strsignal returns valid UTF-8") + )?; + + if siginfo.si_signo == SIGSEGV || siginfo.si_signo == SIGBUS { + // We know this is inside the heap guard, because by the time we get here, + // `lucet_error_verify_trap_safety` will have run and validated it. + write!( + f, + " accessed memory at {:p} (inside heap guard)", + siginfo.si_addr_ext() + )?; + } + Ok(()) + } + State::Terminated { .. } => write!(f, "terminated"), + State::Terminating { .. } => write!(f, "terminating"), + State::Yielding { .. } => write!(f, "yielding"), + State::Yielded { .. } => write!(f, "yielded"), + State::Transitioning { .. } => { + write!(f, "transitioning (IF YOU SEE THIS, THERE'S PROBABLY A BUG)") + } + } + } +} + +impl State { + pub fn is_ready(&self) -> bool { + if let State::Ready { .. } = self { + true + } else { + false + } + } + + pub fn is_running(&self) -> bool { + if let State::Running = self { + true + } else { + false + } + } + + pub fn is_fault(&self) -> bool { + if let State::Faulted { .. } = self { + true + } else { + false + } + } + + pub fn is_fatal(&self) -> bool { + if let State::Faulted { + details: FaultDetails { fatal, .. }, + .. + } = self + { + *fatal + } else { + false + } + } + + pub fn is_terminated(&self) -> bool { + if let State::Terminated { .. } = self { + true + } else { + false + } + } + + pub fn is_yielded(&self) -> bool { + if let State::Yielded { .. } = self { + true + } else { + false + } + } +} + +// TODO: PR into `libc` +extern "C" { + #[no_mangle] + fn strsignal(sig: libc::c_int) -> *mut libc::c_char; +} + +// TODO: PR into `nix` +fn strsignal_wrapper(sig: libc::c_int) -> CString { + unsafe { CStr::from_ptr(strsignal(sig)).to_owned() } +} |