Skip to main content

openentropy_core/sources/
cross_domain.rs

1//! Cross-domain beat frequency entropy sources.
2//!
3//! These sources measure timing across independent clock domains (CPU, I/O,
4//! memory, kernel).  The beat frequency between PLLs driving each domain
5//! creates timing jitter that serves as entropy.
6
7use std::io::Write;
8
9use tempfile::NamedTempFile;
10
11use crate::source::{EntropySource, SourceCategory, SourceInfo};
12
13use super::helpers::{extract_timing_entropy, mach_time};
14
15// ---------------------------------------------------------------------------
16// CPUIOBeatSource
17// ---------------------------------------------------------------------------
18
19static CPU_IO_BEAT_INFO: SourceInfo = SourceInfo {
20    name: "cpu_io_beat",
21    description: "Cross-domain beat frequency between CPU computation and disk I/O timing",
22    physics: "Alternates CPU-bound computation with disk I/O operations and measures the \
23              transition timing. The CPU and I/O subsystem run on independent clock domains \
24              with separate PLLs. When operations cross domains, the beat frequency of their \
25              PLLs creates timing jitter. This is analogous to the acoustic beat frequency \
26              between two tuning forks.",
27    category: SourceCategory::CrossDomain,
28    platform_requirements: &[],
29    entropy_rate_estimate: 1500.0,
30    composite: false,
31};
32
33/// Entropy source that captures beat frequency between CPU and I/O clock domains.
34pub struct CPUIOBeatSource;
35
36impl EntropySource for CPUIOBeatSource {
37    fn info(&self) -> &SourceInfo {
38        &CPU_IO_BEAT_INFO
39    }
40
41    fn is_available(&self) -> bool {
42        true
43    }
44
45    fn collect(&self, n_samples: usize) -> Vec<u8> {
46        let mut tmpfile = match NamedTempFile::new() {
47            Ok(f) => f,
48            Err(_) => return Vec::new(),
49        };
50
51        // Over-collect raw timings: we need 8 bits per byte, and XOR/LSB
52        // extraction reduces the count.
53        let raw_count = n_samples * 10 + 64;
54        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
55
56        for i in 0..raw_count {
57            let t0 = mach_time();
58
59            // CPU-bound computation: 50 iterations of LCG
60            let mut x: u64 = t0;
61            for _ in 0..50 {
62                x = x.wrapping_mul(6364136223846793005).wrapping_add(1);
63            }
64            std::hint::black_box(x);
65
66            let t1 = mach_time();
67
68            // Disk I/O: write to temp file
69            let buf = [i as u8; 64];
70            let _ = tmpfile.write_all(&buf);
71            if i % 16 == 0 {
72                let _ = tmpfile.flush();
73            }
74
75            let t2 = mach_time();
76
77            // Record the domain-crossing latencies.
78            timings.push(t1.wrapping_sub(t0)); // CPU domain
79            timings.push(t2.wrapping_sub(t1)); // I/O domain
80        }
81
82        extract_timing_entropy(&timings, n_samples)
83    }
84}
85
86// ---------------------------------------------------------------------------
87// CPUMemoryBeatSource
88// ---------------------------------------------------------------------------
89
90/// Size of the memory buffer: 16 MB to exceed L2 cache and force DRAM access.
91const MEM_BUFFER_SIZE: usize = 16 * 1024 * 1024;
92
93static CPU_MEMORY_BEAT_INFO: SourceInfo = SourceInfo {
94    name: "cpu_memory_beat",
95    description: "Cross-domain beat frequency between CPU computation and random memory access timing",
96    physics: "Interleaves CPU computation with random memory accesses to large arrays \
97              (>L2 cache). The memory controller runs on its own clock domain. Cache misses \
98              force the CPU to wait for the memory controller\u{2019}s arbitration, whose timing \
99              depends on: DRAM refresh state, competing DMA from GPU/ANE, and row buffer \
100              conflicts.",
101    category: SourceCategory::CrossDomain,
102    platform_requirements: &[],
103    entropy_rate_estimate: 2500.0,
104    composite: false,
105};
106
107/// Entropy source that captures beat frequency between CPU and memory controller
108/// clock domains.
109pub struct CPUMemoryBeatSource;
110
111impl EntropySource for CPUMemoryBeatSource {
112    fn info(&self) -> &SourceInfo {
113        &CPU_MEMORY_BEAT_INFO
114    }
115
116    fn is_available(&self) -> bool {
117        true
118    }
119
120    fn collect(&self, n_samples: usize) -> Vec<u8> {
121        // Allocate a 16 MB buffer to force DRAM access (exceeds L2 cache).
122        let mut buffer = vec![0u8; MEM_BUFFER_SIZE];
123
124        // Initialize with a simple pattern so the pages are faulted in.
125        for (i, byte) in buffer.iter_mut().enumerate() {
126            *byte = i as u8;
127        }
128
129        let raw_count = n_samples * 10 + 64;
130        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
131
132        // Use an LCG to generate pseudo-random indices into the buffer.
133        let mut lcg: u64 = mach_time() | 1;
134
135        for _ in 0..raw_count {
136            let t0 = mach_time();
137
138            // CPU-bound computation: 50 iterations of LCG
139            let mut x: u64 = t0;
140            for _ in 0..50 {
141                x = x.wrapping_mul(6364136223846793005).wrapping_add(1);
142            }
143            std::hint::black_box(x);
144
145            let t1 = mach_time();
146
147            // Random memory access (likely cache miss for large buffer).
148            lcg = lcg
149                .wrapping_mul(6364136223846793005)
150                .wrapping_add(1442695040888963407);
151            let idx = (lcg as usize) % MEM_BUFFER_SIZE;
152            // SAFETY: idx is bounded by MEM_BUFFER_SIZE via modulo.
153            let val = unsafe { std::ptr::read_volatile(&buffer[idx]) };
154            std::hint::black_box(val);
155
156            let t2 = mach_time();
157
158            timings.push(t1.wrapping_sub(t0)); // CPU domain
159            timings.push(t2.wrapping_sub(t1)); // Memory domain
160        }
161
162        extract_timing_entropy(&timings, n_samples)
163    }
164}
165
166// ---------------------------------------------------------------------------
167// MultiDomainBeatSource
168// ---------------------------------------------------------------------------
169
170/// Size of the memory buffer for multi-domain source: 4 MB.
171const MULTI_BUFFER_SIZE: usize = 4 * 1024 * 1024;
172
173static MULTI_DOMAIN_BEAT_INFO: SourceInfo = SourceInfo {
174    name: "multi_domain_beat",
175    description: "Composite beat frequency across CPU, memory, disk I/O, and kernel syscall clock domains",
176    physics: "Rapidly interleaves operations across 4 clock domains: CPU computation, memory \
177              access, disk I/O, and kernel syscalls. Each domain has its own PLL and \
178              arbitration logic. The composite timing captures interference patterns \
179              between all domains simultaneously.",
180    category: SourceCategory::CrossDomain,
181    platform_requirements: &[],
182    entropy_rate_estimate: 3000.0,
183    composite: false,
184};
185
186/// Entropy source that captures beat frequency across CPU, memory, I/O, and
187/// kernel syscall clock domains simultaneously.
188pub struct MultiDomainBeatSource;
189
190impl EntropySource for MultiDomainBeatSource {
191    fn info(&self) -> &SourceInfo {
192        &MULTI_DOMAIN_BEAT_INFO
193    }
194
195    fn is_available(&self) -> bool {
196        true
197    }
198
199    fn collect(&self, n_samples: usize) -> Vec<u8> {
200        // Allocate a 4 MB buffer for memory accesses.
201        let mut buffer = vec![0u8; MULTI_BUFFER_SIZE];
202        for (i, byte) in buffer.iter_mut().enumerate() {
203            *byte = i as u8;
204        }
205
206        let raw_count = n_samples * 10 + 64;
207        let mut timings: Vec<u64> = Vec::with_capacity(raw_count * 4);
208
209        let mut lcg: u64 = mach_time() | 1;
210
211        for _ in 0..raw_count {
212            // Domain 1: CPU computation (XOR operations)
213            let t0 = mach_time();
214            let mut x: u64 = t0;
215            for _ in 0..50 {
216                x = x.wrapping_mul(6364136223846793005).wrapping_add(1);
217            }
218            std::hint::black_box(x);
219            let t1 = mach_time();
220
221            // Domain 2: Random memory access
222            lcg = lcg
223                .wrapping_mul(6364136223846793005)
224                .wrapping_add(1442695040888963407);
225            let idx = (lcg as usize) % MULTI_BUFFER_SIZE;
226            // SAFETY: idx is bounded by MULTI_BUFFER_SIZE via modulo.
227            let val = unsafe { std::ptr::read_volatile(&buffer[idx]) };
228            std::hint::black_box(val);
229            let t2 = mach_time();
230
231            // SAFETY: getpid() is always safe — it's a simple read-only syscall.
232            unsafe { libc::getpid() };
233            let t3 = mach_time();
234
235            // Record all domain crossing timings.
236            timings.push(t1.wrapping_sub(t0)); // CPU
237            timings.push(t2.wrapping_sub(t1)); // Memory
238            timings.push(t3.wrapping_sub(t2)); // Syscall
239        }
240
241        extract_timing_entropy(&timings, n_samples)
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::super::helpers::extract_lsbs_u64;
248    use super::*;
249
250    #[test]
251    fn cpu_io_beat_info() {
252        let src = CPUIOBeatSource;
253        assert_eq!(src.name(), "cpu_io_beat");
254        assert_eq!(src.info().category, SourceCategory::CrossDomain);
255        assert!((src.info().entropy_rate_estimate - 1500.0).abs() < f64::EPSILON);
256    }
257
258    #[test]
259    #[ignore] // Run with: cargo test -- --ignored
260    fn cpu_io_beat_collects_bytes() {
261        let src = CPUIOBeatSource;
262        assert!(src.is_available());
263        let data = src.collect(64);
264        assert!(!data.is_empty());
265        assert!(data.len() <= 64);
266    }
267
268    #[test]
269    fn cpu_memory_beat_info() {
270        let src = CPUMemoryBeatSource;
271        assert_eq!(src.name(), "cpu_memory_beat");
272        assert_eq!(src.info().category, SourceCategory::CrossDomain);
273        assert!((src.info().entropy_rate_estimate - 2500.0).abs() < f64::EPSILON);
274    }
275
276    #[test]
277    #[ignore] // Run with: cargo test -- --ignored
278    fn cpu_memory_beat_collects_bytes() {
279        let src = CPUMemoryBeatSource;
280        assert!(src.is_available());
281        let data = src.collect(64);
282        assert!(!data.is_empty());
283        assert!(data.len() <= 64);
284    }
285
286    #[test]
287    fn multi_domain_beat_info() {
288        let src = MultiDomainBeatSource;
289        assert_eq!(src.name(), "multi_domain_beat");
290        assert_eq!(src.info().category, SourceCategory::CrossDomain);
291        assert!((src.info().entropy_rate_estimate - 3000.0).abs() < f64::EPSILON);
292    }
293
294    #[test]
295    #[ignore] // Run with: cargo test -- --ignored
296    fn multi_domain_beat_collects_bytes() {
297        let src = MultiDomainBeatSource;
298        assert!(src.is_available());
299        let data = src.collect(64);
300        assert!(!data.is_empty());
301        assert!(data.len() <= 64);
302    }
303
304    #[test]
305    fn extract_lsbs_basic() {
306        let deltas = vec![1u64, 2, 3, 4, 5, 6, 7, 8];
307        let bytes = extract_lsbs_u64(&deltas);
308        // Bits: 1,0,1,0,1,0,1,0 -> 0xAA
309        assert_eq!(bytes.len(), 1);
310        assert_eq!(bytes[0], 0xAA);
311    }
312}