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::{TrapRegisters, TrapTest, tls};
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 range = &info.vm_store_context.as_ref().async_guard_range;
173 if range.start.addr() <= faulting_addr && faulting_addr < range.end.addr() {
174 abort_stack_overflow();
175 }
176 }
177 return false;
178 }
179 TrapTest::HandledByEmbedder => return true,
180 TrapTest::Trap { jmp_buf } => jmp_buf,
181 };
182 // On macOS this is a bit special, unfortunately. If we were to
183 // `siglongjmp` out of the signal handler that notably does
184 // *not* reset the sigaltstack state of our signal handler. This
185 // seems to trick the kernel into thinking that the sigaltstack
186 // is still in use upon delivery of the next signal, meaning
187 // that the sigaltstack is not ever used again if we immediately
188 // call `wasmtime_longjmp` here.
189 //
190 // Note that if we use `longjmp` instead of `siglongjmp` then
191 // the problem is fixed. The problem with that, however, is that
192 // `setjmp` is much slower than `sigsetjmp` due to the
193 // preservation of the process's signal mask. The reason
194 // `longjmp` appears to work is that it seems to call a function
195 // (according to published macOS sources) called
196 // `_sigunaltstack` which updates the kernel to say the
197 // sigaltstack is no longer in use. We ideally want to call that
198 // here but I don't think there's a stable way for us to call
199 // that.
200 //
201 // Given all that, on macOS only, we do the next best thing. We
202 // return from the signal handler after updating the register
203 // context. This will cause control to return to our shim
204 // function defined here which will perform the
205 // `wasmtime_longjmp` (`siglongjmp`) for us. The reason this
206 // works is that by returning from the signal handler we'll
207 // trigger all the normal machinery for "the signal handler is
208 // done running" which will clear the sigaltstack flag and allow
209 // reusing it for the next signal. Then upon resuming in our custom
210 // code we blow away the stack anyway with a longjmp.
211 if cfg!(target_vendor = "apple") {
212 unsafe extern "C" fn wasmtime_longjmp_shim(jmp_buf: *const u8) {
213 wasmtime_longjmp(jmp_buf)
214 }
215 set_pc(context, wasmtime_longjmp_shim as usize, jmp_buf as usize);
216 return true;
217 }
218 wasmtime_longjmp(jmp_buf)
219 });
220
221 if handled {
222 return;
223 }
224
225 delegate_signal_to_previous_handler(previous, signum, siginfo, context)
226}
227
228pub unsafe fn delegate_signal_to_previous_handler(
229 previous: *const libc::sigaction,
230 signum: libc::c_int,
231 siginfo: *mut libc::siginfo_t,
232 context: *mut libc::c_void,
233) {
234 // This signal is not for any compiled wasm code we expect, so we
235 // need to forward the signal to the next handler. If there is no
236 // next handler (SIG_IGN or SIG_DFL), then it's time to crash. To do
237 // this, we set the signal back to its original disposition and
238 // return. This will cause the faulting op to be re-executed which
239 // will crash in the normal way. If there is a next handler, call
240 // it. It will either crash synchronously, fix up the instruction
241 // so that execution can continue and return, or trigger a crash by
242 // returning the signal to it's original disposition and returning.
243 let previous = *previous;
244 if previous.sa_flags & libc::SA_SIGINFO != 0 {
245 mem::transmute::<usize, extern "C" fn(libc::c_int, *mut libc::siginfo_t, *mut libc::c_void)>(
246 previous.sa_sigaction,
247 )(signum, siginfo, context)
248 } else if previous.sa_sigaction == libc::SIG_DFL || previous.sa_sigaction == libc::SIG_IGN {
249 libc::sigaction(signum, &previous as *const _, ptr::null_mut());
250 } else {
251 mem::transmute::<usize, extern "C" fn(libc::c_int)>(previous.sa_sigaction)(signum)
252 }
253}
254
255pub fn abort_stack_overflow() -> ! {
256 unsafe {
257 let msg = "execution on async fiber has overflowed its stack";
258 libc::write(libc::STDERR_FILENO, msg.as_ptr().cast(), msg.len());
259 libc::abort();
260 }
261}
262
263#[allow(clippy::cast_possible_truncation)] // too fiddly to handle and wouldn't
264// help much anyway
265unsafe fn get_trap_registers(cx: *mut libc::c_void, _signum: libc::c_int) -> TrapRegisters {
266 cfg_if::cfg_if! {
267 if #[cfg(all(any(target_os = "linux", target_os = "android", target_os = "illumos"), target_arch = "x86_64"))] {
268 let cx = &*(cx as *const libc::ucontext_t);
269 TrapRegisters {
270 pc: cx.uc_mcontext.gregs[libc::REG_RIP as usize] as usize,
271 fp: cx.uc_mcontext.gregs[libc::REG_RBP as usize] as usize,
272 }
273 } else if #[cfg(all(target_os = "linux", target_arch = "x86"))] {
274 let cx = &*(cx as *const libc::ucontext_t);
275 TrapRegisters {
276 pc: cx.uc_mcontext.gregs[libc::REG_EIP as usize] as usize,
277 fp: cx.uc_mcontext.gregs[libc::REG_EBP as usize] as usize,
278 }
279 } else if #[cfg(all(any(target_os = "linux", target_os = "android"), target_arch = "aarch64"))] {
280 let cx = &*(cx as *const libc::ucontext_t);
281 TrapRegisters {
282 pc: cx.uc_mcontext.pc as usize,
283 fp: cx.uc_mcontext.regs[29] as usize,
284 }
285 } else if #[cfg(all(target_os = "linux", target_arch = "s390x"))] {
286 // On s390x, SIGILL and SIGFPE are delivered with the PSW address
287 // pointing *after* the faulting instruction, while SIGSEGV and
288 // SIGBUS are delivered with the PSW address pointing *to* the
289 // faulting instruction. To handle this, the code generator registers
290 // any trap that results in one of "late" signals on the last byte
291 // of the instruction, and any trap that results in one of the "early"
292 // signals on the first byte of the instruction (as usual). This
293 // means we simply need to decrement the reported PSW address by
294 // one in the case of a "late" signal here to ensure we always
295 // correctly find the associated trap handler.
296 let trap_offset = match _signum {
297 libc::SIGILL | libc::SIGFPE => 1,
298 _ => 0,
299 };
300 let cx = &*(cx as *const libc::ucontext_t);
301 TrapRegisters {
302 pc: (cx.uc_mcontext.psw.addr - trap_offset) as usize,
303 fp: *(cx.uc_mcontext.gregs[15] as *const usize),
304 }
305 } else if #[cfg(all(target_vendor = "apple", target_arch = "x86_64"))] {
306 let cx = &*(cx as *const libc::ucontext_t);
307 TrapRegisters {
308 pc: (*cx.uc_mcontext).__ss.__rip as usize,
309 fp: (*cx.uc_mcontext).__ss.__rbp as usize,
310 }
311 } else if #[cfg(all(target_vendor = "apple", target_arch = "aarch64"))] {
312 let cx = &*(cx as *const libc::ucontext_t);
313 TrapRegisters {
314 pc: (*cx.uc_mcontext).__ss.__pc as usize,
315 fp: (*cx.uc_mcontext).__ss.__fp as usize,
316 }
317 } else if #[cfg(all(target_os = "freebsd", target_arch = "x86_64"))] {
318 let cx = &*(cx as *const libc::ucontext_t);
319 TrapRegisters {
320 pc: cx.uc_mcontext.mc_rip as usize,
321 fp: cx.uc_mcontext.mc_rbp as usize,
322 }
323 } else if #[cfg(all(target_os = "linux", target_arch = "riscv64"))] {
324 let cx = &*(cx as *const libc::ucontext_t);
325 TrapRegisters {
326 pc: cx.uc_mcontext.__gregs[libc::REG_PC] as usize,
327 fp: cx.uc_mcontext.__gregs[libc::REG_S0] as usize,
328 }
329 } else if #[cfg(all(target_os = "freebsd", target_arch = "aarch64"))] {
330 let cx = &*(cx as *const libc::mcontext_t);
331 TrapRegisters {
332 pc: cx.mc_gpregs.gp_elr as usize,
333 fp: cx.mc_gpregs.gp_x[29] as usize,
334 }
335 } else if #[cfg(all(target_os = "openbsd", target_arch = "x86_64"))] {
336 let cx = &*(cx as *const libc::ucontext_t);
337 TrapRegisters {
338 pc: cx.sc_rip as usize,
339 fp: cx.sc_rbp as usize,
340 }
341 } else if #[cfg(all(target_os = "linux", target_arch = "arm"))] {
342 let cx = &*(cx as *const libc::ucontext_t);
343 TrapRegisters {
344 pc: cx.uc_mcontext.arm_pc as usize,
345 fp: cx.uc_mcontext.arm_fp as usize,
346 }
347 } else {
348 compile_error!("unsupported platform");
349 panic!();
350 }
351 }
352}
353
354// This is only used on macOS targets for calling an unwinding shim
355// function to ensure that we return from the signal handler.
356//
357// See more comments above where this is called for what it's doing.
358unsafe fn set_pc(cx: *mut libc::c_void, pc: usize, arg1: usize) {
359 cfg_if::cfg_if! {
360 if #[cfg(not(target_vendor = "apple"))] {
361 let _ = (cx, pc, arg1);
362 unreachable!(); // not used on these platforms
363 } else if #[cfg(target_arch = "x86_64")] {
364 let cx = &mut *(cx as *mut libc::ucontext_t);
365 (*cx.uc_mcontext).__ss.__rip = pc as u64;
366 (*cx.uc_mcontext).__ss.__rdi = arg1 as u64;
367 // We're simulating a "pseudo-call" so we need to ensure
368 // stack alignment is properly respected, notably that on a
369 // `call` instruction the stack is 8/16-byte aligned, then
370 // the function adjusts itself to be 16-byte aligned.
371 //
372 // Most of the time the stack pointer is 16-byte aligned at
373 // the time of the trap but for more robust-ness with JIT
374 // code where it may ud2 in a prologue check before the
375 // stack is aligned we double-check here.
376 if (*cx.uc_mcontext).__ss.__rsp % 16 == 0 {
377 (*cx.uc_mcontext).__ss.__rsp -= 8;
378 }
379 } else if #[cfg(target_arch = "aarch64")] {
380 let cx = &mut *(cx as *mut libc::ucontext_t);
381 (*cx.uc_mcontext).__ss.__pc = pc as u64;
382 (*cx.uc_mcontext).__ss.__x[0] = arg1 as u64;
383 } else {
384 compile_error!("unsupported apple target architecture");
385 }
386 }
387}
388
389/// A function for registering a custom alternate signal stack (sigaltstack).
390///
391/// Rust's libstd installs an alternate stack with size `SIGSTKSZ`, which is not
392/// always large enough for our signal handling code. Override it by creating
393/// and registering our own alternate stack that is large enough and has a guard
394/// page.
395///
396/// Note that one might reasonably ask why do this at all? Why not remove
397/// `SA_ONSTACK` from our signal handlers entirely? The basic reason for that is
398/// because we want to print a message on stack overflow. The Rust standard
399/// library will print this message by default and by us overriding the
400/// `SIGSEGV` handler above we're now sharing responsibility for that as well.
401/// We must have `SA_ONSTACK` to even attempt to being able to printing this
402/// message, and so we leave it turned on. Wasmtime will determine a stack
403/// overflow fault isn't caused by wasm and then forward to libstd's signal
404/// handler which will actually print-and-abort.
405///
406/// Another reasonable question might be why we need to increase the size of the
407/// sigaltstack at all? This is something which we may want to reconsider in the
408/// future. For now it helps keep debug builds working which consume more stack
409/// when handling normal wasm out-of-bounds and faults. Perhaps in the future we
410/// could optimize this more or maybe even do something clever like lazily
411/// allocate the sigaltstack on the fault itself. (e.g. trampoline from a tiny
412/// stack to the "big stack" during a wasm fault or something like that)
413#[cold]
414pub fn lazy_per_thread_init() {
415 // This is a load-bearing requirement to keep address-sanitizer working and
416 // prevent crashes during fuzzing. The general idea here is that we skip the
417 // sigaltstack setup below entirely on asan builds, aka fuzzing. The exact
418 // reason for this is not entirely known, but the closest guess we have at
419 // this time is something like:
420 //
421 // * ASAN builds intercept mmap/munmap to keep track of what's going on.
422 // * The sigaltstack below registers a TLS destructor for when the current
423 // thread exits to deallocate the stack.
424 // * ASAN looks to also have TLS destructors for its own internal state.
425 // * The current assumption is that the order of these TLS destructors can
426 // cause corruption in ASAN state where if we run after asan's destructor
427 // it may intercept munmap and then asan doesn't know it's been
428 // de-initialized yet.
429 //
430 // The reproduction of this involved a standalone project built with
431 // `-Zsanitizer=address` where internally it would spawn two threads. Each
432 // thread would build a "hello world" module and then one of the threads
433 // would execute a noop exported function. If this was run thousands of
434 // times in a loop in the same process it would eventually crash under asan.
435 //
436 // It's notably not quite so simple as frobbing TLS destructors. There's
437 // clearly something else going on with ASAN state internally which we don't
438 // fully understand at this time. An attempt to make a standalone C++
439 // reproduction, for example, was not successful. In lieu of that the best
440 // we have for now is to disable our custom and larger sigaltstack in asan
441 // builds.
442 //
443 // The exact source was
444 // https://gist.github.com/alexcrichton/6815a5d57a3c5ca94a8d816a9fcc91af for
445 // future reference if necessary.
446 if cfg!(asan) {
447 return;
448 }
449
450 // This thread local is purely used to register a `Stack` to get deallocated
451 // when the thread exists. Otherwise this function is only ever called at
452 // most once per-thread.
453 std::thread_local! {
454 static STACK: RefCell<Option<Stack>> = const { RefCell::new(None) };
455 }
456
457 /// The size of the sigaltstack (not including the guard, which will be
458 /// added). Make this large enough to run our signal handlers.
459 ///
460 /// The main current requirement of the signal handler in terms of stack
461 /// space is that `malloc`/`realloc` are called to create a `Backtrace` of
462 /// wasm frames.
463 ///
464 /// Historically this was 16k. Turns out jemalloc requires more than 16k of
465 /// stack space in debug mode, so this was bumped to 64k.
466 const MIN_STACK_SIZE: usize = 64 * 4096;
467
468 struct Stack {
469 mmap_ptr: *mut libc::c_void,
470 mmap_size: usize,
471 }
472
473 return STACK.with(|s| {
474 *s.borrow_mut() = unsafe { allocate_sigaltstack() };
475 });
476
477 unsafe fn allocate_sigaltstack() -> Option<Stack> {
478 // Check to see if the existing sigaltstack, if it exists, is big
479 // enough. If so we don't need to allocate our own.
480 let mut old_stack = mem::zeroed();
481 let r = libc::sigaltstack(ptr::null(), &mut old_stack);
482 assert_eq!(
483 r,
484 0,
485 "learning about sigaltstack failed: {}",
486 io::Error::last_os_error()
487 );
488 if old_stack.ss_flags & libc::SS_DISABLE == 0 && old_stack.ss_size >= MIN_STACK_SIZE {
489 return None;
490 }
491
492 // ... but failing that we need to allocate our own, so do all that
493 // here.
494 let page_size = crate::runtime::vm::host_page_size();
495 let guard_size = page_size;
496 let alloc_size = guard_size + MIN_STACK_SIZE;
497
498 let ptr = rustix::mm::mmap_anonymous(
499 null_mut(),
500 alloc_size,
501 rustix::mm::ProtFlags::empty(),
502 rustix::mm::MapFlags::PRIVATE,
503 )
504 .expect("failed to allocate memory for sigaltstack");
505
506 // Prepare the stack with readable/writable memory and then register it
507 // with `sigaltstack`.
508 let stack_ptr = (ptr as usize + guard_size) as *mut std::ffi::c_void;
509 rustix::mm::mprotect(
510 stack_ptr,
511 MIN_STACK_SIZE,
512 rustix::mm::MprotectFlags::READ | rustix::mm::MprotectFlags::WRITE,
513 )
514 .expect("mprotect to configure memory for sigaltstack failed");
515 let new_stack = libc::stack_t {
516 ss_sp: stack_ptr,
517 ss_flags: 0,
518 ss_size: MIN_STACK_SIZE,
519 };
520 let r = libc::sigaltstack(&new_stack, ptr::null_mut());
521 assert_eq!(
522 r,
523 0,
524 "registering new sigaltstack failed: {}",
525 io::Error::last_os_error()
526 );
527
528 Some(Stack {
529 mmap_ptr: ptr,
530 mmap_size: alloc_size,
531 })
532 }
533
534 impl Drop for Stack {
535 fn drop(&mut self) {
536 unsafe {
537 // Deallocate the stack memory.
538 let r = rustix::mm::munmap(self.mmap_ptr, self.mmap_size);
539 debug_assert!(r.is_ok(), "munmap failed during thread shutdown");
540 }
541 }
542 }
543}