Skip to main content

openentropy_core/sources/timing/
mach_timing.rs

1//! Mach absolute time entropy source (macOS only).
2//!
3//! Reads the ARM system counter at sub-nanosecond resolution with variable
4//! micro-workloads between samples.
5
6use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
7use crate::sources::helpers::{extract_timing_entropy, mach_time};
8
9/// Reads the ARM system counter (`mach_absolute_time`) at sub-nanosecond
10/// resolution with variable micro-workloads between samples. Returns raw
11/// LSBs of timing deltas — no conditioning applied.
12pub struct MachTimingSource;
13
14static MACH_TIMING_INFO: SourceInfo = SourceInfo {
15    name: "mach_timing",
16    description: "mach_absolute_time() with micro-workload jitter (raw LSBs)",
17    physics: "Reads the ARM system counter (mach_absolute_time) at sub-nanosecond \
18              resolution with variable micro-workloads between samples. The timing \
19              jitter comes from CPU pipeline state: instruction reordering, branch \
20              prediction, cache state, interrupt coalescing, and power-state \
21              transitions.",
22    category: SourceCategory::Timing,
23    platform: Platform::MacOS,
24    requirements: &[],
25    entropy_rate_estimate: 0.3,
26    composite: false,
27    is_fast: true,
28};
29
30impl EntropySource for MachTimingSource {
31    fn info(&self) -> &SourceInfo {
32        &MACH_TIMING_INFO
33    }
34
35    fn is_available(&self) -> bool {
36        cfg!(target_os = "macos")
37    }
38
39    fn collect(&self, n_samples: usize) -> Vec<u8> {
40        let raw_count = n_samples + 64;
41        let mut timings = Vec::with_capacity(raw_count);
42
43        // LCG for randomizing workload size (seeded from clock).
44        let mut lcg: u64 = mach_time() | 1;
45
46        for _ in 0..raw_count {
47            let t0 = mach_time();
48
49            // Variable micro-workload: randomized iteration count (1-8)
50            // via LCG so the branch predictor can't settle.
51            lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
52            let iterations = ((lcg >> 32) & 7) + 1;
53            let mut sink: u64 = t0;
54            for _ in 0..iterations {
55                sink = sink.wrapping_mul(6364136223846793005).wrapping_add(1);
56            }
57            std::hint::black_box(sink);
58
59            let t1 = mach_time();
60            timings.push(t1.wrapping_sub(t0));
61        }
62
63        extract_timing_entropy(&timings, n_samples)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    #[cfg(target_os = "macos")]
73    #[ignore] // Run with: cargo test -- --ignored
74    fn mach_timing_collects_bytes() {
75        let src = MachTimingSource;
76        assert!(src.is_available());
77        let data = src.collect(64);
78        assert!(!data.is_empty());
79        assert!(data.len() <= 64);
80    }
81
82    #[test]
83    fn source_info_name() {
84        assert_eq!(MachTimingSource.name(), "mach_timing");
85    }
86
87    #[test]
88    fn source_info_category() {
89        assert_eq!(MachTimingSource.info().category, SourceCategory::Timing);
90        assert!(!MachTimingSource.info().composite);
91    }
92}