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;