Add an exception handler on Windows (#14582)

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 <3933065+samypr100@users.noreply.github.com>
This commit is contained in:
Geoffrey Thomas
2025-07-14 10:42:35 -04:00
committed by GitHub
parent 852aba4f90
commit df44199ceb
5 changed files with 138 additions and 1 deletions
Generated
+1
View File
@@ -4734,6 +4734,7 @@ dependencies = [
"walkdir",
"which",
"whoami",
"windows 0.59.0",
"wiremock",
"zip",
]
+1 -1
View File
@@ -184,7 +184,7 @@ url = { version = "2.5.2", features = ["serde"] }
version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" }
walkdir = { version = "2.5.0" }
which = { version = "8.0.0", features = ["regex"] }
windows = { version = "0.59.0", features = ["Win32_Storage_FileSystem"] }
windows = { version = "0.59.0", features = ["Win32_System_Kernel", "Win32_System_Diagnostics_Debug", "Win32_Storage_FileSystem"] }
windows-core = { version = "0.59.0" }
windows-registry = { version = "0.5.0" }
windows-result = { version = "0.3.0" }
+1
View File
@@ -108,6 +108,7 @@ zip = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
self-replace = { workspace = true }
windows = { workspace = true }
[dev-dependencies]
assert_cmd = { version = "2.0.16" }
+5
View File
@@ -57,6 +57,8 @@ pub(crate) mod commands;
pub(crate) mod logging;
pub(crate) mod printer;
pub(crate) mod settings;
#[cfg(windows)]
mod windows_exception;
#[instrument(skip_all)]
async fn run(mut cli: Cli) -> Result<ExitStatus> {
@@ -2189,6 +2191,9 @@ where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
#[cfg(windows)]
windows_exception::setup();
// Set the `UV` variable to the current executable so it is implicitly propagated to all child
// processes, e.g., in `uv run`.
if let Ok(current_exe) = std::env::current_exe() {
+130
View File
@@ -0,0 +1,130 @@
//! Helper for setting up Windows exception handling.
//!
//! Recent versions of Windows seem to no longer show dialog boxes on access violations
//! (segfaults) or similar errors. The user experience is that the command exits with
//! the exception code as its exit status and no visible output. In order to see these
//! errors both in the field and in CI, we need to install our own exception handler.
//!
//! This is a relatively simple exception handler that leans on Rust's own backtrace
//! implementation and also displays some minimal information from the exception itself.
#![allow(unsafe_code)]
#![allow(clippy::print_stderr)]
use windows::Win32::{
Foundation,
System::Diagnostics::Debug::{
CONTEXT, EXCEPTION_CONTINUE_SEARCH, EXCEPTION_POINTERS, SetUnhandledExceptionFilter,
},
};
fn display_exception_info(name: &str, info: &[usize; 15]) {
match info[0] {
0 => eprintln!("{name} reading {:#x}", info[1]),
1 => eprintln!("{name} writing {:#x}", info[1]),
8 => eprintln!("{name} executing {:#x}", info[1]),
_ => eprintln!("{name} from operation {} at {:#x}", info[0], info[1]),
}
}
#[cfg(target_arch = "x86")]
fn dump_regs(c: &CONTEXT) {
eprintln!(
"eax={:08x} ebx={:08x} ecx={:08x} edx={:08x} esi={:08x} edi={:08x}",
c.Eax, c.Ebx, c.Ecx, c.Edx, c.Esi, c.Edi
);
eprintln!(
"eip={:08x} ebp={:08x} esp={:08x} eflags={:08x}",
c.Eip, c.Ebp, c.Esp, c.EFlags
);
}
#[cfg(target_arch = "x86_64")]
fn dump_regs(c: &CONTEXT) {
eprintln!("rax={:016x} rbx={:016x} rcx={:016x}", c.Rax, c.Rbx, c.Rcx);
eprintln!("rdx={:016x} rsx={:016x} rdi={:016x}", c.Rdx, c.Rsi, c.Rdi);
eprintln!("rsp={:016x} rbp={:016x} r8={:016x}", c.Rsp, c.Rbp, c.R8);
eprintln!(" r9={:016x} r10={:016x} r11={:016x}", c.R9, c.R10, c.R11);
eprintln!("r12={:016x} r13={:016x} r14={:016x}", c.R12, c.R13, c.R14);
eprintln!(
"r15={:016x} rip={:016x} eflags={:016x}",
c.R15, c.Rip, c.EFlags
);
}
#[cfg(target_arch = "aarch64")]
fn dump_regs(c: &CONTEXT) {
// SAFETY: The two variants of this anonymous union are equivalent,
// one's an array and one has named registers.
let r = unsafe { c.Anonymous.Anonymous };
eprintln!("cpsr={:016x} sp={:016x} pc={:016x}", c.Cpsr, c.Sp, c.Pc);
eprintln!(" x0={:016x} x1={:016x} x2={:016x}", r.X0, r.X1, r.X2);
eprintln!(" x3={:016x} x4={:016x} x5={:016x}", r.X3, r.X4, r.X5);
eprintln!(" x6={:016x} x7={:016x} x8={:016x}", r.X6, r.X7, r.X8);
eprintln!(" x9={:016x} x10={:016x} x11={:016x}", r.X9, r.X10, r.X11);
eprintln!(" x12={:016x} x13={:016x} x14={:016x}", r.X12, r.X13, r.X14);
eprintln!(" x15={:016x} x16={:016x} x17={:016x}", r.X15, r.X16, r.X17);
eprintln!(" x18={:016x} x19={:016x} x20={:016x}", r.X18, r.X19, r.X20);
eprintln!(" x21={:016x} x22={:016x} x23={:016x}", r.X21, r.X22, r.X23);
eprintln!(" x24={:016x} x25={:016x} x26={:016x}", r.X24, r.X25, r.X26);
eprintln!(" x27={:016x} x28={:016x}", r.X27, r.X28);
eprintln!(" fp={:016x} lr={:016x}", r.Fp, r.Lr);
}
unsafe extern "system" fn unhandled_exception_filter(
exception_info: *const EXCEPTION_POINTERS,
) -> i32 {
// TODO: Really we should not be using eprintln here because Stderr is not async-signal-safe.
// Probably we should be calling the console APIs directly.
eprintln!("error: unhandled exception in uv, please report a bug:");
let mut context = None;
// SAFETY: Pointer comes from the OS
if let Some(info) = unsafe { exception_info.as_ref() } {
// SAFETY: Pointer comes from the OS
if let Some(exc) = unsafe { info.ExceptionRecord.as_ref() } {
eprintln!(
"code {:#X} at address {:?}",
exc.ExceptionCode.0, exc.ExceptionAddress
);
match exc.ExceptionCode {
Foundation::EXCEPTION_ACCESS_VIOLATION => {
display_exception_info("EXCEPTION_ACCESS_VIOLATION", &exc.ExceptionInformation);
}
Foundation::EXCEPTION_IN_PAGE_ERROR => {
display_exception_info("EXCEPTION_IN_PAGE_ERROR", &exc.ExceptionInformation);
}
Foundation::EXCEPTION_ILLEGAL_INSTRUCTION => {
eprintln!("EXCEPTION_ILLEGAL_INSTRUCTION");
}
Foundation::EXCEPTION_STACK_OVERFLOW => {
eprintln!("EXCEPTION_STACK_OVERFLOW");
}
_ => {}
}
} else {
eprintln!("(ExceptionRecord is NULL)");
}
// SAFETY: Pointer comes from the OS
context = unsafe { info.ContextRecord.as_ref() };
} else {
eprintln!("(ExceptionInfo is NULL)");
}
let backtrace = std::backtrace::Backtrace::capture();
if backtrace.status() == std::backtrace::BacktraceStatus::Disabled {
eprintln!("note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace");
} else {
if let Some(context) = context {
dump_regs(context);
}
eprintln!("stack backtrace:\n{backtrace:#}");
}
EXCEPTION_CONTINUE_SEARCH
}
/// Set up our handler for unhandled exceptions.
pub(crate) fn setup() {
// SAFETY: winapi call
unsafe {
SetUnhandledExceptionFilter(Some(Some(unhandled_exception_filter)));
}
}