Skip to main content

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