Skip to main content

openentropy_core/sources/frontier/
thread_lifecycle.rs

1//! Thread lifecycle timing — entropy from pthread create/join scheduling.
2
3use std::thread;
4
5use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
6use crate::sources::helpers::{extract_timing_entropy, mach_time};
7
8/// Harvests timing jitter from thread creation and destruction.
9///
10/// # What it measures
11/// Nanosecond timing of the full `pthread_create` + `pthread_join` cycle,
12/// with variable per-thread workloads.
13///
14/// # Why it's entropic
15/// Each thread lifecycle exercises deep kernel scheduling paths:
16/// - **Mach thread port allocation** from the kernel IPC port name space
17/// - **Zone allocator** allocation for the kernel thread structure
18/// - **CPU core selection** — P-core vs E-core, influenced by ALL threads
19/// - **Stack page allocation** via `vm_allocate`
20/// - **TLS setup** including dyld per-thread state
21/// - **Context switch on join** — depends on current runqueue state
22/// - **Core migration** — new thread may run on a different core
23///
24/// # What makes it unique
25/// Thread lifecycle timing is a previously untapped entropy source. The
26/// combination of kernel memory allocation, scheduling decisions, and
27/// cross-core communication produces 89 unique LSB values — the richest
28/// of any frontier source.
29///
30/// # Configuration
31/// No configuration needed — this source has no tunable parameters.
32pub struct ThreadLifecycleSource;
33
34static THREAD_LIFECYCLE_INFO: SourceInfo = SourceInfo {
35    name: "thread_lifecycle",
36    description: "Thread create/join kernel scheduling and allocation jitter",
37    physics: "Creates and immediately joins threads, measuring the full lifecycle timing. \
38              Each cycle involves: Mach thread port allocation, zone allocator allocation, \
39              CPU core selection (P-core vs E-core), stack page allocation, TLS setup, and \
40              context switch on join. The scheduler\u{2019}s core selection depends on thermal \
41              state, load from ALL processes, and QoS priorities.",
42    category: SourceCategory::Scheduling,
43    platform: Platform::Any,
44    requirements: &[],
45    entropy_rate_estimate: 3000.0,
46    composite: false,
47};
48
49impl EntropySource for ThreadLifecycleSource {
50    fn info(&self) -> &SourceInfo {
51        &THREAD_LIFECYCLE_INFO
52    }
53
54    fn is_available(&self) -> bool {
55        true
56    }
57
58    fn collect(&self, n_samples: usize) -> Vec<u8> {
59        let raw_count = n_samples * 4 + 64;
60        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
61        let mut lcg: u64 = mach_time() | 1;
62
63        for _ in 0..raw_count {
64            lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
65            let work_amount = (lcg >> 48) as u32 % 100;
66
67            let t0 = mach_time();
68            let handle = thread::spawn(move || {
69                let mut sink: u64 = 0;
70                for j in 0..work_amount {
71                    sink = sink.wrapping_add(j as u64);
72                }
73                std::hint::black_box(sink);
74            });
75            let _ = handle.join();
76            let t1 = mach_time();
77            timings.push(t1.wrapping_sub(t0));
78        }
79
80        extract_timing_entropy(&timings, n_samples)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn info() {
90        let src = ThreadLifecycleSource;
91        assert_eq!(src.name(), "thread_lifecycle");
92        assert_eq!(src.info().category, SourceCategory::Scheduling);
93        assert!(!src.info().composite);
94    }
95
96    #[test]
97    #[ignore] // Spawns threads
98    fn collects_bytes() {
99        let src = ThreadLifecycleSource;
100        assert!(src.is_available());
101        let data = src.collect(64);
102        assert!(!data.is_empty());
103        assert!(data.len() <= 64);
104        if data.len() > 1 {
105            let first = data[0];
106            assert!(data.iter().any(|&b| b != first), "all bytes identical");
107        }
108    }
109}