Skip to main content

openentropy_core/sources/
ioregistry.rs

1//! IORegistryEntropySource -- Mines the macOS IORegistry for all fluctuating
2//! hardware counters.  Takes multiple snapshots of `ioreg -l -w0`, identifies
3//! numeric keys that change between snapshots, computes deltas, XORs
4//! consecutive deltas, and extracts LSBs.
5
6use std::collections::HashMap;
7use std::thread;
8use std::time::Duration;
9
10use crate::source::{EntropySource, SourceCategory, SourceInfo};
11
12use super::helpers::{extract_delta_bytes_i64, run_command};
13
14/// Path to the ioreg binary on macOS.
15const IOREG_PATH: &str = "/usr/sbin/ioreg";
16
17/// Delay between ioreg snapshots.
18const SNAPSHOT_DELAY: Duration = Duration::from_millis(80);
19
20/// Number of snapshots to collect (3-5 range).
21const NUM_SNAPSHOTS: usize = 4;
22
23static IOREGISTRY_INFO: SourceInfo = SourceInfo {
24    name: "ioregistry",
25    description: "Mines macOS IORegistry for fluctuating hardware counters and extracts LSBs of their deltas",
26    physics: "Mines the macOS IORegistry for all fluctuating hardware counters \u{2014} GPU \
27              utilization, NVMe SMART counters, memory controller stats, Neural Engine \
28              buffer allocations, DART IOMMU activity, Mach port counts, and display \
29              vsync counters. Each counter is driven by independent hardware subsystems. \
30              The LSBs of their deltas capture silicon-level activity across the entire SoC.",
31    category: SourceCategory::System,
32    platform_requirements: &["macos"],
33    entropy_rate_estimate: 1000.0,
34    composite: false,
35};
36
37/// Entropy source that mines the macOS IORegistry for hardware counter deltas.
38pub struct IORegistryEntropySource;
39
40/// Run `ioreg -l -w0` and parse lines matching `"key" = number` patterns into
41/// a HashMap of key -> value.
42fn snapshot_ioreg() -> Option<HashMap<String, i64>> {
43    let stdout = run_command(IOREG_PATH, &["-l", "-w0"])?;
44    let mut map = HashMap::new();
45
46    for line in stdout.lines() {
47        let trimmed = line.trim();
48        let trimmed = trimmed
49            .trim_start_matches('|')
50            .trim_start_matches('+')
51            .trim();
52
53        // Extract all "key"=number patterns from the line (covers both
54        // top-level `"key" = 123` and nested dict `"key"=123` formats).
55        extract_quoted_key_numbers(trimmed, &mut map);
56    }
57
58    Some(map)
59}
60
61/// Scan a string for all `"key"=number` or `"key" = number` patterns and
62/// insert them into the map. This handles both top-level ioreg properties
63/// and values nested inside `{...}` dictionaries on the same line.
64fn extract_quoted_key_numbers(s: &str, map: &mut HashMap<String, i64>) {
65    let bytes = s.as_bytes();
66    let len = bytes.len();
67    let mut i = 0;
68
69    while i < len {
70        // Find next opening quote
71        if bytes[i] != b'"' {
72            i += 1;
73            continue;
74        }
75
76        // Extract key between quotes
77        let key_start = i + 1;
78        let mut key_end = key_start;
79        while key_end < len && bytes[key_end] != b'"' {
80            key_end += 1;
81        }
82        if key_end >= len {
83            break;
84        }
85
86        let key = &s[key_start..key_end];
87        let mut j = key_end + 1;
88
89        // Skip optional whitespace then expect '='
90        while j < len && bytes[j] == b' ' {
91            j += 1;
92        }
93        if j >= len || bytes[j] != b'=' {
94            i = key_end + 1;
95            continue;
96        }
97        j += 1; // skip '='
98
99        // Skip optional whitespace after '='
100        while j < len && bytes[j] == b' ' {
101            j += 1;
102        }
103
104        // Try to parse a decimal integer (possibly negative)
105        let num_start = j;
106        if j < len && bytes[j] == b'-' {
107            j += 1;
108        }
109        while j < len && bytes[j].is_ascii_digit() {
110            j += 1;
111        }
112
113        if j > num_start
114            && (j >= len || !bytes[j].is_ascii_alphanumeric())
115            && let Ok(v) = s[num_start..j].parse::<i64>()
116        {
117            map.insert(key.to_string(), v);
118        }
119
120        i = j.max(key_end + 1);
121    }
122}
123
124impl EntropySource for IORegistryEntropySource {
125    fn info(&self) -> &SourceInfo {
126        &IOREGISTRY_INFO
127    }
128
129    fn is_available(&self) -> bool {
130        std::path::Path::new(IOREG_PATH).exists()
131    }
132
133    fn collect(&self, n_samples: usize) -> Vec<u8> {
134        // Take NUM_SNAPSHOTS snapshots with delays between them.
135        let mut snapshots: Vec<HashMap<String, i64>> = Vec::with_capacity(NUM_SNAPSHOTS);
136
137        for i in 0..NUM_SNAPSHOTS {
138            if i > 0 {
139                thread::sleep(SNAPSHOT_DELAY);
140            }
141            match snapshot_ioreg() {
142                Some(snap) => snapshots.push(snap),
143                None => return Vec::new(),
144            }
145        }
146
147        if snapshots.len() < 2 {
148            return Vec::new();
149        }
150
151        // Find keys present in ALL snapshots.
152        let common_keys: Vec<String> = {
153            let first = &snapshots[0];
154            first
155                .keys()
156                .filter(|k| snapshots.iter().all(|snap| snap.contains_key(*k)))
157                .cloned()
158                .collect()
159        };
160
161        // For each common key, extract deltas across consecutive snapshots.
162        let mut all_deltas: Vec<i64> = Vec::new();
163
164        for key in &common_keys {
165            for pair in snapshots.windows(2) {
166                let v1 = pair[0][key];
167                let v2 = pair[1][key];
168                let delta = v2.wrapping_sub(v1);
169                if delta != 0 {
170                    all_deltas.push(delta);
171                }
172            }
173        }
174
175        extract_delta_bytes_i64(&all_deltas, n_samples)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::super::helpers::extract_lsbs_i64 as extract_lsbs;
182    use super::*;
183
184    #[test]
185    fn ioregistry_info() {
186        let src = IORegistryEntropySource;
187        assert_eq!(src.name(), "ioregistry");
188        assert_eq!(src.info().category, SourceCategory::System);
189        assert!((src.info().entropy_rate_estimate - 1000.0).abs() < f64::EPSILON);
190    }
191
192    #[test]
193    fn extract_lsbs_basic() {
194        let deltas = vec![1i64, 2, 3, 4, 5, 6, 7, 8];
195        let bytes = extract_lsbs(&deltas);
196        // Bits: 1,0,1,0,1,0,1,0 -> 0xAA
197        assert_eq!(bytes.len(), 1);
198        assert_eq!(bytes[0], 0xAA);
199    }
200
201    #[test]
202    #[cfg(target_os = "macos")]
203    #[ignore] // Run with: cargo test -- --ignored
204    fn ioregistry_collects_bytes() {
205        let src = IORegistryEntropySource;
206        if src.is_available() {
207            let data = src.collect(64);
208            assert!(!data.is_empty());
209            assert!(data.len() <= 64);
210        }
211    }
212}