coreshift_core/fd.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Owned file-descriptor primitives.
6//!
7//! [`Fd`] is the crate-wide owned descriptor type (move-only, closes on drop).
8//! [`Token`] and [`Event`] describe readiness events delivered by the
9//! [`crate::reactor::Reactor`].
10
11use crate::CoreError;
12use crate::error::syscall_ret;
13use std::io::Error as IoError;
14use std::time::Duration;
15
16#[inline(always)]
17fn errno() -> i32 {
18 IoError::last_os_error().raw_os_error().unwrap_or(0)
19}
20
21/// An owned file descriptor that closes on drop.
22///
23/// `Fd` is move-only. Constructing one from a raw descriptor transfers close
24/// ownership to `Fd`; do not also close the raw descriptor elsewhere.
25///
26/// ### Fork Safety
27/// `Fd` instances created by Core usually have `O_CLOEXEC` set. If the process
28/// forks, the descriptor will be inherited by the child but will be closed
29/// automatically upon `exec`. Callers that need a descriptor to survive `exec`
30/// must clear the flag manually.
31pub struct Fd(RawFd);
32
33use std::os::unix::io::{AsRawFd, RawFd};
34
35impl AsRawFd for Fd {
36 fn as_raw_fd(&self) -> RawFd {
37 self.0
38 }
39}
40
41impl Fd {
42 /// Wrap a raw file descriptor.
43 ///
44 /// # Errors
45 /// Returns a [`CoreError`] if the descriptor is negative.
46 #[inline(always)]
47 pub(crate) fn new(fd: RawFd, op: &'static str) -> Result<Self, CoreError> {
48 if fd < 0 {
49 Err(CoreError::sys(errno(), op))
50 } else {
51 Ok(Self(fd))
52 }
53 }
54
55 /// Wrap an owned raw file descriptor.
56 ///
57 /// # Safety
58 /// The caller must guarantee `fd` is valid, open, and uniquely owned by the
59 /// returned `Fd`. Passing a borrowed fd, or closing `fd` after this call,
60 /// can cause double-close or use-after-close bugs.
61 #[inline(always)]
62 pub unsafe fn from_owned_raw_fd(fd: RawFd, op: &'static str) -> Result<Self, CoreError> {
63 Self::new(fd, op)
64 }
65
66 /// Create a non-blocking `eventfd` with `EFD_CLOEXEC`.
67 ///
68 /// The descriptor is created with `FD_CLOEXEC` set.
69 ///
70 /// ### Errors
71 /// - `EINVAL`: `init` is invalid.
72 /// - `EMFILE`: Process limit on open file descriptors hit.
73 /// - `ENFILE`: System-wide limit on open files hit.
74 pub fn eventfd(init: u32) -> Result<Self, CoreError> {
75 let fd = unsafe { libc::eventfd(init, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) };
76 syscall_ret(fd, "eventfd")?;
77 Self::new(fd, "eventfd")
78 }
79
80 /// Create a non-blocking `timerfd` using `CLOCK_MONOTONIC` with `TFD_CLOEXEC`.
81 ///
82 /// The descriptor is created with `FD_CLOEXEC` set.
83 ///
84 /// ### Errors
85 /// - `EMFILE`: Process limit on open file descriptors hit.
86 /// - `ENFILE`: System-wide limit on open files hit.
87 /// - `ENOMEM`: Insufficient kernel memory.
88 pub fn timerfd() -> Result<Self, CoreError> {
89 let fd = unsafe {
90 libc::timerfd_create(
91 libc::CLOCK_MONOTONIC,
92 libc::TFD_CLOEXEC | libc::TFD_NONBLOCK,
93 )
94 };
95 syscall_ret(fd, "timerfd_create")?;
96 Self::new(fd, "timerfd_create")
97 }
98
99 /// Access the underlying raw file descriptor.
100 ///
101 /// NOTE: This is an escape hatch for low-level interactions. Prefer using
102 /// the safe methods on `Fd` or implementing `AsRawFd`.
103 #[inline(always)]
104 pub(crate) fn raw(&self) -> RawFd {
105 self.0
106 }
107
108 /// Duplicate this descriptor, returning a new owned `Fd`.
109 ///
110 /// Both descriptors share the same open file description, so a `dup` of
111 /// an eventfd remains a single signalable object: a write on either copy
112 /// is observed on the other. Useful for fan-out wakeups (hub keeps one
113 /// copy, a worker reactor owns another).
114 ///
115 /// ### Errors
116 /// - `EBADF`: The source descriptor is invalid.
117 /// - `EMFILE`: The process file descriptor limit is reached.
118 pub fn dup(&self) -> Result<Self, CoreError> {
119 let r = loop {
120 let d = unsafe { libc::dup(self.0) };
121 if d < 0 && errno() == libc::EINTR {
122 continue;
123 }
124 break d;
125 };
126 if r < 0 {
127 let e = errno();
128 Err(CoreError::sys(e, "dup"))
129 } else {
130 // SAFETY: `r` is a freshly duplicated owned descriptor.
131 unsafe { Self::from_owned_raw_fd(r, "dup") }
132 }
133 }
134
135 /// Perform a `dup2` syscall.
136 ///
137 /// ### Errors
138 /// - `EBADF`: The source or target file descriptor is invalid.
139 /// - `EMFILE`: The target descriptor exceeds the process limit.
140 pub fn dup2(&self, target: RawFd) -> Result<(), CoreError> {
141 loop {
142 let r = unsafe { libc::dup2(self.0, target) };
143 if r < 0 {
144 let e = errno();
145 if e == libc::EINTR {
146 continue;
147 }
148 return syscall_ret(r, "dup2");
149 }
150 return Ok(());
151 }
152 }
153
154 /// Set the `O_NONBLOCK` flag on the descriptor.
155 ///
156 /// ### Errors
157 /// - `EBADF`: The file descriptor is invalid.
158 pub fn set_nonblock(&self) -> Result<(), CoreError> {
159 let flags = unsafe { libc::fcntl(self.0, libc::F_GETFL) };
160 syscall_ret(flags, "fcntl(F_GETFL)")?;
161 let r = unsafe { libc::fcntl(self.0, libc::F_SETFL, flags | libc::O_NONBLOCK) };
162 syscall_ret(r, "fcntl(F_SETFL)")
163 }
164
165 /// Set the `FD_CLOEXEC` flag on the descriptor.
166 ///
167 /// ### Errors
168 /// - `EBADF`: The file descriptor is invalid.
169 pub fn set_cloexec(&self) -> Result<(), CoreError> {
170 let flags = unsafe { libc::fcntl(self.0, libc::F_GETFD) };
171 syscall_ret(flags, "fcntl(F_GETFD)")?;
172 let r = unsafe { libc::fcntl(self.0, libc::F_SETFD, flags | libc::FD_CLOEXEC) };
173 syscall_ret(r, "fcntl(F_SETFD)")
174 }
175
176 /// Read bytes into a mutable slice.
177 ///
178 /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
179 ///
180 /// ### Edge Cases
181 /// - **Zero-length read**: Returns `Ok(Some(0))` immediately.
182 /// - **Partial read**: Returns the number of bytes actually read.
183 ///
184 /// ### Errors
185 /// - `EBADF`: The file descriptor is invalid or not open for reading.
186 /// - `EFAULT`: `buf` points outside the process's address space.
187 /// - `EIO`: Low-level I/O error.
188 pub fn read_slice(&self, buf: &mut [u8]) -> Result<Option<usize>, CoreError> {
189 self.read_raw(buf.as_mut_ptr(), buf.len())
190 }
191
192 /// Seek to an absolute file offset.
193 ///
194 /// ### Errors
195 /// - `EBADF`: The file descriptor is not seekable.
196 /// - `EINVAL`: `offset` is invalid.
197 /// - `EOVERFLOW`: The resulting offset exceeds the off_t range.
198 pub fn seek_set(&self, offset: i64) -> Result<u64, CoreError> {
199 loop {
200 let pos = unsafe { libc::lseek(self.0, offset as libc::off_t, libc::SEEK_SET) };
201 if pos < 0 {
202 let e = errno();
203 if e == libc::EINTR {
204 continue;
205 }
206 return Err(CoreError::sys(e, "lseek"));
207 }
208 return Ok(pos as u64);
209 }
210 }
211
212 /// Write bytes from a slice.
213 ///
214 /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
215 ///
216 /// ### Edge Cases
217 /// - **Zero-length write**: Returns `Ok(Some(0))` immediately.
218 /// - **Partial write**: Returns the number of bytes actually written.
219 ///
220 /// ### Errors
221 /// - `EBADF`: The file descriptor is invalid or not open for writing.
222 /// - `EFAULT`: `buf` points outside the process's address space.
223 /// - `EPIPE`: The reading end of a pipe or socket was closed.
224 pub fn write_slice(&self, buf: &[u8]) -> Result<Option<usize>, CoreError> {
225 self.write_raw(buf.as_ptr(), buf.len())
226 }
227
228 /// Read a native-endian `u64`, blocking until data is available.
229 ///
230 /// Unlike `read_u64`, this never returns `Ok(None)` — it retries on `EINTR`
231 /// and returns `Err` only on a hard I/O failure. Intended for **blocking**
232 /// eventfds used as inter-thread notification primitives.
233 ///
234 /// CORE-M10: [`Fd::eventfd`] always creates a *non-blocking* descriptor
235 /// (`EFD_NONBLOCK`); on such an fd this returns `Err(EAGAIN)` when empty.
236 /// This method's contract assumes a genuinely blocking fd (e.g. the
237 /// `libc::eventfd` in `task_stack.rs`), and it bounds the EINTR retry so a
238 /// signal storm cannot spin the calling thread forever.
239 pub fn read_u64_blocking(&self) -> Result<u64, CoreError> {
240 let mut bytes = [0u8; std::mem::size_of::<u64>()];
241 let mut eintr_retries = 0;
242 loop {
243 let n =
244 unsafe { libc::read(self.0, bytes.as_mut_ptr() as *mut libc::c_void, bytes.len()) };
245 if n == bytes.len() as isize {
246 return Ok(u64::from_ne_bytes(bytes));
247 }
248 if n < 0 {
249 let e = errno();
250 if e == libc::EINTR {
251 eintr_retries += 1;
252 if eintr_retries >= MAX_BLOCKING_READ_EINTR_RETRIES {
253 return Err(CoreError::sys(
254 libc::EINTR,
255 "read_u64_blocking:eintr_exhausted",
256 ));
257 }
258 continue;
259 }
260 return Err(CoreError::sys(e, "read_u64_blocking"));
261 }
262 return Err(CoreError::sys(libc::EIO, "read_u64_blocking:short_read"));
263 }
264 }
265
266 /// Read a native-endian `u64`.
267 ///
268 /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
269 pub fn read_u64(&self) -> Result<Option<u64>, CoreError> {
270 let mut bytes = [0u8; std::mem::size_of::<u64>()];
271 match self.read_slice(&mut bytes)? {
272 Some(n) if n == bytes.len() => Ok(Some(u64::from_ne_bytes(bytes))),
273 Some(_) => Err(CoreError::sys(libc::EIO, "read_u64")),
274 None => Ok(None),
275 }
276 }
277
278 /// Write a native-endian `u64`.
279 ///
280 /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
281 pub fn write_u64(&self, value: u64) -> Result<Option<usize>, CoreError> {
282 self.write_slice(&value.to_ne_bytes())
283 }
284
285 /// Arm or disarm a one-shot `timerfd`.
286 ///
287 /// Passing `None` disarms the timer. Zero durations are rounded up to one
288 /// nanosecond so the timer still expires.
289 ///
290 /// ### Errors
291 /// - `EBADF`: The file descriptor is invalid.
292 /// - `EINVAL`: The duration is invalid or not supported by the kernel.
293 pub fn set_timer_oneshot(&self, delay: Option<Duration>) -> Result<(), CoreError> {
294 let mut spec: libc::itimerspec = unsafe { std::mem::zeroed() };
295 if let Some(delay) = delay {
296 let delay = delay.max(Duration::from_nanos(1));
297 spec.it_value.tv_sec = delay.as_secs() as libc::time_t;
298 spec.it_value.tv_nsec = delay.subsec_nanos() as libc::c_long;
299 }
300
301 let ret = unsafe { libc::timerfd_settime(self.raw(), 0, &spec, std::ptr::null_mut()) };
302 syscall_ret(ret, "timerfd_settime")
303 }
304
305 /// Read bytes into a raw buffer.
306 ///
307 /// Internal callers must ensure `buf` points to a valid writable region of
308 /// at least `count` bytes.
309 ///
310 /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
311 pub(crate) fn read_raw(&self, buf: *mut u8, count: usize) -> Result<Option<usize>, CoreError> {
312 loop {
313 let n = unsafe { libc::read(self.0, buf as *mut libc::c_void, count) };
314 if n < 0 {
315 let e = errno();
316 if e == libc::EINTR {
317 continue;
318 }
319 if e == libc::EAGAIN || e == libc::EWOULDBLOCK {
320 return Ok(None);
321 }
322 return Err(CoreError::sys(e, "read"));
323 }
324 return Ok(Some(n as usize));
325 }
326 }
327
328 /// Write bytes from a raw buffer.
329 ///
330 /// Internal callers must ensure `buf` points to a valid readable region of
331 /// at least `count` bytes.
332 ///
333 /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
334 pub(crate) fn write_raw(
335 &self,
336 buf: *const u8,
337 count: usize,
338 ) -> Result<Option<usize>, CoreError> {
339 loop {
340 let n = unsafe { libc::write(self.0, buf as *const libc::c_void, count) };
341 if n < 0 {
342 let e = errno();
343 if e == libc::EINTR {
344 continue;
345 }
346 if e == libc::EAGAIN || e == libc::EWOULDBLOCK {
347 return Ok(None);
348 }
349 return Err(CoreError::sys(e, "write"));
350 }
351 return Ok(Some(n as usize));
352 }
353 }
354}
355
356impl Drop for Fd {
357 fn drop(&mut self) {
358 if self.0 >= 0 {
359 unsafe {
360 libc::close(self.0);
361 }
362 }
363 }
364}
365
366/// An opaque token representing a registered file descriptor.
367#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
368pub struct Token(pub(crate) u64);
369
370#[allow(dead_code)]
371impl Token {
372 #[inline(always)]
373 pub(crate) fn new(val: u64) -> Self {
374 Self(val)
375 }
376
377 #[inline(always)]
378 pub(crate) fn val(&self) -> u64 {
379 self.0
380 }
381}
382
383/// A readiness event generated by the reactor.
384#[derive(Clone, Copy, Debug)]
385pub struct Event {
386 /// Token associated with the ready descriptor.
387 pub token: Token,
388 /// Descriptor is ready for reading (`EPOLLIN`).
389 pub readable: bool,
390 /// Descriptor has priority data or an exceptional condition (`EPOLLPRI`).
391 pub priority: bool,
392 /// Descriptor is ready for writing (`EPOLLOUT`).
393 pub writable: bool,
394 /// Indicates an error condition (`EPOLLERR`).
395 ///
396 /// NOTE: For edge-triggered readiness, an error condition often means both
397 /// readable and writable are set to ensure the handler drains the FD.
398 pub error: bool,
399 /// Indicates a remote hangup (`EPOLLHUP`).
400 pub hangup: bool,
401}
402
403const _: () = assert!(std::mem::size_of::<Event>() == 16);
404const _: () = assert!(std::mem::align_of::<Event>() == 8);
405
406/// CORE-M10: cap on consecutive `EINTR` retries inside
407/// [`read_u64_blocking`](Fd::read_u64_blocking). A real signal storm would
408/// otherwise spin the caller forever; this turns a pathological storm into a
409/// surfaced error while remaining generous enough for legitimate interruption.
410const MAX_BLOCKING_READ_EINTR_RETRIES: u32 = 10_000;