Closes the remaining library rows of the final completeness audit (docs/rewrite/notes/final-Z3.md): - libghostty-vt: OSC parser storage goes through the caller-supplied C allocator via a ByteStorage abstraction; ghostty_osc_new reports GHOSTTY_OUT_OF_MEMORY and failed grows invalidate the OSC like Zig. Tested with a counting/failing/limiting C allocator. - shadertoy: sampler parameter rewriting is keyed by full signature so overloads work; the Metal uniform block keeps std140 strides for iChannelTime[4] (MSL offsets checked against the upload layout). - tripwire.zig ported (named trip points, hit counts, verification) and used to port the previously skipped allocation-failure tests that have a fallible Rust path; the rest are documented. - crash: fatal signals capture siginfo + registers and a forked helper writes a minidump (minidump-writer) as an envelope attachment, like sentry-native/Breakpad; tests parse it with the minidump crate. - kitty LoadingImage::debug_dump (debug builds) and the Darwin Instruments signpost around benchmark runs. Test Plan: - cargo test -p ghostty-vt-capi --test c_allocator - cargo test -p ghostty-crash --test minidump
166 lines
5.8 KiB
Rust
166 lines
5.8 KiB
Rust
//! The fatal-signal path writes an envelope whose second item is a minidump
|
|
//! (`event.minidump` attachment) of the crashed process. A child process
|
|
//! (this test executable re-run with `GHOSTTY_MD_CHILD`) crashes on a spawned
|
|
//! thread; the parent parses the minidump with the `minidump` crate and checks
|
|
//! that it contains the crashing thread, its stack and the exception.
|
|
#![cfg(target_os = "linux")]
|
|
#![allow(clippy::unwrap_used, unsafe_code)]
|
|
|
|
use std::{os::unix::process::ExitStatusExt, path::Path, process::Command};
|
|
|
|
use ghostty_crash::{
|
|
Options,
|
|
envelope::{Envelope, Item, ItemType},
|
|
};
|
|
use minidump::{
|
|
Minidump,
|
|
MinidumpException,
|
|
MinidumpMemoryList,
|
|
MinidumpModuleList,
|
|
MinidumpSystemInfo,
|
|
MinidumpThreadList,
|
|
Module,
|
|
};
|
|
|
|
fn child_main(mode: &str) {
|
|
let dir = std::env::var("GHOSTTY_CRASH_DIR").unwrap();
|
|
let tid_file = std::env::var("GHOSTTY_MD_TID").unwrap();
|
|
ghostty_crash::install(&Options {
|
|
dir: Some(dir.into()),
|
|
..Options::default()
|
|
})
|
|
.unwrap();
|
|
let mode = mode.to_owned();
|
|
// Crash on a non-main thread so "the crashing thread" is distinguishable
|
|
// from the thread that is merely waiting.
|
|
let h = std::thread::Builder::new()
|
|
.name("crasher".into())
|
|
.spawn(move || {
|
|
// SAFETY: gettid has no preconditions.
|
|
let tid = unsafe { libc::syscall(libc::SYS_gettid) };
|
|
std::fs::write(tid_file, tid.to_string()).unwrap();
|
|
match mode.as_str() {
|
|
"segv" => {
|
|
// SAFETY: intentionally invalid write to fault.
|
|
unsafe { std::ptr::write_volatile(8 as *mut u8, 1) };
|
|
}
|
|
"abort" => std::process::abort(),
|
|
"overflow" => {
|
|
// Unbounded on purpose: overflowing the stack is the test.
|
|
#[allow(unconditional_recursion)]
|
|
fn recurse(n: u64) -> u64 {
|
|
let pad = [n as u8; 256];
|
|
std::hint::black_box(&pad);
|
|
recurse(n + 1) + u64::from(pad[0])
|
|
}
|
|
std::process::exit(i32::from(recurse(0) == 1));
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
})
|
|
.unwrap();
|
|
let _ = h.join();
|
|
std::thread::sleep(std::time::Duration::from_secs(30));
|
|
}
|
|
|
|
#[test]
|
|
fn minidump_child() {
|
|
if let Ok(mode) = std::env::var("GHOSTTY_MD_CHILD") {
|
|
child_main(&mode);
|
|
}
|
|
}
|
|
|
|
/// Run the crashing child; returns (exit status, crashing tid, temp dir).
|
|
fn crash(mode: &str) -> (std::process::ExitStatus, i32, tempfile::TempDir) {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let dir = t.path().join("crash");
|
|
let tid_file = t.path().join("tid");
|
|
let st = Command::new(std::env::current_exe().unwrap())
|
|
.args([
|
|
"--exact",
|
|
"minidump_child",
|
|
"--nocapture",
|
|
"--test-threads=1",
|
|
])
|
|
.env("GHOSTTY_MD_CHILD", mode)
|
|
.env("GHOSTTY_CRASH_DIR", &dir)
|
|
.env("GHOSTTY_MD_TID", &tid_file)
|
|
.env("RUST_BACKTRACE", "0")
|
|
.status()
|
|
.unwrap();
|
|
let tid = std::fs::read_to_string(tid_file).unwrap().parse().unwrap();
|
|
(st, tid, t)
|
|
}
|
|
|
|
fn only_report(dir: &Path) -> Vec<u8> {
|
|
let files: Vec<_> = std::fs::read_dir(dir)
|
|
.unwrap()
|
|
.map(|e| e.unwrap().path())
|
|
.collect();
|
|
assert_eq!(1, files.len(), "{files:?}");
|
|
std::fs::read(&files[0]).unwrap()
|
|
}
|
|
|
|
fn check(mode: &str, signal: i32) {
|
|
let (st, tid, t) = crash(mode);
|
|
assert_eq!(Some(signal), st.signal(), "{st:?}");
|
|
|
|
// Envelope: event + minidump attachment.
|
|
let data = only_report(&t.path().join("crash"));
|
|
let mut env = Envelope::parse_slice(&data).unwrap();
|
|
assert_eq!(2, env.items.len(), "no minidump attached");
|
|
assert_eq!(ItemType::Event, env.items[0].item_type());
|
|
env.items[1].decode().unwrap();
|
|
let Item::Attachment(a) = &env.items[1] else {
|
|
panic!("second item is not an attachment")
|
|
};
|
|
assert_eq!("minidump.dmp", a.filename);
|
|
assert_eq!(Some("event.minidump"), a.attachment_type.as_deref());
|
|
|
|
// The minidump parses.
|
|
let dump = Minidump::read(a.payload.clone()).expect("minidump parses");
|
|
|
|
// The crashing thread is in the thread list, with stack memory.
|
|
let threads = dump.get_stream::<MinidumpThreadList>().unwrap();
|
|
let ids: Vec<u32> = threads.threads.iter().map(|t| t.raw.thread_id).collect();
|
|
let crasher = threads
|
|
.threads
|
|
.iter()
|
|
.find(|th| th.raw.thread_id == tid as u32)
|
|
.unwrap_or_else(|| panic!("tid {tid} not in {ids:?}"));
|
|
assert!(crasher.raw.stack.memory.data_size > 0, "no stack captured");
|
|
assert!(ids.len() >= 2, "main thread missing: {ids:?}");
|
|
|
|
// The exception stream blames that thread and carries registers + signal.
|
|
let exc = dump.get_stream::<MinidumpException>().unwrap();
|
|
assert_eq!(tid as u32, exc.get_crashing_thread_id());
|
|
assert_eq!(signal as u32, exc.raw.exception_record.exception_code);
|
|
let sys = dump.get_stream::<MinidumpSystemInfo>().unwrap();
|
|
let ctx = exc
|
|
.context(&sys, None)
|
|
.expect("exception context (registers)");
|
|
assert!(ctx.valid_registers().count() > 10);
|
|
|
|
// Modules and memory were captured (needed to symbolicate/stack-walk).
|
|
let modules = dump.get_stream::<MinidumpModuleList>().unwrap();
|
|
assert!(modules.iter().count() > 1);
|
|
assert!(modules.iter().any(|m| m.code_file().contains("minidump")));
|
|
let mem = dump.get_stream::<MinidumpMemoryList>().unwrap();
|
|
assert!(mem.iter().count() > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn sigsegv_report_contains_minidump() {
|
|
check("segv", libc::SIGSEGV);
|
|
}
|
|
|
|
#[test]
|
|
fn abort_report_contains_minidump() {
|
|
check("abort", libc::SIGABRT);
|
|
}
|
|
|
|
#[test]
|
|
fn stack_overflow_report_contains_minidump() {
|
|
check("overflow", libc::SIGSEGV);
|
|
}
|