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}