47 lines
2.3 KiB
Markdown
47 lines
2.3 KiB
Markdown
|
|
# xai-crash-handler
|
||
|
|
|
||
|
|
Crash handler for SIGBUS/SIGSEGV with best-effort backtrace capture.
|
||
|
|
|
||
|
|
## How it works
|
||
|
|
|
||
|
|
`install()` registers a `sigaction` handler. On crash it writes a binary blob (`GCRX` format) to `crash_dir/last-crash.bin` and restores the terminal via pre-computed escape sequences. The handler uses only async-signal-safe operations for file I/O, terminal restore, and re-raise.
|
||
|
|
|
||
|
|
On next launch, `check_previous_crash()` reads the blob, resolves IPs to symbols via `backtrace`, writes `last-crash-report.txt`, and archives it (keeping the last 5 reports).
|
||
|
|
|
||
|
|
No-ops on non-unix platforms. On musl-based Linux (release builds), the handler still records signal/address/version but skips frame capture since musl does not provide `backtrace()`.
|
||
|
|
|
||
|
|
## Limitations
|
||
|
|
|
||
|
|
### Frame capture is best-effort
|
||
|
|
|
||
|
|
Frame capture uses two fully async-signal-safe techniques:
|
||
|
|
1. The crash instruction pointer is extracted directly from the `ucontext_t` passed by the kernel.
|
||
|
|
2. Additional frames are captured by walking the frame-pointer chain (RBP on x86_64, x29 on aarch64) with raw pointer reads.
|
||
|
|
|
||
|
|
In release builds without `-C force-frame-pointers`, the frame-pointer chain may be incomplete or empty (the compiler omits frame pointers by default for optimization). The crash PC is always captured. In debug/dev builds, frame pointers are retained by default, producing fuller call stacks.
|
||
|
|
|
||
|
|
### sigaltstack is per-thread
|
||
|
|
|
||
|
|
The alternate signal stack is installed only on the thread that calls `install()`. Tokio worker threads do not inherit it. Stack overflows on worker threads will still trigger the handler (sigaction is process-wide), but without altstack protection the handler itself may fault on the overflowed stack.
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::path::PathBuf;
|
||
|
|
|
||
|
|
let crash_dir = PathBuf::from("/home/user/.myapp/crash");
|
||
|
|
|
||
|
|
// check_previous_crash MUST be called before install(), because
|
||
|
|
// install() opens last-crash.bin with O_TRUNC.
|
||
|
|
if let Some(r) = xai_crash_handler::check_previous_crash(&crash_dir) {
|
||
|
|
eprintln!("Crashed last session: {}", r.signal_name);
|
||
|
|
eprintln!("Report: {}", r.report_path.display());
|
||
|
|
}
|
||
|
|
|
||
|
|
// install() before any threads or async runtime — sigaltstack is per-thread.
|
||
|
|
// Creates crash_dir if it does not exist.
|
||
|
|
xai_crash_handler::install(xai_crash_handler::CrashHandlerConfig {
|
||
|
|
app_version: env!("CARGO_PKG_VERSION").to_string(),
|
||
|
|
crash_dir,
|
||
|
|
});
|
||
|
|
```
|