diff --git a/codex-rs/utils/pty/src/pipe.rs b/codex-rs/utils/pty/src/pipe.rs index d77c11383c..f62d121793 100644 --- a/codex-rs/utils/pty/src/pipe.rs +++ b/codex-rs/utils/pty/src/pipe.rs @@ -119,6 +119,8 @@ async fn spawn_process_with_stdin_mode( unsafe { command.pre_exec(move || { crate::process_group::detach_from_tty()?; + #[cfg(target_os = "macos")] + let _ = crate::process_group::reset_to_system_crash_reporter(); #[cfg(target_os = "linux")] crate::process_group::set_parent_death_signal(parent_pid)?; Ok(()) diff --git a/codex-rs/utils/pty/src/process_group.rs b/codex-rs/utils/pty/src/process_group.rs index dadff29f9e..fcbdf4126e 100644 --- a/codex-rs/utils/pty/src/process_group.rs +++ b/codex-rs/utils/pty/src/process_group.rs @@ -19,6 +19,13 @@ use std::io; use tokio::process::Child; +#[cfg(target_os = "macos")] +use libc::c_char; +#[cfg(target_os = "macos")] +use libc::c_int; +#[cfg(target_os = "macos")] +use libc::c_uint; + #[cfg(target_os = "linux")] /// Ensure the child receives SIGTERM when the original parent dies. /// @@ -44,6 +51,103 @@ pub fn set_parent_death_signal(_parent_pid: i32) -> io::Result<()> { Ok(()) } +#[cfg(target_os = "macos")] +/// Reset exception ports to the system crash reporter. +/// +/// This breaks any inherited connection to a wrapping crash handler before +/// `exec`, so crashes in the spawned process are reported by the system. +pub fn reset_to_system_crash_reporter() -> bool { + type MachPort = c_uint; + type KernReturn = c_int; + type ExceptionMask = c_uint; + type ExceptionBehavior = c_int; + type ThreadStateFlavor = c_int; + + const KERN_SUCCESS: KernReturn = 0; + const MACH_PORT_NULL: MachPort = 0; + const MACH_PORT_DEAD: MachPort = MachPort::MAX; + + const EXC_CRASH: u32 = 10; + const EXC_RESOURCE: u32 = 11; + const EXC_GUARD: u32 = 12; + + const EXC_MASK_CRASH: ExceptionMask = 1 << EXC_CRASH; + const EXC_MASK_RESOURCE: ExceptionMask = 1 << EXC_RESOURCE; + const EXC_MASK_GUARD: ExceptionMask = 1 << EXC_GUARD; + + const EXCEPTION_STATE_IDENTITY: ExceptionBehavior = 3; + const MACH_EXCEPTION_CODES: ExceptionBehavior = 0x8000_0000u32 as ExceptionBehavior; + + #[cfg(target_arch = "aarch64")] + const MACHINE_THREAD_STATE: ThreadStateFlavor = 1; + #[cfg(target_arch = "x86_64")] + const MACHINE_THREAD_STATE: ThreadStateFlavor = 7; + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + const MACHINE_THREAD_STATE: ThreadStateFlavor = 1; + + unsafe extern "C" { + static bootstrap_port: MachPort; + fn bootstrap_look_up( + bp: MachPort, + service_name: *const c_char, + sp: *mut MachPort, + ) -> KernReturn; + fn mach_task_self() -> MachPort; + fn task_set_exception_ports( + task: MachPort, + exception_mask: ExceptionMask, + new_port: MachPort, + behavior: ExceptionBehavior, + new_flavor: ThreadStateFlavor, + ) -> KernReturn; + } + + const REPORT_CRASH_SERVICE: &[u8] = b"com.apple.ReportCrash\0"; + let service_name = REPORT_CRASH_SERVICE.as_ptr().cast::(); + + let mut report_crash: MachPort = MACH_PORT_NULL; + let lookup = unsafe { + bootstrap_look_up( + bootstrap_port, + service_name, + &mut report_crash as *mut MachPort, + ) + }; + if lookup != KERN_SUCCESS || report_crash == MACH_PORT_DEAD || report_crash == MACH_PORT_NULL { + return false; + } + + // Best-effort: include resource/guard masks, then fall back to crash-only + // when those exceptions are unsupported in this context. + let mask = EXC_MASK_CRASH | EXC_MASK_RESOURCE | EXC_MASK_GUARD; + let behavior = EXCEPTION_STATE_IDENTITY | MACH_EXCEPTION_CODES; + + let task = unsafe { mach_task_self() }; + let primary = unsafe { + task_set_exception_ports(task, mask, report_crash, behavior, MACHINE_THREAD_STATE) + }; + if primary == KERN_SUCCESS { + return true; + } + + let fallback = unsafe { + task_set_exception_ports( + task, + EXC_MASK_CRASH, + report_crash, + behavior, + MACHINE_THREAD_STATE, + ) + }; + fallback == KERN_SUCCESS +} + +#[cfg(not(target_os = "macos"))] +/// No-op on non-macOS platforms. +pub fn reset_to_system_crash_reporter() -> bool { + true +} + #[cfg(unix)] /// Detach from the controlling TTY by starting a new session. pub fn detach_from_tty() -> io::Result<()> {