Skip to content

Commit 7f978f3

Browse files
geofftsamypr100
andcommitted
Add an exception handler on Windows
We've seen a few cases of uv.exe exiting with an exception code as its exit status and no user-visible output (#14563 in the field, and #13812 in CI). It seems that recent versions of Windows no longer show dialog boxes on access violations (what UNIX calls segfaults) or similar errors. Something is probably sent to Windows Error Reporting, and we can maybe sign up to get the crashes from Microsoft, but the user experience of seeing uv exit with no output is poor, both for end users and during development. While it's possible to opt out of this behavior or set up a debugger, this isn't the default configuration. (See https://superuser.com/q/1246626 for some pointers.) In order to get some output on a crash, we need to install our own default handler for unhandled exceptions (or call all our code inside a Structured Exception Handling __try/__catch block, which is complicated on Rust). This is the moral equivalent of a segfault handler on Windows; the kernel creates a new stack frame and passes arguments to it with some processor state. This commit adds a relatively simple exception handler that leans on Rust's own backtrace implementation and also displays some minimal information from the exception itself. This should be enough info to communicate that something went wrong and let us collect enough information to attempt to debug. There are also a handful of (non-Rust) open-source libraries for this like Breakpad and Crashpad (both from Google) and crashrpt. The approach here, of using SetUnhandledExceptionFilter, seems to be the standard one taken by other such libraries. Crashpad also seems to try to use a newer mechanism for an out-of-tree DLL to report the crash: https://issues.chromium.org/issues/42310037 If we have serious problems with memory corruption, it might be worth adopting some third-party library that has already implemented this approach. (In general, the docs of other crash reporting libraries are worth skimming to understand how these things ought to work.) Co-authored-by: samypr100 <[email protected]>
1 parent 7ea030a commit 7f978f3

File tree

5 files changed

+135
-1
lines changed

5 files changed

+135
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ url = { version = "2.5.2", features = ["serde"] }
184184
version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" }
185185
walkdir = { version = "2.5.0" }
186186
which = { version = "8.0.0", features = ["regex"] }
187-
windows = { version = "0.59.0", features = ["Win32_Storage_FileSystem"] }
187+
windows = { version = "0.59.0", features = ["Win32_System_Kernel", "Win32_System_Diagnostics_Debug", "Win32_Storage_FileSystem"] }
188188
windows-core = { version = "0.59.0" }
189189
windows-registry = { version = "0.5.0" }
190190
windows-result = { version = "0.3.0" }

crates/uv/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ zip = { workspace = true }
108108

109109
[target.'cfg(target_os = "windows")'.dependencies]
110110
self-replace = { workspace = true }
111+
windows = { workspace = true }
111112

112113
[dev-dependencies]
113114
assert_cmd = { version = "2.0.16" }

crates/uv/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ pub(crate) mod commands;
5757
pub(crate) mod logging;
5858
pub(crate) mod printer;
5959
pub(crate) mod settings;
60+
#[cfg(windows)]
61+
mod windows_exception;
6062

