//! 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::()) { 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::()) 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 = 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::()) { break; } let prev_fp = unsafe { *(fp as *const usize) }; let ret_addr = unsafe { *((fp + core::mem::size_of::()) 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 = 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" ); } }