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+
187use crate :: prelude:: * ;
288use crate :: runtime:: vm:: traphandlers:: { TrapRegisters , TrapTest , tls} ;
89+ use std:: cell:: Cell ;
390use std:: ffi:: c_void;
491use std:: io;
592use windows_sys:: Win32 :: Foundation :: * ;
@@ -9,25 +96,40 @@ use windows_sys::Win32::System::Diagnostics::Debug::*;
996pub type SignalHandler = Box < dyn Fn ( * mut EXCEPTION_POINTERS ) -> bool + Send + Sync > ;
1097
1198pub struct TrapHandler {
12- handle : * mut c_void ,
99+ exception_handler : * mut c_void ,
100+ continue_handler : * mut c_void ,
13101}
14102
15103unsafe impl Send for TrapHandler { }
16104unsafe impl Sync for TrapHandler { }
17105
18106impl 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 {
36138impl 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