air_sys_syscall/futex.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 `futex` — blocage/réveil noyau sur un mot 32 bits (`futex(2)`).
6//!
7//! Ajoutée par **ADR-048** (descellement unique de la couche 0 : `std`-free +
8//! `futex(2)`, re-sceau `couche-0-v1.6`) pour fournir la primitive de
9//! synchronisation `std`-free dont dépendent la libc Air (`air-thread`, couche 1)
10//! **et** le mutex interne de l'état io_uring partagé de la couche 0 (qui remplace
11//! ainsi son dernier `std::sync::Mutex`). Cf. `docs/specs/layer-0/family-futex.md`.
12//!
13//! Doctrine « kernel = bible », alignée sur la famille `poll` (ADR-044) :
14//!
15//! - **`futex(2)` classique** (`SYS_futex`) sur un [`AtomicU32`] de l'appelant —
16//! distinct du futex *via io_uring* (trop lourd pour un `Mutex` basique, cf.
17//! ADR-048 alternatives écartées).
18//! - **Une fonction typée par opération** (ADR-021 §3) : [`futex_wait`] et
19//! [`futex_wake`]. Aucun multiplexeur `futex(op, …)` exposé ; l'`op` brut reste
20//! un détail interne du wrapper asm.
21//! - **`EINTR` remonté, jamais de retry** (ADR-021 §2) : une attente interrompue
22//! par un signal renvoie [`Errno::EINTR`] **tel quel** ; la reprise (échéance
23//! recalculée) vit en couche 1. Le wrapper ne contient **aucune** boucle.
24//! - **Portée et compte typés** (pas de drapeau brut) : [`FutexScope`] (privé par
25//! défaut) et [`FutexWakeCount`] (borné à `INT_MAX` par construction).
26//! - **Pas d'allocation** (Principe 4) : le mot est emprunté (`&AtomicU32`), le
27//! timeout est un `KernelTimespec` de pile (réutilisé de `poll`). Errno =
28//! `NonZeroI32`.
29
30use core::num::NonZeroI32;
31use core::sync::atomic::AtomicU32;
32use core::time::Duration;
33
34use air_sys_types::{Errno, FutexScope, FutexWakeCount};
35
36use crate::poll::{KernelTimespec, kernel_timespec_from_duration_clamped};
37
38/// `FUTEX_WAIT` (opération 0) : attend tant que `*uaddr == val`.
39const FUTEX_WAIT: u32 = 0;
40/// `FUTEX_WAKE` (opération 1) : réveille jusqu'à `val` waiters.
41const FUTEX_WAKE: u32 = 1;
42/// `FUTEX_PRIVATE_FLAG` (`0x80` = 128) : restreint le futex au processus courant
43/// (clé basée sur l'adresse virtuelle, pas l'inode du mapping) — plus rapide.
44const FUTEX_PRIVATE_FLAG: u32 = 128;
45
46/// Compose le code d'opération `futex(2)` à partir de l'opération de base et de la
47/// portée typée. Détail **interne** : la surface publique reste deux fonctions
48/// dédiées (ADR-021 §3), jamais un `op` exposé.
49const fn futex_op(base: u32, scope: FutexScope) -> u32 {
50 match scope {
51 // `base | FUTEX_PRIVATE_FLAG` : `bitor`, pas d'arithmétique (aucun overflow).
52 FutexScope::Private => base | FUTEX_PRIVATE_FLAG,
53 FutexScope::Shared => base,
54 }
55}
56
57/// `futex(2)` `FUTEX_WAIT` : bloque le thread courant tant que `word == expected`.
58///
59/// L'appelant fournit un mot futex [`AtomicU32`] et la valeur `expected` qu'il
60/// **vient** d'observer. Le noyau revérifie `*word == expected` **atomiquement**
61/// avant de s'endormir : si la valeur a déjà changé entre la lecture de l'appelant
62/// et l'entrée dans le syscall, l'attente est inutile et renvoie immédiatement
63/// [`Errno::EAGAIN`] (c'est l'élimination de la course « réveil perdu »).
64///
65/// - `timeout = None` → attente **indéfinie** (timespec `NULL`).
66/// - `timeout = Some(d)` → attente bornée **relative** (`FUTEX_WAIT` mesure contre
67/// `CLOCK_MONOTONIC`) ; [`Duration::ZERO`] = sondage immédiat.
68/// - `scope` → [`FutexScope::Private`] (défaut, intra-processus) ou
69/// [`FutexScope::Shared`] (mémoire partagée). Doit être **cohérent** avec le
70/// `futex_wake` correspondant.
71///
72/// Retour : `Ok(())` quand le thread a été **réveillé** par un `futex_wake` (ou un
73/// réveil spurious — l'appelant relit toujours `*word` après retour).
74///
75/// # Errors
76///
77/// - [`Errno::EAGAIN`] : `*word != expected` au moment de l'appel — **aucune
78/// attente** n'a eu lieu (réveil perdu évité).
79/// - [`Errno::EINTR`] : un signal a interrompu l'attente — **remonté tel quel**
80/// (ADR-021 §2), aucune reprise automatique en couche 0.
81/// - [`Errno::ETIMEDOUT`] : l'échéance `timeout` a expiré avant tout réveil.
82/// - [`Errno::EFAULT`] : pointeur invalide — **inatteignable** via cette API safe
83/// (`word` est une référence vivante, `timeout` un local vivant ou `NULL`).
84pub fn futex_wait(
85 word: &AtomicU32,
86 expected: u32,
87 timeout: Option<Duration>,
88 scope: FutexScope,
89) -> Result<(), Errno> {
90 // Échéance : `None` → pointeur NULL (attente infinie) ; `Some` → adresse d'un
91 // `KernelTimespec` local vivant pour toute la durée de l'appel (réutilisé de la
92 // famille `poll` : même `repr(C)`, même conversion défensive — Principe 2).
93 let ts = timeout.map(kernel_timespec_from_duration_clamped);
94 let timeout_ptr: u64 = match &ts {
95 Some(t) => {
96 let p: *const KernelTimespec = t;
97 p as u64
98 }
99 None => 0,
100 };
101
102 let op = futex_op(FUTEX_WAIT, scope);
103 let uaddr = word.as_ptr() as u64;
104
105 // SAFETY: SYS_futex / FUTEX_WAIT. `uaddr` pointe l'`AtomicU32` emprunté par
106 // l'appelant (vivant pour la durée de l'appel) ; le noyau y **lit** 4 octets
107 // (comparaison à `expected`), n'écrit rien dans la mémoire utilisateur.
108 // `timeout_ptr` pointe un `KernelTimespec` local vivant (16 octets, lu seul si
109 // non-NULL) ou vaut 0 (NULL = attente infinie). `op`/`expected` sont des scalaires
110 // passés par registre. Aucun pointeur ne survit à l'appel.
111 let ret = unsafe { raw_syscall_futex_wait(uaddr, op, expected, timeout_ptr) };
112
113 // COUVERTURE (ADR-035 — cf. docs/COVERAGE-EXCEPTIONS.md, section `futex`). Le bras
114 // d'erreur est **couvert** par `EAGAIN` (`*word != expected`, déterministe) et
115 // `ETIMEDOUT` (échéance courte, déterministe). `EINTR` (signal pendant l'attente)
116 // partage ce même bras mais n'est pas injectable de façon déterministe en CI sans
117 // trampoline de restorer de signal x86_64 (cf. précédent `ppoll`) — il est donc
118 // documenté STRUCTURAL sans test dédié ; le décodage d'errno lui-même est couvert
119 // par `errno_from_negative_syscall_ret_maps_known`.
120 if ret < 0 {
121 return Err(errno_from_negative_syscall_ret(ret));
122 }
123 Ok(())
124}
125
126/// `futex(2)` `FUTEX_WAKE` : réveille jusqu'à `count` waiters bloqués sur `word`.
127///
128/// - `word` : le **même** mot futex que celui sur lequel les waiters se sont
129/// endormis (le noyau identifie la file d'attente par l'adresse).
130/// - `count` : [`FutexWakeCount`] — typiquement [`FutexWakeCount::ONE`] (relâche de
131/// `Mutex`) ou [`FutexWakeCount::ALL`] (`Condvar::notify_all`). Borné à `INT_MAX`
132/// par construction.
133/// - `scope` : doit correspondre à celui des `futex_wait` ciblés.
134///
135/// L'appelant publie en général la nouvelle valeur dans `word` (`store`) **avant**
136/// d'appeler `futex_wake`, pour que les waiters relisant `*word` voient l'état à jour.
137///
138/// Retour : le **nombre de waiters effectivement réveillés** (`0` si aucun n'était
139/// en attente — ce n'est pas une erreur).
140///
141/// # Errors
142///
143/// - [`Errno::EFAULT`] : pointeur invalide — **inatteignable** via cette API safe
144/// (`word` est une référence vivante). `FUTEX_WAKE` ne lit ni n'écrit le contenu
145/// du mot (clé d'adresse uniquement) et ne prend aucun timeout.
146pub fn futex_wake(
147 word: &AtomicU32,
148 count: FutexWakeCount,
149 scope: FutexScope,
150) -> Result<u32, Errno> {
151 let op = futex_op(FUTEX_WAKE, scope);
152 let uaddr = word.as_ptr() as u64;
153 // `count.get()` est garanti `≤ INT_MAX` par construction (newtype) : le `val`
154 // passé au noyau reste un `int` positif, sans conversion qui échoue.
155 let val = count.get();
156
157 // SAFETY: SYS_futex / FUTEX_WAKE. `uaddr` pointe l'`AtomicU32` emprunté par
158 // l'appelant (vivant) ; `FUTEX_WAKE` n'utilise que l'**adresse** comme clé de file
159 // — aucune lecture ni écriture du contenu du mot, aucun pointeur `timeout`.
160 // `op`/`val` sont des scalaires passés par registre. Aucun pointeur ne survit.
161 let ret = unsafe { raw_syscall_futex_wake(uaddr, op, val) };
162
163 // COUVERTURE (ADR-035, STRUCTURAL — cf. docs/COVERAGE-EXCEPTIONS.md, section
164 // `futex`). `FUTEX_WAKE` sur un `&AtomicU32` valide ne peut échouer que par
165 // `EFAULT` (pointeur invalide — exclu par construction) : le bras `ret < 0` est
166 // inatteignable via cette API safe. Le chemin succès (compte de réveils) est
167 // couvert par le test multi-thread.
168 if ret < 0 {
169 return Err(errno_from_negative_syscall_ret(ret));
170 }
171 // `ret ≥ 0` est le nombre de waiters réveillés (`≤` nombre de waiters, lui-même
172 // borné par les threads vivants) : conversion défensive `i64 → u32` (Principe 2,
173 // pas d'`as`). Le bras d'erreur est inatteignable (`ret` tient toujours dans un
174 // `u32`) — STRUCTURAL (cf. docs/COVERAGE-EXCEPTIONS.md).
175 u32::try_from(ret).map_err(|_| Errno::EINVAL)
176}
177
178// ─────────────────────────────────────────────────────────────────────────
179// Helper commun : conversion d'une valeur de retour syscall négative en Errno.
180// ─────────────────────────────────────────────────────────────────────────
181
182fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
183 debug_assert!(ret < 0 && ret > -4096);
184 #[allow(clippy::cast_possible_truncation)]
185 let raw = ret.wrapping_neg() as i32;
186 let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
187 Errno::from_nonzero(nz)
188}
189
190// ─────────────────────────────────────────────────────────────────────────
191// asm! wrappers x86_64 / aarch64 — une fonction dédiée par opération.
192// Signature noyau : futex(uaddr, op, val, timeout, uaddr2, val3).
193// FUTEX_WAIT utilise `timeout` ; uaddr2/val3 sont nuls (non-PI, non-REQUEUE).
194// FUTEX_WAKE n'utilise que uaddr/op/val.
195// ─────────────────────────────────────────────────────────────────────────
196
197#[cfg(target_arch = "x86_64")]
198#[inline]
199unsafe fn raw_syscall_futex_wait(uaddr: u64, op: u32, val: u32, timeout: u64) -> i64 {
200 let ret: i64;
201 // SAFETY: SYS_futex (x86_64 = 202). Le noyau **lit** 4 octets à `uaddr`
202 // (comparaison à `val`) et 16 octets à `timeout` si non-NULL ; aucune écriture
203 // mémoire utilisateur → `readonly` valide. `uaddr2`/`val3` sont nuls.
204 unsafe {
205 core::arch::asm!(
206 "syscall",
207 in("rax") 202_i64,
208 in("rdi") uaddr,
209 in("rsi") u64::from(op),
210 in("rdx") u64::from(val),
211 in("r10") timeout,
212 in("r8") 0_u64,
213 in("r9") 0_u64,
214 lateout("rax") ret,
215 lateout("rcx") _,
216 lateout("r11") _,
217 options(nostack, preserves_flags, readonly),
218 );
219 }
220 ret
221}
222
223#[cfg(target_arch = "aarch64")]
224#[inline]
225unsafe fn raw_syscall_futex_wait(uaddr: u64, op: u32, val: u32, timeout: u64) -> i64 {
226 let ret: i64;
227 // SAFETY: SYS_futex (aarch64 = 98). Mêmes accès mémoire que sur x86_64 : lecture
228 // seule de `*uaddr` (4 octets) et `*timeout` (16 octets, si non-NULL).
229 unsafe {
230 core::arch::asm!(
231 "svc 0",
232 in("x8") 98_i64,
233 inout("x0") uaddr => ret,
234 in("x1") u64::from(op),
235 in("x2") u64::from(val),
236 in("x3") timeout,
237 in("x4") 0_u64,
238 in("x5") 0_u64,
239 options(nostack, preserves_flags, readonly),
240 );
241 }
242 ret
243}
244
245#[cfg(target_arch = "x86_64")]
246#[inline]
247unsafe fn raw_syscall_futex_wake(uaddr: u64, op: u32, val: u32) -> i64 {
248 let ret: i64;
249 // SAFETY: SYS_futex (x86_64 = 202). `FUTEX_WAKE` n'accède **pas** au contenu de
250 // `*uaddr` (clé d'adresse uniquement) : aucun accès mémoire utilisateur observable
251 // → `nomem` valide. `timeout`/`uaddr2`/`val3` sont nuls (ignorés par `FUTEX_WAKE`).
252 unsafe {
253 core::arch::asm!(
254 "syscall",
255 in("rax") 202_i64,
256 in("rdi") uaddr,
257 in("rsi") u64::from(op),
258 in("rdx") u64::from(val),
259 in("r10") 0_u64,
260 in("r8") 0_u64,
261 in("r9") 0_u64,
262 lateout("rax") ret,
263 lateout("rcx") _,
264 lateout("r11") _,
265 options(nostack, preserves_flags, nomem),
266 );
267 }
268 ret
269}
270
271#[cfg(target_arch = "aarch64")]
272#[inline]
273unsafe fn raw_syscall_futex_wake(uaddr: u64, op: u32, val: u32) -> i64 {
274 let ret: i64;
275 // SAFETY: SYS_futex (aarch64 = 98). Comme sur x86_64 : `FUTEX_WAKE` n'accède pas
276 // au contenu mémoire utilisateur (clé d'adresse) → `nomem` valide.
277 unsafe {
278 core::arch::asm!(
279 "svc 0",
280 in("x8") 98_i64,
281 inout("x0") uaddr => ret,
282 in("x1") u64::from(op),
283 in("x2") u64::from(val),
284 in("x3") 0_u64,
285 in("x4") 0_u64,
286 in("x5") 0_u64,
287 options(nostack, preserves_flags, nomem),
288 );
289 }
290 ret
291}
292
293#[cfg(all(test, target_os = "linux", not(loom)))]
294mod tests {
295 use super::{FUTEX_PRIVATE_FLAG, FUTEX_WAIT, FUTEX_WAKE, futex_op, futex_wait, futex_wake};
296 use air_sys_types::{Errno, FutexScope, FutexWakeCount};
297 use core::num::NonZeroI32;
298 use core::sync::atomic::{AtomicU32, Ordering};
299 use core::time::Duration;
300 use std::sync::Arc;
301 use std::thread;
302
303 /// `futex_wait` revient immédiatement par `EAGAIN` si la valeur observée ne
304 /// correspond plus — aucune attente, aucun blocage (course « réveil perdu »).
305 #[test]
306 fn futex_wait_eagain_on_value_mismatch() {
307 let word = AtomicU32::new(1);
308 // On prétend attendre la valeur 0 alors que `word == 1` : EAGAIN immédiat.
309 let err = futex_wait(&word, 0, None, FutexScope::Private).unwrap_err();
310 assert_eq!(err, Errno::EAGAIN);
311 }
312
313 /// `futex_wait` avec une portée `Shared` exerce l'autre bras de `futex_op` et
314 /// renvoie aussi `EAGAIN` sur valeur divergente (mot privé à la pile du test,
315 /// mais le drapeau de portée est bien transmis sans drapeau brut).
316 #[test]
317 fn futex_wait_shared_scope_eagain_on_mismatch() {
318 let word = AtomicU32::new(42);
319 let err = futex_wait(&word, 7, None, FutexScope::Shared).unwrap_err();
320 assert_eq!(err, Errno::EAGAIN);
321 }
322
323 /// `futex_wait` avec un court timeout sur un mot qui ne change jamais expire en
324 /// `ETIMEDOUT` (échéance relative `CLOCK_MONOTONIC`).
325 #[test]
326 fn futex_wait_times_out() {
327 let word = AtomicU32::new(0);
328 let err = futex_wait(
329 &word,
330 0,
331 Some(Duration::from_millis(20)),
332 FutexScope::Private,
333 )
334 .unwrap_err();
335 assert_eq!(err, Errno::ETIMEDOUT);
336 }
337
338 /// `Duration::ZERO` = sondage immédiat : un mot inchangé expire aussitôt.
339 #[test]
340 fn futex_wait_zero_timeout_polls_immediately() {
341 let word = AtomicU32::new(5);
342 let err = futex_wait(&word, 5, Some(Duration::ZERO), FutexScope::Private).unwrap_err();
343 assert_eq!(err, Errno::ETIMEDOUT);
344 }
345
346 /// `futex_wake` sur un mot sans aucun waiter réveille `0` thread — ce n'est pas
347 /// une erreur (chemin succès du décodage du compte).
348 #[test]
349 fn futex_wake_without_waiters_returns_zero() {
350 let word = AtomicU32::new(0);
351 let woken = futex_wake(&word, FutexWakeCount::ALL, FutexScope::Private).expect("wake");
352 assert_eq!(woken, 0);
353 }
354
355 /// Rendez-vous réel : un thread `futex_wait` (timeout infini) sur un mot, le
356 /// thread principal change la valeur puis `futex_wake` → le waiter se réveille,
357 /// et `futex_wake` rapporte **exactement 1** réveil.
358 #[test]
359 fn futex_wake_releases_one_waiter() {
360 let word = Arc::new(AtomicU32::new(0));
361 let word_waiter = Arc::clone(&word);
362
363 let waiter = thread::spawn(move || {
364 // Boucle de mutex idiomatique : tant que la valeur vaut 0, on attend.
365 // `futex_wait` peut renvoyer `Ok(())` (réveil) ou `EAGAIN` (la valeur a
366 // déjà changé) ; les deux sortent quand `word != 0`. Aucun retry sur
367 // `EINTR` ici n'est nécessaire : le test ne s'envoie pas de signal.
368 loop {
369 if word_waiter.load(Ordering::Acquire) != 0 {
370 return;
371 }
372 match futex_wait(&word_waiter, 0, None, FutexScope::Private) {
373 Ok(()) | Err(Errno::EAGAIN) => {}
374 Err(other) => panic!("futex_wait inattendu : {other:?}"),
375 }
376 }
377 });
378
379 // Laisse le waiter atteindre `futex_wait` (réduit la fenêtre où la valeur
380 // change avant l'entrée dans le syscall — non requis pour la justesse).
381 thread::sleep(Duration::from_millis(50));
382 word.store(1, Ordering::Release);
383
384 // Réveille jusqu'à tous les waiters ; il y en a exactement un. On reboucle
385 // tant que le compte est 0 (le waiter peut ne pas encore être endormi).
386 let mut total_woken = 0_u32;
387 while total_woken == 0 {
388 total_woken =
389 futex_wake(&word, FutexWakeCount::ALL, FutexScope::Private).expect("wake");
390 if total_woken == 0 {
391 thread::sleep(Duration::from_millis(5));
392 }
393 }
394 assert_eq!(total_woken, 1, "un seul waiter doit être réveillé");
395 waiter.join().expect("join waiter");
396 }
397
398 /// Exerce explicitement `futex_op` (les deux portées, les deux opérations de
399 /// base) — le drapeau privé n'est posé que pour `Private`.
400 #[test]
401 fn futex_op_composes_private_flag() {
402 assert_eq!(
403 futex_op(FUTEX_WAIT, FutexScope::Private),
404 FUTEX_WAIT | FUTEX_PRIVATE_FLAG
405 );
406 assert_eq!(futex_op(FUTEX_WAIT, FutexScope::Shared), FUTEX_WAIT);
407 assert_eq!(
408 futex_op(FUTEX_WAKE, FutexScope::Private),
409 FUTEX_WAKE | FUTEX_PRIVATE_FLAG
410 );
411 assert_eq!(futex_op(FUTEX_WAKE, FutexScope::Shared), FUTEX_WAKE);
412 }
413
414 #[test]
415 fn errno_from_negative_syscall_ret_maps_known() {
416 assert_eq!(super::errno_from_negative_syscall_ret(-11), Errno::EAGAIN);
417 assert_eq!(
418 super::errno_from_negative_syscall_ret(-110),
419 Errno::ETIMEDOUT
420 );
421 // Cohérence du décodage : -EINTR (4) → Errno::EINTR (remonté sans retry).
422 let nz = NonZeroI32::new(4).unwrap();
423 assert_eq!(Errno::from_nonzero(nz), Errno::EINTR);
424 }
425
426 // Couvrent les deux opérandes du `debug_assert!(ret < 0 && ret > -4096)` de
427 // `errno_from_negative_syscall_ret` (`ret = 0` ; `ret = -5000`).
428 #[test]
429 #[should_panic(expected = "ret < 0 && ret > -4096")]
430 fn errno_from_negative_syscall_ret_panics_on_non_negative() {
431 let _ = super::errno_from_negative_syscall_ret(0);
432 }
433
434 #[test]
435 #[should_panic(expected = "ret < 0 && ret > -4096")]
436 fn errno_from_negative_syscall_ret_panics_below_errno_range() {
437 let _ = super::errno_from_negative_syscall_ret(-5000);
438 }
439}