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)
This commit is contained in:
1 parent
7a3f0b0f2c
commit
f7fec6f44d
92 files changed
+2967
-992
No files matched your search
@@ -19,7 +19,8 @@ pub fn default_dir() -> io::Result<Dir> {
|
||||
})
|
||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
||||
Ok(Dir {
|
||||
path: PathBuf::from(path),
|
||||
// The XDG helpers return the encoded form of the OS string.
|
||||
path: path.to_path(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ impl ReportIterator {
|
||||
if !entry.file_type()?.is_file() {
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
let name = entry.file_name();
|
||||
// statFile follows symlinks; entries here are regular files.
|
||||
let mtime = fs::metadata(self.dir.join(entry.file_name()))?.modified()?;
|
||||
let mtime = match mtime.duration_since(UNIX_EPOCH) {
|
||||
@@ -84,7 +85,8 @@ impl Iterator for ReportIterator {
|
||||
/// A crash report file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Report {
|
||||
pub name: String,
|
||||
/// The file name as the raw OS string (Zig `[]const u8`).
|
||||
pub name: std::ffi::OsString,
|
||||
/// Modification time in nanoseconds since the unix epoch.
|
||||
pub mtime: i128,
|
||||
}
|
||||
@@ -121,6 +123,29 @@ mod tests {
|
||||
assert_eq!("a.ghosttycrash", r[0].name);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn default_dir_decodes_non_utf8_xdg_state() {
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
let v = std::ffi::OsString::from_vec(b"/tmp/state\xff".to_vec());
|
||||
let d = temp_env::with_var("XDG_STATE_HOME", Some(&v), || default_dir().unwrap());
|
||||
assert_eq!(
|
||||
d.path.as_os_str().as_bytes(),
|
||||
b"/tmp/state\xff/ghostty/crash"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn non_utf8_report_name_is_kept() {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let name = std::ffi::OsStr::from_bytes(b"a\xff.ghosttycrash");
|
||||
fs::write(t.path().join(name), "x").unwrap();
|
||||
let r = list_reports(t.path()).unwrap();
|
||||
assert_eq!(r[0].name, name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_dir_uses_xdg_state() {
|
||||
let d = default_dir().unwrap();
|
||||
|
||||
@@ -15,8 +15,51 @@ pub(crate) const MACH_EXCEPTION_RAISE: i32 = 2405;
|
||||
pub(crate) const EXC_BAD_ACCESS: u32 = 1;
|
||||
pub(crate) const EXC_BAD_INSTRUCTION: u32 = 2;
|
||||
pub(crate) const EXC_ARITHMETIC: u32 = 3;
|
||||
pub(crate) const EXC_SOFTWARE: u32 = 5;
|
||||
pub(crate) const EXC_BREAKPOINT: u32 = 6;
|
||||
|
||||
/// `EXC_SOFT_SIGNAL`: the code of an `EXC_SOFTWARE` exception that stands for
|
||||
/// a BSD signal (the subcode is the signal number). This is how an
|
||||
/// `abort()` (SIGABRT) is described, as Apple's crash reporter does.
|
||||
pub(crate) const EXC_SOFT_SIGNAL: i64 = 0x10003;
|
||||
|
||||
/// Message id of the "write a dump for this signal" request the signal
|
||||
/// handler sends to the exception handler thread (not a Mach exception id).
|
||||
pub(crate) const ABORT_MSG_ID: i32 = 0x6768_6f73;
|
||||
|
||||
impl MachException {
|
||||
/// The exception describing a fatal BSD signal that never went through
|
||||
/// the Mach exception path (`abort()` raises SIGABRT directly).
|
||||
pub(crate) fn from_signal(signo: i32) -> MachException {
|
||||
MachException {
|
||||
kind: EXC_SOFTWARE,
|
||||
code: EXC_SOFT_SIGNAL,
|
||||
subcode: i64::from(signo),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire layout of the abort request after the 24 byte Mach header:
|
||||
/// `thread: u32`, `signo: i32` (native endian).
|
||||
pub(crate) const ABORT_BODY_LEN: usize = 8;
|
||||
|
||||
/// Encode the body of an abort request.
|
||||
pub(crate) fn abort_body(thread: u32, signo: i32) -> [u8; ABORT_BODY_LEN] {
|
||||
let mut b = [0u8; ABORT_BODY_LEN];
|
||||
b[..4].copy_from_slice(&thread.to_ne_bytes());
|
||||
b[4..].copy_from_slice(&signo.to_ne_bytes());
|
||||
b
|
||||
}
|
||||
|
||||
/// Decode an abort request message (header included): the faulting thread
|
||||
/// and the signal. `None` for any other message.
|
||||
pub(crate) fn parse_abort(msg: &[u8]) -> Option<(u32, i32)> {
|
||||
if u32_at(msg, 20)? as i32 != ABORT_MSG_ID {
|
||||
return None;
|
||||
}
|
||||
Some((u32_at(msg, HEADER)?, u32_at(msg, HEADER + 4)? as i32))
|
||||
}
|
||||
|
||||
/// A decoded Mach exception.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct MachException {
|
||||
@@ -61,6 +104,7 @@ impl MachException {
|
||||
EXC_BAD_INSTRUCTION => (4, 1), // SIGILL, ILL_ILLOPC
|
||||
EXC_ARITHMETIC => (8, 0), // SIGFPE
|
||||
EXC_BREAKPOINT => (5, 1), // SIGTRAP, TRAP_BRKPT
|
||||
EXC_SOFTWARE if self.code == EXC_SOFT_SIGNAL => (self.subcode as i32, 0),
|
||||
_ => (0, 0),
|
||||
}
|
||||
}
|
||||
@@ -248,6 +292,28 @@ mod tests {
|
||||
assert_eq!(e(EXC_BREAKPOINT, 1).signal().0, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abort_request_round_trips() {
|
||||
let mut msg = vec![0u8; 20];
|
||||
msg.extend_from_slice(&ABORT_MSG_ID.to_ne_bytes());
|
||||
msg.extend_from_slice(&abort_body(77, 6));
|
||||
assert_eq!(parse_abort(&msg), Some((77, 6)));
|
||||
// A Mach exception message or a truncated one is not an abort request.
|
||||
assert_eq!(parse_abort(&raise(EXC_BAD_ACCESS, &[1, 2])), None);
|
||||
assert_eq!(parse_abort(&msg[..26]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sigabrt_is_described_as_a_soft_signal() {
|
||||
let e = MachException::from_signal(6);
|
||||
assert_eq!(e.name(), "EXC_SOFTWARE");
|
||||
assert_eq!(e.signal(), (6, 0));
|
||||
assert_eq!(e.fault_addr(), 0);
|
||||
let mut s = String::new();
|
||||
e.describe(&mut s);
|
||||
assert_eq!(s, "EXC_SOFTWARE (code=0x10003, subcode=0x6)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_names() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -28,6 +28,7 @@ mod signal;
|
||||
#[cfg(windows)]
|
||||
#[path = "signal_windows.rs"]
|
||||
mod signal;
|
||||
mod suspend;
|
||||
|
||||
pub use context::{DimsSnapshot, SurfaceDims, ThreadGuard, ThreadType, current_dims};
|
||||
pub use dir::{Dir, Report, ReportIterator, default_dir};
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
//! sends a `mach_exception_raise` message; the handler thread (which runs on
|
||||
//! its own healthy stack, so stack overflows work) then:
|
||||
//!
|
||||
//! 1. suspends all other threads,
|
||||
//! 1. suspends all other threads (and resumes *all* of them, the faulting one
|
||||
//! included, before replying; otherwise the process hangs instead of
|
||||
//! dying),
|
||||
//! 2. reads the crashed thread's registers with `thread_get_state` for the
|
||||
//! event's stack trace,
|
||||
//! 3. writes a minidump with `minidump-writer` for the whole task and attaches
|
||||
@@ -20,9 +22,11 @@
|
||||
//! the process dies as usual (Apple's crash reporter included). The signal
|
||||
//! path (`signal.rs`) sees `BUSY` set and writes nothing more.
|
||||
//!
|
||||
//! Signals that are not Mach exceptions (`SIGABRT` from `abort()`) keep going
|
||||
//! through the signal handler. A watchdog `alarm(2)` kills the process if the
|
||||
//! dump hangs (e.g. a suspended thread held the allocator lock).
|
||||
//! `SIGABRT` from `abort()` is not a Mach exception. Like Breakpad, the signal
|
||||
//! handler asks this handler thread for the dump (`request_abort_dump`: a
|
||||
//! message to the exception port, then it waits on a reply port), so
|
||||
//! `abort()` gets a minidump too. A watchdog `alarm(2)` kills the process if
|
||||
//! the dump hangs (e.g. a suspended thread held the allocator lock).
|
||||
//!
|
||||
//! Unlike Breakpad this does not forward the exception to previously
|
||||
//! registered handlers; it restores them, which only matters for debuggers
|
||||
@@ -53,12 +57,14 @@ use mach2::{
|
||||
mach_types::{thread_act_t, thread_t},
|
||||
message::{
|
||||
MACH_MSG_TIMEOUT_NONE,
|
||||
MACH_MSG_TYPE_COPY_SEND,
|
||||
MACH_MSG_TYPE_MAKE_SEND,
|
||||
MACH_MSGH_BITS,
|
||||
MACH_MSGH_BITS_REMOTE_MASK,
|
||||
MACH_RCV_LARGE,
|
||||
MACH_RCV_MSG,
|
||||
MACH_RCV_PORT_DIED,
|
||||
MACH_RCV_TIMEOUT,
|
||||
MACH_SEND_MSG,
|
||||
mach_msg,
|
||||
mach_msg_header_t,
|
||||
@@ -77,6 +83,7 @@ use crate::{
|
||||
native::Native,
|
||||
sentry,
|
||||
signal::BUSY,
|
||||
suspend::{SuspendGuard, Threads},
|
||||
};
|
||||
|
||||
/// Maximum number of registered exception handlers (`EXC_TYPES_COUNT`).
|
||||
@@ -99,6 +106,11 @@ struct Previous {
|
||||
|
||||
static PREVIOUS: OnceLock<Previous> = OnceLock::new();
|
||||
static PORT: AtomicU32 = AtomicU32::new(MACH_PORT_NULL);
|
||||
/// Receive port the signal handler waits on while the handler thread dumps.
|
||||
static REPLY_PORT: AtomicU32 = AtomicU32::new(MACH_PORT_NULL);
|
||||
|
||||
/// How long the signal handler waits for the dump (a bit above the watchdog).
|
||||
const ABORT_WAIT_MS: u32 = (WATCHDOG_SECS + 10) * 1000;
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
type ThreadState = mach2::structs::x86_thread_state64_t;
|
||||
@@ -136,6 +148,21 @@ pub(crate) fn install() -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reply port for the SIGABRT path (best effort: without it SIGABRT
|
||||
// keeps the event-only report).
|
||||
let mut reply: mach_port_t = MACH_PORT_NULL;
|
||||
if mach2::mach_port::mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &raw mut reply)
|
||||
== KERN_SUCCESS
|
||||
{
|
||||
if mach2::mach_port::mach_port_insert_right(task, reply, reply, MACH_MSG_TYPE_MAKE_SEND)
|
||||
== KERN_SUCCESS
|
||||
{
|
||||
REPLY_PORT.store(reply, Ordering::Release);
|
||||
} else {
|
||||
mach2::mach_port::mach_port_destroy(task, reply);
|
||||
}
|
||||
}
|
||||
|
||||
let mut prev = Previous {
|
||||
count: MAX_PORTS as u32,
|
||||
masks: [0; MAX_PORTS],
|
||||
@@ -237,6 +264,9 @@ fn handler_thread(port: mach_port_t) {
|
||||
continue;
|
||||
}
|
||||
let Some(req) = exception::parse_raise(&buf.0) else {
|
||||
if let Some((thread, signo)) = exception::parse_abort(&buf.0) {
|
||||
handle_abort(thread, signo, handler);
|
||||
}
|
||||
continue;
|
||||
};
|
||||
handle_exception(&req, handler);
|
||||
@@ -273,23 +303,40 @@ fn handler_thread(port: mach_port_t) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Suspend every thread of the task except `keep` (the handler thread).
|
||||
fn suspend_other_threads(keep: thread_act_t) {
|
||||
// SAFETY: task_threads returns a vm-allocated array we only read.
|
||||
unsafe {
|
||||
let task = mach2::traps::mach_task_self();
|
||||
let mut list: mach2::mach_types::thread_act_array_t = std::ptr::null_mut();
|
||||
let mut n: u32 = 0;
|
||||
if mach2::task::task_threads(task, &raw mut list, &raw mut n) != KERN_SUCCESS {
|
||||
return;
|
||||
}
|
||||
for i in 0..n as usize {
|
||||
let t = *list.add(i);
|
||||
if t != keep {
|
||||
mach2::thread_act::thread_suspend(t);
|
||||
/// The Mach threads of this task (for [`SuspendGuard`]).
|
||||
struct MachThreads;
|
||||
|
||||
impl Threads for MachThreads {
|
||||
type Id = thread_act_t;
|
||||
|
||||
fn list(&self) -> Vec<thread_act_t> {
|
||||
// SAFETY: task_threads returns a vm-allocated array we copy and free.
|
||||
unsafe {
|
||||
let task = mach2::traps::mach_task_self();
|
||||
let mut list: mach2::mach_types::thread_act_array_t = std::ptr::null_mut();
|
||||
let mut n: u32 = 0;
|
||||
if mach2::task::task_threads(task, &raw mut list, &raw mut n) != KERN_SUCCESS {
|
||||
return Vec::new();
|
||||
}
|
||||
let threads = std::slice::from_raw_parts(list, n as usize).to_vec();
|
||||
mach2::vm::mach_vm_deallocate(
|
||||
task,
|
||||
list as mach2::vm_types::mach_vm_address_t,
|
||||
u64::from(n) * size_of::<thread_act_t>() as u64,
|
||||
);
|
||||
threads
|
||||
}
|
||||
}
|
||||
|
||||
fn suspend(&self, t: thread_act_t) {
|
||||
// SAFETY: plain Mach call on a thread port of this task.
|
||||
unsafe { mach2::thread_act::thread_suspend(t) };
|
||||
}
|
||||
|
||||
fn resume(&self, t: thread_act_t) {
|
||||
// SAFETY: as above.
|
||||
unsafe { mach2::thread_act::thread_resume(t) };
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_exception(req: &exception::RaiseRequest, handler: thread_t) {
|
||||
@@ -299,17 +346,141 @@ fn handle_exception(req: &exception::RaiseRequest, handler: thread_t) {
|
||||
// SAFETY: alarm has no preconditions. SIGALRM's default action ends
|
||||
// the process if the dump below hangs.
|
||||
unsafe { libc::alarm(WATCHDOG_SECS) };
|
||||
suspend_other_threads(handler);
|
||||
write_report(st, req, handler);
|
||||
{
|
||||
// Every thread (the faulting one included) is resumed when the
|
||||
// guard drops, before the exception is handed back to the kernel.
|
||||
let _threads = SuspendGuard::suspend_all_except(&MachThreads, handler);
|
||||
write_report(st, req.exception, req.thread, req.task, handler);
|
||||
}
|
||||
// SAFETY: as above.
|
||||
unsafe { libc::alarm(0) };
|
||||
}
|
||||
restore_ports();
|
||||
}
|
||||
|
||||
fn write_report(st: &sentry::State, req: &exception::RaiseRequest, handler: thread_t) {
|
||||
let ex: MachException = req.exception;
|
||||
/// A `SIGABRT` dump request from the signal handler: dump the whole task with
|
||||
/// `thread` as the crashed thread, then wake the waiting signal handler.
|
||||
fn handle_abort(thread: u32, signo: i32, handler: thread_t) {
|
||||
if let Some(st) = sentry::state()
|
||||
&& !BUSY.swap(true, Ordering::Acquire)
|
||||
{
|
||||
// SAFETY: as in `handle_exception`.
|
||||
unsafe { libc::alarm(WATCHDOG_SECS) };
|
||||
{
|
||||
let _threads = SuspendGuard::suspend_all_except(&MachThreads, handler);
|
||||
// SAFETY: plain Mach trap.
|
||||
let task = unsafe { mach2::traps::mach_task_self() };
|
||||
write_report(st, MachException::from_signal(signo), thread, task, handler);
|
||||
}
|
||||
// SAFETY: as above.
|
||||
unsafe { libc::alarm(0) };
|
||||
}
|
||||
notify_signal_handler();
|
||||
}
|
||||
|
||||
/// Wake the signal handler waiting in [`request_abort_dump`].
|
||||
fn notify_signal_handler() {
|
||||
let reply = REPLY_PORT.load(Ordering::Acquire);
|
||||
if reply == MACH_PORT_NULL {
|
||||
return;
|
||||
}
|
||||
let mut head = mach_msg_header_t {
|
||||
msgh_bits: MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0),
|
||||
msgh_size: size_of::<mach_msg_header_t>() as u32,
|
||||
msgh_remote_port: reply,
|
||||
msgh_local_port: MACH_PORT_NULL,
|
||||
msgh_voucher_port: 0,
|
||||
msgh_id: exception::ABORT_MSG_ID + 1,
|
||||
};
|
||||
// SAFETY: `head` is a complete message with no body.
|
||||
unsafe {
|
||||
mach_msg(
|
||||
&raw mut head,
|
||||
MACH_SEND_MSG,
|
||||
size_of::<mach_msg_header_t>() as u32,
|
||||
0,
|
||||
MACH_PORT_NULL,
|
||||
MACH_MSG_TIMEOUT_NONE,
|
||||
MACH_PORT_NULL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, packed(4))]
|
||||
struct Request {
|
||||
head: mach_msg_header_t,
|
||||
body: [u8; exception::ABORT_BODY_LEN],
|
||||
}
|
||||
|
||||
/// Called from the `SIGABRT` signal handler: ask the exception handler thread
|
||||
/// to write the report with a minidump and wait for it. Returns false if the
|
||||
/// handler thread is unavailable (the caller then writes the event-only
|
||||
/// report itself).
|
||||
pub(crate) fn request_abort_dump(signo: libc::c_int) -> bool {
|
||||
let port = PORT.load(Ordering::Acquire);
|
||||
let reply = REPLY_PORT.load(Ordering::Acquire);
|
||||
if port == MACH_PORT_NULL || reply == MACH_PORT_NULL || sentry::state().is_none() {
|
||||
return false;
|
||||
}
|
||||
if BUSY.load(Ordering::Acquire) {
|
||||
// Another crash is already being written.
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAFETY: mach_thread_self has no preconditions.
|
||||
let thread = unsafe { mach2::mach_init::mach_thread_self() };
|
||||
let mut req = Request {
|
||||
head: mach_msg_header_t {
|
||||
msgh_bits: MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0),
|
||||
msgh_size: size_of::<Request>() as u32,
|
||||
msgh_remote_port: port,
|
||||
msgh_local_port: MACH_PORT_NULL,
|
||||
msgh_voucher_port: 0,
|
||||
msgh_id: exception::ABORT_MSG_ID,
|
||||
},
|
||||
body: exception::abort_body(thread, signo),
|
||||
};
|
||||
// SAFETY: `req` is a fully initialized message of the given size.
|
||||
let sent = unsafe {
|
||||
mach_msg(
|
||||
(&raw mut req).cast(),
|
||||
MACH_SEND_MSG,
|
||||
size_of::<Request>() as u32,
|
||||
0,
|
||||
MACH_PORT_NULL,
|
||||
MACH_MSG_TIMEOUT_NONE,
|
||||
MACH_PORT_NULL,
|
||||
)
|
||||
};
|
||||
if sent != 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block until the handler thread has finished (it resumes this thread's
|
||||
// suspension before replying). Bounded, in case the dump hangs.
|
||||
let mut buf = Buffer([0; 1024]);
|
||||
// SAFETY: `buf` is a writable 1024 byte aligned buffer.
|
||||
unsafe {
|
||||
mach_msg(
|
||||
buf.0.as_mut_ptr().cast(),
|
||||
MACH_RCV_MSG | MACH_RCV_TIMEOUT,
|
||||
0,
|
||||
buf.0.len() as u32,
|
||||
reply,
|
||||
ABORT_WAIT_MS,
|
||||
MACH_PORT_NULL,
|
||||
);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn write_report(
|
||||
st: &sentry::State,
|
||||
ex: MachException,
|
||||
thread: thread_t,
|
||||
task: mach_port_t,
|
||||
handler: thread_t,
|
||||
) {
|
||||
// Registers and frame-pointer walk of the faulting thread.
|
||||
let mut native = Box::new(Native::new());
|
||||
(native.signo, native.code) = ex.signal();
|
||||
@@ -319,7 +490,7 @@ fn write_report(st: &sentry::State, req: &exception::RaiseRequest, handler: thre
|
||||
// SAFETY: `state` is a writable thread state of the requested flavor.
|
||||
let kr = unsafe {
|
||||
mach2::thread_act::thread_get_state(
|
||||
req.thread,
|
||||
thread,
|
||||
state_flavor(),
|
||||
(&raw mut state).cast(),
|
||||
&raw mut count,
|
||||
@@ -331,8 +502,8 @@ fn write_report(st: &sentry::State, req: &exception::RaiseRequest, handler: thre
|
||||
}
|
||||
|
||||
let cc = CrashContext {
|
||||
task: req.task,
|
||||
thread: req.thread,
|
||||
task,
|
||||
thread,
|
||||
handler_thread: handler,
|
||||
exception: Some(ExceptionInfo {
|
||||
kind: ex.kind,
|
||||
|
||||
@@ -138,7 +138,8 @@ pub fn install(opts: &Options) -> Result<(), InitError> {
|
||||
}
|
||||
|
||||
let state = State {
|
||||
dir: dir.as_os_str().as_encoded_bytes().to_vec(),
|
||||
// Bytes on Unix, WTF-8 on Windows (lossless both ways).
|
||||
dir: ghostty_os::osbytes::os_bytes(dir.as_os_str()),
|
||||
prefix,
|
||||
};
|
||||
if STATE.set(state).is_err() {
|
||||
@@ -415,17 +416,8 @@ pub(crate) fn write_report_ext(
|
||||
data.extend_from_slice(&enc.payload);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
let dir = {
|
||||
use std::os::unix::ffi::OsStrExt as _;
|
||||
std::path::Path::new(std::ffi::OsStr::from_bytes(&st.dir))
|
||||
};
|
||||
// Windows paths are not arbitrary bytes; the dir was UTF-8 in practice.
|
||||
#[cfg(not(unix))]
|
||||
let dir_buf = PathBuf::from(String::from_utf8_lossy(&st.dir).into_owned());
|
||||
#[cfg(not(unix))]
|
||||
let dir = dir_buf.as_path();
|
||||
std::fs::create_dir_all(dir)?;
|
||||
let dir = PathBuf::from(ghostty_os::osbytes::os_from_bytes(st.dir.clone()));
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let path = dir.join(report_file_name(uuid));
|
||||
std::fs::write(&path, data)?;
|
||||
Ok(path)
|
||||
|
||||
@@ -252,7 +252,14 @@ fn write_signal_report(name: &str, desc: &str, info: *mut libc::siginfo_t, uc: *
|
||||
}
|
||||
|
||||
extern "C" fn handler(sig: libc::c_int, info: *mut libc::siginfo_t, ctx: *mut libc::c_void) {
|
||||
if let Some((_, name, desc)) = SIGNALS.iter().find(|(s, ..)| *s == sig) {
|
||||
// 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
|
||||
|
||||
@@ -124,7 +124,7 @@ fn write_report(st: &sentry::State, info: *mut EXCEPTION_POINTERS, tid: u32) {
|
||||
.map_or(0, |r| r.ExceptionCode) as u32;
|
||||
let (kind, desc) = exception::windows_exception(code);
|
||||
|
||||
let dir = std::path::PathBuf::from(String::from_utf8_lossy(&st.dir).into_owned());
|
||||
let dir = std::path::PathBuf::from(ghostty_os::osbytes::os_from_bytes(st.dir.clone()));
|
||||
let dump = (|| -> Result<Vec<u8>, String> {
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
let tmp = dir.join(format!("minidump-{}.tmp", std::process::id()));
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
//! Suspending every other thread of the process while a crash report is
|
||||
//! written, and *always* resuming them afterwards.
|
||||
//!
|
||||
//! The macOS exception handler thread suspends all other threads so the dump
|
||||
//! is consistent. Breakpad resumes them before handing the exception back to
|
||||
//! the kernel; without that, the faulting thread (which the kernel parked
|
||||
//! until the handler replies) stays suspended forever and the process hangs
|
||||
//! instead of dying through BSD signal delivery. The platform specific
|
||||
//! thread operations are behind [`Threads`] so the bookkeeping is testable
|
||||
//! everywhere.
|
||||
|
||||
// Only the macOS handler uses this outside of tests.
|
||||
#![cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
|
||||
/// The thread operations needed by [`SuspendGuard`].
|
||||
pub(crate) trait Threads {
|
||||
type Id: Copy + PartialEq;
|
||||
|
||||
/// All threads of the process.
|
||||
fn list(&self) -> Vec<Self::Id>;
|
||||
fn suspend(&self, id: Self::Id);
|
||||
fn resume(&self, id: Self::Id);
|
||||
}
|
||||
|
||||
/// Resumes the threads it suspended when dropped (also on panic).
|
||||
pub(crate) struct SuspendGuard<'a, T: Threads> {
|
||||
ops: &'a T,
|
||||
suspended: Vec<T::Id>,
|
||||
}
|
||||
|
||||
impl<'a, T: Threads> SuspendGuard<'a, T> {
|
||||
/// Suspend every thread except `keep` (the calling handler thread).
|
||||
pub(crate) fn suspend_all_except(ops: &'a T, keep: T::Id) -> Self {
|
||||
let suspended: Vec<T::Id> = ops.list().into_iter().filter(|t| *t != keep).collect();
|
||||
for t in &suspended {
|
||||
ops.suspend(*t);
|
||||
}
|
||||
SuspendGuard { ops, suspended }
|
||||
}
|
||||
|
||||
/// The number of threads this guard suspended.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.suspended.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Threads> Drop for SuspendGuard<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
for t in self.suspended.drain(..) {
|
||||
self.ops.resume(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Mock {
|
||||
// (id, suspend count)
|
||||
threads: RefCell<Vec<(u32, i32)>>,
|
||||
}
|
||||
|
||||
impl Threads for Mock {
|
||||
type Id = u32;
|
||||
fn list(&self) -> Vec<u32> {
|
||||
self.threads.borrow().iter().map(|t| t.0).collect()
|
||||
}
|
||||
fn suspend(&self, id: u32) {
|
||||
for t in self.threads.borrow_mut().iter_mut().filter(|t| t.0 == id) {
|
||||
t.1 += 1;
|
||||
}
|
||||
}
|
||||
fn resume(&self, id: u32) {
|
||||
for t in self.threads.borrow_mut().iter_mut().filter(|t| t.0 == id) {
|
||||
t.1 -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suspends_all_but_handler_and_resumes_all_on_drop() {
|
||||
// Thread 1 is the faulting thread, 9 the handler.
|
||||
let m = Mock {
|
||||
threads: RefCell::new(vec![(1, 0), (2, 0), (9, 0)]),
|
||||
};
|
||||
{
|
||||
let g = SuspendGuard::suspend_all_except(&m, 9);
|
||||
assert_eq!(g.len(), 2);
|
||||
assert_eq!(*m.threads.borrow(), [(1, 1), (2, 1), (9, 0)]);
|
||||
}
|
||||
// Every thread, including the faulting one, runs again.
|
||||
assert_eq!(*m.threads.borrow(), [(1, 0), (2, 0), (9, 0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumes_even_if_the_dump_panics() {
|
||||
let m = Mock {
|
||||
threads: RefCell::new(vec![(1, 0), (9, 0)]),
|
||||
};
|
||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let _g = SuspendGuard::suspend_all_except(&m, 9);
|
||||
panic!("dump failed");
|
||||
}));
|
||||
assert!(r.is_err());
|
||||
assert_eq!(*m.threads.borrow(), [(1, 0), (9, 0)]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user