Skip to main content

openentropy_core/sources/frontier/
usb_timing.rs

1//! USB IORegistry query timing — crystal oscillator phase noise.
2//!
3//! USB host controllers use a crystal oscillator for the 1 kHz SOF signal.
4//! By rapidly querying USB device properties via IOKit/IORegistry, we measure
5//! timing jitter from the USB bus arbitration and clock domain crossing.
6//!
7//! The crystal has thermally-driven phase noise from:
8//! - Mechanical vibrations of quartz lattice (phonon noise)
9//! - Load capacitance thermal noise
10//! - Oscillator circuit Johnson-Nyquist noise
11//!
12
13use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
14#[cfg(target_os = "macos")]
15use crate::sources::helpers::extract_timing_entropy;
16
17static USB_TIMING_INFO: SourceInfo = SourceInfo {
18    name: "usb_timing",
19    description: "USB IORegistry query timing jitter from crystal oscillator phase noise",
20    physics: "Rapidly queries USB device properties via IOKit. Each query traverses the \
21              USB host controller\u{2019}s IORegistry tree, crossing the USB crystal oscillator / \
22              CPU clock domain boundary. The USB crystal has thermally-driven phase noise \
23              from quartz lattice phonon excitations, load capacitance Johnson-Nyquist noise, \
24              and oscillator circuit thermal fluctuations. Timing jitter also includes USB \
25              bus arbitration contention.",
26    category: SourceCategory::IO,
27    platform: Platform::MacOS,
28    requirements: &[Requirement::Usb, Requirement::IOKit],
29    entropy_rate_estimate: 1500.0,
30    composite: false,
31};
32
33/// Entropy source that harvests timing jitter from USB IORegistry queries.
34pub struct USBTimingSource;
35
36/// IOKit FFI for USB device enumeration and property reads.
37#[cfg(target_os = "macos")]
38mod iokit {
39    use std::ffi::{c_char, c_void};
40
41    // IOKit types
42    pub type IOReturn = i32;
43    pub type MachPort = u32;
44
45    #[link(name = "IOKit", kind = "framework")]
46    unsafe extern "C" {
47        pub fn IOServiceGetMatchingServices(
48            main_port: MachPort,
49            matching: *const c_void,
50            existing: *mut u32,
51        ) -> IOReturn;
52
53        pub fn IOServiceMatching(name: *const c_char) -> *mut c_void;
54
55        pub fn IOIteratorNext(iterator: u32) -> u32;
56
57        pub fn IOObjectRelease(object: u32) -> IOReturn;
58
59        pub fn IORegistryEntryCreateCFProperty(
60            entry: u32,
61            key: *const c_void,
62            allocator: *const c_void,
63            options: u32,
64        ) -> *const c_void;
65    }
66
67    #[link(name = "CoreFoundation", kind = "framework")]
68    unsafe extern "C" {
69        pub fn CFRelease(cf: *const c_void);
70
71        pub fn CFStringCreateWithCString(
72            alloc: *const c_void,
73            c_str: *const i8,
74            encoding: u32,
75        ) -> *const c_void;
76    }
77
78    /// kIOMainPortDefault is 0 on modern macOS.
79    pub const K_IO_MAIN_PORT_DEFAULT: MachPort = 0;
80
81    pub const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
82
83    /// Create a CFString from a null-terminated byte slice.
84    /// Returns null if `CFStringCreateWithCString` fails.
85    pub fn cfstr(s: &[u8]) -> *const c_void {
86        // SAFETY: CFStringCreateWithCString is a CoreFoundation API that reads a
87        // null-terminated C string. The caller must ensure `s` is null-terminated.
88        // We pass kCFAllocatorDefault (null) for the default allocator.
89        unsafe {
90            CFStringCreateWithCString(
91                std::ptr::null(),
92                s.as_ptr() as *const i8,
93                K_CF_STRING_ENCODING_UTF8,
94            )
95        }
96    }
97
98    /// Find USB devices and return their IOKit service handles.
99    ///
100    /// The caller is responsible for releasing each returned handle via `IOObjectRelease`.
101    pub fn find_usb_devices() -> Vec<u32> {
102        let mut devices = Vec::new();
103        // SAFETY: IOServiceMatching returns a CFDictionary matching USB host devices.
104        // The returned dictionary is consumed by IOServiceGetMatchingServices.
105        let matching = unsafe { IOServiceMatching(c"IOUSBHostDevice".as_ptr()) };
106        if matching.is_null() {
107            return devices;
108        }
109
110        let mut iter: u32 = 0;
111        // SAFETY: IOServiceGetMatchingServices consumes `matching` (even on failure)
112        // and writes an iterator handle to `iter`. We check the return code.
113        let kr =
114            unsafe { IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iter) };
115        if kr != 0 || iter == 0 {
116            return devices;
117        }
118
119        loop {
120            // SAFETY: IOIteratorNext returns the next service handle or 0 when exhausted.
121            let service = unsafe { IOIteratorNext(iter) };
122            if service == 0 {
123                break;
124            }
125            devices.push(service);
126        }
127        // SAFETY: Releasing the iterator we own. Each service handle is still valid.
128        unsafe { IOObjectRelease(iter) };
129        devices
130    }
131
132    /// Query a device property and return the elapsed time.
133    pub fn query_device_property(device: u32, key: &[u8]) -> std::time::Duration {
134        let cf_key = cfstr(key);
135        if cf_key.is_null() {
136            return std::time::Duration::ZERO;
137        }
138        let t0 = std::time::Instant::now();
139        // SAFETY: IORegistryEntryCreateCFProperty reads a property from a valid
140        // IOKit service handle using a valid CFString key. Returns null on failure.
141        let prop = unsafe { IORegistryEntryCreateCFProperty(device, cf_key, std::ptr::null(), 0) };
142        let elapsed = t0.elapsed();
143        if !prop.is_null() {
144            // SAFETY: Releasing a non-null CF object we received from IOKit.
145            unsafe { CFRelease(prop) };
146        }
147        // SAFETY: Releasing the CFString we created in cfstr().
148        unsafe { CFRelease(cf_key) };
149        elapsed
150    }
151}
152
153impl EntropySource for USBTimingSource {
154    fn info(&self) -> &SourceInfo {
155        &USB_TIMING_INFO
156    }
157
158    fn is_available(&self) -> bool {
159        #[cfg(target_os = "macos")]
160        {
161            // Quick check: find one USB device without full enumeration.
162            let devices = iokit::find_usb_devices();
163            let available = !devices.is_empty();
164            // Release any handles we acquired during the check.
165            for device in &devices {
166                // SAFETY: Releasing IOKit service handles from find_usb_devices().
167                unsafe { iokit::IOObjectRelease(*device) };
168            }
169            available
170        }
171        #[cfg(not(target_os = "macos"))]
172        {
173            false
174        }
175    }
176
177    fn collect(&self, n_samples: usize) -> Vec<u8> {
178        #[cfg(not(target_os = "macos"))]
179        {
180            let _ = n_samples;
181            Vec::new()
182        }
183
184        #[cfg(target_os = "macos")]
185        {
186            let devices = iokit::find_usb_devices();
187            if devices.is_empty() {
188                return Vec::new();
189            }
190
191            let raw_count = n_samples * 4 + 64;
192            let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
193
194            let property_keys: &[&[u8]] = &[b"sessionID\0", b"USB Address\0"];
195
196            for i in 0..raw_count {
197                let device = devices[i % devices.len()];
198                let key = property_keys[i % property_keys.len()];
199                let elapsed = iokit::query_device_property(device, key);
200                timings.push(elapsed.as_nanos() as u64);
201            }
202
203            // Release device handles before any further processing.
204            // Done eagerly so handles aren't leaked if extract_timing_entropy panics.
205            for device in &devices {
206                // SAFETY: Releasing IOKit service handles we own from find_usb_devices().
207                unsafe { iokit::IOObjectRelease(*device) };
208            }
209            drop(devices);
210
211            extract_timing_entropy(&timings, n_samples)
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn info() {
222        let src = USBTimingSource;
223        assert_eq!(src.name(), "usb_timing");
224        assert_eq!(src.info().category, SourceCategory::IO);
225        assert!(!src.info().composite);
226    }
227
228    #[test]
229    #[cfg(target_os = "macos")]
230    #[ignore] // Requires USB devices
231    fn collects_bytes() {
232        let src = USBTimingSource;
233        if src.is_available() {
234            let data = src.collect(64);
235            assert!(!data.is_empty());
236            assert!(data.len() <= 64);
237        }
238    }
239}