Skip to main content

openentropy_core/sources/timing/
ane_timing.rs

1//! Apple Neural Engine (ANE) timing — clock domain crossing jitter from ANE dispatch.
2//!
3//! The Apple Neural Engine has its own independent clock domain, separate from the
4//! CPU, GPU, audio PLL, display PLL, and PCIe PHY. Dispatching even a trivial
5//! workload to the ANE forces a clock domain crossing that introduces timing jitter.
6//!
7//! ## Entropy mechanism
8//!
9//! - **ANE clock domain crossing**: CPU crystal (24 MHz) vs ANE's independent PLL
10//! - **DMA setup variance**: ANE workloads require DMA buffer setup with variable latency
11//! - **Power state transitions**: ANE may be in low-power state, adding wake jitter
12//! - **Fabric contention**: ANE shares the memory fabric with CPU/GPU, adding noise
13//!
14//! ## Why this is unique
15//!
16//! No published entropy source extracts timing jitter from neural accelerator dispatch.
17//! The ANE is a completely separate compute domain with its own clocking, power gating,
18//! and DMA engine — making it an independent noise source from all existing oscillator
19//! beat sources.
20//!
21//! ## How it works
22//!
23//! We don't actually need CoreML or a real model. We probe the ANE subsystem via IOKit
24//! by reading properties from AppleH13ANEInterface / AppleANE* services. Each IOKit
25//! traversal crosses into the ANE's clock/power domain. CNTVCT_EL0 timestamps capture
26//! the timing jitter.
27
28use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
29#[cfg(target_os = "macos")]
30use crate::sources::helpers::extract_timing_entropy;
31
32static ANE_TIMING_INFO: SourceInfo = SourceInfo {
33    name: "ane_timing",
34    description: "Apple Neural Engine clock domain crossing jitter via IOKit property reads",
35    physics: "Probes Apple Neural Engine (ANE) IOKit services, forcing clock domain \
36              crossings between the CPU\u{2019}s 24 MHz crystal and the ANE\u{2019}s independent \
37              PLL. The ANE is a separate compute block with its own clocking, power gating, \
38              and DMA engine. Timing jitter arises from ANE PLL thermal noise (VCO \
39              Johnson-Nyquist), power state transition latency, DMA setup variance, and \
40              memory fabric contention. CNTVCT_EL0 timestamps before/after each IOKit \
41              call capture the beat between CPU and ANE clock domains.",
42    category: SourceCategory::Timing,
43    platform: Platform::MacOS,
44    requirements: &[Requirement::AppleSilicon, Requirement::IOKit],
45    entropy_rate_estimate: 3.0,
46    composite: false,
47    is_fast: true,
48};
49
50/// Apple Neural Engine timing jitter entropy source.
51pub struct AneTimingSource;
52
53#[cfg(target_os = "macos")]
54mod iokit {
55    use crate::sources::helpers::read_cntvct;
56    use std::ffi::{CString, c_char, c_void};
57
58    type IOReturn = i32;
59
60    #[allow(non_camel_case_types)]
61    type mach_port_t = u32;
62    #[allow(non_camel_case_types)]
63    type io_iterator_t = u32;
64    #[allow(non_camel_case_types)]
65    type io_object_t = u32;
66    #[allow(non_camel_case_types)]
67    type io_registry_entry_t = u32;
68
69    type CFTypeRef = *const c_void;
70    type CFAllocatorRef = *const c_void;
71    type CFMutableDictionaryRef = *mut c_void;
72    type CFDictionaryRef = *const c_void;
73
74    const K_IO_MAIN_PORT_DEFAULT: mach_port_t = 0;
75    const K_CF_ALLOCATOR_DEFAULT: CFAllocatorRef = std::ptr::null();
76
77    #[link(name = "IOKit", kind = "framework")]
78    #[allow(clashing_extern_declarations)]
79    unsafe extern "C" {
80        fn IOServiceMatching(name: *const c_char) -> CFMutableDictionaryRef;
81        fn IOServiceGetMatchingServices(
82            main_port: mach_port_t,
83            matching: CFDictionaryRef,
84            existing: *mut io_iterator_t,
85        ) -> IOReturn;
86        fn IOIteratorNext(iterator: io_iterator_t) -> io_object_t;
87        fn IORegistryEntryCreateCFProperties(
88            entry: io_registry_entry_t,
89            properties: *mut CFMutableDictionaryRef,
90            allocator: CFAllocatorRef,
91            options: u32,
92        ) -> IOReturn;
93        fn IOObjectRelease(object: io_object_t) -> IOReturn;
94    }
95
96    #[link(name = "CoreFoundation", kind = "framework")]
97    unsafe extern "C" {
98        fn CFRelease(cf: CFTypeRef);
99        fn CFDictionaryGetCount(dict: CFDictionaryRef) -> isize;
100    }
101
102    /// IOKit service class names for Apple Neural Engine subsystem.
103    /// Different Apple Silicon generations use different class names.
104    const ANE_SERVICE_CLASSES: &[&str] = &[
105        "H11ANEIn",         // ANE input/dispatch service (M1/M2/M3/M4)
106        "H11ANE",           // ANE controller (all Apple Silicon)
107        "AppleT6041ANEHAL", // ANE HAL (chip-specific, e.g. M3)
108        "ANEClientHints",   // ANE client hints service
109    ];
110
111    /// Probe an ANE IOKit service. Returns CNTVCT tick duration.
112    pub fn probe_ane_service(class_name: &str) -> u64 {
113        let c_name = match CString::new(class_name) {
114            Ok(s) => s,
115            Err(_) => return 0,
116        };
117
118        let counter_before = read_cntvct();
119
120        let matching = unsafe { IOServiceMatching(c_name.as_ptr()) };
121        if matching.is_null() {
122            return read_cntvct().wrapping_sub(counter_before);
123        }
124
125        let mut iterator: io_iterator_t = 0;
126        let kr = unsafe {
127            IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iterator)
128        };
129
130        if kr != 0 {
131            return read_cntvct().wrapping_sub(counter_before);
132        }
133
134        let service = unsafe { IOIteratorNext(iterator) };
135
136        if service != 0 {
137            let mut props: CFMutableDictionaryRef = std::ptr::null_mut();
138            let kr = unsafe {
139                IORegistryEntryCreateCFProperties(service, &mut props, K_CF_ALLOCATOR_DEFAULT, 0)
140            };
141
142            if kr == 0 && !props.is_null() {
143                let count = unsafe { CFDictionaryGetCount(props as CFDictionaryRef) };
144                std::hint::black_box(count);
145                unsafe { CFRelease(props as CFTypeRef) };
146            }
147
148            unsafe {
149                IOObjectRelease(service);
150            }
151        }
152
153        unsafe {
154            IOObjectRelease(iterator);
155        }
156
157        read_cntvct().wrapping_sub(counter_before)
158    }
159
160    /// Check if any ANE services are reachable.
161    pub fn has_ane_services() -> bool {
162        for class in ANE_SERVICE_CLASSES {
163            let c_name = match CString::new(*class) {
164                Ok(s) => s,
165                Err(_) => continue,
166            };
167            unsafe {
168                let matching = IOServiceMatching(c_name.as_ptr());
169                if matching.is_null() {
170                    continue;
171                }
172                let mut iter: io_iterator_t = 0;
173                let kr = IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iter);
174                if kr == 0 {
175                    let svc = IOIteratorNext(iter);
176                    IOObjectRelease(iter);
177                    if svc != 0 {
178                        IOObjectRelease(svc);
179                        return true;
180                    }
181                }
182            }
183        }
184        false
185    }
186
187    pub fn service_classes() -> &'static [&'static str] {
188        ANE_SERVICE_CLASSES
189    }
190}
191
192impl EntropySource for AneTimingSource {
193    fn info(&self) -> &SourceInfo {
194        &ANE_TIMING_INFO
195    }
196
197    fn is_available(&self) -> bool {
198        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
199        {
200            iokit::has_ane_services()
201        }
202        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
203        {
204            false
205        }
206    }
207
208    fn collect(&self, n_samples: usize) -> Vec<u8> {
209        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
210        {
211            let _ = n_samples;
212            Vec::new()
213        }
214
215        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
216        {
217            let classes = iokit::service_classes();
218            if classes.is_empty() {
219                return Vec::new();
220            }
221
222            let raw_count = n_samples * 4 + 64;
223            let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
224
225            for i in 0..raw_count {
226                let class = classes[i % classes.len()];
227                let duration = iokit::probe_ane_service(class);
228                timings.push(duration);
229            }
230
231            extract_timing_entropy(&timings, n_samples)
232        }
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn info() {
242        let src = AneTimingSource;
243        assert_eq!(src.name(), "ane_timing");
244        assert_eq!(src.info().category, SourceCategory::Timing);
245        assert!(!src.info().composite);
246    }
247
248    #[test]
249    fn physics_mentions_ane() {
250        let src = AneTimingSource;
251        assert!(src.info().physics.contains("Neural Engine"));
252        assert!(src.info().physics.contains("CNTVCT_EL0"));
253        assert!(src.info().physics.contains("PLL"));
254    }
255
256    #[test]
257    #[ignore] // Requires macOS Apple Silicon with ANE
258    fn collects_bytes() {
259        let src = AneTimingSource;
260        if src.is_available() {
261            let data = src.collect(64);
262            assert!(!data.is_empty());
263            assert!(data.len() <= 64);
264        }
265    }
266}