Skip to main content

openentropy_core/sources/frontier/
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::{mach_time, xor_fold_u64};
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::Any,
46    requirements: &[],
47    entropy_rate_estimate: 5000.0,
48    composite: false,
49};
50
51impl EntropySource for DVFSRaceSource {
52    fn info(&self) -> &SourceInfo {
53        &DVFS_RACE_INFO
54    }
55
56    fn is_available(&self) -> bool {
57        true
58    }
59
60    fn collect(&self, n_samples: usize) -> Vec<u8> {
61        // We need enough race differentials to extract n_samples bytes.
62        // Each race produces one u64 differential; XOR-fold pairs → bytes.
63        let raw_count = n_samples * 4 + 64;
64        let mut diffs: Vec<u64> = Vec::with_capacity(raw_count);
65
66        // Get timebase for ~2μs window calculation.
67        // On Apple Silicon, mach_absolute_time ticks at 24MHz → 1 tick ≈ 41.67ns.
68        // 2μs ≈ 48 ticks. Use a small window to keep collection fast.
69        let window_ticks: u64 = 48; // ~2μs on Apple Silicon
70
71        for _ in 0..raw_count {
72            let stop = Arc::new(AtomicBool::new(false));
73            let count1 = Arc::new(AtomicU64::new(0));
74            let count2 = Arc::new(AtomicU64::new(0));
75            let ready1 = Arc::new(AtomicBool::new(false));
76            let ready2 = Arc::new(AtomicBool::new(false));
77
78            let s1 = stop.clone();
79            let c1 = count1.clone();
80            let r1 = ready1.clone();
81            let handle1 = thread::spawn(move || {
82                let mut local_count: u64 = 0;
83                r1.store(true, Ordering::Release);
84                while !s1.load(Ordering::Relaxed) {
85                    local_count = local_count.wrapping_add(1);
86                }
87                c1.store(local_count, Ordering::Release);
88            });
89
90            let s2 = stop.clone();
91            let c2 = count2.clone();
92            let r2 = ready2.clone();
93            let handle2 = thread::spawn(move || {
94                let mut local_count: u64 = 0;
95                r2.store(true, Ordering::Release);
96                while !s2.load(Ordering::Relaxed) {
97                    local_count = local_count.wrapping_add(1);
98                }
99                c2.store(local_count, Ordering::Release);
100            });
101
102            // Wait for both threads to be ready.
103            while !ready1.load(Ordering::Acquire) || !ready2.load(Ordering::Acquire) {
104                std::hint::spin_loop();
105            }
106
107            // Let them race for ~2μs.
108            let t_start = mach_time();
109            let t_end = t_start.wrapping_add(window_ticks);
110            while mach_time() < t_end {
111                std::hint::spin_loop();
112            }
113
114            // Stop both threads.
115            stop.store(true, Ordering::Release);
116            let _ = handle1.join();
117            let _ = handle2.join();
118
119            let v1 = count1.load(Ordering::Acquire);
120            let v2 = count2.load(Ordering::Acquire);
121            let diff = v1.abs_diff(v2);
122            diffs.push(diff);
123        }
124
125        // Extract entropy: XOR adjacent diffs, then xor-fold to bytes.
126        let xored: Vec<u64> = diffs.windows(2).map(|w| w[0] ^ w[1]).collect();
127        let mut raw: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
128        raw.truncate(n_samples);
129        raw
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn info() {
139        let src = DVFSRaceSource;
140        assert_eq!(src.info().name, "dvfs_race");
141        assert!(matches!(src.info().category, SourceCategory::Microarch));
142        assert!(!src.info().composite);
143    }
144
145    #[test]
146    #[ignore] // Hardware-dependent: requires multi-core CPU
147    fn collects_bytes() {
148        let src = DVFSRaceSource;
149        assert!(src.is_available());
150        let data = src.collect(64);
151        assert!(!data.is_empty());
152        // Check we get some variation (not all zeros or all same).
153        let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
154        assert!(unique.len() > 1, "Expected variation in collected bytes");
155    }
156}