diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/crash-context/src/mac | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/crash-context/src/mac')
-rw-r--r-- | third_party/rust/crash-context/src/mac/guard.rs | 92 | ||||
-rw-r--r-- | third_party/rust/crash-context/src/mac/ipc.rs | 553 | ||||
-rw-r--r-- | third_party/rust/crash-context/src/mac/resource.rs | 516 |
3 files changed, 1161 insertions, 0 deletions
diff --git a/third_party/rust/crash-context/src/mac/guard.rs b/third_party/rust/crash-context/src/mac/guard.rs new file mode 100644 index 0000000000..7caee5ffe3 --- /dev/null +++ b/third_party/rust/crash-context/src/mac/guard.rs @@ -0,0 +1,92 @@ +//! Contains types and helpers for dealing with `EXC_GUARD` exceptions. +//! +//! `EXC_GUARD` exceptions embed details about the guarded resource in the `code` +//! and `subcode` fields of the exception +//! +//! See <https://github.com/apple-oss-distributions/xnu/blob/e7776783b89a353188416a9a346c6cdb4928faad/osfmk/kern/exc_guard.h> +//! for the top level types that this module wraps. + +use mach2::exception_types::EXC_GUARD; + +/// The set of possible guard kinds +#[derive(Copy, Clone, Debug)] +#[repr(u8)] +pub enum GuardKind { + /// Null variant + None = 0, + /// A `mach_port_t` + MachPort = 1, + /// File descriptor + Fd = 2, + /// Userland assertion + User = 3, + /// Vnode + Vnode = 4, + /// Virtual memory operation + VirtualMemory = 5, + /// Rejected system call trap + RejectedSyscall = 6, +} + +#[inline] +pub fn extract_guard_kind(code: u64) -> u8 { + ((code >> 61) & 0x7) as u8 +} + +#[inline] +pub fn extract_guard_flavor(code: u64) -> u32 { + ((code >> 32) & 0x1fffffff) as u32 +} + +#[inline] +pub fn extract_guard_target(code: u64) -> u32 { + code as u32 +} + +/// The extracted details of an `EXC_GUARD` exception +pub struct GuardException { + /// One of [`GuardKind`] + pub kind: u8, + /// The specific guard flavor that was violated, specific to each `kind` + pub flavor: u32, + /// The resource that was guarded + pub target: u32, + /// Target specific guard information + pub identifier: u64, +} + +/// Extracts the guard details from an exceptions code and subcode +/// +/// code: +/// +-------------------+----------------+--------------+ +/// |[63:61] guard type | [60:32] flavor | [31:0] target| +/// +-------------------+----------------+--------------+ +/// +/// subcode: +/// +---------------------------------------------------+ +/// |[63:0] guard identifier | +/// +---------------------------------------------------+ +#[inline] +pub fn extract_guard_exception(code: u64, subcode: u64) -> GuardException { + GuardException { + kind: extract_guard_kind(code), + flavor: extract_guard_flavor(code), + target: extract_guard_target(code), + identifier: subcode, + } +} + +impl super::ExceptionInfo { + /// If this is an `EXC_GUARD` exception, retrieves the exception metadata + /// from the code, otherwise returns `None` + pub fn guard_exception(&self) -> Option<GuardException> { + if self.kind != EXC_GUARD { + return None; + } + + Some(extract_guard_exception( + self.code, + self.subcode.unwrap_or_default(), + )) + } +} 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); + } + } +} diff --git a/third_party/rust/crash-context/src/mac/resource.rs b/third_party/rust/crash-context/src/mac/resource.rs new file mode 100644 index 0000000000..c93acfe97a --- /dev/null +++ b/third_party/rust/crash-context/src/mac/resource.rs @@ -0,0 +1,516 @@ +//! Contains types and helpers for dealing with `EXC_RESOURCE` exceptions. +//! +//! `EXC_RESOURCE` exceptions embed details about the resource and the limits +//! it exceeded within the `code` and, in some cases `subcode`, fields of the exception +//! +//! See <https://github.com/apple-oss-distributions/xnu/blob/e6231be02a03711ca404e5121a151b24afbff733/osfmk/kern/exc_resource.h> +//! for the various constants and decoding of exception information wrapped in +//! this module. + +use mach2::exception_types::EXC_RESOURCE; +use std::time::Duration; + +/// The details for an `EXC_RESOURCE` exception as retrieved from the exception's +/// code and subcode +pub enum ResourceException { + /// This is sent by the kernel when the CPU usage monitor is tripped. Possibly fatal. + Cpu(CpuResourceException), + /// This is sent by the kernel when the platform idle wakeups monitor is tripped. Possibly fatal. + Wakeups(WakeupsResourceException), + /// This is sent by the kernel when a task crosses its high watermark memory limit. Never fatal at least on current MacOS versions. + Memory(MemoryResourceException), + /// This is sent by the kernel when a task crosses its I/O limits. Never fatal. + Io(IoResourceException), + /// This is sent by the kernel when a task crosses its thread limit. Always fatal. + Threads(ThreadsResourceException), + /// This is sent by the kernel when the process is leaking ipc ports and has + /// filled its port space. Always fatal. + Ports(PortsResourceException), + /// An unknown resource kind due to an addition to the set of possible + /// resource exception kinds in exc_resource.h + Unknown { kind: u8, flavor: u8 }, +} + +/// Each different resource exception type has 1 or more flavors that it can be, +/// and while these most likely don't change often, we try to be forward +/// compatible by not failing if a particular flavor is unknown +#[derive(Copy, Clone, Debug)] +pub enum Flavor<T: Copy + Clone + std::fmt::Debug> { + Known(T), + Unknown(u8), +} + +impl<T: TryFrom<u8> + Copy + Clone + std::fmt::Debug> From<u64> for Flavor<T> { + #[inline] + fn from(code: u64) -> Self { + let flavor = resource_exc_flavor(code); + if let Ok(known) = T::try_from(flavor) { + Self::Known(known) + } else { + Self::Unknown(flavor) + } + } +} + +impl<T: PartialEq + Copy + Clone + std::fmt::Debug> PartialEq<T> for Flavor<T> { + fn eq(&self, o: &T) -> bool { + match self { + Self::Known(flavor) => flavor == o, + Self::Unknown(_) => false, + } + } +} + +/// Retrieves the resource exception kind from an exception code +#[inline] +pub fn resource_exc_kind(code: u64) -> u8 { + ((code >> 61) & 0x7) as u8 +} + +/// Retrieves the resource exception flavor from an exception code +#[inline] +pub fn resource_exc_flavor(code: u64) -> u8 { + ((code >> 58) & 0x7) as u8 +} + +impl super::ExceptionInfo { + /// If this is an `EXC_RESOURCE` exception, retrieves the exception metadata + /// from the code, otherwise returns `None` + pub fn resource_exception(&self) -> Option<ResourceException> { + if self.kind != EXC_RESOURCE { + return None; + } + + let kind = resource_exc_kind(self.code); + + let res_exc = if kind == ResourceKind::Cpu as u8 { + ResourceException::Cpu(CpuResourceException::from_exc_info(self.code, self.subcode)) + } else if kind == ResourceKind::Wakeups as u8 { + ResourceException::Wakeups(WakeupsResourceException::from_exc_info( + self.code, + self.subcode, + )) + } else if kind == ResourceKind::Memory as u8 { + ResourceException::Memory(MemoryResourceException::from_exc_info(self.code)) + } else if kind == ResourceKind::Io as u8 { + ResourceException::Io(IoResourceException::from_exc_info(self.code, self.subcode)) + } else if kind == ResourceKind::Threads as u8 { + ResourceException::Threads(ThreadsResourceException::from_exc_info(self.code)) + } else if kind == ResourceKind::Ports as u8 { + ResourceException::Ports(PortsResourceException::from_exc_info(self.code)) + } else { + ResourceException::Unknown { + kind, + flavor: resource_exc_flavor(self.code), + } + }; + + Some(res_exc) + } +} + +/// The types of resources that an `EXC_RESOURCE` exception can pertain to +#[repr(u8)] +pub enum ResourceKind { + Cpu = 1, + Wakeups = 2, + Memory = 3, + Io = 4, + Threads = 5, + Ports = 6, +} + +/// The flavors for a [`CpuResourceException`] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum CpuFlavor { + /// The process has surpassed its CPU limit + Monitor = 1, + /// The process has surpassed its CPU limit, and the process has been configured + /// to make this exception fatal + MonitorFatal = 2, +} + +impl TryFrom<u8> for CpuFlavor { + type Error = (); + + fn try_from(flavor: u8) -> Result<Self, Self::Error> { + match flavor { + 1 => Ok(Self::Monitor), + 2 => Ok(Self::MonitorFatal), + _ => Err(()), + } + } +} + +/// These exceptions _may_ be fatal. They are not fatal by default at task +/// creation but can be made fatal by calling `proc_rlimit_control` with +/// `RLIMIT_CPU_USAGE_MONITOR` as the second argument and `CPUMON_MAKE_FATAL` +/// set in the flags. The flavor extracted from the exception code determines if +/// the exception is fatal. +/// +/// [Kernel code](https://github.com/apple-oss-distributions/xnu/blob/e7776783b89a353188416a9a346c6cdb4928faad/osfmk/kern/thread.c#L2475-L2616) +#[derive(Copy, Clone, Debug)] +pub struct CpuResourceException { + pub flavor: Flavor<CpuFlavor>, + /// If the exception is fatal. Currently only true if the flavor is [`CpuFlavor::MonitorFatal`] + pub is_fatal: bool, + /// The time period in which the CPU limit was surpassed + pub observation_interval: Duration, + /// The CPU % limit + pub limit: u8, + /// The CPU % consumed by the task + pub consumed: u8, +} + +impl CpuResourceException { + /* + * code: + * +-----------------------------------------------+ + * |[63:61] RESOURCE |[60:58] FLAVOR_CPU_ |[57:32] | + * |_TYPE_CPU |MONITOR[_FATAL] |Unused | + * +-----------------------------------------------+ + * |[31:7] Interval (sec) | [6:0] CPU limit (%)| + * +-----------------------------------------------+ + * + * subcode: + * +-----------------------------------------------+ + * | | [6:0] % of CPU | + * | | actually consumed | + * +-----------------------------------------------+ + * + */ + #[inline] + pub fn from_exc_info(code: u64, subcode: Option<u64>) -> Self { + debug_assert_eq!(resource_exc_kind(code), ResourceKind::Cpu as u8); + + let flavor = Flavor::from(code); + let interval_seconds = (code >> 7) & 0x1ffffff; + let limit = (code & 0x7f) as u8; + let consumed = subcode.map_or(0, |sc| sc & 0x7f) as u8; + + // The default is that cpu resource exceptions are not fatal, so + // we only check the flavor against the (currently) one known value + // that indicates the exception is fatal + Self { + flavor, + is_fatal: flavor == CpuFlavor::MonitorFatal, + observation_interval: Duration::from_secs(interval_seconds), + limit, + consumed, + } + } +} + +/// The flavors for a [`WakeupsResourceException`] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum WakeupsFlavor { + Monitor = 1, +} + +impl TryFrom<u8> for WakeupsFlavor { + type Error = (); + + fn try_from(flavor: u8) -> Result<Self, Self::Error> { + match flavor { + 1 => Ok(Self::Monitor), + _ => Err(()), + } + } +} + +/// These exceptions may be fatal. They are not fatal by default at task +/// creation, but can be made fatal by calling `proc_rlimit_control` with +/// `RLIMIT_WAKEUPS_MONITOR` as the second argument and `WAKEMON_MAKE_FATAL` +/// set in the flags. Calling [`proc_get_wakemon_params`](https://github.com/apple-oss-distributions/xnu/blob/e6231be02a03711ca404e5121a151b24afbff733/libsyscall/wrappers/libproc/libproc.c#L592-L608) +/// determines whether these exceptions are fatal. +/// +/// [Kernel source](https://github.com/apple-oss-distributions/xnu/blob/e6231be02a03711ca404e5121a151b24afbff733/osfmk/kern/task.c#L7501-L7580) +pub struct WakeupsResourceException { + pub flavor: Flavor<WakeupsFlavor>, + /// The time period in which the number of wakeups was surpassed + pub observation_interval: Duration, + /// The number of wakeups permitted per second + pub permitted: u32, + /// The number of wakeups observed per second + pub observed: u32, +} + +impl WakeupsResourceException { + /* + * code: + * +-----------------------------------------------+ + * |[63:61] RESOURCE |[60:58] FLAVOR_ |[57:32] | + * |_TYPE_WAKEUPS |WAKEUPS_MONITOR |Unused | + * +-----------------------------------------------+ + * | [31:20] Observation | [19:0] # of wakeups | + * | interval (sec) | permitted (per sec) | + * +-----------------------------------------------+ + * + * subcode: + * +-----------------------------------------------+ + * | | [19:0] # of wakeups | + * | | observed (per sec) | + * +-----------------------------------------------+ + * + */ + #[inline] + pub fn from_exc_info(code: u64, subcode: Option<u64>) -> Self { + debug_assert_eq!(resource_exc_kind(code), ResourceKind::Wakeups as u8); + + let flavor = Flavor::from(code); + // Note that Apple has a bug in exc_resource.h where the masks in the + // decode macros for the interval and the permitted wakeups have been swapped + let interval_seconds = (code >> 20) & 0xfff; + let permitted = (code & 0xfffff) as u32; + let observed = subcode.map_or(0, |sc| sc & 0xfffff) as u32; + + Self { + flavor, + observation_interval: Duration::from_secs(interval_seconds), + permitted, + observed, + } + } +} + +/// The flavors for a [`MemoryResourceException`] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum MemoryFlavor { + HighWatermark = 1, +} + +impl TryFrom<u8> for MemoryFlavor { + type Error = (); + + fn try_from(flavor: u8) -> Result<Self, Self::Error> { + match flavor { + 1 => Ok(Self::HighWatermark), + _ => Err(()), + } + } +} + +/// These exceptions, as of this writing, are never fatal. +/// +/// While memory exceptions _can_ be fatal, this appears to only be possible if +/// the kernel is built with `CONFIG_JETSAM` or in `DEVELOPMENT` or `DEBUG` modes, +/// so as of now, they should never be considered fatal, at least on `MacOS` +/// +/// [Kernel source](https://github.com/apple-oss-distributions/xnu/blob/e6231be02a03711ca404e5121a151b24afbff733/osfmk/kern/task.c#L6767-L6874) +pub struct MemoryResourceException { + pub flavor: Flavor<MemoryFlavor>, + /// The limit in MiB of the high watermark + pub limit_mib: u16, +} + +impl MemoryResourceException { + /* + * code: + * +------------------------------------------------+ + * |[63:61] RESOURCE |[60:58] FLAVOR_HIGH_ |[57:32] | + * |_TYPE_MEMORY |WATERMARK |Unused | + * +------------------------------------------------+ + * | | [12:0] HWM limit (MB)| + * +------------------------------------------------+ + * + * subcode: + * +------------------------------------------------+ + * | unused | + * +------------------------------------------------+ + * + */ + #[inline] + pub fn from_exc_info(code: u64) -> Self { + debug_assert_eq!(resource_exc_kind(code), ResourceKind::Memory as u8); + + let flavor = Flavor::from(code); + let limit_mib = (code & 0x1fff) as u16; + + Self { flavor, limit_mib } + } +} + +/// The flavors for an [`IoResourceException`] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum IoFlavor { + PhysicalWrites = 1, + LogicalWrites = 2, +} + +impl TryFrom<u8> for IoFlavor { + type Error = (); + + fn try_from(flavor: u8) -> Result<Self, Self::Error> { + match flavor { + 1 => Ok(Self::PhysicalWrites), + 2 => Ok(Self::LogicalWrites), + _ => Err(()), + } + } +} + +/// These exceptions are never fatal. +/// +/// [Kernel source](https://github.com/apple-oss-distributions/xnu/blob/e6231be02a03711ca404e5121a151b24afbff733/osfmk/kern/task.c#L7739-L7792) +pub struct IoResourceException { + pub flavor: Flavor<MemoryFlavor>, + /// The time period in which the I/O limit was surpassed + pub observation_interval: Duration, + /// The I/O limit in MiB of the high watermark + pub limit_mib: u16, + /// The observed I/O in MiB + pub observed_mib: u16, +} + +impl IoResourceException { + /* + * code: + * +-----------------------------------------------+ + * |[63:61] RESOURCE |[60:58] FLAVOR_IO_ |[57:32] | + * |_TYPE_IO |PHYSICAL/LOGICAL |Unused | + * +-----------------------------------------------+ + * |[31:15] Interval (sec) | [14:0] Limit (MB) | + * +-----------------------------------------------+ + * + * subcode: + * +-----------------------------------------------+ + * | | [14:0] I/O Count | + * | | (in MB) | + * +-----------------------------------------------+ + * + */ + #[inline] + pub fn from_exc_info(code: u64, subcode: Option<u64>) -> Self { + debug_assert_eq!(resource_exc_kind(code), ResourceKind::Io as u8); + + let flavor = Flavor::from(code); + let interval_seconds = (code >> 15) & 0x1ffff; + let limit_mib = (code & 0x7fff) as u16; + let observed_mib = subcode.map_or(0, |sc| sc & 0x7fff) as u16; + + Self { + flavor, + observation_interval: Duration::from_secs(interval_seconds), + limit_mib, + observed_mib, + } + } +} + +/// The flavors for a [`ThreadsResourceException`] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum ThreadsFlavor { + HighWatermark = 1, +} + +impl TryFrom<u8> for ThreadsFlavor { + type Error = (); + + fn try_from(flavor: u8) -> Result<Self, Self::Error> { + match flavor { + 1 => Ok(Self::HighWatermark), + _ => Err(()), + } + } +} + +/// This exception is provided for completeness sake, but is only possible if +/// the kernel is built in `DEVELOPMENT` or `DEBUG` modes. +/// +/// [Kernel source](https://github.com/apple-oss-distributions/xnu/blob/e6231be02a03711ca404e5121a151b24afbff733/osfmk/kern/thread.c#L2575-L2620) +pub struct ThreadsResourceException { + pub flavor: Flavor<ThreadsFlavor>, + /// The thread limit + pub limit: u16, +} + +impl ThreadsResourceException { + /* + * code: + * +--------------------------------------------------+ + * |[63:61] RESOURCE |[60:58] FLAVOR_ |[57:32] | + * |_TYPE_THREADS |THREADS_HIGH_WATERMARK |Unused | + * +--------------------------------------------------+ + * |[31:15] Unused | [14:0] Limit | + * +--------------------------------------------------+ + * + * subcode: + * +-----------------------------------------------+ + * | | Unused | + * | | | + * +-----------------------------------------------+ + * + */ + #[inline] + pub fn from_exc_info(code: u64) -> Self { + debug_assert_eq!(resource_exc_kind(code), ResourceKind::Threads as u8); + + let flavor = Flavor::from(code); + let limit = (code & 0x7fff) as u16; + + Self { flavor, limit } + } +} + +/// The flavors for a [`PortsResourceException`] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum PortsFlavor { + SpaceFull = 1, +} + +impl TryFrom<u8> for PortsFlavor { + type Error = (); + + fn try_from(flavor: u8) -> Result<Self, Self::Error> { + match flavor { + 1 => Ok(Self::SpaceFull), + _ => Err(()), + } + } +} + +/// This exception is always fatal, and in fact I'm unsure if this exception +/// is even observable, as the kernel will kill the offending process if +/// the port space is full +/// +/// [Kernel source](https://github.com/apple-oss-distributions/xnu/blob/e7776783b89a353188416a9a346c6cdb4928faad/osfmk/kern/task.c#L7907-L7969) +pub struct PortsResourceException { + pub flavor: Flavor<ThreadsFlavor>, + /// The number of allocated ports + pub allocated: u32, +} + +impl PortsResourceException { + /* + * code: + * +-----------------------------------------------+ + * |[63:61] RESOURCE |[60:58] FLAVOR_ |[57:32] | + * |_TYPE_PORTS |PORT_SPACE_FULL |Unused | + * +-----------------------------------------------+ + * | [31:24] Unused | [23:0] # of ports | + * | | allocated | + * +-----------------------------------------------+ + * + * subcode: + * +-----------------------------------------------+ + * | | Unused | + * | | | + * +-----------------------------------------------+ + * + */ + #[inline] + pub fn from_exc_info(code: u64) -> Self { + debug_assert_eq!(resource_exc_kind(code), ResourceKind::Ports as u8); + + let flavor = Flavor::from(code); + let allocated = (code & 0xffffff) as u32; + + Self { flavor, allocated } + } +} |