From 54a44621e89139b6d1531709c1b2da44c7f9962f Mon Sep 17 00:00:00 2001 From: Dominik Behr Date: Tue, 8 Sep 2026 05:36:43 -0700 Subject: [PATCH] crash_diag: log silent Windows deaths (0xC000041D) to iris-crash.log The native CLI dies on some Windows machines during startup with exit code 0xC000041D (STATUS_FATAL_USER_CALLBACK_EXCEPTION) and no message: the fault happens on the stack of a Win32 callback (winit's wndproc), and 64-bit Windows re-raises any exception that escapes a kernel callback as that fixed status, discarding the original cause. The release profile is panic = "abort", so a Rust panic there also prints nothing useful. This is the shape of issue #94. src/crash_diag.rs, installed first thing in main(): * panic hook - runs even under panic=abort, before the abort; writes message + thread + location + backtrace. * Windows vectored exception handler - runs first-chance, before the callback dispatcher swallows the exception; logs the real code, faulting address and a module+offset stack for access violations, illegal/privileged instructions, stack overflow, heap corruption and the fatal-callback status itself. * Windows SetUnhandledExceptionFilter as a backstop. Everything is appended to iris-crash.log and echoed to stderr. Dormant until something crashes - no steady-state cost (verified: no polling thread, no allocation, handlers only entered on an actual OS exception, never for Result::Err / None / release overflow). Self-test: IRIS_CRASH_SELFTEST=panic|thread|segv. Escape hatch if the VEH ever gets noisy: IRIS_CRASH_DIAG=off (panic hook stays). rules/gui/windows-silent-exit-0xc000041d.md records the investigation: what 0xC000041D means, how to symbolize the report, and which re-entrancy theories were ruled out (vendored winit 0.30.13 buffers the re-entrant request_inner_size path; not reproducible on a desktop; not fixed upstream through winit 0.31-beta). Co-Authored-By: Claude Sonnet 5 --- Cargo.toml | 7 +- rules/gui/windows-silent-exit-0xc000041d.md | 88 ++++ src/crash_diag.rs | 447 ++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 6 + 5 files changed, 548 insertions(+), 1 deletion(-) create mode 100644 rules/gui/windows-silent-exit-0xc000041d.md create mode 100644 src/crash_diag.rs diff --git a/Cargo.toml b/Cargo.toml index e48991ab..272dfe3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -277,7 +277,12 @@ nokhwa = { version = "0.10", features = ["input-msmf"], optional = true } # Win32_System_Threading. It used to arrive by feature unification from rfd / # socket2 / anstyle-wincon; declare it so the Windows build doesn't depend on # who else happens to be in the graph. -windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_System_Memory", "Win32_System_SystemInformation", "Win32_Security"] } +# crash_diag.rs installs a vectored exception handler + unhandled-exception +# filter (Win32_System_Diagnostics_Debug: AddVectoredExceptionHandler, +# SetUnhandledExceptionFilter, RtlCaptureStackBackTrace, EXCEPTION_POINTERS) and +# resolves stack frames to module+offset (Win32_System_LibraryLoader: +# GetModuleHandleExW / GetModuleFileNameW). +windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_System_Memory", "Win32_System_SystemInformation", "Win32_Security", "Win32_System_Diagnostics_Debug", "Win32_System_Kernel", "Win32_System_LibraryLoader"] } [target.'cfg(not(windows))'.dependencies] libc = "0.2" diff --git a/rules/gui/windows-silent-exit-0xc000041d.md b/rules/gui/windows-silent-exit-0xc000041d.md new file mode 100644 index 00000000..f1955feb --- /dev/null +++ b/rules/gui/windows-silent-exit-0xc000041d.md @@ -0,0 +1,88 @@ +# Windows: CLI vanishes with exit 0xC000041D and no message + +**Keywords:** windows,winit,0.30,0.30.13,crash,STATUS_FATAL_USER_CALLBACK_EXCEPTION,0xC000041D,-1073740771,panic=abort,wndproc,window procedure,vectored exception handler,VEH,access violation,request_inner_size,SetWindowPos,WM_SIZE,re-entrant event handler,event loop,aspect ratio,issue #94,iris-crash.log,crash_diag +**Category:** gui + +## Symptom + +On some Windows machines the native (winit) CLI dies during startup — right +after `Rex3: Resolution changed to 1282x1024` — with exit code `0xC000041D` +(`-1073740771`, `STATUS_FATAL_USER_CALLBACK_EXCEPTION`) and **no** Rust panic +message, no backtrace, no window. `iris-gui` (eframe) is unaffected. +issue #94: ~90% of launches on one reporter's internal laptop panel, 0% on an +external monitor; unaffected by DPI scale. + +## What that exit code means + +`0xC000041D` is what 64-bit Windows raises when an exception escapes a +**user-mode callback invoked by the kernel** — a window procedure, hook proc, +`Enum*` callback. The kernel's callback dispatcher catches the original +exception, tears down the callback frame, and re-raises this fixed status. The +original cause (a Rust panic, or a native access violation in a driver) is gone +by the time any top-level `SetUnhandledExceptionFilter` runs. + +Because the release profile is `panic = "abort"`, a Rust panic on a wndproc +stack aborts immediately — the default hook prints the message but it is easily +lost, and there is no unwind and no backtrace unless `RUST_BACKTRACE` is set. + +## Getting a diagnosis + +`src/crash_diag.rs` (installed first thing in `main`) exists for exactly this: + +* **Panic hook** — fires even under `panic = "abort"`, before the abort. Writes + message + thread + location + backtrace to `iris-crash.log`. +* **Vectored exception handler** (Windows) — runs *first-chance*, before the + callback dispatcher swallows anything. Logs the real exception code, faulting + address and a `module+offset` stack for access violations / illegal + instructions / stack overflow / heap corruption / the fatal-callback status + itself. + +Ask a reporter to reproduce once and attach `iris-crash.log`. Symbolize the +`iris.exe+0xNNNN` frames against the matching build with +`addr2line -e iris.exe -f -C 0xNNNN` (or the `.pdb`). + +Self-test the wiring: `IRIS_CRASH_SELFTEST=panic|thread|segv target/release/iris.exe`. +Escape hatch if the VEH ever gets noisy: `IRIS_CRASH_DIAG=off` (panic hook stays). + +## What is NOT the cause (verified — don't re-investigate) + +The obvious theory was re-entrancy: `WindowEvent::Resized`'s aspect-ratio lock +calls `Window::request_inner_size` from *inside* the winit event callback; on +Windows that reaches `SetWindowPos` synchronously, which re-enters the wndproc +with `WM_SIZE`. + +* The **REX3-refresh-thread** `request_inner_size` in `GlRenderer::resize` is + *not* it — cross-thread `SetWindowPos` uses `SWP_ASYNCWINDOWPOS`, so the + resize is posted, never re-entrant. (Commit c2e085a's rationale is imprecise + on this point.) +* The **event-thread** re-entrant `request_inner_size` is buffered, not + crashed: vendored winit 0.30.13's `EventLoopRunner::should_buffer()` detects + the taken handler and defers the nested `WindowEvent`. Forcing an *infinite* + re-entrant `request_inner_size` from the `Resized` handler just ping-pongs + the window ±1px forever without panicking. +* Could not reproduce on a desktop (single 2560×1440 @ 96 DPI) across ~130 + launches: delayed cross-thread resize, spammed resize, forced work-area + clamp, forced infinite re-entrancy — all 0 crashes. +* The fix commits (c2e085a + 81c34ba) do **not** stop the reporter's crash. + +## Upstream winit status + +Not fixed. 0.30.13 is the last 0.30.x. `v0.31.0-beta.3` keeps the identical +`call_event_handler` re-entrancy assert (`"either event handler is re-entrant +(likely)…"`) and the identical `send_event` path that dispatches +`RedrawRequested` **directly, bypassing `should_buffer`** — the one genuine +re-entrancy hole. No changelog entry addresses Windows re-entrancy from +`request_inner_size`/`WM_SIZE`. Upstream's pattern for this class is deferral: +the `pending_drag` / `source_drag` fields were added so the blocking +`DoDragDrop` runs only after the app returns control to winit. Any iris fix +should follow suit — never call a synchronous window-mutating method from +inside a winit callback; queue it and apply it from outside dispatch. + +## Working hypothesis + +Given the panel dependency and that `panic = "unwind"` + `RUST_BACKTRACE=full` +produced *no* Rust output, the fault is most likely **not a Rust panic** but a +native access violation inside the wndproc — the GL ICD mishandling the HWND/DC +while `GlRenderer::ensure_init` creates the window surface on the REX3 thread at +the same moment the event thread services the mode-change resize. The VEH in +`crash_diag.rs` is what will confirm or refute this from a reporter's machine. diff --git a/src/crash_diag.rs b/src/crash_diag.rs new file mode 100644 index 00000000..a116095e --- /dev/null +++ b/src/crash_diag.rs @@ -0,0 +1,447 @@ +//! Crash diagnostics — turn "iris just vanished with no message" into a log file. +//! +//! Two independent failure classes have bitten the CLI on Windows: +//! +//! * **Rust panics.** The release profile is `panic = "abort"`, so a panic +//! prints its message (the default hook still runs) and then `abort()`s with +//! no unwinding and no backtrace unless `RUST_BACKTRACE` is set. If the +//! panic happens on the stack of a Win32 callback (winit's window +//! procedure), even the message can be lost in the noise and the exit code +//! is `0xC000041D` (`STATUS_FATAL_USER_CALLBACK_EXCEPTION`). +//! +//! * **Non-Rust faults inside a callback** — e.g. an access violation in a GL +//! driver reached from `WM_SIZE`/`WM_PAINT`. 64-bit Windows does not run +//! `SetUnhandledExceptionFilter` for these (the kernel's user-callback +//! dispatcher swallows the original exception and re-raises it as +//! `0xC000041D`), so a plain top-level filter never sees the real cause. +//! This is the shape of issue #94. +//! +//! [`install`] wires up handlers for both. Everything is appended to +//! `iris-crash.log` in the working directory and echoed to stderr. All of it is +//! dormant until something actually crashes — no hot-path cost (see the note on +//! [`install`]). +//! +//! Limitation: the exception handler formats into a fixed stack buffer but +//! still writes the file through `std::fs`, so a *heap-corruption* or +//! *stack-overflow* fault may not manage to produce a report. Access +//! violations — the expected shape of #94 — are fine. + +use std::sync::atomic::{AtomicBool, Ordering}; + +/// Install the panic hook and (on Windows) the exception handlers. +/// +/// Call once, as early in `main` as possible. +/// +/// ## Performance +/// +/// Zero steady-state cost. The panic hook only runs on `panic!`; the Windows +/// vectored exception handler is only entered when the OS raises an exception, +/// which does not happen on any normal code path (it is *not* invoked for +/// `Result::Err`, `None`, integer overflow in release, etc.). No polling +/// thread, no allocation, no syscalls after `install` returns. +pub fn install() { + install_panic_hook(); + #[cfg(windows)] + { + // Escape hatch: `IRIS_CRASH_DIAG=off` skips the Windows exception + // handlers (the panic hook always stays). Only needed if some + // dependency ever turns out to use first-chance access violations as + // control flow and spams the log. + if std::env::var("IRIS_CRASH_DIAG").as_deref() != Ok("off") { + windows::install_exception_handlers(); + } + } +} + +/// Path we append crash reports to. +const LOG_PATH: &str = "iris-crash.log"; + +/// Deliberately crash, to prove the handlers are wired up. Driven by +/// `IRIS_CRASH_SELFTEST` from `main`: +/// * `panic` — a normal `panic!` (exercises the panic hook) +/// * `segv` — a null dereference (exercises the vectored handler) +/// * `thread` — a panic on a spawned, named thread +/// +/// After running it, check that `iris-crash.log` gained an entry. +pub fn selftest(kind: &str) { + match kind { + "panic" => panic!("crash_diag self-test: deliberate panic on the main thread"), + "thread" => { + let h = std::thread::Builder::new() + .name("selftest-victim".into()) + .spawn(|| panic!("crash_diag self-test: deliberate panic on a worker thread")) + .unwrap(); + let _ = h.join(); + // If we get here the build is `panic = "unwind"` (the worker's + // panic only killed the worker). A `panic = "abort"` release build + // would have taken the whole process down at the panic above — + // which is the point of this variant. Don't fall through into a + // full emulator boot. + eprintln!( + "crash_diag: worker thread panicked and the process survived \ + (panic=unwind build); a release build would have aborted here" + ); + std::process::exit(70); + } + "segv" => unsafe { + // Write through a null pointer -> STATUS_ACCESS_VIOLATION. + let p: *mut u8 = std::ptr::null_mut(); + std::ptr::write_volatile(p, 1); + }, + other => eprintln!("crash_diag: unknown IRIS_CRASH_SELFTEST={other:?} (want panic|thread|segv)"), + } +} + +fn install_panic_hook() { + let default_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + // Guard against a panic *inside* the hook (e.g. from the backtrace + // machinery) turning into an infinite recursion. + static IN_HOOK: AtomicBool = AtomicBool::new(false); + if IN_HOOK.swap(true, Ordering::SeqCst) { + default_hook(info); + return; + } + + let thread = std::thread::current(); + let bt = std::backtrace::Backtrace::force_capture(); + let report = format!( + "\n================ iris panic ================\n\ + when : {}\n\ + thread : {}\n\ + location: {}\n\ + message: {}\n\ + backtrace:\n{bt}\n\ + ===========================================\n", + timestamp(), + thread.name().unwrap_or(""), + info.location().map(|l| l.to_string()).unwrap_or_else(|| "".into()), + payload_str(info), + ); + + append_to_log(report.as_bytes()); + eprint!("{report}"); + use std::io::Write; + let _ = std::io::stderr().flush(); + + IN_HOOK.store(false, Ordering::SeqCst); + default_hook(info); + })); +} + +fn payload_str<'a>(info: &'a std::panic::PanicHookInfo<'a>) -> &'a str { + let p = info.payload(); + if let Some(s) = p.downcast_ref::<&str>() { + s + } else if let Some(s) = p.downcast_ref::() { + s.as_str() + } else { + "" + } +} + +/// Best-effort "seconds since the Unix epoch" stamp. Avoids pulling a date +/// crate into the binary just for a log header; correlate with the log file's +/// mtime if you need wall-clock. +fn timestamp() -> String { + use std::time::{SystemTime, UNIX_EPOCH}; + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(d) => format!("unix+{}.{:03}s", d.as_secs(), d.subsec_millis()), + Err(_) => "unix+?".into(), + } +} + +/// Append bytes to `iris-crash.log`, creating it if needed. Silent on failure — +/// we are on a crash path and there is nothing useful to do if this fails. +fn append_to_log(bytes: &[u8]) { + use std::io::Write; + if let Ok(mut f) = + std::fs::OpenOptions::new().create(true).append(true).open(LOG_PATH) + { + let _ = f.write_all(bytes); + let _ = f.flush(); + } +} + +#[cfg(windows)] +mod windows { + use super::{append_to_log, LOG_PATH}; + use std::sync::atomic::{AtomicBool, Ordering}; + + use core::fmt::Write as _; + + use windows_sys::Win32::Foundation::HMODULE; + use windows_sys::Win32::System::Diagnostics::Debug::{ + AddVectoredExceptionHandler, RtlCaptureStackBackTrace, SetUnhandledExceptionFilter, + EXCEPTION_POINTERS, + }; + use windows_sys::Win32::System::LibraryLoader::{ + GetModuleFileNameW, GetModuleHandleExW, GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + }; + use windows_sys::Win32::System::SystemInformation::GetTickCount64; + + // From . windows-sys spreads these across modules / doesn't re-export + // them all as consts, so spell them out. + const EXCEPTION_CONTINUE_SEARCH: i32 = 0; + const STATUS_ACCESS_VIOLATION: u32 = 0xC000_0005; + const STATUS_IN_PAGE_ERROR: u32 = 0xC000_0006; + const STATUS_ILLEGAL_INSTRUCTION: u32 = 0xC000_001D; + const STATUS_PRIVILEGED_INSTRUCTION: u32 = 0xC000_0096; + const STATUS_STACK_OVERFLOW: u32 = 0xC000_00FD; + const STATUS_FATAL_USER_CALLBACK_EXCEPTION: u32 = 0xC000_041D; + const STATUS_HEAP_CORRUPTION: u32 = 0xC000_0374; + + /// Codes worth a report. Deliberately excludes the subset of first-chance + /// exceptions that normal code handles on purpose (`0xE06D7363` C++ + /// exceptions, `0x406D1388` the thread-name marker, DBG_* control codes, + /// etc.) so a diagnostic build doesn't cry wolf. + fn is_interesting(code: u32) -> bool { + matches!( + code, + STATUS_ACCESS_VIOLATION + | STATUS_IN_PAGE_ERROR + | STATUS_ILLEGAL_INSTRUCTION + | STATUS_PRIVILEGED_INSTRUCTION + | STATUS_STACK_OVERFLOW + | STATUS_FATAL_USER_CALLBACK_EXCEPTION + | STATUS_HEAP_CORRUPTION + ) + } + + fn code_name(code: u32) -> &'static str { + match code { + STATUS_ACCESS_VIOLATION => "ACCESS_VIOLATION", + STATUS_IN_PAGE_ERROR => "IN_PAGE_ERROR", + STATUS_ILLEGAL_INSTRUCTION => "ILLEGAL_INSTRUCTION", + STATUS_PRIVILEGED_INSTRUCTION => "PRIVILEGED_INSTRUCTION", + STATUS_STACK_OVERFLOW => "STACK_OVERFLOW", + STATUS_FATAL_USER_CALLBACK_EXCEPTION => "FATAL_USER_CALLBACK_EXCEPTION", + STATUS_HEAP_CORRUPTION => "HEAP_CORRUPTION", + _ => "?", + } + } + + pub fn install_exception_handlers() { + unsafe { + // First arg != 0 => call us *first*, ahead of any other vectored + // handler and, crucially, before the frame-based dispatch that on + // x64 would otherwise swallow a fault that escaped a kernel + // callback and re-raise it as 0xC000041D with the original cause + // gone. + let _ = AddVectoredExceptionHandler(1, Some(vectored_handler)); + let _ = SetUnhandledExceptionFilter(Some(unhandled_filter)); + } + } + + /// Vectored handler: sees every exception first-chance. We report the + /// interesting ones exactly once, then always return CONTINUE_SEARCH so the + /// normal handling path is unchanged (if something legitimately handles the + /// exception, we merely logged a spurious line; if nothing does, the + /// process dies as before — but now with a report). + unsafe extern "system" fn vectored_handler(info: *mut EXCEPTION_POINTERS) -> i32 { + if info.is_null() { + return EXCEPTION_CONTINUE_SEARCH; + } + let rec = (*info).ExceptionRecord; + if rec.is_null() { + return EXCEPTION_CONTINUE_SEARCH; + } + let code = (*rec).ExceptionCode as u32; + if !is_interesting(code) { + return EXCEPTION_CONTINUE_SEARCH; + } + + // Only the first interesting fault gets a full dump. A second one is + // very likely our own stack-walk touching bad memory. + static DUMPED: AtomicBool = AtomicBool::new(false); + if DUMPED.swap(true, Ordering::SeqCst) { + return EXCEPTION_CONTINUE_SEARCH; + } + + let fault_addr = (*rec).ExceptionAddress as usize; + let (rw, data_addr) = if (*rec).NumberParameters >= 2 + && matches!(code, STATUS_ACCESS_VIOLATION | STATUS_IN_PAGE_ERROR) + { + let kind = match (*rec).ExceptionInformation[0] { + 0 => "read", + 1 => "write", + 8 => "execute (DEP)", + _ => "?", + }; + (kind, (*rec).ExceptionInformation[1]) + } else { + ("", 0) + }; + + // No heap from here down — a heap-corruption or stack-overflow fault + // can't afford an allocator call. Format into a fixed stack buffer. + let mut buf = FixedBuf::<8192>::new(); + let _ = core::fmt::write( + &mut buf, + format_args!( + "\n============ iris hardware exception ============\n\ + os_tick_ms : {} (GetTickCount64; correlate with the log mtime)\n\ + code : 0x{:08X} {}\n\ + fault addr : 0x{:016X}\n", + GetTickCount64(), + code, + code_name(code), + fault_addr, + ), + ); + if !rw.is_empty() { + let _ = core::fmt::write( + &mut buf, + format_args!("access : {rw} @ 0x{data_addr:016X}\n"), + ); + } + if code == STATUS_FATAL_USER_CALLBACK_EXCEPTION { + let _ = core::fmt::write( + &mut buf, + format_args!( + "note : this is the *re-raised* status; the original fault \ + (in a wndproc / driver callback) should appear above as an earlier \ + first-chance exception if it was one we recognise.\n" + ), + ); + } + // Flush the header now, *before* the stack walk — if walking a corrupt + // stack faults, the VEH re-enters, hits the DUMPED guard and bails, and + // we'd otherwise have written nothing at all. The code + fault address + // alone are already useful. + emit(buf.as_bytes()); + + let mut tail = FixedBuf::<8192>::new(); + let _ = tail.write_str("stack (return addresses, resolve with the matching .pdb / addr2line):\n"); + write_backtrace(&mut tail); + let _ = tail.write_str("================================================\n"); + emit(tail.as_bytes()); + + // Breadcrumb in case stderr is a pipe nobody is tailing. + let _ = std::io::Write::write_all( + &mut std::io::stderr(), + format!("iris: wrote crash report to {LOG_PATH}\n").as_bytes(), + ); + EXCEPTION_CONTINUE_SEARCH + } + + /// Last-ditch top-level filter. Reached for a fault that was *not* inside a + /// kernel callback (those never get here on x64). Complements the vectored + /// handler rather than replacing it. + unsafe extern "system" fn unhandled_filter(info: *const EXCEPTION_POINTERS) -> i32 { + // Reuse the vectored path's formatting; it self-guards against a second + // dump, so if the VEH already reported this fault we stay quiet. + vectored_handler(info as *mut EXCEPTION_POINTERS); + EXCEPTION_CONTINUE_SEARCH + } + + /// Walk up to 62 frames and append `module.dll+0xoffset (0xabsolute)` lines. + unsafe fn write_backtrace(buf: &mut dyn core::fmt::Write) { + let mut frames: [*mut core::ffi::c_void; 62] = [core::ptr::null_mut(); 62]; + let n = RtlCaptureStackBackTrace(0, frames.len() as u32, frames.as_mut_ptr(), core::ptr::null_mut()); + for &frame in frames.iter().take(n as usize) { + let addr = frame as usize; + let mut module: HMODULE = core::ptr::null_mut(); + let ok = GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + frame as *const u16, + &mut module, + ); + if ok != 0 && !module.is_null() { + let base = module as usize; + let mut wide = [0u16; 260]; + let len = GetModuleFileNameW(module, wide.as_mut_ptr(), wide.len() as u32) as usize; + let name = basename_utf8(&wide[..len.min(wide.len())]); + let _ = core::fmt::write( + buf, + format_args!(" {}+0x{:X} (0x{:016X})\n", name.as_str(), addr - base, addr), + ); + } else { + let _ = core::fmt::write(buf, format_args!(" ???+0x0 (0x{addr:016X})\n")); + } + } + if n == 0 { + let _ = buf.write_str(" \n"); + } + } + + /// Last path component of a UTF-16 path, lossily narrowed into a small + /// fixed buffer (no heap). + fn basename_utf8(wide: &[u16]) -> ArrString<128> { + let start = wide + .iter() + .rposition(|&c| c == b'\\' as u16 || c == b'/' as u16) + .map(|i| i + 1) + .unwrap_or(0); + let mut s = ArrString::<128>::new(); + for ch in char::decode_utf16(wide[start..].iter().copied()) { + let c = ch.unwrap_or('\u{FFFD}'); + if c == '\0' { + break; + } + s.push(c); + } + s + } + + /// Write to both the log file and stderr. File first: on a hard crash the + /// stderr handle may already be torn down. + fn emit(bytes: &[u8]) { + append_to_log(bytes); + use std::io::Write; + let _ = std::io::stderr().write_all(bytes); + let _ = std::io::stderr().flush(); + } + + // ---- tiny no-alloc formatting helpers --------------------------------- + + struct FixedBuf { + data: [u8; N], + len: usize, + } + impl FixedBuf { + fn new() -> Self { + Self { data: [0; N], len: 0 } + } + fn as_bytes(&self) -> &[u8] { + &self.data[..self.len] + } + } + impl core::fmt::Write for FixedBuf { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + let b = s.as_bytes(); + let take = b.len().min(N - self.len); + self.data[self.len..self.len + take].copy_from_slice(&b[..take]); + self.len += take; + if take < b.len() { + Err(core::fmt::Error) + } else { + Ok(()) + } + } + } + + struct ArrString { + data: [u8; N], + len: usize, + } + impl ArrString { + fn new() -> Self { + Self { data: [0; N], len: 0 } + } + fn push(&mut self, c: char) { + let mut tmp = [0u8; 4]; + let s = c.encode_utf8(&mut tmp); + let b = s.as_bytes(); + if self.len + b.len() <= N { + self.data[self.len..self.len + b.len()].copy_from_slice(b); + self.len += b.len(); + } + } + fn as_str(&self) -> &str { + core::str::from_utf8(&self.data[..self.len]).unwrap_or("") + } + } +} diff --git a/src/lib.rs b/src/lib.rs index cd7a9cfa..7b681739 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -254,6 +254,7 @@ pub mod jitv2_html_default; #[cfg(all(feature = "jitv2", feature = "j2wp"))] pub mod jitv2_html_j2wp; pub mod jit_feedback; +pub mod crash_diag; #[cfg(test)] mod platform_profile_tests; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index c10a5f11..4107c72f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,12 @@ use iris::config::load_config; use iris::machine::Machine; fn main() { + // Turn a silent death (exit 0xC000041D and friends) into iris-crash.log. + // Cheap: dormant until something actually crashes. See the module docs. + iris::crash_diag::install(); + if let Ok(kind) = std::env::var("IRIS_CRASH_SELFTEST") { + iris::crash_diag::selftest(&kind); + } print_build_features(); // `mut` is only used by the pcap interface prompt below.