wasmtime/runtime/vm/sys/unix/
signals.rs

1//! Trap handling on Unix based on POSIX signals.
2
3use crate::prelude::*;
4use crate::runtime::vm::sys::traphandlers::wasmtime_longjmp;
5use crate::runtime::vm::traphandlers::{tls, TrapRegisters, TrapTest};
6use std::cell::RefCell;
7use std::io;
8use std::mem;
9use std::ptr::{self, null_mut};
10
11/// Function which may handle custom signals while processing traps.
12pub type SignalHandler =
13    Box<dyn Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool + Send + Sync>;
14
15const UNINIT_SIGACTION: libc::sigaction = unsafe { mem::zeroed() };
16static mut PREV_SIGSEGV: libc::sigaction = UNINIT_SIGACTION;
17static mut PREV_SIGBUS: libc::sigaction = UNINIT_SIGACTION;
18static mut PREV_SIGILL: libc::sigaction = UNINIT_SIGACTION;
19static mut PREV_SIGFPE: libc::sigaction = UNINIT_SIGACTION;
20
21pub struct TrapHandler;
22
23impl TrapHandler {
24    /// Installs all trap handlers.
25    ///
26    /// # Unsafety
27    ///
28    /// This function is unsafe because it's not safe to call concurrently and
29    /// it's not safe to call if the trap handlers have already been initialized
30    /// for this process.
31    pub unsafe fn new(macos_use_mach_ports: bool) -> TrapHandler {
32        // Either mach ports shouldn't be in use or we shouldn't be on macOS,
33        // otherwise the `machports.rs` module should be used instead.
34        assert!(!macos_use_mach_ports || !cfg!(target_vendor = "apple"));
35
36        foreach_handler(|slot, signal| {
37            let mut handler: libc::sigaction = mem::zeroed();
38            // The flags here are relatively careful, and they are...
39            //
40            // SA_SIGINFO gives us access to information like the program
41            // counter from where the fault happened.
42            //
43            // SA_ONSTACK allows us to handle signals on an alternate stack,
44            // so that the handler can run in response to running out of
45            // stack space on the main stack. Rust installs an alternate
46            // stack with sigaltstack, so we rely on that.
47            //
48            // SA_NODEFER allows us to reenter the signal handler if we
49            // crash while handling the signal, and fall through to the
50            // Breakpad handler by testing handlingSegFault.
51            handler.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER | libc::SA_ONSTACK;
52            handler.sa_sigaction = trap_handler as usize;
53            libc::sigemptyset(&mut handler.sa_mask);
54            if libc::sigaction(signal, &handler, slot) != 0 {
55                panic!(
56                    "unable to install signal handler: {}",
57                    io::Error::last_os_error(),
58                );
59            }
60        });
61
62        TrapHandler
63    }
64
65    pub fn validate_config(&self, macos_use_mach_ports: bool) {
66        assert!(!macos_use_mach_ports || !cfg!(target_vendor = "apple"));
67    }
68}
69
70fn foreach_handler(mut f: impl FnMut(*mut libc::sigaction, i32)) {
71    // Allow handling OOB with signals on all architectures
72    f(&raw mut PREV_SIGSEGV, libc::SIGSEGV);
73
74    // Handle `unreachable` instructions which execute `ud2` right now
75    f(&raw mut PREV_SIGILL, libc::SIGILL);
76
77    // x86 and s390x use SIGFPE to report division by zero
78    if cfg!(target_arch = "x86_64") || cfg!(target_arch = "s390x") {
79        f(&raw mut PREV_SIGFPE, libc::SIGFPE);
80    }
81
82    // Sometimes we need to handle SIGBUS too:
83    // - On Darwin, guard page accesses are raised as SIGBUS.
84    if cfg!(target_vendor = "apple") || cfg!(target_os = "freebsd") {
85        f(&raw mut PREV_SIGBUS, libc::SIGBUS);
86    }
87
88    // TODO(#1980): x86-32, if we support it, will also need a SIGFPE handler.
89    // TODO(#1173): ARM32, if we support it, will also need a SIGBUS handler.
90}
91
92impl Drop for TrapHandler {
93    fn drop(&mut self) {
94        unsafe {
95            foreach_handler(|slot, signal| {
96                let mut prev: libc::sigaction = mem::zeroed();
97
98                // Restore the previous handler that this signal had.
99                if libc::sigaction(signal, slot, &mut prev) != 0 {
100                    eprintln!(
101                        "unable to reinstall signal handler: {}",
102                        io::Error::last_os_error(),
103                    );
104                    libc::abort();
105                }
106
107                // If our trap handler wasn't currently listed for this process
108                // then that's a problem because we have just corrupted the
109                // signal handler state and don't know how to remove ourselves
110                // from the signal handling state. Inform the user of this and
111                // abort the process.
112                if prev.sa_sigaction != trap_handler as usize {
113                    eprintln!(
114                        "
115Wasmtime's signal handler was not the last signal handler to be installed
116in the process so it's not certain how to unload signal handlers. In this
117situation the Engine::unload_process_handlers API is not applicable and requires
118perhaps initializing libraries in a different order. The process will be aborted
119now.
120"
121                    );
122                    libc::abort();
123                }
124            });
125        }
126    }
127}
128
129unsafe extern "C" fn trap_handler(
130    signum: libc::c_int,
131    siginfo: *mut libc::siginfo_t,
132    context: *mut libc::c_void,
133) {
134    let previous = match signum {
135        libc::SIGSEGV => &raw const PREV_SIGSEGV,
136        libc::SIGBUS => &raw const PREV_SIGBUS,
137        libc::SIGFPE => &raw const PREV_SIGFPE,
138        libc::SIGILL => &raw const PREV_SIGILL,
139        _ => panic!("unknown signal: {signum}"),
140    };
141    let handled = tls::with(|info| {
142        // If no wasm code is executing, we don't handle this as a wasm
143        // trap.
144        let info = match info {
145            Some(info) => info,
146            None => return false,
147        };
148
149        // If we hit an exception while handling a previous trap, that's
150        // quite bad, so bail out and let the system handle this
151        // recursive segfault.
152        //
153        // Otherwise flag ourselves as handling a trap, do the trap
154        // handling, and reset our trap handling flag. Then we figure
155        // out what to do based on the result of the trap handling.
156        let faulting_addr = match signum {
157            libc::SIGSEGV | libc::SIGBUS => Some((*siginfo).si_addr() as usize),
158            _ => None,
159        };
160        let regs = get_trap_registers(context, signum);
161        let test = info.test_if_trap(regs, faulting_addr, |handler| {
162            handler(signum, siginfo, context)
163        });
164
165        // Figure out what to do based on the result of this handling of
166        // the trap. Note that our sentinel value of 1 means that the
167        // exception was handled by a custom exception handler, so we
168        // keep executing.
169        let jmp_buf = match test {
170            TrapTest::NotWasm => {
171                if let Some(faulting_addr) = faulting_addr {
172                    let start = info.async_guard_range.start;
173                    let end = info.async_guard_range.end;
174                    if start as usize <= faulting_addr && faulting_addr < end as usize {
175                        abort_stack_overflow();
176                    }
177                }
178                return false;
179            }
180            TrapTest::HandledByEmbedder => return true,
181            TrapTest::Trap { jmp_buf } => jmp_buf,
182        };
183        // On macOS this is a bit special, unfortunately. If we were to
184        // `siglongjmp` out of the signal handler that notably does
185        // *not* reset the sigaltstack state of our signal handler. This
186        // seems to trick the kernel into thinking that the sigaltstack
187        // is still in use upon delivery of the next signal, meaning
188        // that the sigaltstack is not ever used again if we immediately
189        // call `wasmtime_longjmp` here.
190        //
191        // Note that if we use `longjmp` instead of `siglongjmp` then
192        // the problem is fixed. The problem with that, however, is that
193        // `setjmp` is much slower than `sigsetjmp` due to the
194        // preservation of the process's signal mask. The reason
195        // `longjmp` appears to work is that it seems to call a function
196        // (according to published macOS sources) called
197        // `_sigunaltstack` which updates the kernel to say the
198        // sigaltstack is no longer in use. We ideally want to call that
199        // here but I don't think there's a stable way for us to call
200        // that.
201        //
202        // Given all that, on macOS only, we do the next best thing. We
203        // return from the signal handler after updating the register
204        // context. This will cause control to return to our shim
205        // function defined here which will perform the
206        // `wasmtime_longjmp` (`siglongjmp`) for us. The reason this
207        // works is that by returning from the signal handler we'll
208        // trigger all the normal machinery for "the signal handler is
209        // done running" which will clear the sigaltstack flag and allow
210        // reusing it for the next signal. Then upon resuming in our custom
211        // code we blow away the stack anyway with a longjmp.
212        if cfg!(target_vendor = "apple") {
213            unsafe extern "C" fn wasmtime_longjmp_shim(jmp_buf: *const u8) {
214                wasmtime_longjmp(jmp_buf)
215            }
216            set_pc(context, wasmtime_longjmp_shim as usize, jmp_buf as usize);
217            return true;
218        }
219        wasmtime_longjmp(jmp_buf)
220    });
221
222    if handled {
223        return;
224    }
225
226    delegate_signal_to_previous_handler(previous, signum, siginfo, context)
227}
228
229pub unsafe fn delegate_signal_to_previous_handler(
230    previous: *const libc::sigaction,
231    signum: libc::c_int,
232    siginfo: *mut libc::siginfo_t,
233    context: *mut libc::c_void,
234) {
235    // This signal is not for any compiled wasm code we expect, so we
236    // need to forward the signal to the next handler. If there is no
237    // next handler (SIG_IGN or SIG_DFL), then it's time to crash. To do
238    // this, we set the signal back to its original disposition and
239    // return. This will cause the faulting op to be re-executed which
240    // will crash in the normal way. If there is a next handler, call
241    // it. It will either crash synchronously, fix up the instruction
242    // so that execution can continue and return, or trigger a crash by
243    // returning the signal to it's original disposition and returning.
244    let previous = *previous;
245    if previous.sa_flags & libc::SA_SIGINFO != 0 {
246        mem::transmute::<usize, extern "C" fn(libc::c_int, *mut libc::siginfo_t, *mut libc::c_void)>(
247            previous.sa_sigaction,
248        )(signum, siginfo, context)
249    } else if previous.sa_sigaction == libc::SIG_DFL || previous.sa_sigaction == libc::SIG_IGN {
250        libc::sigaction(signum, &previous as *const _, ptr::null_mut());
251    } else {
252        mem::transmute::<usize, extern "C" fn(libc::c_int)>(previous.sa_sigaction)(signum)
253    }
254}
255
256pub fn abort_stack_overflow() -> ! {
257    unsafe {
258        let msg = "execution on async fiber has overflowed its stack";
259        libc::write(libc::STDERR_FILENO, msg.as_ptr().cast(), msg.len());
260        libc::abort();
261    }
262}
263
264#[allow(clippy::cast_possible_truncation)] // too fiddly to handle and wouldn't
265                                           // help much anyway
266unsafe fn get_trap_registers(cx: *mut libc::c_void, _signum: libc::c_int) -> TrapRegisters {
267    cfg_if::cfg_if! {
268        if #[cfg(all(any(target_os = "linux", target_os = "android", target_os = "illumos"), target_arch = "x86_64"))] {
269            let cx = &*(cx as *const libc::ucontext_t);
270            TrapRegisters {
271                pc: cx.uc_mcontext.gregs[libc::REG_RIP as usize] as usize,
272                fp: cx.uc_mcontext.gregs[libc::REG_RBP as usize] as usize,
273            }
274        } else if #[cfg(all(target_os = "linux", target_arch = "x86"))] {
275            let cx = &*(cx as *const libc::ucontext_t);
276            TrapRegisters {
277                pc: cx.uc_mcontext.gregs[libc::REG_EIP as usize] as usize,
278                fp: cx.uc_mcontext.gregs[libc::REG_EBP as usize] as usize,
279            }
280        } else if #[cfg(all(any(target_os = "linux", target_os = "android"), target_arch = "aarch64"))] {
281            let cx = &*(cx as *const libc::ucontext_t);
282            TrapRegisters {
283                pc: cx.uc_mcontext.pc as usize,
284                fp: cx.uc_mcontext.regs[29] as usize,
285            }
286        } else if #[cfg(all(target_os = "linux", target_arch = "s390x"))] {
287            // On s390x, SIGILL and SIGFPE are delivered with the PSW address
288            // pointing *after* the faulting instruction, while SIGSEGV and
289            // SIGBUS are delivered with the PSW address pointing *to* the
290            // faulting instruction.  To handle this, the code generator registers
291            // any trap that results in one of "late" signals on the last byte
292            // of the instruction, and any trap that results in one of the "early"
293            // signals on the first byte of the instruction (as usual).  This
294            // means we simply need to decrement the reported PSW address by
295            // one in the case of a "late" signal here to ensure we always
296            // correctly find the associated trap handler.
297            let trap_offset = match _signum {
298                libc::SIGILL | libc::SIGFPE => 1,
299                _ => 0,
300            };
301            let cx = &*(cx as *const libc::ucontext_t);
302            TrapRegisters {
303                pc: (cx.uc_mcontext.psw.addr - trap_offset) as usize,
304                fp: *(cx.uc_mcontext.gregs[15] as *const usize),
305            }
306        } else if #[cfg(all(target_vendor = "apple", target_arch = "x86_64"))] {
307            let cx = &*(cx as *const libc::ucontext_t);
308            TrapRegisters {
309                pc: (*cx.uc_mcontext).__ss.__rip as usize,
310                fp: (*cx.uc_mcontext).__ss.__rbp as usize,
311            }
312        } else if #[cfg(all(target_vendor = "apple", target_arch = "aarch64"))] {
313            let cx = &*(cx as *const libc::ucontext_t);
314            TrapRegisters {
315                pc: (*cx.uc_mcontext).__ss.__pc as usize,
316                fp: (*cx.uc_mcontext).__ss.__fp as usize,
317            }
318        } else if #[cfg(all(target_os = "freebsd", target_arch = "x86_64"))] {
319            let cx = &*(cx as *const libc::ucontext_t);
320            TrapRegisters {
321                pc: cx.uc_mcontext.mc_rip as usize,
322                fp: cx.uc_mcontext.mc_rbp as usize,
323            }
324        } else if #[cfg(all(target_os = "linux", target_arch = "riscv64"))] {
325            let cx = &*(cx as *const libc::ucontext_t);
326            TrapRegisters {
327                pc: cx.uc_mcontext.__gregs[libc::REG_PC] as usize,
328                fp: cx.uc_mcontext.__gregs[libc::REG_S0] as usize,
329            }
330        } else if #[cfg(all(target_os = "freebsd", target_arch = "aarch64"))] {
331            let cx = &*(cx as *const libc::mcontext_t);
332            TrapRegisters {
333                pc: cx.mc_gpregs.gp_elr as usize,
334                fp: cx.mc_gpregs.gp_x[29] as usize,
335            }
336        } else if #[cfg(all(target_os = "openbsd", target_arch = "x86_64"))] {
337            let cx = &*(cx as *const libc::ucontext_t);
338            TrapRegisters {
339                pc: cx.sc_rip as usize,
340                fp: cx.sc_rbp as usize,
341            }
342        } else if #[cfg(all(target_os = "linux", target_arch = "arm"))] {
343            let cx = &*(cx as *const libc::ucontext_t);
344            TrapRegisters {
345                pc: cx.uc_mcontext.arm_pc as usize,
346                fp: cx.uc_mcontext.arm_fp as usize,
347            }
348        } else {
349            compile_error!("unsupported platform");
350            panic!();
351        }
352    }
353}
354
355// This is only used on macOS targets for calling an unwinding shim
356// function to ensure that we return from the signal handler.
357//
358// See more comments above where this is called for what it's doing.
359unsafe fn set_pc(cx: *mut libc::c_void, pc: usize, arg1: usize) {
360    cfg_if::cfg_if! {
361        if #[cfg(not(target_vendor = "apple"))] {
362            let _ = (cx, pc, arg1);
363            unreachable!(); // not used on these platforms
364        } else if #[cfg(target_arch = "x86_64")] {
365            let cx = &mut *(cx as *mut libc::ucontext_t);
366            (*cx.uc_mcontext).__ss.__rip = pc as u64;
367            (*cx.uc_mcontext).__ss.__rdi = arg1 as u64;
368            // We're simulating a "pseudo-call" so we need to ensure
369            // stack alignment is properly respected, notably that on a
370            // `call` instruction the stack is 8/16-byte aligned, then
371            // the function adjusts itself to be 16-byte aligned.
372            //
373            // Most of the time the stack pointer is 16-byte aligned at
374            // the time of the trap but for more robust-ness with JIT
375            // code where it may ud2 in a prologue check before the
376            // stack is aligned we double-check here.
377            if (*cx.uc_mcontext).__ss.__rsp % 16 == 0 {
378                (*cx.uc_mcontext).__ss.__rsp -= 8;
379            }
380        } else if #[cfg(target_arch = "aarch64")] {
381            let cx = &mut *(cx as *mut libc::ucontext_t);
382            (*cx.uc_mcontext).__ss.__pc = pc as u64;
383            (*cx.uc_mcontext).__ss.__x[0] = arg1 as u64;
384        } else {
385            compile_error!("unsupported apple target architecture");
386        }
387    }
388}
389
390/// A function for registering a custom alternate signal stack (sigaltstack).
391///
392/// Rust's libstd installs an alternate stack with size `SIGSTKSZ`, which is not
393/// always large enough for our signal handling code. Override it by creating
394/// and registering our own alternate stack that is large enough and has a guard
395/// page.
396///
397/// Note that one might reasonably ask why do this at all? Why not remove
398/// `SA_ONSTACK` from our signal handlers entirely? The basic reason for that is
399/// because we want to print a message on stack overflow. The Rust standard
400/// library will print this message by default and by us overriding the
401/// `SIGSEGV` handler above we're now sharing responsibility for that as well.
402/// We must have `SA_ONSTACK` to even attempt to being able to printing this
403/// message, and so we leave it turned on. Wasmtime will determine a stack
404/// overflow fault isn't caused by wasm and then forward to libstd's signal
405/// handler which will actually print-and-abort.
406///
407/// Another reasonable question might be why we need to increase the size of the
408/// sigaltstack at all? This is something which we may want to reconsider in the
409/// future. For now it helps keep debug builds working which consume more stack
410/// when handling normal wasm out-of-bounds and faults. Perhaps in the future we
411/// could optimize this more or maybe even do something clever like lazily
412/// allocate the sigaltstack on the fault itself. (e.g. trampoline from a tiny
413/// stack to the "big stack" during a wasm fault or something like that)
414#[cold]
415pub fn lazy_per_thread_init() {
416    // This is a load-bearing requirement to keep address-sanitizer working and
417    // prevent crashes during fuzzing. The general idea here is that we skip the
418    // sigaltstack setup below entirely on asan builds, aka fuzzing. The exact
419    // reason for this is not entirely known, but the closest guess we have at
420    // this time is something like:
421    //
422    // * ASAN builds intercept mmap/munmap to keep track of what's going on.
423    // * The sigaltstack below registers a TLS destructor for when the current
424    //   thread exits to deallocate the stack.
425    // * ASAN looks to also have TLS destructors for its own internal state.
426    // * The current assumption is that the order of these TLS destructors can
427    //   cause corruption in ASAN state where if we run after asan's destructor
428    //   it may intercept munmap and then asan doesn't know it's been
429    //   de-initialized yet.
430    //
431    // The reproduction of this involved a standalone project built with
432    // `-Zsanitizer=address` where internally it would spawn two threads. Each
433    // thread would build a "hello world" module and then one of the threads
434    // would execute a noop exported function. If this was run thousands of
435    // times in a loop in the same process it would eventually crash under asan.
436    //
437    // It's notably not quite so simple as frobbing TLS destructors. There's
438    // clearly something else going on with ASAN state internally which we don't
439    // fully understand at this time. An attempt to make a standalone C++
440    // reproduction, for example, was not successful. In lieu of that the best
441    // we have for now is to disable our custom and larger sigaltstack in asan
442    // builds.
443    //
444    // The exact source was
445    // https://gist.github.com/alexcrichton/6815a5d57a3c5ca94a8d816a9fcc91af for
446    // future reference if necessary.
447    if cfg!(asan) {
448        return;
449    }
450
451    // This thread local is purely used to register a `Stack` to get deallocated
452    // when the thread exists. Otherwise this function is only ever called at
453    // most once per-thread.
454    std::thread_local! {
455        static STACK: RefCell<Option<Stack>> = const { RefCell::new(None) };
456    }
457
458    /// The size of the sigaltstack (not including the guard, which will be
459    /// added). Make this large enough to run our signal handlers.
460    ///
461    /// The main current requirement of the signal handler in terms of stack
462    /// space is that `malloc`/`realloc` are called to create a `Backtrace` of
463    /// wasm frames.
464    ///
465    /// Historically this was 16k. Turns out jemalloc requires more than 16k of
466    /// stack space in debug mode, so this was bumped to 64k.
467    const MIN_STACK_SIZE: usize = 64 * 4096;
468
469    struct Stack {
470        mmap_ptr: *mut libc::c_void,
471        mmap_size: usize,
472    }
473
474    return STACK.with(|s| {
475        *s.borrow_mut() = unsafe { allocate_sigaltstack() };
476    });
477
478    unsafe fn allocate_sigaltstack() -> Option<Stack> {
479        // Check to see if the existing sigaltstack, if it exists, is big
480        // enough. If so we don't need to allocate our own.
481        let mut old_stack = mem::zeroed();
482        let r = libc::sigaltstack(ptr::null(), &mut old_stack);
483        assert_eq!(
484            r,
485            0,
486            "learning about sigaltstack failed: {}",
487            io::Error::last_os_error()
488        );
489        if old_stack.ss_flags & libc::SS_DISABLE == 0 && old_stack.ss_size >= MIN_STACK_SIZE {
490            return None;
491        }
492
493        // ... but failing that we need to allocate our own, so do all that
494        // here.
495        let page_size = crate::runtime::vm::host_page_size();
496        let guard_size = page_size;
497        let alloc_size = guard_size + MIN_STACK_SIZE;
498
499        let ptr = rustix::mm::mmap_anonymous(
500            null_mut(),
501            alloc_size,
502            rustix::mm::ProtFlags::empty(),
503            rustix::mm::MapFlags::PRIVATE,
504        )
505        .expect("failed to allocate memory for sigaltstack");
506
507        // Prepare the stack with readable/writable memory and then register it
508        // with `sigaltstack`.
509        let stack_ptr = (ptr as usize + guard_size) as *mut std::ffi::c_void;
510        rustix::mm::mprotect(
511            stack_ptr,
512            MIN_STACK_SIZE,
513            rustix::mm::MprotectFlags::READ | rustix::mm::MprotectFlags::WRITE,
514        )
515        .expect("mprotect to configure memory for sigaltstack failed");
516        let new_stack = libc::stack_t {
517            ss_sp: stack_ptr,
518            ss_flags: 0,
519            ss_size: MIN_STACK_SIZE,
520        };
521        let r = libc::sigaltstack(&new_stack, ptr::null_mut());
522        assert_eq!(
523            r,
524            0,
525            "registering new sigaltstack failed: {}",
526            io::Error::last_os_error()
527        );
528
529        Some(Stack {
530            mmap_ptr: ptr,
531            mmap_size: alloc_size,
532        })
533    }
534
535    impl Drop for Stack {
536        fn drop(&mut self) {
537            unsafe {
538                // Deallocate the stack memory.
539                let r = rustix::mm::munmap(self.mmap_ptr, self.mmap_size);
540                debug_assert!(r.is_ok(), "munmap failed during thread shutdown");
541            }
542        }
543    }
544}