openentropy_core/sources/system/
process.rs1use std::time::Instant;
8
9use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
10
11use crate::sources::helpers::{run_command_raw, xor_fold_u64};
12
13const JITTER_ROUNDS: usize = 256;
15
16pub 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
50fn 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 unsafe {
60 libc::getpid();
61 }
62 let elapsed = start.elapsed().as_nanos() as u64;
63 timings.push(elapsed);
64 }
65
66 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
78fn 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 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 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] 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}