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;