Skip to main content

openentropy_core/sources/system/
process.rs

1//! ProcessSource — Snapshots the process table via `ps` and combines it with
2//! getpid() timing jitter for entropy.
3//!
4//! **Raw output characteristics:** Mix of timing LSBs and process table byte
5//! deltas.
6
7use std::time::Instant;
8
9use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
10
11use crate::sources::helpers::{run_command_raw, xor_fold_u64};
12
13/// Number of getpid() calls to measure for timing jitter.
14const JITTER_ROUNDS: usize = 256;
15
16/// Entropy source that snapshots the process table via `ps` and combines it
17/// with `getpid()` timing jitter.
18///
19/// No tunable parameters — the source reads the full process table and
20/// automatically extracts entropy from byte-level changes.
21pub struct ProcessSource;
22
23static PROCESS_INFO: SourceInfo = SourceInfo {
24    name: "process_table",
25    description: "Process table snapshots combined with getpid() timing jitter",
26    physics: "Snapshots the process table (PIDs, CPU usage, memory) and extracts \
27              entropy from the constantly-changing state. New PIDs are allocated \
28              semi-randomly, CPU percentages fluctuate with scheduling decisions, and \
29              resident memory sizes shift with page reclamation.",
30    category: SourceCategory::System,
31    platform: Platform::MacOS,
32    requirements: &[],
33    entropy_rate_estimate: 1.0,
34    composite: false,
35    is_fast: false,
36};
37
38impl ProcessSource {
39    pub fn new() -> Self {
40        Self
41    }
42}
43
44impl Default for ProcessSource {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50/// Collect timing jitter from repeated getpid() syscalls.
51/// Returns raw LSBs of nanosecond timing deltas.
52fn collect_getpid_jitter(n_bytes: usize) -> Vec<u8> {
53    let rounds = JITTER_ROUNDS.max(n_bytes * 2);
54    let mut timings: Vec<u64> = Vec::with_capacity(rounds);
55
56    for _ in 0..rounds {
57        let start = Instant::now();
58        // SAFETY: getpid() is always safe — it's a simple read-only syscall.
59        unsafe {
60            libc::getpid();
61        }
62        let elapsed = start.elapsed().as_nanos() as u64;
63        timings.push(elapsed);
64    }
65
66    // Extract LSBs of timing deltas
67    let mut raw = Vec::with_capacity(n_bytes);
68    for pair in timings.windows(2) {
69        let delta = pair[1].wrapping_sub(pair[0]);
70        raw.push(delta as u8);
71        if raw.len() >= n_bytes {
72            break;
73        }
74    }
75    raw
76}
77
78/// Run `ps -eo pid,pcpu,rss` and return its raw stdout bytes.
79fn snapshot_process_table() -> Option<Vec<u8>> {
80    run_command_raw("/bin/ps", &["-eo", "pid,pcpu,rss"])
81}
82
83impl EntropySource for ProcessSource {
84    fn info(&self) -> &SourceInfo {
85        &PROCESS_INFO
86    }
87
88    fn is_available(&self) -> bool {
89        crate::sources::helpers::command_exists("/bin/ps")
90    }
91
92    fn collect(&self, n_samples: usize) -> Vec<u8> {
93        let mut entropy = Vec::with_capacity(n_samples);
94
95        // 1. Extract raw bytes from process table snapshot.
96        // ps output is ASCII (0x20-0x7E + 0x0A), so XOR of adjacent bytes
97        // would produce a biased distribution. Use 8-byte XOR-fold to mix
98        // more input bits per output byte and reduce ASCII range bias.
99        if let Some(stdout) = snapshot_process_table() {
100            for chunk in stdout.chunks(8) {
101                let mut val = 0u64;
102                for (i, &b) in chunk.iter().enumerate() {
103                    val |= (b as u64) << (i * 8);
104                }
105                entropy.push(xor_fold_u64(val));
106                if entropy.len() >= n_samples {
107                    break;
108                }
109            }
110        }
111
112        // 2. Fill remaining with getpid() timing jitter
113        if entropy.len() < n_samples {
114            let jitter = collect_getpid_jitter(n_samples - entropy.len());
115            entropy.extend_from_slice(&jitter);
116        }
117
118        entropy.truncate(n_samples);
119        entropy
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn process_info() {
129        let src = ProcessSource::new();
130        assert_eq!(src.name(), "process_table");
131        assert_eq!(src.info().category, SourceCategory::System);
132        assert!(!src.info().composite);
133    }
134
135    #[test]
136    #[ignore] // Requires ps command
137    fn process_collects_bytes() {
138        let src = ProcessSource::new();
139        if src.is_available() {
140            let data = src.collect(64);
141            assert!(!data.is_empty());
142            assert!(data.len() <= 64);
143        }
144    }
145}