Source code

Revision control

Copy as Markdown

Other Tools

//! 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
//!
//! 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.
///
#[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`
/// determines whether these exceptions are fatal.
///
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`
///
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.
///
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.
///
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
///
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 }
}
}