Skip to main content

openentropy_core/sources/microarch/
prefetcher_state.rs

1//! Hardware prefetcher state timing entropy.
2//!
3//! Modern CPUs have hardware prefetchers that learn memory access patterns
4//! and speculatively fetch data into cache before it's requested. The M4's
5//! L1 and L2 prefetchers track stride patterns and sequential access streams.
6//!
7//! ## Physics
8//!
9//! When we access memory with a consistent stride (e.g., +64 bytes per access),
10//! the prefetcher learns the pattern and begins prefetching ahead. Subsequent
11//! accesses hit in L1 cache (fast). When we scramble the access pattern with
12//! random strides, the prefetcher's learned state is invalidated, causing
13//! cache misses (slow).
14//!
15//! Empirically on M4 Mac mini (N=500, 100 accesses per sample):
16//! - **Learned-stride access**: mean=302.1 ticks, CV=24.1%, range=[250,792]
17//! - **Random-stride access**: mean=679.9 ticks, CV=23.1%, range=[583,1667]
18//! - **Speedup ratio**: 2.25×
19//!
20//! ## Why This Is Entropy
21//!
22//! The prefetcher state encodes:
23//!
24//! 1. **Recent access history** — what strides has this core seen recently?
25//! 2. **Prefetch buffer occupancy** — how many prefetches are in flight?
26//! 3. **Training confidence** — how strongly has the prefetcher locked onto a pattern?
27//! 4. **Contention from other processes** — other threads' access patterns
28//!    interfere with our training
29//!
30//! The 2.25× timing difference between learned and random access provides
31//! a high-SNR measurement of the prefetcher's internal state, which is
32//! influenced by cross-process memory access patterns.
33
34use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
35
36#[cfg(target_os = "macos")]
37use crate::sources::helpers::extract_timing_entropy;
38
39static PREFETCHER_STATE_INFO: SourceInfo = SourceInfo {
40    name: "prefetcher_state",
41    description: "Hardware prefetcher stride-learning state — 2.25× learned vs random speedup",
42    physics: "Trains the L1/L2 hardware prefetcher with consistent stride accesses, then \
43              measures learned-stride vs random-stride timing. Learned: mean=302.1 ticks, \
44              CV=24.1%. Random: mean=679.9 ticks, CV=23.1%. Speedup=2.25×. The prefetcher \
45              state encodes: recent access history, prefetch buffer occupancy, training \
46              confidence, and cross-process memory contention. Other threads' access \
47              patterns interfere with our training, creating a cross-process covert channel.",
48    category: SourceCategory::Microarch,
49    platform: Platform::MacOS,
50    requirements: &[],
51    entropy_rate_estimate: 2.0,
52    composite: false,
53    is_fast: false,
54};
55
56/// Entropy source from hardware prefetcher state timing.
57pub struct PrefetcherStateSource;
58
59#[cfg(target_os = "macos")]
60impl EntropySource for PrefetcherStateSource {
61    fn info(&self) -> &SourceInfo {
62        &PREFETCHER_STATE_INFO
63    }
64
65    fn is_available(&self) -> bool {
66        true
67    }
68
69    fn collect(&self, n_samples: usize) -> Vec<u8> {
70        use std::ptr;
71
72        const STRIDE: usize = 64;
73        const N_ACCESSES: usize = 500;
74        const N_TRAIN: usize = 1000;
75
76        // 8 MB buffer exceeds L2 cache.
77        // Use Vec for RAII — automatically freed on drop, including panic unwind.
78        let buf_size = 8 * 1024 * 1024 + 4096;
79        let mut buf_vec: Vec<u8> = vec![0u8; buf_size];
80        let buf = buf_vec.as_mut_ptr();
81
82        // Touch all pages
83        for i in (0..buf_size).step_by(4096) {
84            unsafe { ptr::write_volatile(buf.add(i), i as u8) };
85        }
86
87        let raw = n_samples * 2 + 32;
88        let mut timings = Vec::with_capacity(raw * 2);
89
90        for s in 0..raw {
91            // Phase 1: Train prefetcher with consistent stride
92            for i in 0..N_TRAIN {
93                let offset = (i * STRIDE) % (buf_size - STRIDE);
94                unsafe { ptr::read_volatile(buf.add(offset)) };
95            }
96
97            // Measure learned-stride access (fast if prefetcher learned)
98            let t0 = super::super::helpers::mach_time();
99            for i in 0..N_ACCESSES {
100                let offset = (i * STRIDE) % (buf_size - STRIDE);
101                unsafe { ptr::read_volatile(buf.add(offset)) };
102            }
103            let learned_t = super::super::helpers::mach_time().wrapping_sub(t0);
104
105            // Phase 2: Scramble prefetcher with random strides
106            for i in 0..N_TRAIN {
107                let offset = ((i * 7919) % (buf_size / STRIDE)) * STRIDE;
108                unsafe { ptr::read_volatile(buf.add(offset)) };
109            }
110
111            // Measure random-stride access (slow if prefetcher confused)
112            let t1 = super::super::helpers::mach_time();
113            for i in 0..N_ACCESSES {
114                let offset = ((i * 7919) % (buf_size / STRIDE)) * STRIDE;
115                unsafe { ptr::read_volatile(buf.add(offset)) };
116            }
117            let random_t = super::super::helpers::mach_time().wrapping_sub(t1);
118
119            // Encode both measurements
120            timings.push(learned_t);
121            timings.push(random_t);
122
123            // Prevent compiler from optimizing away
124            unsafe { ptr::read_volatile(buf.add(s % buf_size)) };
125        }
126
127        drop(buf_vec);
128
129        // XOR learned and random timings to capture prefetcher state
130        let combined: Vec<u64> = timings
131            .chunks(2)
132            .map(|c| c[0] ^ c[1].wrapping_shl(5))
133            .collect();
134
135        extract_timing_entropy(&combined, n_samples)
136    }
137}
138
139#[cfg(not(target_os = "macos"))]
140impl EntropySource for PrefetcherStateSource {
141    fn info(&self) -> &SourceInfo {
142        &PREFETCHER_STATE_INFO
143    }
144    fn is_available(&self) -> bool {
145        false
146    }
147    fn collect(&self, _: usize) -> Vec<u8> {
148        Vec::new()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn info() {
158        let src = PrefetcherStateSource;
159        assert_eq!(src.info().name, "prefetcher_state");
160        assert!(matches!(src.info().category, SourceCategory::Microarch));
161        assert_eq!(src.info().platform, Platform::MacOS);
162    }
163
164    #[test]
165    #[cfg(target_os = "macos")]
166    fn is_available() {
167        assert!(PrefetcherStateSource.is_available());
168    }
169
170    #[test]
171    #[ignore]
172    fn collects_prefetcher_state() {
173        let data = PrefetcherStateSource.collect(32);
174        assert!(!data.is_empty());
175    }
176}