Skip to main content

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;