Skip to main content

openentropy_core/sources/system/
sysctl.rs

1//! SysctlSource — Batch-reads kernel counters via `sysctl -a`, takes multiple
2//! snapshots, finds keys that change between snapshots, extracts deltas of
3//! changing values, XORs consecutive deltas, and extracts LSBs.
4
5use std::collections::HashMap;
6use std::thread;
7use std::time::Duration;
8
9use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
10
11use crate::sources::helpers::{extract_delta_bytes_i64, run_command};
12
13/// Path to the sysctl binary on macOS.
14const SYSCTL_PATH: &str = "/usr/sbin/sysctl";
15
16/// Delay between the two sysctl snapshots.
17const SNAPSHOT_DELAY: Duration = Duration::from_millis(100);
18
19/// Entropy source that batch-reads kernel counters via `sysctl -a` and extracts
20/// deltas from the ~40-60 that change within 200ms.
21///
22/// No tunable parameters — the source reads all available sysctl keys and
23/// automatically identifies the ones that change between snapshots.
24pub struct SysctlSource;
25
26static SYSCTL_INFO: SourceInfo = SourceInfo {
27    name: "sysctl_deltas",
28    description: "Batch-reads ~1600 kernel counters via sysctl -a and extracts deltas from the ~40-60 that change within 200ms",
29    physics: "Batch-reads ~1600 kernel counters via sysctl and extracts deltas from \
30              the ~40-60 that change within 200ms. These counters track page faults, context \
31              switches, TCP segments, interrupts \u{2014} each driven by independent processes. \
32              The LSBs of their deltas reflect the unpredictable micro-timing of the entire \
33              operating system\u{2019}s activity.",
34    category: SourceCategory::System,
35    platform: Platform::MacOS,
36    requirements: &[],
37    entropy_rate_estimate: 3.0,
38    composite: false,
39    is_fast: false,
40};
41
42impl SysctlSource {
43    pub fn new() -> Self {
44        Self
45    }
46}
47
48impl Default for SysctlSource {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54/// Run `sysctl -a` and parse every line that has a numeric value into a HashMap.
55///
56/// Handles both `key: value` (macOS) and `key = value` (Linux) formats.
57fn snapshot_sysctl() -> Option<HashMap<String, i64>> {
58    let stdout = run_command(SYSCTL_PATH, &["-a"])?;
59    let mut map = HashMap::new();
60
61    for line in stdout.lines() {
62        // Try "key: value" first (macOS style), then "key = value" (Linux style)
63        let (key, val_str) = if let Some(idx) = line.find(": ") {
64            (&line[..idx], line[idx + 2..].trim())
65        } else if let Some(idx) = line.find(" = ") {
66            (&line[..idx], line[idx + 3..].trim())
67        } else {
68            continue;
69        };
70
71        // Only keep entries whose value is a plain integer
72        if let Ok(v) = val_str.parse::<i64>() {
73            map.insert(key.to_string(), v);
74        }
75    }
76
77    Some(map)
78}
79
80impl EntropySource for SysctlSource {
81    fn info(&self) -> &SourceInfo {
82        &SYSCTL_INFO
83    }
84
85    fn is_available(&self) -> bool {
86        std::path::Path::new(SYSCTL_PATH).exists()
87    }
88
89    fn collect(&self, n_samples: usize) -> Vec<u8> {
90        // Take two snapshots separated by a small delay
91        let snap1 = match snapshot_sysctl() {
92            Some(s) => s,
93            None => return Vec::new(),
94        };
95
96        thread::sleep(SNAPSHOT_DELAY);
97
98        let snap2 = match snapshot_sysctl() {
99            Some(s) => s,
100            None => return Vec::new(),
101        };
102
103        // Find keys that changed between the two snapshots and compute deltas
104        let mut deltas: Vec<i64> = Vec::new();
105        for (key, v2) in &snap2 {
106            if let Some(v1) = snap1.get(key) {
107                let delta = v2.wrapping_sub(*v1);
108                if delta != 0 {
109                    deltas.push(delta);
110                }
111            }
112        }
113
114        extract_delta_bytes_i64(&deltas, n_samples)
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn sysctl_info() {
124        let src = SysctlSource::new();
125        assert_eq!(src.name(), "sysctl_deltas");
126        assert_eq!(src.info().category, SourceCategory::System);
127        assert!(!src.info().composite);
128    }
129
130    #[test]
131    #[cfg(target_os = "macos")]
132    #[ignore] // Requires sysctl binary
133    fn sysctl_collects_bytes() {
134        let src = SysctlSource::new();
135        if src.is_available() {
136            let data = src.collect(64);
137            assert!(!data.is_empty());
138            assert!(data.len() <= 64);
139        }
140    }
141}