Skip to main content

openentropy_core/sources/io/
usb_enumeration.rs

1//! USB device enumeration timing entropy.
2//!
3//! macOS's IOKit USB subsystem enumerates USB devices through the USB controller.
4//! The enumeration process involves:
5//! 1. Querying the USB controller for device list
6//! 2. Walking the USB device tree
7//! 3. Reading device descriptors from each device
8//!
9//! ## Physics
10//!
11//! USB enumeration timing varies based on:
12//!
13//! 1. **USB controller state**: The USB xHCI controller has internal state
14//!    machines for each port. Port state (connected, enumerating, error)
15//!    affects enumeration latency.
16//!
17//! 2. **USB bus traffic**: Active transfers on the USB bus delay enumeration
18//!    requests. High-bandwidth devices (external drives, cameras) create
19//!    bus contention.
20//!
21//! 3. **Device descriptor cache**: macOS caches USB device descriptors.
22//!    Cold enumeration (first access after boot) is slower than warm
23//!    enumeration (cached descriptors).
24//!
25//! 4. **IOKit registry lock**: The IOKit registry is protected by locks.
26//!    Concurrent device hot-plug events hold these locks, delaying our
27//!    enumeration.
28//!
29//! Empirically on M4 Mac mini (N=200):
30//! - mean=13046 ticks (~544 µs), **CV=116.2%**, range=[9791,169338]
31//!
32//! ## Why This Is Entropy
33//!
34//! USB enumeration timing captures:
35//!
36//! 1. **USB bus activity** — other devices' transfers create bus contention
37//! 2. **Hot-plug events** — device insertions/removals hold IOKit locks
38//! 3. **Controller power state** — USB controller in low-power mode adds wake latency
39//! 4. **Cross-process sensitivity** — any process using USB devices changes timing
40
41use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
42
43#[cfg(target_os = "macos")]
44use crate::sources::helpers::{extract_timing_entropy, mach_time};
45
46static USB_ENUMERATION_INFO: SourceInfo = SourceInfo {
47    name: "usb_enumeration",
48    description: "IOKit USB device enumeration timing — CV=116%, USB controller state",
49    physics: "Times IOKit USB device enumeration (IOServiceMatching(kIOUSBDeviceClassName) + \
50              device tree walk). USB enumeration latency varies with: USB xHCI controller \
51              port state, USB bus traffic from active devices, IOKit registry lock contention \
52              from hot-plug events, controller power state wake-up latency. \
53              Measured: mean=13046 ticks (~544 µs), CV=116.2%, range=[9791,169338]. \
54              Cross-process sensitivity: any process using USB devices or hot-plugging \
55              changes enumeration timing.",
56    category: SourceCategory::IO,
57    platform: Platform::MacOS,
58    requirements: &[],
59    entropy_rate_estimate: 1.5,
60    composite: false,
61    is_fast: false,
62};
63
64/// Entropy source from USB device enumeration timing.
65pub struct USBEnumerationSource;
66
67#[cfg(target_os = "macos")]
68mod usb_imp {
69    use std::ffi::c_void;
70
71    pub type IOReturn = i32;
72    pub type MachPort = u32;
73
74    #[link(name = "IOKit", kind = "framework")]
75    unsafe extern "C" {
76        pub fn IOServiceMatching(name: *const i8) -> *mut c_void;
77        pub fn IOServiceGetMatchingServices(
78            main_port: MachPort,
79            matching: *const c_void,
80            iter: *mut u32,
81        ) -> IOReturn;
82        pub fn IOIteratorNext(iterator: u32) -> u32;
83        pub fn IOObjectRelease(obj: u32) -> IOReturn;
84    }
85
86    pub const K_IO_MAIN_PORT_DEFAULT: MachPort = 0;
87}
88
89#[cfg(target_os = "macos")]
90impl EntropySource for USBEnumerationSource {
91    fn info(&self) -> &SourceInfo {
92        &USB_ENUMERATION_INFO
93    }
94
95    fn is_available(&self) -> bool {
96        true
97    }
98
99    fn collect(&self, n_samples: usize) -> Vec<u8> {
100        use usb_imp::*;
101
102        let raw = n_samples * 2 + 32;
103        let mut timings = Vec::with_capacity(raw);
104
105        // Warm up
106        for _ in 0..4 {
107            let matching = unsafe { IOServiceMatching(c"IOUSBDevice".as_ptr()) };
108            if !matching.is_null() {
109                let mut iter: u32 = 0;
110                unsafe {
111                    IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iter);
112                    if iter != 0 {
113                        let mut obj = IOIteratorNext(iter);
114                        while obj != 0 {
115                            IOObjectRelease(obj);
116                            obj = IOIteratorNext(iter);
117                        }
118                        IOObjectRelease(iter);
119                    }
120                }
121            }
122        }
123
124        for _ in 0..raw {
125            let matching = unsafe { IOServiceMatching(c"IOUSBDevice".as_ptr()) };
126            if matching.is_null() {
127                continue;
128            }
129
130            let t0 = mach_time();
131            let mut iter: u32 = 0;
132            let kr = unsafe {
133                IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iter)
134            };
135
136            if kr == 0 && iter != 0 {
137                // Enumerate devices
138                let mut count = 0;
139                let mut obj = unsafe { IOIteratorNext(iter) };
140                while obj != 0 && count < 50 {
141                    unsafe { IOObjectRelease(obj) };
142                    obj = unsafe { IOIteratorNext(iter) };
143                    count += 1;
144                }
145                unsafe { IOObjectRelease(iter) };
146            }
147
148            let elapsed = mach_time().wrapping_sub(t0);
149            // Cap at 100ms
150            if elapsed < 2_400_000 {
151                timings.push(elapsed);
152            }
153        }
154
155        extract_timing_entropy(&timings, n_samples)
156    }
157}
158
159#[cfg(not(target_os = "macos"))]
160impl EntropySource for USBEnumerationSource {
161    fn info(&self) -> &SourceInfo {
162        &USB_ENUMERATION_INFO
163    }
164    fn is_available(&self) -> bool {
165        false
166    }
167    fn collect(&self, _: usize) -> Vec<u8> {
168        Vec::new()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn info() {
178        let src = USBEnumerationSource;
179        assert_eq!(src.info().name, "usb_enumeration");
180        assert!(matches!(src.info().category, SourceCategory::IO));
181        assert_eq!(src.info().platform, Platform::MacOS);
182    }
183
184    #[test]
185    #[cfg(target_os = "macos")]
186    fn is_available() {
187        assert!(USBEnumerationSource.is_available());
188    }
189
190    #[test]
191    #[ignore]
192    fn collects_usb_controller_state() {
193        let data = USBEnumerationSource.collect(32);
194        assert!(!data.is_empty());
195    }
196}