//! Unfortunately, sending a [`CrashContext`] to another process on Macos //! needs to be done via mach ports, as, for example, `mach_task_self` is a //! special handle that needs to be translated into the "actual" task when used //! by another process, this _might_ be possible completely in userspace, but //! examining the source code for this leads me to believe that there are enough //! footguns, particularly around security, that this might take a while, so for //! now, if you need to use a [`CrashContext`] across processes, you need //! to use the IPC mechanisms here to get meaningful/accurate data //! //! Note that in all cases of an optional timeout, a `None` will return //! immediately regardless of whether the messaged has been enqueued or //! dequeued from the kernel queue, so it is _highly_ recommended to use //! reasonable timeouts for sending and receiving messages between processes. use crate::CrashContext; use mach2::{ bootstrap, kern_return::KERN_SUCCESS, mach_port, message as msg, port, task, traps::mach_task_self, }; pub use mach2::{kern_return::kern_return_t, message::mach_msg_return_t}; use std::{ffi::CStr, time::Duration}; extern "C" { /// From , there is no binding for this in mach2 pub fn pid_for_task(task: port::mach_port_name_t, pid: *mut i32) -> kern_return_t; } /// #[repr(C, packed(4))] struct MachMsgPortDescriptor { name: u32, __pad1: u32, __pad2: u16, disposition: u8, __type: u8, } impl MachMsgPortDescriptor { fn new(name: u32, disposition: u32) -> Self { Self { name, disposition: disposition as u8, __pad1: 0, __pad2: 0, __type: msg::MACH_MSG_PORT_DESCRIPTOR as u8, } } } #[repr(C, packed(4))] struct MachMsgBody { pub descriptor_count: u32, } #[repr(C, packed(4))] pub struct MachMsgTrailer { pub kind: u32, pub size: u32, } /// #[repr(C, packed(4))] struct MachMsgHeader { pub bits: u32, pub size: u32, pub remote_port: u32, pub local_port: u32, pub voucher_port: u32, pub id: u32, } /// The actual crash context message sent and received. This message is a single /// struct since it needs to be contiguous block of memory. I suppose it's like /// this because people are expected to use MIG to generate the interface code, /// but it's ugly as hell regardless. #[repr(C, packed(4))] struct CrashContextMessage { head: MachMsgHeader, /// When providing port descriptors, this must be present to say how many /// of them follow the header and body body: MachMsgBody, // These are the really the critical piece of the payload, during // sending (or receiving?) these are turned into descriptors that // can actually be used by another process /// The task that crashed (ie `mach_task_self`) task: MachMsgPortDescriptor, /// The thread that crashed crash_thread: MachMsgPortDescriptor, /// The handler thread, probably, but not necessarily `mach_thread_self` handler_thread: MachMsgPortDescriptor, // Port opened by the client to receive an ack from the server ack_port: MachMsgPortDescriptor, /// Combination of the FLAG_* constants flags: u32, /// The exception type exception_kind: u32, /// The exception code exception_code: u64, /// The optional exception subcode exception_subcode: u64, /// We don't actually send this, but it's tacked on by the kernel :( trailer: MachMsgTrailer, } const FLAG_HAS_EXCEPTION: u32 = 0x1; const FLAG_HAS_SUBCODE: u32 = 0x2; /// Message sent from the [`Receiver`] upon receiving and handling a [`CrashContextMessage`] #[repr(C, packed(4))] struct AcknowledgementMessage { head: MachMsgHeader, result: u32, } /// An error that can occur while interacting with mach ports #[derive(Copy, Clone, Debug)] pub enum Error { /// A kernel error will generally indicate an error occurred while creating /// or modifying a mach port Kernel(kern_return_t), /// A message error indicates an error occurred while sending or receiving /// a message on a mach port Message(mach_msg_return_t), } impl std::error::Error for Error {} use std::fmt; impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // TODO: use a good string for the error codes write!(f, "{:?}", self) } } macro_rules! kern { ($call:expr) => {{ let res = $call; if res != KERN_SUCCESS { return Err(Error::Kernel(res)); } }}; } macro_rules! msg { ($call:expr) => {{ let res = $call; if res != msg::MACH_MSG_SUCCESS { return Err(Error::Message(res)); } }}; } /// Sends a [`CrashContext`] from a crashing process to another process running /// a [`Server`] with the same name pub struct Client { port: port::mach_port_t, } impl Client { /// Attempts to create a new client that can send messages to a [`Server`] /// that was created with the specified name. /// /// # Errors /// /// The specified port is not available for some reason, if you expect the /// port to be created you can retry this function until it connects. pub fn create(name: &CStr) -> Result { // SAFETY: syscalls. The user has no invariants to uphold, hence the // unsafe not being on the function as a whole unsafe { let mut task_bootstrap_port = 0; kern!(task::task_get_special_port( mach_task_self(), task::TASK_BOOTSTRAP_PORT, &mut task_bootstrap_port )); let mut port = 0; kern!(bootstrap::bootstrap_look_up( task_bootstrap_port, name.as_ptr(), &mut port )); Ok(Self { port }) } } /// Sends the specified [`CrashContext`] to a [`Server`]. /// /// If the ack from the [`Server`] times out `Ok(None)` is returned, otherwise /// it is the value specified in the [`Server`] process to [`Acknowledger::send_ack`] /// /// # Errors /// /// The send of the [`CrashContext`] or the receive of the ack fails. pub fn send_crash_context( &self, ctx: &CrashContext, send_timeout: Option, receive_timeout: Option, ) -> Result, Error> { // SAFETY: syscalls. Again, the user has no invariants to uphold, so // the function itself is not marked unsafe unsafe { // Create a new port to receive a response from the reciving end of // this port so we that we know when it has actually processed the // CrashContext, which is (presumably) interesting for the caller. If // that is not interesting they can set the receive_timeout to 0 to // just return immediately let mut ack_port = AckReceiver::new()?; let (flags, exception_kind, exception_code, exception_subcode) = if let Some(exc) = ctx.exception { ( FLAG_HAS_EXCEPTION | if exc.subcode.is_some() { FLAG_HAS_SUBCODE } else { 0 }, exc.kind, exc.code, exc.subcode.unwrap_or_default(), ) } else { (0, 0, 0, 0) }; let mut msg = CrashContextMessage { head: MachMsgHeader { bits: msg::MACH_MSG_TYPE_COPY_SEND | msg::MACH_MSGH_BITS_COMPLEX, // We don't send the trailer, that's added by the kernel size: std::mem::size_of::() as u32 - 8, remote_port: self.port, local_port: port::MACH_PORT_NULL, voucher_port: port::MACH_PORT_NULL, id: 0, }, body: MachMsgBody { descriptor_count: 4, }, task: MachMsgPortDescriptor::new(ctx.task, msg::MACH_MSG_TYPE_COPY_SEND), crash_thread: MachMsgPortDescriptor::new(ctx.thread, msg::MACH_MSG_TYPE_COPY_SEND), handler_thread: MachMsgPortDescriptor::new( ctx.handler_thread, msg::MACH_MSG_TYPE_COPY_SEND, ), ack_port: MachMsgPortDescriptor::new(ack_port.port, msg::MACH_MSG_TYPE_COPY_SEND), flags, exception_kind, exception_code, exception_subcode, // We don't actually send this but I didn't feel like making // two types trailer: MachMsgTrailer { kind: 0, size: 8 }, }; // Try to actually send the message to the Server msg!(msg::mach_msg( ((&mut msg.head) as *mut MachMsgHeader).cast(), msg::MACH_SEND_MSG | msg::MACH_SEND_TIMEOUT, msg.head.size, 0, port::MACH_PORT_NULL, send_timeout .map(|st| st.as_millis() as u32) .unwrap_or_default(), port::MACH_PORT_NULL )); // Wait for a response from the Server match ack_port.recv_ack(receive_timeout) { Ok(result) => Ok(Some(result)), Err(Error::Message(msg::MACH_RCV_TIMED_OUT)) => Ok(None), Err(e) => Err(e), } } } } /// Returned from [`Server::try_recv_crash_context`] when a [`Client`] has sent /// a crash context pub struct ReceivedCrashContext { /// The crash context sent by a [`Client`] pub crash_context: CrashContext, /// Allows the sending of an ack back to the [`Client`] to acknowledge that /// your code has received and processed the [`CrashContext`] pub acker: Acknowledger, /// The process id of the process the [`Client`] lives in. This is retrieved /// via `pid_for_task`. pub pid: u32, } /// Receives a [`CrashContext`] from another process pub struct Server { port: port::mach_port_t, } impl Server { /// Creates a new [`Server`] "bound" to the specified service name. /// /// # Errors /// /// We fail to acquire the bootstrap port, or fail to register the service. pub fn create(name: &CStr) -> Result { // SAFETY: syscalls. Again, the caller has no invariants to uphold, so // the entire function is not marked as unsafe unsafe { let mut task_bootstrap_port = 0; kern!(task::task_get_special_port( mach_task_self(), task::TASK_BOOTSTRAP_PORT, &mut task_bootstrap_port )); let mut port = 0; // Note that Breakpad uses bootstrap_register instead of this function as // MacOS 10.5 apparently deprecated bootstrap_register and then provided // bootstrap_check_in, but broken. However, 10.5 had its most recent update // over 13 years ago, and is not supported by Apple, so why should we? kern!(bootstrap::bootstrap_check_in( task_bootstrap_port, name.as_ptr(), &mut port, )); Ok(Self { port }) } } /// Attempts to retrieve a [`CrashContext`] sent from a crashing process. /// /// Note that in event of a timeout, this method will return `Ok(None)` to /// indicate that a crash context was unavailable rather than an error. /// /// # Errors /// /// We fail to receive the [`CrashContext`] message for a reason other than /// one not being in the queue, or we fail to translate the task identifier /// into a pid pub fn try_recv_crash_context( &mut self, timeout: Option, ) -> Result, Error> { // SAFETY: syscalls. The caller has no invariants to uphold, so the // entire function is not marked unsafe. unsafe { let mut crash_ctx_msg: CrashContextMessage = std::mem::zeroed(); crash_ctx_msg.head.local_port = self.port; let ret = msg::mach_msg( ((&mut crash_ctx_msg.head) as *mut MachMsgHeader).cast(), msg::MACH_RCV_MSG | msg::MACH_RCV_TIMEOUT, 0, std::mem::size_of::() as u32, self.port, timeout.map(|t| t.as_millis() as u32).unwrap_or_default(), port::MACH_PORT_NULL, ); if ret == msg::MACH_RCV_TIMED_OUT { return Ok(None); } else if ret != msg::MACH_MSG_SUCCESS { return Err(Error::Message(ret)); } // Reconstruct a crash context from the message we received let exception = if crash_ctx_msg.flags & FLAG_HAS_EXCEPTION != 0 { Some(crate::ExceptionInfo { kind: crash_ctx_msg.exception_kind, code: crash_ctx_msg.exception_code, subcode: (crash_ctx_msg.flags & FLAG_HAS_SUBCODE != 0) .then_some(crash_ctx_msg.exception_subcode), }) } else { None }; let crash_context = CrashContext { task: crash_ctx_msg.task.name, thread: crash_ctx_msg.crash_thread.name, handler_thread: crash_ctx_msg.handler_thread.name, exception, }; // Translate the task to a pid so the user doesn't have to do it // since there is not a binding available in libc/mach/mach2 for it let mut pid = 0; kern!(pid_for_task(crash_ctx_msg.task.name, &mut pid)); let ack_port = crash_ctx_msg.ack_port.name; // Provide a way for the user to tell the client when they are done // processing the crash context, unless the specified port was not // set or somehow died immediately let acker = Acknowledger { port: (ack_port != port::MACH_PORT_DEAD && ack_port != port::MACH_PORT_NULL) .then_some(ack_port), }; Ok(Some(ReceivedCrashContext { crash_context, acker, pid: pid as u32, })) } } } impl Drop for Server { fn drop(&mut self) { // SAFETY: syscall unsafe { mach_port::mach_port_deallocate(mach_task_self(), self.port); } } } /// Used by a process running the [`Server`] to send a response back to the /// [`Client`] that sent a [`CrashContext`] after it has finished /// processing. pub struct Acknowledger { port: Option, } impl Acknowledger { /// Sends an ack back to the client that sent a [`CrashContext`] /// /// # Errors /// /// We fail to send the ack to the port created in the [`Client`] process pub fn send_ack(&mut self, ack: u32, timeout: Option) -> Result<(), Error> { if let Some(port) = self.port { // SAFETY: syscalls. The caller has no invariants to uphold, so the // entire function is not marked unsafe. unsafe { let mut msg = AcknowledgementMessage { head: MachMsgHeader { bits: msg::MACH_MSG_TYPE_COPY_SEND, size: std::mem::size_of::() as u32, remote_port: port, local_port: port::MACH_PORT_NULL, voucher_port: port::MACH_PORT_NULL, id: 0, }, result: ack, }; // Try to actually send the message msg!(msg::mach_msg( ((&mut msg.head) as *mut MachMsgHeader).cast(), msg::MACH_SEND_MSG | msg::MACH_SEND_TIMEOUT, msg.head.size, 0, port::MACH_PORT_NULL, timeout.map(|t| t.as_millis() as u32).unwrap_or_default(), port::MACH_PORT_NULL )); Ok(()) } } else { Ok(()) } } } /// Used by [`Sender::send_crash_context`] to create a port to receive the /// external process's response to sending a [`CrashContext`] struct AckReceiver { port: port::mach_port_t, } impl AckReceiver { /// Allocates a new port to receive an ack from a [`Server`] /// /// # Errors /// /// We fail to allocate a port, or fail to add a send right to it. /// /// # Safety /// /// Performs syscalls. Only used internally hence the entire function being /// marked unsafe. unsafe fn new() -> Result { let mut port = 0; kern!(mach_port::mach_port_allocate( mach_task_self(), port::MACH_PORT_RIGHT_RECEIVE, &mut port )); kern!(mach_port::mach_port_insert_right( mach_task_self(), port, port, msg::MACH_MSG_TYPE_MAKE_SEND )); Ok(Self { port }) } /// Waits for the specified duration to receive a result from the [`Server`] /// that was sent a [`CrashContext`] /// /// # Errors /// /// We fail to receive an ack for some reason /// /// # Safety /// /// Performs syscalls. Only used internally hence the entire function being /// marked unsafe. unsafe fn recv_ack(&mut self, timeout: Option) -> Result { let mut ack = AcknowledgementMessage { head: MachMsgHeader { bits: 0, size: std::mem::size_of::() as u32, remote_port: port::MACH_PORT_NULL, local_port: self.port, voucher_port: port::MACH_PORT_NULL, id: 0, }, result: 0, }; // Wait for a response from the Server msg!(msg::mach_msg( ((&mut ack.head) as *mut MachMsgHeader).cast(), msg::MACH_RCV_MSG | msg::MACH_RCV_TIMEOUT, 0, ack.head.size, self.port, timeout.map(|t| t.as_millis() as u32).unwrap_or_default(), port::MACH_PORT_NULL )); Ok(ack.result) } } impl Drop for AckReceiver { fn drop(&mut self) { // SAFETY: syscall unsafe { mach_port::mach_port_deallocate(mach_task_self(), self.port); } } }