openentropy_core/sources/
ioregistry.rs1use 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
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: 1000.0,
35 composite: false,
36};
37
38pub struct IORegistryEntropySource;
40
41fn 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_quoted_key_numbers(trimmed, &mut map);
57 }
58
59 Some(map)
60}
61
62fn 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 if bytes[i] != b'"' {
73 i += 1;
74 continue;
75 }
76
77 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 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; while j < len && bytes[j] == b' ' {
102 j += 1;
103 }
104
105 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 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 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 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 assert_eq!(bytes.len(), 1);
199 assert_eq!(bytes[0], 0xAA);
200 }
201
202 #[test]
203 #[cfg(target_os = "macos")]
204 #[ignore] 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}