// run-pass // no-prefer-dynamic // ignore-wasm32-bare no libc // ignore-windows // ignore-sgx no libc // ignore-emscripten no processes // ignore-sgx no processes // ignore-fuchsia no fork #![feature(rustc_private)] #![feature(never_type)] #![feature(panic_always_abort)] extern crate libc; use std::alloc::{GlobalAlloc, Layout}; use std::fmt; use std::panic::{self, panic_any}; use std::os::unix::process::{CommandExt, ExitStatusExt}; use std::process::{self, Command, ExitStatus}; use std::sync::atomic::{AtomicU32, Ordering}; use libc::c_int; /// This stunt allocator allows us to spot heap allocations in the child. struct PidChecking { parent: A, require_pid: AtomicU32, } #[global_allocator] static ALLOCATOR: PidChecking = PidChecking { parent: std::alloc::System, require_pid: AtomicU32::new(0), }; impl PidChecking { fn engage(&self) { let parent_pid = process::id(); eprintln!("engaging allocator trap, parent pid={}", parent_pid); self.require_pid.store(parent_pid, Ordering::Release); } fn check(&self) { let require_pid = self.require_pid.load(Ordering::Acquire); if require_pid != 0 { let actual_pid = process::id(); if require_pid != actual_pid { unsafe { libc::raise(libc::SIGUSR1); } } } } } unsafe impl GlobalAlloc for PidChecking { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { self.check(); self.parent.alloc(layout) } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { self.check(); self.parent.dealloc(ptr, layout) } unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { self.check(); self.parent.alloc_zeroed(layout) } unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { self.check(); self.parent.realloc(ptr, layout, new_size) } } fn expect_aborted(status: ExitStatus) { dbg!(status); let signal = status.signal().expect("expected child process to die of signal"); #[cfg(not(target_os = "android"))] assert!(signal == libc::SIGABRT || signal == libc::SIGILL || signal == libc::SIGTRAP); #[cfg(target_os = "android")] { // Android signals an abort() call with SIGSEGV at address 0xdeadbaad // See e.g. https://groups.google.com/g/android-ndk/c/laW1CJc7Icc assert!(signal == libc::SIGSEGV); // Additional checks performed: // 1. Find last tombstone (similar to coredump but in text format) from the // same executable (path) as we are (must be because of usage of fork): // This ensures that we look into the correct tombstone. // 2. Cause of crash is a SIGSEGV with address 0xdeadbaad. // 3. libc::abort call is in one of top two functions on callstack. // The last two steps distinguish between a normal SIGSEGV and one caused // by libc::abort. let this_exe = std::env::current_exe().unwrap().into_os_string().into_string().unwrap(); let exe_string = format!(">>> {this_exe} <<<"); let tombstone = (0..100) .map(|n| format!("/data/tombstones/tombstone_{n:02}")) .filter(|f| std::path::Path::new(&f).exists()) .map(|f| std::fs::read_to_string(&f).expect("Cannot read tombstone file")) .filter(|f| f.contains(&exe_string)) .last() .expect("no tombstone found"); println!("Content of tombstone:\n{tombstone}"); assert!( tombstone.contains("signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr deadbaad") ); let abort_on_top = tombstone .lines() .skip_while(|l| !l.contains("backtrace:")) .skip(1) .take_while(|l| l.starts_with(" #")) .take(2) .any(|f| f.contains("/system/lib/libc.so (abort")); assert!(abort_on_top); } } fn main() { ALLOCATOR.engage(); fn run(do_panic: &dyn Fn()) -> ExitStatus { let child = unsafe { libc::fork() }; assert!(child >= 0); if child == 0 { panic::always_abort(); do_panic(); process::exit(0); } let mut status: c_int = 0; let got = unsafe { libc::waitpid(child, &mut status, 0) }; assert_eq!(got, child); let status = ExitStatus::from_raw(status.into()); status } fn one(do_panic: &dyn Fn()) { let status = run(do_panic); expect_aborted(status); } one(&|| panic!()); one(&|| panic!("some message")); one(&|| panic!("message with argument: {}", 42)); #[derive(Debug)] struct Wotsit { } one(&|| panic_any(Wotsit { })); let mut c = Command::new("echo"); unsafe { c.pre_exec(|| panic!("{}", "crash now!")); } let st = c.status().expect("failed to get command status"); expect_aborted(st); struct DisplayWithHeap; impl fmt::Display for DisplayWithHeap { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { let s = vec![0; 100]; let s = std::hint::black_box(s); write!(f, "{:?}", s) } } // Some panics in the stdlib that we want not to allocate, as // otherwise these facilities become impossible to use in the // child after fork, which is really quite awkward. one(&|| { None::.unwrap(); }); one(&|| { None::.expect("unwrapped a none"); }); one(&|| { std::str::from_utf8(b"\xff").unwrap(); }); one(&|| { let x = [0, 1, 2, 3]; let y = x[std::hint::black_box(4)]; let _z = std::hint::black_box(y); }); // Finally, check that our stunt allocator can actually catch an allocation after fork. // ie, that our test is effective. let status = run(&|| panic!("allocating to display... {}", DisplayWithHeap)); dbg!(status); assert_eq!(status.signal(), Some(libc::SIGUSR1)); }