wasmtime_runtime/sys/unix/signals.rs
1//! Trap handling on Unix based on POSIX signals.
2
3use crate::traphandlers::{tls, TrapTest};
4use crate::VMContext;
5use std::cell::RefCell;
6use std::io;
7use std::mem::{self, MaybeUninit};
8use std::ptr::{self, null_mut};
9
10#[link(name = "wasmtime-helpers")]
11extern "C" {
12 #[wasmtime_versioned_export_macros::versioned_link]
13 #[allow(improper_ctypes)]
14 pub fn wasmtime_setjmp(
15 jmp_buf: *mut *const u8,
16 callback: extern "C" fn(*mut u8, *mut VMContext),
17 payload: *mut u8,
18 callee: *mut VMContext,
19 ) -> i32;
20
21 #[wasmtime_versioned_export_macros::versioned_link]
22 pub fn wasmtime_longjmp(jmp_buf: *const u8) -> !;
23}
24
25/// Function which may handle custom signals while processing traps.
26pub type SignalHandler<'a> =
27 dyn Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool + Send + Sync + 'a;
28
29static mut PREV_SIGSEGV: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
30static mut PREV_SIGBUS: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
31static mut PREV_SIGILL: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
32static mut PREV_SIGFPE: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
33
34pub unsafe fn platform_init(macos_use_mach_ports: bool) {
35 // Either mach ports shouldn't be in use or we shouldn't be on macOS,
36 // otherwise the `machports.rs` module should be used instead.
37 assert!(!macos_use_mach_ports || !cfg!(target_os = "macos"));
38
39 let register = |slot: *mut libc::sigaction, signal: i32| {
40 let mut handler: libc::sigaction = mem::zeroed();
41 // The flags here are relatively careful, and they are...
42 //
43 // SA_SIGINFO gives us access to information like the program
44 // counter from where the fault happened.
45 //
46 // SA_ONSTACK allows us to handle signals on an alternate stack,
47 // so that the handler can run in response to running out of
48 // stack space on the main stack. Rust installs an alternate
49 // stack with sigaltstack, so we rely on that.
50 //
51 // SA_NODEFER allows us to reenter the signal handler if we
52 // crash while handling the signal, and fall through to the
53 // Breakpad handler by testing handlingSegFault.
54 handler.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER | libc::SA_ONSTACK;
55 handler.sa_sigaction = trap_handler as usize;
56 libc::sigemptyset(&mut handler.sa_mask);
57 if libc::sigaction(signal, &handler, slot) != 0 {
58 panic!(
59 "unable to install signal handler: {}",
60 io::Error::last_os_error(),
61 );
62 }
63 };
64
65 // Allow handling OOB with signals on all architectures
66 register(PREV_SIGSEGV.as_mut_ptr(), libc::SIGSEGV);
67
68 // Handle `unreachable` instructions which execute `ud2` right now
69 register(PREV_SIGILL.as_mut_ptr(), libc::SIGILL);
70
71 // x86 and s390x use SIGFPE to report division by zero
72 if cfg!(target_arch = "x86_64") || cfg!(target_arch = "s390x") {
73 register(PREV_SIGFPE.as_mut_ptr(), libc::SIGFPE);
74 }
75
76 // Sometimes we need to handle SIGBUS too:
77 // - On Darwin, guard page accesses are raised as SIGBUS.
78 if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
79 register(PREV_SIGBUS.as_mut_ptr(), libc::SIGBUS);
80 }
81
82 // TODO(#1980): x86-32, if we support it, will also need a SIGFPE handler.
83 // TODO(#1173): ARM32, if we support it, will also need a SIGBUS handler.
84}
85
86unsafe extern "C" fn trap_handler(
87 signum: libc::c_int,
88 siginfo: *mut libc::siginfo_t,
89 context: *mut libc::c_void,
90) {
91 let previous = match signum {
92 libc::SIGSEGV => PREV_SIGSEGV.as_ptr(),
93 libc::SIGBUS => PREV_SIGBUS.as_ptr(),
94 libc::SIGFPE => PREV_SIGFPE.as_ptr(),
95 libc::SIGILL => PREV_SIGILL.as_ptr(),
96 _ => panic!("unknown signal: {}", signum),
97 };
98 let handled = tls::with(|info| {
99 // If no wasm code is executing, we don't handle this as a wasm
100 // trap.
101 let info = match info {
102 Some(info) => info,
103 None => return false,
104 };
105
106 // If we hit an exception while handling a previous trap, that's
107 // quite bad, so bail out and let the system handle this
108 // recursive segfault.
109 //
110 // Otherwise flag ourselves as handling a trap, do the trap
111 // handling, and reset our trap handling flag. Then we figure
112 // out what to do based on the result of the trap handling.
113 let (pc, fp) = get_pc_and_fp(context, signum);
114 let test = info.test_if_trap(pc, |handler| handler(signum, siginfo, context));
115
116 // Figure out what to do based on the result of this handling of
117 // the trap. Note that our sentinel value of 1 means that the
118 // exception was handled by a custom exception handler, so we
119 // keep executing.
120 let (jmp_buf, trap) = match test {
121 TrapTest::NotWasm => return false,
122 TrapTest::HandledByEmbedder => return true,
123 TrapTest::Trap { jmp_buf, trap } => (jmp_buf, trap),
124 };
125 let faulting_addr = match signum {
126 libc::SIGSEGV | libc::SIGBUS => Some((*siginfo).si_addr() as usize),
127 _ => None,
128 };
129 info.set_jit_trap(pc, fp, faulting_addr, trap);
130 // On macOS this is a bit special, unfortunately. If we were to
131 // `siglongjmp` out of the signal handler that notably does
132 // *not* reset the sigaltstack state of our signal handler. This
133 // seems to trick the kernel into thinking that the sigaltstack
134 // is still in use upon delivery of the next signal, meaning
135 // that the sigaltstack is not ever used again if we immediately
136 // call `wasmtime_longjmp` here.
137 //
138 // Note that if we use `longjmp` instead of `siglongjmp` then
139 // the problem is fixed. The problem with that, however, is that
140 // `setjmp` is much slower than `sigsetjmp` due to the
141 // preservation of the proceses signal mask. The reason
142 // `longjmp` appears to work is that it seems to call a function
143 // (according to published macOS sources) called
144 // `_sigunaltstack` which updates the kernel to say the
145 // sigaltstack is no longer in use. We ideally want to call that
146 // here but I don't think there's a stable way for us to call
147 // that.
148 //
149 // Given all that, on macOS only, we do the next best thing. We
150 // return from the signal handler after updating the register
151 // context. This will cause control to return to our shim
152 // function defined here which will perform the
153 // `wasmtime_longjmp` (`siglongjmp`) for us. The reason this
154 // works is that by returning from the signal handler we'll
155 // trigger all the normal machinery for "the signal handler is
156 // done running" which will clear the sigaltstack flag and allow
157 // reusing it for the next signal. Then upon resuming in our custom
158 // code we blow away the stack anyway with a longjmp.
159 if cfg!(target_os = "macos") {
160 unsafe extern "C" fn wasmtime_longjmp_shim(jmp_buf: *const u8) {
161 wasmtime_longjmp(jmp_buf)
162 }
163 set_pc(context, wasmtime_longjmp_shim as usize, jmp_buf as usize);
164 return true;
165 }
166 wasmtime_longjmp(jmp_buf)
167 });
168
169 if handled {
170 return;
171 }
172
173 // This signal is not for any compiled wasm code we expect, so we
174 // need to forward the signal to the next handler. If there is no
175 // next handler (SIG_IGN or SIG_DFL), then it's time to crash. To do
176 // this, we set the signal back to its original disposition and
177 // return. This will cause the faulting op to be re-executed which
178 // will crash in the normal way. If there is a next handler, call
179 // it. It will either crash synchronously, fix up the instruction
180 // so that execution can continue and return, or trigger a crash by
181 // returning the signal to it's original disposition and returning.
182 let previous = *previous;
183 if previous.sa_flags & libc::SA_SIGINFO != 0 {
184 mem::transmute::<usize, extern "C" fn(libc::c_int, *mut libc::siginfo_t, *mut libc::c_void)>(
185 previous.sa_sigaction,
186 )(signum, siginfo, context)
187 } else if previous.sa_sigaction == libc::SIG_DFL || previous.sa_sigaction == libc::SIG_IGN {
188 libc::sigaction(signum, &previous as *const _, ptr::null_mut());
189 } else {
190 mem::transmute::<usize, extern "C" fn(libc::c_int)>(previous.sa_sigaction)(signum)
191 }
192}
193
194unsafe fn get_pc_and_fp(cx: *mut libc::c_void, _signum: libc::c_int) -> (*const u8, usize) {
195 cfg_if::cfg_if! {
196 if #[cfg(all(any(target_os = "linux", target_os = "android"), target_arch = "x86_64"))] {
197 let cx = &*(cx as *const libc::ucontext_t);
198 (
199 cx.uc_mcontext.gregs[libc::REG_RIP as usize] as *const u8,
200 cx.uc_mcontext.gregs[libc::REG_RBP as usize] as usize
201 )
202 } else if #[cfg(all(any(target_os = "linux", target_os = "android"), target_arch = "aarch64"))] {
203 let cx = &*(cx as *const libc::ucontext_t);
204 (
205 cx.uc_mcontext.pc as *const u8,
206 cx.uc_mcontext.regs[29] as usize,
207 )
208 } else if #[cfg(all(target_os = "linux", target_arch = "s390x"))] {
209 // On s390x, SIGILL and SIGFPE are delivered with the PSW address
210 // pointing *after* the faulting instruction, while SIGSEGV and
211 // SIGBUS are delivered with the PSW address pointing *to* the
212 // faulting instruction. To handle this, the code generator registers
213 // any trap that results in one of "late" signals on the last byte
214 // of the instruction, and any trap that results in one of the "early"
215 // signals on the first byte of the instruction (as usual). This
216 // means we simply need to decrement the reported PSW address by
217 // one in the case of a "late" signal here to ensure we always
218 // correctly find the associated trap handler.
219 let trap_offset = match _signum {
220 libc::SIGILL | libc::SIGFPE => 1,
221 _ => 0,
222 };
223 let cx = &*(cx as *const libc::ucontext_t);
224 (
225 (cx.uc_mcontext.psw.addr - trap_offset) as *const u8,
226 *(cx.uc_mcontext.gregs[15] as *const usize),
227 )
228 } else if #[cfg(all(target_os = "macos", target_arch = "x86_64"))] {
229 let cx = &*(cx as *const libc::ucontext_t);
230 (
231 (*cx.uc_mcontext).__ss.__rip as *const u8,
232 (*cx.uc_mcontext).__ss.__rbp as usize,
233 )
234 } else if #[cfg(all(target_os = "macos", target_arch = "aarch64"))] {
235 let cx = &*(cx as *const libc::ucontext_t);
236 (
237 (*cx.uc_mcontext).__ss.__pc as *const u8,
238 (*cx.uc_mcontext).__ss.__fp as usize,
239 )
240 } else if #[cfg(all(target_os = "freebsd", target_arch = "x86_64"))] {
241 let cx = &*(cx as *const libc::ucontext_t);
242 (
243 cx.uc_mcontext.mc_rip as *const u8,
244 cx.uc_mcontext.mc_rbp as usize,
245 )
246 } else if #[cfg(all(target_os = "linux", target_arch = "riscv64"))] {
247 let cx = &*(cx as *const libc::ucontext_t);
248 (
249 cx.uc_mcontext.__gregs[libc::REG_PC] as *const u8,
250 cx.uc_mcontext.__gregs[libc::REG_S0] as usize,
251 )
252 } else if #[cfg(all(target_os = "freebsd", target_arch = "aarch64"))] {
253 let cx = &*(cx as *const libc::mcontext_t);
254 (
255 cx.mc_gpregs.gp_elr as *const u8,
256 cx.mc_gpregs.gp_x[29] as usize,
257 )
258 }
259 else {
260 compile_error!("unsupported platform");
261 }
262 }
263}
264
265// This is only used on macOS targets for calling an unwinding shim
266// function to ensure that we return from the signal handler.
267//
268// See more comments above where this is called for what it's doing.
269unsafe fn set_pc(cx: *mut libc::c_void, pc: usize, arg1: usize) {
270 cfg_if::cfg_if! {
271 if #[cfg(not(target_os = "macos"))] {
272 let _ = (cx, pc, arg1);
273 unreachable!(); // not used on these platforms
274 } else if #[cfg(target_arch = "x86_64")] {
275 let cx = &mut *(cx as *mut libc::ucontext_t);
276 (*cx.uc_mcontext).__ss.__rip = pc as u64;
277 (*cx.uc_mcontext).__ss.__rdi = arg1 as u64;
278 // We're simulating a "pseudo-call" so we need to ensure
279 // stack alignment is properly respected, notably that on a
280 // `call` instruction the stack is 8/16-byte aligned, then
281 // the function adjusts itself to be 16-byte aligned.
282 //
283 // Most of the time the stack pointer is 16-byte aligned at
284 // the time of the trap but for more robust-ness with JIT
285 // code where it may ud2 in a prologue check before the
286 // stack is aligned we double-check here.
287 if (*cx.uc_mcontext).__ss.__rsp % 16 == 0 {
288 (*cx.uc_mcontext).__ss.__rsp -= 8;
289 }
290 } else if #[cfg(target_arch = "aarch64")] {
291 let cx = &mut *(cx as *mut libc::ucontext_t);
292 (*cx.uc_mcontext).__ss.__pc = pc as u64;
293 (*cx.uc_mcontext).__ss.__x[0] = arg1 as u64;
294 } else {
295 compile_error!("unsupported macos target architecture");
296 }
297 }
298}
299
300/// A function for registering a custom alternate signal stack (sigaltstack).
301///
302/// Rust's libstd installs an alternate stack with size `SIGSTKSZ`, which is not
303/// always large enough for our signal handling code. Override it by creating
304/// and registering our own alternate stack that is large enough and has a guard
305/// page.
306#[cold]
307pub fn lazy_per_thread_init() {
308 // This thread local is purely used to register a `Stack` to get deallocated
309 // when the thread exists. Otherwise this function is only ever called at
310 // most once per-thread.
311 thread_local! {
312 static STACK: RefCell<Option<Stack>> = const { RefCell::new(None) };
313 }
314
315 /// The size of the sigaltstack (not including the guard, which will be
316 /// added). Make this large enough to run our signal handlers.
317 ///
318 /// The main current requirement of the signal handler in terms of stack
319 /// space is that `malloc`/`realloc` are called to create a `Backtrace` of
320 /// wasm frames.
321 ///
322 /// Historically this was 16k. Turns out jemalloc requires more than 16k of
323 /// stack space in debug mode, so this was bumped to 64k.
324 const MIN_STACK_SIZE: usize = 64 * 4096;
325
326 struct Stack {
327 mmap_ptr: *mut libc::c_void,
328 mmap_size: usize,
329 }
330
331 return STACK.with(|s| {
332 *s.borrow_mut() = unsafe { allocate_sigaltstack() };
333 });
334
335 unsafe fn allocate_sigaltstack() -> Option<Stack> {
336 // Check to see if the existing sigaltstack, if it exists, is big
337 // enough. If so we don't need to allocate our own.
338 let mut old_stack = mem::zeroed();
339 let r = libc::sigaltstack(ptr::null(), &mut old_stack);
340 assert_eq!(
341 r,
342 0,
343 "learning about sigaltstack failed: {}",
344 io::Error::last_os_error()
345 );
346 if old_stack.ss_flags & libc::SS_DISABLE == 0 && old_stack.ss_size >= MIN_STACK_SIZE {
347 return None;
348 }
349
350 // ... but failing that we need to allocate our own, so do all that
351 // here.
352 let page_size = crate::page_size();
353 let guard_size = page_size;
354 let alloc_size = guard_size + MIN_STACK_SIZE;
355
356 let ptr = rustix::mm::mmap_anonymous(
357 null_mut(),
358 alloc_size,
359 rustix::mm::ProtFlags::empty(),
360 rustix::mm::MapFlags::PRIVATE,
361 )
362 .expect("failed to allocate memory for sigaltstack");
363
364 // Prepare the stack with readable/writable memory and then register it
365 // with `sigaltstack`.
366 let stack_ptr = (ptr as usize + guard_size) as *mut std::ffi::c_void;
367 rustix::mm::mprotect(
368 stack_ptr,
369 MIN_STACK_SIZE,
370 rustix::mm::MprotectFlags::READ | rustix::mm::MprotectFlags::WRITE,
371 )
372 .expect("mprotect to configure memory for sigaltstack failed");
373 let new_stack = libc::stack_t {
374 ss_sp: stack_ptr,
375 ss_flags: 0,
376 ss_size: MIN_STACK_SIZE,
377 };
378 let r = libc::sigaltstack(&new_stack, ptr::null_mut());
379 assert_eq!(
380 r,
381 0,
382 "registering new sigaltstack failed: {}",
383 io::Error::last_os_error()
384 );
385
386 Some(Stack {
387 mmap_ptr: ptr,
388 mmap_size: alloc_size,
389 })
390 }
391
392 impl Drop for Stack {
393 fn drop(&mut self) {
394 unsafe {
395 // Deallocate the stack memory.
396 let r = rustix::mm::munmap(self.mmap_ptr, self.mmap_size);
397 debug_assert!(r.is_ok(), "munmap failed during thread shutdown");
398 }
399 }
400 }
401}