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, Platform, 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::Composite,
28    platform: Platform::Any,
29    requirements: &[],
30    entropy_rate_estimate: 1500.0,
31    composite: false,
32};
33
34/// Entropy source that captures beat frequency between CPU and I/O clock domains.
35pub struct CPUIOBeatSource;
36
37impl EntropySource for CPUIOBeatSource {
38    fn info(&self) -> &SourceInfo {
39        &CPU_IO_BEAT_INFO
40    }
41
42    fn is_available(&self) -> bool {
43        true
44    }
45
46    fn collect(&self, n_samples: usize) -> Vec<u8> {
47        let mut tmpfile = match NamedTempFile::new() {
48            Ok(f) => f,
49            Err(_) => return Vec::new(),
50        };
51
52        // Over-collect raw timings: we need 8 bits per byte, and XOR/LSB
53        // extraction reduces the count.
54        let raw_count = n_samples * 10 + 64;
55        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
56
57        for i in 0..raw_count {
58            let t0 = mach_time();
59
60            // CPU-bound computation: 50 iterations of LCG
61            let mut x: u64 = t0;
62            for _ in 0..50 {
63                x = x.wrapping_mul(6364136223846793005).wrapping_add(1);
64            }
65            std::hint::black_box(x);
66
67            let t1 = mach_time();
68
69            // Disk I/O: write to temp file
70            let buf = [i as u8; 64];
71            let _ = tmpfile.write_all(&buf);
72            if i % 16 == 0 {
73                let _ = tmpfile.flush();
74            }
75
76            let t2 = mach_time();
77
78            // Record the domain-crossing latencies.
79            timings.push(t1.wrapping_sub(t0)); // CPU domain
80            timings.push(t2.wrapping_sub(t1)); // I/O domain
81        }
82
83        extract_timing_entropy(&timings, n_samples)
84    }
85}
86
87// ---------------------------------------------------------------------------
88// CPUMemoryBeatSource
89// ---------------------------------------------------------------------------
90
91/// Size of the memory buffer: 16 MB to exceed L2 cache and force DRAM access.
92const MEM_BUFFER_SIZE: usize = 16 * 1024 * 1024;
93
94static CPU_MEMORY_BEAT_INFO: SourceInfo = SourceInfo {
95    name: "cpu_memory_beat",
96    description: "Cross-domain beat frequency between CPU computation and random memory access timing",
97    physics: "Interleaves CPU computation with random memory accesses to large arrays \
98              (>L2 cache). The memory controller runs on its own clock domain. Cache misses \
99              force the CPU to wait for the memory controller\u{2019}s arbitration, whose timing \
100              depends on: DRAM refresh state, competing DMA from GPU/ANE, and row buffer \
101              conflicts.",
102    category: SourceCategory::Composite,
103    platform: Platform::Any,
104    requirements: &[],
105    entropy_rate_estimate: 2500.0,
106    composite: false,
107};
108
109/// Entropy source that captures beat frequency between CPU and memory controller
110/// clock domains.
111pub struct CPUMemoryBeatSource;
112
113impl EntropySource for CPUMemoryBeatSource {
114    fn info(&self) -> &SourceInfo {
115        &CPU_MEMORY_BEAT_INFO
116    }
117
118    fn is_available(&self) -> bool {
119        true
120    }
121
122    fn collect(&self, n_samples: usize) -> Vec<u8> {
123        // Allocate a 16 MB buffer to force DRAM access (exceeds L2 cache).
124        let mut buffer = vec![0u8; MEM_BUFFER_SIZE];
125
126        // Initialize with a simple pattern so the pages are faulted in.
127        for (i, byte) in buffer.iter_mut().enumerate() {
128            *byte = i as u8;
129        }
130
131        let raw_count = n_samples * 10 + 64;
132        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
133
134        // Use an LCG to generate pseudo-random indices into the buffer.
135        let mut lcg: u64 = mach_time() | 1;
136
137        for _ in 0..raw_count {
138            let t0 = mach_time();
139
140            // CPU-bound computation: 50 iterations of LCG
141            let mut x: u64 = t0;
142            for _ in 0..50 {
143                x = x.wrapping_mul(6364136223846793005).wrapping_add(1);
144            }
145            std::hint::black_box(x);
146
147            let t1 = mach_time();
148
149            // Random memory access (likely cache miss for large buffer).
150            lcg = lcg
151                .wrapping_mul(6364136223846793005)
152                .wrapping_add(1442695040888963407);
153            let idx = (lcg as usize) % MEM_BUFFER_SIZE;
154            // SAFETY: idx is bounded by MEM_BUFFER_SIZE via modulo.
155            let val = unsafe { std::ptr::read_volatile(&buffer[idx]) };
156            std::hint::black_box(val);
157
158            let t2 = mach_time();
159
160            timings.push(t1.wrapping_sub(t0)); // CPU domain
161            timings.push(t2.wrapping_sub(t1)); // Memory domain
162        }
163
164        extract_timing_entropy(&timings, n_samples)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::super::helpers::extract_lsbs_u64;
171    use super::*;
172
173    #[test]
174    fn cpu_io_beat_info() {
175        let src = CPUIOBeatSource;
176        assert_eq!(src.name(), "cpu_io_beat");
177        assert_eq!(src.info().category, SourceCategory::Composite);
178        assert!((src.info().entropy_rate_estimate - 1500.0).abs() < f64::EPSILON);
179    }
180
181    #[test]
182    #[ignore] // Run with: cargo test -- --ignored
183    fn cpu_io_beat_collects_bytes() {
184        let src = CPUIOBeatSource;
185        assert!(src.is_available());
186        let data = src.collect(64);
187        assert!(!data.is_empty());
188        assert!(data.len() <= 64);
189    }
190
191    #[test]
192    fn cpu_memory_beat_info() {
193        let src = CPUMemoryBeatSource;
194        assert_eq!(src.name(), "cpu_memory_beat");
195        assert_eq!(src.info().category, SourceCategory::Composite);
196        assert!((src.info().entropy_rate_estimate - 2500.0).abs() < f64::EPSILON);
197    }
198
199    #[test]
200    #[ignore] // Run with: cargo test -- --ignored
201    fn cpu_memory_beat_collects_bytes() {
202        let src = CPUMemoryBeatSource;
203        assert!(src.is_available());
204        let data = src.collect(64);
205        assert!(!data.is_empty());
206        assert!(data.len() <= 64);
207    }
208
209    #[test]
210    fn extract_lsbs_basic() {
211        let deltas = vec![1u64, 2, 3, 4, 5, 6, 7, 8];
212        let bytes = extract_lsbs_u64(&deltas);
213        // Bits: 1,0,1,0,1,0,1,0 -> 0xAA
214        assert_eq!(bytes.len(), 1);
215        assert_eq!(bytes[0], 0xAA);
216    }
217}