Skip to main content

openentropy_core/sources/
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 super::helpers::run_command_raw;
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::Any,
32    requirements: &[],
33    entropy_rate_estimate: 400.0,
34    composite: false,
35};
36
37impl ProcessSource {
38    pub fn new() -> Self {
39        Self
40    }
41}
42
43impl Default for ProcessSource {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49/// Collect timing jitter from repeated getpid() syscalls.
50/// Returns raw LSBs of nanosecond timing deltas.
51fn collect_getpid_jitter(n_bytes: usize) -> Vec<u8> {
52    let rounds = JITTER_ROUNDS.max(n_bytes * 2);
53    let mut timings: Vec<u64> = Vec::with_capacity(rounds);
54
55    for _ in 0..rounds {
56        let start = Instant::now();
57        // SAFETY: getpid() is always safe — it's a simple read-only syscall.
58        unsafe {
59            libc::getpid();
60        }
61        let elapsed = start.elapsed().as_nanos() as u64;
62        timings.push(elapsed);
63    }
64
65    // Extract LSBs of timing deltas
66    let mut raw = Vec::with_capacity(n_bytes);
67    for pair in timings.windows(2) {
68        let delta = pair[1].wrapping_sub(pair[0]);
69        raw.push(delta as u8);
70        if raw.len() >= n_bytes {
71            break;
72        }
73    }
74    raw
75}
76
77/// Run `ps -eo pid,pcpu,rss` and return its raw stdout bytes.
78fn snapshot_process_table() -> Option<Vec<u8>> {
79    run_command_raw("ps", &["-eo", "pid,pcpu,rss"])
80}
81
82impl EntropySource for ProcessSource {
83    fn info(&self) -> &SourceInfo {
84        &PROCESS_INFO
85    }
86
87    fn is_available(&self) -> bool {
88        super::helpers::command_exists("ps")
89    }
90
91    fn collect(&self, n_samples: usize) -> Vec<u8> {
92        let mut entropy = Vec::with_capacity(n_samples);
93
94        // 1. Extract raw bytes from process table snapshot
95        if let Some(stdout) = snapshot_process_table() {
96            // XOR consecutive byte pairs for mixing
97            for pair in stdout.chunks(2) {
98                if pair.len() == 2 {
99                    entropy.push(pair[0] ^ pair[1]);
100                }
101                if entropy.len() >= n_samples {
102                    break;
103                }
104            }
105        }
106
107        // 2. Fill remaining with getpid() timing jitter
108        if entropy.len() < n_samples {
109            let jitter = collect_getpid_jitter(n_samples - entropy.len());
110            entropy.extend_from_slice(&jitter);
111        }
112
113        entropy.truncate(n_samples);
114        entropy
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn process_info() {
124        let src = ProcessSource::new();
125        assert_eq!(src.name(), "process_table");
126        assert_eq!(src.info().category, SourceCategory::System);
127        assert!(!src.info().composite);
128    }
129
130    #[test]
131    #[ignore] // Requires ps command
132    fn process_collects_bytes() {
133        let src = ProcessSource::new();
134        if src.is_available() {
135            let data = src.collect(64);
136            assert!(!data.is_empty());
137            assert!(data.len() <= 64);
138        }
139    }
140}