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, Platform, 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::Microarch,
108    platform: Platform::Any,
109    requirements: &[],
110    entropy_rate_estimate: 2000.0,
111    composite: false,
112};
113
114impl EntropySource for TLBShootdownSource {
115    fn info(&self) -> &SourceInfo {
116        &TLB_SHOOTDOWN_INFO
117    }
118
119    fn is_available(&self) -> bool {
120        cfg!(unix)
121    }
122
123    fn collect(&self, n_samples: usize) -> Vec<u8> {
124        // SAFETY: sysconf(_SC_PAGESIZE) is always safe and returns the page size.
125        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize };
126        let region_pages = self.config.region_pages.max(8);
127        let region_size = page_size * region_pages;
128        let (min_pages, max_pages) = self.config.page_count_range;
129        let min_pages = min_pages.max(1).min(region_pages);
130        let max_pages = max_pages.max(min_pages).min(region_pages);
131
132        // SAFETY: mmap with MAP_ANONYMOUS|MAP_PRIVATE creates a private anonymous
133        // mapping. We check for MAP_FAILED before using the returned address.
134        let addr = unsafe {
135            libc::mmap(
136                std::ptr::null_mut(),
137                region_size,
138                libc::PROT_READ | libc::PROT_WRITE,
139                libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
140                -1,
141                0,
142            )
143        };
144
145        if addr == libc::MAP_FAILED {
146            return Vec::new();
147        }
148
149        // Touch every page to establish TLB entries on this core.
150        for p in 0..region_pages {
151            // SAFETY: addr is valid mmap'd region, p * page_size < region_size.
152            unsafe {
153                std::ptr::write_volatile((addr as *mut u8).add(p * page_size), 0xAA);
154            }
155        }
156
157        let raw_count = n_samples * 4 + 64;
158        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
159        let mut lcg: u64 = mach_time() | 1;
160
161        for _ in 0..raw_count {
162            // Vary number of pages to invalidate.
163            lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
164            let num_pages = if min_pages == max_pages {
165                min_pages
166            } else {
167                min_pages + ((lcg >> 32) as usize % (max_pages - min_pages + 1))
168            };
169            let prot_size = num_pages * page_size;
170
171            // Vary the region offset to use different memory each time.
172            let max_offset_pages = region_pages.saturating_sub(num_pages);
173            lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
174            let offset_pages = if max_offset_pages > 0 {
175                (lcg >> 48) as usize % max_offset_pages
176            } else {
177                0
178            };
179            let offset = offset_pages * page_size;
180
181            let t0 = mach_time();
182
183            // SAFETY: addr+offset is within the mmap'd region, prot_size fits.
184            unsafe {
185                let target = (addr as *mut u8).add(offset) as *mut libc::c_void;
186                libc::mprotect(target, prot_size, libc::PROT_READ);
187                libc::mprotect(target, prot_size, libc::PROT_READ | libc::PROT_WRITE);
188            }
189
190            let t1 = mach_time();
191            timings.push(t1.wrapping_sub(t0));
192        }
193
194        // SAFETY: addr was returned by mmap (checked != MAP_FAILED) with size region_size.
195        unsafe {
196            libc::munmap(addr, region_size);
197        }
198
199        if self.config.measure_variance {
200            extract_timing_entropy_variance(&timings, n_samples)
201        } else {
202            extract_timing_entropy(&timings, n_samples)
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn info() {
213        let src = TLBShootdownSource::default();
214        assert_eq!(src.name(), "tlb_shootdown");
215        assert_eq!(src.info().category, SourceCategory::Microarch);
216        assert!(!src.info().composite);
217    }
218
219    #[test]
220    fn default_config() {
221        let config = TLBShootdownConfig::default();
222        assert_eq!(config.page_count_range, (8, 128));
223        assert_eq!(config.region_pages, 256);
224        assert!(config.measure_variance);
225    }
226
227    #[test]
228    fn custom_config() {
229        let src = TLBShootdownSource {
230            config: TLBShootdownConfig {
231                page_count_range: (4, 64),
232                region_pages: 128,
233                measure_variance: false,
234            },
235        };
236        assert_eq!(src.config.page_count_range, (4, 64));
237    }
238
239    #[test]
240    #[ignore] // Uses mmap/mprotect
241    fn collects_bytes() {
242        let src = TLBShootdownSource::default();
243        if src.is_available() {
244            let data = src.collect(64);
245            assert!(!data.is_empty());
246            assert!(data.len() <= 64);
247        }
248    }
249
250    #[test]
251    #[ignore] // Uses mmap/mprotect
252    fn absolute_mode() {
253        let src = TLBShootdownSource {
254            config: TLBShootdownConfig {
255                measure_variance: false,
256                ..TLBShootdownConfig::default()
257            },
258        };
259        if src.is_available() {
260            assert!(!src.collect(64).is_empty());
261        }
262    }
263}