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