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. Shannon entropy ~3-5 bits/byte. The timing jitter component has
6//! higher entropy density than the process table bytes.
7
8use std::time::Instant;
9
10use crate::source::{EntropySource, SourceCategory, SourceInfo};
11
12use super::helpers::run_command_raw;
13
14/// Number of getpid() calls to measure for timing jitter.
15const JITTER_ROUNDS: usize = 256;
16
17pub struct ProcessSource {
18    info: SourceInfo,
19}
20
21impl ProcessSource {
22    pub fn new() -> Self {
23        Self {
24            info: SourceInfo {
25                name: "process_table",
26                description: "Process table snapshots combined with getpid() timing jitter",
27                physics: "Snapshots the process table (PIDs, CPU usage, memory) and extracts \
28                    entropy from the constantly-changing state. New PIDs are allocated \
29                    semi-randomly, CPU percentages fluctuate with scheduling decisions, and \
30                    resident memory sizes shift with page reclamation.",
31                category: SourceCategory::System,
32                platform_requirements: &[],
33                entropy_rate_estimate: 400.0,
34                composite: false,
35            },
36        }
37    }
38}
39
40impl Default for ProcessSource {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46/// Collect timing jitter from repeated getpid() syscalls.
47/// Returns raw LSBs of nanosecond timing deltas.
48fn collect_getpid_jitter(n_bytes: usize) -> Vec<u8> {
49    let rounds = JITTER_ROUNDS.max(n_bytes * 2);
50    let mut timings: Vec<u64> = Vec::with_capacity(rounds);
51
52    for _ in 0..rounds {
53        let start = Instant::now();
54        // SAFETY: getpid() is always safe — it's a simple read-only syscall.
55        unsafe {
56            libc::getpid();
57        }
58        let elapsed = start.elapsed().as_nanos() as u64;
59        timings.push(elapsed);
60    }
61
62    // Extract LSBs of timing deltas
63    let mut raw = Vec::with_capacity(n_bytes);
64    for pair in timings.windows(2) {
65        let delta = pair[1].wrapping_sub(pair[0]);
66        raw.push(delta as u8);
67        if raw.len() >= n_bytes {
68            break;
69        }
70    }
71    raw
72}
73
74/// Run `ps -eo pid,pcpu,rss` and return its raw stdout bytes.
75fn snapshot_process_table() -> Option<Vec<u8>> {
76    run_command_raw("ps", &["-eo", "pid,pcpu,rss"])
77}
78
79impl EntropySource for ProcessSource {
80    fn info(&self) -> &SourceInfo {
81        &self.info
82    }
83
84    fn is_available(&self) -> bool {
85        super::helpers::command_exists("ps")
86    }
87
88    fn collect(&self, n_samples: usize) -> Vec<u8> {
89        let mut entropy = Vec::with_capacity(n_samples);
90
91        // 1. Extract raw bytes from process table snapshot
92        if let Some(stdout) = snapshot_process_table() {
93            // XOR consecutive byte pairs for mixing
94            for pair in stdout.chunks(2) {
95                if pair.len() == 2 {
96                    entropy.push(pair[0] ^ pair[1]);
97                }
98                if entropy.len() >= n_samples {
99                    break;
100                }
101            }
102        }
103
104        // 2. Fill remaining with getpid() timing jitter
105        if entropy.len() < n_samples {
106            let jitter = collect_getpid_jitter(n_samples - entropy.len());
107            entropy.extend_from_slice(&jitter);
108        }
109
110        entropy.truncate(n_samples);
111        entropy
112    }
113}