openentropy_core/sources/frontier/
dvfs_race.rs1use 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
16pub 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 let raw_count = n_samples * 4 + 64;
64 let mut diffs: Vec<u64> = Vec::with_capacity(raw_count);
65
66 let window_ticks: u64 = 48; 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 while !ready1.load(Ordering::Acquire) || !ready2.load(Ordering::Acquire) {
104 std::hint::spin_loop();
105 }
106
107 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.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 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] fn collects_bytes() {
148 let src = DVFSRaceSource;
149 assert!(src.is_available());
150 let data = src.collect(64);
151 assert!(!data.is_empty());
152 let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
154 assert!(unique.len() > 1, "Expected variation in collected bytes");
155 }
156}