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, Platform, 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::Timing,
22    platform: Platform::Any,
23    requirements: &[],
24    entropy_rate_estimate: 1500.0,
25    composite: false,
26};
27
28/// Entropy source that harvests timing jitter from memory allocation and access.
29pub struct MemoryTimingSource;
30
31impl EntropySource for MemoryTimingSource {
32    fn info(&self) -> &SourceInfo {
33        &MEMORY_TIMING_INFO
34    }
35
36    fn is_available(&self) -> bool {
37        // mmap with MAP_ANONYMOUS is available on all Unix-like systems.
38        cfg!(unix)
39    }
40
41    fn collect(&self, n_samples: usize) -> Vec<u8> {
42        let mut output = Vec::with_capacity(n_samples);
43        let mut prev_ns: u64 = 0;
44
45        // Each iteration: mmap a page, touch it, munmap it, measure timing.
46        // We need n_samples + 1 iterations to get n_samples deltas.
47        let iterations = n_samples + 1;
48
49        for i in 0..iterations {
50            let t0 = Instant::now();
51
52            // SAFETY: mmap with MAP_ANONYMOUS|MAP_PRIVATE creates a private anonymous
53            // mapping. We check for MAP_FAILED before using the returned address.
54            let addr = unsafe {
55                libc::mmap(
56                    ptr::null_mut(),
57                    PAGE_SIZE,
58                    libc::PROT_READ | libc::PROT_WRITE,
59                    libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
60                    -1, // no file descriptor
61                    0,  // offset
62                )
63            };
64
65            if addr == libc::MAP_FAILED {
66                continue;
67            }
68
69            // SAFETY: addr is a valid mmap'd region of PAGE_SIZE bytes (checked
70            // != MAP_FAILED above). page.add(PAGE_SIZE - 1) is within the mapping.
71            // munmap at the end deallocates the region.
72            unsafe {
73                let page = addr as *mut u8;
74                // Write to first and last byte of the page to touch both ends.
75                ptr::write_volatile(page, 0xAA);
76                ptr::write_volatile(page.add(PAGE_SIZE - 1), 0x55);
77
78                // Read back to force a full round-trip.
79                let _v1 = ptr::read_volatile(page);
80                let _v2 = ptr::read_volatile(page.add(PAGE_SIZE - 1));
81
82                // Unmap immediately to keep memory usage bounded.
83                libc::munmap(addr, PAGE_SIZE);
84            }
85
86            let elapsed_ns = t0.elapsed().as_nanos() as u64;
87
88            if i > 0 {
89                // Delta between consecutive allocation timings.
90                let delta = elapsed_ns.wrapping_sub(prev_ns);
91                // XOR the low two bytes together for extra mixing, then take LSB.
92                let mixed = (delta as u8) ^ ((delta >> 8) as u8);
93                output.push(mixed);
94
95                if output.len() >= n_samples {
96                    break;
97                }
98            }
99
100            prev_ns = elapsed_ns;
101        }
102
103        output.truncate(n_samples);
104        output
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    #[cfg(unix)]
114    #[ignore] // Run with: cargo test -- --ignored
115    fn memory_timing_collects_bytes() {
116        let src = MemoryTimingSource;
117        assert!(src.is_available());
118        let data = src.collect(128);
119        assert!(!data.is_empty());
120        assert!(data.len() <= 128);
121    }
122
123    #[test]
124    fn memory_timing_info() {
125        let src = MemoryTimingSource;
126        assert_eq!(src.name(), "memory_timing");
127        assert_eq!(src.info().category, SourceCategory::Timing);
128    }
129}