Skip to content

Commit 707f83d

Browse files
authored
Fix preservation of the sigaltstack on macOS (#2676)
* Fix preservation of the sigaltstack on macOS This commit fixes an issue discovered in the wasmtime-go bindings when the Go runtime was crashing on macOS only when running wasm code that trapped. It turns out that our switch to `siglongjmp` from `longjmp` actually broke macOS! This breakage happens because all subsequent signals after the first signal are all delivered on the main stack, not the sigaltstack, even if the sigaltstack is configured. This causes the Go runtime to crash since it expects to run on the sigaltstack. The fix in this commit is to actually return from the signal handler to trigger the kernel's updating of the sigaltstack no longer being in use. Before we return, however, we configure the register context to return to to call some custom code which immediately does the unwind we would otherwise have done. This works around the issue on macOS hopefully without adding too many portability problems. Ideally this will all go away as well with #2632 as well. * Fix compile warning
1 parent ee1ddfb commit 707f83d

1 file changed

Lines changed: 71 additions & 0 deletions

File tree

crates/runtime/src/traphandlers.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,42 @@ cfg_if::cfg_if! {
118118
return false;
119119
} else if jmp_buf as usize == 1 {
120120
return true;
121+
122+
// on macOS this is a bit special, unfortunately. If we were to
123+
// `siglongjmp` out of the signal handler that notably does
124+
// *not* reset the sigaltstack state of our signal handler. This
125+
// seems to trick the kernel into thinking that the sigaltstack
126+
// is still in use upon delivery of the next signal, meaning
127+
// that the sigaltstack is not ever used again if we immediately
128+
// call `Unwind` here.
129+
//
130+
// Note that if we use `longjmp` instead of `siglongjmp` then
131+
// the problem is fixed. The problem with that, however, is that
132+
// `setjmp` is much slower than `sigsetjmp` due to the
133+
// preservation of the proceses signal mask. The reason
134+
// `longjmp` appears to work is that it seems to call a function
135+
// (according to published macOS sources) called
136+
// `_sigunaltstack` which updates the kernel to say the
137+
// sigaltstack is no longer in use. We ideally want to call that
138+
// here but I don't think there's a stable way for us to call
139+
// that.
140+
//
141+
// Given all that, on macOS only, we do the next best thing. We
142+
// return from the signal handler after updating the register
143+
// context. This will cause control to return to our
144+
// `unwind_shim` function defined here which will perform the
145+
// `Unwind` (`siglongjmp`) for us. The reason this works is that
146+
// by returning from the signal handler we'll trigger all the
147+
// normal machinery for "the signal handler is done running"
148+
// which will clear the sigaltstack flag and allow reusing it
149+
// for the next signal. Then upon resuming in our custom code we
150+
// blow away the stack anyway with a longjmp.
151+
} else if cfg!(target_os = "macos") {
152+
unsafe extern "C" fn unwind_shim(jmp_buf: *const u8) {
153+
Unwind(jmp_buf)
154+
}
155+
set_pc(context, unwind_shim as usize, jmp_buf as usize);
156+
return true;
121157
} else {
122158
Unwind(jmp_buf)
123159
}
@@ -181,6 +217,41 @@ cfg_if::cfg_if! {
181217
}
182218
}
183219
}
220+
221+
// This is only used on macOS targets for calling an unwinding shim
222+
// function to ensure that we return from the signal handler.
223+
//
224+
// See more comments above where this is called for what it's doing.
225+
unsafe fn set_pc(cx: *mut libc::c_void, pc: usize, arg1: usize) {
226+
cfg_if::cfg_if! {
227+
if #[cfg(not(target_os = "macos"))] {
228+
drop((cx, pc, arg1));
229+
unreachable!(); // not used on these platforms
230+
} else if #[cfg(target_arch = "x86_64")] {
231+
let cx = &mut *(cx as *mut libc::ucontext_t);
232+
(*cx.uc_mcontext).__ss.__rip = pc as u64;
233+
(*cx.uc_mcontext).__ss.__rdi = arg1 as u64;
234+
// We're simulating a "pseudo-call" so we need to ensure
235+
// stack alignment is properly respected, notably that on a
236+
// `call` instruction the stack is 8/16-byte aligned, then
237+
// the function adjusts itself to be 16-byte aligned.
238+
//
239+
// Most of the time the stack pointer is 16-byte aligned at
240+
// the time of the trap but for more robust-ness with JIT
241+
// code where it may ud2 in a prologue check before the
242+
// stack is aligned we double-check here.
243+
if (*cx.uc_mcontext).__ss.__rsp % 16 == 0 {
244+
(*cx.uc_mcontext).__ss.__rsp -= 8;
245+
}
246+
} else if #[cfg(target_arch = "aarch64")] {
247+
let cx = &mut *(cx as *mut libc::ucontext_t);
248+
(*cx.uc_mcontext).__ss.__pc = pc as u64;
249+
(*cx.uc_mcontext).__ss.__x[0] = arg1 as u64;
250+
} else {
251+
compile_error!("unsupported macos target architecture");
252+
}
253+
}
254+
}
184255
} else if #[cfg(target_os = "windows")] {
185256
use winapi::um::errhandlingapi::*;
186257
use winapi::um::winnt::*;

0 commit comments

Comments
 (0)