Skip to main content

openentropy_core/sources/timing/
mach_continuous_timing.rs

1//! mach_continuous_time() timing entropy.
2//!
3//! macOS exposes two monotonic time sources:
4//! - `mach_absolute_time()` — stops advancing during sleep
5//! - `mach_continuous_time()` — continues advancing during sleep
6//!
7//! They differ in their kernel implementation path. While `mach_absolute_time()`
8//! is optimized as a near-zero-overhead COMMPAGE read, `mach_continuous_time()`
9//! must account for accumulated sleep time, requiring a more complex kernel path.
10//!
11//! ## Physics
12//!
13//! Empirically on M4 Mac mini (N=3000):
14//! - `mach_absolute_time()`: mean=19.71 ticks, CV=106.0%, range=[0,125]
15//! - `mach_continuous_time()`: mean=20.26 ticks, **CV=474.9%**, range=[0,5166]
16//!
17//! The 4.5× higher CV for `mach_continuous_time` reflects a fundamentally
18//! different kernel code path:
19//!
20//! 1. **Sleep offset read**: `mach_continuous_time` must read the accumulated
21//!    sleep offset from a kernel structure. This structure is updated during
22//!    every sleep/wake cycle by a different kernel thread, creating read/write
23//!    contention via a seqlock or atomic.
24//!
25//! 2. **Atomic addition**: The sum of mach_absolute_time + sleep_offset requires
26//!    an atomic read + add, which has higher variance than a simple register read.
27//!
28//! 3. **Cross-domain dependency**: The sleep_offset value lives in a kernel
29//!    memory region that may not be in L1 cache if sleep/wake has not occurred
30//!    recently, causing an occasional L2/L3 hit.
31//!
32//! The range=[0,5166] for `mach_continuous_time` vs range=[0,125] for
33//! `mach_absolute_time` shows the much wider latency distribution — the
34//! maximum is 41× the typical value.
35//!
36//! ## Why This Is Entropy
37//!
38//! The `mach_continuous_time` path captures:
39//!
40//! 1. **Kernel sleep-offset structure contention** — concurrent access with
41//!    the power management kernel thread
42//! 2. **Cache pressure on sleep-offset memory** — other kernel operations
43//!    may evict the structure from L1/L2
44//! 3. **Power state transition residuals** — recent sleep/wake cycles leave
45//!    traces in the cache hierarchy
46
47use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
48
49#[cfg(target_os = "macos")]
50use crate::sources::helpers::{extract_timing_entropy, mach_time};
51
52static MACH_CONTINUOUS_TIMING_INFO: SourceInfo = SourceInfo {
53    name: "mach_continuous_timing",
54    description: "mach_continuous_time() kernel sleep-offset path — CV=475% vs abs_time 106%",
55    physics: "Times mach_continuous_time() calls, which unlike mach_absolute_time() must \
56              read the accumulated sleep offset from a kernel structure (seqlock-protected, \
57              updated on every sleep/wake cycle). This creates 4.5× higher CV than \
58              mach_absolute_time(): mean=20.26 ticks, CV=474.9%, range=[0,5166]. \
59              Captures: kernel sleep-offset structure lock contention, cache pressure from \
60              power management kernel thread, residuals from recent sleep/wake cycles. \
61              The maximum (5166 ticks = 215µs) vs typical (0 ticks, same tick) creates \
62              a sparse-event distribution similar to CNTPCT physical timer.",
63    category: SourceCategory::Timing,
64    platform: Platform::MacOS,
65    requirements: &[],
66    entropy_rate_estimate: 2.0,
67    composite: false,
68    is_fast: false,
69};
70
71/// Entropy source from mach_continuous_time() kernel path timing.
72pub struct MachContinuousTimingSource;
73
74#[cfg(target_os = "macos")]
75unsafe extern "C" {
76    fn mach_continuous_time() -> u64;
77}
78
79#[cfg(target_os = "macos")]
80impl EntropySource for MachContinuousTimingSource {
81    fn info(&self) -> &SourceInfo {
82        &MACH_CONTINUOUS_TIMING_INFO
83    }
84
85    fn is_available(&self) -> bool {
86        true
87    }
88
89    fn collect(&self, n_samples: usize) -> Vec<u8> {
90        let raw = n_samples * 4 + 64;
91        let mut timings = Vec::with_capacity(raw);
92
93        // Warm up
94        for _ in 0..16 {
95            unsafe { mach_continuous_time() };
96        }
97
98        for _ in 0..raw {
99            let t0 = mach_time();
100            let _ct = unsafe { mach_continuous_time() };
101            let elapsed = mach_time().wrapping_sub(t0);
102
103            // Cap at 10ms — reject suspend/resume
104            if elapsed < 240_000 {
105                timings.push(elapsed);
106            }
107        }
108
109        extract_timing_entropy(&timings, n_samples)
110    }
111}
112
113#[cfg(not(target_os = "macos"))]
114impl EntropySource for MachContinuousTimingSource {
115    fn info(&self) -> &SourceInfo {
116        &MACH_CONTINUOUS_TIMING_INFO
117    }
118    fn is_available(&self) -> bool {
119        false
120    }
121    fn collect(&self, _: usize) -> Vec<u8> {
122        Vec::new()
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn info() {
132        let src = MachContinuousTimingSource;
133        assert_eq!(src.info().name, "mach_continuous_timing");
134        assert!(matches!(src.info().category, SourceCategory::Timing));
135        assert_eq!(src.info().platform, Platform::MacOS);
136    }
137
138    #[test]
139    #[cfg(target_os = "macos")]
140    fn is_available_on_macos() {
141        assert!(MachContinuousTimingSource.is_available());
142    }
143
144    #[test]
145    #[ignore]
146    fn collects_sleep_offset_timing() {
147        let data = MachContinuousTimingSource.collect(32);
148        assert!(!data.is_empty());
149    }
150}