openentropy_core/sources/system/
ioregistry.rs1use 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
14const IOREG_PATH: &str = "/usr/sbin/ioreg";
16
17const SNAPSHOT_DELAY: Duration = Duration::from_millis(80);
19
20const 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
39pub struct IORegistryEntropySource;
41
42fn 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_quoted_key_numbers(trimmed, &mut map);
58 }
59
60 Some(map)
61}
62
63fn 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 if bytes[i] != b'"' {
74 i += 1;
75 continue;
76 }
77
78 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 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; while j < len && bytes[j] == b' ' {
103 j += 1;
104 }
105
106 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 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 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 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 assert_eq!(bytes.len(), 1);
200 assert_eq!(bytes[0], 0xAA);
201 }
202
203 #[test]
204 #[cfg(target_os = "macos")]
205 #[ignore] 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}