openentropy_core/sources/frontier/
dvfs_race.rs1use crate::source::{EntropySource, SourceCategory, SourceInfo};
11use crate::sources::helpers::{mach_time, xor_fold_u64};
12
13use std::sync::Arc;
14use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
15use std::thread;
16
17pub struct DVFSRaceSource;
33
34static DVFS_RACE_INFO: SourceInfo = SourceInfo {
35 name: "dvfs_race",
36 description: "Cross-core DVFS frequency race between thread pairs",
37 physics: "Spawns two threads running tight counting loops on different cores. \
38 After a ~2\u{00b5}s race window, the absolute difference in iteration \
39 counts captures nondeterminism from: scheduler core placement (P-core vs \
40 E-core), cache coherence latency for the stop signal, interrupt jitter, \
41 and cross-core pipeline state differences. On Apple Silicon, P-core and \
42 E-core clusters have separate frequency domains, but the 2\u{00b5}s window is \
43 too short for DVFS transitions (~100\u{00b5}s-1ms); the primary entropy comes \
44 from scheduling and cache-coherence nondeterminism. \
45 PoC measured H\u{221e} = 7.381 bits/byte.",
46 category: SourceCategory::Frontier,
47 platform_requirements: &[],
48 entropy_rate_estimate: 5000.0,
49 composite: false,
50};
51
52impl EntropySource for DVFSRaceSource {
53 fn info(&self) -> &SourceInfo {
54 &DVFS_RACE_INFO
55 }
56
57 fn is_available(&self) -> bool {
58 true
59 }
60
61 fn collect(&self, n_samples: usize) -> Vec<u8> {
62 let raw_count = n_samples * 4 + 64;
65 let mut diffs: Vec<u64> = Vec::with_capacity(raw_count);
66
67 let window_ticks: u64 = 48; 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 while !ready1.load(Ordering::Acquire) || !ready2.load(Ordering::Acquire) {
105 std::hint::spin_loop();
106 }
107
108 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.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 let xored: Vec<u64> = diffs.windows(2).map(|w| w[0] ^ w[1]).collect();
128 let mut raw: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
129 raw.truncate(n_samples);
130 raw
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn info() {
140 let src = DVFSRaceSource;
141 assert_eq!(src.info().name, "dvfs_race");
142 assert!(matches!(src.info().category, SourceCategory::Frontier));
143 assert!(!src.info().composite);
144 }
145
146 #[test]
147 #[ignore] fn collects_bytes() {
149 let src = DVFSRaceSource;
150 assert!(src.is_available());
151 let data = src.collect(64);
152 assert!(!data.is_empty());
153 let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
155 assert!(unique.len() > 1, "Expected variation in collected bytes");
156 }
157}