Skip to main content

openentropy_core/sources/scheduling/
timer_coalescing.rs

1//! OS timer coalescing wakeup jitter — entropy from the system-wide timer queue.
2//!
3//! Modern operating systems batch timer wakeups to reduce power consumption.
4//! When a thread calls `nanosleep(1ns)`, it does not wake up after 1 nanosecond.
5//! It wakes up when the *next coalesced timer fires*, which depends on the
6//! pending timer queue across **all processes on the system**.
7//!
8//! ## Physics
9//!
10//! macOS uses "timer coalescing" (introduced in 10.9) to align timer wakeups
11//! within configurable windows. The actual wakeup time after a 1 ns sleep is
12//! determined by:
13//!
14//! 1. Which other processes have pending timers (every app, daemon, and kernel
15//!    subsystem contributes to the shared timer wheel)
16//! 2. The current coalescence window size (varies with power state and system
17//!    activity level)
18//! 3. The phase of the hardware timer interrupt relative to our wakeup request
19//! 4. Scheduler decisions about which runqueue to place the thread on after wakeup
20//!
21//! Empirically this source produces a bimodal distribution (~3 µs and ~13 µs)
22//! with CV > 70%. The *position within each cluster* (the intra-cluster jitter)
23//! encodes the real physical entropy: the phase relationship between our wakeup
24//! request and the hardware interrupt firing cycle.
25//!
26//! ## Platform notes
27//!
28//! Available on all Unix-like systems with `nanosleep`. On Linux, behavior
29//! depends on `CONFIG_HZ` and the high-resolution timer subsystem. On macOS,
30//! the bimodal distribution reflects the 2-level coalescing structure. The
31//! actual distribution shape is irrelevant — we extract LSB entropy from the
32//! raw tick counts, which captures the sub-cluster jitter regardless of
33//! coalescing policy.
34
35use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
36use crate::sources::helpers::extract_timing_entropy;
37
38static TIMER_COALESCING_INFO: SourceInfo = SourceInfo {
39    name: "timer_coalescing",
40    description: "OS timer coalescing wakeup jitter from system-wide timer queue state",
41    physics: "Calls nanosleep(1ns) and measures the actual wakeup latency. The OS \
42              batches timer wakeups across all processes; actual wakeup time depends on \
43              pending timers from every process, daemon, and kernel subsystem on the \
44              machine. Produces bimodal distribution (~3\u{00b5}s / ~13\u{00b5}s clusters on macOS) \
45              with CV >70%. Intra-cluster jitter encodes the phase of the hardware timer \
46              interrupt relative to our wakeup request — a system-wide aggregate noise \
47              source with no per-process equivalent.",
48    category: SourceCategory::Scheduling,
49    platform: Platform::Any,
50    requirements: &[],
51    entropy_rate_estimate: 2.0,
52    composite: false,
53    is_fast: false,
54};
55
56/// Entropy source that harvests OS timer coalescing wakeup jitter.
57///
58/// Each sample calls `nanosleep(1ns)` and records the actual elapsed time
59/// (hardware ticks). The jitter comes from the shared timer queue across all
60/// running processes on the system.
61pub struct TimerCoalescingSource;
62
63impl EntropySource for TimerCoalescingSource {
64    fn info(&self) -> &SourceInfo {
65        &TIMER_COALESCING_INFO
66    }
67
68    fn is_available(&self) -> bool {
69        // nanosleep is available on all Unix targets.
70        cfg!(unix)
71    }
72
73    fn collect(&self, n_samples: usize) -> Vec<u8> {
74        #[cfg(unix)]
75        {
76            collect_unix(n_samples)
77        }
78        #[cfg(not(unix))]
79        {
80            let _ = n_samples;
81            Vec::new()
82        }
83    }
84}
85
86#[cfg(unix)]
87fn collect_unix(n_samples: usize) -> Vec<u8> {
88    use std::time::Instant;
89
90    // 12× oversampling: each wakeup contributes ~2-3 bits but has structural bias.
91    let raw_count = n_samples * 12 + 64;
92    let mut timings = Vec::with_capacity(raw_count);
93
94    // Warm-up: let the OS scheduler settle our thread into its normal wakeup pattern.
95    let warmup_req = libc_timespec(0, 1);
96    for _ in 0..32 {
97        let mut rem = libc_timespec(0, 0);
98        // SAFETY: pointers are to stack-allocated timespec structs.
99        unsafe { libc::nanosleep(&warmup_req, &mut rem) };
100    }
101
102    for _ in 0..raw_count {
103        let req = libc_timespec(0, 1); // 1 ns request
104        let mut rem = libc_timespec(0, 0);
105
106        let t0 = Instant::now();
107        // SAFETY: req and rem are valid stack-allocated timespec structs.
108        unsafe { libc::nanosleep(&req, &mut rem) };
109        let elapsed_ns = t0.elapsed().as_nanos() as u64;
110
111        // Sanity filter: reject absurd values (>500ms would indicate a suspend/resume).
112        if elapsed_ns < 500_000_000 {
113            timings.push(elapsed_ns);
114        }
115    }
116
117    extract_timing_entropy(&timings, n_samples)
118}
119
120#[cfg(unix)]
121#[inline]
122fn libc_timespec(secs: i64, nsecs: i64) -> libc::timespec {
123    libc::timespec {
124        tv_sec: secs as libc::time_t,
125        tv_nsec: nsecs as libc::c_long,
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn info() {
135        let src = TimerCoalescingSource;
136        assert_eq!(src.info().name, "timer_coalescing");
137        assert!(matches!(src.info().category, SourceCategory::Scheduling));
138        assert_eq!(src.info().platform, Platform::Any);
139        assert!(!src.info().composite);
140    }
141
142    #[test]
143    #[cfg(unix)]
144    fn is_available_on_unix() {
145        assert!(TimerCoalescingSource.is_available());
146    }
147
148    #[test]
149    #[ignore] // Hardware timing — can be slow in constrained environments
150    fn collects_bytes_with_variation() {
151        let src = TimerCoalescingSource;
152        if !src.is_available() {
153            return;
154        }
155        let data = src.collect(32);
156        assert!(!data.is_empty(), "expected non-empty output");
157        let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
158        assert!(
159            unique.len() > 2,
160            "expected byte variation from coalescing jitter"
161        );
162    }
163}