fix: C allocator contract, shadertoy overloads/MSL stride, minidumps
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
This commit is contained in:
1 parent
5568f99bae
commit
8af6ab68d3
22 files changed
+2079
-57
No files matched your search
@@ -19,7 +19,11 @@ thiserror = "2.0.21"
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
minidump-writer = "0.13.0"
|
||||
|
||||
[dev-dependencies]
|
||||
minidump = "0.27.0"
|
||||
tempfile = "3.27.0"
|
||||
|
||||
# The fatal-signal handler needs sigaction/sigaltstack/raw syscalls. Unsafe is
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
pub mod context;
|
||||
pub mod dir;
|
||||
pub mod envelope;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod minidump;
|
||||
pub mod sentry;
|
||||
#[cfg(unix)]
|
||||
mod signal;
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
//! Minidump capture for fatal signals (Linux), the counterpart of what
|
||||
//! sentry-native's Breakpad/crashpad backend does upstream.
|
||||
//!
|
||||
//! A process cannot ptrace itself, so (like Breakpad's `ExceptionHandler`)
|
||||
//! the signal handler forks a short-lived child, allows it to ptrace the
|
||||
//! crashing process (`PR_SET_PTRACER`), and waits. The child uses
|
||||
//! `minidump-writer` to snapshot the parent -- every thread's registers and
|
||||
//! stack, the module list, memory mappings and, for the crashing thread, the
|
||||
//! `siginfo` + machine context captured by the handler -- and writes the final
|
||||
//! envelope: the event item followed by the minidump as an
|
||||
//! `event.minidump` attachment. If anything fails (ptrace forbidden by a
|
||||
//! sandbox, the child dies or times out) the caller falls back to the plain
|
||||
//! event-only envelope written from the signal-safe path.
|
||||
//!
|
||||
//! The fork uses the raw `clone` syscall without `CLONE_VM`, so no `atfork`
|
||||
//! handlers (which could take locks the crashed thread holds) run. The child
|
||||
//! allocates, which could in theory deadlock on a copied locked malloc arena;
|
||||
//! the parent therefore enforces a timeout and kills the child.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{
|
||||
cell::UnsafeCell,
|
||||
io::{Cursor, Write},
|
||||
mem::MaybeUninit,
|
||||
os::fd::FromRawFd,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use minidump_writer::{
|
||||
CrashContextExt,
|
||||
crash_context::{CrashContext, ucontext_t},
|
||||
minidump_writer::MinidumpWriterConfig,
|
||||
};
|
||||
|
||||
/// Stack size of the dumping child.
|
||||
const STACK: usize = 4 << 20;
|
||||
|
||||
/// How long the crashing process waits for the dumping child.
|
||||
const TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
/// Everything the child needs; borrowed from the handler's scratch space.
|
||||
pub(crate) struct Capture<'a> {
|
||||
/// NUL-terminated destination path of the envelope.
|
||||
pub path: &'a [u8],
|
||||
/// Envelope header line + event item header line.
|
||||
pub head: &'a [u8],
|
||||
/// The event JSON payload.
|
||||
pub event: &'a [u8],
|
||||
}
|
||||
|
||||
/// The crash context lives in static memory: the handler may run on a tiny
|
||||
/// alternate stack (Rust's default is a few KiB) that cannot hold a ~2 KiB
|
||||
/// struct plus the temporaries of building it.
|
||||
struct CcCell(UnsafeCell<MaybeUninit<CrashContext>>);
|
||||
// SAFETY: access is serialized by the caller (`signal::BUSY`).
|
||||
unsafe impl Sync for CcCell {}
|
||||
static CC: CcCell = CcCell(UnsafeCell::new(MaybeUninit::zeroed()));
|
||||
|
||||
/// Fill the static `crash-context` description of the fault from the
|
||||
/// handler's arguments and return it.
|
||||
///
|
||||
/// # Safety
|
||||
/// `info` and `uc` must be the pointers the kernel passed to an `SA_SIGINFO`
|
||||
/// handler (or null), and the caller must have exclusive access to the static
|
||||
/// (the crash flag `signal::BUSY`).
|
||||
pub(crate) unsafe fn crash_context(
|
||||
info: *const libc::siginfo_t,
|
||||
uc: *const libc::c_void,
|
||||
) -> &'static CrashContext {
|
||||
// SAFETY: exclusive access per the contract; CrashContext is plain old
|
||||
// data for which the all-zero pattern is valid.
|
||||
let cc: &'static mut CrashContext = unsafe { (*CC.0.get()).assume_init_mut() };
|
||||
// SAFETY: gettid/getpid have no preconditions.
|
||||
cc.tid = unsafe { libc::syscall(libc::SYS_gettid) } as libc::pid_t;
|
||||
// SAFETY: as above.
|
||||
cc.pid = unsafe { libc::getpid() };
|
||||
|
||||
// SAFETY: per the function contract; the kernel's siginfo is valid.
|
||||
if let Some(info) = unsafe { info.as_ref() } {
|
||||
cc.siginfo.ssi_signo = info.si_signo as u32;
|
||||
cc.siginfo.ssi_code = info.si_code;
|
||||
cc.siginfo.ssi_errno = info.si_errno;
|
||||
// SAFETY: si_addr is valid for fault signals; for others it is
|
||||
// unspecified but reading it is harmless.
|
||||
cc.siginfo.ssi_addr = unsafe { info.si_addr() } as u64;
|
||||
}
|
||||
if !uc.is_null() {
|
||||
// SAFETY: the kernel passes a ucontext whose layout is arch-specific
|
||||
// and identical to `crash_context::ucontext_t` (the premise of that
|
||||
// crate); `uc` is valid for reads of that size and `cc.context` for
|
||||
// writes; they cannot overlap (static vs. signal frame).
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(uc.cast::<ucontext_t>(), &raw mut cc.context, 1);
|
||||
}
|
||||
#[cfg(not(target_arch = "arm"))]
|
||||
{
|
||||
let fp = cc.context.uc_mcontext.fpregs;
|
||||
if !fp.is_null() {
|
||||
// SAFETY: the kernel points fpregs at the saved FP state in
|
||||
// the signal frame; the destination is our static.
|
||||
unsafe { std::ptr::copy_nonoverlapping(fp, &raw mut cc.float_state, 1) };
|
||||
}
|
||||
}
|
||||
}
|
||||
cc
|
||||
}
|
||||
|
||||
/// Fork the dumping child and wait for it. Returns true if the envelope with
|
||||
/// the minidump has been written to `cap.path`.
|
||||
pub(crate) fn capture(cap: &Capture<'_>, cc: &CrashContext) -> bool {
|
||||
// Allow any process (our child) to ptrace us under Yama's ptrace_scope=1.
|
||||
// SAFETY: prctl with these arguments only changes our own ptracer policy.
|
||||
unsafe {
|
||||
libc::prctl(
|
||||
libc::PR_SET_PTRACER,
|
||||
libc::PR_SET_PTRACER_ANY as libc::c_ulong,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
|
||||
// The handler runs on a small alternate stack, so the child gets its own.
|
||||
// SAFETY: anonymous private mapping; checked for failure below.
|
||||
let stack = unsafe {
|
||||
libc::mmap(
|
||||
std::ptr::null_mut(),
|
||||
STACK,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_NORESERVE | libc::MAP_STACK,
|
||||
-1,
|
||||
0,
|
||||
)
|
||||
};
|
||||
if stack == libc::MAP_FAILED {
|
||||
return false;
|
||||
}
|
||||
let mut arg = ChildArg { cap, cc };
|
||||
// SAFETY: fork-style clone (no CLONE_VM, so the child gets a private
|
||||
// copy-on-write image and `arg` stays valid in it) running `child_entry`
|
||||
// on the fresh stack; glibc's wrapper runs no atfork handlers.
|
||||
let pid = unsafe {
|
||||
libc::clone(
|
||||
child_entry,
|
||||
stack.cast::<u8>().add(STACK).cast(),
|
||||
libc::SIGCHLD,
|
||||
(&raw mut arg).cast(),
|
||||
)
|
||||
};
|
||||
if pid < 0 {
|
||||
// SAFETY: unmapping our own mapping.
|
||||
unsafe { libc::munmap(stack, STACK) };
|
||||
return false;
|
||||
}
|
||||
let r = wait_for(pid);
|
||||
// SAFETY: the child is gone; unmapping our own mapping.
|
||||
unsafe { libc::munmap(stack, STACK) };
|
||||
r
|
||||
}
|
||||
|
||||
fn wait_for(pid: libc::pid_t) -> bool {
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
let mut status = 0;
|
||||
// SAFETY: waitpid with a valid out pointer.
|
||||
let r = unsafe { libc::waitpid(pid, &raw mut status, libc::WNOHANG) };
|
||||
if r == pid {
|
||||
return libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0;
|
||||
}
|
||||
if r < 0 && std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted {
|
||||
return false;
|
||||
}
|
||||
if start.elapsed() > TIMEOUT {
|
||||
// SAFETY: kill/waitpid on our own child.
|
||||
unsafe {
|
||||
libc::kill(pid, libc::SIGKILL);
|
||||
libc::waitpid(pid, &raw mut status, 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let ts = libc::timespec {
|
||||
tv_sec: 0,
|
||||
tv_nsec: 5_000_000,
|
||||
};
|
||||
// SAFETY: nanosleep with valid pointers.
|
||||
unsafe { libc::nanosleep(&raw const ts, std::ptr::null_mut()) };
|
||||
}
|
||||
}
|
||||
|
||||
struct ChildArg<'a, 'b> {
|
||||
cap: &'a Capture<'b>,
|
||||
cc: &'a CrashContext,
|
||||
}
|
||||
|
||||
extern "C" fn child_entry(arg: *mut libc::c_void) -> libc::c_int {
|
||||
// SAFETY: `arg` points to the parent's `ChildArg`, copied into this
|
||||
// process by the clone.
|
||||
let arg = unsafe { &*arg.cast::<ChildArg<'_, '_>>() };
|
||||
i32::from(!child_main(arg.cap, arg.cc))
|
||||
}
|
||||
|
||||
fn child_main(cap: &Capture<'_>, cc: &CrashContext) -> bool {
|
||||
#[allow(clippy::result_large_err)]
|
||||
let attempt = |cc: &CrashContext| {
|
||||
let mut writer = MinidumpWriterConfig::new(cc.pid, cc.tid);
|
||||
writer.set_crash_context(CrashContextExt { inner: cc.clone() });
|
||||
let mut sink = Cursor::new(Vec::new());
|
||||
writer
|
||||
.write(&mut sink)
|
||||
.map(|d| if d.is_empty() { sink.into_inner() } else { d })
|
||||
};
|
||||
let dump = match attempt(cc) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
// The child may allocate; say why the first try failed.
|
||||
let _ = writeln!(
|
||||
std::io::stderr(),
|
||||
"warning(sentry): minidump failed err={e}"
|
||||
);
|
||||
// A stack overflow faults with the stack pointer in the guard
|
||||
// page, which the writer cannot read a stack from. Retry with
|
||||
// the pointer moved to the first readable address above it so
|
||||
// the thread's stack is still captured.
|
||||
let mut fixed = cc.clone();
|
||||
if !clamp_stack_pointer(&mut fixed) {
|
||||
return false;
|
||||
}
|
||||
match attempt(&fixed) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
let _ = writeln!(
|
||||
std::io::stderr(),
|
||||
"warning(sentry): minidump retry failed err={e}"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
write_envelope(cap, &dump)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
fn sp_mut(cc: &mut CrashContext) -> &mut i64 {
|
||||
&mut cc.context.uc_mcontext.gregs[libc::REG_RSP as usize]
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
fn sp_mut(cc: &mut CrashContext) -> &mut u64 {
|
||||
&mut cc.context.uc_mcontext.sp
|
||||
}
|
||||
|
||||
/// If the page holding the stack pointer cannot be read (a stack overflow
|
||||
/// faults in/at the guard page), move the pointer up to the first readable page
|
||||
/// above it (within the 1 MiB the kernel's stack guard gap can span). Returns
|
||||
/// whether it was changed.
|
||||
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
|
||||
fn clamp_stack_pointer(cc: &mut CrashContext) -> bool {
|
||||
const PAGE: u64 = 4096;
|
||||
let pid = cc.pid;
|
||||
let readable = |addr: u64| {
|
||||
let mut b = [0u8; 8];
|
||||
let local = libc::iovec {
|
||||
iov_base: b.as_mut_ptr().cast(),
|
||||
iov_len: b.len(),
|
||||
};
|
||||
let remote = libc::iovec {
|
||||
iov_base: addr as *mut libc::c_void,
|
||||
iov_len: b.len(),
|
||||
};
|
||||
// SAFETY: both iovecs describe valid ranges (the remote one is only
|
||||
// interpreted by the kernel in the other process).
|
||||
unsafe { libc::process_vm_readv(pid, &raw const local, 1, &raw const remote, 1, 0) == 8 }
|
||||
};
|
||||
let sp = *sp_mut(cc) as u64;
|
||||
let base = sp & !(PAGE - 1);
|
||||
if readable(base) {
|
||||
return false;
|
||||
}
|
||||
let mut addr = base + PAGE;
|
||||
while addr <= base + (1 << 20) {
|
||||
if readable(addr) {
|
||||
*sp_mut(cc) = addr as _;
|
||||
return true;
|
||||
}
|
||||
addr += PAGE;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
fn clamp_stack_pointer(_cc: &mut CrashContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn write_envelope(cap: &Capture<'_>, dump: &[u8]) -> bool {
|
||||
// SAFETY: `path` is NUL terminated by the caller.
|
||||
let fd = unsafe {
|
||||
libc::open(
|
||||
cap.path.as_ptr().cast(),
|
||||
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC,
|
||||
0o644,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: we own the fd just opened.
|
||||
let mut f = unsafe { std::fs::File::from_raw_fd(fd) };
|
||||
let item_head = format!(
|
||||
"\n{{\"type\":\"attachment\",\"length\":{},\"filename\":\"minidump.dmp\",\"attachment_type\":\"event.minidump\"}}\n",
|
||||
dump.len()
|
||||
);
|
||||
let ok = f
|
||||
.write_all(cap.head)
|
||||
.and_then(|()| f.write_all(cap.event))
|
||||
.and_then(|()| f.write_all(item_head.as_bytes()))
|
||||
.and_then(|()| f.write_all(dump))
|
||||
.and_then(|()| f.write_all(b"\n"))
|
||||
.is_ok();
|
||||
drop(f);
|
||||
if !ok {
|
||||
// SAFETY: remove the partial file so the caller can fall back.
|
||||
unsafe { libc::unlink(cap.path.as_ptr().cast()) };
|
||||
}
|
||||
ok
|
||||
}
|
||||
@@ -133,7 +133,7 @@ fn mkdir_p(path: &mut [u8], len: usize) {
|
||||
|
||||
/// 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) {
|
||||
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;
|
||||
@@ -191,6 +191,30 @@ fn write_signal_report(name: &str, desc: &str) {
|
||||
|
||||
// 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(
|
||||
@@ -212,9 +236,9 @@ fn write_signal_report(name: &str, desc: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handler(sig: libc::c_int, _info: *mut libc::siginfo_t, _ctx: *mut libc::c_void) {
|
||||
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) {
|
||||
write_signal_report(name, desc);
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
//! 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);
|
||||
}
|
||||
Reference in new issue
Block a user