Skip to main content

openentropy_core/sources/microarch/
sev_event_timing.rs

1//! SEV/SEVL (Send Event) instruction timing entropy.
2//!
3//! The ARM64 `SEV` (Send Event) instruction broadcasts a wakeup signal to
4//! all cores that are waiting in `WFE` (Wait For Event) low-power state.
5//! `SEVL` (Send Event Local) sends the event only to the local core's
6//! event register.
7//!
8//! ## Physics
9//!
10//! **SEV** must notify every core on the chip. On Apple Silicon, with
11//! 4–16 cores across multiple clusters, the SEV broadcast traverses the
12//! same ICC (Inter-Cluster Coherency) fabric as cache coherency traffic.
13//! When other processes are doing WFE/SEV synchronization (network drivers,
14//! kernel spinlocks, multimedia processing), the ICC bus is busier and
15//! our SEV takes longer.
16//!
17//! Measured on M4 Mac mini (N=2000 each):
18//! - `SEV`:  mean=6.14 ticks (~256 ns), CV=240.4%, range=0–42 ticks, LSB=0.051
19//! - `SEVL`: mean=20.12 ticks (~838 ns), CV=103.5%, range=0–58 ticks, LSB=0.160
20//!
21//! **Paradox**: SEVL (local-only) is 3× SLOWER than SEV (all-cores broadcast).
22//! This reveals that the "local event register" write path is less optimized
23//! than the ICC broadcast path — Apple likely implements SEV through a
24//! fast-path in the coherency fabric, while SEVL uses a slower register-write
25//! path to the local core's hardware event monitor.
26//!
27//! This is a genuine microarchitectural discovery: on Apple Silicon,
28//! **broadcasting to all cores is faster than writing to one core's
29//! local event register**.
30//!
31//! ## Entropy Sources
32//!
33//! SEV timing reflects:
34//! - ICC bus load from all current WFE waiters across all processes
35//! - Number of cores that need to be woken (those in WFE sleep)
36//! - Thermal state of the interconnect (latency increases with temperature)
37//!
38//! SEVL timing reflects:
39//! - Local core's event register path (less shared, more predictable)
40//! - Pipeline state and execution unit contention on the current core
41//! - Thermal throttling of the specific P-core we're running on
42
43use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
44
45#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
46use crate::sources::helpers::{extract_timing_entropy, mach_time};
47
48static SEV_EVENT_TIMING_INFO: SourceInfo = SourceInfo {
49    name: "sev_event_timing",
50    description: "ARM64 SEV/SEVL broadcast event timing via ICC fabric load",
51    physics: "Times `SEV` (Send Event, all-core broadcast) and `SEVL` (Send Event Local). \
52              SEV traverses the ICC fabric to wake WFE-waiting cores; timing reflects ICC \
53              bus load from all concurrent WFE/SEV operations across all processes. \
54              Paradox: SEVL is 3\u{00d7} slower than SEV (local event register write is less \
55              optimized than ICC broadcast path). Measured: SEV CV=240.4%, mean=6.1 ticks; \
56              SEVL CV=103.5%, mean=20.1 ticks, range=0\u{2013}58 ticks. XOR-mixed to combine \
57              ICC fabric state (SEV) with local core pipeline state (SEVL).",
58    category: SourceCategory::Microarch,
59    platform: Platform::MacOS,
60    requirements: &[],
61    entropy_rate_estimate: 3.0,
62    composite: false,
63    is_fast: false,
64};
65
66/// Entropy source from ARM64 SEV/SEVL broadcast event timing.
67pub struct SEVEventTimingSource;
68
69#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
70impl EntropySource for SEVEventTimingSource {
71    fn info(&self) -> &SourceInfo {
72        &SEV_EVENT_TIMING_INFO
73    }
74
75    fn is_available(&self) -> bool {
76        true
77    }
78
79    fn collect(&self, n_samples: usize) -> Vec<u8> {
80        // Take equal samples from SEV and SEVL, then XOR-mix for independence.
81        let raw = n_samples * 4 + 32;
82        let mut sev_times = Vec::with_capacity(raw);
83        let mut sevl_times = Vec::with_capacity(raw);
84
85        // Warm up
86        for _ in 0..16 {
87            unsafe {
88                core::arch::asm!("sev", options(nostack, nomem));
89                core::arch::asm!("sevl", options(nostack, nomem));
90            }
91        }
92
93        for _ in 0..raw {
94            let t0 = mach_time();
95            unsafe {
96                core::arch::asm!("sev", options(nostack, nomem));
97            }
98            let sev_t = mach_time().wrapping_sub(t0);
99
100            let t1 = mach_time();
101            unsafe {
102                core::arch::asm!("sevl", options(nostack, nomem));
103            }
104            let sevl_t = mach_time().wrapping_sub(t1);
105
106            // Reject suspend/resume spikes (>2ms).
107            if sev_t < 48_000 && sevl_t < 48_000 {
108                // XOR combines ICC state (sev_t) with local core state (sevl_t).
109                // Since they measure different microarchitectural paths, the XOR
110                // maximizes independence between the two timing channels.
111                sev_times.push(sev_t);
112                sevl_times.push(sevl_t ^ (sev_t.wrapping_shl(7)));
113            }
114        }
115
116        // Interleave for extraction.
117        let min = sev_times.len().min(sevl_times.len());
118        let mut combined = Vec::with_capacity(min * 2);
119        for i in 0..min {
120            combined.push(sev_times[i]);
121            combined.push(sevl_times[i]);
122        }
123
124        extract_timing_entropy(&combined, n_samples)
125    }
126}
127
128#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
129impl EntropySource for SEVEventTimingSource {
130    fn info(&self) -> &SourceInfo {
131        &SEV_EVENT_TIMING_INFO
132    }
133    fn is_available(&self) -> bool {
134        false
135    }
136    fn collect(&self, _n_samples: usize) -> Vec<u8> {
137        Vec::new()
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn info() {
147        let src = SEVEventTimingSource;
148        assert_eq!(src.info().name, "sev_event_timing");
149        assert!(matches!(src.info().category, SourceCategory::Microarch));
150        assert_eq!(src.info().platform, Platform::MacOS);
151        assert!(!src.info().composite);
152    }
153
154    #[test]
155    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
156    fn is_available_on_apple_silicon() {
157        assert!(SEVEventTimingSource.is_available());
158    }
159
160    #[test]
161    #[ignore]
162    fn collects_with_variation() {
163        let data = SEVEventTimingSource.collect(32);
164        assert!(!data.is_empty());
165    }
166}