Skip to main content

openentropy_core/sources/frontier/
pcie_pll.rs

1//! PCIe PHY PLL timing — clock domain crossing jitter from PCIe subsystem.
2//!
3//! Apple Silicon Macs have multiple independent PLLs for the PCIe/Thunderbolt
4//! physical layer (visible in IORegistry as CIO3PLL, AUSPLL, ACIOPHY_PLL, etc.).
5//! These PLLs are electrically independent from the CPU crystal, audio PLL, RTC,
6//! and display PLL.
7//!
8//! ## Entropy mechanism
9//!
10//! We probe the PCIe subsystem via IOKit, reading properties from Thunderbolt/
11//! PCIe IOService entries. Each IOKit property read crosses from the CPU clock
12//! domain into the PCIe PHY's clock domain (or at minimum traverses IOKit's
13//! internal locking and serialization which is driven by PCIe bus timing).
14//!
15//! The PCIe PHY PLLs have thermal noise from:
16//! - VCO transistor Johnson-Nyquist noise
17//! - Spread-spectrum clocking (SSC) modulation if enabled
18//! - Lane-to-lane skew from manufacturing variation
19//! - Reference clock phase noise
20//!
21//! ## Why this is unique
22//!
23//! - **Fourth independent oscillator domain**: separate from CPU, audio, RTC, display
24//! - **Multiple PLLs**: each PCIe lane has its own clock recovery PLL
25//! - **Spread-spectrum clocking**: PCIe often uses SSC which intentionally modulates
26//!   the clock, adding an extra noise dimension
27//! - **Deep kernel path**: IOKit traversal exercises more kernel subsystems than
28//!   simple syscalls
29
30use 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
51/// PCIe PHY PLL timing jitter entropy source.
52pub struct PciePllSource;
53
54/// IOKit FFI for reading PCIe/Thunderbolt service properties.
55#[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    // CFTypeRef aliases
72    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    /// IOKit service class names that touch the PCIe/Thunderbolt clock domains.
114    /// Each represents a different clock domain or subsystem path.
115    const PCIE_SERVICE_CLASSES: &[&str] = &[
116        "AppleThunderboltHAL",
117        "IOPCIDevice",
118        "IOThunderboltController",
119        "IONVMeController",
120        "AppleUSBHostController",
121    ];
122
123    /// Probe an IOKit service matching the given class name.
124    /// Returns the time (in CNTVCT ticks) spent traversing IOKit.
125    #[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        // SAFETY: IOServiceMatching creates a matching dictionary from a class name.
135        // The returned dictionary is consumed by IOServiceGetMatchingServices.
136        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        // SAFETY: IOServiceGetMatchingServices consumes the matching dict
143        // and writes a valid iterator to `iterator`.
144        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        // Get first matching service.
153        // SAFETY: IOIteratorNext returns the next service or 0 if exhausted.
154        let service = unsafe { IOIteratorNext(iterator) };
155
156        if service != 0 {
157            let mut props: CFMutableDictionaryRef = std::ptr::null_mut();
158            // SAFETY: IORegistryEntryCreateCFProperties reads all properties
159            // from a valid IOService entry. `props` receives a retained CF dict.
160            let kr = unsafe {
161                IORegistryEntryCreateCFProperties(service, &mut props, K_CF_ALLOCATOR_DEFAULT, 0)
162            };
163
164            if kr == 0 && !props.is_null() {
165                // Read property count to force traversal of the dictionary.
166                // SAFETY: CFDictionaryGetCount on a valid dict.
167                let count = unsafe { CFDictionaryGetCount(props as CFDictionaryRef) };
168                std::hint::black_box(count);
169
170                // Try to read a specific property to go deeper into the subsystem.
171                let key_name = CString::new("IOPCIExpressLinkStatus").unwrap_or_default();
172                // SAFETY: CFStringCreateWithCString and CFDictionaryGetValue are
173                // read-only CF operations on valid objects.
174                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            // SAFETY: IOObjectRelease releases a valid IOKit object.
190            unsafe {
191                IOObjectRelease(service);
192            }
193        }
194
195        // Release iterator.
196        // SAFETY: IOObjectRelease releases a valid iterator.
197        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    /// Check if any PCIe services are reachable.
210    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            // SAFETY: IOServiceMatching + IOServiceGetMatchingServices are read-only lookups.
217            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}