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;