Skip to main content

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}