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, 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
19pub struct SysctlSource {
20    info: SourceInfo,
21}
22
23impl SysctlSource {
24    pub fn new() -> Self {
25        Self {
26            info: 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_requirements: &["macos"],
36                entropy_rate_estimate: 5000.0,
37                composite: false,
38            },
39        }
40    }
41}
42
43impl Default for SysctlSource {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49/// Run `sysctl -a` and parse every line that has a numeric value into a HashMap.
50///
51/// Handles both `key: value` (macOS) and `key = value` (Linux) formats.
52fn snapshot_sysctl() -> Option<HashMap<String, i64>> {
53    let stdout = run_command(SYSCTL_PATH, &["-a"])?;
54    let mut map = HashMap::new();
55
56    for line in stdout.lines() {
57        // Try "key: value" first (macOS style), then "key = value" (Linux style)
58        let (key, val_str) = if let Some(idx) = line.find(": ") {
59            (&line[..idx], line[idx + 2..].trim())
60        } else if let Some(idx) = line.find(" = ") {
61            (&line[..idx], line[idx + 3..].trim())
62        } else {
63            continue;
64        };
65
66        // Only keep entries whose value is a plain integer
67        if let Ok(v) = val_str.parse::<i64>() {
68            map.insert(key.to_string(), v);
69        }
70    }
71
72    Some(map)
73}
74
75impl EntropySource for SysctlSource {
76    fn info(&self) -> &SourceInfo {
77        &self.info
78    }
79
80    fn is_available(&self) -> bool {
81        std::path::Path::new(SYSCTL_PATH).exists()
82    }
83
84    fn collect(&self, n_samples: usize) -> Vec<u8> {
85        // Take two snapshots separated by a small delay
86        let snap1 = match snapshot_sysctl() {
87            Some(s) => s,
88            None => return Vec::new(),
89        };
90
91        thread::sleep(SNAPSHOT_DELAY);
92
93        let snap2 = match snapshot_sysctl() {
94            Some(s) => s,
95            None => return Vec::new(),
96        };
97
98        // Find keys that changed between the two snapshots and compute deltas
99        let mut deltas: Vec<i64> = Vec::new();
100        for (key, v2) in &snap2 {
101            if let Some(v1) = snap1.get(key) {
102                let delta = v2.wrapping_sub(*v1);
103                if delta != 0 {
104                    deltas.push(delta);
105                }
106            }
107        }
108
109        extract_delta_bytes_i64(&deltas, n_samples)
110    }
111}