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}