openentropy_core/sources/frontier/
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: 2000.0,
48 composite: false,
49};
50
51pub struct PciePllSource;
53
54#[cfg(target_os = "macos")]
56mod iokit {
57 use crate::sources::helpers::read_cntvct;
58 use std::ffi::{CString, c_char, c_void};
59
60 type IOReturn = i32;
61
62 #[allow(non_camel_case_types)]
63 type mach_port_t = u32;
64 #[allow(non_camel_case_types)]
65 type io_iterator_t = u32;
66 #[allow(non_camel_case_types)]
67 type io_object_t = u32;
68 #[allow(non_camel_case_types)]
69 type io_registry_entry_t = u32;
70
71 type CFTypeRef = *const c_void;
73 type CFStringRef = *const c_void;
74 type CFAllocatorRef = *const c_void;
75 type CFMutableDictionaryRef = *mut c_void;
76 type CFDictionaryRef = *const c_void;
77
78 const K_IO_MAIN_PORT_DEFAULT: mach_port_t = 0;
79 const K_CF_ALLOCATOR_DEFAULT: CFAllocatorRef = std::ptr::null();
80 const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
81
82 #[link(name = "IOKit", kind = "framework")]
83 #[allow(clashing_extern_declarations)]
84 unsafe extern "C" {
85 fn IOServiceMatching(name: *const c_char) -> CFMutableDictionaryRef;
86 fn IOServiceGetMatchingServices(
87 main_port: mach_port_t,
88 matching: CFDictionaryRef,
89 existing: *mut io_iterator_t,
90 ) -> IOReturn;
91 fn IOIteratorNext(iterator: io_iterator_t) -> io_object_t;
92 fn IORegistryEntryCreateCFProperties(
93 entry: io_registry_entry_t,
94 properties: *mut CFMutableDictionaryRef,
95 allocator: CFAllocatorRef,
96 options: u32,
97 ) -> IOReturn;
98 fn IOObjectRelease(object: io_object_t) -> IOReturn;
99 }
100
101 #[link(name = "CoreFoundation", kind = "framework")]
102 unsafe extern "C" {
103 fn CFRelease(cf: CFTypeRef);
104 fn CFDictionaryGetCount(dict: CFDictionaryRef) -> isize;
105 fn CFStringCreateWithCString(
106 alloc: CFAllocatorRef,
107 c_str: *const c_char,
108 encoding: u32,
109 ) -> CFStringRef;
110 fn CFDictionaryGetValue(dict: CFDictionaryRef, key: CFTypeRef) -> CFTypeRef;
111 }
112
113 const PCIE_SERVICE_CLASSES: &[&str] = &[
116 "AppleThunderboltHAL",
117 "IOPCIDevice",
118 "IOThunderboltController",
119 "IONVMeController",
120 "AppleUSBHostController",
121 ];
122
123 #[cfg(target_os = "macos")]
126 pub fn probe_service(class_name: &str) -> u64 {
127 let c_name = match CString::new(class_name) {
128 Ok(s) => s,
129 Err(_) => return 0,
130 };
131
132 let counter_before = read_cntvct();
133
134 let matching = unsafe { IOServiceMatching(c_name.as_ptr()) };
137 if matching.is_null() {
138 return read_cntvct().wrapping_sub(counter_before);
139 }
140
141 let mut iterator: io_iterator_t = 0;
142 let kr = unsafe {
145 IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iterator)
146 };
147
148 if kr != 0 {
149 return read_cntvct().wrapping_sub(counter_before);
150 }
151
152 let service = unsafe { IOIteratorNext(iterator) };
155
156 if service != 0 {
157 let mut props: CFMutableDictionaryRef = std::ptr::null_mut();
158 let kr = unsafe {
161 IORegistryEntryCreateCFProperties(service, &mut props, K_CF_ALLOCATOR_DEFAULT, 0)
162 };
163
164 if kr == 0 && !props.is_null() {
165 let count = unsafe { CFDictionaryGetCount(props as CFDictionaryRef) };
168 std::hint::black_box(count);
169
170 let key_name = CString::new("IOPCIExpressLinkStatus").unwrap_or_default();
172 unsafe {
175 let key = CFStringCreateWithCString(
176 K_CF_ALLOCATOR_DEFAULT,
177 key_name.as_ptr(),
178 K_CF_STRING_ENCODING_UTF8,
179 );
180 if !key.is_null() {
181 let val = CFDictionaryGetValue(props as CFDictionaryRef, key);
182 std::hint::black_box(val);
183 CFRelease(key);
184 }
185 CFRelease(props as CFTypeRef);
186 }
187 }
188
189 unsafe {
191 IOObjectRelease(service);
192 }
193 }
194
195 unsafe {
198 IOObjectRelease(iterator);
199 }
200
201 read_cntvct().wrapping_sub(counter_before)
202 }
203
204 #[cfg(not(target_os = "macos"))]
205 pub fn probe_service(_class_name: &str) -> u64 {
206 0
207 }
208
209 pub fn has_pcie_services() -> bool {
211 for class in PCIE_SERVICE_CLASSES {
212 let c_name = match CString::new(*class) {
213 Ok(s) => s,
214 Err(_) => continue,
215 };
216 unsafe {
218 let matching = IOServiceMatching(c_name.as_ptr());
219 if matching.is_null() {
220 continue;
221 }
222 let mut iter: io_iterator_t = 0;
223 let kr = IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iter);
224 if kr == 0 {
225 let svc = IOIteratorNext(iter);
226 IOObjectRelease(iter);
227 if svc != 0 {
228 IOObjectRelease(svc);
229 return true;
230 }
231 }
232 }
233 }
234 false
235 }
236
237 pub fn service_classes() -> &'static [&'static str] {
238 PCIE_SERVICE_CLASSES
239 }
240}
241
242impl EntropySource for PciePllSource {
243 fn info(&self) -> &SourceInfo {
244 &PCIE_PLL_INFO
245 }
246
247 fn is_available(&self) -> bool {
248 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
249 {
250 iokit::has_pcie_services()
251 }
252 #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
253 {
254 false
255 }
256 }
257
258 fn collect(&self, n_samples: usize) -> Vec<u8> {
259 #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
260 {
261 let _ = n_samples;
262 Vec::new()
263 }
264
265 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
266 {
267 let classes = iokit::service_classes();
268 if classes.is_empty() {
269 return Vec::new();
270 }
271
272 let raw_count = n_samples * 4 + 64;
273 let mut beats: Vec<u64> = Vec::with_capacity(raw_count);
274
275 for i in 0..raw_count {
276 let class = classes[i % classes.len()];
277 let duration = iokit::probe_service(class);
278 beats.push(duration);
279 }
280
281 extract_timing_entropy(&beats, n_samples)
282 }
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn info() {
292 let src = PciePllSource;
293 assert_eq!(src.name(), "pcie_pll");
294 assert_eq!(src.info().category, SourceCategory::Thermal);
295 assert!(!src.info().composite);
296 }
297
298 #[test]
299 fn physics_mentions_pcie() {
300 let src = PciePllSource;
301 assert!(src.info().physics.contains("PCIe"));
302 assert!(src.info().physics.contains("PLL"));
303 assert!(src.info().physics.contains("CNTVCT_EL0"));
304 }
305
306 #[test]
307 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
308 fn collects_bytes() {
309 let src = PciePllSource;
310 if src.is_available() {
311 let data = src.collect(64);
312 assert!(!data.is_empty());
313 assert!(data.len() <= 64);
314 }
315 }
316}