Skip to main content

openentropy_core/sources/
silicon.rs

1//! Silicon-level entropy sources that exploit CPU and DRAM microarchitecture
2//! timing: row buffer contention, cache hierarchy interference, page fault
3//! resolution, and speculative execution pipeline state.
4
5use rand::Rng;
6
7use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
8
9use super::helpers::{extract_timing_entropy, mach_time};
10
11// ---------------------------------------------------------------------------
12// 1. DRAMRowBufferSource
13// ---------------------------------------------------------------------------
14
15/// Measures DRAM row buffer hit/miss timing by accessing random locations in a
16/// large (32 MB) buffer that exceeds L2/L3 cache capacity. The exact timing of
17/// each access depends on physical address mapping, row buffer state from all
18/// system activity, memory controller scheduling, and DRAM refresh
19/// interference.
20pub struct DRAMRowBufferSource;
21
22static DRAM_ROW_BUFFER_INFO: SourceInfo = SourceInfo {
23    name: "dram_row_buffer",
24    description: "DRAM row buffer hit/miss timing from random memory accesses",
25    physics: "Measures DRAM row buffer hit/miss timing by accessing different memory rows. \
26              DRAM is organized into rows of capacitor cells. Accessing an open row (hit) \
27              is fast; accessing a different row requires precharge + activate (miss), \
28              which is slower. The exact timing depends on: physical address mapping, \
29              row buffer state from ALL system activity, memory controller scheduling, \
30              and DRAM refresh interference.",
31    category: SourceCategory::Timing,
32    platform: Platform::Any,
33    requirements: &[],
34    entropy_rate_estimate: 3000.0,
35    composite: false,
36};
37
38impl EntropySource for DRAMRowBufferSource {
39    fn info(&self) -> &SourceInfo {
40        &DRAM_ROW_BUFFER_INFO
41    }
42
43    fn is_available(&self) -> bool {
44        true
45    }
46
47    fn collect(&self, n_samples: usize) -> Vec<u8> {
48        const BUF_SIZE: usize = 32 * 1024 * 1024; // 32 MB — exceeds L2/L3 cache
49
50        // We need ~(n_samples + 2) XOR'd deltas, which requires ~(n_samples + 4)
51        // raw timings. Oversample 4x to give XOR-folding more to work with.
52        let num_accesses = n_samples * 4 + 64;
53
54        // Allocate a large buffer and touch it to ensure pages are backed.
55        let mut buffer: Vec<u8> = vec![0u8; BUF_SIZE];
56        for i in (0..BUF_SIZE).step_by(4096) {
57            buffer[i] = i as u8;
58        }
59
60        let mut rng = rand::rng();
61        let mut timings = Vec::with_capacity(num_accesses);
62
63        for _ in 0..num_accesses {
64            // Access two distant random locations per measurement to amplify
65            // row buffer miss timing variation.
66            let idx1 = rng.random_range(0..BUF_SIZE);
67            let idx2 = rng.random_range(0..BUF_SIZE);
68
69            let t0 = mach_time();
70            // SAFETY: idx1 and idx2 are bounded by BUF_SIZE via random_range.
71            // read_volatile prevents the compiler from eliding the accesses.
72            let _v1 = unsafe { std::ptr::read_volatile(&buffer[idx1]) };
73            let _v2 = unsafe { std::ptr::read_volatile(&buffer[idx2]) };
74            let t1 = mach_time();
75
76            timings.push(t1.wrapping_sub(t0));
77        }
78
79        // Prevent the buffer from being optimized away.
80        std::hint::black_box(&buffer);
81
82        extract_timing_entropy(&timings, n_samples)
83    }
84}
85
86// ---------------------------------------------------------------------------
87// 2. CacheContentionSource
88// ---------------------------------------------------------------------------
89
90/// Measures L1/L2 cache miss patterns by alternating between sequential
91/// (cache-friendly) and random (cache-hostile) access patterns on an 8 MB
92/// buffer that spans the L2 boundary. Cache timing depends on what every other
93/// process and hardware unit is doing — the cache is a shared resource whose
94/// state is fundamentally unpredictable.
95pub struct CacheContentionSource;
96
97static CACHE_CONTENTION_INFO: SourceInfo = SourceInfo {
98    name: "cache_contention",
99    description: "L1/L2 cache contention timing from alternating access patterns",
100    physics: "Measures L1/L2 cache miss patterns by alternating access patterns. Cache \
101              timing depends on what every other process and hardware unit is doing \
102              \u{2014} the cache is a shared resource whose state is fundamentally \
103              unpredictable. A cache miss requires main memory access (100+ ns vs \
104              1 ns for L1 hit).",
105    category: SourceCategory::Timing,
106    platform: Platform::Any,
107    requirements: &[],
108    entropy_rate_estimate: 2500.0,
109    composite: false,
110};
111
112impl EntropySource for CacheContentionSource {
113    fn info(&self) -> &SourceInfo {
114        &CACHE_CONTENTION_INFO
115    }
116
117    fn is_available(&self) -> bool {
118        true
119    }
120
121    fn collect(&self, n_samples: usize) -> Vec<u8> {
122        const BUF_SIZE: usize = 8 * 1024 * 1024; // 8 MB — spans L2 boundary
123
124        let mut buffer: Vec<u8> = vec![0u8; BUF_SIZE];
125        // Touch pages to ensure they are resident.
126        for i in (0..BUF_SIZE).step_by(4096) {
127            buffer[i] = i as u8;
128        }
129
130        // 4x oversampling for better XOR-fold quality.
131        let num_rounds = n_samples * 4 + 64;
132        let mut rng = rand::rng();
133        let mut timings = Vec::with_capacity(num_rounds);
134
135        for round in 0..num_rounds {
136            let t0 = mach_time();
137
138            // Cycle through 3 access patterns for more contention diversity:
139            // sequential, random, and strided (cache-line bouncing).
140            match round % 3 {
141                0 => {
142                    // Sequential access — cache-friendly.
143                    let start = rng.random_range(0..BUF_SIZE.saturating_sub(512));
144                    let mut sink: u8 = 0;
145                    for offset in 0..512 {
146                        // SAFETY: start + offset < BUF_SIZE due to saturating_sub(512) bound.
147                        sink ^= unsafe { std::ptr::read_volatile(&buffer[start + offset]) };
148                    }
149                    std::hint::black_box(sink);
150                }
151                1 => {
152                    // Random access — cache-hostile.
153                    let mut sink: u8 = 0;
154                    for _ in 0..512 {
155                        let idx = rng.random_range(0..BUF_SIZE);
156                        // SAFETY: idx is bounded by BUF_SIZE via random_range.
157                        sink ^= unsafe { std::ptr::read_volatile(&buffer[idx]) };
158                    }
159                    std::hint::black_box(sink);
160                }
161                _ => {
162                    // Strided access — cache-line bouncing (64-byte stride).
163                    let start = rng.random_range(0..BUF_SIZE.saturating_sub(512 * 64));
164                    let mut sink: u8 = 0;
165                    for i in 0..512 {
166                        // SAFETY: start + i*64 < BUF_SIZE due to saturating_sub(512*64) bound.
167                        sink ^= unsafe { std::ptr::read_volatile(&buffer[start + i * 64]) };
168                    }
169                    std::hint::black_box(sink);
170                }
171            }
172
173            let t1 = mach_time();
174            timings.push(t1.wrapping_sub(t0));
175        }
176
177        std::hint::black_box(&buffer);
178
179        extract_timing_entropy(&timings, n_samples)
180    }
181}
182
183// ---------------------------------------------------------------------------
184// 3. PageFaultTimingSource
185// ---------------------------------------------------------------------------
186
187/// Triggers and times minor page faults via `mmap`/`munmap`. Page fault
188/// resolution requires TLB lookup, hardware page table walk (up to 4 levels on
189/// ARM64), physical page allocation from the kernel free list, and zero-fill
190/// for security. The timing depends on physical memory fragmentation.
191pub struct PageFaultTimingSource;
192
193static PAGE_FAULT_TIMING_INFO: SourceInfo = SourceInfo {
194    name: "page_fault_timing",
195    description: "Minor page fault timing via mmap/munmap cycles",
196    physics: "Triggers and times minor page faults via mmap/munmap. Page fault resolution \
197              requires: TLB lookup, hardware page table walk (up to 4 levels on ARM64), \
198              physical page allocation from the kernel free list, and zero-fill for \
199              security. The timing depends on physical memory fragmentation.",
200    category: SourceCategory::Timing,
201    platform: Platform::Any,
202    requirements: &[],
203    entropy_rate_estimate: 1500.0,
204    composite: false,
205};
206
207impl EntropySource for PageFaultTimingSource {
208    fn info(&self) -> &SourceInfo {
209        &PAGE_FAULT_TIMING_INFO
210    }
211
212    fn is_available(&self) -> bool {
213        true
214    }
215
216    fn collect(&self, n_samples: usize) -> Vec<u8> {
217        // SAFETY: sysconf(_SC_PAGESIZE) is always safe and returns the page size.
218        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize };
219        let num_pages: usize = 8;
220        let map_size = page_size * num_pages;
221
222        // 4x oversampling; each cycle produces `num_pages` timings.
223        let num_cycles = (n_samples * 4 / num_pages) + 4;
224
225        let mut timings = Vec::with_capacity(num_cycles * num_pages);
226
227        for _ in 0..num_cycles {
228            // SAFETY: mmap with MAP_ANONYMOUS|MAP_PRIVATE creates a private anonymous
229            // mapping. We check for MAP_FAILED before using the returned address.
230            let addr = unsafe {
231                libc::mmap(
232                    std::ptr::null_mut(),
233                    map_size,
234                    libc::PROT_READ | libc::PROT_WRITE,
235                    libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
236                    -1,
237                    0,
238                )
239            };
240
241            if addr == libc::MAP_FAILED {
242                continue;
243            }
244
245            // Touch each page to trigger a minor fault and time it with
246            // high-resolution mach_time instead of Instant.
247            for p in 0..num_pages {
248                // SAFETY: addr points to a valid mmap region of map_size bytes.
249                // p * page_size < map_size since p < num_pages and map_size = num_pages * page_size.
250                let page_ptr = unsafe { (addr as *mut u8).add(p * page_size) };
251
252                let t0 = mach_time();
253                // SAFETY: page_ptr points within a valid mmap'd region. We write then
254                // read to trigger a page fault and install a TLB entry.
255                unsafe {
256                    std::ptr::write_volatile(page_ptr, 0xAA);
257                    let _v = std::ptr::read_volatile(page_ptr);
258                }
259                let t1 = mach_time();
260
261                timings.push(t1.wrapping_sub(t0));
262            }
263
264            // SAFETY: addr was returned by mmap (checked != MAP_FAILED) with size map_size.
265            unsafe {
266                libc::munmap(addr, map_size);
267            }
268        }
269
270        extract_timing_entropy(&timings, n_samples)
271    }
272}
273
274// ---------------------------------------------------------------------------
275// 4. SpeculativeExecutionSource
276// ---------------------------------------------------------------------------
277
278/// Measures timing variations from the CPU's speculative execution engine. The
279/// branch predictor maintains per-address history that depends on ALL
280/// previously executed code. Mispredictions cause pipeline flushes (~15 cycle
281/// penalty on M4). By running data-dependent branches and measuring timing, we
282/// capture the predictor's internal state which is influenced by all prior
283/// program activity on the core.
284pub struct SpeculativeExecutionSource;
285
286static SPECULATIVE_EXECUTION_INFO: SourceInfo = SourceInfo {
287    name: "speculative_execution",
288    description: "Branch predictor state timing via data-dependent branches",
289    physics: "Measures timing variations from the CPU's speculative execution engine. \
290              The branch predictor maintains per-address history that depends on ALL \
291              previously executed code. Mispredictions cause pipeline flushes (~15 cycle \
292              penalty on M4). By running data-dependent branches and measuring timing, \
293              we capture the predictor's internal state.",
294    category: SourceCategory::Microarch,
295    platform: Platform::Any,
296    requirements: &[],
297    entropy_rate_estimate: 2000.0,
298    composite: false,
299};
300
301impl EntropySource for SpeculativeExecutionSource {
302    fn info(&self) -> &SourceInfo {
303        &SPECULATIVE_EXECUTION_INFO
304    }
305
306    fn is_available(&self) -> bool {
307        true
308    }
309
310    fn collect(&self, n_samples: usize) -> Vec<u8> {
311        // Variable batch sizes to create different workloads per measurement.
312        let num_batches = n_samples * 4 + 64;
313        let mut timings = Vec::with_capacity(num_batches);
314
315        // LCG state — seeded from the high-resolution clock so every call
316        // exercises a different branch sequence.
317        let mut lcg_state: u64 = mach_time() ^ 0xDEAD_BEEF_CAFE_BABE;
318
319        for batch_idx in 0..num_batches {
320            // Variable workload: batch size varies with index for different
321            // branch predictor pressure levels.
322            let batch_size = 10 + (batch_idx % 31);
323
324            let t0 = mach_time();
325
326            // Execute a batch of data-dependent branches that defeat the
327            // branch predictor because outcomes depend on runtime LCG values.
328            let mut accumulator: u64 = 0;
329            for _ in 0..batch_size {
330                // Advance LCG: x' = x * 6364136223846793005 + 1442695040888963407
331                lcg_state = lcg_state
332                    .wrapping_mul(6364136223846793005)
333                    .wrapping_add(1442695040888963407);
334
335                // Data-dependent branch — outcome unknowable to predictor.
336                if lcg_state & 0x8000_0000 != 0 {
337                    accumulator = accumulator.wrapping_add(lcg_state);
338                } else {
339                    accumulator = accumulator.wrapping_mul(lcg_state | 1);
340                }
341
342                // Additional branch for more predictor pressure.
343                if (lcg_state >> 16) & 0xFF > 128 {
344                    accumulator ^= lcg_state.rotate_left(7);
345                } else {
346                    accumulator ^= lcg_state.rotate_right(11);
347                }
348
349                // Third branch varying with batch index for extra state diversity.
350                if (lcg_state >> 32) & 0x1 != 0 {
351                    accumulator = accumulator.wrapping_add(batch_idx as u64);
352                }
353            }
354
355            // Prevent the compiler from optimizing away the computation.
356            std::hint::black_box(accumulator);
357
358            let t1 = mach_time();
359            timings.push(t1.wrapping_sub(t0));
360        }
361
362        extract_timing_entropy(&timings, n_samples)
363    }
364}
365
366// ---------------------------------------------------------------------------
367// Tests
368// ---------------------------------------------------------------------------
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    #[ignore] // Run with: cargo test -- --ignored
376    fn dram_row_buffer_collects_bytes() {
377        let src = DRAMRowBufferSource;
378        assert!(src.is_available());
379        let data = src.collect(128);
380        assert!(!data.is_empty());
381        assert!(data.len() <= 128);
382        // Sanity: not all bytes should be identical.
383        if data.len() > 1 {
384            let first = data[0];
385            assert!(data.iter().any(|&b| b != first), "all bytes were identical");
386        }
387    }
388
389    #[test]
390    #[ignore] // Run with: cargo test -- --ignored
391    fn cache_contention_collects_bytes() {
392        let src = CacheContentionSource;
393        assert!(src.is_available());
394        let data = src.collect(128);
395        assert!(!data.is_empty());
396        assert!(data.len() <= 128);
397        if data.len() > 1 {
398            let first = data[0];
399            assert!(data.iter().any(|&b| b != first), "all bytes were identical");
400        }
401    }
402
403    #[test]
404    #[ignore] // Run with: cargo test -- --ignored
405    fn page_fault_timing_collects_bytes() {
406        let src = PageFaultTimingSource;
407        assert!(src.is_available());
408        let data = src.collect(64);
409        assert!(!data.is_empty());
410        assert!(data.len() <= 64);
411    }
412
413    #[test]
414    #[ignore] // Run with: cargo test -- --ignored
415    fn speculative_execution_collects_bytes() {
416        let src = SpeculativeExecutionSource;
417        assert!(src.is_available());
418        let data = src.collect(128);
419        assert!(!data.is_empty());
420        assert!(data.len() <= 128);
421        if data.len() > 1 {
422            let first = data[0];
423            assert!(data.iter().any(|&b| b != first), "all bytes were identical");
424        }
425    }
426
427    #[test]
428    fn source_info_categories() {
429        assert_eq!(DRAMRowBufferSource.info().category, SourceCategory::Timing);
430        assert_eq!(
431            CacheContentionSource.info().category,
432            SourceCategory::Timing
433        );
434        assert_eq!(
435            PageFaultTimingSource.info().category,
436            SourceCategory::Timing
437        );
438        assert_eq!(
439            SpeculativeExecutionSource.info().category,
440            SourceCategory::Microarch
441        );
442    }
443
444    #[test]
445    fn source_info_names() {
446        assert_eq!(DRAMRowBufferSource.name(), "dram_row_buffer");
447        assert_eq!(CacheContentionSource.name(), "cache_contention");
448        assert_eq!(PageFaultTimingSource.name(), "page_fault_timing");
449        assert_eq!(SpeculativeExecutionSource.name(), "speculative_execution");
450    }
451}