openentropy_core/sources/thermal/
pcie_pll.rs1use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
31#[cfg(target_os = "macos")]
32use crate::sources::helpers::extract_timing_entropy;
33
34static PCIE_PLL_INFO: SourceInfo = SourceInfo {
35 name: "pcie_pll",
36 description: "PCIe PHY PLL jitter from IOKit property reads across PCIe clock domains",
37 physics: "Reads IOKit properties from PCIe/Thunderbolt IOService entries, forcing \
38 clock domain crossings into the PCIe PHY\u{2019}s independent PLL oscillators \
39 (CIO3PLL, AUSPLL, etc.). These PLLs are electrically separate from the \
40 CPU crystal, audio PLL, RTC crystal, and display PLL. Phase noise arises \
41 from VCO thermal noise, spread-spectrum clocking modulation, and lane skew. \
42 CNTVCT_EL0 timestamps before/after each IOKit call capture the beat between \
43 CPU crystal and PCIe clock domain.",
44 category: SourceCategory::Thermal,
45 platform: Platform::MacOS,
46 requirements: &[Requirement::AppleSilicon, Requirement::IOKit],
47 entropy_rate_estimate: 4.0,
48 composite: false,
49 is_fast: true,
50};
51
52pub struct PciePllSource;
54
55#[cfg(target_os = "macos")]
57mod iokit {
58 use crate::sources::helpers::read_cntvct;
59 use std::ffi::{CString, c_char, c_void};
60
61 type IOReturn = i32;
62
63 #[allow(non_camel_case_types)]
64 type mach_port_t = u32;
65 #[allow(non_camel_case_types)]
66 type io_iterator_t = u32;
67 #[allow(non_camel_case_types)]
68 type io_object_t = u32;
69 #[allow(non_camel_case_types)]
70 type io_registry_entry_t = u32;
71
72 type CFTypeRef = *const c_void;
74 type CFStringRef = *const c_void;
75 type CFAllocatorRef = *const c_void;
76 type CFMutableDictionaryRef = *mut c_void;
77 type CFDictionaryRef = *const c_void;
78
79 const K_IO_MAIN_PORT_DEFAULT: mach_port_t = 0;
80 const K_CF_ALLOCATOR_DEFAULT: CFAllocatorRef = std::ptr::null();
81 const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
82
83 #[link(name = "IOKit", kind = "framework")]
84 #[allow(clashing_extern_declarations)]
85 unsafe extern "C" {
86 fn IOServiceMatching(name: *const c_char) -> CFMutableDictionaryRef;
87 fn IOServiceGetMatchingServices(
88 main_port: mach_port_t,
89 matching: CFDictionaryRef,
90 existing: *mut io_iterator_t,
91 ) -> IOReturn;
92 fn IOIteratorNext(iterator: io_iterator_t) -> io_object_t;
93 fn IORegistryEntryCreateCFProperties(
94 entry: io_registry_entry_t,
95 properties: *mut CFMutableDictionaryRef,
96 allocator: CFAllocatorRef,
97 options: u32,
98 ) -> IOReturn;
99 fn IOObjectRelease(object: io_object_t) -> IOReturn;
100 }
101
102 #[link(name = "CoreFoundation", kind = "framework")]
103 unsafe extern "C" {
104 fn CFRelease(cf: CFTypeRef);
105 fn CFDictionaryGetCount(dict: CFDictionaryRef) -> isize;
106 fn CFStringCreateWithCString(
107 alloc: CFAllocatorRef,
108 c_str: *const c_char,
109 encoding: u32,
110 ) -> CFStringRef;
111 fn CFDictionaryGetValue(dict: CFDictionaryRef, key: CFTypeRef) -> CFTypeRef;
112 }
113
114 const PCIE_SERVICE_CLASSES: &[&str] = &[
117 "AppleThunderboltHAL",
118 "IOPCIDevice",
119 "IOThunderboltController",
120 "IONVMeController",
121 "AppleUSBHostController",
122 ];
123
124 #[cfg(target_os = "macos")]
127 pub fn probe_service(class_name: &str) -> u64 {
128 let c_name = match CString::new(class_name) {
129 Ok(s) => s,
130 Err(_) => return 0,
131 };
132
133 let counter_before = read_cntvct();
134
135 let matching = unsafe { IOServiceMatching(c_name.as_ptr()) };
138 if matching.is_null() {
139 return read_cntvct().wrapping_sub(counter_before);
140 }
141
142 let mut iterator: io_iterator_t = 0;
143 let kr = unsafe {
146 IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iterator)
147 };
148
149 if kr != 0 {
150 return read_cntvct().wrapping_sub(counter_before);
151 }
152
153 let service = unsafe { IOIteratorNext(iterator) };
156
157 if service != 0 {
158 let mut props: CFMutableDictionaryRef = std::ptr::null_mut();
159 let kr = unsafe {
162 IORegistryEntryCreateCFProperties(service, &mut props, K_CF_ALLOCATOR_DEFAULT, 0)
163 };
164
165 if kr == 0 && !props.is_null() {
166 let count = unsafe { CFDictionaryGetCount(props as CFDictionaryRef) };
169 std::hint::black_box(count);
170
171 let key_name = CString::new("IOPCIExpressLinkStatus").unwrap_or_default();
173 unsafe {
176 let key = CFStringCreateWithCString(
177 K_CF_ALLOCATOR_DEFAULT,
178 key_name.as_ptr(),
179 K_CF_STRING_ENCODING_UTF8,
180 );
181 if !key.is_null() {
182 let val = CFDictionaryGetValue(props as CFDictionaryRef, key);
183 std::hint::black_box(val);
184 CFRelease(key);
185 }
186 CFRelease(props as CFTypeRef);
187 }
188 }
189
190 unsafe {
192 IOObjectRelease(service);
193 }
194 }
195
196 unsafe {
199 IOObjectRelease(iterator);
200 }
201
202 read_cntvct().wrapping_sub(counter_before)
203 }
204
205 #[cfg(not(target_os = "macos"))]
206 pub fn probe_service(_class_name: &str) -> u64 {
207 0
208 }
209
210 pub fn has_pcie_services() -> bool {
212 for class in PCIE_SERVICE_CLASSES {
213 let c_name = match CString::new(*class) {
214 Ok(s) => s,
215 Err(_) => continue,
216 };
217 unsafe {
219 let matching = IOServiceMatching(c_name.as_ptr());
220 if matching.is_null() {
221 continue;
222 }
223 let mut iter: io_iterator_t = 0;
224 let kr = IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iter);
225 if kr == 0 {
226 let svc = IOIteratorNext(iter);
227 IOObjectRelease(iter);
228 if svc != 0 {
229 IOObjectRelease(svc);
230 return true;
231 }
232 }
233 }
234 }
235 false
236 }
237
238 pub fn service_classes() -> &'static [&'static str] {
239 PCIE_SERVICE_CLASSES
240 }
241}
242
243impl EntropySource for PciePllSource {
244 fn info(&self) -> &SourceInfo {
245 &PCIE_PLL_INFO
246 }
247
248 fn is_available(&self) -> bool {
249 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
250 {
251 iokit::has_pcie_services()
252 }
253 #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
254 {
255 false
256 }
257 }
258
259 fn collect(&self, n_samples: usize) -> Vec<u8> {
260 #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
261 {
262 let _ = n_samples;
263 Vec::new()
264 }
265
266 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
267 {
268 let classes = iokit::service_classes();
269 if classes.is_empty() {
270 return Vec::new();
271 }
272
273 let raw_count = n_samples * 4 + 64;
274 let mut beats: Vec<u64> = Vec::with_capacity(raw_count);
275
276 for i in 0..raw_count {
277 let class = classes[i % classes.len()];
278 let duration = iokit::probe_service(class);
279 beats.push(duration);
280 }
281
282 extract_timing_entropy(&beats, n_samples)
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn info() {
293 let src = PciePllSource;
294 assert_eq!(src.name(), "pcie_pll");
295 assert_eq!(src.info().category, SourceCategory::Thermal);
296 assert!(!src.info().composite);
297 }
298
299 #[test]
300 fn physics_mentions_pcie() {
301 let src = PciePllSource;
302 assert!(src.info().physics.contains("PCIe"));
303 assert!(src.info().physics.contains("PLL"));
304 assert!(src.info().physics.contains("CNTVCT_EL0"));
305 }
306
307 #[test]
308 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
309 fn collects_bytes() {
310 let src = PciePllSource;
311 if src.is_available() {
312 let data = src.collect(64);
313 assert!(!data.is_empty());
314 assert!(data.len() <= 64);
315 }
316 }
317}