Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
923
crates/codegen/xai-crash-handler/src/handler.rs
Normal file
923
crates/codegen/xai-crash-handler/src/handler.rs
Normal file
|
|
@ -0,0 +1,923 @@
|
|||
//! Cross-platform crash handler for fatal memory faults.
|
||||
//!
|
||||
//! - **Unix**: SIGBUS/SIGSEGV via `sigaction(2)`.
|
||||
//! - **Windows**: `EXCEPTION_ACCESS_VIOLATION` et al. via `SetUnhandledExceptionFilter`.
|
||||
//!
|
||||
//! Captures crash PC + frame-pointer chain. All handler operations are
|
||||
//! minimal (raw pointer reads, direct file I/O, atomics — no allocation).
|
||||
//! The crash PC is written to disk before frame walking so a secondary
|
||||
//! fault during the walk still produces a usable report.
|
||||
|
||||
#[cfg(unix)]
|
||||
mod imp {
|
||||
use std::ffi::CString;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
||||
|
||||
use crate::format::{self, MAX_FILE_SIZE, MAX_FRAMES};
|
||||
use crate::terminal;
|
||||
|
||||
// ── Platform-specific ucontext access ────────────────────────────────
|
||||
//
|
||||
// The libc crate does not expose ucontext_t on macOS. We define minimal
|
||||
// repr(C) types covering only the fields we need (PC and frame pointer).
|
||||
|
||||
/// Extract the crash instruction pointer and frame pointer from the
|
||||
/// signal handler's context parameter.
|
||||
///
|
||||
/// Returns `(instruction_pointer, frame_pointer)`. Both may be 0 if
|
||||
/// the context is null or the platform is unsupported.
|
||||
unsafe fn extract_pc_and_fp(ctx: *mut libc::c_void) -> (usize, usize) {
|
||||
if ctx.is_null() {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
unsafe {
|
||||
let uc = ctx as *const libc::ucontext_t;
|
||||
let gregs = &(*uc).uc_mcontext.gregs;
|
||||
let ip = gregs[libc::REG_RIP as usize] as usize;
|
||||
let fp = gregs[libc::REG_RBP as usize] as usize;
|
||||
return (ip, fp);
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
|
||||
unsafe {
|
||||
let uc = ctx as *const libc::ucontext_t;
|
||||
let mc = &(*uc).uc_mcontext;
|
||||
let ip = mc.pc as usize;
|
||||
let fp = mc.regs[29] as usize; // x29 = frame pointer
|
||||
return (ip, fp);
|
||||
}
|
||||
|
||||
// macOS does not expose ucontext_t in the libc crate.
|
||||
// Define minimal repr(C) types for the fields we need.
|
||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||
{
|
||||
#[repr(C)]
|
||||
struct Arm64ThreadState {
|
||||
regs: [u64; 29], // x0-x28
|
||||
fp: u64, // x29
|
||||
lr: u64, // x30
|
||||
sp: u64,
|
||||
pc: u64,
|
||||
cpsr: u32,
|
||||
_pad: u32,
|
||||
}
|
||||
#[repr(C)]
|
||||
struct MachMcontext {
|
||||
_es: [u8; 16], // __darwin_arm_exception_state64 (far:u64 + esr:u32 + exception:u32)
|
||||
ss: Arm64ThreadState,
|
||||
// neon state follows but we don't need it
|
||||
}
|
||||
#[repr(C)]
|
||||
struct DarwinUcontext {
|
||||
_onstack: i32,
|
||||
_sigmask: u32,
|
||||
_stack: libc::stack_t,
|
||||
_link: *mut libc::c_void,
|
||||
_mcsize: usize,
|
||||
mctx: *const MachMcontext,
|
||||
}
|
||||
let (ip, fp) = unsafe {
|
||||
let uc = ctx as *const DarwinUcontext;
|
||||
let mctx = (*uc).mctx;
|
||||
if mctx.is_null() {
|
||||
return (0, 0);
|
||||
}
|
||||
((*mctx).ss.pc as usize, (*mctx).ss.fp as usize)
|
||||
};
|
||||
return (ip, fp);
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
||||
{
|
||||
#[repr(C)]
|
||||
struct X86ThreadState {
|
||||
_rax: u64,
|
||||
_rbx: u64,
|
||||
_rcx: u64,
|
||||
_rdx: u64,
|
||||
_rdi: u64,
|
||||
_rsi: u64,
|
||||
rbp: u64,
|
||||
_rsp: u64,
|
||||
_r8: u64,
|
||||
_r9: u64,
|
||||
_r10: u64,
|
||||
_r11: u64,
|
||||
_r12: u64,
|
||||
_r13: u64,
|
||||
_r14: u64,
|
||||
_r15: u64,
|
||||
rip: u64,
|
||||
_rflags: u64,
|
||||
_cs: u64,
|
||||
_fs: u64,
|
||||
_gs: u64,
|
||||
}
|
||||
#[repr(C)]
|
||||
struct MachMcontext {
|
||||
_es: [u8; 16], // __darwin_x86_exception_state64
|
||||
ss: X86ThreadState,
|
||||
}
|
||||
#[repr(C)]
|
||||
struct DarwinUcontext {
|
||||
_onstack: i32,
|
||||
_sigmask: u32,
|
||||
_stack: libc::stack_t,
|
||||
_link: *mut libc::c_void,
|
||||
_mcsize: usize,
|
||||
mctx: *const MachMcontext,
|
||||
}
|
||||
let (ip, fp) = unsafe {
|
||||
let uc = ctx as *const DarwinUcontext;
|
||||
let mctx = (*uc).mctx;
|
||||
if mctx.is_null() {
|
||||
return (0, 0);
|
||||
}
|
||||
((*mctx).ss.rip as usize, (*mctx).ss.rbp as usize)
|
||||
};
|
||||
return (ip, fp);
|
||||
}
|
||||
|
||||
// Unsupported platform — no frames.
|
||||
#[allow(unreachable_code)]
|
||||
(0, 0)
|
||||
}
|
||||
|
||||
/// Walk the frame-pointer chain, collecting return addresses.
|
||||
///
|
||||
/// Fully async-signal-safe: only raw pointer reads, no library calls.
|
||||
/// Stops at the first invalid (null, misaligned, or suspiciously small)
|
||||
/// frame pointer.
|
||||
unsafe fn walk_frame_pointers(initial_fp: usize, out: &mut [usize], max: usize) -> usize {
|
||||
let mut fp = initial_fp;
|
||||
let mut count = 0;
|
||||
|
||||
while count < max {
|
||||
// Validate: non-null, pointer-aligned, not in the zero page.
|
||||
if fp == 0 || fp < 4096 || !fp.is_multiple_of(core::mem::size_of::<usize>()) {
|
||||
break;
|
||||
}
|
||||
// On both x86_64 and aarch64, the frame layout is:
|
||||
// [fp+0] = previous frame pointer
|
||||
// [fp+8] = return address
|
||||
let prev_fp = unsafe { *(fp as *const usize) };
|
||||
let ret_addr = unsafe { *((fp + core::mem::size_of::<usize>()) as *const usize) };
|
||||
|
||||
if ret_addr == 0 || ret_addr < 4096 {
|
||||
break;
|
||||
}
|
||||
out[count] = ret_addr;
|
||||
count += 1;
|
||||
|
||||
// Frame pointer must move upward (toward higher addresses on
|
||||
// most architectures) to avoid infinite loops.
|
||||
if prev_fp <= fp {
|
||||
break;
|
||||
}
|
||||
fp = prev_fp;
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
/// File descriptor for the pre-opened crash file.
|
||||
static CRASH_FD: AtomicI32 = AtomicI32::new(-1);
|
||||
|
||||
/// Pre-allocated write buffer (lives in .bss, zero cost when not crashing).
|
||||
static mut CRASH_BUF: [u8; MAX_FILE_SIZE] = [0; MAX_FILE_SIZE];
|
||||
|
||||
/// Saved original terminal state for restoration in the signal handler.
|
||||
static mut ORIGINAL_TERMIOS: libc::termios = unsafe { std::mem::zeroed() };
|
||||
|
||||
/// Whether we successfully saved the original termios.
|
||||
static mut HAS_TERMIOS: bool = false;
|
||||
|
||||
/// Application version string, set at install time.
|
||||
static mut APP_VERSION: [u8; format::VERSION_STRING_LEN] = [0; format::VERSION_STRING_LEN];
|
||||
|
||||
/// Alternate signal stack memory (16 KiB via mmap).
|
||||
const ALT_STACK_SIZE: usize = 16 * 1024;
|
||||
|
||||
/// Guards against double-allocating the alternate signal stack when
|
||||
/// [`install_terminal_restore_only`] is followed by [`install`].
|
||||
static ALT_STACK_INSTALLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Save the current terminal state for restoration in signal handlers.
|
||||
fn save_termios() {
|
||||
unsafe {
|
||||
let termios = &mut *std::ptr::addr_of_mut!(ORIGINAL_TERMIOS);
|
||||
if libc::tcgetattr(0, termios) == 0 {
|
||||
*std::ptr::addr_of_mut!(HAS_TERMIOS) = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate an alternate signal stack via mmap (survives stack overflow).
|
||||
///
|
||||
/// No-op if already installed (idempotent across
|
||||
/// [`install_terminal_restore_only`] → [`install`] sequences).
|
||||
fn setup_alt_stack() {
|
||||
if ALT_STACK_INSTALLED.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let stack_mem = libc::mmap(
|
||||
std::ptr::null_mut(),
|
||||
ALT_STACK_SIZE,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
|
||||
-1,
|
||||
0,
|
||||
);
|
||||
if stack_mem != libc::MAP_FAILED {
|
||||
let ss = libc::stack_t {
|
||||
ss_sp: stack_mem,
|
||||
ss_flags: 0,
|
||||
ss_size: ALT_STACK_SIZE,
|
||||
};
|
||||
libc::sigaltstack(&ss, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore termios and re-raise. No escape codes.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Must only be called from a signal handler context.
|
||||
unsafe fn restore_termios_and_reraise(sig: libc::c_int) {
|
||||
unsafe {
|
||||
if *std::ptr::addr_of!(HAS_TERMIOS) {
|
||||
libc::tcsetattr(0, libc::TCSANOW, std::ptr::addr_of!(ORIGINAL_TERMIOS));
|
||||
}
|
||||
let mut sa: libc::sigaction = std::mem::zeroed();
|
||||
sa.sa_sigaction = libc::SIG_DFL;
|
||||
sa.sa_flags = 0;
|
||||
libc::sigemptyset(&mut sa.sa_mask);
|
||||
libc::sigaction(sig, &sa, std::ptr::null_mut());
|
||||
libc::raise(sig);
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore terminal escape codes + termios, then re-raise.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Must only be called from a signal handler context.
|
||||
unsafe fn restore_terminal_and_reraise(sig: libc::c_int) {
|
||||
unsafe {
|
||||
terminal::restore_in_signal_handler();
|
||||
restore_termios_and_reraise(sig);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a signal handler for SIGBUS and SIGSEGV.
|
||||
///
|
||||
/// Flags: `SA_SIGINFO | SA_ONSTACK | SA_RESETHAND`. `SA_RESETHAND`
|
||||
/// resets disposition to `SIG_DFL` after delivery, preventing recursive
|
||||
/// faults in the handler from looping.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `handler` must be a valid `sa_sigaction`-compatible function pointer.
|
||||
unsafe fn register_crash_signals(
|
||||
handler: unsafe extern "C" fn(libc::c_int, *mut libc::siginfo_t, *mut libc::c_void),
|
||||
) {
|
||||
unsafe {
|
||||
let mut sa: libc::sigaction = std::mem::zeroed();
|
||||
sa.sa_sigaction = handler as *const () as usize;
|
||||
sa.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK | libc::SA_RESETHAND;
|
||||
libc::sigemptyset(&mut sa.sa_mask);
|
||||
|
||||
libc::sigaction(libc::SIGBUS, &sa, std::ptr::null_mut());
|
||||
libc::sigaction(libc::SIGSEGV, &sa, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal handler: restore termios only (no escape codes), then re-raise.
|
||||
unsafe extern "C" fn terminal_restore_handler_basic(
|
||||
sig: libc::c_int,
|
||||
_info: *mut libc::siginfo_t,
|
||||
_ctx: *mut libc::c_void,
|
||||
) {
|
||||
unsafe {
|
||||
restore_termios_and_reraise(sig);
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal handler: restore escape codes + termios, then re-raise.
|
||||
unsafe extern "C" fn terminal_restore_handler(
|
||||
sig: libc::c_int,
|
||||
_info: *mut libc::siginfo_t,
|
||||
_ctx: *mut libc::c_void,
|
||||
) {
|
||||
unsafe {
|
||||
restore_terminal_and_reraise(sig);
|
||||
}
|
||||
}
|
||||
|
||||
/// Write crash blob to the pre-opened fd. Shared by crash handler variants.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Signal handler context. Only async-signal-safe operations.
|
||||
unsafe fn write_crash_blob(
|
||||
sig: libc::c_int,
|
||||
info: *mut libc::siginfo_t,
|
||||
ctx: *mut libc::c_void,
|
||||
) {
|
||||
unsafe {
|
||||
let fd = CRASH_FD.load(Ordering::Relaxed);
|
||||
if fd >= 0 {
|
||||
let si_code = if !info.is_null() { (*info).si_code } else { 0 };
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let si_addr = if !info.is_null() {
|
||||
(*info).si_addr as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
let si_addr = if !info.is_null() {
|
||||
(*info).si_addr() as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
let si_addr: u64 = 0;
|
||||
|
||||
let pid = libc::getpid() as u32;
|
||||
let timestamp = libc::time(std::ptr::null_mut()) as u64;
|
||||
|
||||
let mut frames: [usize; MAX_FRAMES] = [0; MAX_FRAMES];
|
||||
let mut n_frames: u16 = 0;
|
||||
let buf = &mut *std::ptr::addr_of_mut!(CRASH_BUF);
|
||||
let version = &*std::ptr::addr_of!(APP_VERSION);
|
||||
|
||||
let (crash_pc, crash_fp) = extract_pc_and_fp(ctx);
|
||||
if crash_pc != 0 {
|
||||
frames[0] = crash_pc;
|
||||
n_frames = 1;
|
||||
}
|
||||
|
||||
// Write the blob with the crash PC before walking frames.
|
||||
// Frame walking dereferences arbitrary pointers and can fault;
|
||||
// SA_RESETHAND would kill us without writing anything.
|
||||
let mut offset = format::writer::write_header(
|
||||
buf, sig as u8, si_code, si_addr, pid, timestamp, n_frames, version,
|
||||
);
|
||||
for frame in frames.iter().take(n_frames as usize) {
|
||||
offset = format::writer::write_frame(buf, offset, *frame);
|
||||
}
|
||||
libc::write(fd, buf.as_ptr() as *const libc::c_void, offset);
|
||||
|
||||
// Best-effort: walk frame pointers for additional context.
|
||||
// If this faults, the 1-frame blob above is already on disk.
|
||||
if crash_fp != 0 && crash_pc != 0 {
|
||||
let walked = walk_frame_pointers(crash_fp, &mut frames[1..], MAX_FRAMES - 1);
|
||||
if walked > 0 {
|
||||
n_frames += walked as u16;
|
||||
let mut offset = format::writer::write_header(
|
||||
buf, sig as u8, si_code, si_addr, pid, timestamp, n_frames, version,
|
||||
);
|
||||
for frame in frames.iter().take(n_frames as usize) {
|
||||
offset = format::writer::write_frame(buf, offset, *frame);
|
||||
}
|
||||
libc::lseek(fd, 0, libc::SEEK_SET);
|
||||
libc::write(fd, buf.as_ptr() as *const libc::c_void, offset);
|
||||
}
|
||||
}
|
||||
|
||||
CRASH_FD.store(-1, Ordering::Relaxed);
|
||||
libc::close(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Crash handler: blob + termios only (no escape codes).
|
||||
unsafe extern "C" fn crash_handler_basic(
|
||||
sig: libc::c_int,
|
||||
info: *mut libc::siginfo_t,
|
||||
ctx: *mut libc::c_void,
|
||||
) {
|
||||
unsafe {
|
||||
libc::alarm(3);
|
||||
write_crash_blob(sig, info, ctx);
|
||||
restore_termios_and_reraise(sig);
|
||||
}
|
||||
}
|
||||
|
||||
/// Crash handler: blob + escape codes + termios.
|
||||
unsafe extern "C" fn crash_handler(
|
||||
sig: libc::c_int,
|
||||
info: *mut libc::siginfo_t,
|
||||
ctx: *mut libc::c_void,
|
||||
) {
|
||||
unsafe {
|
||||
libc::alarm(3);
|
||||
write_crash_blob(sig, info, ctx);
|
||||
restore_terminal_and_reraise(sig);
|
||||
}
|
||||
}
|
||||
|
||||
/// Install a minimal SIGSEGV/SIGBUS handler that restores termios on crash.
|
||||
///
|
||||
/// Does NOT write terminal escape codes — call
|
||||
/// [`enable_terminal_escape_restore`] after TUI modes are enabled.
|
||||
///
|
||||
/// If [`install`] is called later, it replaces these handlers.
|
||||
pub fn install_terminal_restore_only() {
|
||||
save_termios();
|
||||
setup_alt_stack();
|
||||
unsafe { register_crash_signals(terminal_restore_handler_basic) };
|
||||
}
|
||||
|
||||
/// Install the crash handler. Must be called early in `main()`, before any
|
||||
/// terminal initialization or async runtime setup.
|
||||
pub fn install(crash_dir: &Path, grok_version: &str) -> bool {
|
||||
let crash_file = crash_dir.join("last-crash.bin");
|
||||
|
||||
// Create the crash directory if it doesn't exist.
|
||||
if std::fs::create_dir_all(crash_dir).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Open crash file (pre-opened fd for the signal handler).
|
||||
let c_path = match CString::new(crash_file.as_os_str().as_bytes()) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let fd = unsafe {
|
||||
libc::open(
|
||||
c_path.as_ptr(),
|
||||
libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC,
|
||||
0o644,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
return false;
|
||||
}
|
||||
CRASH_FD.store(fd, Ordering::Relaxed);
|
||||
|
||||
// Store version string.
|
||||
unsafe {
|
||||
let version = &mut *std::ptr::addr_of_mut!(APP_VERSION);
|
||||
version.fill(0);
|
||||
let copy_len = grok_version.len().min(format::VERSION_STRING_LEN);
|
||||
version[..copy_len].copy_from_slice(&grok_version.as_bytes()[..copy_len]);
|
||||
}
|
||||
|
||||
save_termios();
|
||||
setup_alt_stack();
|
||||
unsafe { register_crash_signals(crash_handler_basic) };
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Upgrade SIGSEGV/SIGBUS handlers to include terminal escape code
|
||||
/// restoration. Call when TUI modes are enabled.
|
||||
pub fn enable_terminal_escape_restore() {
|
||||
unsafe {
|
||||
register_crash_signals(if CRASH_FD.load(Ordering::Relaxed) >= 0 {
|
||||
crash_handler
|
||||
} else {
|
||||
terminal_restore_handler
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Downgrade SIGSEGV/SIGBUS handlers to termios-only restoration.
|
||||
/// Call when TUI modes are disabled.
|
||||
pub fn disable_terminal_escape_restore() {
|
||||
unsafe {
|
||||
register_crash_signals(if CRASH_FD.load(Ordering::Relaxed) >= 0 {
|
||||
crash_handler_basic
|
||||
} else {
|
||||
terminal_restore_handler_basic
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub use imp::{
|
||||
disable_terminal_escape_restore, enable_terminal_escape_restore, install,
|
||||
install_terminal_restore_only,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
mod win {
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicPtr, Ordering};
|
||||
|
||||
use crate::format::{self, MAX_FILE_SIZE, MAX_FRAMES};
|
||||
|
||||
static CRASH_HANDLE: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
|
||||
static mut CRASH_BUF: [u8; MAX_FILE_SIZE] = [0; MAX_FILE_SIZE];
|
||||
static mut APP_VERSION: [u8; format::VERSION_STRING_LEN] = [0; format::VERSION_STRING_LEN];
|
||||
|
||||
const EXCEPTION_ACCESS_VIOLATION: i32 = 0xC0000005_u32 as i32;
|
||||
const EXCEPTION_STACK_OVERFLOW: i32 = 0xC00000FD_u32 as i32;
|
||||
const EXCEPTION_IN_PAGE_ERROR: i32 = 0xC0000006_u32 as i32;
|
||||
const EXCEPTION_ILLEGAL_INSTRUCTION: i32 = 0xC000001D_u32 as i32;
|
||||
const EXCEPTION_ARRAY_BOUNDS_EXCEEDED: i32 = 0xC000008C_u32 as i32;
|
||||
const EXCEPTION_CONTINUE_SEARCH: i32 = 0;
|
||||
const INVALID_HANDLE_VALUE: *mut c_void = -1isize as *mut c_void;
|
||||
|
||||
// CreateFileW constants.
|
||||
const GENERIC_WRITE: u32 = 0x40000000;
|
||||
const CREATE_ALWAYS: u32 = 2;
|
||||
const FILE_ATTRIBUTE_NORMAL: u32 = 0x00000080;
|
||||
const FILE_BEGIN: u32 = 0;
|
||||
|
||||
/// Walk the frame-pointer chain, collecting return addresses.
|
||||
///
|
||||
/// [fp+0] = previous frame pointer, [fp+8] = return address.
|
||||
/// Stops at null, misaligned, or non-ascending frame pointers.
|
||||
unsafe fn walk_frame_pointers(initial_fp: usize, out: &mut [usize], max: usize) -> usize {
|
||||
let mut fp = initial_fp;
|
||||
let mut count = 0;
|
||||
|
||||
while count < max {
|
||||
if fp == 0 || fp < 4096 || !fp.is_multiple_of(core::mem::size_of::<usize>()) {
|
||||
break;
|
||||
}
|
||||
let prev_fp = unsafe { *(fp as *const usize) };
|
||||
let ret_addr = unsafe { *((fp + core::mem::size_of::<usize>()) as *const usize) };
|
||||
|
||||
if ret_addr == 0 || ret_addr < 4096 {
|
||||
break;
|
||||
}
|
||||
out[count] = ret_addr;
|
||||
count += 1;
|
||||
|
||||
if prev_fp <= fp {
|
||||
break;
|
||||
}
|
||||
fp = prev_fp;
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
/// Map Windows exception code to a Unix signal number for the blob format.
|
||||
fn exception_to_signal(code: i32) -> u8 {
|
||||
match code {
|
||||
EXCEPTION_IN_PAGE_ERROR => 7, // SIGBUS
|
||||
EXCEPTION_ILLEGAL_INSTRUCTION => 4, // SIGILL
|
||||
_ => 11, // SIGSEGV
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the exception code is a fatal memory/instruction fault that
|
||||
/// warrants crash handling.
|
||||
fn is_fatal_exception(code: i32) -> bool {
|
||||
matches!(
|
||||
code,
|
||||
EXCEPTION_ACCESS_VIOLATION
|
||||
| EXCEPTION_STACK_OVERFLOW
|
||||
| EXCEPTION_IN_PAGE_ERROR
|
||||
| EXCEPTION_ILLEGAL_INSTRUCTION
|
||||
| EXCEPTION_ARRAY_BOUNDS_EXCEEDED
|
||||
)
|
||||
}
|
||||
|
||||
unsafe extern "system" fn crash_handler(
|
||||
info: *const windows_sys::Win32::System::Diagnostics::Debug::EXCEPTION_POINTERS,
|
||||
) -> i32 {
|
||||
unsafe {
|
||||
if info.is_null() {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
let exception_record = (*info).ExceptionRecord;
|
||||
let context_record = (*info).ContextRecord;
|
||||
if exception_record.is_null() || context_record.is_null() {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
let exception_code = (*exception_record).ExceptionCode;
|
||||
if !is_fatal_exception(exception_code) {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
let handle = CRASH_HANDLE.load(Ordering::Relaxed);
|
||||
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
let signal = exception_to_signal(exception_code);
|
||||
let si_code = exception_code as i32;
|
||||
|
||||
// ExceptionInformation[1] holds the faulting address for ACCESS_VIOLATION.
|
||||
let si_addr = if exception_code == EXCEPTION_ACCESS_VIOLATION
|
||||
&& (*exception_record).NumberParameters >= 2
|
||||
{
|
||||
(*exception_record).ExceptionInformation[1] as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let pid = windows_sys::Win32::System::Threading::GetCurrentProcessId();
|
||||
|
||||
let mut ft = windows_sys::Win32::Foundation::FILETIME {
|
||||
dwLowDateTime: 0,
|
||||
dwHighDateTime: 0,
|
||||
};
|
||||
windows_sys::Win32::System::SystemInformation::GetSystemTimeAsFileTime(&mut ft);
|
||||
let win_ticks = (ft.dwHighDateTime as u64) << 32 | ft.dwLowDateTime as u64;
|
||||
// FILETIME epoch (1601) → Unix epoch (1970): 116444736000000000 100ns ticks.
|
||||
let timestamp = win_ticks.saturating_sub(116_444_736_000_000_000) / 10_000_000;
|
||||
|
||||
let mut frames: [usize; MAX_FRAMES] = [0; MAX_FRAMES];
|
||||
let mut n_frames: u16 = 0;
|
||||
let buf = &mut *std::ptr::addr_of_mut!(CRASH_BUF);
|
||||
let version = &*std::ptr::addr_of!(APP_VERSION);
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
let (crash_pc, crash_fp) = (
|
||||
(*context_record).Rip as usize,
|
||||
(*context_record).Rbp as usize,
|
||||
);
|
||||
// ARM64 Windows: capture PC only; frame-pointer walking is
|
||||
// unreliable without verifying the exact windows-sys CONTEXT layout.
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
let (crash_pc, crash_fp) = (0usize, 0usize);
|
||||
|
||||
if crash_pc != 0 {
|
||||
frames[0] = crash_pc;
|
||||
n_frames = 1;
|
||||
}
|
||||
|
||||
// Write crash PC blob first (frame walking can fault).
|
||||
let mut offset = format::writer::write_header(
|
||||
buf, signal, si_code, si_addr, pid, timestamp, n_frames, version,
|
||||
);
|
||||
for frame in frames.iter().take(n_frames as usize) {
|
||||
offset = format::writer::write_frame(buf, offset, *frame);
|
||||
}
|
||||
write_to_handle(handle, buf, offset);
|
||||
|
||||
// Best-effort: walk frame pointers for a full backtrace.
|
||||
if crash_fp != 0 && crash_pc != 0 {
|
||||
let walked = walk_frame_pointers(crash_fp, &mut frames[1..], MAX_FRAMES - 1);
|
||||
if walked > 0 {
|
||||
n_frames += walked as u16;
|
||||
let mut offset = format::writer::write_header(
|
||||
buf, signal, si_code, si_addr, pid, timestamp, n_frames, version,
|
||||
);
|
||||
for frame in frames.iter().take(n_frames as usize) {
|
||||
offset = format::writer::write_frame(buf, offset, *frame);
|
||||
}
|
||||
windows_sys::Win32::Storage::FileSystem::SetFilePointer(
|
||||
handle,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
FILE_BEGIN,
|
||||
);
|
||||
write_to_handle(handle, buf, offset);
|
||||
}
|
||||
}
|
||||
|
||||
CRASH_HANDLE.store(std::ptr::null_mut(), Ordering::Relaxed);
|
||||
windows_sys::Win32::Foundation::CloseHandle(handle);
|
||||
|
||||
EXCEPTION_CONTINUE_SEARCH
|
||||
}
|
||||
}
|
||||
|
||||
/// Crash handler with escape code restoration (TUI active).
|
||||
unsafe extern "system" fn crash_handler_with_terminal(
|
||||
info: *const windows_sys::Win32::System::Diagnostics::Debug::EXCEPTION_POINTERS,
|
||||
) -> i32 {
|
||||
let result = unsafe { crash_handler(info) };
|
||||
crate::terminal::restore_in_signal_handler();
|
||||
result
|
||||
}
|
||||
|
||||
unsafe fn write_to_handle(handle: *mut c_void, buf: &[u8], len: usize) {
|
||||
let mut written: u32 = 0;
|
||||
unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::WriteFile(
|
||||
handle,
|
||||
buf.as_ptr(),
|
||||
len as u32,
|
||||
&mut written,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal exception filter: no-op (no escape codes, no crash reporting).
|
||||
unsafe extern "system" fn terminal_restore_filter_basic(
|
||||
_info: *const windows_sys::Win32::System::Diagnostics::Debug::EXCEPTION_POINTERS,
|
||||
) -> i32 {
|
||||
EXCEPTION_CONTINUE_SEARCH
|
||||
}
|
||||
|
||||
/// Minimal exception filter: restore terminal escape codes (TUI active).
|
||||
unsafe extern "system" fn terminal_restore_filter(
|
||||
info: *const windows_sys::Win32::System::Diagnostics::Debug::EXCEPTION_POINTERS,
|
||||
) -> i32 {
|
||||
unsafe {
|
||||
if info.is_null() {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
let exception_record = (*info).ExceptionRecord;
|
||||
if exception_record.is_null() {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
let exception_code = (*exception_record).ExceptionCode;
|
||||
if !is_fatal_exception(exception_code) {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
crate::terminal::restore_in_signal_handler();
|
||||
EXCEPTION_CONTINUE_SEARCH
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install_terminal_restore_only() {
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Diagnostics::Debug::SetUnhandledExceptionFilter(Some(
|
||||
terminal_restore_filter_basic,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install(crash_dir: &Path, grok_version: &str) -> bool {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
let crash_file = crash_dir.join("last-crash.bin");
|
||||
|
||||
if std::fs::create_dir_all(crash_dir).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let wide_path: Vec<u16> = crash_file
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
let handle = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::CreateFileW(
|
||||
wide_path.as_ptr(),
|
||||
GENERIC_WRITE,
|
||||
0,
|
||||
std::ptr::null(),
|
||||
CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
|
||||
return false;
|
||||
}
|
||||
CRASH_HANDLE.store(handle, Ordering::Relaxed);
|
||||
|
||||
unsafe {
|
||||
let version = &mut *std::ptr::addr_of_mut!(APP_VERSION);
|
||||
version.fill(0);
|
||||
let copy_len = grok_version.len().min(format::VERSION_STRING_LEN);
|
||||
version[..copy_len].copy_from_slice(&grok_version.as_bytes()[..copy_len]);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Diagnostics::Debug::SetUnhandledExceptionFilter(Some(
|
||||
crash_handler,
|
||||
));
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn enable_terminal_escape_restore() {
|
||||
unsafe {
|
||||
let filter = if !CRASH_HANDLE.load(Ordering::Relaxed).is_null() {
|
||||
crash_handler_with_terminal
|
||||
} else {
|
||||
terminal_restore_filter
|
||||
};
|
||||
windows_sys::Win32::System::Diagnostics::Debug::SetUnhandledExceptionFilter(Some(
|
||||
filter,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable_terminal_escape_restore() {
|
||||
unsafe {
|
||||
let filter = if !CRASH_HANDLE.load(Ordering::Relaxed).is_null() {
|
||||
crash_handler
|
||||
} else {
|
||||
terminal_restore_filter_basic
|
||||
};
|
||||
windows_sys::Win32::System::Diagnostics::Debug::SetUnhandledExceptionFilter(Some(
|
||||
filter,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub use win::{
|
||||
disable_terminal_escape_restore, enable_terminal_escape_restore, install,
|
||||
install_terminal_restore_only,
|
||||
};
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
pub fn install(_crash_dir: &std::path::Path, _app_version: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
pub fn install_terminal_restore_only() {}
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
pub fn enable_terminal_escape_restore() {}
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
pub fn disable_terminal_escape_restore() {}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use std::sync::Mutex;
|
||||
|
||||
// SIGSEGV/SIGBUS handlers are process-global. Tests in this binary run on
|
||||
// parallel threads, so any two tests that install/read these handlers race.
|
||||
// Serialize them through this lock (poison-tolerant: a real assertion
|
||||
// failure in one test must not cascade into the other).
|
||||
static SIGNAL_STATE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn install_terminal_restore_only_registers_handlers() {
|
||||
let _guard = SIGNAL_STATE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
super::install_terminal_restore_only();
|
||||
unsafe {
|
||||
let mut sa: libc::sigaction = std::mem::zeroed();
|
||||
|
||||
assert_eq!(libc::sigaction(libc::SIGSEGV, std::ptr::null(), &mut sa), 0);
|
||||
assert_ne!(
|
||||
sa.sa_sigaction,
|
||||
libc::SIG_DFL,
|
||||
"SIGSEGV handler should not be SIG_DFL after install"
|
||||
);
|
||||
assert_ne!(
|
||||
sa.sa_flags & libc::SA_ONSTACK,
|
||||
0,
|
||||
"SIGSEGV handler must use alternate signal stack"
|
||||
);
|
||||
// Note: SA_RESETHAND is set in our sigaction call but macOS XNU
|
||||
// does not round-trip it through the sigaction query — the kernel
|
||||
// stores it in ps_sigreset internally but returns sa_flags=0x41
|
||||
// (SA_SIGINFO|SA_ONSTACK only). The flag IS honored for signal
|
||||
// delivery. Verified via the integration test
|
||||
// `sigsegv_produces_valid_crash_blob` which relies on SA_RESETHAND
|
||||
// to re-raise with SIG_DFL after the handler runs.
|
||||
|
||||
assert_eq!(libc::sigaction(libc::SIGBUS, std::ptr::null(), &mut sa), 0);
|
||||
assert_ne!(
|
||||
sa.sa_sigaction,
|
||||
libc::SIG_DFL,
|
||||
"SIGBUS handler should not be SIG_DFL after install"
|
||||
);
|
||||
assert_ne!(
|
||||
sa.sa_flags & libc::SA_ONSTACK,
|
||||
0,
|
||||
"SIGBUS handler must use alternate signal stack"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_install_replaces_minimal_handler() {
|
||||
let _guard = SIGNAL_STATE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
super::install_terminal_restore_only();
|
||||
|
||||
let handler_before = unsafe {
|
||||
let mut sa: libc::sigaction = std::mem::zeroed();
|
||||
libc::sigaction(libc::SIGSEGV, std::ptr::null(), &mut sa);
|
||||
sa.sa_sigaction
|
||||
};
|
||||
|
||||
let dir = std::env::temp_dir().join("xai-crash-handler-test-replace");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
super::install(&dir, "test-version");
|
||||
|
||||
let handler_after = unsafe {
|
||||
let mut sa: libc::sigaction = std::mem::zeroed();
|
||||
libc::sigaction(libc::SIGSEGV, std::ptr::null(), &mut sa);
|
||||
sa.sa_sigaction
|
||||
};
|
||||
|
||||
assert_ne!(
|
||||
handler_after, handler_before,
|
||||
"full install should replace the minimal handler"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue