openentropy_core/sources/system/proc_info_timing.rs
1//! proc_info / proc_pid_rusage system call timing entropy.
2//!
3//! The `proc_pidinfo()` and `proc_pid_rusage()` system calls query the kernel's
4//! process information subsystem. Each call must acquire the kernel's BSD process
5//! lock (`proc_lock`), walk the process table, and collect the requested data.
6//!
7//! ## Physics
8//!
9//! The timing of these system calls varies based on:
10//!
11//! 1. **`proc_lock` contention**: Any concurrent fork/exec/exit/wait4 operation
12//! holds `proc_lock` exclusively. Our call must wait for the lock to be
13//! released, creating variable delay proportional to concurrent process
14//! lifecycle activity.
15//!
16//! 2. **CPU affinity and scheduler state**: The kernel thread handling our
17//! system call may be preempted or migrated between when we enter the kernel
18//! and when we return, adding scheduler jitter.
19//!
20//! 3. **Page fault cost for result struct**: If the kernel's result buffer or
21//! the process's task struct is not in L2/L3 cache (e.g., after a long idle
22//! period), the kernel must page-fault the data in.
23//!
24//! 4. **Hardware performance counter collection (rusage only)**: `proc_pid_rusage`
25//! with RUSAGE_INFO_V4 collects CPU cycle counts and memory bandwidth stats,
26//! requiring a cross-core hardware counter read that adds variable latency.
27//!
28//! Empirically on M4 Mac mini (N=1000):
29//! - `proc_pidinfo(TBSDINFO)`: mean=478.8 ticks, CV=47.7%, range=[434,7667]
30//! - `proc_pid_rusage(V4)`: mean=726.3 ticks, CV=43.0%, range=[666,10583]
31//! - Both have LSB≈0.24–0.29 (near-uniform, unlike the "always even" cluster)
32//!
33//! ## Cross-Process Sensitivity
34//!
35//! This is a genuine cross-process covert channel: any process on the system
36//! that creates/destroys processes, forks, or executes programs increases
37//! `proc_lock` contention and extends our call duration. Terminal commands,
38//! build systems, shell scripts, and browser tab management all leak into
39//! our timing distribution.
40//!
41//! ## Why LSB≈0.24 (Near-Uniform)
42//!
43//! Unlike instruction-timing sources (LSB=0.015–0.026, always even), proc_info
44//! timing is dominated by kernel lock scheduling — a higher-level stochastic
45//! process with no microarchitectural quantization. The LSB distribution is
46//! close to uniform (0.5), indicating the kernel path length has genuine
47//! bit-level randomness.
48
49use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
50
51#[cfg(target_os = "macos")]
52use crate::sources::helpers::{extract_timing_entropy, mach_time};
53
54static PROC_INFO_TIMING_INFO: SourceInfo = SourceInfo {
55 name: "proc_info_timing",
56 description: "proc_pidinfo / proc_pid_rusage syscall — kernel proc_lock contention timing",
57 physics: "Times proc_pidinfo(TBSDINFO) and proc_pid_rusage(V4) syscalls. Each acquires \
58 the BSD kernel proc_lock, walks the process table, and optionally reads hardware \
59 perf counters. Lock contention from concurrent fork/exec/exit operations creates \
60 variable delay. LSB=0.24\u{2013}0.29 (near-uniform, unlike always-even instruction \
61 timing) — kernel scheduling dominates, not microarch quantization. CV=43\u{2013}48%, \
62 range=[434,10583]. Cross-process sensitivity: any process lifecycle activity \
63 (terminal commands, builds, browser tabs) leaks into our timing distribution \
64 through proc_lock contention. Genuine covert channel.",
65 category: SourceCategory::System,
66 platform: Platform::MacOS,
67 requirements: &[],
68 entropy_rate_estimate: 1.5,
69 composite: false,
70 is_fast: false,
71};
72
73/// Entropy source from proc_info/proc_pid_rusage system call timing.
74pub struct ProcInfoTimingSource;
75
76#[cfg(target_os = "macos")]
77unsafe extern "C" {
78 // proc_pidinfo returns the number of bytes written, or -1 on error.
79 fn proc_pidinfo(
80 pid: i32,
81 flavor: i32,
82 arg: u64,
83 buffer: *mut core::ffi::c_void,
84 buffersize: i32,
85 ) -> i32;
86
87 // proc_pid_rusage fills in a rusage_info struct.
88 // rusage_info_t is typedef void*, so the C sig is (int, int, rusage_info_t*) = (int, int, void**).
89 // In practice the kernel writes directly into the pointed-to struct.
90 fn proc_pid_rusage(pid: i32, flavor: i32, buffer: *mut core::ffi::c_void) -> i32;
91
92 fn getpid() -> i32;
93}
94
95/// PROC_PIDTBSDINFO flavor — basic BSD process info.
96#[cfg(target_os = "macos")]
97const PROC_PIDTBSDINFO: i32 = 3;
98
99/// RUSAGE_INFO_V4 — includes CPU cycles and memory bandwidth.
100#[cfg(target_os = "macos")]
101const RUSAGE_INFO_V4: i32 = 4;
102
103#[cfg(target_os = "macos")]
104#[repr(C, align(8))]
105struct ProcBSDInfo {
106 _pad: [u8; 512], // large enough for proc_bsdinfo
107}
108
109#[cfg(target_os = "macos")]
110#[repr(C, align(8))]
111struct RusageInfoV4 {
112 _pad: [u8; 320], // rusage_info_v4 is 296 bytes; pad to 320 for safety
113}
114
115#[cfg(target_os = "macos")]
116impl EntropySource for ProcInfoTimingSource {
117 fn info(&self) -> &SourceInfo {
118 &PROC_INFO_TIMING_INFO
119 }
120
121 fn is_available(&self) -> bool {
122 true
123 }
124
125 fn collect(&self, n_samples: usize) -> Vec<u8> {
126 let raw = n_samples * 2 + 32;
127 let mut timings = Vec::with_capacity(raw * 2);
128
129 let pid = unsafe { getpid() };
130 let mut bsd_info = ProcBSDInfo { _pad: [0u8; 512] };
131 let mut ru_info = RusageInfoV4 { _pad: [0u8; 320] };
132
133 // Warm up — first call has extra kernel setup cost
134 for _ in 0..4 {
135 unsafe {
136 proc_pidinfo(
137 pid,
138 PROC_PIDTBSDINFO,
139 0,
140 bsd_info._pad.as_mut_ptr() as *mut core::ffi::c_void,
141 bsd_info._pad.len() as i32,
142 );
143 }
144 }
145
146 for _ in 0..raw {
147 // proc_pidinfo: process table + BSD info lock
148 let t0 = mach_time();
149 unsafe {
150 proc_pidinfo(
151 pid,
152 PROC_PIDTBSDINFO,
153 0,
154 bsd_info._pad.as_mut_ptr() as *mut core::ffi::c_void,
155 bsd_info._pad.len() as i32,
156 );
157 }
158 let t_pid = mach_time().wrapping_sub(t0);
159
160 // proc_pid_rusage V4: performance counter cross-core read
161 let t1 = mach_time();
162 unsafe {
163 proc_pid_rusage(
164 pid,
165 RUSAGE_INFO_V4,
166 ru_info._pad.as_mut_ptr() as *mut core::ffi::c_void,
167 );
168 }
169 let t_ru = mach_time().wrapping_sub(t1);
170
171 // Cap at 5ms (abnormal; suspend/resume artefact)
172 if t_pid < 120_000 {
173 timings.push(t_pid);
174 }
175 if t_ru < 120_000 {
176 timings.push(t_ru);
177 }
178 }
179
180 extract_timing_entropy(&timings, n_samples)
181 }
182}
183
184#[cfg(not(target_os = "macos"))]
185impl EntropySource for ProcInfoTimingSource {
186 fn info(&self) -> &SourceInfo {
187 &PROC_INFO_TIMING_INFO
188 }
189 fn is_available(&self) -> bool {
190 false
191 }
192 fn collect(&self, _: usize) -> Vec<u8> {
193 Vec::new()
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn info() {
203 let src = ProcInfoTimingSource;
204 assert_eq!(src.info().name, "proc_info_timing");
205 assert!(matches!(src.info().category, SourceCategory::System));
206 assert_eq!(src.info().platform, Platform::MacOS);
207 assert!(!src.info().composite);
208 }
209
210 #[test]
211 #[cfg(target_os = "macos")]
212 fn is_available_on_macos() {
213 assert!(ProcInfoTimingSource.is_available());
214 }
215
216 #[test]
217 #[ignore]
218 fn collects_lock_contention_timing() {
219 let data = ProcInfoTimingSource.collect(32);
220 assert!(!data.is_empty());
221 }
222}