Skip to main content

openentropy_core/sources/timing/
page_fault_timing.rs

1//! Page fault timing entropy source via mmap/munmap cycles.
2
3use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
4use crate::sources::helpers::{extract_timing_entropy, mach_time};
5
6/// Triggers and times minor page faults via `mmap`/`munmap`. Page fault
7/// resolution requires TLB lookup, hardware page table walk (up to 4 levels on
8/// ARM64), physical page allocation from the kernel free list, and zero-fill
9/// for security. The timing depends on physical memory fragmentation.
10pub struct PageFaultTimingSource;
11
12static PAGE_FAULT_TIMING_INFO: SourceInfo = SourceInfo {
13    name: "page_fault_timing",
14    description: "Minor page fault timing via mmap/munmap cycles",
15    physics: "Triggers and times minor page faults via mmap/munmap. Page fault resolution \
16              requires: TLB lookup, hardware page table walk (up to 4 levels on ARM64), \
17              physical page allocation from the kernel free list, and zero-fill for \
18              security. The timing depends on physical memory fragmentation.",
19    category: SourceCategory::Timing,
20    platform: Platform::Any,
21    requirements: &[],
22    entropy_rate_estimate: 2.0,
23    composite: false,
24    is_fast: true,
25};
26
27impl EntropySource for PageFaultTimingSource {
28    fn info(&self) -> &SourceInfo {
29        &PAGE_FAULT_TIMING_INFO
30    }
31
32    fn is_available(&self) -> bool {
33        cfg!(unix)
34    }
35
36    fn collect(&self, n_samples: usize) -> Vec<u8> {
37        // SAFETY: sysconf(_SC_PAGESIZE) is always safe and returns the page size.
38        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize };
39        let num_pages: usize = 8;
40        let map_size = page_size * num_pages;
41
42        // 4x oversampling; each cycle produces `num_pages` timings.
43        let num_cycles = (n_samples * 4 / num_pages) + 4;
44
45        let mut timings = Vec::with_capacity(num_cycles * num_pages);
46
47        for _ in 0..num_cycles {
48            // SAFETY: mmap with MAP_ANONYMOUS|MAP_PRIVATE creates a private anonymous
49            // mapping. We check for MAP_FAILED before using the returned address.
50            let addr = unsafe {
51                libc::mmap(
52                    std::ptr::null_mut(),
53                    map_size,
54                    libc::PROT_READ | libc::PROT_WRITE,
55                    libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
56                    -1,
57                    0,
58                )
59            };
60
61            if addr == libc::MAP_FAILED {
62                continue;
63            }
64
65            // Touch each page to trigger a minor fault and time it with
66            // high-resolution mach_time instead of Instant.
67            for p in 0..num_pages {
68                // SAFETY: addr points to a valid mmap region of map_size bytes.
69                // p * page_size < map_size since p < num_pages and map_size = num_pages * page_size.
70                let page_ptr = unsafe { (addr as *mut u8).add(p * page_size) };
71
72                let t0 = mach_time();
73                // SAFETY: page_ptr points within a valid mmap'd region. We write then
74                // read to trigger a page fault and install a TLB entry.
75                unsafe {
76                    std::ptr::write_volatile(page_ptr, 0xAA);
77                    let _v = std::ptr::read_volatile(page_ptr);
78                }
79                let t1 = mach_time();
80
81                timings.push(t1.wrapping_sub(t0));
82            }
83
84            // SAFETY: addr was returned by mmap (checked != MAP_FAILED) with size map_size.
85            unsafe {
86                libc::munmap(addr, map_size);
87            }
88        }
89
90        extract_timing_entropy(&timings, n_samples)
91    }
92}
93
94// ---------------------------------------------------------------------------
95// Tests
96// ---------------------------------------------------------------------------
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    #[ignore] // Run with: cargo test -- --ignored
104    fn page_fault_timing_collects_bytes() {
105        let src = PageFaultTimingSource;
106        assert!(src.is_available());
107        let data = src.collect(64);
108        assert!(!data.is_empty());
109        assert!(data.len() <= 64);
110    }
111
112    #[test]
113    fn source_info_category() {
114        assert_eq!(
115            PageFaultTimingSource.info().category,
116            SourceCategory::Timing
117        );
118    }
119
120    #[test]
121    fn source_info_name() {
122        assert_eq!(PageFaultTimingSource.name(), "page_fault_timing");
123    }
124}