//! 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 //! 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 { Known(T), Unknown(u8), } impl + Copy + Clone + std::fmt::Debug> From for Flavor { #[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 PartialEq for Flavor { 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 { 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 for CpuFlavor { type Error = (); fn try_from(flavor: u8) -> Result { 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, /// 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) -> 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 for WakeupsFlavor { type Error = (); fn try_from(flavor: u8) -> Result { 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, /// 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) -> 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 for MemoryFlavor { type Error = (); fn try_from(flavor: u8) -> Result { 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, /// 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 for IoFlavor { type Error = (); fn try_from(flavor: u8) -> Result { 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, /// 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) -> 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 for ThreadsFlavor { type Error = (); fn try_from(flavor: u8) -> Result { 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, /// 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 for PortsFlavor { type Error = (); fn try_from(flavor: u8) -> Result { 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, /// 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 } } }