openentropy_core/sources/frontier/
kqueue_events.rs1use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::thread;
6
7use crate::source::{EntropySource, SourceCategory, SourceInfo};
8use crate::sources::helpers::{extract_timing_entropy, mach_time};
9
10#[derive(Debug, Clone)]
23pub struct KqueueEventsConfig {
24 pub num_file_watchers: usize,
32
33 pub num_timers: usize,
41
42 pub num_sockets: usize,
50
51 pub timeout_ms: u32,
59}
60
61impl Default for KqueueEventsConfig {
62 fn default() -> Self {
63 Self {
64 num_file_watchers: 4,
65 num_timers: 8,
66 num_sockets: 4,
67 timeout_ms: 1,
68 }
69 }
70}
71
72#[derive(Default)]
104pub struct KqueueEventsSource {
105 pub config: KqueueEventsConfig,
107}
108
109static KQUEUE_EVENTS_INFO: SourceInfo = SourceInfo {
110 name: "kqueue_events",
111 description: "Kqueue event multiplexing timing from timers, files, and sockets",
112 physics: "Registers diverse kqueue event types (timers, file watchers, socket monitors) \
113 and measures kevent() notification timing. Timer events capture kernel timer \
114 coalescing and interrupt jitter. File watchers exercise VFS/APFS notification \
115 paths. Socket events capture mbuf allocator timing. Multiple simultaneous watchers \
116 create knote lock contention and dispatch queue interference. The combination of \
117 independent event sources produces high min-entropy.",
118 category: SourceCategory::Frontier,
119 platform_requirements: &[],
120 entropy_rate_estimate: 2500.0,
121 composite: false,
122};
123
124impl EntropySource for KqueueEventsSource {
125 fn info(&self) -> &SourceInfo {
126 &KQUEUE_EVENTS_INFO
127 }
128
129 fn is_available(&self) -> bool {
130 cfg!(any(
131 target_os = "macos",
132 target_os = "freebsd",
133 target_os = "netbsd",
134 target_os = "openbsd"
135 ))
136 }
137
138 #[cfg(any(
139 target_os = "macos",
140 target_os = "freebsd",
141 target_os = "netbsd",
142 target_os = "openbsd"
143 ))]
144 fn collect(&self, n_samples: usize) -> Vec<u8> {
145 let raw_count = n_samples * 4 + 64;
146 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
147
148 let kq = unsafe { libc::kqueue() };
150 if kq < 0 {
151 return Vec::new();
152 }
153
154 let mut changes: Vec<libc::kevent> = Vec::new();
155 let mut cleanup_fds: Vec<i32> = Vec::new();
156
157 for i in 0..self.config.num_timers {
159 let interval_ms = 1 + (i % 10);
160 let mut ev: libc::kevent = unsafe { std::mem::zeroed() };
161 ev.ident = i;
162 ev.filter = libc::EVFILT_TIMER;
163 ev.flags = libc::EV_ADD | libc::EV_ENABLE;
164 ev.fflags = 0;
165 ev.data = interval_ms as isize;
166 ev.udata = std::ptr::null_mut();
167 changes.push(ev);
168 }
169
170 for _i in 0..self.config.num_sockets {
172 let mut sv: [i32; 2] = [0; 2];
173 let ret =
174 unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, sv.as_mut_ptr()) };
175 if ret == 0 {
176 cleanup_fds.push(sv[0]);
177 cleanup_fds.push(sv[1]);
178
179 let mut ev: libc::kevent = unsafe { std::mem::zeroed() };
180 ev.ident = sv[0] as usize;
181 ev.filter = libc::EVFILT_READ;
182 ev.flags = libc::EV_ADD | libc::EV_ENABLE;
183 ev.udata = std::ptr::null_mut();
184 changes.push(ev);
185
186 let byte = [0xAAu8];
187 unsafe {
188 libc::write(sv[1], byte.as_ptr() as *const _, 1);
189 }
190
191 let mut ev2: libc::kevent = unsafe { std::mem::zeroed() };
192 ev2.ident = sv[1] as usize;
193 ev2.filter = libc::EVFILT_WRITE;
194 ev2.flags = libc::EV_ADD | libc::EV_ENABLE;
195 ev2.udata = std::ptr::null_mut();
196 changes.push(ev2);
197 }
198 }
199
200 let mut temp_files: Vec<(i32, std::path::PathBuf)> = Vec::new();
202 for i in 0..self.config.num_file_watchers {
203 let path = std::env::temp_dir().join(format!("oe_kq_{i}_{}", std::process::id()));
204 if std::fs::write(&path, b"entropy").is_ok() {
205 let path_str = path.to_str().unwrap_or("");
206 let c_path = match std::ffi::CString::new(path_str) {
207 Ok(c) => c,
208 Err(_) => continue, };
210 let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDONLY) };
211 if fd >= 0 {
212 let mut ev: libc::kevent = unsafe { std::mem::zeroed() };
213 ev.ident = fd as usize;
214 ev.filter = libc::EVFILT_VNODE;
215 ev.flags = libc::EV_ADD | libc::EV_ENABLE | libc::EV_CLEAR;
216 ev.fflags = libc::NOTE_WRITE | libc::NOTE_ATTRIB;
217 ev.udata = std::ptr::null_mut();
218 changes.push(ev);
219 temp_files.push((fd, path));
220 }
221 }
222 }
223
224 if !changes.is_empty() {
226 unsafe {
227 libc::kevent(
228 kq,
229 changes.as_ptr(),
230 changes.len() as i32,
231 std::ptr::null_mut(),
232 0,
233 std::ptr::null(),
234 );
235 }
236 }
237
238 let stop = Arc::new(AtomicBool::new(false));
240 let stop2 = stop.clone();
241 let socket_write_fds: Vec<i32> = cleanup_fds.iter().skip(1).step_by(2).copied().collect();
242 let file_paths: Vec<std::path::PathBuf> =
243 temp_files.iter().map(|(_, p)| p.clone()).collect();
244
245 let poker = thread::spawn(move || {
246 let byte = [0xBBu8];
247 while !stop2.load(Ordering::Relaxed) {
248 for &fd in &socket_write_fds {
249 unsafe {
250 libc::write(fd, byte.as_ptr() as *const _, 1);
251 }
252 }
253 for path in &file_paths {
254 let _ = std::fs::write(path, b"poke");
255 }
256 std::thread::sleep(std::time::Duration::from_micros(500));
257 }
258 });
259
260 let timeout = libc::timespec {
262 tv_sec: 0,
263 tv_nsec: self.config.timeout_ms as i64 * 1_000_000,
264 };
265 let mut events: Vec<libc::kevent> =
266 vec![unsafe { std::mem::zeroed() }; changes.len().max(16)];
267
268 let socket_read_fds: Vec<i32> = cleanup_fds.iter().step_by(2).copied().collect();
269
270 for _ in 0..raw_count {
271 let t0 = mach_time();
272
273 let n = unsafe {
274 libc::kevent(
275 kq,
276 std::ptr::null(),
277 0,
278 events.as_mut_ptr(),
279 events.len() as i32,
280 &timeout,
281 )
282 };
283
284 let t1 = mach_time();
285
286 if n > 0 {
288 let mut drain = [0u8; 64];
289 for &fd in &socket_read_fds {
290 unsafe {
291 libc::read(fd, drain.as_mut_ptr() as *mut _, drain.len());
292 }
293 }
294 }
295
296 timings.push(t1.wrapping_sub(t0));
297 }
298
299 stop.store(true, Ordering::Relaxed);
301 let _ = poker.join();
302
303 for (fd, path) in &temp_files {
305 unsafe {
306 libc::close(*fd);
307 }
308 let _ = std::fs::remove_file(path);
309 }
310 for &fd in &cleanup_fds {
311 unsafe {
312 libc::close(fd);
313 }
314 }
315 unsafe {
316 libc::close(kq);
317 }
318
319 extract_timing_entropy(&timings, n_samples)
320 }
321
322 #[cfg(not(any(
323 target_os = "macos",
324 target_os = "freebsd",
325 target_os = "netbsd",
326 target_os = "openbsd"
327 )))]
328 fn collect(&self, _n_samples: usize) -> Vec<u8> {
329 Vec::new()
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn info() {
339 let src = KqueueEventsSource::default();
340 assert_eq!(src.name(), "kqueue_events");
341 assert_eq!(src.info().category, SourceCategory::Frontier);
342 assert!(!src.info().composite);
343 }
344
345 #[test]
346 fn default_config() {
347 let config = KqueueEventsConfig::default();
348 assert_eq!(config.num_file_watchers, 4);
349 assert_eq!(config.num_timers, 8);
350 assert_eq!(config.num_sockets, 4);
351 assert_eq!(config.timeout_ms, 1);
352 }
353
354 #[test]
355 fn custom_config() {
356 let src = KqueueEventsSource {
357 config: KqueueEventsConfig {
358 num_file_watchers: 2,
359 num_timers: 4,
360 num_sockets: 2,
361 timeout_ms: 5,
362 },
363 };
364 assert_eq!(src.config.num_timers, 4);
365 }
366
367 #[test]
368 #[ignore] fn collects_bytes() {
370 let src = KqueueEventsSource::default();
371 if src.is_available() {
372 let data = src.collect(64);
373 assert!(!data.is_empty());
374 assert!(data.len() <= 64);
375 }
376 }
377}