openentropy_core/sources/
novel.rs1use std::process::Command;
5use std::ptr;
6use std::sync::mpsc;
7use std::thread;
8use std::time::{Duration, Instant};
9
10use crate::source::{EntropySource, SourceCategory, SourceInfo};
11
12use super::helpers::extract_timing_entropy;
13
14static DISPATCH_QUEUE_INFO: SourceInfo = SourceInfo {
19 name: "dispatch_queue",
20 description: "Thread scheduling latency jitter from concurrent dispatch queue operations",
21 physics: "Submits blocks to GCD (Grand Central Dispatch) queues and measures scheduling \
22 latency. macOS dynamically migrates work between P-cores (performance) and \
23 E-cores (efficiency) based on thermal state and load. The migration decisions, \
24 queue priority inversions, and QoS tier scheduling create non-deterministic \
25 dispatch timing.",
26 category: SourceCategory::Novel,
27 platform_requirements: &[],
28 entropy_rate_estimate: 1500.0,
29 composite: false,
30};
31
32pub struct DispatchQueueSource;
35
36impl EntropySource for DispatchQueueSource {
37 fn info(&self) -> &SourceInfo {
38 &DISPATCH_QUEUE_INFO
39 }
40
41 fn is_available(&self) -> bool {
42 true
43 }
44
45 fn collect(&self, n_samples: usize) -> Vec<u8> {
46 let raw_count = n_samples * 10 + 64;
47 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
48
49 let num_workers = 4;
51 let mut senders: Vec<mpsc::Sender<Instant>> = Vec::with_capacity(num_workers);
52 let (result_tx, result_rx) = mpsc::channel::<u64>();
53
54 for _ in 0..num_workers {
55 let (tx, rx) = mpsc::channel::<Instant>();
56 let rtx = result_tx.clone();
57 senders.push(tx);
58
59 thread::spawn(move || {
60 while let Ok(sent_at) = rx.recv() {
61 let latency_ns = sent_at.elapsed().as_nanos() as u64;
63 if rtx.send(latency_ns).is_err() {
64 break;
65 }
66 }
67 });
68 }
69
70 for i in 0..raw_count {
72 let worker_idx = i % num_workers;
73 let sent_at = Instant::now();
74 if senders[worker_idx].send(sent_at).is_err() {
75 break;
76 }
77 match result_rx.recv() {
78 Ok(latency_ns) => timings.push(latency_ns),
79 Err(_) => break,
80 }
81 }
82
83 drop(senders);
85
86 extract_timing_entropy(&timings, n_samples)
87 }
88}
89
90#[cfg(target_os = "macos")]
96const DYLD_LIBRARIES: &[&str] = &[
97 "libz.dylib",
98 "libc++.dylib",
99 "libobjc.dylib",
100 "libSystem.B.dylib",
101];
102
103#[cfg(target_os = "linux")]
105const DYLD_LIBRARIES: &[&str] = &["libc.so.6", "libm.so.6"];
106
107#[cfg(not(any(target_os = "macos", target_os = "linux")))]
109const DYLD_LIBRARIES: &[&str] = &[];
110
111static DYLD_TIMING_INFO: SourceInfo = SourceInfo {
112 name: "dyld_timing",
113 description: "Dynamic library loading (dlopen/dlsym) timing jitter",
114 physics: "Times dynamic library loading (dlopen/dlsym) which requires: searching the \
115 dyld shared cache, resolving symbol tables, rebasing pointers, and running \
116 initializers. The timing varies with: shared cache page residency (depends on \
117 what other apps have loaded), ASLR randomization, and filesystem metadata \
118 cache state.",
119 category: SourceCategory::Novel,
120 platform_requirements: &[],
121 entropy_rate_estimate: 1200.0,
122 composite: false,
123};
124
125pub struct DyldTimingSource;
127
128impl EntropySource for DyldTimingSource {
129 fn info(&self) -> &SourceInfo {
130 &DYLD_TIMING_INFO
131 }
132
133 fn is_available(&self) -> bool {
134 !DYLD_LIBRARIES.is_empty()
135 }
136
137 fn collect(&self, n_samples: usize) -> Vec<u8> {
138 if DYLD_LIBRARIES.is_empty() {
139 return Vec::new();
140 }
141
142 let raw_count = n_samples * 10 + 64;
143 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
144 let lib_count = DYLD_LIBRARIES.len();
145
146 for i in 0..raw_count {
147 let lib_name = DYLD_LIBRARIES[i % lib_count];
148
149 let t0 = Instant::now();
151
152 let result = unsafe { libloading::Library::new(lib_name) };
154 if let Ok(lib) = result {
155 std::hint::black_box(&lib);
157 drop(lib);
158 }
159
160 let elapsed_ns = t0.elapsed().as_nanos() as u64;
161 timings.push(elapsed_ns);
162 }
163
164 extract_timing_entropy(&timings, n_samples)
165 }
166}
167
168const PAGE_SIZE: usize = 4096;
174
175static VM_PAGE_TIMING_INFO: SourceInfo = SourceInfo {
176 name: "vm_page_timing",
177 description: "Mach VM page fault timing jitter from mmap/munmap cycles",
178 physics: "Times Mach VM operations (mmap/munmap cycles). Each operation requires: \
179 VM map entry allocation, page table updates, TLB shootdown across cores \
180 (IPI interrupt), and physical page management. The timing depends on: \
181 VM map fragmentation, physical memory pressure, and cross-core \
182 synchronization latency.",
183 category: SourceCategory::Novel,
184 platform_requirements: &[],
185 entropy_rate_estimate: 1300.0,
186 composite: false,
187};
188
189pub struct VMPageTimingSource;
191
192impl EntropySource for VMPageTimingSource {
193 fn info(&self) -> &SourceInfo {
194 &VM_PAGE_TIMING_INFO
195 }
196
197 fn is_available(&self) -> bool {
198 cfg!(unix)
199 }
200
201 fn collect(&self, n_samples: usize) -> Vec<u8> {
202 let raw_count = n_samples * 10 + 64;
203 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
204
205 for _ in 0..raw_count {
206 let t0 = Instant::now();
207
208 let addr = unsafe {
211 libc::mmap(
212 ptr::null_mut(),
213 PAGE_SIZE,
214 libc::PROT_READ | libc::PROT_WRITE,
215 libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
216 -1,
217 0,
218 )
219 };
220
221 if addr == libc::MAP_FAILED {
222 continue;
223 }
224
225 unsafe {
228 ptr::write_volatile(addr as *mut u8, 0xBE);
229 ptr::write_volatile((addr as *mut u8).add(PAGE_SIZE - 1), 0xEF);
230
231 let _v = ptr::read_volatile(addr as *const u8);
233 }
234
235 unsafe {
237 libc::munmap(addr, PAGE_SIZE);
238 }
239
240 let elapsed_ns = t0.elapsed().as_nanos() as u64;
241 timings.push(elapsed_ns);
242 }
243
244 extract_timing_entropy(&timings, n_samples)
245 }
246}
247
248const SPOTLIGHT_FILES: &[&str] = &[
254 "/usr/bin/true",
255 "/usr/bin/false",
256 "/usr/bin/env",
257 "/usr/bin/which",
258];
259
260const MDLS_PATH: &str = "/usr/bin/mdls";
262
263const MDLS_TIMEOUT: Duration = Duration::from_secs(2);
265
266static SPOTLIGHT_TIMING_INFO: SourceInfo = SourceInfo {
267 name: "spotlight_timing",
268 description: "Spotlight metadata index query timing jitter via mdls",
269 physics: "Queries Spotlight\u{2019}s metadata index (mdls) and measures response time. \
270 The index is a complex B-tree/inverted index structure. Query timing depends \
271 on: index size, disk cache residency, concurrent indexing activity, and \
272 filesystem metadata state. When Spotlight is actively indexing new files, \
273 query latency becomes highly variable.",
274 category: SourceCategory::Novel,
275 platform_requirements: &["macos"],
276 entropy_rate_estimate: 800.0,
277 composite: false,
278};
279
280pub struct SpotlightTimingSource;
282
283impl EntropySource for SpotlightTimingSource {
284 fn info(&self) -> &SourceInfo {
285 &SPOTLIGHT_TIMING_INFO
286 }
287
288 fn is_available(&self) -> bool {
289 std::path::Path::new(MDLS_PATH).exists()
290 }
291
292 fn collect(&self, n_samples: usize) -> Vec<u8> {
293 let raw_count = (n_samples * 10 + 64).min(200);
298 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
299 let file_count = SPOTLIGHT_FILES.len();
300
301 for i in 0..raw_count {
302 let file = SPOTLIGHT_FILES[i % file_count];
303
304 let t0 = Instant::now();
307
308 let child = Command::new(MDLS_PATH)
309 .args(["-name", "kMDItemFSName", file])
310 .stdout(std::process::Stdio::null())
311 .stderr(std::process::Stdio::null())
312 .spawn();
313
314 if let Ok(mut child) = child {
315 let deadline = Instant::now() + MDLS_TIMEOUT;
316 loop {
317 match child.try_wait() {
318 Ok(Some(_)) => break,
319 Ok(None) => {
320 if Instant::now() >= deadline {
321 let _ = child.kill();
322 let _ = child.wait();
323 break;
324 }
325 std::thread::sleep(Duration::from_millis(10));
326 }
327 Err(_) => break,
328 }
329 }
330 }
331
332 let elapsed_ns = t0.elapsed().as_nanos() as u64;
334 timings.push(elapsed_ns);
335 }
336
337 extract_timing_entropy(&timings, n_samples)
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::super::helpers::extract_lsbs_u64;
344 use super::*;
345
346 #[test]
347 fn dispatch_queue_info() {
348 let src = DispatchQueueSource;
349 assert_eq!(src.name(), "dispatch_queue");
350 assert_eq!(src.info().category, SourceCategory::Novel);
351 assert!((src.info().entropy_rate_estimate - 1500.0).abs() < f64::EPSILON);
352 }
353
354 #[test]
355 #[ignore] fn dispatch_queue_collects_bytes() {
357 let src = DispatchQueueSource;
358 assert!(src.is_available());
359 let data = src.collect(64);
360 assert!(!data.is_empty());
361 assert!(data.len() <= 64);
362 }
363
364 #[test]
365 fn dyld_timing_info() {
366 let src = DyldTimingSource;
367 assert_eq!(src.name(), "dyld_timing");
368 assert_eq!(src.info().category, SourceCategory::Novel);
369 assert!((src.info().entropy_rate_estimate - 1200.0).abs() < f64::EPSILON);
370 }
371
372 #[test]
373 fn vm_page_timing_info() {
374 let src = VMPageTimingSource;
375 assert_eq!(src.name(), "vm_page_timing");
376 assert_eq!(src.info().category, SourceCategory::Novel);
377 assert!((src.info().entropy_rate_estimate - 1300.0).abs() < f64::EPSILON);
378 }
379
380 #[test]
381 #[cfg(unix)]
382 #[ignore] fn vm_page_timing_collects_bytes() {
384 let src = VMPageTimingSource;
385 assert!(src.is_available());
386 let data = src.collect(64);
387 assert!(!data.is_empty());
388 assert!(data.len() <= 64);
389 }
390
391 #[test]
392 fn spotlight_timing_info() {
393 let src = SpotlightTimingSource;
394 assert_eq!(src.name(), "spotlight_timing");
395 assert_eq!(src.info().category, SourceCategory::Novel);
396 assert!((src.info().entropy_rate_estimate - 800.0).abs() < f64::EPSILON);
397 }
398
399 #[test]
400 #[cfg(target_os = "macos")]
401 #[ignore] fn spotlight_timing_collects_bytes() {
403 let src = SpotlightTimingSource;
404 if src.is_available() {
405 let data = src.collect(32);
406 assert!(!data.is_empty());
407 assert!(data.len() <= 32);
408 }
409 }
410
411 #[test]
412 fn extract_lsbs_packing() {
413 let deltas = vec![1u64, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0];
414 let bytes = extract_lsbs_u64(&deltas);
415 assert_eq!(bytes.len(), 2);
416 assert_eq!(bytes[0], 0xAA);
418 assert_eq!(bytes[1], 0xF0);
420 }
421}