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
147 lines
5.1 KiB
Rust
147 lines
5.1 KiB
Rust
//! Darwin glue for [`crate::native`]: fill the native crash context from the
|
|
//! `siginfo_t` and `ucontext_t` passed to the fatal-signal handler.
|
|
//!
|
|
//! The handler runs on the faulting thread, so the `ucontext` holds the
|
|
//! thread state at the fault (the equivalent of `thread_get_state` on the
|
|
//! crashed thread that breakpad's Mach exception handler does). Memory is read
|
|
//! with `mach_vm_read_overwrite`, which returns an error instead of faulting
|
|
//! for unmapped addresses, so walking a corrupted stack is safe.
|
|
#![allow(unsafe_code)]
|
|
|
|
use crate::native::Native;
|
|
|
|
/// `struct __darwin_ucontext` (`<sys/_types/_ucontext.h>`, 64-bit).
|
|
#[repr(C)]
|
|
#[allow(clippy::struct_field_names)]
|
|
struct Ucontext {
|
|
uc_onstack: libc::c_int,
|
|
uc_sigmask: u32,
|
|
uc_stack: libc::stack_t,
|
|
uc_link: *mut Ucontext,
|
|
uc_mcsize: usize,
|
|
/// `struct __darwin_mcontext64 *`: exception state (16 bytes on both
|
|
/// x86_64 and arm64) followed by the thread state.
|
|
uc_mcontext: *const u8,
|
|
}
|
|
|
|
#[cfg(target_arch = "aarch64")]
|
|
const X_NAMES: [&str; 29] = [
|
|
"x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", "x10", "x11", "x12", "x13", "x14",
|
|
"x15", "x16", "x17", "x18", "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27",
|
|
"x28",
|
|
];
|
|
|
|
/// Size of `__darwin_x86_exception_state64` / `__darwin_arm_exception_state64`,
|
|
/// which precedes the thread state in `__darwin_mcontext64`.
|
|
const EXCEPTION_STATE_SIZE: usize = 16;
|
|
|
|
fn read_mem(addr: u64) -> Option<[u8; 16]> {
|
|
let mut buf = [0u8; 16];
|
|
let mut out: mach2::vm_types::mach_vm_size_t = 0;
|
|
// SAFETY: `buf` is a valid 16 byte destination; the call validates the
|
|
// source address and fails instead of faulting.
|
|
let kr = unsafe {
|
|
mach2::vm::mach_vm_read_overwrite(
|
|
mach2::traps::mach_task_self(),
|
|
addr,
|
|
16,
|
|
buf.as_mut_ptr() as mach2::vm_types::mach_vm_address_t,
|
|
&raw mut out,
|
|
)
|
|
};
|
|
(kr == mach2::kern_return::KERN_SUCCESS && out == 16).then_some(buf)
|
|
}
|
|
|
|
/// Fill `n` from the handler arguments.
|
|
///
|
|
/// # Safety
|
|
/// `info` and `uc` must be the pointers the kernel passed to an `SA_SIGINFO`
|
|
/// handler (or null).
|
|
pub(crate) unsafe fn capture(
|
|
n: &mut Native,
|
|
info: *const libc::siginfo_t,
|
|
uc: *const libc::c_void,
|
|
) {
|
|
// SAFETY: per the contract (a non-null siginfo is valid for reads).
|
|
if let Some(info) = unsafe { info.as_ref() } {
|
|
n.signo = info.si_signo;
|
|
n.code = info.si_code;
|
|
n.addr = info.si_addr as u64;
|
|
}
|
|
// SAFETY: per the contract.
|
|
let Some(uc) = (unsafe { uc.cast::<Ucontext>().as_ref() }) else {
|
|
return;
|
|
};
|
|
if uc.uc_mcontext.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the mcontext holds the exception state followed by the thread
|
|
// state; the kernel built it for this signal frame.
|
|
let ss = unsafe { uc.uc_mcontext.add(EXCEPTION_STATE_SIZE) };
|
|
|
|
// SAFETY: `ss` points at the thread state in the kernel built mcontext.
|
|
unsafe { fill_thread_state(n, ss) };
|
|
}
|
|
|
|
/// Fill registers and the frame-pointer stack walk from a thread state
|
|
/// (`x86_thread_state64_t` / `arm_thread_state64_t`, as found in a
|
|
/// `ucontext` or returned by `thread_get_state`).
|
|
///
|
|
/// # Safety
|
|
/// `ss` must point to a valid, aligned thread state of the native flavor.
|
|
pub(crate) unsafe fn fill_thread_state(n: &mut Native, ss: *const u8) {
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
// SAFETY: as above; the layout is `x86_thread_state64_t`.
|
|
let s = unsafe { &*ss.cast::<mach2::structs::x86_thread_state64_t>() };
|
|
for (name, v) in [
|
|
("rax", s.__rax),
|
|
("rbx", s.__rbx),
|
|
("rcx", s.__rcx),
|
|
("rdx", s.__rdx),
|
|
("rdi", s.__rdi),
|
|
("rsi", s.__rsi),
|
|
("rbp", s.__rbp),
|
|
("rsp", s.__rsp),
|
|
("r8", s.__r8),
|
|
("r9", s.__r9),
|
|
("r10", s.__r10),
|
|
("r11", s.__r11),
|
|
("r12", s.__r12),
|
|
("r13", s.__r13),
|
|
("r14", s.__r14),
|
|
("r15", s.__r15),
|
|
("rip", s.__rip),
|
|
("rflags", s.__rflags),
|
|
("cs", s.__cs),
|
|
("fs", s.__fs),
|
|
("gs", s.__gs),
|
|
] {
|
|
n.push_reg(name, v);
|
|
}
|
|
n.walk(s.__rip, s.__rbp, read_mem);
|
|
}
|
|
|
|
#[cfg(target_arch = "aarch64")]
|
|
{
|
|
// SAFETY: as above; the layout is `arm_thread_state64_t`.
|
|
let s = unsafe { &*ss.cast::<mach2::structs::arm_thread_state64_t>() };
|
|
for (name, v) in X_NAMES.iter().zip(s.__x) {
|
|
n.push_reg(name, v);
|
|
}
|
|
n.push_reg("fp", s.__fp);
|
|
n.push_reg("lr", s.__lr);
|
|
n.push_reg("sp", s.__sp);
|
|
n.push_reg("pc", s.__pc);
|
|
n.push_reg("cpsr", u64::from(s.__cpsr));
|
|
// arm64 may sign pointers (PAC): strip the high bits before use.
|
|
let strip = |v: u64| v & 0x0000_007f_ffff_ffff;
|
|
// The pc is exact; the faulting leaf function did not necessarily
|
|
// push a frame record, so the link register is the caller.
|
|
n.walk(s.__pc, strip(s.__fp), read_mem);
|
|
if n.nframes == 1 && s.__lr != 0 {
|
|
n.push_frame(strip(s.__lr));
|
|
}
|
|
}
|
|
}
|