openentropy_core/sources/frontier/
cas_contention.rs1use crate::source::{EntropySource, SourceCategory, SourceInfo};
9use crate::sources::helpers::{mach_time, xor_fold_u64};
10
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::thread;
14
15const NUM_THREADS: usize = 4;
16const NUM_TARGETS: usize = 64;
17const TARGET_SPACING: usize = 16; #[derive(Debug, Clone)]
22pub struct CASContentionConfig {
23 pub num_threads: usize,
29}
30
31impl Default for CASContentionConfig {
32 fn default() -> Self {
33 Self {
34 num_threads: NUM_THREADS,
35 }
36 }
37}
38
39pub struct CASContentionSource {
52 config: CASContentionConfig,
53}
54
55impl CASContentionSource {
56 pub fn new(config: CASContentionConfig) -> Self {
57 Self { config }
58 }
59}
60
61impl Default for CASContentionSource {
62 fn default() -> Self {
63 Self::new(CASContentionConfig::default())
64 }
65}
66
67static CAS_CONTENTION_INFO: SourceInfo = SourceInfo {
68 name: "cas_contention",
69 description: "Multi-thread atomic CAS arbitration contention jitter",
70 physics: "Spawns 4 threads performing atomic compare-and-swap operations on \
71 shared targets spread across 128-byte-aligned cache lines. The \
72 hardware coherence engine (MOESI protocol on Apple Silicon) must \
73 arbitrate concurrent exclusive-access requests. This arbitration is \
74 physically nondeterministic due to interconnect fabric latency \
75 variations, thermal state, and traffic from other cores/devices. \
76 XOR-combining timing measurements from all threads amplifies the \
77 arbitration entropy. PoC measured H\u{221e} = 2.463 bits/byte.",
78 category: SourceCategory::Frontier,
79 platform_requirements: &[],
80 entropy_rate_estimate: 2000.0,
81 composite: false,
82};
83
84struct ThreadResult {
85 timings: Vec<u64>,
86}
87
88impl EntropySource for CASContentionSource {
89 fn info(&self) -> &SourceInfo {
90 &CAS_CONTENTION_INFO
91 }
92
93 fn is_available(&self) -> bool {
94 true
95 }
96
97 fn collect(&self, n_samples: usize) -> Vec<u8> {
98 let samples_per_thread = n_samples * 4 + 64;
99 let nthreads = self.config.num_threads;
100
101 let total_atomics = NUM_TARGETS * TARGET_SPACING;
103 let targets: Arc<Vec<AtomicU64>> =
104 Arc::new((0..total_atomics).map(|_| AtomicU64::new(0)).collect());
105
106 let go = Arc::new(AtomicU64::new(0));
107 let stop = Arc::new(AtomicU64::new(0));
108
109 let mut handles = Vec::with_capacity(nthreads);
110
111 for thread_id in 0..nthreads {
112 let targets = targets.clone();
113 let go = go.clone();
114 let stop = stop.clone();
115 let count = samples_per_thread;
116
117 handles.push(thread::spawn(move || {
118 let mut timings = Vec::with_capacity(count);
119 let mut lcg: u64 = mach_time() ^ ((thread_id as u64) << 32) | 1;
120
121 while go.load(Ordering::Acquire) == 0 {
123 std::hint::spin_loop();
124 }
125
126 for _ in 0..count {
127 if stop.load(Ordering::Relaxed) != 0 {
128 break;
129 }
130
131 lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
132 let idx = ((lcg >> 32) as usize % NUM_TARGETS) * TARGET_SPACING;
133
134 let t0 = mach_time();
135
136 let expected = targets[idx].load(Ordering::Relaxed);
137 let _ = targets[idx].compare_exchange_weak(
138 expected,
139 expected.wrapping_add(1),
140 Ordering::AcqRel,
141 Ordering::Relaxed,
142 );
143
144 let t1 = mach_time();
145 timings.push(t1.wrapping_sub(t0));
146 }
147
148 ThreadResult { timings }
149 }));
150 }
151
152 go.store(1, Ordering::Release);
154
155 let results: Vec<ThreadResult> = handles
157 .into_iter()
158 .map(|h| h.join().unwrap_or(ThreadResult { timings: vec![] }))
159 .collect();
160
161 stop.store(1, Ordering::Release);
163
164 let min_len = results.iter().map(|r| r.timings.len()).min().unwrap_or(0);
166 if min_len < 4 {
167 return Vec::new();
168 }
169
170 let mut combined: Vec<u64> = Vec::with_capacity(min_len);
171 for i in 0..min_len {
172 let mut val = 0u64;
173 for result in &results {
174 val ^= result.timings[i];
175 }
176 combined.push(val);
177 }
178
179 let deltas: Vec<u64> = combined
181 .windows(2)
182 .map(|w| w[1].wrapping_sub(w[0]))
183 .collect();
184 let xored: Vec<u64> = deltas.windows(2).map(|w| w[0] ^ w[1]).collect();
185 let mut raw: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
186 raw.truncate(n_samples);
187 raw
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn info() {
197 let src = CASContentionSource::default();
198 assert_eq!(src.info().name, "cas_contention");
199 assert!(matches!(src.info().category, SourceCategory::Frontier));
200 assert!(!src.info().composite);
201 }
202
203 #[test]
204 fn custom_config() {
205 let config = CASContentionConfig { num_threads: 2 };
206 let src = CASContentionSource::new(config);
207 assert_eq!(src.config.num_threads, 2);
208 }
209
210 #[test]
211 #[ignore] fn collects_bytes() {
213 let src = CASContentionSource::default();
214 assert!(src.is_available());
215 let data = src.collect(64);
216 assert!(!data.is_empty());
217 let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
218 assert!(unique.len() > 1, "Expected variation in collected bytes");
219 }
220}