Skip to main content

openentropy_core/sources/frontier/
tlb_shootdown.rs

1//! TLB shootdown timing — entropy from mprotect-induced IPI broadcasts.
2
3use crate::source::{EntropySource, SourceCategory, SourceInfo};
4use crate::sources::helpers::{extract_timing_entropy, mach_time};
5
6use super::extract_timing_entropy_variance;
7
8/// Configuration for TLB shootdown entropy collection.
9///
10/// # Example
11/// ```
12/// # use openentropy_core::sources::frontier::TLBShootdownConfig;
13/// let config = TLBShootdownConfig {
14///     page_count_range: (16, 64),   // fewer pages = fewer IPIs
15///     region_pages: 128,            // smaller region
16///     measure_variance: true,       // delta-of-deltas (recommended)
17/// };
18/// ```
19#[derive(Debug, Clone)]
20pub struct TLBShootdownConfig {
21    /// Range of pages to invalidate per measurement `(min, max)`.
22    ///
23    /// Varying the page count changes the number of Inter-Processor Interrupts
24    /// (IPIs) sent per `mprotect()` call. More pages = more IPIs = longer and
25    /// more variable latency.
26    ///
27    /// Both values are clamped to `[1, region_pages]`.
28    ///
29    /// **Range:** min 1, max = `region_pages`. **Default:** `(8, 128)`
30    pub page_count_range: (usize, usize),
31
32    /// Total memory region size in pages.
33    ///
34    /// Larger regions use different physical pages each measurement, preventing
35    /// TLB prefetch patterns. The region is allocated via `mmap` and touched
36    /// on every page to establish TLB entries before measurement begins.
37    ///
38    /// **Range:** 8+. **Default:** `256` (1 MB with 4KB pages)
39    pub region_pages: usize,
40
41    /// Use delta-of-deltas (variance) extraction (`true`) or standard
42    /// absolute timing extraction (`false`).
43    ///
44    /// Variance mode computes second-order deltas between consecutive
45    /// shootdowns, removing systematic bias and amplifying the nondeterministic
46    /// component. Produces higher min-entropy at the cost of ~2x raw samples.
47    ///
48    /// **Default:** `true`
49    pub measure_variance: bool,
50}
51
52impl Default for TLBShootdownConfig {
53    fn default() -> Self {
54        Self {
55            page_count_range: (8, 128),
56            region_pages: 256,
57            measure_variance: true,
58        }
59    }
60}
61
62/// Harvests timing jitter from TLB invalidation broadcasts via `mprotect()`.
63///
64/// # What it measures
65/// Nanosecond timing of `mprotect()` permission toggles (read-write → read-only
66/// → read-write) on varying numbers of pages within a pre-allocated memory region.
67///
68/// # Why it's entropic
69/// When `mprotect()` changes page protection on a multi-core system, the kernel
70/// must invalidate stale TLB entries on ALL cores:
71/// - **Inter-Processor Interrupt (IPI)** — the kernel sends an IPI to every
72///   core that might have cached TLB entries for the affected pages
73/// - **TLB flush latency** — each receiving core must drain its pipeline,
74///   flush matching TLB entries, and acknowledge
75/// - **Cross-cluster latency** — Apple Silicon has separate P-core and E-core
76///   clusters with different interconnect latencies
77/// - **Concurrent IPI traffic** — other processes' `mprotect()`/`munmap()` calls
78///   create IPI storms that interfere with our measurements
79///
80/// # What makes it unique
81/// TLB shootdowns are a microarchitectural side-channel that has been studied
82/// for attacks but never harvested as an entropy source. The IPI broadcast
83/// mechanism creates system-wide nondeterminism that depends on the state of
84/// EVERY core simultaneously.
85///
86/// # Configuration
87/// See [`TLBShootdownConfig`] for tunable parameters. Key options:
88/// - `measure_variance`: delta-of-deltas extraction (recommended: `true`)
89/// - `page_count_range`: controls IPI storm intensity
90/// - `region_pages`: controls physical page diversity
91#[derive(Default)]
92pub struct TLBShootdownSource {
93    /// Source configuration. Use `Default::default()` for recommended settings.
94    pub config: TLBShootdownConfig,
95}
96
97static TLB_SHOOTDOWN_INFO: SourceInfo = SourceInfo {
98    name: "tlb_shootdown",
99    description: "TLB invalidation broadcast timing via variable-count mprotect IPI storms",
100    physics: "Toggles page protection via mprotect() on varying page counts to trigger TLB \
101              shootdown broadcasts. Each mprotect() sends IPIs to ALL cores to flush stale \
102              TLB entries. Varying page counts creates different IPI patterns. Different \
103              memory regions each time prevent TLB prefetch. Variance between consecutive \
104              shootdowns captures relative timing with higher min-entropy. IPI latency depends \
105              on: what each core is executing, P-core vs E-core cluster latency, core power \
106              states, and concurrent IPI traffic.",
107    category: SourceCategory::Frontier,
108    platform_requirements: &[],
109    entropy_rate_estimate: 2000.0,
110    composite: false,
111};
112
113impl EntropySource for TLBShootdownSource {
114    fn info(&self) -> &SourceInfo {
115        &TLB_SHOOTDOWN_INFO
116    }
117
118    fn is_available(&self) -> bool {
119        cfg!(unix)
120    }
121
122    fn collect(&self, n_samples: usize) -> Vec<u8> {
123        // SAFETY: sysconf(_SC_PAGESIZE) is always safe and returns the page size.
124        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize };
125        let region_pages = self.config.region_pages.max(8);
126        let region_size = page_size * region_pages;
127        let (min_pages, max_pages) = self.config.page_count_range;
128        let min_pages = min_pages.max(1).min(region_pages);
129        let max_pages = max_pages.max(min_pages).min(region_pages);
130
131        // SAFETY: mmap with MAP_ANONYMOUS|MAP_PRIVATE creates a private anonymous
132        // mapping. We check for MAP_FAILED before using the returned address.
133        let addr = unsafe {
134            libc::mmap(
135                std::ptr::null_mut(),
136                region_size,
137                libc::PROT_READ | libc::PROT_WRITE,
138                libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
139                -1,
140                0,
141            )
142        };
143
144        if addr == libc::MAP_FAILED {
145            return Vec::new();
146        }
147
148        // Touch every page to establish TLB entries on this core.
149        for p in 0..region_pages {
150            // SAFETY: addr is valid mmap'd region, p * page_size < region_size.
151            unsafe {
152                std::ptr::write_volatile((addr as *mut u8).add(p * page_size), 0xAA);
153            }
154        }
155
156        let raw_count = n_samples * 4 + 64;
157        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
158        let mut lcg: u64 = mach_time() | 1;
159
160        for _ in 0..raw_count {
161            // Vary number of pages to invalidate.
162            lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
163            let num_pages = if min_pages == max_pages {
164                min_pages
165            } else {
166                min_pages + ((lcg >> 32) as usize % (max_pages - min_pages + 1))
167            };
168            let prot_size = num_pages * page_size;
169
170            // Vary the region offset to use different memory each time.
171            let max_offset_pages = region_pages.saturating_sub(num_pages);
172            lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
173            let offset_pages = if max_offset_pages > 0 {
174                (lcg >> 48) as usize % max_offset_pages
175            } else {
176                0
177            };
178            let offset = offset_pages * page_size;
179
180            let t0 = mach_time();
181
182            // SAFETY: addr+offset is within the mmap'd region, prot_size fits.
183            unsafe {
184                let target = (addr as *mut u8).add(offset) as *mut libc::c_void;
185                libc::mprotect(target, prot_size, libc::PROT_READ);
186                libc::mprotect(target, prot_size, libc::PROT_READ | libc::PROT_WRITE);
187            }
188
189            let t1 = mach_time();
190            timings.push(t1.wrapping_sub(t0));
191        }
192
193        // SAFETY: addr was returned by mmap (checked != MAP_FAILED) with size region_size.
194        unsafe {
195            libc::munmap(addr, region_size);
196        }
197
198        if self.config.measure_variance {
199            extract_timing_entropy_variance(&timings, n_samples)
200        } else {
201            extract_timing_entropy(&timings, n_samples)
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn info() {
212        let src = TLBShootdownSource::default();
213        assert_eq!(src.name(), "tlb_shootdown");
214        assert_eq!(src.info().category, SourceCategory::Frontier);
215        assert!(!src.info().composite);
216    }
217
218    #[test]
219    fn default_config() {
220        let config = TLBShootdownConfig::default();
221        assert_eq!(config.page_count_range, (8, 128));
222        assert_eq!(config.region_pages, 256);
223        assert!(config.measure_variance);
224    }
225
226    #[test]
227    fn custom_config() {
228        let src = TLBShootdownSource {
229            config: TLBShootdownConfig {
230                page_count_range: (4, 64),
231                region_pages: 128,
232                measure_variance: false,
233            },
234        };
235        assert_eq!(src.config.page_count_range, (4, 64));
236    }
237
238    #[test]
239    #[ignore] // Uses mmap/mprotect
240    fn collects_bytes() {
241        let src = TLBShootdownSource::default();
242        if src.is_available() {
243            let data = src.collect(64);
244            assert!(!data.is_empty());
245            assert!(data.len() <= 64);
246        }
247    }
248
249    #[test]
250    #[ignore] // Uses mmap/mprotect
251    fn absolute_mode() {
252        let src = TLBShootdownSource {
253            config: TLBShootdownConfig {
254                measure_variance: false,
255                ..TLBShootdownConfig::default()
256            },
257        };
258        if src.is_available() {
259            assert!(!src.collect(64).is_empty());
260        }
261    }
262}