6163
#[instrument(skip_all)]
6264
async fn run(mut cli: Cli) -> Result<ExitStatus> {
@@ -2189,6 +2191,9 @@ where
21892191
I: IntoIterator<Item = T>,
21902192
T: Into<OsString> + Clone,
21912193
{
2194+
#[cfg(windows)]
2195+
windows_exception::setup();
2196+
21922197
// Set the `UV` variable to the current executable so it is implicitly propagated to all child
21932198
// processes, e.g., in `uv run`.
21942199
if let Ok(current_exe) = std::env::current_exe() {

crates/uv/src/windows_exception.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//! Helper for setting up Windows exception handling.
2+
//!
3+
//! Recent versions of Windows seem to no longer show dialog boxes on access violations
4+
//! (segfaults) or similar errors. The user experience is that the command exits with
5+
//! the exception code as its exit status and no visible output. In order to see these
6+
//! errors both in the field and in CI, we need to install our own exception handler.
7+
//!
8+
//! This is a relatively simple exception handler that leans on Rust's own backtrace
9+
//! implementation and also displays some minimal information from the exception itself.
10+
11+
#![allow(unsafe_code)]
12+
13+
use windows::Win32::{
14+
Foundation,
15+
System::Diagnostics::Debug::{
16+
CONTEXT, EXCEPTION_CONTINUE_SEARCH, EXCEPTION_POINTERS, SetUnhandledExceptionFilter,
17+
},
18+
};
19+
20+
fn display_exception_info(name: &str, info: &[usize; 15]) {
21+
match info[0] {
22+
0 => eprintln!("{name} reading {:#16x}", info[1]),
23+
1 => eprintln!("{name} writing {:#16x}", info[1]),
24+
8 => eprintln!("{name} executing {:#16x}", info[1]),
25+
_ => eprintln!("{name} from operation {} at {:#16x}", info[0], info[1]),
26+
}
27+
}
28+
29+
#[cfg(target_arch = "x86")]
30+
fn dump_regs(c: &CONTEXT) {
31+
eprintln!(
32+
"eax={:08x} ebx={:08x} ecx={:08x} edx={:08x} esi={:08x} edi={:08x}",
33+
c.Eax, c.Ebx, c.Ecx, c.Edx, c.Esi, c.Edi
34+
);
35+
eprintln!(
36+
"eip={:08x} ebp={:08x} esp={:08x} eflags={:08x}",
37+
c.Eip, c.Ebp, c.Esp, c.EFlags
38+
);
39+
}
40+
41+
#[cfg(target_arch = "x86_64")]
42+
fn dump_regs(c: &CONTEXT) {
43+
eprintln!("rax={:016x} rbx={:016x} rcx={:016x}", c.Rax, c.Rbx, c.Rcx);
44+
eprintln!("rdx={:016x} rsx={:016x} rdi={:016x}", c.Rdx, c.Rsi, c.Rdi);
45+
eprintln!("rsp={:016x} rbp={:016x} r8={:016x}", c.Rsp, c.Rbp, c.R8);
46+
eprintln!(" r9={:016x} r10={:016x} r11={:016x}", c.R9, c.R10, c.R11);
47+
eprintln!("r12={:016x} r13={:016x} r14={:016x}", c.R12, c.R13, c.R14);
48+
eprintln!(
49+
"r15={:016x} rip={:016x} eflags={:016x}",
50+
c.R15, c.Rip, c.EFlags
51+
);
52+
}
53+
54+
#[cfg(target_arch = "aarch64")]
55+
fn dump_regs(c: &CONTEXT) {
56+
let r = c.Anonymous.Anonymous;
57+
eprintln!("cpsr={:016x} sp={:016x} pc={:016x}", c.Cpsr, c.Sp, c.Pc);
58+
eprintln!(" x0={:016x} x1={:016x} x2={:016x}", r.X0, r.X1, r.X2);
59+
eprintln!(" x3={:016x} x4={:016x} x5={:016x}", r.X3, r.X4, r.X5);
60+
eprintln!(" x6={:016x} x7={:016x} x8={:016x}", r.X6, r.X7, r.X8);
61+
eprintln!(" x9={:016x} x10={:016x} x11={:016x}", r.X9, r.X10, r.X11);
62+
eprintln!(" x12={:016x} x13={:016x} x14={:016x}", r.X12, r.X13, r.X14);
63+
eprintln!(" x15={:016x} x16={:016x} x17={:016x}", r.X15, r.X16, r.X17);
64+
eprintln!(" x18={:016x} x19={:016x} x20={:016x}", r.X18, r.X19, r.X20);
65+
eprintln!(" x21={:016x} x22={:016x} x23={:016x}", r.X21, r.X22, r.X23);
66+
eprintln!(" x24={:016x} x25={:016x} x26={:016x}", r.X24, r.X25, r.X26);
67+
eprintln!(" x27={:016x} x28={:016x}", r.X27, r.X28);
68+
eprintln!(" fp={:016x} lr={:016x}", r.Fp, r.Lr);
69+
}
70+
71+
unsafe extern "system" fn unhandled_exception_filter(
72+
exception_info: *const EXCEPTION_POINTERS,
73+
) -> i32 {
74+
// TODO: Really we should not be using eprintln here because Stderr is not async-signal-safe.
75+
// Probably we should be calling the console APIs directly.
76+
eprintln!("error: unhandled exception in uv, please report a bug:");
77+
let mut context = None;
78+
// SAFETY: Pointer comes from the OS
79+
if let Some(info) = unsafe { exception_info.as_ref() } {
80+
// SAFETY: Pointer comes from the OS
81+
if let Some(exc) = unsafe { info.ExceptionRecord.as_ref() } {
82+
eprintln!(
83+
"code {:#X} at address {:?}",
84+
exc.ExceptionCode.0, exc.ExceptionAddress
85+
);
86+
match exc.ExceptionCode {
87+
Foundation::EXCEPTION_ACCESS_VIOLATION => {
88+
display_exception_info("EXCEPTION_ACCESS_VIOLATION", &exc.ExceptionInformation);
89+
}
90+
Foundation::EXCEPTION_IN_PAGE_ERROR => {
91+
display_exception_info("EXCEPTION_IN_PAGE_ERROR", &exc.ExceptionInformation);
92+
}
93+
Foundation::EXCEPTION_ILLEGAL_INSTRUCTION => {
94+
eprintln!("EXCEPTION_ILLEGAL_INSTRUCTION");
95+
}
96+
Foundation::EXCEPTION_STACK_OVERFLOW => {
97+
eprintln!("EXCEPTION_STACK_OVEFLOW");
98+
}
99+
_ => {}
100+
}
101+
} else {
102+
eprintln!("(ExceptionRecord is NULL)");
103+
}
104+
// SAFETY: Pointer comes from the OS
105+
context = unsafe { info.ContextRecord.as_ref() };
106+
} else {
107+
eprintln!("(ExceptionInfo is NULL)");
108+
}
109+
let backtrace = std::backtrace::Backtrace::capture();
110+
if backtrace.status() == std::backtrace::BacktraceStatus::Disabled {
111+
eprintln!("note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace");
112+
} else {
113+
if let Some(context) = context {
114+
dump_regs(context);
115+
}
116+
eprintln!("stack backtrace:\n{backtrace:#}");
117+
}
118+
EXCEPTION_CONTINUE_SEARCH
119+
}
120+
121+
/// Set up our handler for unhandled exceptions.
122+
pub(crate) fn setup() {
123+
// SAFETY: winapi call
124+
unsafe {
125+
SetUnhandledExceptionFilter(Some(Some(unhandled_exception_filter)));
126+
}
127+
}

0 commit comments

Comments
 (0)