Files
ghostty-rs/crates/ghostty-crash/src/signal.rs
T
ddidderr 8f3e206d9f refactor: type escaped OS strings and fix the final audit findings
Escaped config strings (ghostty_os::osbytes) kept reaching OS
boundaries undecoded, and decoding was occasionally applied to plain
text. The root cause was that both were plain `String`s. Now:
- `EncodedOs` is a newtype (no Display/AsRef<Path>) that decodes only
  via to_os/to_path/to_bytes/to_wide; plain text enters via
  from_plain_text. OS helpers (env, home, xdg, passwd, resources dir,
  path expansion), config::Path, WorkingDirectory, theme/file_load/edit
  paths, terminal pwd, OpenUrl/Pwd actions and termio cwd use it, so the
  compiler forces decoding at every boundary. os::open takes &OsStr.
  Config output is unchanged.

Fixes all rows of docs/rewrite/notes/review-rv5-codex.md (final-Z7.md):
screen-file copy; regex/OSC 8 URLs no longer decoded (regression);
lossless Windows WTF-8/WTF-16 for args, env and CreateProcessW; Windows
resources dir/TERMINFO; color OSC request lists and X11 color lookup no
longer allocate globally (C zero-allocation test extended); macOS Mach
handler resumes all threads before KERN_FAILURE (hang regression) and
SIGABRT minidumps go through the exception thread; plus open-config,
background images, crash dir, ssh cache, exe dir/PATH, fontconfig
filenames, file links, kitty TMPDIR, .hushlogin and benchmark paths.
The typing also surfaced and fixed gtk-custom-css, bell audio, bash
HISTFILE, OSC 52 writes and the C config getters.

Test Plan:
- cargo test --workspace (3376 passed)
- just check-macos (both), just check-windows, just check-wasm
- diff <(ghostty-rs +show-config --default --docs) \
  <(ghostty +show-config --default --docs)
2026-10-12 10:06:07 +02:00

320 lines
11 KiB
Rust

