Skip to main content

air_sys_syscall/
poll.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//! Famille `poll` — attente synchrone bornée et interruptible (`ppoll(2)`).
6//!
7//! Ajoutée par **ADR-044** (RFC d'extension d'une couche scellée, re-sceau
8//! `couche-0-v1.5`) pour débloquer l'attente bornée/interruptible de
9//! `air-process` **sans tirer io_uring** dans la couche 1. Cf.
10//! `docs/specs/layer-0/family-poll.md`.
11//!
12//! Caractéristiques (doctrine « kernel = bible ») :
13//!
14//! - **`ppoll` uniquement** : précision nanoseconde (aligne [`Duration`]),
15//!   `timeout` non muté au retour, masque de signaux atomique — la variante
16//!   moderne (ADR-021).
17//! - **`EINTR` remonté, jamais de retry** (ADR-021 §2) : la reprise (échéance
18//!   recalculée) vit en couche 1.
19//! - **`PollFd` emprunté (+0)** : la slice de l'appelant est passée au kernel
20//!   sans copie ni allocation (Principe 4) ; les `revents` sont restitués
21//!   intégralement (ADR-032).
22//! - **Conversion d'échéance défensive** (Principe 2) : `tv_sec` clampé à
23//!   `i64::MAX` en cas de débordement, jamais d'`as` lossy.
24
25use core::num::NonZeroI32;
26use core::time::Duration;
27
28use air_sys_types::{Errno, PollFd, SignalMask};
29
30/// Taille du `sigset_t` kernel (1 mot de 64 bits sur x86_64/aarch64, `_NSIG = 64`).
31const SIGSET_SIZE_U64: u64 = 8;
32
33/// Miroir `#[repr(C)]` de `struct timespec` kernel (`__kernel_timespec`).
34///
35/// `pub(crate)` : **réutilisé tel quel** par la famille `futex` (ADR-048) pour le
36/// timeout relatif de `futex_wait` — même `repr`, même conversion défensive.
37#[repr(C)]
38#[derive(Debug, Clone, Copy)]
39pub(crate) struct KernelTimespec {
40    tv_sec: i64,
41    tv_nsec: i64,
42}
43
44/// `ppoll(2)`. Attend un événement sur l'un des `fds` jusqu'à `timeout`.
45///
46/// - `timeout = None`     → attente **indéfinie** (timespec `NULL`).
47/// - `timeout = Some(d)`  → attente bornée ; [`Duration::ZERO`] = **sondage** non
48///   bloquant.
49/// - `sigmask = None`     → `NULL` (aucune substitution de masque ; réservé à un
50///   usage futur).
51///
52/// Au retour, chaque [`PollFd`] de `fds` porte ses `revents` remplis par le
53/// kernel (vide si le descripteur n'est pas prêt).
54///
55/// Retour : **nombre de fd** dont `revents` est non vide ; **`0` = expiration**.
56///
57/// # Errors
58///
59/// - [`Errno::EINTR`] : un signal a interrompu l'attente — **remonté tel quel**
60///   (ADR-021 §2), aucune reprise automatique en couche 0.
61/// - [`Errno::EINVAL`] : `timespec` invalide — impossible via la conversion
62///   défensive interne (`tv_nsec < 1e9`, `tv_sec ≥ 0`).
63/// - [`Errno::EFAULT`] : pointeur invalide — inatteignable via cette API safe
64///   (la slice et le masque proviennent de références vivantes).
65pub fn ppoll(
66    fds: &mut [PollFd<'_>],
67    timeout: Option<Duration>,
68    sigmask: Option<&SignalMask>,
69) -> Result<usize, Errno> {
70    // Échéance : `None` → pointeur NULL (attente infinie) ; `Some` → adresse d'un
71    // `KernelTimespec` local vivant pour toute la durée de l'appel.
72    let ts = timeout.map(kernel_timespec_from_duration_clamped);
73    let timeout_ptr: u64 = match &ts {
74        Some(t) => {
75            let p: *const KernelTimespec = t;
76            p as u64
77        }
78        None => 0,
79    };
80
81    // Masque de signaux : `None` → NULL ; `Some` → adresse d'un mot `u64` local.
82    let mask_bits: u64 = sigmask.map_or(0, |m| m.bits());
83    let mask_ptr: u64 = match sigmask {
84        Some(_) => {
85            let p: *const u64 = &mask_bits;
86            p as u64
87        }
88        None => 0,
89    };
90
91    let nfds = fds.len() as u64;
92    let fds_ptr = fds.as_mut_ptr() as u64;
93
94    // SAFETY: `fds` est une slice vivante de `PollFd` (`#[repr(C)]` bit-pour-bit
95    // `struct pollfd`) ; le kernel lit `fd`/`events` et écrit `revents` de chacune
96    // des `nfds` entrées — accès borné à la slice possédée par l'appelant.
97    // `timeout_ptr` pointe un `KernelTimespec` local vivant (ou NULL) ; `mask_ptr`
98    // pointe un `u64` local vivant (ou NULL), `sigsetsize = 8` = taille kernel du
99    // `sigset_t`. Aucun pointeur ne survit à l'appel.
100    let ret = unsafe { raw_syscall_ppoll(fds_ptr, nfds, timeout_ptr, mask_ptr, SIGSET_SIZE_U64) };
101
102    // COUVERTURE (ADR-035, STRUCTURAL — cf. docs/COVERAGE-EXCEPTIONS.md, section
103    // `poll`). Le bras d'erreur n'est atteignable que par `EINTR` (signal pendant
104    // l'attente — non injectable de façon déterministe en CI, comme le précédent
105    // `waitid`), `EFAULT` (pointeur invalide — exclu : slice/masque vivants par
106    // construction) ou `EINVAL` (timespec invalide — exclu par la conversion
107    // défensive : `tv_nsec < 1e9`, `tv_sec ≥ 0`). Le décodage d'errno lui-même est
108    // couvert par `errno_from_negative_syscall_ret_maps_eintr`.
109    if ret < 0 {
110        return Err(errno_from_negative_syscall_ret(ret));
111    }
112    // `ret ≥ 0` est le nombre de fd prêts (≤ nfds) : conversion `i64 → usize`
113    // défensive (Principe 2, pas d'`as`). Inatteignable en erreur : `ret` est
114    // borné par `nfds` qui provient d'un `usize`.
115    usize::try_from(ret).map_err(|_| Errno::EINVAL)
116}
117
118/// Conversion `Duration` → `KernelTimespec` **défensive** (Principe 2).
119///
120/// - `tv_sec` = `i64::try_from(d.as_secs())`, **clampé à `i64::MAX`** en cas de
121///   débordement (« ~infini borné », ≈ 292 milliards d'années) — jamais d'`as`,
122///   jamais de troncature silencieuse.
123/// - `tv_nsec` = `d.subsec_nanos()` (`< 1e9`, conversion `u32 → i64` sûre via
124///   `i64::from`).
125/// - `Duration::ZERO` → `{0, 0}` → sondage immédiat.
126///
127/// `pub(crate)` : **réutilisé tel quel** par la famille `futex` (ADR-048).
128pub(crate) fn kernel_timespec_from_duration_clamped(d: Duration) -> KernelTimespec {
129    let tv_sec = i64::try_from(d.as_secs()).unwrap_or(i64::MAX);
130    KernelTimespec {
131        tv_sec,
132        tv_nsec: i64::from(d.subsec_nanos()),
133    }
134}
135
136// ─────────────────────────────────────────────────────────────────────────
137// Helper commun : conversion d'une valeur de retour syscall négative en Errno.
138// ─────────────────────────────────────────────────────────────────────────
139
140fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
141    debug_assert!(ret < 0 && ret > -4096);
142    #[allow(clippy::cast_possible_truncation)]
143    let raw = ret.wrapping_neg() as i32;
144    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
145    Errno::from_nonzero(nz)
146}
147
148// ─────────────────────────────────────────────────────────────────────────
149// asm! wrappers x86_64 / aarch64.
150// ─────────────────────────────────────────────────────────────────────────
151
152#[cfg(target_arch = "x86_64")]
153#[inline]
154unsafe fn raw_syscall_ppoll(
155    fds: u64,
156    nfds: u64,
157    timeout: u64,
158    sigmask: u64,
159    sigsetsize: u64,
160) -> i64 {
161    let ret: i64;
162    // SAFETY: SYS_ppoll (x86_64 = 271). Le kernel lit `*fds` (nfds × `struct
163    // pollfd`) et y écrit les `revents` ; lit `*timeout` si non-null (16 octets),
164    // lit `*sigmask` si non-null (8 octets). Pas de `readonly` (écriture sur
165    // `fds`).
166    unsafe {
167        core::arch::asm!(
168            "syscall",
169            in("rax") 271_i64,
170            in("rdi") fds,
171            in("rsi") nfds,
172            in("rdx") timeout,
173            in("r10") sigmask,
174            in("r8") sigsetsize,
175            lateout("rax") ret,
176            lateout("rcx") _,
177            lateout("r11") _,
178            options(nostack, preserves_flags),
179        );
180    }
181    ret
182}
183
184#[cfg(target_arch = "aarch64")]
185#[inline]
186unsafe fn raw_syscall_ppoll(
187    fds: u64,
188    nfds: u64,
189    timeout: u64,
190    sigmask: u64,
191    sigsetsize: u64,
192) -> i64 {
193    let ret: i64;
194    // SAFETY: SYS_ppoll (aarch64 = 73). Mêmes accès mémoire que sur x86_64.
195    unsafe {
196        core::arch::asm!(
197            "svc 0",
198            in("x8") 73_i64,
199            inout("x0") fds => ret,
200            in("x1") nfds,
201            in("x2") timeout,
202            in("x3") sigmask,
203            in("x4") sigsetsize,
204            options(nostack, preserves_flags),
205        );
206    }
207    ret
208}
209
210#[cfg(all(test, target_os = "linux", not(loom)))]
211mod tests {
212    use super::{KernelTimespec, kernel_timespec_from_duration_clamped, ppoll};
213    use crate::ipc::pipe2;
214    use air_sys_types::fd::{AsFd, OwnedFd};
215    use air_sys_types::{Errno, PipeFlags, PollEvents, PollFd, SignalMask};
216    use core::num::NonZeroI32;
217    use core::time::Duration;
218
219    /// Crée un pipe `(lecteur, écrivain)` via le wrapper couche 0 `ipc::pipe2`.
220    /// Les `OwnedFd` ferment leurs descripteurs à `Drop` (RAII).
221    fn make_pipe() -> (OwnedFd, OwnedFd) {
222        pipe2(PipeFlags::CLOEXEC).expect("pipe2")
223    }
224
225    /// Écrit un octet sur l'extrémité écriture d'un pipe (réveille `POLLIN`).
226    fn poke(writer: &OwnedFd) {
227        use air_sys_types::fd::AsFd;
228        // Écrit via le wrapper couche 0 (FD emprunté Air) — pas de pont std.
229        crate::fs::write(writer.as_fd(), b"x").expect("write pipe");
230    }
231
232    #[test]
233    fn ppoll_reports_pollin_after_write() {
234        let (reader, writer) = make_pipe();
235        poke(&writer);
236        let mut fds = [PollFd::new(reader.as_fd(), PollEvents::IN)];
237        // Échéance large : le fd est déjà prêt, l'appel revient immédiatement.
238        let n = ppoll(&mut fds, Some(Duration::from_secs(5)), None).expect("ppoll");
239        assert_eq!(n, 1);
240        assert!(fds[0].revents().contains(PollEvents::IN));
241    }
242
243    #[test]
244    fn ppoll_infinite_timeout_returns_on_ready_fd() {
245        // `timeout = None` ⇒ timespec NULL (attente indéfinie). On rend le fd prêt
246        // AVANT l'appel : ppoll revient immédiatement, sans bloquer — ce qui
247        // exerce le bras `None` (pointeur NULL) de façon déterministe.
248        let (reader, writer) = make_pipe();
249        poke(&writer);
250        let mut fds = [PollFd::new(reader.as_fd(), PollEvents::IN)];
251        let n = ppoll(&mut fds, None, None).expect("ppoll");
252        assert_eq!(n, 1);
253        assert!(fds[0].revents().contains(PollEvents::IN));
254    }
255
256    #[test]
257    fn ppoll_times_out_on_silent_pipe() {
258        let (reader, _writer) = make_pipe();
259        let mut fds = [PollFd::new(reader.as_fd(), PollEvents::IN)];
260        // Pipe muet, timeout court : expiration → Ok(0), aucun revents.
261        let n = ppoll(&mut fds, Some(Duration::from_millis(10)), None).expect("ppoll");
262        assert_eq!(n, 0);
263        assert_eq!(fds[0].revents(), PollEvents::empty());
264    }
265
266    #[test]
267    fn ppoll_zero_timeout_is_nonblocking_poll() {
268        let (reader, _writer) = make_pipe();
269        let mut fds = [PollFd::new(reader.as_fd(), PollEvents::IN)];
270        // Sondage non bloquant sur un pipe muet : revient aussitôt avec Ok(0).
271        let n = ppoll(&mut fds, Some(Duration::ZERO), None).expect("ppoll");
272        assert_eq!(n, 0);
273    }
274
275    #[test]
276    fn ppoll_multi_fd_only_ready_one_carries_revents() {
277        let (ready_reader, ready_writer) = make_pipe();
278        let (idle_reader, _idle_writer) = make_pipe();
279        poke(&ready_writer);
280        let mut fds = [
281            PollFd::new(idle_reader.as_fd(), PollEvents::IN),
282            PollFd::new(ready_reader.as_fd(), PollEvents::IN),
283        ];
284        let n = ppoll(&mut fds, Some(Duration::from_secs(5)), None).expect("ppoll");
285        assert_eq!(n, 1, "un seul fd est prêt");
286        // Seul le fd écrit porte un revents non vide (info kernel non masquée).
287        assert_eq!(fds[0].revents(), PollEvents::empty());
288        assert!(fds[1].revents().contains(PollEvents::IN));
289    }
290
291    #[test]
292    fn ppoll_empty_slice_times_out() {
293        // Slice vide (`nfds = 0`) : ppoll dégénère en simple sommeil borné → Ok(0).
294        let mut fds: [PollFd<'_>; 0] = [];
295        let n = ppoll(&mut fds, Some(Duration::ZERO), None).expect("ppoll");
296        assert_eq!(n, 0);
297    }
298
299    #[test]
300    fn ppoll_with_empty_sigmask_still_polls() {
301        // Exerce la branche `sigmask = Some(_)` (pointeur non-null) : un masque
302        // vide ne bloque aucun signal et ne change pas le résultat.
303        let (reader, writer) = make_pipe();
304        poke(&writer);
305        let mut fds = [PollFd::new(reader.as_fd(), PollEvents::IN)];
306        let mask = SignalMask::empty();
307        let n = ppoll(&mut fds, Some(Duration::from_secs(5)), Some(&mask)).expect("ppoll");
308        assert_eq!(n, 1);
309        assert!(fds[0].revents().contains(PollEvents::IN));
310    }
311
312    #[test]
313    fn timespec_conversion_zero_is_immediate() {
314        let ts = kernel_timespec_from_duration_clamped(Duration::ZERO);
315        assert_eq!(ts.tv_sec, 0);
316        assert_eq!(ts.tv_nsec, 0);
317    }
318
319    #[test]
320    fn timespec_conversion_clamps_giant_seconds_to_i64_max() {
321        // `Duration` géante (secondes > i64::MAX) → clamp à i64::MAX, jamais d'`as`.
322        let giant = Duration::new(u64::MAX, 999_999_999);
323        let ts = kernel_timespec_from_duration_clamped(giant);
324        assert_eq!(ts.tv_sec, i64::MAX);
325        assert_eq!(ts.tv_nsec, 999_999_999);
326    }
327
328    #[test]
329    fn timespec_conversion_preserves_small_values() {
330        let ts = kernel_timespec_from_duration_clamped(Duration::new(3, 250));
331        assert_eq!(ts.tv_sec, 3);
332        assert_eq!(ts.tv_nsec, 250);
333    }
334
335    #[test]
336    fn errno_from_negative_syscall_ret_maps_eintr() {
337        assert_eq!(super::errno_from_negative_syscall_ret(-4), Errno::EINTR);
338        // Cohérence du décodage : -EINVAL (22) → Errno::EINVAL.
339        let nz = NonZeroI32::new(22).unwrap();
340        assert_eq!(Errno::from_nonzero(nz), Errno::EINVAL);
341    }
342
343    // Couvrent les deux opérandes du `debug_assert!(ret < 0 && ret > -4096)`
344    // de `errno_from_negative_syscall_ret` (`ret = 0` ; `ret = -5000`).
345    #[test]
346    #[should_panic(expected = "ret < 0 && ret > -4096")]
347    fn errno_from_negative_syscall_ret_panics_on_non_negative() {
348        let _ = super::errno_from_negative_syscall_ret(0);
349    }
350
351    #[test]
352    #[should_panic(expected = "ret < 0 && ret > -4096")]
353    fn errno_from_negative_syscall_ret_panics_below_errno_range() {
354        let _ = super::errno_from_negative_syscall_ret(-5000);
355    }
356
357    proptest::proptest! {
358        /// Property : la conversion `Duration → timespec` ne panique jamais,
359        /// `tv_nsec` reste dans `[0, 1e9)` et `tv_sec` est clampé à `i64::MAX`.
360        #[test]
361        fn timespec_conversion_is_total(secs in 0_u64..=u64::MAX, nanos in 0_u32..1_000_000_000) {
362            let d = Duration::new(secs, nanos);
363            let ts = kernel_timespec_from_duration_clamped(d);
364            proptest::prop_assert!(ts.tv_nsec >= 0 && ts.tv_nsec < 1_000_000_000);
365            proptest::prop_assert_eq!(ts.tv_nsec, i64::from(nanos));
366            let expected = i64::try_from(secs).unwrap_or(i64::MAX);
367            proptest::prop_assert_eq!(ts.tv_sec, expected);
368        }
369    }
370
371    /// `KernelTimespec` est `#[repr(C)]` de 16 octets (`__kernel_timespec`).
372    #[test]
373    fn kernel_timespec_layout() {
374        assert_eq!(core::mem::size_of::<KernelTimespec>(), 16);
375        let ts = KernelTimespec {
376            tv_sec: 1,
377            tv_nsec: 2,
378        };
379        let _ = format!("{ts:?}");
380        let copy = ts;
381        assert_eq!(copy.tv_sec, 1);
382    }
383}