#![cfg(target_os = "macos")]
#![allow(unsafe_code)]
use crash_handler as ch;
use std::{
os::unix::process::ExitStatusExt,
process::{Command, ExitStatus, Stdio},
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
const CHILD_CASE: &str = "CRASH_HANDLER_FOREIGN_TASK_CASE";
const EXIT_NOT_INHERITED: i32 = 3;
const EXIT_CAUGHT: i32 = 42;
const DEADLINE: Duration = Duration::from_secs(10);
static PARENT_CALLBACK_INVOKED: AtomicBool = AtomicBool::new(false);
#[derive(Debug)]
enum Expected {
Signal(&'static [i32]),
Exit(i32),
}
impl Expected {
fn matches(&self, status: ExitStatus) -> bool {
match self {
Self::Signal(signals) => status.signal().is_some_and(|sig| signals.contains(&sig)),
Self::Exit(code) => status.code() == Some(*code),
}
}
}
#[test]
fn passes_on_foreign_task_exceptions() {
if let Ok(case) = std::env::var(CHILD_CASE) {
run_child(&case);
}
let _handler = unsafe {
ch::CrashHandler::attach(ch::make_crash_event(|_cc: &ch::CrashContext| {
PARENT_CALLBACK_INVOKED.store(true, Ordering::SeqCst);
ch::CrashEventResult::Handled(false)
}))
.unwrap()
};
let cases = [
("segv", Expected::Signal(&[libc::SIGSEGV, libc::SIGBUS])),
("sigcatch", Expected::Exit(EXIT_CAUGHT)),
("illegal", Expected::Signal(&[libc::SIGILL])),
("trap", Expected::Signal(&[libc::SIGTRAP])),
];
let dead_names_before = dead_port_names();
let failures: Vec<_> = cases
.iter()
.filter_map(|(case, expected)| match spawn_child(case) {
Some(status) if status.code() == Some(EXIT_NOT_INHERITED) => Some(format!(
"{case}: child did not inherit the handler's exception port, so the test is vacuous"
)),
Some(status) if expected.matches(status) => None,
Some(status) => Some(format!("{case}: expected {expected:?}, got {status}")),
None => Some(format!(
"{case}: child was still running after {DEADLINE:?} and was killed"
)),
})
.collect();
assert!(failures.is_empty(), "{failures:#?}");
assert!(
!PARENT_CALLBACK_INVOKED.load(Ordering::SeqCst),
"the parent's crash callback was invoked for a child's exception"
);
assert_eq!(
dead_names_before,
dead_port_names(),
"port rights received with the children's exceptions were leaked"
);
}
fn spawn_child(case: &str) -> Option<ExitStatus> {
let mut child = Command::new(std::env::current_exe().unwrap())
.args([
"passes_on_foreign_task_exceptions",
"--exact",
"--nocapture",
])
.env(CHILD_CASE, case)
.stdout(Stdio::null())
.spawn()
.expect("failed to spawn child");
let start = Instant::now();
loop {
if let Some(status) = child.try_wait().expect("failed to wait on child") {
return Some(status);
}
if start.elapsed() > DEADLINE {
let _res = child.kill();
let _res = child.wait();
return None;
}
std::thread::sleep(Duration::from_millis(50));
}
}
fn run_child(case: &str) -> ! {
unsafe {
if !inherited_exception_port() {
#[allow(clippy::exit)]
std::process::exit(EXIT_NOT_INHERITED);
}
match case {
"segv" => sadness_generator::raise_segfault(),
"sigcatch" => {
libc::signal(libc::SIGSEGV, exit_caught as *const () as usize);
libc::signal(libc::SIGBUS, exit_caught as *const () as usize);
sadness_generator::raise_segfault()
}
"illegal" => sadness_generator::raise_illegal_instruction(),
"trap" => sadness_generator::raise_trap(),
unknown => panic!("unknown case {unknown}"),
}
}
}
extern "C" fn exit_caught(_signal: i32) {
unsafe { libc::_exit(EXIT_CAUGHT) };
}
unsafe fn inherited_exception_port() -> bool {
use mach2::{
exception_types as et, kern_return::KERN_SUCCESS, port::MACH_PORT_NULL,
traps::mach_task_self,
};
const COUNT: usize = 14;
let mut count = COUNT as u32;
let mut masks = [0; COUNT];
let mut ports = [0; COUNT];
let mut behaviors = [0; COUNT];
let mut flavors = [0; COUNT];
let kr = unsafe {
mach2::task::task_get_exception_ports(
mach_task_self(),
et::EXC_MASK_BAD_ACCESS,
masks.as_mut_ptr(),
&mut count,
ports.as_mut_ptr(),
behaviors.as_mut_ptr(),
flavors.as_mut_ptr(),
)
};
kr == KERN_SUCCESS
&& (0..count as usize).any(|i| {
masks[i] & et::EXC_MASK_BAD_ACCESS != 0
&& ports[i] != MACH_PORT_NULL
&& behaviors[i] as u32 == et::EXCEPTION_DEFAULT | et::MACH_EXCEPTION_CODES
})
}
fn dead_port_names() -> usize {
use mach2::{
kern_return::{KERN_SUCCESS, kern_return_t},
mach_types::task_t,
message::mach_msg_type_number_t,
port::{MACH_PORT_RIGHT_DEAD_NAME, mach_port_name_t, mach_port_type_t},
traps::mach_task_self,
vm::mach_vm_deallocate,
};
const MACH_PORT_TYPE_DEAD_NAME: mach_port_type_t = 1 << (MACH_PORT_RIGHT_DEAD_NAME + 16);
unsafe extern "C" {
fn mach_port_names(
task: task_t,
names: *mut *mut mach_port_name_t,
names_count: *mut mach_msg_type_number_t,
types: *mut *mut mach_port_type_t,
types_count: *mut mach_msg_type_number_t,
) -> kern_return_t;
}
unsafe {
let mut names = std::ptr::null_mut();
let mut names_count = 0;
let mut types = std::ptr::null_mut();
let mut types_count = 0;
assert_eq!(
mach_port_names(
mach_task_self(),
&mut names,
&mut names_count,
&mut types,
&mut types_count,
),
KERN_SUCCESS
);
let dead = std::slice::from_raw_parts(types, types_count as usize)
.iter()
.filter(|ty| **ty & MACH_PORT_TYPE_DEAD_NAME != 0)
.count();
mach_vm_deallocate(
mach_task_self(),
names as _,
(names_count as usize * size_of::<mach_port_name_t>()) as _,
);
mach_vm_deallocate(
mach_task_self(),
types as _,
(types_count as usize * size_of::<mach_port_type_t>()) as _,
);
dead
}
}