Skip to main content

openentropy_core/sources/frontier/
kqueue_events.rs

1//! Kqueue event timing — entropy from BSD kernel event notification multiplexing.
2
3#[cfg(any(
4    target_os = "macos",
5    target_os = "freebsd",
6    target_os = "netbsd",
7    target_os = "openbsd"
8))]
9use std::sync::Arc;
10#[cfg(any(
11    target_os = "macos",
12    target_os = "freebsd",
13    target_os = "netbsd",
14    target_os = "openbsd"
15))]
16use std::sync::atomic::{AtomicBool, Ordering};
17#[cfg(any(
18    target_os = "macos",
19    target_os = "freebsd",
20    target_os = "netbsd",
21    target_os = "openbsd"
22))]
23use std::thread;
24
25use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
26#[cfg(any(
27    target_os = "macos",
28    target_os = "freebsd",
29    target_os = "netbsd",
30    target_os = "openbsd"
31))]
32use crate::sources::helpers::extract_timing_entropy;
33#[cfg(any(
34    target_os = "macos",
35    target_os = "freebsd",
36    target_os = "netbsd",
37    target_os = "openbsd"
38))]
39use crate::sources::helpers::mach_time;
40
41/// Configuration for kqueue events entropy collection.
42///
43/// # Example
44/// ```
45/// # use openentropy_core::sources::frontier::KqueueEventsConfig;
46/// let config = KqueueEventsConfig {
47///     num_file_watchers: 2,   // fewer watchers
48///     num_timers: 16,         // more timers for richer interference
49///     num_sockets: 4,         // default socket pairs
50///     timeout_ms: 2,          // slightly longer timeout
51/// };
52/// ```
53#[derive(Debug, Clone)]
54pub struct KqueueEventsConfig {
55    /// Number of file watchers to register via `EVFILT_VNODE`.
56    ///
57    /// Each watcher monitors a temp file for write/attribute changes.
58    /// The filesystem notification path traverses VFS, APFS/HFS event queues,
59    /// and the kqueue knote hash table.
60    ///
61    /// **Range:** 0+. **Default:** `4`
62    pub num_file_watchers: usize,
63
64    /// Number of timer events to register via `EVFILT_TIMER`.
65    ///
66    /// Each timer fires at a different interval (1-10ms). Multiple timers
67    /// create scheduling contention and exercise kernel timer coalescing.
68    /// Timer delivery is affected by interrupt handling and power management.
69    ///
70    /// **Range:** 0+. **Default:** `8`
71    pub num_timers: usize,
72
73    /// Number of socket pairs for `EVFILT_READ`/`EVFILT_WRITE` monitoring.
74    ///
75    /// Socket buffer management interacts with the network stack's mbuf
76    /// allocator. A background thread periodically writes to sockets to
77    /// generate asynchronous events.
78    ///
79    /// **Range:** 0+. **Default:** `4`
80    pub num_sockets: usize,
81
82    /// Timeout in milliseconds for `kevent()` calls.
83    ///
84    /// Controls how long each `kevent()` waits for events. Shorter timeouts
85    /// capture more frequent timing samples; longer timeouts allow more
86    /// events to accumulate per call.
87    ///
88    /// **Range:** 1+. **Default:** `1`
89    pub timeout_ms: u32,
90}
91
92impl Default for KqueueEventsConfig {
93    fn default() -> Self {
94        Self {
95            num_file_watchers: 4,
96            num_timers: 8,
97            num_sockets: 4,
98            timeout_ms: 1,
99        }
100    }
101}
102
103/// Harvests timing jitter from kqueue event notification multiplexing.
104///
105/// # What it measures
106/// Nanosecond timing of `kevent()` calls with multiple registered event
107/// types (timers, file watchers, socket monitors) firing concurrently.
108///
109/// # Why it's entropic
110/// kqueue is the macOS/BSD kernel event notification system. Registering
111/// diverse event types simultaneously creates rich interference:
112/// - **Timer events** — `EVFILT_TIMER` with different intervals fire at
113///   kernel-determined times affected by timer coalescing, interrupt handling,
114///   and power management state
115/// - **File watchers** — `EVFILT_VNODE` on temp files monitors inode changes;
116///   traverses VFS, APFS/HFS event queues, and the kqueue knote hash table
117/// - **Socket events** — `EVFILT_READ`/`EVFILT_WRITE` on socket pairs monitors
118///   buffer state; interacts with the network stack's mbuf allocator
119/// - **Knote lock contention** — many registered watchers all compete for the
120///   kqueue's internal knote lock and dispatch queue
121///
122/// # What makes it unique
123/// No prior work has combined multiple kqueue event types as an entropy source.
124/// The cross-event-type interference (timer delivery affecting socket
125/// notification timing) produces entropy that is independent from any single
126/// event source.
127///
128/// # Configuration
129/// See [`KqueueEventsConfig`] for tunable parameters. Key options:
130/// - `num_timers`: controls timer coalescing interference
131/// - `num_sockets`: controls mbuf allocator contention
132/// - `num_file_watchers`: controls VFS notification path diversity
133/// - `timeout_ms`: controls `kevent()` wait duration
134#[derive(Default)]
135pub struct KqueueEventsSource {
136    /// Source configuration. Use `Default::default()` for recommended settings.
137    pub config: KqueueEventsConfig,
138}
139
140static KQUEUE_EVENTS_INFO: SourceInfo = SourceInfo {
141    name: "kqueue_events",
142    description: "Kqueue event multiplexing timing from timers, files, and sockets",
143    physics: "Registers diverse kqueue event types (timers, file watchers, socket monitors) \
144              and measures kevent() notification timing. Timer events capture kernel timer \
145              coalescing and interrupt jitter. File watchers exercise VFS/APFS notification \
146              paths. Socket events capture mbuf allocator timing. Multiple simultaneous watchers \
147              create knote lock contention and dispatch queue interference. The combination of \
148              independent event sources produces high min-entropy.",
149    category: SourceCategory::IPC,
150    platform: Platform::MacOS,
151    requirements: &[],
152    entropy_rate_estimate: 2500.0,
153    composite: false,
154};
155
156impl EntropySource for KqueueEventsSource {
157    fn info(&self) -> &SourceInfo {
158        &KQUEUE_EVENTS_INFO
159    }
160
161    fn is_available(&self) -> bool {
162        cfg!(any(
163            target_os = "macos",
164            target_os = "freebsd",
165            target_os = "netbsd",
166            target_os = "openbsd"
167        ))
168    }
169
170    #[cfg(any(
171        target_os = "macos",
172        target_os = "freebsd",
173        target_os = "netbsd",
174        target_os = "openbsd"
175    ))]
176    fn collect(&self, n_samples: usize) -> Vec<u8> {
177        let raw_count = n_samples * 4 + 64;
178        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
179
180        // SAFETY: kqueue() creates a new kernel event queue (always safe).
181        let kq = unsafe { libc::kqueue() };
182        if kq < 0 {
183            return Vec::new();
184        }
185
186        let mut changes: Vec<libc::kevent> = Vec::new();
187        let mut cleanup_fds: Vec<i32> = Vec::new();
188
189        // Register timer events with different intervals (1-10ms).
190        for i in 0..self.config.num_timers {
191            let interval_ms = 1 + (i % 10);
192            let mut ev: libc::kevent = unsafe { std::mem::zeroed() };
193            ev.ident = i;
194            ev.filter = libc::EVFILT_TIMER;
195            ev.flags = libc::EV_ADD | libc::EV_ENABLE;
196            ev.fflags = 0;
197            ev.data = interval_ms as isize;
198            ev.udata = std::ptr::null_mut();
199            changes.push(ev);
200        }
201
202        // Register socket pair events.
203        for _i in 0..self.config.num_sockets {
204            let mut sv: [i32; 2] = [0; 2];
205            let ret =
206                unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, sv.as_mut_ptr()) };
207            if ret == 0 {
208                cleanup_fds.push(sv[0]);
209                cleanup_fds.push(sv[1]);
210
211                let mut ev: libc::kevent = unsafe { std::mem::zeroed() };
212                ev.ident = sv[0] as usize;
213                ev.filter = libc::EVFILT_READ;
214                ev.flags = libc::EV_ADD | libc::EV_ENABLE;
215                ev.udata = std::ptr::null_mut();
216                changes.push(ev);
217
218                let byte = [0xAAu8];
219                unsafe {
220                    libc::write(sv[1], byte.as_ptr() as *const _, 1);
221                }
222
223                let mut ev2: libc::kevent = unsafe { std::mem::zeroed() };
224                ev2.ident = sv[1] as usize;
225                ev2.filter = libc::EVFILT_WRITE;
226                ev2.flags = libc::EV_ADD | libc::EV_ENABLE;
227                ev2.udata = std::ptr::null_mut();
228                changes.push(ev2);
229            }
230        }
231
232        // Register file watchers on temp files.
233        let mut temp_files: Vec<(i32, std::path::PathBuf)> = Vec::new();
234        for i in 0..self.config.num_file_watchers {
235            let path = std::env::temp_dir().join(format!("oe_kq_{i}_{}", std::process::id()));
236            if std::fs::write(&path, b"entropy").is_ok() {
237                let path_str = path.to_str().unwrap_or("");
238                let c_path = match std::ffi::CString::new(path_str) {
239                    Ok(c) => c,
240                    Err(_) => continue, // skip paths with null bytes
241                };
242                let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDONLY) };
243                if fd >= 0 {
244                    let mut ev: libc::kevent = unsafe { std::mem::zeroed() };
245                    ev.ident = fd as usize;
246                    ev.filter = libc::EVFILT_VNODE;
247                    ev.flags = libc::EV_ADD | libc::EV_ENABLE | libc::EV_CLEAR;
248                    ev.fflags = libc::NOTE_WRITE | libc::NOTE_ATTRIB;
249                    ev.udata = std::ptr::null_mut();
250                    changes.push(ev);
251                    temp_files.push((fd, path));
252                }
253            }
254        }
255
256        // Register all changes.
257        if !changes.is_empty() {
258            unsafe {
259                libc::kevent(
260                    kq,
261                    changes.as_ptr(),
262                    changes.len() as i32,
263                    std::ptr::null_mut(),
264                    0,
265                    std::ptr::null(),
266                );
267            }
268        }
269
270        // Spawn a thread to periodically poke watched files and sockets.
271        let stop = Arc::new(AtomicBool::new(false));
272        let stop2 = stop.clone();
273        let socket_write_fds: Vec<i32> = cleanup_fds.iter().skip(1).step_by(2).copied().collect();
274        let file_paths: Vec<std::path::PathBuf> =
275            temp_files.iter().map(|(_, p)| p.clone()).collect();
276
277        let poker = thread::spawn(move || {
278            let byte = [0xBBu8];
279            while !stop2.load(Ordering::Relaxed) {
280                for &fd in &socket_write_fds {
281                    unsafe {
282                        libc::write(fd, byte.as_ptr() as *const _, 1);
283                    }
284                }
285                for path in &file_paths {
286                    let _ = std::fs::write(path, b"poke");
287                }
288                std::thread::sleep(std::time::Duration::from_micros(500));
289            }
290        });
291
292        // Collect timing samples.
293        let timeout = libc::timespec {
294            tv_sec: 0,
295            tv_nsec: self.config.timeout_ms as i64 * 1_000_000,
296        };
297        let mut events: Vec<libc::kevent> =
298            vec![unsafe { std::mem::zeroed() }; changes.len().max(16)];
299
300        let socket_read_fds: Vec<i32> = cleanup_fds.iter().step_by(2).copied().collect();
301
302        for _ in 0..raw_count {
303            let t0 = mach_time();
304
305            let n = unsafe {
306                libc::kevent(
307                    kq,
308                    std::ptr::null(),
309                    0,
310                    events.as_mut_ptr(),
311                    events.len() as i32,
312                    &timeout,
313                )
314            };
315
316            let t1 = mach_time();
317
318            // Drain socket read buffers to prevent saturation.
319            if n > 0 {
320                let mut drain = [0u8; 64];
321                for &fd in &socket_read_fds {
322                    unsafe {
323                        libc::read(fd, drain.as_mut_ptr() as *mut _, drain.len());
324                    }
325                }
326            }
327
328            timings.push(t1.wrapping_sub(t0));
329        }
330
331        // Shutdown poker thread.
332        stop.store(true, Ordering::Relaxed);
333        let _ = poker.join();
334
335        // Cleanup.
336        for (fd, path) in &temp_files {
337            unsafe {
338                libc::close(*fd);
339            }
340            let _ = std::fs::remove_file(path);
341        }
342        for &fd in &cleanup_fds {
343            unsafe {
344                libc::close(fd);
345            }
346        }
347        unsafe {
348            libc::close(kq);
349        }
350
351        extract_timing_entropy(&timings, n_samples)
352    }
353
354    #[cfg(not(any(
355        target_os = "macos",
356        target_os = "freebsd",
357        target_os = "netbsd",
358        target_os = "openbsd"
359    )))]
360    fn collect(&self, _n_samples: usize) -> Vec<u8> {
361        Vec::new()
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn info() {
371        let src = KqueueEventsSource::default();
372        assert_eq!(src.name(), "kqueue_events");
373        assert_eq!(src.info().category, SourceCategory::IPC);
374        assert!(!src.info().composite);
375    }
376
377    #[test]
378    fn default_config() {
379        let config = KqueueEventsConfig::default();
380        assert_eq!(config.num_file_watchers, 4);
381        assert_eq!(config.num_timers, 8);
382        assert_eq!(config.num_sockets, 4);
383        assert_eq!(config.timeout_ms, 1);
384    }
385
386    #[test]
387    fn custom_config() {
388        let src = KqueueEventsSource {
389            config: KqueueEventsConfig {
390                num_file_watchers: 2,
391                num_timers: 4,
392                num_sockets: 2,
393                timeout_ms: 5,
394            },
395        };
396        assert_eq!(src.config.num_timers, 4);
397    }
398
399    #[test]
400    #[ignore] // Uses kqueue syscall
401    fn collects_bytes() {
402        let src = KqueueEventsSource::default();
403        if src.is_available() {
404            let data = src.collect(64);
405            assert!(!data.is_empty());
406            assert!(data.len() <= 64);
407        }
408    }
409}