summaryrefslogtreecommitdiffstats
path: root/third_party/rust/crash-context/src/mac/ipc.rs
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/crash-context/src/mac/ipc.rs')
-rw-r--r--third_party/rust/crash-context/src/mac/ipc.rs553
1 files changed, 553 insertions, 0 deletions
diff --git a/third_party/rust/crash-context/src/mac/ipc.rs b/third_party/rust/crash-context/src/mac/ipc.rs
new file mode 100644
index 0000000000..cb5fb2fcf1
--- /dev/null
+++ b/third_party/rust/crash-context/src/mac/ipc.rs
@@ -0,0 +1,553 @@
+//! 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 <usr/include/mach/mach_traps.h>, 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;
+}
+
+/// <https://github.com/apple-oss-distributions/xnu/blob/e6231be02a03711ca404e5121a151b24afbff733/osfmk/mach/message.h#L379-L391>
+#[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,
+}
+
+/// <https://github.com/apple-oss-distributions/xnu/blob/e6231be02a03711ca404e5121a151b24afbff733/osfmk/mach/message.h#L545-L552>
+#[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<Self, Error> {
+ // 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<Duration>,
+ receive_timeout: Option<Duration>,
+ ) -> Result<Option<u32>, 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::<CrashContextMessage>() 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<Self, Error> {
+ // 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<Duration>,
+ ) -> Result<Option<ReceivedCrashContext>, 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::<CrashContextMessage>() 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<port::mach_port_t>,
+}
+
+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<Duration>) -> 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::<AcknowledgementMessage>() 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<Self, Error> {
+ 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<Duration>) -> Result<u32, Error> {
+ let mut ack = AcknowledgementMessage {
+ head: MachMsgHeader {
+ bits: 0,
+ size: std::mem::size_of::<AcknowledgementMessage>() 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);
+ }
+ }
+}