openentropy_core/sources/microarch/
dvfs_race.rs1use 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
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::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 let raw_count = (n_samples * 4 + 64).min(256);
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 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] fn collects_bytes() {
145 let src = DVFSRaceSource;
146 assert!(src.is_available());
147 let data = src.collect(64);
148 assert!(!data.is_empty());
149 let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
151 assert!(unique.len() > 1, "Expected variation in collected bytes");
152 }
153}