diff --git a/ports/winit/crash_handler.rs b/ports/winit/crash_handler.rs index b80b70b651e..77adba159ae 100644 --- a/ports/winit/crash_handler.rs +++ b/ports/winit/crash_handler.rs @@ -8,24 +8,46 @@ pub fn install() {} #[cfg(any(target_os = "macos", target_os = "linux"))] pub fn install() { use crate::backtrace; - use libc::_exit; use sig::ffi::Sig; use std::{io::Write, sync::atomic, thread}; extern "C" fn handler(sig: i32) { + // Only print crash message and backtrace the first time, to avoid + // infinite recursion if the printing causes another signal. static BEEN_HERE_BEFORE: atomic::AtomicBool = atomic::AtomicBool::new(false); if !BEEN_HERE_BEFORE.swap(true, atomic::Ordering::SeqCst) { - let stdout = std::io::stdout(); - let mut stdout = stdout.lock(); - let _ = write!(&mut stdout, "Stack trace"); + // stderr is unbuffered, so we won’t lose output if we crash later + // in this handler, and the std::io::stderr() call never allocates. + // std::io::stdout() allocates the first time it’s called, which in + // practice will often segfault (see below). + let stderr = std::io::stderr(); + let mut stderr = stderr.lock(); + let _ = write!(&mut stderr, "Caught signal {sig}"); if let Some(name) = thread::current().name() { - let _ = write!(&mut stdout, " for thread \"{}\"", name); + let _ = write!(&mut stderr, " in thread \"{}\"", name); } - let _ = write!(&mut stdout, "\n"); - let _ = backtrace::print(&mut stdout); + let _ = write!(&mut stderr, "\n"); + + // This call always allocates, which in practice will segfault if + // we’re handling a non-main-thread (e.g. layout) segfault. Strictly + // speaking in POSIX terms, this is also undefined behaviour. + let _ = backtrace::print(&mut stderr); } + + // Outside the BEEN_HERE_BEFORE check, we must only call functions we + // know to be “async-signal-safe”, which includes sigaction(), raise(), + // and _exit(), but generally doesn’t include anything that allocates. + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03_03 unsafe { - _exit(sig); + // Reset the signal to the default action, and reraise the signal. + // Unlike libc::_exit(sig), which terminates the process normally, + // this terminates abnormally just like an uncaught signal, allowing + // mach (or your shell) to distinguish it from an ordinary exit, and + // allows your kernel to make a core dump if configured to do so. + let mut action: libc::sigaction = std::mem::zeroed(); + action.sa_sigaction = libc::SIG_DFL; + libc::sigaction(sig, &action, std::ptr::null_mut()); + libc::raise(sig); } } diff --git a/python/servo/post_build_commands.py b/python/servo/post_build_commands.py index f54d5a3f838..2450991080b 100644 --- a/python/servo/post_build_commands.py +++ b/python/servo/post_build_commands.py @@ -169,7 +169,10 @@ class PostBuildCommands(CommandBase): try: check_call(args, env=env) except subprocess.CalledProcessError as e: - print("Servo exited with return value %d" % e.returncode) + if e.returncode < 0: + print(f"Servo was terminated by signal {-e.returncode}") + else: + print(f"Servo exited with non-zero status {e.returncode}") return e.returncode except OSError as e: if e.errno == 2: