Skip to main content

openentropy_core/sources/microarch/
dvfs_race.rs

1//! Cross-core DVFS race — entropy from independent frequency scaling controllers.
2//!
3//! Two threads race on different CPU cores running tight counting loops. The
4//! difference in iteration counts captures physical frequency jitter from
5//! independent DVFS (Dynamic Voltage and Frequency Scaling) controllers on
6//! P-core vs E-core clusters.
7//!
8
9use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
10use crate::sources::helpers::{extract_timing_entropy, mach_time};
11
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
14use std::thread;
15
16/// Cross-core DVFS race entropy source.
17///
18/// Spawns two threads that race via tight counting loops. The absolute
19/// difference in their iteration counts after a short window (~2μs) is
20/// physically nondeterministic because:
21///
22/// 1. P-cores and E-cores have independent DVFS controllers that adjust
23///    frequency based on thermal sensors, power budget, and workload.
24/// 2. The scheduler assigns threads to different core clusters
25///    nondeterministically based on system-wide load and QoS.
26/// 3. Even within a single core type, frequency transitions are asynchronous
27///    and thermally-driven — two identical cores can run at different
28///    frequencies at the same instant.
29/// 4. The stop signal propagation has cache-coherence latency that varies
30///    by which cores the threads landed on.
31pub struct DVFSRaceSource;
32
33static DVFS_RACE_INFO: SourceInfo = SourceInfo {
34    name: "dvfs_race",
35    description: "Cross-core DVFS frequency race between thread pairs",
36    physics: "Spawns two threads running tight counting loops on different cores. \
37              After a ~2\u{00b5}s race window, the absolute difference in iteration \
38              counts captures nondeterminism from: scheduler core placement (P-core vs \
39              E-core), cache coherence latency for the stop signal, interrupt jitter, \
40              and cross-core pipeline state differences. On Apple Silicon, P-core and \
41              E-core clusters have separate frequency domains, but the 2\u{00b5}s window is \
42              too short for DVFS transitions (~100\u{00b5}s-1ms); the primary entropy comes \
43              from scheduling and cache-coherence nondeterminism.",
44    category: SourceCategory::Microarch,
45    platform: Platform::MacOS,
46    requirements: &[],
47    entropy_rate_estimate: 3.0,
48    composite: false,
49    is_fast: true,
50};
51
52impl EntropySource for DVFSRaceSource {
53    fn info(&self) -> &SourceInfo {
54        &DVFS_RACE_INFO
55    }
56
57    fn is_available(&self) -> bool {
58        cfg!(target_os = "macos")
59    }
60
61    fn collect(&self, n_samples: usize) -> Vec<u8> {
62        // Cap thread spawns: each race spawns 2 threads, so 256 races = 512 threads max.
63        // extract_timing_entropy handles the oversampling/debiasing.
64        let raw_count = (n_samples * 4 + 64).min(256);
65        let mut diffs: Vec<u64> = Vec::with_capacity(raw_count);
66
67        // Get timebase for ~2μs window calculation.
68        // On Apple Silicon, mach_absolute_time ticks at 24MHz → 1 tick ≈ 41.67ns.
69        // 2μs ≈ 48 ticks. Use a small window to keep collection fast.
70        let window_ticks: u64 = 48; // ~2μs on Apple Silicon
71
72        for _ in 0..raw_count {
73            let stop = Arc::new(AtomicBool::new(false));
74            let count1 = Arc::new(AtomicU64::new(0));
75            let count2 = Arc::new(AtomicU64::new(0));
76            let ready1 = Arc::new(AtomicBool::new(false));
77            let ready2 = Arc::new(AtomicBool::new(false));
78
79            let s1 = stop.clone();
80            let c1 = count1.clone();
81            let r1 = ready1.clone();
82            let handle1 = thread::spawn(move || {
83                let mut local_count: u64 = 0;
84                r1.store(true, Ordering::Release);
85                while !s1.load(Ordering::Relaxed) {
86                    local_count = local_count.wrapping_add(1);
87                }
88                c1.store(local_count, Ordering::Release);
89            });
90
91            let s2 = stop.clone();
92            let c2 = count2.clone();
93            let r2 = ready2.clone();
94            let handle2 = thread::spawn(move || {
95                let mut local_count: u64 = 0;
96                r2.store(true, Ordering::Release);
97                while !s2.load(Ordering::Relaxed) {
98                    local_count = local_count.wrapping_add(1);
99                }
100                c2.store(local_count, Ordering::Release);
101            });
102
103            // Wait for both threads to be ready.
104            while !ready1.load(Ordering::Acquire) || !ready2.load(Ordering::Acquire) {
105                std::hint::spin_loop();
106            }
107
108            // Let them race for ~2μs.
109            let t_start = mach_time();
110            let t_end = t_start.wrapping_add(window_ticks);
111            while mach_time() < t_end {
112                std::hint::spin_loop();
113            }
114
115            // Stop both threads.
116            stop.store(true, Ordering::Release);
117            let _ = handle1.join();
118            let _ = handle2.join();
119
120            let v1 = count1.load(Ordering::Acquire);
121            let v2 = count2.load(Ordering::Acquire);
122            let diff = v1.abs_diff(v2);
123            diffs.push(diff);
124        }
125
126        extract_timing_entropy(&diffs, n_samples)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn info() {
136        let src = DVFSRaceSource;
137        assert_eq!(src.info().name, "dvfs_race");
138        assert!(matches!(src.info().category, SourceCategory::Microarch));
139        assert!(!src.info().composite);
140    }
141
142    #[test]
143    #[ignore] // Hardware-dependent: requires multi-core CPU
144    fn collects_bytes() {
145        let src = DVFSRaceSource;
146        assert!(src.is_available());
147        let data = src.collect(64);
148        assert!(!data.is_empty());
149        // Check we get some variation (not all zeros or all same).
150        let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
151        assert!(unique.len() > 1, "Expected variation in collected bytes");
152    }
153}