Skip to main content

openentropy_core/sources/frontier/
kqueue_events.rs

1//! Kqueue event timing — entropy from BSD kernel event notification multiplexing.
2
3use 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/// Configuration for kqueue events entropy collection.
11///
12/// # Example
13/// ```
14/// # use openentropy_core::sources::frontier::KqueueEventsConfig;
15/// let config = KqueueEventsConfig {
16///     num_file_watchers: 2,   // fewer watchers
17///     num_timers: 16,         // more timers for richer interference
18///     num_sockets: 4,         // default socket pairs
19///     timeout_ms: 2,          // slightly longer timeout
20/// };
21/// ```
22#[derive(Debug, Clone)]
23pub struct KqueueEventsConfig {
24    /// Number of file watchers to register via `EVFILT_VNODE`.
25    ///
26    /// Each watcher monitors a temp file for write/attribute changes.
27    /// The filesystem notification path traverses VFS, APFS/HFS event queues,
28    /// and the kqueue knote hash table.
29    ///
30    /// **Range:** 0+. **Default:** `4`
31    pub num_file_watchers: usize,
32
33    /// Number of timer events to register via `EVFILT_TIMER`.
34    ///
35    /// Each timer fires at a different interval (1-10ms). Multiple timers
36    /// create scheduling contention and exercise kernel timer coalescing.
37    /// Timer delivery is affected by interrupt handling and power management.
38    ///
39    /// **Range:** 0+. **Default:** `8`
40    pub num_timers: usize,
41
42    /// Number of socket pairs for `EVFILT_READ`/`EVFILT_WRITE` monitoring.
43    ///
44    /// Socket buffer management interacts with the network stack's mbuf
45    /// allocator. A background thread periodically writes to sockets to
46    /// generate asynchronous events.
47    ///
48    /// **Range:** 0+. **Default:** `4`
49    pub num_sockets: usize,
50
51    /// Timeout in milliseconds for `kevent()` calls.
52    ///
53    /// Controls how long each `kevent()` waits for events. Shorter timeouts
54    /// capture more frequent timing samples; longer timeouts allow more
55    /// events to accumulate per call.
56    ///
57    /// **Range:** 1+. **Default:** `1`
58    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/// Harvests timing jitter from kqueue event notification multiplexing.
73///
74/// # What it measures
75/// Nanosecond timing of `kevent()` calls with multiple registered event
76/// types (timers, file watchers, socket monitors) firing concurrently.
77///
78/// # Why it's entropic
79/// kqueue is the macOS/BSD kernel event notification system. Registering
80/// diverse event types simultaneously creates rich interference:
81/// - **Timer events** — `EVFILT_TIMER` with different intervals fire at
82///   kernel-determined times affected by timer coalescing, interrupt handling,
83///   and power management state
84/// - **File watchers** — `EVFILT_VNODE` on temp files monitors inode changes;
85///   traverses VFS, APFS/HFS event queues, and the kqueue knote hash table
86/// - **Socket events** — `EVFILT_READ`/`EVFILT_WRITE` on socket pairs monitors
87///   buffer state; interacts with the network stack's mbuf allocator
88/// - **Knote lock contention** — many registered watchers all compete for the
89///   kqueue's internal knote lock and dispatch queue
90///
91/// # What makes it unique
92/// No prior work has combined multiple kqueue event types as an entropy source.
93/// The cross-event-type interference (timer delivery affecting socket
94/// notification timing) produces entropy that is independent from any single
95/// event source.
96///
97/// # Configuration
98/// See [`KqueueEventsConfig`] for tunable parameters. Key options:
99/// - `num_timers`: controls timer coalescing interference
100/// - `num_sockets`: controls mbuf allocator contention
101/// - `num_file_watchers`: controls VFS notification path diversity
102/// - `timeout_ms`: controls `kevent()` wait duration
103#[derive(Default)]
104pub struct KqueueEventsSource {
105    /// Source configuration. Use `Default::default()` for recommended settings.
106    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        // SAFETY: kqueue() creates a new kernel event queue (always safe).
149        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        // Register timer events with different intervals (1-10ms).
158        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        // Register socket pair events.
171        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        // Register file watchers on temp files.
201        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, // skip paths with null bytes
209                };
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        // Register all changes.
225        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        // Spawn a thread to periodically poke watched files and sockets.
239        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        // Collect timing samples.
261        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            // Drain socket read buffers to prevent saturation.
287            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        // Shutdown poker thread.
300        stop.store(true, Ordering::Relaxed);
301        let _ = poker.join();
302
303        // Cleanup.
304        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] // Uses kqueue syscall
369    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}