Skip to content

Commit aba7144

Browse files
authored
Fix compatibility with the Go runtime on Windows for exceptions (#11892) (#11900)
* Fix compatibility with the Go runtime on Windows for exceptions This commit fixes some fallout of #11592 where that PR resulted in Wasmtime no longer running within the context of the Go runtime (e.g. `wasmtime-go`). The reasons for this are quite Windows-specific and I've attempted to document the situation in `vectored_exceptions.go`. The basic TL;DR; is that by returning from a vectored exception handler (which #11592 introduced) we're now subjecting ourselves to "continue handlers" as well, and Go's continue handlers will abort the process for non-Go exceptions. Some logic is added here to try to bypass Go's continue handlers and get back to wasm. * Fix typos * Review comments
1 parent f2140f6 commit aba7144

File tree

1 file changed

+154
-6
lines changed

1 file changed

+154
-6
lines changed

crates/wasmtime/src/runtime/vm/sys/windows/vectored_exceptions.rs

Lines changed: 154 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,92 @@
1+
//! Implementation of handling hardware traps generated by wasm (e.g. segfaults)
2+
//! on Windows.
3+
//!
4+
//! This module is implemented with Windows Vectored Exception Handling which
5+
//! is, I think, implemented on top of Structured Exception Handling (SEH). This
6+
//! is distinct from Unix signals where instead of a single global handler
7+
//! there's a list of vectored exception handlers which is managed by the
8+
//! Windows runtime. This list is sort of like a `VecDeque` where you can push
9+
//! on either end, and then you're able to remove any pushed entry later on.
10+
//!
11+
//! Windows's behavior here seems to first execute the ordered list of vectored
12+
//! exception handlers until one returns `EXCEPTION_CONTINUE_EXECUTION`. If this
13+
//! list is exhausted then it seems to go to default SEH routines which abort
14+
//! the process.
15+
//!
16+
//! Another interesting part, however, is that once an exception handler returns
17+
//! `EXCEPTION_CONTINUE_EXECUTION` Windows will then consult a similar deque of
18+
//! "continue handlers". These continue handlers have the same signature as the
19+
//! exception handlers and are managed with similar functions
20+
//! (`AddVectoredContinueHandler` instead of `AddVectoredExceptionHandler`). The
21+
//! difference here is that the first continue handler to return
22+
//! `EXCEPTION_CONTINUE_EXECUTION` will short-circuit the rest of the list. If
23+
//! none of them return `EXCEPTION_CONTINUE_EXECUTION` then the programs
24+
//! still resumes as normal.
25+
//!
26+
//! # Wasmtime's implementation
27+
//!
28+
//! Wasmtime installs both an exception handler and a continue handler. The
29+
//! purpose of the exception handler is to return `EXCEPTION_CONTINUE_EXECUTION`
30+
//! for any wasm exceptions that we want to catch (e.g. divide-by-zero, out of
31+
//! bounds memory accesses in wasm, `unreachable` via illegal instruction, etc).
32+
//! Note that this exception handler is installed at the front of the list to
33+
//! try to run it as soon as possible as, if we catch something, we want to
34+
//! bypass all other handlers.
35+
//!
36+
//! Wasmtime then also installs a continue handler, also at the front of the
37+
//! list, where the sole purpose of the continue handler is to also return
38+
//! `EXCEPTION_CONTINUE_EXECUTION` and bypass the rest of the continue handler
39+
//! list to get back to wasm ASAP. The reason for this is explained in the next
40+
//! section.
41+
//!
42+
//! To implement the continue handler in Wasmtime a thread-local variable
43+
//! `LAST_EXCEPTION_PC` is used here which is set during the exception handler
44+
//! and then tested during the continue handler. If it matches the current PC
45+
//! then it's assume that Wasmtime is the one that processed the exception and
46+
//! the `EXCEPTION_CONTINUE_EXECUTION` is returned.
47+
//!
48+
//! # Why both an exception and continue handler?
49+
//!
50+
//! All of Wasmtime's tests in this repository will pass if the continue handler
51+
//! is removed, so why have it? The primary reason at this time is integration
52+
//! with the Go runtime as discovered in the `wasmtime-go` embedding.
53+
//!
54+
//! Go's behavior for exceptions is:
55+
//!
56+
//! * An exception handler is installed at the front of the list of handlers
57+
//! which looks for Go-originating exceptions. If one is found it returns
58+
//! `EXCEPTION_CONTINUE_EXECUTION`, otherwise it forwards along with
59+
//! `EXCEPTION_CONTINUE_SEARCH`. Wasmtime exceptions will properly go through
60+
//! this handler and then hit Wasmtime's handler, so no issues yet.
61+
//!
62+
//! * Go then additionally installs *two* continue handlers. One at the front of
63+
//! the list and one at the end. The continue handler at the front of the list
64+
//! looks for Go-related exceptions dealing with things like
65+
//! async/preemption/etc to resume execution back into Go. This means that the
66+
//! handler will return `EXCEPTION_CONTINUE_EXECUTION` sometimes for
67+
//! Go-specific reasons, and otherwise the handler returns
68+
//! `EXCEPTION_CONTINUE_SEARCH`. As before this isn't a problem for Wasmtime
69+
//! as nothing happens for non-Go-related exceptions.
70+
//!
71+
//! * The problem with Go is the second, final, continue handler. This will, by
72+
//! default, abort the process for all exceptions whether or not they're Go
73+
//! related. This seems to have some logic for whether or not Go was built as
74+
//! a library or dylib but that seem to apply for Go-built executables (e.g.
75+
//! `go test` in the wasmtime-go repository). This second handler is the
76+
//! problematic one because in Wasmtime we "catch" the exception in the
77+
//! exception handler function but then the process still aborts as all
78+
//! continue handlers are run, including Go's abort-the-process handler.
79+
//!
80+
//! Thus the reason Wasmtime has a continue handler in addition to an exception
81+
//! handler. By installing a high-priority continue handler that pairs with the
82+
//! high-priority exception handler we can ensure that, for example, Go's
83+
//! fallback continue handler is never executed.
84+
//!
85+
//! This is all... a bit... roundabout. Sorry.
86+
187
use crate::prelude::*;
288
use crate::runtime::vm::traphandlers::{TrapRegisters, TrapTest, tls};
89+
use std::cell::Cell;
390
use std::ffi::c_void;
491
use std::io;
592
use windows_sys::Win32::Foundation::*;
@@ -9,25 +96,40 @@ use windows_sys::Win32::System::Diagnostics::Debug::*;
996
pub type SignalHandler = Box<dyn Fn(*mut EXCEPTION_POINTERS) -> bool + Send + Sync>;
1097

1198
pub struct TrapHandler {
12-
handle: *mut c_void,
99+
exception_handler: *mut c_void,
100+
continue_handler: *mut c_void,
13101
}
14102

15103
unsafe impl Send for TrapHandler {}
16104
unsafe impl Sync for TrapHandler {}
17105

18106
impl TrapHandler {
19107
pub unsafe fn new(_macos_use_mach_ports: bool) -> TrapHandler {
20-
// our trap handler needs to go first, so that we can recover from
108+
// Our trap handler needs to go first, so that we can recover from
21109
// wasm faults and continue execution, so pass `1` as a true value
22110
// here.
23-
let handle = unsafe { AddVectoredExceptionHandler(1, Some(exception_handler)) };
24-
if handle.is_null() {
111+
//
112+
// Note that this is true for the "continue" handler as well since we
113+
// want to short-circuit as many other continue handlers as we can on
114+
// wasm exceptions.
115+
let exception_handler = unsafe { AddVectoredExceptionHandler(1, Some(exception_handler)) };
116+
if exception_handler.is_null() {
25117
panic!(
26118
"failed to add exception handler: {}",
27119
io::Error::last_os_error()
28120
);
29121
}
30-
TrapHandler { handle }
122+
let continue_handler = unsafe { AddVectoredContinueHandler(1, Some(continue_handler)) };
123+
if continue_handler.is_null() {
124+
panic!(
125+
"failed to add continue handler: {}",
126+
io::Error::last_os_error()
127+
);
128+
}
129+
TrapHandler {
130+
exception_handler,
131+
continue_handler,
132+
}
31133
}
32134

33135
pub fn validate_config(&self, _macos_use_mach_ports: bool) {}
@@ -36,18 +138,36 @@ impl TrapHandler {
36138
impl Drop for TrapHandler {
37139
fn drop(&mut self) {
38140
unsafe {
39-
let rc = RemoveVectoredExceptionHandler(self.handle);
141+
let rc = RemoveVectoredExceptionHandler(self.exception_handler);
40142
if rc == 0 {
41143
eprintln!(
42144
"failed to remove exception handler: {}",
43145
io::Error::last_os_error()
44146
);
45147
libc::abort();
46148
}
149+
let rc = RemoveVectoredContinueHandler(self.continue_handler);
150+
if rc == 0 {
151+
eprintln!(
152+
"failed to remove continue handler: {}",
153+
io::Error::last_os_error()
154+
);
155+
libc::abort();
156+
}
47157
}
48158
}
49159
}
50160

161+
std::thread_local! {
162+
static LAST_EXCEPTION_PC: Cell<usize> = const { Cell::new(0) };
163+
}
164+
165+
/// Wasmtime's exception handler for Windows. See module docs for more.
166+
///
167+
/// # Safety
168+
///
169+
/// Invoked by Windows' vectored exception system; should not be called by
170+
/// anyone else.
51171
#[allow(
52172
clippy::cast_possible_truncation,
53173
reason = "too fiddly to handle and wouldn't help much anyway"
@@ -116,6 +236,7 @@ unsafe extern "system" fn exception_handler(exception_info: *mut EXCEPTION_POINT
116236
TrapTest::HandledByEmbedder => EXCEPTION_CONTINUE_EXECUTION,
117237
TrapTest::Trap(handler) => {
118238
let context = unsafe { exception_info.ContextRecord.as_mut().unwrap() };
239+
LAST_EXCEPTION_PC.with(|s| s.set(handler.pc));
119240
cfg_if::cfg_if! {
120241
if #[cfg(target_arch = "x86_64")] {
121242
context.Rip = handler.pc as _;
@@ -139,3 +260,30 @@ unsafe extern "system" fn exception_handler(exception_info: *mut EXCEPTION_POINT
139260
}
140261
})
141262
}
263+
264+
/// See module docs for more information on what this is doing.
265+
///
266+
/// # Safety
267+
///
268+
/// Invoked by Windows' vectored exception system; should not be called by
269+
/// anyone else.
270+
unsafe extern "system" fn continue_handler(exception_info: *mut EXCEPTION_POINTERS) -> i32 {
271+
let context = unsafe { &(*(*exception_info).ContextRecord) };
272+
let last_exception_pc = LAST_EXCEPTION_PC.with(|s| s.replace(0));
273+
274+
cfg_if::cfg_if! {
275+
if #[cfg(target_arch = "x86_64")] {
276+
let context_pc = context.Rip as usize;
277+
} else if #[cfg(target_arch = "aarch64")] {
278+
let context_pc = context.Pc as usize;
279+
} else {
280+
compile_error!("unsupported platform");
281+
}
282+
}
283+
284+
if last_exception_pc == context_pc {
285+
EXCEPTION_CONTINUE_EXECUTION
286+
} else {
287+
EXCEPTION_CONTINUE_SEARCH
288+
}
289+
}

0 commit comments

Comments
 (0)