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}