openentropy_core/sources/
process.rs1use std::time::Instant;
8
9use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
10
11use super::helpers::run_command_raw;
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::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
49fn 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 unsafe {
59 libc::getpid();
60 }
61 let elapsed = start.elapsed().as_nanos() as u64;
62 timings.push(elapsed);
63 }
64
65 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
77fn 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 if let Some(stdout) = snapshot_process_table() {
96 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 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] 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}