Skip to main content

openentropy_core/sources/
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 super::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: 5000.0,
38    composite: false,
39};
40
41impl SysctlSource {
42    pub fn new() -> Self {
43        Self
44    }
45}
46
47impl Default for SysctlSource {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53/// Run `sysctl -a` and parse every line that has a numeric value into a HashMap.
54///
55/// Handles both `key: value` (macOS) and `key = value` (Linux) formats.
56fn snapshot_sysctl() -> Option<HashMap<String, i64>> {
57    let stdout = run_command(SYSCTL_PATH, &["-a"])?;
58    let mut map = HashMap::new();
59
60    for line in stdout.lines() {
61        // Try "key: value" first (macOS style), then "key = value" (Linux style)
62        let (key, val_str) = if let Some(idx) = line.find(": ") {
63            (&line[..idx], line[idx + 2..].trim())
64        } else if let Some(idx) = line.find(" = ") {
65            (&line[..idx], line[idx + 3..].trim())
66        } else {
67            continue;
68        };
69
70        // Only keep entries whose value is a plain integer
71        if let Ok(v) = val_str.parse::<i64>() {
72            map.insert(key.to_string(), v);
73        }
74    }
75
76    Some(map)
77}
78
79impl EntropySource for SysctlSource {
80    fn info(&self) -> &SourceInfo {
81        &SYSCTL_INFO
82    }
83
84    fn is_available(&self) -> bool {
85        std::path::Path::new(SYSCTL_PATH).exists()
86    }
87
88    fn collect(&self, n_samples: usize) -> Vec<u8> {
89        // Take two snapshots separated by a small delay
90        let snap1 = match snapshot_sysctl() {
91            Some(s) => s,
92            None => return Vec::new(),
93        };
94
95        thread::sleep(SNAPSHOT_DELAY);
96
97        let snap2 = match snapshot_sysctl() {
98            Some(s) => s,
99            None => return Vec::new(),
100        };
101
102        // Find keys that changed between the two snapshots and compute deltas
103        let mut deltas: Vec<i64> = Vec::new();
104        for (key, v2) in &snap2 {
105            if let Some(v1) = snap1.get(key) {
106                let delta = v2.wrapping_sub(*v1);
107                if delta != 0 {
108                    deltas.push(delta);
109                }
110            }
111        }
112
113        extract_delta_bytes_i64(&deltas, n_samples)
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn sysctl_info() {
123        let src = SysctlSource::new();
124        assert_eq!(src.name(), "sysctl_deltas");
125        assert_eq!(src.info().category, SourceCategory::System);
126        assert!(!src.info().composite);
127    }
128
129    #[test]
130    #[cfg(target_os = "macos")]
131    #[ignore] // Requires sysctl binary
132    fn sysctl_collects_bytes() {
133        let src = SysctlSource::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}