feat(crash): Breakpad-style minidumps on macOS and Windows
Ghostty links sentry-native with the breakpad backend on every platform (SharedDeps.zig), so it writes minidumps on macOS and Windows as well. - macOS: a Mach exception port handler thread (structured like Breakpad's exception_handler) catches EXC_BAD_ACCESS/BAD_INSTRUCTION/ ARITHMETIC/BREAKPOINT, suspends other threads, reads the crashed thread state and writes a minidump via minidump-writer as the event.minidump attachment, then restores the previous ports so the BSD signal and default termination follow. The signal path remains the fallback (and handles SIGABRT); a watchdog alarm bounds dumping. - Windows: SetUnhandledExceptionFilter hands off to a pre-started dump thread using minidump-writer's dump_crash_context. - The screen-file open action decodes the escaped path to raw OS bytes (also for the C action passed to embedders). macOS/Windows code is compile-checked (clippy -D warnings) only; the shared Mach message decoding and minidump attachment logic have Linux unit tests. Platform matrix in docs/rewrite/notes/crash-CR.md. Test Plan: - cargo test --workspace (3348 passed) - just check-macos; just check-macos x86_64-apple-darwin; just check-windows
This commit is contained in:
1 parent
41e0b9cb68
commit
b1ad29fa97
12 files changed
+914
-34
No files matched your search
@@ -1,8 +1,52 @@
|
||||
//! Windows stand-in for `signal.rs`: Zig's `crash/sentry.zig` does not
|
||||
//! initialise crash capture on Windows, so there are no fatal-signal
|
||||
//! handlers. Only the helpers the report writer shares are provided.
|
||||
//! Windows crash capture, mirroring Breakpad's in-process `ExceptionHandler`
|
||||
//! (which sentry-native's breakpad backend uses upstream): an unhandled
|
||||
//! exception filter hands the exception to a pre-started handler thread, which
|
||||
//! writes a minidump of the process with `minidump-writer`
|
||||
//! (`MiniDumpWriteDump`) and attaches it to the envelope (`event.minidump`).
|
||||
//! Running the dump on a separate thread with its own stack keeps it working
|
||||
//! after a stack overflow. After writing, the filter returns
|
||||
//! `EXCEPTION_CONTINUE_SEARCH` so the normal termination (and Windows Error
|
||||
//! Reporting) proceeds.
|
||||
//!
|
||||
//! Faults raised by `__fastfail` (Rust's `abort`) bypass exception filters
|
||||
//! and are not captured; panics are covered by the panic hook.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::hash::{BuildHasher, Hasher, RandomState};
|
||||
use std::{
|
||||
fs::File,
|
||||
hash::{BuildHasher, Hasher, RandomState},
|
||||
sync::atomic::{AtomicBool, AtomicIsize, AtomicPtr, Ordering},
|
||||
};
|
||||
|
||||
use minidump_writer::{crash_context::CrashContext, minidump_writer::MinidumpWriter};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{CloseHandle, FALSE, HANDLE, WAIT_OBJECT_0},
|
||||
System::{
|
||||
Diagnostics::Debug::{EXCEPTION_POINTERS, SetUnhandledExceptionFilter},
|
||||
Threading::{
|
||||
CreateEventW,
|
||||
GetCurrentProcessId,
|
||||
GetCurrentThreadId,
|
||||
INFINITE,
|
||||
SetEvent,
|
||||
WaitForSingleObject,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{exception, sentry};
|
||||
|
||||
/// How long the faulting thread waits for the dump.
|
||||
const TIMEOUT_MS: u32 = 30_000;
|
||||
|
||||
/// `EXCEPTION_CONTINUE_SEARCH`.
|
||||
const CONTINUE_SEARCH: i32 = 0;
|
||||
|
||||
static BUSY: AtomicBool = AtomicBool::new(false);
|
||||
static REQUEST: AtomicIsize = AtomicIsize::new(0);
|
||||
static DONE: AtomicIsize = AtomicIsize::new(0);
|
||||
static POINTERS: AtomicPtr<EXCEPTION_POINTERS> = AtomicPtr::new(std::ptr::null_mut());
|
||||
static FAULT_THREAD: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||
|
||||
/// Fill `out` with random bytes (used for the event UUID). The standard
|
||||
/// library's per-instance random hasher keys are good enough for an id.
|
||||
@@ -14,5 +58,112 @@ pub(crate) fn random_bytes(out: &mut [u8; 16]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// No fatal-signal handlers on Windows.
|
||||
pub(crate) fn install_handlers() {}
|
||||
/// Start the dump thread and register the unhandled exception filter.
|
||||
pub(crate) fn install_handlers() {
|
||||
// SAFETY: plain Win32 calls; the events live for the whole process.
|
||||
let (request, done) = unsafe {
|
||||
(
|
||||
CreateEventW(std::ptr::null(), FALSE, FALSE, std::ptr::null()),
|
||||
CreateEventW(std::ptr::null(), FALSE, FALSE, std::ptr::null()),
|
||||
)
|
||||
};
|
||||
if request.is_null() || done.is_null() {
|
||||
return;
|
||||
}
|
||||
REQUEST.store(request as isize, Ordering::Release);
|
||||
DONE.store(done as isize, Ordering::Release);
|
||||
let spawned = std::thread::Builder::new()
|
||||
.name("ghostty-exception-handler".into())
|
||||
.spawn(dump_thread);
|
||||
if spawned.is_err() {
|
||||
// SAFETY: the handles were just created and are unused.
|
||||
unsafe {
|
||||
CloseHandle(request);
|
||||
CloseHandle(done);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// SAFETY: `filter` has the required signature and lives forever.
|
||||
unsafe { SetUnhandledExceptionFilter(Some(filter)) };
|
||||
}
|
||||
|
||||
unsafe extern "system" fn filter(info: *const EXCEPTION_POINTERS) -> i32 {
|
||||
if sentry::state().is_some() && !BUSY.swap(true, Ordering::AcqRel) {
|
||||
POINTERS.store(info.cast_mut(), Ordering::Release);
|
||||
// SAFETY: trivial Win32 calls with handles created at install.
|
||||
unsafe {
|
||||
FAULT_THREAD.store(GetCurrentThreadId(), Ordering::Release);
|
||||
SetEvent(REQUEST.load(Ordering::Acquire) as HANDLE);
|
||||
let r = WaitForSingleObject(DONE.load(Ordering::Acquire) as HANDLE, TIMEOUT_MS);
|
||||
let _ = r == WAIT_OBJECT_0;
|
||||
}
|
||||
}
|
||||
CONTINUE_SEARCH
|
||||
}
|
||||
|
||||
fn dump_thread() {
|
||||
loop {
|
||||
// SAFETY: handle created at install.
|
||||
let r = unsafe { WaitForSingleObject(REQUEST.load(Ordering::Acquire) as HANDLE, INFINITE) };
|
||||
if r != WAIT_OBJECT_0 {
|
||||
return;
|
||||
}
|
||||
if let Some(st) = sentry::state() {
|
||||
let info = POINTERS.load(Ordering::Acquire);
|
||||
write_report(st, info, FAULT_THREAD.load(Ordering::Acquire));
|
||||
}
|
||||
// SAFETY: handle created at install.
|
||||
unsafe { SetEvent(DONE.load(Ordering::Acquire) as HANDLE) };
|
||||
}
|
||||
}
|
||||
|
||||
fn write_report(st: &sentry::State, info: *mut EXCEPTION_POINTERS, tid: u32) {
|
||||
// SAFETY: the faulting thread is blocked in the filter, so the pointers
|
||||
// it passed stay valid.
|
||||
let code = unsafe { info.as_ref().and_then(|i| i.ExceptionRecord.as_ref()) }
|
||||
.map_or(0, |r| r.ExceptionCode) as u32;
|
||||
let (kind, desc) = exception::windows_exception(code);
|
||||
|
||||
let dir = std::path::PathBuf::from(String::from_utf8_lossy(&st.dir).into_owned());
|
||||
let dump = (|| -> Result<Vec<u8>, String> {
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
let tmp = dir.join(format!("minidump-{}.tmp", std::process::id()));
|
||||
let mut file = File::create(&tmp).map_err(|e| e.to_string())?;
|
||||
let cc = CrashContext {
|
||||
exception_pointers: info.cast_const().cast(),
|
||||
// SAFETY: trivial Win32 call.
|
||||
process_id: unsafe { GetCurrentProcessId() },
|
||||
thread_id: tid,
|
||||
exception_code: code as i32,
|
||||
};
|
||||
let r = MinidumpWriter::dump_crash_context(&cc, None, &mut file).map_err(|e| e.to_string());
|
||||
drop(file);
|
||||
let data = r.and_then(|()| std::fs::read(&tmp).map_err(|e| e.to_string()));
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
data
|
||||
})();
|
||||
let dump = match dump {
|
||||
Ok(d) => Some(d),
|
||||
Err(e) => {
|
||||
log::warn!("minidump failed err={e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let value = format!("{desc} (code=0x{code:08x})");
|
||||
match sentry::write_report_ext(
|
||||
st,
|
||||
kind,
|
||||
&value,
|
||||
"exceptionfilter",
|
||||
None,
|
||||
None,
|
||||
dump.as_deref(),
|
||||
) {
|
||||
Ok(path) => eprintln!(
|
||||
"warning(sentry): crash report with minidump written to disk path={}",
|
||||
path.display()
|
||||
),
|
||||
Err(e) => eprintln!("warning(sentry): failed to persist crash report err={e}"),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user