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