Skip to main content

yip_io/
epoll.rs

1//! A minimal, safe `epoll` primitive for the QUIC-mimicry driver (3c.1).
2//!
3//! `yipd` is `#![forbid(unsafe_code)]`, but the QUIC pump (`bin/yipd/src/quic.rs`)
4//! needs the same "wait on UDP + TUN with a caller-chosen timeout" capability
5//! that [`crate::poll::run_poll`] has — except `quinn-proto`'s decoupled I/O
6//! and dynamic timers want the loop *outside* yip-io, with the driver choosing
7//! `min(Connection::poll_timeout(), 10ms)` per iteration. This module exposes
8//! exactly that as a small safe wrapper ([`Epoll`]) so all the `epoll`/`fcntl`
9//! `unsafe` stays here, in the one crate permitted to hold it.
10//!
11//! This is intentionally *not* an event loop — it only blocks until one of the
12//! two fds is readable (or the timeout fires) and reports which. The driver
13//! owns the recv/send/timer logic.
14
15use std::io;
16use std::os::fd::RawFd;
17
18/// Which of the two watched fds became readable in the last [`Epoll::wait`].
19///
20/// Both may be set (both fds ready), or neither (the `wait` timed out — the
21/// driver should still run its timer/tick work).
22#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
23pub struct Ready {
24    /// The UDP socket has at least one datagram to read.
25    pub udp: bool,
26    /// The TUN/TAP device has at least one frame to read.
27    pub tun: bool,
28}
29
30/// An `epoll` instance watching a UDP socket fd and a TUN/TAP fd for `EPOLLIN`.
31///
32/// Owns the epoll fd and closes it on drop. The two watched fds are *not*
33/// owned (they belong to the caller's `UdpSocket`/`TunTap`) and are only
34/// referenced to identify readiness in [`Epoll::wait`].
35pub struct Epoll {
36    epoll_fd: RawFd,
37    udp_fd: RawFd,
38    tun_fd: RawFd,
39}
40
41/// Set a file descriptor to non-blocking mode via `fcntl(O_NONBLOCK)`.
42///
43/// Exposed so a `forbid(unsafe_code)` driver (yipd's QUIC pump) can make its
44/// socket/device non-blocking without an `unsafe` block of its own.
45pub fn set_nonblocking(fd: RawFd) -> io::Result<()> {
46    // SAFETY: `fd` is a valid open file descriptor supplied by the caller.
47    // `fcntl(F_SETFL, O_NONBLOCK)` is a pure flag-set on the open-file
48    // description; it cannot invalidate memory or cause UB.
49    let rc = unsafe { libc::fcntl(fd, libc::F_SETFL, libc::O_NONBLOCK) };
50    if rc < 0 {
51        Err(io::Error::last_os_error())
52    } else {
53        Ok(())
54    }
55}
56
57/// Read from a raw fd into `buf`, returning the number of bytes read.
58///
59/// A thin safe wrapper over `read(2)` so a `forbid(unsafe_code)` driver can
60/// pull frames off a non-socket fd (e.g. a TUN/TAP device, which is not a
61/// `UdpSocket` and so has no std read method available through this crate's
62/// callers). A would-block condition surfaces as `io::ErrorKind::WouldBlock`.
63pub fn read_fd(fd: RawFd, buf: &mut [u8]) -> io::Result<usize> {
64    // SAFETY: `buf` is a valid, writable slice of length `buf.len()`; `fd` is a
65    // valid open fd supplied by the caller. `read` writes at most `buf.len()`
66    // bytes into `buf` and never beyond it.
67    let n = unsafe { libc::read(fd, buf.as_mut_ptr().cast(), buf.len()) };
68    if n < 0 {
69        return Err(io::Error::last_os_error());
70    }
71    Ok(usize::try_from(n).expect("non-negative read return fits usize"))
72}
73
74/// Write `buf` to a raw fd, returning the number of bytes written.
75///
76/// The safe counterpart of [`read_fd`] for a non-socket fd (e.g. writing a
77/// decoded inner frame to a TUN/TAP device).
78pub fn write_fd(fd: RawFd, buf: &[u8]) -> io::Result<usize> {
79    // SAFETY: `buf` is a valid, readable slice of length `buf.len()`; `fd` is a
80    // valid open fd supplied by the caller. `write` reads at most `buf.len()`
81    // bytes from `buf` and never beyond it.
82    let n = unsafe { libc::write(fd, buf.as_ptr().cast(), buf.len()) };
83    if n < 0 {
84        return Err(io::Error::last_os_error());
85    }
86    Ok(usize::try_from(n).expect("non-negative write return fits usize"))
87}
88
89impl Epoll {
90    /// Create a new, empty `epoll` instance with `EPOLL_CLOEXEC`.
91    pub fn create() -> io::Result<Self> {
92        // SAFETY: `epoll_create1` creates a new epoll instance. `EPOLL_CLOEXEC`
93        // is the only flag passed; the return value is checked below.
94        let epoll_fd = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) };
95        if epoll_fd < 0 {
96            return Err(io::Error::last_os_error());
97        }
98        Ok(Self {
99            epoll_fd,
100            udp_fd: -1,
101            tun_fd: -1,
102        })
103    }
104
105    /// Register one fd for `EPOLLIN` with the given `token` user-data.
106    ///
107    /// Sets `fd` to non-blocking mode as a side effect.
108    pub fn add(&self, fd: RawFd, token: u64) -> io::Result<()> {
109        set_nonblocking(fd)?;
110        let mut ev = libc::epoll_event {
111            events: libc::EPOLLIN as u32,
112            u64: token,
113        };
114        // SAFETY: `self.epoll_fd` is a valid epoll fd we created and own;
115        // `fd` is a valid fd supplied by the caller; `ev` is a correctly
116        // initialised `epoll_event` living on this stack frame for the duration
117        // of the call.
118        let rc = unsafe { libc::epoll_ctl(self.epoll_fd, libc::EPOLL_CTL_ADD, fd, &raw mut ev) };
119        if rc < 0 {
120            return Err(io::Error::last_os_error());
121        }
122        Ok(())
123    }
124
125    /// Unregister an fd from the epoll instance (`EPOLL_CTL_DEL`).
126    pub fn remove(&self, fd: RawFd) -> io::Result<()> {
127        // SAFETY: `self.epoll_fd` is a valid epoll fd we created and own.
128        // For `EPOLL_CTL_DEL`, Linux ignores the event pointer; passing null is safe.
129        let rc = unsafe {
130            libc::epoll_ctl(self.epoll_fd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut())
131        };
132        if rc < 0 {
133            return Err(io::Error::last_os_error());
134        }
135        Ok(())
136    }
137
138    /// Block until at least one registered fd is readable, or `timeout_ms`
139    /// milliseconds elapse (`-1` blocks indefinitely).
140    ///
141    /// Clears `out` and appends all ready tokens. On timeout or `EINTR`, `out`
142    /// is left empty and `Ok(())` is returned.
143    pub fn wait_tokens(&self, timeout_ms: i32, out: &mut Vec<u64>) -> io::Result<()> {
144        out.clear();
145        let mut events = [libc::epoll_event { events: 0, u64: 0 }; 64];
146        // SAFETY: `self.epoll_fd` is a valid epoll fd. `events` is a valid,
147        // stack-allocated array whose length we pass as `maxevents`. The kernel
148        // writes at most `maxevents` initialised entries, which we read below.
149        let nfds = unsafe {
150            libc::epoll_wait(
151                self.epoll_fd,
152                events.as_mut_ptr(),
153                libc::c_int::try_from(events.len()).expect("event array len fits c_int"),
154                timeout_ms,
155            )
156        };
157        if nfds < 0 {
158            let e = io::Error::last_os_error();
159            if e.raw_os_error() == Some(libc::EINTR) {
160                return Ok(());
161            }
162            return Err(e);
163        }
164
165        for ev in &events[..usize::try_from(nfds).expect("non-negative nfds fits usize")] {
166            out.push(ev.u64);
167        }
168        Ok(())
169    }
170
171    /// Create an `epoll` instance registered for `EPOLLIN` on both `udp_fd`
172    /// and `tun_fd`. Both fds are set non-blocking as a side effect (the
173    /// driver drains them edge-safely with `MSG_DONTWAIT`/non-blocking reads).
174    pub fn new(udp_fd: RawFd, tun_fd: RawFd) -> io::Result<Self> {
175        let mut this = Self::create()?;
176        this.udp_fd = udp_fd;
177        this.tun_fd = tun_fd;
178        this.add(udp_fd, udp_fd as u64)?;
179        this.add(tun_fd, tun_fd as u64)?;
180        Ok(this)
181    }
182
183    /// Block until one of the watched fds is readable, or `timeout_ms`
184    /// milliseconds elapse (`-1` blocks indefinitely). Returns which fds are
185    /// ready; an `EINTR` is reported as "nothing ready" so the caller retries
186    /// its loop (running its timers) rather than erroring out.
187    pub fn wait(&self, timeout_ms: i32) -> io::Result<Ready> {
188        // Two watched fds ⇒ at most two ready events per wait.
189        let mut events = [libc::epoll_event { events: 0, u64: 0 }; 2];
190        // SAFETY: `self.epoll_fd` is a valid epoll fd. `events` is a valid,
191        // stack-allocated array whose length we pass as `maxevents`. The kernel
192        // writes at most `maxevents` initialised entries, which we read below.
193        let nfds = unsafe {
194            libc::epoll_wait(
195                self.epoll_fd,
196                events.as_mut_ptr(),
197                libc::c_int::try_from(events.len()).expect("event array len fits c_int"),
198                timeout_ms,
199            )
200        };
201        if nfds < 0 {
202            let e = io::Error::last_os_error();
203            if e.raw_os_error() == Some(libc::EINTR) {
204                return Ok(Ready::default());
205            }
206            return Err(e);
207        }
208
209        let mut ready = Ready::default();
210        for ev in &events[..usize::try_from(nfds).expect("non-negative nfds fits usize")] {
211            let fd = ev.u64 as RawFd;
212            if fd == self.udp_fd {
213                ready.udp = true;
214            } else if fd == self.tun_fd {
215                ready.tun = true;
216            }
217        }
218        Ok(ready)
219    }
220}
221
222impl Drop for Epoll {
223    fn drop(&mut self) {
224        // SAFETY: `self.epoll_fd` is a valid fd we created in `new` and have not
225        // closed elsewhere; closing it exactly once here is correct.
226        unsafe { libc::close(self.epoll_fd) };
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use std::net::UdpSocket;
234    use std::os::fd::AsRawFd;
235
236    #[test]
237    fn wait_reports_each_fd_independently() {
238        // Two loopback UDP sockets stand in for the "udp" and "tun" fds — both
239        // are epoll-able (a regular file like /dev/null is not: epoll_ctl EPERM).
240        let udp_rx = UdpSocket::bind("127.0.0.1:0").unwrap();
241        let udp_tx = UdpSocket::bind("127.0.0.1:0").unwrap();
242        udp_tx.connect(udp_rx.local_addr().unwrap()).unwrap();
243
244        let tun_rx = UdpSocket::bind("127.0.0.1:0").unwrap();
245        let tun_tx = UdpSocket::bind("127.0.0.1:0").unwrap();
246        tun_tx.connect(tun_rx.local_addr().unwrap()).unwrap();
247
248        let ep = Epoll::new(udp_rx.as_raw_fd(), tun_rx.as_raw_fd()).unwrap();
249
250        // Nothing sent yet ⇒ a short wait times out with neither fd ready.
251        assert_eq!(ep.wait(10).unwrap(), Ready::default());
252
253        // Only the "udp" fd is fed ⇒ only `udp` is reported.
254        udp_tx.send(b"ping").unwrap();
255        let after_udp = ep.wait(200).unwrap();
256        assert!(after_udp.udp, "udp must be ready after a datagram arrives");
257        assert!(
258            !after_udp.tun,
259            "tun must not be reported when only udp was fed"
260        );
261        udp_rx.recv(&mut [0u8; 8]).unwrap(); // drain so the next wait is clean
262
263        // Only the "tun" fd is fed ⇒ only `tun` is reported.
264        tun_tx.send(b"frame").unwrap();
265        let after_tun = ep.wait(200).unwrap();
266        assert!(after_tun.tun, "tun must be ready after a frame arrives");
267        assert!(
268            !after_tun.udp,
269            "udp must not be reported when only tun was fed"
270        );
271    }
272
273    #[test]
274    fn wait_times_out_with_nothing_ready() {
275        let a = UdpSocket::bind("127.0.0.1:0").unwrap();
276        let b = UdpSocket::bind("127.0.0.1:0").unwrap();
277        let ep = Epoll::new(a.as_raw_fd(), b.as_raw_fd()).unwrap();
278        assert_eq!(
279            ep.wait(5).unwrap(),
280            Ready::default(),
281            "idle fds must report nothing ready"
282        );
283    }
284
285    #[test]
286    fn test_read_write_fd_and_error_paths() {
287        let sock1 = UdpSocket::bind("127.0.0.1:0").unwrap();
288        let sock2 = UdpSocket::bind("127.0.0.1:0").unwrap();
289        sock1.connect(sock2.local_addr().unwrap()).unwrap();
290
291        let written = write_fd(sock1.as_raw_fd(), b"hello").unwrap();
292        assert_eq!(written, 5);
293
294        let mut buf = [0u8; 16];
295        let read = read_fd(sock2.as_raw_fd(), &mut buf).unwrap();
296        assert_eq!(read, 5);
297        assert_eq!(&buf[..5], b"hello");
298
299        // Error paths
300        assert!(set_nonblocking(-1).is_err());
301        assert!(read_fd(-1, &mut buf).is_err());
302        assert!(write_fd(-1, b"fail").is_err());
303    }
304
305    #[test]
306    fn epoll_dynamic_add_remove_and_wait_tokens() {
307        let epoll = Epoll::create().expect("create epoll");
308        let mut fds = [0; 2];
309        // SAFETY: `fds` is a valid 2-element array passed as a mutable pointer to pipe(2).
310        unsafe {
311            assert_eq!(libc::pipe(fds.as_mut_ptr()), 0);
312        }
313        let (rx, tx) = (fds[0], fds[1]);
314
315        epoll.add(rx, 42).expect("add rx fd");
316
317        let mut ready = Vec::new();
318        epoll.wait_tokens(0, &mut ready).expect("wait");
319        assert!(ready.is_empty());
320
321        // Write byte to pipe
322        // SAFETY: `tx` is a valid open pipe write end; 1-byte array buffer is valid and readable.
323        let written = unsafe { libc::write(tx, [1u8].as_ptr().cast(), 1) };
324        assert_eq!(written, 1);
325
326        epoll.wait_tokens(50, &mut ready).expect("wait");
327        assert_eq!(ready, vec![42]);
328
329        epoll.remove(rx).expect("remove rx fd");
330        ready.clear();
331        epoll.wait_tokens(0, &mut ready).expect("wait");
332        assert!(ready.is_empty());
333
334        // SAFETY: `rx` and `tx` are valid open pipe fds created by pipe(2) above.
335        unsafe {
336            libc::close(rx);
337            libc::close(tx);
338        }
339    }
340}