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 
87 use crate::prelude::*;
88 use crate::runtime::vm::traphandlers::{TrapRegisters, TrapTest, tls};
89 use std::cell::Cell;
90 use std::ffi::c_void;
91 use std::io;
92 use windows_sys::Win32::Foundation::*;
93 use windows_sys::Win32::System::Diagnostics::Debug::*;
94 
95 /// Function which may handle custom signals while processing traps.
96 pub type SignalHandler = Box<dyn Fn(*mut EXCEPTION_POINTERS) -> bool + Send + Sync>;
97 
98 pub struct TrapHandler {
99     exception_handler: *mut c_void,
100     continue_handler: *mut c_void,
101 }
102 
103 unsafe impl Send for TrapHandler {}
104 unsafe impl Sync for TrapHandler {}
105 
106 impl TrapHandler {
new(_macos_use_mach_ports: bool) -> TrapHandler107     pub unsafe fn new(_macos_use_mach_ports: bool) -> TrapHandler {
108         // Our trap handler needs to go first, so that we can recover from
109         // wasm faults and continue execution, so pass `1` as a true value
110         // here.
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() {
117             panic!(
118                 "failed to add exception handler: {}",
119                 io::Error::last_os_error()
120             );
121         }
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         }
133     }
134 
validate_config(&self, _macos_use_mach_ports: bool)135     pub fn validate_config(&self, _macos_use_mach_ports: bool) {}
136 }
137 
138 impl Drop for TrapHandler {
drop(&mut self)139     fn drop(&mut self) {
140         unsafe {
141             let rc = RemoveVectoredExceptionHandler(self.exception_handler);
142             if rc == 0 {
143                 eprintln!(
144                     "failed to remove exception handler: {}",
145                     io::Error::last_os_error()
146                 );
147                 libc::abort();
148             }
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             }
157         }
158     }
159 }
160 
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.
171 #[allow(
172     clippy::cast_possible_truncation,
173     reason = "too fiddly to handle and wouldn't help much anyway"
174 )]
exception_handler(exception_info: *mut EXCEPTION_POINTERS) -> i32175 unsafe extern "system" fn exception_handler(exception_info: *mut EXCEPTION_POINTERS) -> i32 {
176     let exception_info = unsafe { exception_info.as_mut().unwrap() };
177     // Check the kind of exception, since we only handle a subset within
178     // wasm code. If anything else happens we want to defer to whatever
179     // the rest of the system wants to do for this exception.
180     let record = unsafe { &*exception_info.ExceptionRecord };
181     if record.ExceptionCode != EXCEPTION_ACCESS_VIOLATION
182         && record.ExceptionCode != EXCEPTION_ILLEGAL_INSTRUCTION
183         && record.ExceptionCode != EXCEPTION_INT_DIVIDE_BY_ZERO
184         && record.ExceptionCode != EXCEPTION_INT_OVERFLOW
185     {
186         return EXCEPTION_CONTINUE_SEARCH;
187     }
188 
189     // FIXME: this is what the previous C++ did to make sure that TLS
190     // works by the time we execute this trap handling code. This isn't
191     // exactly super easy to call from Rust though and it's not clear we
192     // necessarily need to do so. Leaving this here in case we need this
193     // in the future, but for now we can probably wait until we see a
194     // strange fault before figuring out how to reimplement this in
195     // Rust.
196     //
197     // if (!NtCurrentTeb()->Reserved1[sThreadLocalArrayPointerIndex]) {
198     //     return EXCEPTION_CONTINUE_SEARCH;
199     // }
200 
201     // This is basically the same as the unix version above, only with a
202     // few parameters tweaked here and there.
203     tls::with(|info| {
204         let info = match info {
205             Some(info) => info,
206             None => return EXCEPTION_CONTINUE_SEARCH,
207         };
208         let context = unsafe { exception_info.ContextRecord.as_ref().unwrap() };
209         cfg_if::cfg_if! {
210             if #[cfg(target_arch = "x86_64")] {
211                 let regs = TrapRegisters {
212                     pc: context.Rip as usize,
213                     fp: context.Rbp as usize,
214                 };
215             } else if #[cfg(target_arch = "aarch64")] {
216                 let regs = TrapRegisters {
217                     pc: context.Pc as usize,
218                     fp: unsafe { context.Anonymous.Anonymous.Fp as usize },
219                 };
220             } else {
221                 compile_error!("unsupported platform");
222             }
223         }
224         // For access violations the first element in `ExceptionInformation` is
225         // an indicator as to whether the fault was a read/write. The second
226         // element is the address of the inaccessible data causing this
227         // violation.
228         let faulting_addr = if record.ExceptionCode == EXCEPTION_ACCESS_VIOLATION {
229             assert!(record.NumberParameters >= 2);
230             Some(record.ExceptionInformation[1])
231         } else {
232             None
233         };
234         match info.test_if_trap(regs, faulting_addr, |handler| handler(exception_info)) {
235             TrapTest::NotWasm => EXCEPTION_CONTINUE_SEARCH,
236             TrapTest::HandledByEmbedder => EXCEPTION_CONTINUE_EXECUTION,
237             TrapTest::Trap(handler) => {
238                 let context = unsafe { exception_info.ContextRecord.as_mut().unwrap() };
239                 LAST_EXCEPTION_PC.with(|s| s.set(handler.pc));
240                 cfg_if::cfg_if! {
241                     if #[cfg(target_arch = "x86_64")] {
242                         context.Rip = handler.pc as _;
243                         context.Rbp = handler.fp as _;
244                         context.Rsp = handler.sp as _;
245                         context.Rax = 0;
246                         context.Rdx = 0;
247                     } else if #[cfg(target_arch = "aarch64")] {
248                         context.Pc = handler.pc as _;
249                         context.Sp = handler.sp as _;
250                         context.Anonymous.Anonymous.Fp = handler.fp as _;
251                         context.Anonymous.Anonymous.X0 = 0;
252                         context.Anonymous.Anonymous.X1 = 0;
253                     } else {
254                         compile_error!("unsupported platform");
255                     }
256                 }
257 
258                 EXCEPTION_CONTINUE_EXECUTION
259             }
260         }
261     })
262 }
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.
continue_handler(exception_info: *mut EXCEPTION_POINTERS) -> i32270 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 }
290