Skip to main content

openentropy_core/sources/
memory.rs

1//! MemoryTimingSource — DRAM allocation and access timing.
2//!
3//! Repeatedly allocates page-sized anonymous mmap regions, touches them,
4//! measures the timing, and extracts LSBs as entropy.
5
6use std::ptr;
7use std::time::Instant;
8
9use crate::source::{EntropySource, SourceCategory, SourceInfo};
10
11/// Page size for mmap allocations (4 KB on most platforms).
12const PAGE_SIZE: usize = 4096;
13
14static MEMORY_TIMING_INFO: SourceInfo = SourceInfo {
15    name: "memory_timing",
16    description: "DRAM allocation and access timing jitter via mmap",
17    physics: "Times memory allocation (malloc/mmap) and access patterns. Allocation jitter \
18              comes from heap fragmentation, page fault handling, and kernel memory pressure. \
19              Access timing varies with: DRAM refresh interference (~64ms cycle), cache \
20              hierarchy state (L1/L2/L3 hits vs misses), and memory controller scheduling.",
21    category: SourceCategory::Hardware,
22    platform_requirements: &[],
23    entropy_rate_estimate: 1500.0,
24    composite: false,
25};
26
27/// Entropy source that harvests timing jitter from memory allocation and access.
28pub struct MemoryTimingSource;
29
30impl EntropySource for MemoryTimingSource {
31    fn info(&self) -> &SourceInfo {
32        &MEMORY_TIMING_INFO
33    }
34
35    fn is_available(&self) -> bool {
36        // mmap with MAP_ANONYMOUS is available on all Unix-like systems.
37        cfg!(unix)
38    }
39
40    fn collect(&self, n_samples: usize) -> Vec<u8> {
41        let mut output = Vec::with_capacity(n_samples);
42        let mut prev_ns: u64 = 0;
43
44        // Each iteration: mmap a page, touch it, munmap it, measure timing.
45        // We need n_samples + 1 iterations to get n_samples deltas.
46        let iterations = n_samples + 1;
47
48        for i in 0..iterations {
49            let t0 = Instant::now();
50
51            // SAFETY: mmap with MAP_ANONYMOUS|MAP_PRIVATE creates a private anonymous
52            // mapping. We check for MAP_FAILED before using the returned address.
53            let addr = unsafe {
54                libc::mmap(
55                    ptr::null_mut(),
56                    PAGE_SIZE,
57                    libc::PROT_READ | libc::PROT_WRITE,
58                    libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
59                    -1, // no file descriptor
60                    0,  // offset
61                )
62            };
63
64            if addr == libc::MAP_FAILED {
65                continue;
66            }
67
68            // SAFETY: addr is a valid mmap'd region of PAGE_SIZE bytes (checked
69            // != MAP_FAILED above). page.add(PAGE_SIZE - 1) is within the mapping.
70            // munmap at the end deallocates the region.
71            unsafe {
72                let page = addr as *mut u8;
73                // Write to first and last byte of the page to touch both ends.
74                ptr::write_volatile(page, 0xAA);
75                ptr::write_volatile(page.add(PAGE_SIZE - 1), 0x55);
76
77                // Read back to force a full round-trip.
78                let _v1 = ptr::read_volatile(page);
79                let _v2 = ptr::read_volatile(page.add(PAGE_SIZE - 1));
80
81                // Unmap immediately to keep memory usage bounded.
82                libc::munmap(addr, PAGE_SIZE);
83            }
84
85            let elapsed_ns = t0.elapsed().as_nanos() as u64;
86
87            if i > 0 {
88                // Delta between consecutive allocation timings.
89                let delta = elapsed_ns.wrapping_sub(prev_ns);
90                // XOR the low two bytes together for extra mixing, then take LSB.
91                let mixed = (delta as u8) ^ ((delta >> 8) as u8);
92                output.push(mixed);
93
94                if output.len() >= n_samples {
95                    break;
96                }
97            }
98
99            prev_ns = elapsed_ns;
100        }
101
102        output.truncate(n_samples);
103        output
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    #[cfg(unix)]
113    #[ignore] // Run with: cargo test -- --ignored
114    fn memory_timing_collects_bytes() {
115        let src = MemoryTimingSource;
116        assert!(src.is_available());
117        let data = src.collect(128);
118        assert!(!data.is_empty());
119        assert!(data.len() <= 128);
120    }
121
122    #[test]
123    fn memory_timing_info() {
124        let src = MemoryTimingSource;
125        assert_eq!(src.name(), "memory_timing");
126        assert_eq!(src.info().category, SourceCategory::Hardware);
127    }
128}