openentropy_core/sources/timing/
ane_timing.rs1use 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
50pub 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 const ANE_SERVICE_CLASSES: &[&str] = &[
105 "H11ANEIn", "H11ANE", "AppleT6041ANEHAL", "ANEClientHints", ];
110
111 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 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] 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}