//! Fatal-signal handling for crash capture.
//!
//! This is best effort, like the Sentry native handler it replaces: the
//! process is in an undefined state when this runs. We therefore avoid heap
//! allocation and only use async-signal-safe libc calls (`open`, `write`,
//! `mkdir`, `getrandom`, `raise`, ...). All formatting goes into a static
//! buffer; a flag makes sure only one crash is written at a time.
//!
//! Unsafe is needed for `sigaction`/`sigaltstack` and the static buffer.
#![allow(unsafe_code)]
use std::{
cell::UnsafeCell,
fmt,
sync::atomic::{AtomicBool, Ordering},
};
use crate::sentry::{self, Crash};
const SIGNALS: [(libc::c_int, &str, &str); 5] = [
(libc::SIGSEGV, "SIGSEGV", "Segmentation fault"),
(libc::SIGBUS, "SIGBUS", "Bus error"),
(libc::SIGILL, "SIGILL", "Illegal instruction"),
(libc::SIGFPE, "SIGFPE", "Arithmetic exception"),
(libc::SIGABRT, "SIGABRT", "Aborted"),
];
const EVENT_CAP: usize = 6 * 1024;
const HEAD_CAP: usize = 512;
const PATH_CAP: usize = 4096 + 64;
/// Static scratch space for the handler.
struct Scratch {
event: [u8; EVENT_CAP],
head: [u8; HEAD_CAP],
path: [u8; PATH_CAP],
#[cfg(target_vendor = "apple")]
native: crate::native::Native,
}
struct ScratchCell(UnsafeCell<Scratch>);
// SAFETY: access is serialized by `BUSY`.
unsafe impl Sync for ScratchCell {}
static SCRATCH: ScratchCell = ScratchCell(UnsafeCell::new(Scratch {
event: [0; EVENT_CAP],
head: [0; HEAD_CAP],
path: [0; PATH_CAP],
#[cfg(target_vendor = "apple")]
native: crate::native::Native::new(),
}));
pub(crate) static BUSY: AtomicBool = AtomicBool::new(false);
/// A `fmt::Write` into a fixed byte slice; errors when full.
struct SliceWriter<'a> {
buf: &'a mut [u8],
len: usize,
}
impl fmt::Write for SliceWriter<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let b = s.as_bytes();
let end = self.len.checked_add(b.len()).ok_or(fmt::Error)?;
if end > self.buf.len() {
return Err(fmt::Error);
}
self.buf[self.len..end].copy_from_slice(b);
self.len = end;
Ok(())
}
}
/// Fill `out` with random bytes using only async-signal-safe calls, falling
/// back to a time/pid mix if the kernel refuses.
pub(crate) fn random_bytes(out: &mut [u8; 16]) {
#[cfg(not(target_vendor = "apple"))]
{
// SAFETY: `out` is a valid writable buffer of the given length.
let n = unsafe { libc::getrandom(out.as_mut_ptr().cast(), out.len(), libc::GRND_NONBLOCK) };
if usize::try_from(n).is_ok_and(|n| n == out.len()) {
return;
}
}
// Darwin has no getrandom(2); getentropy(2) fills up to 256 bytes.
#[cfg(target_vendor = "apple")]
{
// SAFETY: `out` is a valid writable buffer of the given length.
if unsafe { libc::getentropy(out.as_mut_ptr().cast(), out.len()) } == 0 {
return;
}
}
let (s, us) = sentry::now();
// SAFETY: getpid has no preconditions.
let pid = unsafe { libc::getpid() };
let mut x = (s as u64) ^ (u64::from(us) << 20) ^ (u64::from(pid.unsigned_abs()) << 40);
for chunk in out.chunks_mut(8) {
// splitmix64
x = x.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = x;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^= z >> 31;
chunk.copy_from_slice(&z.to_le_bytes()[..chunk.len()]);
}
}
fn write_all(fd: libc::c_int, mut data: &[u8]) -> bool {
while !data.is_empty() {
// SAFETY: `data` is a valid readable buffer.
let n = unsafe { libc::write(fd, data.as_ptr().cast(), data.len()) };
if n < 0 {
if std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted {
continue;
}
return false;
}
if n == 0 {
return false;
}
data = &data[n as usize..];
}
true
}
/// `mkdir -p` on a NUL-terminated path in `path[..len]` (mutated temporarily).
fn mkdir_p(path: &mut [u8], len: usize) {
for i in 1..=len {
if i == len || path[i] == b'/' {
let saved = path[i];
path[i] = 0;
// SAFETY: path[..=i] is NUL terminated.
unsafe { libc::mkdir(path.as_ptr().cast(), 0o755) };
path[i] = saved;
}
}
}
/// Write a crash report for `sig`. Returns without doing anything if the
/// crash handler isn't active or another crash is being written.
fn write_signal_report(name: &str, desc: &str, info: *mut libc::siginfo_t, uc: *mut libc::c_void) {
let Some(st) = sentry::state() else { return };
if BUSY.swap(true, Ordering::Acquire) {
return;
}
// SAFETY: BUSY gives us exclusive access to the scratch space (and it is
// never released, since the process is about to die).
let sc = unsafe { &mut *SCRATCH.0.get() };
let mut raw = [0u8; 16];
random_bytes(&mut raw);
let mut u = [0u8; 36];
sentry::uuid_string(raw, &mut u);
let Ok(uuid) = std::str::from_utf8(&u) else {
return;
};
let (secs, micros) = sentry::now();
// Darwin: record the faulting thread's registers and stack in the event
// (no minidump there; see `native.rs`).
#[cfg(target_vendor = "apple")]
let native_ctx = {
// SAFETY: `info`/`uc` are the pointers the kernel gave the handler.
unsafe { crate::darwin::capture(&mut sc.native, info, uc) };
Some(&sc.native)
};
#[cfg(not(target_vendor = "apple"))]
let native_ctx = None;
let crash = Crash {
uuid,
secs,
micros,
kind: name,
value: desc,
mechanism: "signalhandler",
native: native_ctx,
};
let mut ev = SliceWriter {
buf: &mut sc.event,
len: 0,
};
if sentry::write_event(&mut ev, st, &crash).is_err() {
return;
}
let ev_len = ev.len;
let mut head = SliceWriter {
buf: &mut sc.head,
len: 0,
};
if sentry::write_envelope_head(&mut head, &crash, ev_len).is_err() {
return;
}
let head_len = head.len;
// <dir>/<uuid>.ghosttycrash\0
let ext = b".ghosttycrash";
let total = st.dir.len() + 1 + u.len() + ext.len();
if total + 1 > PATH_CAP {
return;
}
let mut o = 0;
for part in [st.dir.as_slice(), b"/", &u, ext] {
sc.path[o..o + part.len()].copy_from_slice(part);
o += part.len();
}
sc.path[o] = 0;
// The directory must exist for writing to work.
mkdir_p(&mut sc.path, st.dir.len());
// Preferred: an envelope with a minidump of the whole process, written by
// a forked helper that ptraces us (Linux only; see `minidump`).
#[cfg(target_os = "linux")]
{
// SAFETY: `info`/`uc` are the pointers the kernel gave the handler.
let cc = unsafe { crate::minidump::crash_context(info, uc) };
let cap = crate::minidump::Capture {
path: &sc.path[..=o],
head: &sc.head[..head_len],
event: &sc.event[..ev_len],
};
if crate::minidump::capture(&cap, cc) {
write_all(
2,
b"warning(sentry): crash report with minidump written to disk path=",
);
write_all(2, &sc.path[..o]);
write_all(2, b"\n");
return;
}
}
#[cfg(not(target_os = "linux"))]
let _ = (info, uc);
// SAFETY: path is NUL terminated.
let fd = unsafe {
libc::open(
sc.path.as_ptr().cast(),
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC,
0o644,
)
};
if fd < 0 {
return;
}
let ok = write_all(fd, &sc.head[..head_len]) && write_all(fd, &sc.event[..ev_len]);
// SAFETY: fd is open and owned by us.
unsafe { libc::close(fd) };
if ok {
write_all(2, b"warning(sentry): crash report written to disk path=");
write_all(2, &sc.path[..o]);
write_all(2, b"\n");
}
}
extern "C" fn handler(sig: libc::c_int, info: *mut libc::siginfo_t, ctx: *mut libc::c_void) {
// macOS: `abort()` raises SIGABRT directly (no Mach exception), so ask the
// exception handler thread for the report with a minidump, like
// Breakpad. The event-only report below is the fallback.
#[cfg(target_os = "macos")]
let dumped = sig == libc::SIGABRT && crate::mach_handler::request_abort_dump(sig);
#[cfg(not(target_os = "macos"))]
let dumped = false;
if !dumped && let Some((_, name, desc)) = SIGNALS.iter().find(|(s, ..)| *s == sig) {
write_signal_report(name, desc, info, ctx);
}
// Restore the default action and re-raise so the process dies with the
// original signal (core dump, exit status) as if we were never here.
// SAFETY: signal and raise are async-signal-safe.
unsafe {
libc::signal(sig, libc::SIG_DFL);
libc::raise(sig);
}
}
/// Make sure the calling thread has an alternate signal stack so that stack
/// overflows can be handled. Rust's runtime already installs one for the main
/// thread and threads it spawns, so this is usually a no-op.
fn ensure_altstack() {
// SAFETY: sigaltstack is called with valid pointers; the new stack is
// leaked so it lives for the whole process.
unsafe {
let mut cur: libc::stack_t = std::mem::zeroed();
if libc::sigaltstack(std::ptr::null(), &raw mut cur) == 0
&& cur.ss_flags & libc::SS_DISABLE == 0
{
return;
}
let size = 64 * 1024;
let stack: &'static mut [u8] = Box::leak(vec![0u8; size].into_boxed_slice());
let ss = libc::stack_t {
ss_sp: stack.as_mut_ptr().cast(),
ss_flags: 0,
ss_size: size,
};
libc::sigaltstack(&raw const ss, std::ptr::null_mut());
}
}
/// Install handlers for the fatal signals.
pub(crate) fn install_handlers() {
ensure_altstack();
for (sig, ..) in SIGNALS {
// SAFETY: a zeroed sigaction is valid to fill in; the handler is an
// `extern "C"` fn with the SA_SIGINFO signature.
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_NODEFER;
libc::sigemptyset(&raw mut sa.sa_mask);
libc::sigaction(sig, &raw const sa, std::ptr::null_mut());
}
}
// macOS: memory faults are Mach exceptions first; the exception port
// handler writes the minidump (see `mach_handler`). The signal handlers
// above stay as the path for SIGABRT and as a fallback.
#[cfg(target_os = "macos")]
if !crate::mach_handler::install() {
log::warn!("mach exception port handler unavailable; using signal handlers only");
}
}