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