Skip to main content

openentropy_core/sources/thermal/
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: 4.0,
48    composite: false,
49    is_fast: true,
50};
51
52/// PCIe PHY PLL timing jitter entropy source.
53pub struct PciePllSource;
54
55/// IOKit FFI for reading PCIe/Thunderbolt service properties.
56#[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    // CFTypeRef aliases
73    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    /// IOKit service class names that touch the PCIe/Thunderbolt clock domains.
115    /// Each represents a different clock domain or subsystem path.
116    const PCIE_SERVICE_CLASSES: &[&str] = &[
117        "AppleThunderboltHAL",
118        "IOPCIDevice",
119        "IOThunderboltController",
120        "IONVMeController",
121        "AppleUSBHostController",
122    ];
123
124    /// Probe an IOKit service matching the given class name.
125    /// Returns the time (in CNTVCT ticks) spent traversing IOKit.
126    #[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        // SAFETY: IOServiceMatching creates a matching dictionary from a class name.
136        // The returned dictionary is consumed by IOServiceGetMatchingServices.
137        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        // SAFETY: IOServiceGetMatchingServices consumes the matching dict
144        // and writes a valid iterator to `iterator`.
145        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        // Get first matching service.
154        // SAFETY: IOIteratorNext returns the next service or 0 if exhausted.
155        let service = unsafe { IOIteratorNext(iterator) };
156
157        if service != 0 {
158            let mut props: CFMutableDictionaryRef = std::ptr::null_mut();
159            // SAFETY: IORegistryEntryCreateCFProperties reads all properties
160            // from a valid IOService entry. `props` receives a retained CF dict.
161            let kr = unsafe {
162                IORegistryEntryCreateCFProperties(service, &mut props, K_CF_ALLOCATOR_DEFAULT, 0)
163            };
164
165            if kr == 0 && !props.is_null() {
166                // Read property count to force traversal of the dictionary.
167                // SAFETY: CFDictionaryGetCount on a valid dict.
168                let count = unsafe { CFDictionaryGetCount(props as CFDictionaryRef) };
169                std::hint::black_box(count);
170
171                // Try to read a specific property to go deeper into the subsystem.
172                let key_name = CString::new("IOPCIExpressLinkStatus").unwrap_or_default();
173                // SAFETY: CFStringCreateWithCString and CFDictionaryGetValue are
174                // read-only CF operations on valid objects.
175                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            // SAFETY: IOObjectRelease releases a valid IOKit object.
191            unsafe {
192                IOObjectRelease(service);
193            }
194        }
195
196        // Release iterator.
197        // SAFETY: IOObjectRelease releases a valid iterator.
198        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    /// Check if any PCIe services are reachable.
211    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            // SAFETY: IOServiceMatching + IOServiceGetMatchingServices are read-only lookups.
218            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}