air_sys_syscall/process.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//! Wrappers de la famille `process`.
6//!
7//! Cf. `docs/specs/layer-0/family-process.md`. Couvre l'identité (`getpid`,
8//! `gettid`), la création/attente (`clone3`, `waitid`, `exit_group`), l'`exec`
9//! et la redirection (`execve`, `execveat`, `dup3`, `fchdir`, `chdir`), les
10//! groupes/sessions, `prctl` (opérations individuelles), les rlimits, les
11//! capabilities et l'affinité CPU.
12//!
13//! Les wrappers exécutent les syscalls Linux directement via
14//! `core::arch::asm!` ; la libc n'est pas dans le chemin d'appel
15//! (cf. CLAUDE.md : « variantes modernes préférées, syscalls non wrappés
16//! listés dans UNSUPPORTED.md »).
17
18use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
19use alloc::vec::Vec;
20use core::convert::Infallible;
21use core::ffi::c_char;
22use core::marker::PhantomData;
23use core::num::NonZeroI32;
24
25use alloc::ffi::CString;
26use core::ffi::CStr;
27
28use air_sys_types::fs::{DirFd, Mode};
29use air_sys_types::system::CpuSet;
30use air_sys_types::{
31 Capability, CapabilityMask, CapabilitySet, CapabilityTarget, CloneArgs, CloneFlags,
32 CloneResult, DumpableMode, Dup3Flags, Errno, ExecveatFlags, Gid, Pid, PidFd, PidFdOpenFlags,
33 ResGid, ResUid, Resource, Rlimit, RlimitValue, Rusage, RusageWho, Signal, SignalInfo,
34 StackSpecification, Tid, Uid, WaitEvent, WaitOptions, WaitStatus, WaitTarget,
35};
36
37#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
38compile_error!(
39 "air-sys-syscall ne supporte que x86_64 et aarch64 (cf. ADR-014 catalogue matériel)."
40);
41
42/// Retourne le PID du processus appelant.
43///
44/// Fonction **totale** : le syscall `getpid(2)` ne peut pas échouer et POSIX
45/// garantit que le PID retourné est strictement positif. C'est pourquoi la
46/// signature retourne `Pid` directement, sans `Result`.
47///
48/// # Performance
49///
50/// Très peu coûteux (~30-50 ns via vDSO sur les kernels récents) ; ici on
51/// appelle le syscall en direct sans passer par la vDSO, donc on est du
52/// côté haut de cette fourchette.
53#[must_use]
54pub fn getpid() -> Pid {
55 let raw = raw_syscall_getpid();
56 // POSIX/Linux : `getpid` retourne un `pid_t` (i32) strictement positif.
57 // La troncature de i64 à i32 est intentionnelle ; les 32 bits hauts sont
58 // garantis nuls pour un PID valide.
59 #[allow(clippy::cast_possible_truncation)]
60 let pid_raw = raw as i32;
61 // Construction *vérifiée* du `NonZeroI32` : un `new_unchecked` ici
62 // (Principe 5 : pas d'optimisation avant mesure, et plus grave : si l'asm!
63 // au-dessus est jamais mal câblé, `new_unchecked(0)` est un UB silencieux,
64 // alors qu'un `.expect()` rend la violation d'invariant bruyante et
65 // diagnostiquable). La branche d'échec n'est pas exécutable sur un kernel
66 // POSIX conforme.
67 let nz = NonZeroI32::new(pid_raw)
68 .expect("kernel a violé l'invariant POSIX : getpid doit retourner > 0");
69 Pid::from_nonzero(nz)
70}
71
72/// Retourne le TID du thread appelant.
73///
74/// Fonction **totale** : le syscall `gettid(2)` ne peut pas échouer et le
75/// kernel garantit un TID strictement positif. Sur Linux, chaque thread a un
76/// TID distinct ; le thread principal d'un processus a `gettid() == getpid()`.
77///
78/// Le type de retour [`Tid`] est **distinct** de [`Pid`] pour empêcher par
79/// typage la confusion entre identifiant de thread et identifiant de
80/// processus (cf. ADR-021 convention 1, et Principe d'ingénierie 7).
81#[must_use]
82pub fn gettid() -> Tid {
83 let raw = raw_syscall_gettid();
84 // Même invariant que getpid : TID strictement positif, fits in i32.
85 #[allow(clippy::cast_possible_truncation)]
86 let tid_raw = raw as i32;
87 // Construction *vérifiée* (cf. justification dans `getpid` ci-dessus).
88 let nz = NonZeroI32::new(tid_raw)
89 .expect("kernel a violé l'invariant POSIX : gettid doit retourner > 0");
90 Tid::from_nonzero(nz)
91}
92
93// ── Helpers spécifiques à l'architecture ─────────────────────────────────
94//
95// Une fonction par couple (syscall, architecture). Pas de wrapper générique
96// (cf. convention 3 ADR-021 : pas de wrapper générique pour les syscalls
97// multiplexés ; ici les helpers sont privés et chaque syscall a sa propre
98// fonction dédiée, donc l'esprit de la convention est respecté).
99
100#[cfg(target_arch = "x86_64")]
101#[inline]
102fn raw_syscall_getpid() -> i64 {
103 let ret: i64;
104 // SAFETY:
105 // - SYS_getpid (x86_64 = 39) ne touche à aucune mémoire utilisateur.
106 // - L'ABI syscall x86_64 prend le numéro de syscall dans RAX, retourne
107 // en RAX, et clobbe RCX (RIP de retour) et R11 (RFLAGS sauvegardé).
108 // Les autres registres caller-saved sont préservés.
109 // - `nostack` est correct car le syscall n'utilise pas la pile.
110 // - `readonly` est correct car ce syscall ne modifie aucune mémoire
111 // accessible au programme.
112 // - `preserves_flags` est correct car le kernel restaure RFLAGS via R11
113 // au retour (le clobber explicite de R11 ci-dessus le couvre).
114 unsafe {
115 core::arch::asm!(
116 "syscall",
117 in("rax") 39_i64,
118 lateout("rax") ret,
119 lateout("rcx") _,
120 lateout("r11") _,
121 options(nostack, preserves_flags, readonly),
122 );
123 }
124 ret
125}
126
127#[cfg(target_arch = "x86_64")]
128#[inline]
129fn raw_syscall_gettid() -> i64 {
130 let ret: i64;
131 // SAFETY: identique à `raw_syscall_getpid` ci-dessus, avec SYS_gettid
132 // (x86_64 = 186) à la place. Aucune mémoire utilisateur n'est touchée.
133 unsafe {
134 core::arch::asm!(
135 "syscall",
136 in("rax") 186_i64,
137 lateout("rax") ret,
138 lateout("rcx") _,
139 lateout("r11") _,
140 options(nostack, preserves_flags, readonly),
141 );
142 }
143 ret
144}
145
146#[cfg(target_arch = "aarch64")]
147#[inline]
148fn raw_syscall_getpid() -> i64 {
149 let ret: i64;
150 // SAFETY:
151 // - SYS_getpid (aarch64 = 172) ne touche à aucune mémoire utilisateur.
152 // - L'ABI syscall aarch64 prend le numéro dans X8 et retourne en X0.
153 // Les registres caller-saved en dehors de X0 sont préservés.
154 // - `nostack`, `readonly`, `preserves_flags` : mêmes justifications que
155 // pour la branche x86_64.
156 unsafe {
157 core::arch::asm!(
158 "svc 0",
159 in("x8") 172_i64,
160 lateout("x0") ret,
161 options(nostack, preserves_flags, readonly),
162 );
163 }
164 ret
165}
166
167#[cfg(target_arch = "aarch64")]
168#[inline]
169fn raw_syscall_gettid() -> i64 {
170 let ret: i64;
171 // SAFETY: identique à `raw_syscall_getpid` ci-dessus, avec SYS_gettid
172 // (aarch64 = 178) à la place. Aucune mémoire utilisateur n'est touchée.
173 unsafe {
174 core::arch::asm!(
175 "svc 0",
176 in("x8") 178_i64,
177 lateout("x0") ret,
178 options(nostack, preserves_flags, readonly),
179 );
180 }
181 ret
182}
183
184// ─────────────────────────────────────────────────────────────────────────
185// clone3
186// ─────────────────────────────────────────────────────────────────────────
187
188/// Structure d'arguments transmise au kernel (`struct clone_args` de
189/// `linux/sched.h`). Représente le ABI Linux 5.7+ (88 octets).
190///
191/// Les champs sont des `u64` même quand ils encodent des pointeurs (les
192/// adresses kernel n'utilisent que les premiers 48 bits sur x86_64/aarch64,
193/// mais l'ABI réserve 64 bits pour forward-compat).
194#[repr(C)]
195#[derive(Default)]
196struct KernelCloneArgs {
197 flags: u64,
198 pidfd: u64,
199 child_tid: u64,
200 parent_tid: u64,
201 exit_signal: u64,
202 stack: u64,
203 stack_size: u64,
204 tls: u64,
205 set_tid: u64,
206 set_tid_size: u64,
207 cgroup: u64,
208}
209
210/// Construit la structure ABI kernel à partir de [`CloneArgs`] Air.
211///
212/// Extrait en fonction privée pour pouvoir tester unitairement le mapping
213/// `exit_signal` (notamment la branche `None` qui ne peut pas être
214/// observée bout-en-bout via fork — le kernel détache l'enfant quand
215/// `exit_signal = 0` et `waitid` retourne alors `ECHILD`).
216fn build_kernel_clone_args(args: &CloneArgs, pidfd_addr: u64) -> KernelCloneArgs {
217 let exit_signal_u64: u64 = args.exit_signal.map_or(0, |sig| {
218 // sig.as_raw() > 0 par construction (NonZeroI32) → cast sign-safe.
219 #[allow(clippy::cast_sign_loss)]
220 {
221 sig.as_raw() as u64
222 }
223 });
224
225 // Pile : `None` → 0/0 (fork classique) ; `Some` → base + taille (thread).
226 // `clone3` valide que `stack == None` (fork) ; `clone_thread` exige `Some`.
227 let (stack_addr, stack_size) = match args.stack {
228 Some(s) => (s.addr as u64, s.size as u64),
229 None => (0, 0),
230 };
231
232 KernelCloneArgs {
233 flags: args.flags.bits(),
234 pidfd: pidfd_addr,
235 child_tid: args.child_tid.map_or(0, |r| r.as_ptr() as u64),
236 parent_tid: args.parent_tid.map_or(0, |r| r.as_ptr() as u64),
237 exit_signal: exit_signal_u64,
238 stack: stack_addr,
239 stack_size,
240 tls: args.tls.unwrap_or(0),
241 set_tid: 0,
242 set_tid_size: 0,
243 cgroup: 0,
244 }
245}
246
247/// Crée un nouveau processus via le syscall `clone3(2)`.
248///
249/// Cf. `docs/specs/layer-0/family-process.md` section `clone3`. La fonction
250/// retourne **deux fois** en cas de succès : une fois côté parent (avec le
251/// PID enfant, et un `PidFd` si [`CloneFlags::PIDFD`] était positionné),
252/// une fois côté enfant (avec [`CloneResult::Child`]). Le `match` sur le
253/// résultat est le seul moyen sûr de discriminer.
254///
255/// # Périmètre
256///
257/// `clone3` est dédié à la **création de processus** style fork ; la création
258/// de **thread** passe par [`clone_thread`] (modèle de retour distinct, cf. sa
259/// doc et la note d'implémentation au-dessus de `clone_thread`) :
260///
261/// - `args.flags` peut contenir tous les bits hors [`CloneFlags::VM`] /
262/// [`CloneFlags::THREAD`] / [`CloneFlags::SIGHAND`] : ces drapeaux de
263/// création de thread sont refusés ici avec [`Errno::EINVAL`] (leur retour
264/// `CloneResult::Child` serait *unsound* sur une pile neuve — voir
265/// `clone_thread`).
266/// - `args.stack` doit être `None` (refusé sinon avec [`Errno::EINVAL`] ; une
267/// pile dédiée n'a de sens que pour un thread → `clone_thread`).
268/// - `args.exit_signal` est typiquement `Some(Signal::SIGCHLD)`.
269///
270/// # Safety
271///
272/// `clone3` peut créer un thread partageant la mémoire avec le parent
273/// (`CLONE_VM`), situation que Rust ne peut pas vérifier. L'appelant doit
274/// donc garantir, selon le scénario :
275///
276/// - **Fork classique** (sans `CLONE_VM`) — aucune précondition
277/// particulière sur la mémoire. L'enfant possède une copie CoW de
278/// l'espace d'adressage du parent ; tous les invariants Rust restent
279/// valables des deux côtés.
280///
281/// - **Création de thread** (`CLONE_VM | CLONE_THREAD | CLONE_SIGHAND`)
282/// — non géré par `clone3` ; l'appel échoue avec [`Errno::EINVAL`]. Utiliser
283/// [`clone_thread`], qui exécute le thread via un trampoline assembleur (pas
284/// de retour Rust sur pile neuve) et expose le *join* par
285/// `CLONE_CHILD_CLEARTID`.
286///
287/// - **Namespaces** (`CLONE_NEWUSER`, `CLONE_NEWNS`, `CLONE_NEWPID`, …)
288/// — l'appelant doit posséder les capabilities nécessaires
289/// (typiquement `CAP_SYS_ADMIN`) ; à défaut, le syscall retourne
290/// [`Errno::EPERM`]. Sur Ubuntu 24.04, les user namespaces non
291/// privilégiés sont restreints par AppArmor
292/// (`kernel.apparmor_restrict_unprivileged_userns=1`) et l'appel
293/// échoue avec [`Errno::EPERM`] même côté unprivileged.
294///
295/// - **« Retourne deux fois »** — après succès, le syscall est exécuté
296/// côté parent **et** côté enfant ; les deux retours apparaissent ici
297/// comme `Ok(_)` distincts. L'enfant doit terminer son travail
298/// (typiquement par `std::process::exit` ou un futur `exit_group`
299/// wrapper) ; revenir « plus loin » dans le code appelant produit
300/// généralement un comportement surprenant pour l'utilisateur.
301///
302/// # Errors
303///
304/// - [`Errno::EINVAL`] : combinaison de flags refusée par cette PR
305/// (création de thread) ou par le kernel (configuration invalide).
306/// - [`Errno::EPERM`] : capabilities insuffisantes (NEWUSER refusé,
307/// AppArmor, …).
308/// - `EAGAIN` (constante non encore définie dans le stub partiel `Errno`)
309/// : limites système atteintes (nombre de processus).
310/// - `ENOMEM` (idem) : mémoire kernel insuffisante.
311/// - `EUSERS` (idem) : trop de namespaces utilisateurs créés.
312/// - `ENOSYS` (idem) : kernel antérieur à 5.3 (`clone3` indisponible).
313///
314/// Les constantes nommées en prose ci-dessus seront promues en
315/// `pub const` au fil des PRs qui les rencontrent réellement
316/// (cf. note « stub partiel » en tête du module `errno`).
317pub unsafe fn clone3(args: &CloneArgs) -> Result<CloneResult, Errno> {
318 // Refus explicite des cas non testés (Principe 4 : valider en amont).
319 if args.stack.is_some() {
320 return Err(Errno::EINVAL);
321 }
322 if args
323 .flags
324 .intersects(CloneFlags::VM | CloneFlags::THREAD | CloneFlags::SIGHAND)
325 {
326 return Err(Errno::EINVAL);
327 }
328
329 let want_pidfd = args.flags.contains(CloneFlags::PIDFD);
330 let mut pidfd_storage: i32 = -1;
331
332 let pidfd_addr: u64 = if want_pidfd {
333 // Adresse du i32 que le kernel renseignera côté parent en cas de succès.
334 let ptr: *mut i32 = &mut pidfd_storage;
335 ptr as u64
336 } else {
337 0
338 };
339
340 let kernel_args = build_kernel_clone_args(args, pidfd_addr);
341
342 let args_ptr: *const KernelCloneArgs = &kernel_args;
343 // size_of::<KernelCloneArgs>() = 88. Air ne cible que des cibles
344 // 64-bit (cf. ADR-014) ; `usize == u64`, donc le cast est exact.
345 #[allow(clippy::cast_lossless)]
346 let args_size: u64 = core::mem::size_of::<KernelCloneArgs>() as u64;
347
348 // SAFETY:
349 // - args_ptr pointe sur `kernel_args` (local valide pour toute la fonction).
350 // - args_size est exactement size_of::<KernelCloneArgs>().
351 // - Si CLONE_PIDFD est positionné, pidfd_addr pointe sur `pidfd_storage`
352 // qui est local valide ; le kernel y écrit côté parent uniquement.
353 // - Aucune autre adresse mémoire utilisateur n'est passée au kernel
354 // (parent_tid/child_tid/stack/set_tid/cgroup sont à 0).
355 // - Sémantique « retourne deux fois » : voir doc de la fonction.
356 let ret = unsafe { raw_syscall_clone3(args_ptr as u64, args_size) };
357
358 if ret < 0 {
359 return Err(errno_from_negative_syscall_ret(ret));
360 }
361
362 if ret == 0 {
363 // Côté enfant : ne pas lire `pidfd_storage` (le kernel n'a écrit que
364 // côté parent ; la copie CoW du child contient la valeur initiale).
365 return Ok(CloneResult::Child);
366 }
367
368 // Côté parent : `ret` est le PID enfant, borné par `PID_MAX_LIMIT`
369 // (= 2^22 sur 64-bit Linux), bien en deçà de `i32::MAX`. Troncature
370 // intentionnelle, sûre par construction.
371 #[allow(clippy::cast_possible_truncation)]
372 let child_pid_raw = ret as i32;
373 let child_pid = Pid::try_from_raw(child_pid_raw)
374 .expect("clone3 retour parent : PID strictement positif (POSIX)");
375
376 let child_pidfd = if want_pidfd {
377 let raw_fd = pidfd_storage;
378 // SAFETY: le kernel a écrit un fd valide dans `pidfd_storage` côté
379 // parent après succès de clone3 avec CLONE_PIDFD. Air en prend
380 // ownership ; OwnedFd::Drop fermera le fd.
381 let owned = unsafe { OwnedFd::from_raw_fd(raw_fd) };
382 Some(PidFd::from_owned_fd(owned))
383 } else {
384 None
385 };
386
387 Ok(CloneResult::Parent {
388 child_pid,
389 child_pidfd,
390 })
391}
392
393#[cfg(target_arch = "x86_64")]
394#[inline]
395unsafe fn raw_syscall_clone3(args_ptr: u64, args_size: u64) -> i64 {
396 let ret: i64;
397 // SAFETY:
398 // - SYS_clone3 (x86_64 = 435). Le kernel lit `args_size` octets à
399 // l'adresse `args_ptr` (validité garantie par l'appelant) ; il peut
400 // écrire à des sous-pointeurs (pidfd/parent_tid/child_tid) si
401 // demandés. L'appelant a respecté ces préconditions.
402 // - L'ABI syscall x86_64 prend le numéro dans RAX, arg1/arg2 dans
403 // RDI/RSI ; retourne en RAX ; clobbe RCX (RIP) et R11 (RFLAGS).
404 // - **Pas de `readonly`** : clone3 écrit dans la mémoire utilisateur
405 // (output pointers) ; `readonly` serait incorrect ici.
406 // - Sémantique « retourne deux fois » : le syscall revient côté
407 // parent (rax = pid > 0) et côté enfant (rax = 0). Les deux
408 // retours sortent normalement de cette fonction.
409 unsafe {
410 core::arch::asm!(
411 "syscall",
412 in("rax") 435_i64,
413 in("rdi") args_ptr,
414 in("rsi") args_size,
415 lateout("rax") ret,
416 lateout("rcx") _,
417 lateout("r11") _,
418 options(nostack, preserves_flags),
419 );
420 }
421 ret
422}
423
424#[cfg(target_arch = "aarch64")]
425#[inline]
426unsafe fn raw_syscall_clone3(args_ptr: u64, args_size: u64) -> i64 {
427 let ret: i64;
428 // SAFETY: voir version x86_64 ; aarch64 syscall ABI : numéro en X8,
429 // arg1/arg2 en X0/X1, retour en X0. svc 0 effectue le syscall.
430 // Comme x86_64, **pas de `readonly`** (clone3 écrit en mémoire user).
431 unsafe {
432 core::arch::asm!(
433 "svc 0",
434 in("x8") 435_i64,
435 inout("x0") args_ptr => ret,
436 in("x1") args_size,
437 options(nostack, preserves_flags),
438 );
439 }
440 ret
441}
442
443// ─────────────────────────────────────────────────────────────────────────
444// clone_thread — création d'un thread (CLONE_VM) via clone3 + trampoline asm.
445//
446// Pourquoi une fonction DISTINCTE de `clone3` (et non un simple
447// `CloneResult::Child` étendu) ? Parce que le modèle « clone3 retourne deux
448// fois dans du Rust » est **unsound** pour un thread doté d'une pile NEUVE :
449// après le syscall, l'enfant reprend l'exécution avec `sp` = sommet de la pile
450// fournie. Laisser le code Rust « revenir » (épilogue de fonction : restauration
451// de registres callee-saved + `ret`/`blr x30`) lirait des **ordures** sur cette
452// pile vierge → crash. La discipline universelle (glibc/musl) est donc :
453// l'enfant ne **revient jamais** dans le code Rust appelant ; un trampoline en
454// assembleur appelle directement un point d'entrée `extern "C"` fourni, puis
455// termine le thread via `SYS_exit`. C'est ce que fait `clone_thread`.
456//
457// `clone3` reste, lui, dédié au **fork** (pas de pile neuve, l'enfant partage la
458// copie CoW de la pile parent : `CloneResult::Child` y est sound) et **rejette**
459// `stack`/`CLONE_VM` en amont.
460// ─────────────────────────────────────────────────────────────────────────
461
462/// Taille minimale (octets) acceptée pour la pile d'un thread `clone_thread`.
463/// Une page suffit largement au point d'entrée minimal ; les piles réelles sont
464/// dimensionnées par l'appelant (la libc Air, couche 1).
465const MIN_THREAD_STACK_SIZE: usize = 4096;
466
467/// Crée un **thread** (mémoire partagée, `CLONE_VM`) via `clone3(2)`.
468///
469/// Contrairement à [`clone3`] (réservé au fork), `clone_thread` ne « retourne
470/// pas deux fois » : le nouveau thread exécute le point d'entrée `entry(arg)`
471/// **sur sa pile dédiée** via un trampoline assembleur, puis — si `entry`
472/// revient — termine le **seul** thread courant par `SYS_exit` (jamais
473/// `exit_group`). Seul le **parent** revient de cette fonction, avec le [`Tid`]
474/// du nouveau thread.
475///
476/// # Join (terminaison observable)
477///
478/// Les threads `CLONE_THREAD` ne sont **pas** *waitable* via `waitid`. Le *join*
479/// se fait par `CLONE_CHILD_CLEARTID` : positionner [`CloneFlags::CHILD_CLEARTID`]
480/// et renseigner [`CloneArgs::child_tid`] sur un mot [`core::sync::atomic::AtomicU32`]
481/// partagé. À la mort du thread, le kernel remet ce mot à `0` et émet un
482/// `FUTEX_WAKE` (de portée **partagée** — sans `FUTEX_PRIVATE_FLAG`) dessus. Pour
483/// initialiser le mot avec le TID **sans course**, ajouter
484/// [`CloneFlags::PARENT_SETTID`] avec [`CloneArgs::parent_tid`] sur le **même**
485/// mot (écrit par le kernel, côté parent, avant le retour). Le parent joint
486/// alors en bouclant `futex_wait` jusqu'à lire `0` (cf. la famille
487/// [`futex`](crate::futex)).
488///
489/// # Errors
490///
491/// - [`Errno::EINVAL`] si la validation amont échoue : `stack` absent,
492/// [`CloneFlags::VM`] absent, adresse de pile nulle, taille `< 4096`, `addr`
493/// ou `size` non alignés sur 16, ou sommet de pile en débordement. Le kernel
494/// peut aussi rendre `EINVAL` pour une combinaison de drapeaux incohérente
495/// (p. ex. `CLONE_THREAD` avec un `exit_signal`).
496/// - `EAGAIN`/`ENOMEM` (stub partiel `Errno`) : limites système / mémoire kernel.
497///
498/// # Safety
499///
500/// Hautement `unsafe`. L'appelant doit garantir :
501///
502/// - **Pile.** `stack` désigne une région mémoire valide, possédée, exclusivement
503/// réservée au nouveau thread, d'au moins `size` octets, et **non libérée**
504/// tant que le thread n'est pas joint (sinon use-after-free de la pile).
505/// - **Point d'entrée.** `entry` ne doit exécuter que des opérations sûres dans
506/// ce contexte naissant : **aucune** variable `thread_local` (sauf
507/// [`CloneArgs::tls`] fourni avec un bloc TLS valide via [`CloneFlags::SETTLS`]),
508/// pas d'`unwind`/panic traversant le trampoline, pas de dépendance à un état
509/// d'initialisation propre au thread. Les écritures en mémoire **partagée**
510/// (VM commune) doivent être thread-safe (`Atomic*`).
511/// - **`arg`.** Sa signification est définie par `entry` ; s'il encode un
512/// pointeur, la cible doit rester vivante et correctement synchronisée.
513/// - **Join.** `child_tid`/`parent_tid` (si fournis) pointent un `AtomicU32`
514/// vivant jusqu'à la fin du *join*.
515pub unsafe fn clone_thread(
516 args: &CloneArgs,
517 entry: extern "C" fn(usize),
518 arg: usize,
519) -> Result<Tid, Errno> {
520 // ── Validation amont (Principe 4) ───────────────────────────────────
521 let stack: StackSpecification = args.stack.ok_or(Errno::EINVAL)?;
522 // Un thread partage l'espace d'adressage : CLONE_VM est obligatoire (sinon
523 // la pile neuve ne sert à rien et le join par mot partagé est impossible).
524 if !args.flags.contains(CloneFlags::VM) {
525 return Err(Errno::EINVAL);
526 }
527 if stack.addr.is_null() {
528 return Err(Errno::EINVAL);
529 }
530 if stack.size < MIN_THREAD_STACK_SIZE {
531 return Err(Errno::EINVAL);
532 }
533 let base = stack.addr.addr();
534 // Alignement 16 de la base ET de la taille ⇒ sommet (`base + size`) aligné 16
535 // (exigence ABI x86_64/aarch64 au point d'entrée). `& 0xF` : pas d'arithmétique.
536 if base & 0xF != 0 || stack.size & 0xF != 0 {
537 return Err(Errno::EINVAL);
538 }
539 // Sommet de pile sans débordement d'adresse (Principe 2 : pas d'« + » nu).
540 base.checked_add(stack.size).ok_or(Errno::EINVAL)?;
541
542 // Un thread n'a pas de pidfd : pidfd_addr = 0 (CLONE_PIDFD non pris en charge
543 // par ce chemin ; il appartient au fork).
544 let kernel_args = build_kernel_clone_args(args, 0);
545 let args_ptr: *const KernelCloneArgs = &kernel_args;
546 // size_of::<KernelCloneArgs>() = 88 ; cibles 64-bit uniquement (ADR-014).
547 #[allow(clippy::cast_lossless)]
548 let args_size: u64 = core::mem::size_of::<KernelCloneArgs>() as u64;
549
550 // SAFETY:
551 // - `args_ptr` pointe `kernel_args` (local vivant pour toute la fonction) ;
552 // `args_size` est exactement sa taille. Le kernel lit cette structure et,
553 // via `child_tid`/`parent_tid`, peut écrire les mots `AtomicU32` fournis
554 // (validité garantie par le contrat `unsafe` de cette fonction).
555 // - Le côté ENFANT ne revient pas ici (le trampoline appelle `entry(arg)`
556 // sur la pile neuve puis `SYS_exit`) : aucun retour Rust sur pile vierge.
557 // - Le côté PARENT reçoit `ret` (TID > 0, ou -errno) et le rend normalement.
558 let ret = unsafe { raw_syscall_clone3_thread(args_ptr as u64, args_size, entry, arg) };
559
560 if ret < 0 {
561 return Err(errno_from_negative_syscall_ret(ret));
562 }
563 // `ret == 0` est inobservable ici (l'enfant ne revient jamais dans ce code).
564 // Le parent reçoit le TID enfant, borné par `PID_MAX_LIMIT` ≪ `i32::MAX`.
565 #[allow(clippy::cast_possible_truncation)]
566 let tid_raw = ret as i32;
567 let tid = Tid::try_from_raw(tid_raw)
568 .expect("clone_thread retour parent : TID strictement positif (POSIX)");
569 Ok(tid)
570}
571
572#[cfg(target_arch = "x86_64")]
573#[inline]
574unsafe fn raw_syscall_clone3_thread(
575 args_ptr: u64,
576 args_size: u64,
577 entry: extern "C" fn(usize),
578 arg: usize,
579) -> i64 {
580 let ret: i64;
581 // SAFETY:
582 // - SYS_clone3 (x86_64 = 435) crée le thread ; le kernel lit `args_size`
583 // octets à `args_ptr` (struct clone_args valide) et règle `sp` de l'enfant
584 // au sommet de la pile (`stack`/`stack_size` du struct).
585 // - « Retourne deux fois » : côté PARENT `rax` = TID (> 0) ou -errno → on
586 // branche en `2:` et rend `rax` à Rust. Côté ENFANT `rax` = 0 : on NE
587 // revient PAS dans le code Rust (sa pile est vierge). On place `arg` dans
588 // rdi puis on `call` `entry` (ABI C) sur la pile neuve. Si `entry` revient,
589 // on termine le SEUL thread courant via SYS_exit (x86_64 = 60), jamais
590 // exit_group. `ud2` est un garde-fou inatteignable.
591 // - Clobbers du PARENT uniquement (l'enfant ne rend jamais la main au
592 // compilateur) : rax (ret), rcx et r11 (clobbés par `syscall`), drapeaux
593 // (pas de `preserves_flags`). `entry`/`arg` sont **épinglés** sur des
594 // registres callee-saved (r12/r13) que `syscall` ne touche pas : un
595 // registre callee-saved choisi par le compilateur (`in(reg)`) pourrait
596 // être rcx/r11 et serait écrasé par le `syscall` AVANT l'usage enfant
597 // (l'allocateur suppose les `lateout` produits *après* les `in` lus).
598 // `inout("rdi")` documente que rdi (args_ptr puis arg) est écrasé. Pas de
599 // `nostack` : le `call` enfant pousse sur la pile (neuve).
600 unsafe {
601 core::arch::asm!(
602 "syscall",
603 "test rax, rax",
604 "jnz 2f",
605 "mov rdi, r13",
606 "call r12",
607 "mov rax, 60", // SYS_exit (x86_64 = 60) : termine CE thread.
608 "xor edi, edi",
609 "syscall",
610 "ud2",
611 "2:",
612 inout("rax") 435_i64 => ret, // SYS_clone3 (x86_64 = 435).
613 inout("rdi") args_ptr => _,
614 in("rsi") args_size,
615 in("r12") entry,
616 in("r13") arg,
617 lateout("rcx") _,
618 lateout("r11") _,
619 );
620 }
621 ret
622}
623
624#[cfg(target_arch = "aarch64")]
625#[inline]
626unsafe fn raw_syscall_clone3_thread(
627 args_ptr: u64,
628 args_size: u64,
629 entry: extern "C" fn(usize),
630 arg: usize,
631) -> i64 {
632 let ret: i64;
633 // SAFETY: voir la version x86_64. ABI aarch64 : SYS_clone3 (aarch64 = 435)
634 // en X8, args en X0/X1, retour en X0. Côté ENFANT (`x0 == 0`) : `arg` → x0,
635 // `blr` `entry` sur la pile neuve, puis SYS_exit (aarch64 = 93) — jamais
636 // exit_group. `udf #0` garde-fou inatteignable. Clobbers du PARENT
637 // uniquement : x0 (ret) ; `x8` est `in` (réécrit côté enfant, consommé).
638 // `entry`/`arg` sont **épinglés** sur des registres callee-saved (x21/x20)
639 // intacts jusqu'à leur usage enfant (symétrie avec x86_64). Pas de `nostack`
640 // (`blr` enfant utilise la pile).
641 unsafe {
642 core::arch::asm!(
643 "svc 0",
644 "cbnz x0, 2f",
645 "mov x0, x20",
646 "blr x21",
647 "mov x8, #93", // SYS_exit (aarch64 = 93) : termine CE thread.
648 "mov x0, #0",
649 "svc 0",
650 "udf #0",
651 "2:",
652 in("x8") 435_i64, // SYS_clone3 (aarch64 = 435).
653 inout("x0") args_ptr => ret,
654 in("x1") args_size,
655 in("x21") entry,
656 in("x20") arg,
657 );
658 }
659 ret
660}
661
662// ─────────────────────────────────────────────────────────────────────────
663// exit_group — terminaison immédiate du processus, sans cleanup userspace.
664//
665// Helper **interne** (`pub(crate)`, hors API publique gelée) : c'est la
666// discipline de sortie correcte pour l'enfant d'un `clone3` fork dans un
667// programme multi-thread. `std::process::exit` exécute les atexit handlers
668// (flush stdio, machinerie de capture du harnais de test…) qui peuvent
669// **deadlocker** sur des mutex hérités verrouillés par les threads du parent
670// — inexistants dans l'enfant. Observé sur aarch64 (`futex_do_wait`) là où
671// x86_64 masquait le bug. `exit_group` est un syscall direct : aucun verrou,
672// aucun atexit, terminaison atomique de tous les threads du processus.
673// ─────────────────────────────────────────────────────────────────────────
674
675/// Termine immédiatement le processus appelant (tous ses threads) avec le
676/// code `status`, via le syscall `child_exit(2)`.
677///
678/// Ne retourne jamais (`!`). N'exécute **aucun** atexit handler, ne flush
679/// **aucun** buffer userspace : c'est précisément ce qui la rend sûre comme
680/// sortie d'un enfant forké d'un programme multi-thread (cf. note ci-dessus).
681///
682/// Primitive publique (spec `family-process`, sous-section 3) : c'est le
683/// chemin de sortie correct pour l'**enfant d'un `clone3` fork** dont l'`exec`
684/// a échoué — terminer immédiatement, sans réveiller la machinerie userspace
685/// héritée du parent. `air-process` (couche 1) l'utilise après un `execve`
686/// raté dans l'enfant. Le code de sortie observé par `waitid`/`wait` est
687/// `status & 0xFF`.
688pub fn exit_group(status: i32) -> ! {
689 // SAFETY: `exit_group(2)` ne lit ni n'écrit aucune mémoire utilisateur ;
690 // il termine le processus et ne retourne pas. `options(noreturn)` est
691 // donc correct.
692 unsafe { raw_syscall_exit_group(status) }
693}
694
695#[cfg(target_arch = "x86_64")]
696#[inline]
697unsafe fn raw_syscall_exit_group(status: i32) -> ! {
698 // SAFETY: SYS_exit_group (x86_64 = 231) ne touche aucune mémoire
699 // utilisateur et ne retourne jamais. `noreturn` autorisé.
700 unsafe {
701 core::arch::asm!(
702 "syscall",
703 in("rax") 231_i64,
704 in("rdi") i64::from(status),
705 options(nostack, noreturn),
706 );
707 }
708}
709
710#[cfg(target_arch = "aarch64")]
711#[inline]
712unsafe fn raw_syscall_exit_group(status: i32) -> ! {
713 // SAFETY: SYS_exit_group (aarch64 = 94) ne touche aucune mémoire
714 // utilisateur et ne retourne jamais. `noreturn` autorisé.
715 unsafe {
716 core::arch::asm!(
717 "svc 0",
718 in("x8") 94_i64,
719 in("x0") i64::from(status),
720 options(nostack, noreturn),
721 );
722 }
723}
724
725// ─────────────────────────────────────────────────────────────────────────
726// waitid
727// ─────────────────────────────────────────────────────────────────────────
728
729// `idtype` du syscall waitid (cf. `linux/wait.h`).
730const P_ALL: i64 = 0;
731const P_PID: i64 = 1;
732const P_PGID: i64 = 2;
733const P_PIDFD: i64 = 3;
734
735// `si_code` valeurs pour SIGCHLD (cf. `linux/signal.h`).
736const CLD_EXITED: i32 = 1;
737const CLD_KILLED: i32 = 2;
738const CLD_DUMPED: i32 = 3;
739const CLD_TRAPPED: i32 = 4;
740const CLD_STOPPED: i32 = 5;
741const CLD_CONTINUED: i32 = 6;
742
743/// Représentation locale du `siginfo_t` Linux pour SIGCHLD.
744///
745/// Layout vérifié sur x86_64 et aarch64 (deux seules architectures Air,
746/// cf. ADR-014) : 128 octets, union 8-octets alignée à l'offset 16. Les
747/// champs `si_pid`/`si_uid`/`si_status` sont au début de la variante
748/// `_sigchld` de l'union ; les offsets reflètent cette disposition.
749#[repr(C)]
750#[derive(Clone, Copy)]
751struct KernelSiginfo {
752 si_signo: i32,
753 si_errno: i32,
754 si_code: i32,
755 _pad0: i32,
756 si_pid: i32,
757 si_uid: u32,
758 si_status: i32,
759 _trailing: [u8; 100],
760}
761
762impl KernelSiginfo {
763 fn zeroed() -> Self {
764 Self {
765 si_signo: 0,
766 si_errno: 0,
767 si_code: 0,
768 _pad0: 0,
769 si_pid: 0,
770 si_uid: 0,
771 si_status: 0,
772 _trailing: [0u8; 100],
773 }
774 }
775}
776
777const _: () = {
778 // Vérifie à la compilation que KernelSiginfo fait bien 128 octets : si le
779 // layout C devait dériver (futur portage), cette assertion explose.
780 assert!(core::mem::size_of::<KernelSiginfo>() == 128);
781};
782
783/// Attend un événement sur un processus enfant via le syscall `waitid(2)`.
784///
785/// Cf. `docs/specs/layer-0/family-process.md` section `waitid`. Retourne :
786///
787/// - `Ok(Some(status))` si un événement a été reçu.
788/// - `Ok(None)` si [`WaitOptions::NOHANG`] était positionné et qu'aucun
789/// événement n'était disponible (kernel renvoie une `siginfo` zéroée).
790/// - `Err(Errno::EINTR)` si l'attente a été interrompue par un signal :
791/// **non retried automatiquement** (convention 2 ADR-021). L'appelant
792/// qui veut retry écrit la boucle explicitement.
793///
794/// # Errors
795///
796/// - [`Errno::ECHILD`] : aucun enfant à attendre dans la cible.
797/// - [`Errno::EINTR`] : interrompu par un signal, voir ci-dessus.
798/// - [`Errno::EINVAL`] : options invalides (aucun parmi
799/// `EXITED`/`STOPPED`/`CONTINUED` positionné) ou cible invalide.
800pub fn waitid(target: WaitTarget<'_>, options: WaitOptions) -> Result<Option<WaitStatus>, Errno> {
801 use air_sys_types::fd::AsRawFd;
802
803 let (idtype, id_i64) = match target {
804 WaitTarget::AnyChild => (P_ALL, 0_i64),
805 WaitTarget::Pid(p) => (P_PID, i64::from(p.as_raw())),
806 WaitTarget::ProcessGroup(p) => (P_PGID, i64::from(p.as_raw())),
807 WaitTarget::AnyProcessGroup => (P_PGID, 0_i64),
808 WaitTarget::PidFd(fd) => (P_PIDFD, i64::from(fd.as_raw_fd())),
809 };
810
811 let mut info = KernelSiginfo::zeroed();
812 let info_ptr: *mut KernelSiginfo = &mut info;
813
814 let options_i64 = i64::from(options.bits());
815
816 // SAFETY:
817 // - info_ptr pointe sur un KernelSiginfo local (128 octets, alignement 4) que
818 // le kernel renseigne au plus jusqu'à 128 octets — taille fixe et
819 // exactement celle qu'attend l'ABI Linux pour `siginfo_t`.
820 // - rusage = 0 : on n'expose pas le 5e argument optionnel.
821 let ret = unsafe { raw_syscall_waitid(idtype, id_i64, info_ptr as u64, options_i64) };
822
823 if ret < 0 {
824 return Err(errno_from_negative_syscall_ret(ret));
825 }
826
827 // Cas NOHANG sans événement : le kernel laisse `siginfo` zéroé
828 // (en particulier si_signo == 0, parce qu'un événement waitid réel
829 // a toujours si_signo = SIGCHLD = 17 sur x86_64/aarch64).
830 if info.si_signo == 0 {
831 return Ok(None);
832 }
833
834 let pid = Pid::try_from_raw(info.si_pid)
835 .expect("waitid : si_pid positif pour un événement enfant réel");
836 let event = siginfo_to_wait_event(&info);
837
838 Ok(Some(WaitStatus {
839 pid,
840 uid: info.si_uid,
841 event,
842 }))
843}
844
845/// Helper privé : convertit un `KernelSiginfo` rempli par le kernel en
846/// [`WaitEvent`] typé. Extrait du corps de [`waitid`] pour pouvoir être
847/// testé unitairement avec des `KernelSiginfo` synthétisés en mémoire — ce qui
848/// permet de couvrir les 5 variantes (Exited, Killed, Stopped, Continued,
849/// Trapped) sans dépendre de scénarios kernel réels.
850///
851/// **Limite connue** : ce test sur KernelSiginfo synthétisés couvre la *logique
852/// de dispatch* mais ne valide pas le *remplissage* effectif de
853/// `siginfo_t` par le kernel pour chaque code `CLD_*`. Voir `docs/JOURNAL.md`,
854/// session 2026-05-22 (suite : clone3+waitid), trou de couverture acté.
855fn siginfo_to_wait_event(info: &KernelSiginfo) -> WaitEvent {
856 match info.si_code {
857 CLD_EXITED => WaitEvent::Exited {
858 code: info.si_status,
859 },
860 CLD_KILLED => WaitEvent::Killed {
861 signal: Signal::try_from_raw(info.si_status)
862 .expect("CLD_KILLED : si_status est un numéro de signal valide"),
863 core_dumped: false,
864 },
865 CLD_DUMPED => WaitEvent::Killed {
866 signal: Signal::try_from_raw(info.si_status)
867 .expect("CLD_DUMPED : si_status est un numéro de signal valide"),
868 core_dumped: true,
869 },
870 CLD_TRAPPED => WaitEvent::Trapped {
871 signal: Signal::try_from_raw(info.si_status)
872 .expect("CLD_TRAPPED : si_status est un numéro de signal valide"),
873 },
874 CLD_STOPPED => WaitEvent::Stopped {
875 signal: Signal::try_from_raw(info.si_status)
876 .expect("CLD_STOPPED : si_status est un numéro de signal valide"),
877 },
878 CLD_CONTINUED => WaitEvent::Continued,
879 unknown => unreachable!("kernel a renseigné un si_code inconnu pour SIGCHLD : {unknown}"),
880 }
881}
882
883#[cfg(target_arch = "x86_64")]
884#[inline]
885unsafe fn raw_syscall_waitid(idtype: i64, id: i64, infop: u64, options: i64) -> i64 {
886 let ret: i64;
887 // SAFETY:
888 // - SYS_waitid (x86_64 = 247). Le kernel écrit dans `infop` au plus
889 // 128 octets — validité garantie par l'appelant (KernelSiginfo local).
890 // - rusage (5e arg) à 0 : non utilisé.
891 // - x86_64 syscall ABI : numéro en RAX, args en RDI/RSI/RDX/R10/R8/R9 ;
892 // retour en RAX ; clobbe RCX et R11.
893 // - **Pas de `readonly`** : le kernel écrit dans `*infop`.
894 unsafe {
895 core::arch::asm!(
896 "syscall",
897 in("rax") 247_i64,
898 in("rdi") idtype,
899 in("rsi") id,
900 in("rdx") infop,
901 in("r10") options,
902 in("r8") 0_i64,
903 lateout("rax") ret,
904 lateout("rcx") _,
905 lateout("r11") _,
906 options(nostack, preserves_flags),
907 );
908 }
909 ret
910}
911
912#[cfg(target_arch = "aarch64")]
913#[inline]
914unsafe fn raw_syscall_waitid(idtype: i64, id: i64, infop: u64, options: i64) -> i64 {
915 let ret: i64;
916 // SAFETY: voir version x86_64 ; aarch64 syscall ABI :
917 // numéro en X8 (= 95), args en X0..X4 (5 args, le 5e = rusage = 0).
918 unsafe {
919 core::arch::asm!(
920 "svc 0",
921 in("x8") 95_i64,
922 inout("x0") idtype => ret,
923 in("x1") id,
924 in("x2") infop,
925 in("x3") options,
926 in("x4") 0_i64,
927 options(nostack, preserves_flags),
928 );
929 }
930 ret
931}
932
933// ─────────────────────────────────────────────────────────────────────────
934// pidfd_open
935// ─────────────────────────────────────────────────────────────────────────
936
937/// Ouvre un pidfd qui référence le processus identifié par `pid`.
938///
939/// Cf. `docs/specs/layer-0/family-process.md`, sous-section 3,
940/// `pidfd_open`. Le pidfd retourné reste valide même après la mort du
941/// processus original (pas de race sur le PID recyclé). Il peut être
942/// utilisé par [`waitid`] (variant `WaitTarget::PidFd`),
943/// [`pidfd_send_signal`], [`pidfd_getfd`], et par les syscalls
944/// `poll`/`epoll` pour détecter la mort du processus.
945///
946/// # Errors
947///
948/// - [`Errno::EINVAL`] : un bit invalide dans `flags`.
949/// - [`Errno::ESRCH`] : aucun processus n'a ce PID.
950/// - `ENOMEM` (stub partiel `Errno`) : mémoire kernel insuffisante.
951pub fn pidfd_open(pid: Pid, flags: PidFdOpenFlags) -> Result<PidFd, Errno> {
952 // SAFETY:
953 // - `pidfd_open(2)` ne lit ni n'écrit aucune mémoire utilisateur :
954 // il prend deux scalaires et retourne un fd.
955 // - L'ABI x86_64 / aarch64 est documentée dans
956 // `raw_syscall_pidfd_open` (clobbers, options).
957 let ret = unsafe { raw_syscall_pidfd_open(pid.as_raw(), flags.bits()) };
958 if ret < 0 {
959 return Err(errno_from_negative_syscall_ret(ret));
960 }
961 // `ret` est un fd kernel valide. Linux borne les fd à `i32::MAX`
962 // (`/proc/sys/fs/file-max` n'excède pas cette limite par construction
963 // dans le kernel), donc la troncature est exacte.
964 #[allow(clippy::cast_possible_truncation)]
965 let new_fd: RawFd = ret as RawFd;
966 // SAFETY: le kernel vient d'allouer un fd valide et nous en a
967 // transféré la propriété ; Air le wrappe immédiatement dans
968 // `OwnedFd` puis dans `PidFd`. La fermeture est garantie par Drop.
969 let owned = unsafe { OwnedFd::from_raw_fd(new_fd) };
970 Ok(PidFd::from_owned_fd(owned))
971}
972
973#[cfg(target_arch = "x86_64")]
974#[inline]
975unsafe fn raw_syscall_pidfd_open(pid: i32, flags: u32) -> i64 {
976 let ret: i64;
977 // SAFETY:
978 // - SYS_pidfd_open (x86_64 = 434) ne touche à aucune mémoire
979 // utilisateur ; il ne fait que retourner un fd.
980 // - ABI syscall x86_64 : numéro en RAX, arg1/arg2 en RDI/RSI ;
981 // retour en RAX ; clobbe RCX (RIP) et R11 (RFLAGS).
982 // - `readonly` correct : pas d'écriture en mémoire utilisateur.
983 unsafe {
984 core::arch::asm!(
985 "syscall",
986 in("rax") 434_i64,
987 in("rdi") i64::from(pid),
988 in("rsi") i64::from(flags),
989 lateout("rax") ret,
990 lateout("rcx") _,
991 lateout("r11") _,
992 options(nostack, preserves_flags, readonly),
993 );
994 }
995 ret
996}
997
998#[cfg(target_arch = "aarch64")]
999#[inline]
1000unsafe fn raw_syscall_pidfd_open(pid: i32, flags: u32) -> i64 {
1001 let ret: i64;
1002 // SAFETY: ABI aarch64 ; numéro en X8, args en X0/X1, retour en X0.
1003 unsafe {
1004 core::arch::asm!(
1005 "svc 0",
1006 in("x8") 434_i64,
1007 inout("x0") i64::from(pid) => ret,
1008 in("x1") i64::from(flags),
1009 options(nostack, preserves_flags, readonly),
1010 );
1011 }
1012 ret
1013}
1014
1015// ─────────────────────────────────────────────────────────────────────────
1016// pidfd_send_signal
1017// ─────────────────────────────────────────────────────────────────────────
1018
1019/// Envoie un signal au processus référencé par `pidfd`.
1020///
1021/// Cf. `docs/specs/layer-0/family-process.md`, sous-section 3,
1022/// `pidfd_send_signal`. À préférer à un futur `kill(pid, sig)` quand un
1023/// pidfd est disponible : pas de race sur le PID recyclé.
1024///
1025/// # `info`
1026///
1027/// Si `Some(info)`, le kernel utilise le `siginfo_t` fourni (sémantique
1028/// `rt_sigqueueinfo`). Si `None`, le kernel synthétise un `siginfo_t`
1029/// avec `si_code = SI_USER`, équivalent à un `kill(2)` standard.
1030///
1031/// **Note périmètre.** Cette PR n'expose **aucun constructeur public**
1032/// pour [`SignalInfo`] (stub partiel) ; depuis l'extérieur de la crate,
1033/// seul `None` est passable. Le bras `Some(_)` du wrapper reste
1034/// type-reachable mais value-unreachable jusqu'à la PR `family-signal`.
1035///
1036/// # Errors
1037///
1038/// - [`Errno::EBADF`] : pidfd invalide.
1039/// - [`Errno::EINVAL`] : signal invalide.
1040/// - [`Errno::EPERM`] : permissions insuffisantes pour signaler cette cible.
1041/// - [`Errno::ESRCH`] : le processus référencé n'existe plus.
1042pub fn pidfd_send_signal(
1043 pidfd: BorrowedFd<'_>,
1044 signal: Signal,
1045 info: Option<&SignalInfo>,
1046) -> Result<(), Errno> {
1047 let info_ptr: u64 = match info {
1048 Some(i) => {
1049 let p: *const SignalInfo = i;
1050 p as u64
1051 }
1052 None => 0,
1053 };
1054
1055 // SAFETY:
1056 // - `pidfd_send_signal(2)` lit `*info` si le pointeur est non nul ;
1057 // il ne **modifie** aucune mémoire utilisateur (`readonly` OK).
1058 // - Si `info_ptr == 0` (cas `None`), le kernel synthétise un siginfo.
1059 // - L'appelant garantit que `pidfd` est ouvert (BorrowedFd typé).
1060 // - `flags` (5e arg) est réservé : doit valoir 0.
1061 let ret = unsafe {
1062 raw_syscall_pidfd_send_signal(pidfd.as_raw_fd(), signal.as_raw(), info_ptr, 0_u32)
1063 };
1064 if ret < 0 {
1065 return Err(errno_from_negative_syscall_ret(ret));
1066 }
1067 Ok(())
1068}
1069
1070#[cfg(target_arch = "x86_64")]
1071#[inline]
1072unsafe fn raw_syscall_pidfd_send_signal(pidfd: i32, signal: i32, info: u64, flags: u32) -> i64 {
1073 let ret: i64;
1074 // SAFETY:
1075 // - SYS_pidfd_send_signal (x86_64 = 424). Lecture seulement de
1076 // `*info` (et seulement si non-null) ; pas d'écriture en mémoire
1077 // utilisateur, d'où `readonly`.
1078 // - ABI x86_64 : args en RDI/RSI/RDX/R10 ; retour en RAX ; clobbe
1079 // RCX et R11.
1080 unsafe {
1081 core::arch::asm!(
1082 "syscall",
1083 in("rax") 424_i64,
1084 in("rdi") i64::from(pidfd),
1085 in("rsi") i64::from(signal),
1086 in("rdx") info,
1087 in("r10") i64::from(flags),
1088 lateout("rax") ret,
1089 lateout("rcx") _,
1090 lateout("r11") _,
1091 options(nostack, preserves_flags, readonly),
1092 );
1093 }
1094 ret
1095}
1096
1097#[cfg(target_arch = "aarch64")]
1098#[inline]
1099unsafe fn raw_syscall_pidfd_send_signal(pidfd: i32, signal: i32, info: u64, flags: u32) -> i64 {
1100 let ret: i64;
1101 // SAFETY: ABI aarch64 (numéro en X8, args en X0..X3).
1102 unsafe {
1103 core::arch::asm!(
1104 "svc 0",
1105 in("x8") 424_i64,
1106 inout("x0") i64::from(pidfd) => ret,
1107 in("x1") i64::from(signal),
1108 in("x2") info,
1109 in("x3") i64::from(flags),
1110 options(nostack, preserves_flags, readonly),
1111 );
1112 }
1113 ret
1114}
1115
1116// ─────────────────────────────────────────────────────────────────────────
1117// pidfd_getfd
1118// ─────────────────────────────────────────────────────────────────────────
1119
1120/// Duplique un FD ouvert dans le processus référencé par `pidfd`.
1121///
1122/// Cf. `docs/specs/layer-0/family-process.md`, sous-section 3,
1123/// `pidfd_getfd`. Cas d'usage spécialisé : debuggers, outils de
1124/// surveillance, transfert de ressources entre processus coopérant.
1125///
1126/// Le FD retourné est **propriété de l'appelant** ([`OwnedFd`]) ; il
1127/// pointe sur la même entrée du file table que `target_fd` côté
1128/// processus cible. La fermeture côté cible ne ferme pas la copie locale,
1129/// et réciproquement.
1130///
1131/// # Errors
1132///
1133/// - [`Errno::EBADF`] : `pidfd` invalide, ou `target_fd` non ouvert
1134/// dans le processus cible.
1135/// - [`Errno::EPERM`] : pas de `CAP_SYS_PTRACE` et pas le même
1136/// utilisateur que la cible. Sur Ubuntu, Yama `ptrace_scope ≥ 2`
1137/// peut également bloquer.
1138/// - [`Errno::EINVAL`] : `flags` non nul (réservé).
1139/// - [`Errno::ESRCH`] : le processus référencé n'existe plus.
1140pub fn pidfd_getfd(pidfd: BorrowedFd<'_>, target_fd: RawFd, flags: u32) -> Result<OwnedFd, Errno> {
1141 // SAFETY:
1142 // - `pidfd_getfd(2)` ne touche aucune mémoire utilisateur : il
1143 // alloue côté kernel une nouvelle entrée dans la file table de
1144 // l'appelant pointant sur la même file que `target_fd` côté cible.
1145 // - `flags` est réservé (doit être 0 sur kernel actuel).
1146 let ret = unsafe { raw_syscall_pidfd_getfd(pidfd.as_raw_fd(), target_fd, flags) };
1147 if ret < 0 {
1148 return Err(errno_from_negative_syscall_ret(ret));
1149 }
1150 #[allow(clippy::cast_possible_truncation)]
1151 let new_fd: RawFd = ret as RawFd;
1152 // SAFETY: kernel vient de transférer la propriété d'un fd valide.
1153 Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
1154}
1155
1156#[cfg(target_arch = "x86_64")]
1157#[inline]
1158unsafe fn raw_syscall_pidfd_getfd(pidfd: i32, target_fd: i32, flags: u32) -> i64 {
1159 let ret: i64;
1160 // SAFETY: SYS_pidfd_getfd (x86_64 = 438) ne lit ni n'écrit en
1161 // mémoire utilisateur (`readonly`).
1162 unsafe {
1163 core::arch::asm!(
1164 "syscall",
1165 in("rax") 438_i64,
1166 in("rdi") i64::from(pidfd),
1167 in("rsi") i64::from(target_fd),
1168 in("rdx") i64::from(flags),
1169 lateout("rax") ret,
1170 lateout("rcx") _,
1171 lateout("r11") _,
1172 options(nostack, preserves_flags, readonly),
1173 );
1174 }
1175 ret
1176}
1177
1178#[cfg(target_arch = "aarch64")]
1179#[inline]
1180unsafe fn raw_syscall_pidfd_getfd(pidfd: i32, target_fd: i32, flags: u32) -> i64 {
1181 let ret: i64;
1182 // SAFETY: ABI aarch64 ; numéro en X8 (= 438), args en X0..X2.
1183 unsafe {
1184 core::arch::asm!(
1185 "svc 0",
1186 in("x8") 438_i64,
1187 inout("x0") i64::from(pidfd) => ret,
1188 in("x1") i64::from(target_fd),
1189 in("x2") i64::from(flags),
1190 options(nostack, preserves_flags, readonly),
1191 );
1192 }
1193 ret
1194}
1195
1196// ─────────────────────────────────────────────────────────────────────────
1197// Groupes et sessions (sous-section 4 de family-process.md).
1198// ─────────────────────────────────────────────────────────────────────────
1199
1200/// Place `pid` dans le process group `pgid`.
1201///
1202/// Cf. `docs/specs/layer-0/family-process.md` sous-section 4. Sémantique
1203/// POSIX standard ; cas d'usage : shells, démons, applications terminal.
1204///
1205/// `pid = None` désigne le processus courant (cf. ADR-021 convention 1,
1206/// remplaçant la sentinelle kernel `0`). `pgid = None` désigne « créer un
1207/// nouveau process group dont le leader est `pid` » (sentinelle `0` côté
1208/// kernel).
1209///
1210/// # Errors
1211///
1212/// - [`Errno::EINVAL`] : `pgid` ne désigne pas un process group valide
1213/// dans la session courante.
1214/// - [`Errno::EPERM`] : `pid` n'est pas l'appelant ou un de ses enfants,
1215/// ou tentative de déplacer un processus hors session.
1216/// - [`Errno::ESRCH`] : `pid` n'existe pas.
1217pub fn setpgid(pid: Option<Pid>, pgid: Option<Pid>) -> Result<(), Errno> {
1218 let pid_arg = pid.map_or(0_i32, |p| p.as_raw());
1219 let pgid_arg = pgid.map_or(0_i32, |p| p.as_raw());
1220 // SAFETY: setpgid(2) ne touche aucune mémoire utilisateur ; lecture
1221 // des deux pid_t scalaires.
1222 let ret = unsafe { raw_syscall_setpgid(pid_arg, pgid_arg) };
1223 if ret < 0 {
1224 return Err(errno_from_negative_syscall_ret(ret));
1225 }
1226 Ok(())
1227}
1228
1229/// Retourne le process group ID de `pid` (ou du processus courant si `None`).
1230///
1231/// # Errors
1232///
1233/// - [`Errno::ESRCH`] : `pid` n'existe pas.
1234pub fn getpgid(pid: Option<Pid>) -> Result<Pid, Errno> {
1235 let pid_arg = pid.map_or(0_i32, |p| p.as_raw());
1236 // SAFETY: getpgid(2) ne touche aucune mémoire utilisateur.
1237 let ret = unsafe { raw_syscall_getpgid(pid_arg) };
1238 if ret < 0 {
1239 return Err(errno_from_negative_syscall_ret(ret));
1240 }
1241 // PGID retourné est un pid_t > 0 (process group ID = pid du leader,
1242 // qui est lui-même positif). Troncature exacte.
1243 #[allow(clippy::cast_possible_truncation)]
1244 let raw = ret as i32;
1245 Ok(Pid::try_from_raw(raw).expect("kernel : pgid strictement positif"))
1246}
1247
1248/// Crée une nouvelle session avec l'appelant comme session leader, et le
1249/// place dans un nouveau process group dont il est le leader. Retourne le
1250/// nouveau session ID.
1251///
1252/// # Errors
1253///
1254/// - [`Errno::EPERM`] : l'appelant est déjà process group leader (le test
1255/// runner cargo est typiquement un pgrp leader → `setsid` échoue, ce
1256/// qui est exploité par les tests).
1257pub fn setsid() -> Result<Pid, Errno> {
1258 // SAFETY: setsid(2) ne touche aucune mémoire utilisateur.
1259 let ret = unsafe { raw_syscall_setsid() };
1260 if ret < 0 {
1261 return Err(errno_from_negative_syscall_ret(ret));
1262 }
1263 #[allow(clippy::cast_possible_truncation)]
1264 let raw = ret as i32;
1265 Ok(Pid::try_from_raw(raw).expect("kernel : nouveau SID strictement positif"))
1266}
1267
1268/// Retourne le session ID de `pid` (ou du processus courant si `None`).
1269///
1270/// # Errors
1271///
1272/// - [`Errno::EPERM`] : `pid` est dans une session différente de l'appelant
1273/// et le kernel refuse de divulguer (rare).
1274/// - [`Errno::ESRCH`] : `pid` n'existe pas.
1275pub fn getsid(pid: Option<Pid>) -> Result<Pid, Errno> {
1276 let pid_arg = pid.map_or(0_i32, |p| p.as_raw());
1277 // SAFETY: getsid(2) ne touche aucune mémoire utilisateur.
1278 let ret = unsafe { raw_syscall_getsid(pid_arg) };
1279 if ret < 0 {
1280 return Err(errno_from_negative_syscall_ret(ret));
1281 }
1282 #[allow(clippy::cast_possible_truncation)]
1283 let raw = ret as i32;
1284 Ok(Pid::try_from_raw(raw).expect("kernel : SID strictement positif"))
1285}
1286
1287// ─────────────────────────────────────────────────────────────────────────
1288// Séparation de privilèges (privsep) — setgroups/getgroups + setres*id/getres*id
1289//
1290// Spec : `docs/specs/layer-0/family-process-privsep.md`.
1291//
1292// Opérations **privilégiées** (`CAP_SETUID`/`CAP_SETGID` ou root). Prérequis
1293// d'un `drop_privileges` correct (`air-process`, couche 1) : larguer les
1294// **groupes supplémentaires** ET fixer le ***saved-set*-id** — sans quoi un
1295// `setuid`/`setgid` simple laisse regagner les privilèges.
1296// ─────────────────────────────────────────────────────────────────────────
1297
1298/// `NGROUPS_MAX` (Linux) : nombre maximal de groupes supplémentaires. Au-delà,
1299/// `setgroups(2)` rend `EINVAL` ; on le valide **en amont** (Principe 4).
1300const NGROUPS_MAX: usize = 65536;
1301
1302/// Encode un `Option<Gid>` pour `setresgid` : `None` ⇒ `(gid_t)-1` (composante
1303/// **inchangée**), `Some(g)` ⇒ sa valeur brute. La sentinelle `-1` n'est jamais
1304/// exposée à l'appelant (ADR-021 conv. 1).
1305fn opt_gid_to_raw(gid: Option<Gid>) -> u32 {
1306 gid.map_or(u32::MAX, Gid::as_raw)
1307}
1308
1309/// Encode un `Option<Uid>` pour `setresuid` (`None` ⇒ `(uid_t)-1`). Voir
1310/// [`opt_gid_to_raw`].
1311fn opt_uid_to_raw(uid: Option<Uid>) -> u32 {
1312 uid.map_or(u32::MAX, Uid::as_raw)
1313}
1314
1315/// Remplace la liste des **groupes supplémentaires** du processus (`setgroups`).
1316///
1317/// Pour une réduction de privilèges, passer **`&[]`** (largage total) tant qu'on
1318/// est privilégié : la liste vide est la valeur **normale** du privsep, pas une
1319/// sentinelle. Le layout de `&[Gid]` coïncide avec `&[gid_t]` (`Gid` est
1320/// `#[repr(transparent)]`), donc aucune copie n'est nécessaire.
1321///
1322/// # Errors
1323///
1324/// - [`Errno::EINVAL`] : plus de `NGROUPS_MAX` (65536) groupes (validé en amont).
1325/// - [`Errno::EPERM`] : `CAP_SETGID` absent.
1326/// - [`Errno::EFAULT`] : `groups` hors de l'espace d'adressage (n'arrive pas avec
1327/// une tranche Rust valide).
1328///
1329/// # Examples
1330///
1331/// ```no_run
1332/// use air_sys_syscall::process::set_groups;
1333/// // Largage total des groupes supplémentaires (étape d'un privsep).
1334/// set_groups(&[]).expect("CAP_SETGID requis");
1335/// ```
1336pub fn set_groups(groups: &[Gid]) -> Result<(), Errno> {
1337 // Validation amont (Principe 4) : borne kernel.
1338 if groups.len() > NGROUPS_MAX {
1339 return Err(Errno::EINVAL);
1340 }
1341 let size = groups.len();
1342 let list_ptr = groups.as_ptr() as u64;
1343 // SAFETY: setgroups(2) **lit** `size` entrées `gid_t` à `list_ptr` et n'écrit
1344 // aucune mémoire utilisateur. `groups` est une tranche Rust valide pour la
1345 // durée de l'appel ; `Gid` est `#[repr(transparent)]` sur `u32` (= `gid_t`),
1346 // donc le pointeur et le compte décrivent un tableau de `gid_t` correct.
1347 // `size ≤ NGROUPS_MAX` est validé ci-dessus.
1348 let ret = unsafe { raw_syscall_setgroups(size, list_ptr) };
1349 if ret < 0 {
1350 return Err(errno_from_negative_syscall_ret(ret));
1351 }
1352 Ok(())
1353}
1354
1355/// Lit la liste courante des **groupes supplémentaires** (`getgroups`) dans le
1356/// `buffer` fourni — **zéro allocation** — et retourne la sous-tranche remplie.
1357///
1358/// Vérification défensive de `drop_privileges` (Principe 5 : confirmer la
1359/// réduction). Le `buffer` doit pouvoir contenir tous les groupes courants.
1360///
1361/// # Errors
1362///
1363/// - [`Errno::EINVAL`] : `buffer` trop petit pour la liste complète.
1364/// - [`Errno::EFAULT`] : `buffer` invalide (n'arrive pas avec une tranche Rust).
1365///
1366/// # Examples
1367///
1368/// ```no_run
1369/// use air_sys_syscall::process::get_groups;
1370/// use air_sys_types::Gid;
1371/// let mut buf = [Gid::from_raw(0); 64];
1372/// let groups = get_groups(&mut buf).expect("getgroups");
1373/// assert!(groups.len() <= 64);
1374/// ```
1375pub fn get_groups(buffer: &mut [Gid]) -> Result<&[Gid], Errno> {
1376 let size = buffer.len();
1377 let list_ptr = buffer.as_mut_ptr() as u64;
1378 // SAFETY: getgroups(2) **écrit** jusqu'à `size` entrées `gid_t` à `list_ptr`
1379 // et retourne le nombre écrit. `buffer` est une tranche mutable valide pour
1380 // la durée de l'appel ; `Gid` est `#[repr(transparent)]` sur `gid_t`.
1381 let ret = unsafe { raw_syscall_getgroups(size, list_ptr) };
1382 if ret < 0 {
1383 return Err(errno_from_negative_syscall_ret(ret));
1384 }
1385 // `ret` = nombre de groupes écrits (`0 ≤ ret ≤ size`). Conversion exacte.
1386 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1387 let count = ret as usize;
1388 // `count ≤ size` garanti par le kernel ; `get` évite toute indexation
1389 // panic-able (le bras `None` est structurellement inatteignable).
1390 buffer.get(..count).ok_or(Errno::EINVAL)
1391}
1392
1393/// Fixe **real + effective + saved** GID en un seul appel (`setresgid`).
1394///
1395/// À effectuer **avant** [`set_resuid`] (on ne peut plus changer de GID une fois
1396/// l'UID réduit). `None` pour une composante = « **inchangée** » (sentinelle
1397/// kernel `(gid_t)-1` typée, ADR-021 conv. 1). Fixer le *saved-set-gid* est ce
1398/// qui interdit le **retour** au GID privilégié.
1399///
1400/// # Errors
1401///
1402/// - [`Errno::EPERM`] : changement non autorisé (sans `CAP_SETGID`).
1403/// - [`Errno::EINVAL`] : un des GID n'est pas valide.
1404pub fn set_resgid(
1405 real: Option<Gid>,
1406 effective: Option<Gid>,
1407 saved: Option<Gid>,
1408) -> Result<(), Errno> {
1409 let real = opt_gid_to_raw(real);
1410 let effective = opt_gid_to_raw(effective);
1411 let saved = opt_gid_to_raw(saved);
1412 // SAFETY: setresgid(2) ne touche aucune mémoire utilisateur (3 scalaires
1413 // `gid_t`). `(gid_t)-1` laisse la composante inchangée.
1414 let ret = unsafe { raw_syscall_setresgid(real, effective, saved) };
1415 if ret < 0 {
1416 return Err(errno_from_negative_syscall_ret(ret));
1417 }
1418 Ok(())
1419}
1420
1421/// Fixe **real + effective + saved** UID en un seul appel (`setresuid`).
1422///
1423/// À effectuer **après** [`set_resgid`] (et après [`set_groups`]). `None` =
1424/// composante inchangée (`(uid_t)-1` typé). Fixer le *saved-set-uid* rend la
1425/// réduction de privilèges **irréversible** — garantie qu'un `setuid` simple ne
1426/// donne pas de façon fiable.
1427///
1428/// # Errors
1429///
1430/// - [`Errno::EPERM`] : changement non autorisé (sans `CAP_SETUID`).
1431/// - [`Errno::EINVAL`] : un des UID n'est pas valide.
1432pub fn set_resuid(
1433 real: Option<Uid>,
1434 effective: Option<Uid>,
1435 saved: Option<Uid>,
1436) -> Result<(), Errno> {
1437 let real = opt_uid_to_raw(real);
1438 let effective = opt_uid_to_raw(effective);
1439 let saved = opt_uid_to_raw(saved);
1440 // SAFETY: setresuid(2) ne touche aucune mémoire utilisateur (3 scalaires
1441 // `uid_t`). `(uid_t)-1` laisse la composante inchangée.
1442 let ret = unsafe { raw_syscall_setresuid(real, effective, saved) };
1443 if ret < 0 {
1444 return Err(errno_from_negative_syscall_ret(ret));
1445 }
1446 Ok(())
1447}
1448
1449/// Lit les trois composantes GID **réelle / effective / sauvegardée**
1450/// (`getresgid`) — vérification défensive (confirmer qu'on ne peut plus revenir
1451/// en arrière).
1452///
1453/// # Errors
1454///
1455/// - [`Errno::EFAULT`] : pointeurs invalides (n'arrive pas : pile locale).
1456pub fn get_resgid() -> Result<ResGid, Errno> {
1457 let mut real: u32 = 0;
1458 let mut effective: u32 = 0;
1459 let mut saved: u32 = 0;
1460 // SAFETY: getresgid(2) **écrit** trois `gid_t` aux pointeurs fournis. Les
1461 // trois variables locales sont vivantes, alignées et exclusivement
1462 // empruntées (`from_mut`) pour la durée de l'appel.
1463 let ret = unsafe {
1464 raw_syscall_getresgid(
1465 core::ptr::from_mut(&mut real) as u64,
1466 core::ptr::from_mut(&mut effective) as u64,
1467 core::ptr::from_mut(&mut saved) as u64,
1468 )
1469 };
1470 if ret < 0 {
1471 return Err(errno_from_negative_syscall_ret(ret));
1472 }
1473 Ok(ResGid {
1474 real: Gid::from_raw(real),
1475 effective: Gid::from_raw(effective),
1476 saved: Gid::from_raw(saved),
1477 })
1478}
1479
1480/// Lit les trois composantes UID **réelle / effective / sauvegardée**
1481/// (`getresuid`) — vérification défensive.
1482///
1483/// # Errors
1484///
1485/// - [`Errno::EFAULT`] : pointeurs invalides (n'arrive pas : pile locale).
1486pub fn get_resuid() -> Result<ResUid, Errno> {
1487 let mut real: u32 = 0;
1488 let mut effective: u32 = 0;
1489 let mut saved: u32 = 0;
1490 // SAFETY: getresuid(2) **écrit** trois `uid_t` aux pointeurs fournis. Voir
1491 // [`get_resgid`] pour les préconditions (variables pile vivantes/alignées).
1492 let ret = unsafe {
1493 raw_syscall_getresuid(
1494 core::ptr::from_mut(&mut real) as u64,
1495 core::ptr::from_mut(&mut effective) as u64,
1496 core::ptr::from_mut(&mut saved) as u64,
1497 )
1498 };
1499 if ret < 0 {
1500 return Err(errno_from_negative_syscall_ret(ret));
1501 }
1502 Ok(ResUid {
1503 real: Uid::from_raw(real),
1504 effective: Uid::from_raw(effective),
1505 saved: Uid::from_raw(saved),
1506 })
1507}
1508
1509#[cfg(target_arch = "x86_64")]
1510#[inline]
1511unsafe fn raw_syscall_setpgid(pid: i32, pgid: i32) -> i64 {
1512 let ret: i64;
1513 // SAFETY: SYS_setpgid (x86_64 = 109) — pas d'accès mémoire utilisateur ;
1514 // ABI x86_64 standard (RAX=nr, RDI/RSI=args ; clobbers RCX/R11).
1515 unsafe {
1516 core::arch::asm!(
1517 "syscall",
1518 in("rax") 109_i64,
1519 in("rdi") i64::from(pid),
1520 in("rsi") i64::from(pgid),
1521 lateout("rax") ret,
1522 lateout("rcx") _,
1523 lateout("r11") _,
1524 options(nostack, preserves_flags, readonly),
1525 );
1526 }
1527 ret
1528}
1529
1530#[cfg(target_arch = "x86_64")]
1531#[inline]
1532unsafe fn raw_syscall_getpgid(pid: i32) -> i64 {
1533 let ret: i64;
1534 // SAFETY: SYS_getpgid (x86_64 = 121) — pas d'accès mémoire utilisateur.
1535 unsafe {
1536 core::arch::asm!(
1537 "syscall",
1538 in("rax") 121_i64,
1539 in("rdi") i64::from(pid),
1540 lateout("rax") ret,
1541 lateout("rcx") _,
1542 lateout("r11") _,
1543 options(nostack, preserves_flags, readonly),
1544 );
1545 }
1546 ret
1547}
1548
1549#[cfg(target_arch = "x86_64")]
1550#[inline]
1551unsafe fn raw_syscall_setsid() -> i64 {
1552 let ret: i64;
1553 // SAFETY: SYS_setsid (x86_64 = 112) — pas d'accès mémoire utilisateur,
1554 // pas d'argument.
1555 unsafe {
1556 core::arch::asm!(
1557 "syscall",
1558 in("rax") 112_i64,
1559 lateout("rax") ret,
1560 lateout("rcx") _,
1561 lateout("r11") _,
1562 options(nostack, preserves_flags, readonly),
1563 );
1564 }
1565 ret
1566}
1567
1568#[cfg(target_arch = "x86_64")]
1569#[inline]
1570unsafe fn raw_syscall_getsid(pid: i32) -> i64 {
1571 let ret: i64;
1572 // SAFETY: SYS_getsid (x86_64 = 124) — pas d'accès mémoire utilisateur.
1573 unsafe {
1574 core::arch::asm!(
1575 "syscall",
1576 in("rax") 124_i64,
1577 in("rdi") i64::from(pid),
1578 lateout("rax") ret,
1579 lateout("rcx") _,
1580 lateout("r11") _,
1581 options(nostack, preserves_flags, readonly),
1582 );
1583 }
1584 ret
1585}
1586
1587#[cfg(target_arch = "aarch64")]
1588#[inline]
1589unsafe fn raw_syscall_setpgid(pid: i32, pgid: i32) -> i64 {
1590 let ret: i64;
1591 // SAFETY: SYS_setpgid (aarch64 = 154) — ABI standard, no mem access.
1592 unsafe {
1593 core::arch::asm!(
1594 "svc 0",
1595 in("x8") 154_i64,
1596 inout("x0") i64::from(pid) => ret,
1597 in("x1") i64::from(pgid),
1598 options(nostack, preserves_flags, readonly),
1599 );
1600 }
1601 ret
1602}
1603
1604#[cfg(target_arch = "aarch64")]
1605#[inline]
1606unsafe fn raw_syscall_getpgid(pid: i32) -> i64 {
1607 let ret: i64;
1608 // SAFETY: SYS_getpgid (aarch64 = 155).
1609 unsafe {
1610 core::arch::asm!(
1611 "svc 0",
1612 in("x8") 155_i64,
1613 inout("x0") i64::from(pid) => ret,
1614 options(nostack, preserves_flags, readonly),
1615 );
1616 }
1617 ret
1618}
1619
1620#[cfg(target_arch = "aarch64")]
1621#[inline]
1622unsafe fn raw_syscall_setsid() -> i64 {
1623 let ret: i64;
1624 // SAFETY: SYS_setsid (aarch64 = 157).
1625 unsafe {
1626 core::arch::asm!(
1627 "svc 0",
1628 in("x8") 157_i64,
1629 lateout("x0") ret,
1630 options(nostack, preserves_flags, readonly),
1631 );
1632 }
1633 ret
1634}
1635
1636#[cfg(target_arch = "aarch64")]
1637#[inline]
1638unsafe fn raw_syscall_getsid(pid: i32) -> i64 {
1639 let ret: i64;
1640 // SAFETY: SYS_getsid (aarch64 = 156).
1641 unsafe {
1642 core::arch::asm!(
1643 "svc 0",
1644 in("x8") 156_i64,
1645 inout("x0") i64::from(pid) => ret,
1646 options(nostack, preserves_flags, readonly),
1647 );
1648 }
1649 ret
1650}
1651
1652// ─────────────────────────────────────────────────────────────────────────
1653// Helpers syscall bruts — privsep (numéros vérifiés sur l'uapi 6.12 par arche :
1654// x86_64 = unistd_64.h, aarch64 = asm-generic/unistd.h).
1655// ─────────────────────────────────────────────────────────────────────────
1656
1657#[cfg(target_arch = "x86_64")]
1658#[inline]
1659unsafe fn raw_syscall_setgroups(size: usize, list: u64) -> i64 {
1660 let ret: i64;
1661 // SAFETY: SYS_setgroups (x86_64 = 116). **Lit** `size` `gid_t` à `list` ;
1662 // n'écrit aucune mémoire utilisateur (`readonly`).
1663 unsafe {
1664 core::arch::asm!(
1665 "syscall",
1666 in("rax") 116_i64,
1667 in("rdi") size,
1668 in("rsi") list,
1669 lateout("rax") ret,
1670 lateout("rcx") _,
1671 lateout("r11") _,
1672 options(nostack, preserves_flags, readonly),
1673 );
1674 }
1675 ret
1676}
1677
1678#[cfg(target_arch = "aarch64")]
1679#[inline]
1680unsafe fn raw_syscall_setgroups(size: usize, list: u64) -> i64 {
1681 let ret: i64;
1682 // SAFETY: SYS_setgroups (aarch64 = 159). Lit la liste ; n'écrit rien.
1683 unsafe {
1684 core::arch::asm!(
1685 "svc 0",
1686 in("x8") 159_i64,
1687 inout("x0") size => ret,
1688 in("x1") list,
1689 options(nostack, preserves_flags, readonly),
1690 );
1691 }
1692 ret
1693}
1694
1695#[cfg(target_arch = "x86_64")]
1696#[inline]
1697unsafe fn raw_syscall_getgroups(size: usize, list: u64) -> i64 {
1698 let ret: i64;
1699 // SAFETY: SYS_getgroups (x86_64 = 115). **Écrit** jusqu'à `size` `gid_t` à
1700 // `list` (pas de `readonly`). Retourne le nombre écrit ou `-errno`.
1701 unsafe {
1702 core::arch::asm!(
1703 "syscall",
1704 in("rax") 115_i64,
1705 in("rdi") size,
1706 in("rsi") list,
1707 lateout("rax") ret,
1708 lateout("rcx") _,
1709 lateout("r11") _,
1710 options(nostack, preserves_flags),
1711 );
1712 }
1713 ret
1714}
1715
1716#[cfg(target_arch = "aarch64")]
1717#[inline]
1718unsafe fn raw_syscall_getgroups(size: usize, list: u64) -> i64 {
1719 let ret: i64;
1720 // SAFETY: SYS_getgroups (aarch64 = 158). Écrit la liste (pas de `readonly`).
1721 unsafe {
1722 core::arch::asm!(
1723 "svc 0",
1724 in("x8") 158_i64,
1725 inout("x0") size => ret,
1726 in("x1") list,
1727 options(nostack, preserves_flags),
1728 );
1729 }
1730 ret
1731}
1732
1733#[cfg(target_arch = "x86_64")]
1734#[inline]
1735unsafe fn raw_syscall_setresgid(real: u32, effective: u32, saved: u32) -> i64 {
1736 let ret: i64;
1737 // SAFETY: SYS_setresgid (x86_64 = 119). Trois scalaires `gid_t` ; aucun
1738 // accès mémoire utilisateur (`readonly`).
1739 unsafe {
1740 core::arch::asm!(
1741 "syscall",
1742 in("rax") 119_i64,
1743 in("rdi") i64::from(real),
1744 in("rsi") i64::from(effective),
1745 in("rdx") i64::from(saved),
1746 lateout("rax") ret,
1747 lateout("rcx") _,
1748 lateout("r11") _,
1749 options(nostack, preserves_flags, readonly),
1750 );
1751 }
1752 ret
1753}
1754
1755#[cfg(target_arch = "aarch64")]
1756#[inline]
1757unsafe fn raw_syscall_setresgid(real: u32, effective: u32, saved: u32) -> i64 {
1758 let ret: i64;
1759 // SAFETY: SYS_setresgid (aarch64 = 149). Trois scalaires ; pas d'accès mém.
1760 unsafe {
1761 core::arch::asm!(
1762 "svc 0",
1763 in("x8") 149_i64,
1764 inout("x0") i64::from(real) => ret,
1765 in("x1") i64::from(effective),
1766 in("x2") i64::from(saved),
1767 options(nostack, preserves_flags, readonly),
1768 );
1769 }
1770 ret
1771}
1772
1773#[cfg(target_arch = "x86_64")]
1774#[inline]
1775unsafe fn raw_syscall_setresuid(real: u32, effective: u32, saved: u32) -> i64 {
1776 let ret: i64;
1777 // SAFETY: SYS_setresuid (x86_64 = 117). Trois scalaires `uid_t` ; `readonly`.
1778 unsafe {
1779 core::arch::asm!(
1780 "syscall",
1781 in("rax") 117_i64,
1782 in("rdi") i64::from(real),
1783 in("rsi") i64::from(effective),
1784 in("rdx") i64::from(saved),
1785 lateout("rax") ret,
1786 lateout("rcx") _,
1787 lateout("r11") _,
1788 options(nostack, preserves_flags, readonly),
1789 );
1790 }
1791 ret
1792}
1793
1794#[cfg(target_arch = "aarch64")]
1795#[inline]
1796unsafe fn raw_syscall_setresuid(real: u32, effective: u32, saved: u32) -> i64 {
1797 let ret: i64;
1798 // SAFETY: SYS_setresuid (aarch64 = 147). Trois scalaires ; pas d'accès mém.
1799 unsafe {
1800 core::arch::asm!(
1801 "svc 0",
1802 in("x8") 147_i64,
1803 inout("x0") i64::from(real) => ret,
1804 in("x1") i64::from(effective),
1805 in("x2") i64::from(saved),
1806 options(nostack, preserves_flags, readonly),
1807 );
1808 }
1809 ret
1810}
1811
1812#[cfg(target_arch = "x86_64")]
1813#[inline]
1814unsafe fn raw_syscall_getresgid(real: u64, effective: u64, saved: u64) -> i64 {
1815 let ret: i64;
1816 // SAFETY: SYS_getresgid (x86_64 = 120). **Écrit** trois `gid_t` aux trois
1817 // pointeurs (pas de `readonly`).
1818 unsafe {
1819 core::arch::asm!(
1820 "syscall",
1821 in("rax") 120_i64,
1822 in("rdi") real,
1823 in("rsi") effective,
1824 in("rdx") saved,
1825 lateout("rax") ret,
1826 lateout("rcx") _,
1827 lateout("r11") _,
1828 options(nostack, preserves_flags),
1829 );
1830 }
1831 ret
1832}
1833
1834#[cfg(target_arch = "aarch64")]
1835#[inline]
1836unsafe fn raw_syscall_getresgid(real: u64, effective: u64, saved: u64) -> i64 {
1837 let ret: i64;
1838 // SAFETY: SYS_getresgid (aarch64 = 150). Écrit trois `gid_t` (pas readonly).
1839 unsafe {
1840 core::arch::asm!(
1841 "svc 0",
1842 in("x8") 150_i64,
1843 inout("x0") real => ret,
1844 in("x1") effective,
1845 in("x2") saved,
1846 options(nostack, preserves_flags),
1847 );
1848 }
1849 ret
1850}
1851
1852#[cfg(target_arch = "x86_64")]
1853#[inline]
1854unsafe fn raw_syscall_getresuid(real: u64, effective: u64, saved: u64) -> i64 {
1855 let ret: i64;
1856 // SAFETY: SYS_getresuid (x86_64 = 118). **Écrit** trois `uid_t` aux trois
1857 // pointeurs (pas de `readonly`).
1858 unsafe {
1859 core::arch::asm!(
1860 "syscall",
1861 in("rax") 118_i64,
1862 in("rdi") real,
1863 in("rsi") effective,
1864 in("rdx") saved,
1865 lateout("rax") ret,
1866 lateout("rcx") _,
1867 lateout("r11") _,
1868 options(nostack, preserves_flags),
1869 );
1870 }
1871 ret
1872}
1873
1874#[cfg(target_arch = "aarch64")]
1875#[inline]
1876unsafe fn raw_syscall_getresuid(real: u64, effective: u64, saved: u64) -> i64 {
1877 let ret: i64;
1878 // SAFETY: SYS_getresuid (aarch64 = 148). Écrit trois `uid_t` (pas readonly).
1879 unsafe {
1880 core::arch::asm!(
1881 "svc 0",
1882 in("x8") 148_i64,
1883 inout("x0") real => ret,
1884 in("x1") effective,
1885 in("x2") saved,
1886 options(nostack, preserves_flags),
1887 );
1888 }
1889 ret
1890}
1891
1892// ─────────────────────────────────────────────────────────────────────────
1893// prctl (sous-section 5 de family-process.md).
1894//
1895// Convention 3 ADR-021 : `prctl(op, args...)` n'est PAS exposé. Chaque
1896// opération est exposée comme fonction dédiée typée. Le helper
1897// `raw_syscall_prctl` est strictement interne au module.
1898// ─────────────────────────────────────────────────────────────────────────
1899
1900// Opérations `prctl(2)` exposées par cette PR (cf. `include/uapi/linux/prctl.h`).
1901const PR_SET_PDEATHSIG: i32 = 1;
1902const PR_GET_PDEATHSIG: i32 = 2;
1903const PR_GET_DUMPABLE: i32 = 3;
1904const PR_SET_DUMPABLE: i32 = 4;
1905const PR_GET_KEEPCAPS: i32 = 7;
1906const PR_SET_KEEPCAPS: i32 = 8;
1907const PR_SET_NAME: i32 = 15;
1908const PR_GET_NAME: i32 = 16;
1909const PR_SET_TIMERSLACK: i32 = 29;
1910const PR_SET_NO_NEW_PRIVS: i32 = 38;
1911const PR_GET_NO_NEW_PRIVS: i32 = 39;
1912const PR_CAP_AMBIENT: i32 = 47;
1913
1914// Sous-opérations de PR_CAP_AMBIENT (arg2).
1915const PR_CAP_AMBIENT_IS_SET: u64 = 1;
1916const PR_CAP_AMBIENT_RAISE: u64 = 2;
1917const PR_CAP_AMBIENT_LOWER: u64 = 3;
1918const PR_CAP_AMBIENT_CLEAR_ALL: u64 = 4;
1919
1920/// Définit le signal envoyé au processus courant à la mort de son parent.
1921///
1922/// `None` désactive la notification (le kernel envoie 0, traité comme
1923/// « aucun signal »).
1924///
1925/// # Errors
1926///
1927/// - [`Errno::EINVAL`] : signal hors plage.
1928pub fn set_parent_death_signal(signal: Option<Signal>) -> Result<(), Errno> {
1929 let sig_arg: u64 = signal.map_or(0_u64, |s| {
1930 // s.as_raw() > 0 par construction (NonZeroI32). Cast sign-safe.
1931 #[allow(clippy::cast_sign_loss)]
1932 {
1933 s.as_raw() as u64
1934 }
1935 });
1936 // SAFETY: PR_SET_PDEATHSIG — pas d'accès mémoire utilisateur.
1937 let ret = unsafe { raw_syscall_prctl(PR_SET_PDEATHSIG, sig_arg, 0, 0, 0) };
1938 if ret < 0 {
1939 return Err(errno_from_negative_syscall_ret(ret));
1940 }
1941 Ok(())
1942}
1943
1944/// Retourne le signal envoyé au processus à la mort du parent.
1945/// `Ok(None)` si la notification est désactivée.
1946///
1947/// # Errors
1948///
1949/// - [`Errno::EINVAL`] : ne se produit pas pour PR_GET_PDEATHSIG en
1950/// pratique.
1951pub fn get_parent_death_signal() -> Result<Option<Signal>, Errno> {
1952 // Le kernel écrit le signal dans `*(int *)arg2`.
1953 let mut out: i32 = 0;
1954 let out_ptr: *mut i32 = &mut out;
1955 // SAFETY: PR_GET_PDEATHSIG écrit un int à arg2 ; `out` est local
1956 // valide pour toute la durée du syscall.
1957 let ret = unsafe { raw_syscall_prctl(PR_GET_PDEATHSIG, out_ptr as u64, 0, 0, 0) };
1958 if ret < 0 {
1959 return Err(errno_from_negative_syscall_ret(ret));
1960 }
1961 Ok(Signal::try_from_raw(out))
1962}
1963
1964/// Active le bit `no_new_privs` du processus.
1965///
1966/// **Opération irréversible.** Une fois positionné, ce bit ne peut pas
1967/// être effacé pour la durée de vie du processus — toute tentative
1968/// (`set_no_new_privs` après coup, ou via execve d'un binaire SUID) est
1969/// neutralisée par le kernel. Conséquence pour les tests : exécuter
1970/// `set_no_new_privs()` dans un test contamine **tous les tests
1971/// ultérieurs** dans la même process. Cette PR ne teste donc que
1972/// `get_no_new_privs()` ; le set est laissé à la PR `family-security`
1973/// (seccomp) qui s'exécute dans un processus enfant fork'é.
1974///
1975/// # Errors
1976///
1977/// - [`Errno::EINVAL`] : ne se produit pas en pratique.
1978pub fn set_no_new_privs() -> Result<(), Errno> {
1979 // arg2 doit être 1 selon le contrat kernel ; toute autre valeur
1980 // retourne EINVAL.
1981 // SAFETY: PR_SET_NO_NEW_PRIVS — pas d'accès mémoire utilisateur.
1982 let ret = unsafe { raw_syscall_prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
1983 if ret < 0 {
1984 return Err(errno_from_negative_syscall_ret(ret));
1985 }
1986 Ok(())
1987}
1988
1989/// Indique si le bit `no_new_privs` est positionné pour le processus.
1990///
1991/// # Errors
1992///
1993/// - Aucune en pratique pour cette opération.
1994pub fn get_no_new_privs() -> Result<bool, Errno> {
1995 // SAFETY: PR_GET_NO_NEW_PRIVS — pas d'accès mémoire utilisateur ; la
1996 // valeur est retournée comme code de retour du syscall (0 ou 1).
1997 let ret = unsafe { raw_syscall_prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) };
1998 if ret < 0 {
1999 return Err(errno_from_negative_syscall_ret(ret));
2000 }
2001 Ok(ret != 0)
2002}
2003
2004/// Définit le nom du thread (max 15 caractères + NUL).
2005///
2006/// Le kernel tronque silencieusement à 15 caractères si `name` est plus
2007/// long. Le nom est utilisé par `/proc/PID/task/TID/comm` et exposé par
2008/// `top`/`ps -L`.
2009///
2010/// # Errors
2011///
2012/// - `EFAULT` : pointeur invalide ; ne se produit pas via l'API safe.
2013pub fn set_thread_name(name: &CStr) -> Result<(), Errno> {
2014 let ptr = name.as_ptr();
2015 // SAFETY: PR_SET_NAME lit la C-string à arg2 (kernel tronque à 16
2016 // octets). `name` est un &CStr valide pour la durée du call.
2017 let ret = unsafe { raw_syscall_prctl(PR_SET_NAME, ptr as u64, 0, 0, 0) };
2018 if ret < 0 {
2019 return Err(errno_from_negative_syscall_ret(ret));
2020 }
2021 Ok(())
2022}
2023
2024/// Retourne le nom du thread courant.
2025///
2026/// # Allocation
2027///
2028/// Convention 4 ADR-021 exception explicite : « la sémantique exige un
2029/// type owned ». Le kernel écrit ≤ 16 octets dans un buffer transient ;
2030/// le caller récupère une `CString` qui peut sortir du frame. Allocation
2031/// bornée à 16 octets (un seul `Box<[u8]>`), happy-path.
2032///
2033/// # Errors
2034///
2035/// - Aucune en pratique pour cette opération.
2036pub fn get_thread_name() -> Result<CString, Errno> {
2037 let mut buffer: [u8; 16] = [0; 16];
2038 let ptr: *mut u8 = buffer.as_mut_ptr();
2039 // SAFETY: PR_GET_NAME écrit exactement 16 octets dans `*(char *)arg2` ;
2040 // `buffer` est local valide pour la durée du call. Le kernel garantit la
2041 // terminaison NUL (en tronquant si nécessaire).
2042 let ret = unsafe { raw_syscall_prctl(PR_GET_NAME, ptr as u64, 0, 0, 0) };
2043 if ret < 0 {
2044 return Err(errno_from_negative_syscall_ret(ret));
2045 }
2046 // Le buffer est garanti contenir au moins un NUL (le kernel tronque
2047 // à 15 caractères + NUL). On extrait la C-string jusqu'au premier NUL.
2048 let length = buffer.iter().position(|&b| b == 0).unwrap_or(16);
2049 // CString::new exige absence de NUL interne ; on passe les bytes
2050 // *avant* le NUL trouvé.
2051 Ok(CString::new(&buffer[..length])
2052 .expect("buffer kernel sans NUL interne avant le terminateur"))
2053}
2054
2055/// Définit le mode `dumpable` du processus.
2056///
2057/// # Errors
2058///
2059/// - [`Errno::EINVAL`] : valeur hors plage (ne se produit pas via l'API
2060/// typée).
2061pub fn set_dumpable(mode: DumpableMode) -> Result<(), Errno> {
2062 let mode_arg = u64::from(mode as u32);
2063 // SAFETY: PR_SET_DUMPABLE — pas d'accès mémoire utilisateur.
2064 let ret = unsafe { raw_syscall_prctl(PR_SET_DUMPABLE, mode_arg, 0, 0, 0) };
2065 if ret < 0 {
2066 return Err(errno_from_negative_syscall_ret(ret));
2067 }
2068 Ok(())
2069}
2070
2071/// Retourne le mode `dumpable` actuel.
2072///
2073/// # Errors
2074///
2075/// - Aucune en pratique pour cette opération.
2076pub fn get_dumpable() -> Result<DumpableMode, Errno> {
2077 // SAFETY: PR_GET_DUMPABLE — pas d'accès mémoire utilisateur ;
2078 // valeur retournée comme code de retour.
2079 let ret = unsafe { raw_syscall_prctl(PR_GET_DUMPABLE, 0, 0, 0, 0) };
2080 if ret < 0 {
2081 return Err(errno_from_negative_syscall_ret(ret));
2082 }
2083 match ret {
2084 0 => Ok(DumpableMode::NotDumpable),
2085 1 => Ok(DumpableMode::Dumpable),
2086 2 => Ok(DumpableMode::SuidDumpable),
2087 // Le kernel garantit 0, 1, ou 2. Sortie hors plage = bug kernel.
2088 other => unreachable!("PR_GET_DUMPABLE a retourné {other}, hors plage [0,2]"),
2089 }
2090}
2091
2092/// Définit le bit `keep_caps` : si vrai, les capabilities `permitted`
2093/// sont conservées lors d'un changement d'UID effectif vers non-zéro.
2094///
2095/// # Errors
2096///
2097/// - [`Errno::EINVAL`] : valeur invalide (ne se produit pas via l'API typée).
2098pub fn set_keep_caps(keep: bool) -> Result<(), Errno> {
2099 let arg: u64 = u64::from(keep);
2100 // SAFETY: PR_SET_KEEPCAPS — pas d'accès mémoire utilisateur.
2101 let ret = unsafe { raw_syscall_prctl(PR_SET_KEEPCAPS, arg, 0, 0, 0) };
2102 if ret < 0 {
2103 return Err(errno_from_negative_syscall_ret(ret));
2104 }
2105 Ok(())
2106}
2107
2108/// Indique si le bit `keep_caps` est positionné.
2109///
2110/// # Errors
2111///
2112/// - Aucune en pratique pour cette opération.
2113pub fn get_keep_caps() -> Result<bool, Errno> {
2114 // SAFETY: PR_GET_KEEPCAPS — pas d'accès mémoire utilisateur.
2115 let ret = unsafe { raw_syscall_prctl(PR_GET_KEEPCAPS, 0, 0, 0, 0) };
2116 if ret < 0 {
2117 return Err(errno_from_negative_syscall_ret(ret));
2118 }
2119 Ok(ret != 0)
2120}
2121
2122/// Définit la « timer slack » du processus (nanosecondes).
2123///
2124/// Le timer slack permet au kernel d'agréger les timers proches pour
2125/// réduire les wake-ups. `0` réinitialise à la valeur par défaut héritée.
2126///
2127/// # Errors
2128///
2129/// - Aucune en pratique.
2130pub fn set_timer_slack(slack_ns: u64) -> Result<(), Errno> {
2131 // SAFETY: PR_SET_TIMERSLACK — pas d'accès mémoire utilisateur.
2132 let ret = unsafe { raw_syscall_prctl(PR_SET_TIMERSLACK, slack_ns, 0, 0, 0) };
2133 if ret < 0 {
2134 return Err(errno_from_negative_syscall_ret(ret));
2135 }
2136 Ok(())
2137}
2138
2139/// Ajoute `cap` à l'ensemble ambient du thread.
2140///
2141/// L'ensemble ambient est transmis à `execve`, contrairement aux
2142/// inheritable seuls. Exige `cap` dans `permitted ∩ inheritable`.
2143///
2144/// # Errors
2145///
2146/// - [`Errno::EPERM`] : `cap` n'est pas dans permitted ∩ inheritable, ou
2147/// `SECBIT_NO_CAP_AMBIENT_RAISE` est positionné.
2148/// - [`Errno::EINVAL`] : valeur de capability inconnue côté kernel.
2149pub fn cap_ambient_raise(cap: Capability) -> Result<(), Errno> {
2150 // SAFETY: PR_CAP_AMBIENT/RAISE — pas d'accès mémoire utilisateur.
2151 let ret = unsafe {
2152 raw_syscall_prctl(
2153 PR_CAP_AMBIENT,
2154 PR_CAP_AMBIENT_RAISE,
2155 u64::from(cap.as_raw()),
2156 0,
2157 0,
2158 )
2159 };
2160 if ret < 0 {
2161 return Err(errno_from_negative_syscall_ret(ret));
2162 }
2163 Ok(())
2164}
2165
2166/// Retire `cap` de l'ensemble ambient.
2167///
2168/// # Errors
2169///
2170/// - [`Errno::EINVAL`] : valeur de capability inconnue côté kernel.
2171pub fn cap_ambient_lower(cap: Capability) -> Result<(), Errno> {
2172 // SAFETY: PR_CAP_AMBIENT/LOWER — pas d'accès mémoire utilisateur.
2173 let ret = unsafe {
2174 raw_syscall_prctl(
2175 PR_CAP_AMBIENT,
2176 PR_CAP_AMBIENT_LOWER,
2177 u64::from(cap.as_raw()),
2178 0,
2179 0,
2180 )
2181 };
2182 if ret < 0 {
2183 return Err(errno_from_negative_syscall_ret(ret));
2184 }
2185 Ok(())
2186}
2187
2188/// Vide totalement l'ensemble ambient du thread.
2189///
2190/// # Errors
2191///
2192/// - Aucune en pratique.
2193pub fn cap_ambient_clear_all() -> Result<(), Errno> {
2194 // SAFETY: PR_CAP_AMBIENT/CLEAR_ALL — pas d'accès mémoire utilisateur.
2195 let ret = unsafe { raw_syscall_prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) };
2196 if ret < 0 {
2197 return Err(errno_from_negative_syscall_ret(ret));
2198 }
2199 Ok(())
2200}
2201
2202/// Indique si `cap` est dans l'ensemble ambient.
2203///
2204/// # Errors
2205///
2206/// - [`Errno::EINVAL`] : valeur de capability inconnue côté kernel.
2207pub fn cap_ambient_is_set(cap: Capability) -> Result<bool, Errno> {
2208 // SAFETY: PR_CAP_AMBIENT/IS_SET — pas d'accès mémoire utilisateur ;
2209 // valeur retournée comme code de retour (0 ou 1).
2210 let ret = unsafe {
2211 raw_syscall_prctl(
2212 PR_CAP_AMBIENT,
2213 PR_CAP_AMBIENT_IS_SET,
2214 u64::from(cap.as_raw()),
2215 0,
2216 0,
2217 )
2218 };
2219 if ret < 0 {
2220 return Err(errno_from_negative_syscall_ret(ret));
2221 }
2222 Ok(ret != 0)
2223}
2224
2225#[cfg(target_arch = "x86_64")]
2226#[inline]
2227unsafe fn raw_syscall_prctl(op: i32, arg2: u64, arg3: u64, arg4: u64, arg5: u64) -> i64 {
2228 let ret: i64;
2229 // SAFETY: SYS_prctl (x86_64 = 157). Selon `op`, le kernel peut lire
2230 // ou écrire à des pointeurs passés via arg2..arg5 ; les fonctions
2231 // d'opération individuelles (au-dessus) garantissent la validité de
2232 // ces pointeurs lorsqu'elles en passent. Sans pointeur, pas
2233 // d'accès mémoire utilisateur.
2234 //
2235 // **Pas de `readonly`** : certaines opérations (PR_GET_NAME,
2236 // PR_GET_PDEATHSIG) font écrire le kernel à un pointeur user.
2237 unsafe {
2238 core::arch::asm!(
2239 "syscall",
2240 in("rax") 157_i64,
2241 in("rdi") i64::from(op),
2242 in("rsi") arg2,
2243 in("rdx") arg3,
2244 in("r10") arg4,
2245 in("r8") arg5,
2246 lateout("rax") ret,
2247 lateout("rcx") _,
2248 lateout("r11") _,
2249 options(nostack, preserves_flags),
2250 );
2251 }
2252 ret
2253}
2254
2255#[cfg(target_arch = "aarch64")]
2256#[inline]
2257unsafe fn raw_syscall_prctl(op: i32, arg2: u64, arg3: u64, arg4: u64, arg5: u64) -> i64 {
2258 let ret: i64;
2259 // SAFETY: SYS_prctl (aarch64 = 167). Mêmes considérations que x86_64.
2260 unsafe {
2261 core::arch::asm!(
2262 "svc 0",
2263 in("x8") 167_i64,
2264 inout("x0") i64::from(op) => ret,
2265 in("x1") arg2,
2266 in("x2") arg3,
2267 in("x3") arg4,
2268 in("x4") arg5,
2269 options(nostack, preserves_flags),
2270 );
2271 }
2272 ret
2273}
2274
2275// ─────────────────────────────────────────────────────────────────────────
2276// rlimits (sous-section 6 de family-process.md).
2277//
2278// `prlimit64` est le syscall préféré ; `getrlimit`/`setrlimit` legacy
2279// sont exposés mais routés au plus simple. La représentation kernel de
2280// `struct rlimit64` est { u64 rlim_cur ; u64 rlim_max }, identique à
2281// `struct rlimit` sur LP64. On la traduit en `Rlimit` via mapping
2282// `RLIM_INFINITY <-> RlimitValue::Infinity`.
2283// ─────────────────────────────────────────────────────────────────────────
2284
2285#[repr(C)]
2286#[derive(Default, Clone, Copy)]
2287struct KernelRlimit64 {
2288 rlim_cur: u64,
2289 rlim_max: u64,
2290}
2291
2292const RLIM_INFINITY: u64 = u64::MAX;
2293
2294fn rlim_value_to_kernel(v: RlimitValue) -> u64 {
2295 match v {
2296 RlimitValue::Finite(n) => n,
2297 RlimitValue::Infinity => RLIM_INFINITY,
2298 }
2299}
2300
2301fn rlim_value_from_kernel(n: u64) -> RlimitValue {
2302 if n == RLIM_INFINITY {
2303 RlimitValue::Infinity
2304 } else {
2305 RlimitValue::Finite(n)
2306 }
2307}
2308
2309fn rlimit_to_kernel(r: Rlimit) -> KernelRlimit64 {
2310 KernelRlimit64 {
2311 rlim_cur: rlim_value_to_kernel(r.soft),
2312 rlim_max: rlim_value_to_kernel(r.hard),
2313 }
2314}
2315
2316fn rlimit_from_kernel(k: KernelRlimit64) -> Rlimit {
2317 Rlimit {
2318 soft: rlim_value_from_kernel(k.rlim_cur),
2319 hard: rlim_value_from_kernel(k.rlim_max),
2320 }
2321}
2322
2323/// Retourne la `Rlimit` courante du processus pour `resource`.
2324///
2325/// Air recommande [`prlimit`] qui est plus expressif (cible un autre
2326/// processus, opération atomique) ; `getrlimit` est exposé pour
2327/// compatibilité.
2328///
2329/// # Errors
2330///
2331/// - [`Errno::EINVAL`] : ressource inconnue (impossible via l'API typée).
2332pub fn getrlimit(resource: Resource) -> Result<Rlimit, Errno> {
2333 // Délègue à prlimit(self, resource, None) pour ne pas dupliquer
2334 // l'asm! et bénéficier de la même conversion.
2335 prlimit(None, resource, None)
2336}
2337
2338/// Définit la `Rlimit` courante du processus pour `resource`.
2339///
2340/// # Errors
2341///
2342/// - [`Errno::EINVAL`] : valeurs incohérentes (soft > hard, …).
2343/// - [`Errno::EPERM`] : tentative de relever la hard limit sans
2344/// `CAP_SYS_RESOURCE`.
2345pub fn setrlimit(resource: Resource, limit: Rlimit) -> Result<(), Errno> {
2346 // Délègue à prlimit(self, resource, Some(limit)) ; on ignore l'ancienne
2347 // valeur retournée.
2348 let _ = prlimit(None, resource, Some(limit))?;
2349 Ok(())
2350}
2351
2352/// `prlimit64` : lit et/ou modifie atomiquement la `Rlimit` d'un
2353/// processus.
2354///
2355/// `pid = None` désigne le processus courant. Retourne **toujours** la
2356/// limite *avant* modification (équivalent kernel "old_limit").
2357///
2358/// # Errors
2359///
2360/// - [`Errno::EINVAL`] : valeurs incohérentes.
2361/// - [`Errno::EPERM`] : tentative de relever la hard limit sans
2362/// `CAP_SYS_RESOURCE`, ou ciblage d'un autre processus sans droits.
2363/// - [`Errno::ESRCH`] : `pid` n'existe pas.
2364pub fn prlimit(
2365 pid: Option<Pid>,
2366 resource: Resource,
2367 new_limit: Option<Rlimit>,
2368) -> Result<Rlimit, Errno> {
2369 let pid_arg = pid.map_or(0_i32, |p| p.as_raw());
2370
2371 let new_buf: Option<KernelRlimit64> = new_limit.map(rlimit_to_kernel);
2372 let new_ptr: u64 = match &new_buf {
2373 Some(b) => {
2374 let p: *const KernelRlimit64 = b;
2375 p as u64
2376 }
2377 None => 0,
2378 };
2379
2380 let mut old_buf = KernelRlimit64::default();
2381 let old_ptr: *mut KernelRlimit64 = &mut old_buf;
2382
2383 // SAFETY: prlimit64(2) lit `new_limit` si non-null, écrit `old_limit`
2384 // si non-null. Nous fournissons toujours `old_ptr` valide ; `new_ptr`
2385 // est null ou pointe sur `new_buf` valide pour la durée du call.
2386 let ret = unsafe { raw_syscall_prlimit64(pid_arg, resource as u32, new_ptr, old_ptr as u64) };
2387 if ret < 0 {
2388 return Err(errno_from_negative_syscall_ret(ret));
2389 }
2390 Ok(rlimit_from_kernel(old_buf))
2391}
2392
2393#[cfg(target_arch = "x86_64")]
2394#[inline]
2395unsafe fn raw_syscall_prlimit64(pid: i32, resource: u32, new_limit: u64, old_limit: u64) -> i64 {
2396 let ret: i64;
2397 // SAFETY: SYS_prlimit64 (x86_64 = 302). Le kernel lit `new_limit`
2398 // (16 octets) si non-null ; écrit `old_limit` (16 octets) si non-null.
2399 // Pas de `readonly` (écriture sur `*old_limit`).
2400 unsafe {
2401 core::arch::asm!(
2402 "syscall",
2403 in("rax") 302_i64,
2404 in("rdi") i64::from(pid),
2405 in("rsi") i64::from(resource),
2406 in("rdx") new_limit,
2407 in("r10") old_limit,
2408 lateout("rax") ret,
2409 lateout("rcx") _,
2410 lateout("r11") _,
2411 options(nostack, preserves_flags),
2412 );
2413 }
2414 ret
2415}
2416
2417#[cfg(target_arch = "aarch64")]
2418#[inline]
2419unsafe fn raw_syscall_prlimit64(pid: i32, resource: u32, new_limit: u64, old_limit: u64) -> i64 {
2420 let ret: i64;
2421 // SAFETY: SYS_prlimit64 (aarch64 = 261). Mêmes considérations.
2422 unsafe {
2423 core::arch::asm!(
2424 "svc 0",
2425 in("x8") 261_i64,
2426 inout("x0") i64::from(pid) => ret,
2427 in("x1") i64::from(resource),
2428 in("x2") new_limit,
2429 in("x3") old_limit,
2430 options(nostack, preserves_flags),
2431 );
2432 }
2433 ret
2434}
2435
2436// ─────────────────────────────────────────────────────────────────────────
2437// capget / capset (sous-section 7 de family-process.md).
2438//
2439// Version 3 capabilities (kernel ≥ 2.6.25) : header + array de 2 data
2440// structs ; chaque data struct contient `(effective, permitted,
2441// inheritable)` en `u32`. Index 0 = bits 0-31, index 1 = bits 32-63.
2442//
2443// La conversion `CapabilityMask (u64) ↔ (lo, hi)` est faite par les
2444// helpers privés `mask_to_words` / `words_to_mask` ci-dessous, testés
2445// explicitement pour qu'une éventuelle inversion lo/hi soit détectée
2446// (cf. JOURNAL session 2026-05-24).
2447// ─────────────────────────────────────────────────────────────────────────
2448
2449const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
2450
2451#[repr(C)]
2452struct KernelCapHeader {
2453 version: u32,
2454 pid: i32,
2455}
2456
2457#[repr(C)]
2458#[derive(Default, Clone, Copy)]
2459struct KernelCapData {
2460 effective: u32,
2461 permitted: u32,
2462 inheritable: u32,
2463}
2464
2465fn mask_to_words(mask: CapabilityMask) -> (u32, u32) {
2466 let bits = mask.bits();
2467 // Troncature exacte : `bits & 0xFFFF_FFFF` fit dans u32 par
2468 // construction.
2469 #[allow(clippy::cast_possible_truncation)]
2470 let lo = (bits & 0xFFFF_FFFF) as u32;
2471 // Troncature exacte : après `>> 32`, la valeur résiduelle est dans
2472 // `[0, u32::MAX]`.
2473 #[allow(clippy::cast_possible_truncation)]
2474 let hi = (bits >> 32) as u32;
2475 (lo, hi)
2476}
2477
2478fn words_to_mask(lo: u32, hi: u32) -> CapabilityMask {
2479 let bits = (u64::from(hi) << 32) | u64::from(lo);
2480 CapabilityMask::from_bits(bits)
2481}
2482
2483fn cap_target_to_kernel_pid(target: CapabilityTarget) -> i32 {
2484 match target {
2485 CapabilityTarget::CurrentThread => 0,
2486 CapabilityTarget::Thread(tid) => tid.as_raw(),
2487 CapabilityTarget::Process(pid) => pid.as_raw(),
2488 }
2489}
2490
2491/// Lit les capabilities du thread cible.
2492///
2493/// # Errors
2494///
2495/// - [`Errno::EINVAL`] : version capabilities non supportée par le kernel
2496/// (Air utilise version 3, supportée depuis 2.6.25).
2497/// - [`Errno::EPERM`] : ciblage d'un thread sans droits (rare en lecture).
2498pub fn capget(target: CapabilityTarget) -> Result<CapabilitySet, Errno> {
2499 let mut header = KernelCapHeader {
2500 version: LINUX_CAPABILITY_VERSION_3,
2501 pid: cap_target_to_kernel_pid(target),
2502 };
2503 let mut data: [KernelCapData; 2] = [KernelCapData::default(); 2];
2504 let header_ptr: *mut KernelCapHeader = &mut header;
2505 let data_ptr: *mut KernelCapData = data.as_mut_ptr();
2506 // SAFETY: capget(2) lit `*header` et écrit deux `KernelCapData`
2507 // consécutives à `data_ptr` ; les deux pointeurs sont locaux et
2508 // valides pour la durée du call.
2509 let ret = unsafe { raw_syscall_capget(header_ptr as u64, data_ptr as u64) };
2510 if ret < 0 {
2511 return Err(errno_from_negative_syscall_ret(ret));
2512 }
2513 Ok(CapabilitySet {
2514 effective: words_to_mask(data[0].effective, data[1].effective),
2515 permitted: words_to_mask(data[0].permitted, data[1].permitted),
2516 inheritable: words_to_mask(data[0].inheritable, data[1].inheritable),
2517 })
2518}
2519
2520/// Écrit les capabilities du thread cible.
2521///
2522/// # Errors
2523///
2524/// - [`Errno::EINVAL`] : version capabilities ou layout invalides.
2525/// - [`Errno::EPERM`] : tentative d'élever une capability hors permitted,
2526/// ou modification d'un autre thread sans droits.
2527pub fn capset(target: CapabilityTarget, set: &CapabilitySet) -> Result<(), Errno> {
2528 let mut header = KernelCapHeader {
2529 version: LINUX_CAPABILITY_VERSION_3,
2530 pid: cap_target_to_kernel_pid(target),
2531 };
2532 let (e_lo, e_hi) = mask_to_words(set.effective);
2533 let (p_lo, p_hi) = mask_to_words(set.permitted);
2534 let (i_lo, i_hi) = mask_to_words(set.inheritable);
2535 let data: [KernelCapData; 2] = [
2536 KernelCapData {
2537 effective: e_lo,
2538 permitted: p_lo,
2539 inheritable: i_lo,
2540 },
2541 KernelCapData {
2542 effective: e_hi,
2543 permitted: p_hi,
2544 inheritable: i_hi,
2545 },
2546 ];
2547 let header_ptr: *mut KernelCapHeader = &mut header;
2548 let data_ptr: *const KernelCapData = data.as_ptr();
2549 // SAFETY: capset(2) lit `*header` et lit deux `KernelCapData`
2550 // consécutives à `data_ptr`. Tout est local et valide pour la durée
2551 // du call.
2552 let ret = unsafe { raw_syscall_capset(header_ptr as u64, data_ptr as u64) };
2553 if ret < 0 {
2554 return Err(errno_from_negative_syscall_ret(ret));
2555 }
2556 Ok(())
2557}
2558
2559#[cfg(target_arch = "x86_64")]
2560#[inline]
2561unsafe fn raw_syscall_capget(header: u64, data: u64) -> i64 {
2562 let ret: i64;
2563 // SAFETY: SYS_capget (x86_64 = 125). Le kernel lit `*header` (8 octets)
2564 // et écrit 2 × 12 octets à `data`. Pas de `readonly` (écriture).
2565 unsafe {
2566 core::arch::asm!(
2567 "syscall",
2568 in("rax") 125_i64,
2569 in("rdi") header,
2570 in("rsi") data,
2571 lateout("rax") ret,
2572 lateout("rcx") _,
2573 lateout("r11") _,
2574 options(nostack, preserves_flags),
2575 );
2576 }
2577 ret
2578}
2579
2580#[cfg(target_arch = "x86_64")]
2581#[inline]
2582unsafe fn raw_syscall_capset(header: u64, data: u64) -> i64 {
2583 let ret: i64;
2584 // SAFETY: SYS_capset (x86_64 = 126). Lecture seule de `*header` et
2585 // `*data` côté user ; `readonly` correct.
2586 unsafe {
2587 core::arch::asm!(
2588 "syscall",
2589 in("rax") 126_i64,
2590 in("rdi") header,
2591 in("rsi") data,
2592 lateout("rax") ret,
2593 lateout("rcx") _,
2594 lateout("r11") _,
2595 options(nostack, preserves_flags, readonly),
2596 );
2597 }
2598 ret
2599}
2600
2601#[cfg(target_arch = "aarch64")]
2602#[inline]
2603unsafe fn raw_syscall_capget(header: u64, data: u64) -> i64 {
2604 let ret: i64;
2605 // SAFETY: SYS_capget (aarch64 = 90).
2606 unsafe {
2607 core::arch::asm!(
2608 "svc 0",
2609 in("x8") 90_i64,
2610 inout("x0") header => ret,
2611 in("x1") data,
2612 options(nostack, preserves_flags),
2613 );
2614 }
2615 ret
2616}
2617
2618#[cfg(target_arch = "aarch64")]
2619#[inline]
2620unsafe fn raw_syscall_capset(header: u64, data: u64) -> i64 {
2621 let ret: i64;
2622 // SAFETY: SYS_capset (aarch64 = 91). `readonly` car lecture seule.
2623 unsafe {
2624 core::arch::asm!(
2625 "svc 0",
2626 in("x8") 91_i64,
2627 inout("x0") header => ret,
2628 in("x1") data,
2629 options(nostack, preserves_flags, readonly),
2630 );
2631 }
2632 ret
2633}
2634
2635// ─────────────────────────────────────────────────────────────────────────
2636// Helper interne de conversion `errno`.
2637// ─────────────────────────────────────────────────────────────────────────
2638
2639/// Convertit une valeur de retour de syscall strictement négative en
2640/// [`Errno`]. Le kernel borne les errno à `[-4095, -1]` (`MAX_ERRNO`).
2641fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
2642 debug_assert!(ret < 0 && ret > -4096);
2643 // `ret.wrapping_neg()` est donc dans `[1, 4095]` — toujours fit in i32.
2644 #[allow(clippy::cast_possible_truncation)]
2645 let raw = ret.wrapping_neg() as i32;
2646 let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
2647 Errno::from_nonzero(nz)
2648}
2649
2650// ─────────────────────────────────────────────────────────────────────────
2651// Affinité CPU — sched_setaffinity / sched_getaffinity
2652// Cf. `docs/specs/layer-0/family-process-affinity.md`.
2653// ─────────────────────────────────────────────────────────────────────────
2654
2655#[cfg(target_arch = "x86_64")]
2656#[inline]
2657unsafe fn raw_syscall_sched_setaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2658 let ret: i64;
2659 // SAFETY:
2660 // - SYS_sched_setaffinity (x86_64 = 203). Le kernel **lit** `len` octets à
2661 // `mask` (jamais d'écriture) → `readonly` correct ; validité du buffer
2662 // garantie par l'appelant (`CpuSet` local, 128 octets).
2663 // - x86_64 ABI : numéro en RAX, args en RDI/RSI/RDX ; retour RAX ; clobbe
2664 // RCX/R11.
2665 unsafe {
2666 core::arch::asm!(
2667 "syscall",
2668 in("rax") 203_i64,
2669 in("rdi") i64::from(pid),
2670 in("rsi") len,
2671 in("rdx") mask,
2672 lateout("rax") ret,
2673 lateout("rcx") _,
2674 lateout("r11") _,
2675 options(nostack, preserves_flags, readonly),
2676 );
2677 }
2678 ret
2679}
2680
2681#[cfg(target_arch = "aarch64")]
2682#[inline]
2683unsafe fn raw_syscall_sched_setaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2684 let ret: i64;
2685 // SAFETY: voir version x86_64 ; aarch64 ABI : numéro en X8 (= 122), args en
2686 // X0/X1/X2, retour en X0. Le kernel **lit** `mask` → `readonly`.
2687 unsafe {
2688 core::arch::asm!(
2689 "svc 0",
2690 in("x8") 122_i64,
2691 inout("x0") i64::from(pid) => ret,
2692 in("x1") len,
2693 in("x2") mask,
2694 options(nostack, preserves_flags, readonly),
2695 );
2696 }
2697 ret
2698}
2699
2700#[cfg(target_arch = "x86_64")]
2701#[inline]
2702unsafe fn raw_syscall_sched_getaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2703 let ret: i64;
2704 // SAFETY:
2705 // - SYS_sched_getaffinity (x86_64 = 204). Le kernel **écrit** jusqu'à `len`
2706 // octets à `mask` → **pas de `readonly`** ; validité/taille du buffer
2707 // garanties par l'appelant (`CpuSet` local, 128 octets).
2708 // - Retourne le nombre d'octets écrits (≥ 0) ou `-errno`.
2709 unsafe {
2710 core::arch::asm!(
2711 "syscall",
2712 in("rax") 204_i64,
2713 in("rdi") i64::from(pid),
2714 in("rsi") len,
2715 in("rdx") mask,
2716 lateout("rax") ret,
2717 lateout("rcx") _,
2718 lateout("r11") _,
2719 options(nostack, preserves_flags),
2720 );
2721 }
2722 ret
2723}
2724
2725#[cfg(target_arch = "aarch64")]
2726#[inline]
2727unsafe fn raw_syscall_sched_getaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2728 let ret: i64;
2729 // SAFETY: voir version x86_64 ; aarch64 ABI : numéro en X8 (= 123), args en
2730 // X0/X1/X2, retour en X0. Le kernel **écrit** dans `mask` → pas de `readonly`.
2731 unsafe {
2732 core::arch::asm!(
2733 "svc 0",
2734 in("x8") 123_i64,
2735 inout("x0") i64::from(pid) => ret,
2736 in("x1") len,
2737 in("x2") mask,
2738 options(nostack, preserves_flags),
2739 );
2740 }
2741 ret
2742}
2743
2744/// Identifiant de tâche pour les opérations d'affinité : `None` = **tâche
2745/// appelante** (sentinelle kernel `0` typée en `Option`, ADR-021 conv. 1).
2746fn affinity_pid(tid: Option<Tid>) -> i32 {
2747 // `0` = la tâche appelante (jamais exposé comme entier magique).
2748 tid.map_or(0, Tid::as_raw)
2749}
2750
2751/// Fixe l'**affinité CPU** d'une tâche (`sched_setaffinity(2)`) : la tâche ne sera
2752/// ordonnancée que sur les CPU présents dans `cpus`. `tid = None` cible la tâche
2753/// appelante. Débloque `air-thread::cpu_affinity` (couche 1).
2754///
2755/// # Errors
2756///
2757/// - [`Errno::EINVAL`] : `cpus` ne contient **aucun** CPU autorisé/en ligne.
2758/// - [`Errno::ESRCH`] : aucune tâche ne porte ce `tid`.
2759/// - [`Errno::EPERM`] : privilèges insuffisants pour changer l'affinité de `tid`.
2760pub fn set_cpu_affinity(tid: Option<Tid>, cpus: &CpuSet) -> Result<(), Errno> {
2761 let bytes = cpus.as_bytes();
2762 let len = bytes.len() as u64;
2763 let mask = bytes.as_ptr() as u64;
2764 // SAFETY: `mask` pointe les `len` octets (= 128) du `CpuSet` de l'appelant,
2765 // valides en lecture pour la durée de l'appel ; le kernel ne les écrit pas.
2766 let ret = unsafe { raw_syscall_sched_setaffinity(affinity_pid(tid), len, mask) };
2767 if ret < 0 {
2768 return Err(errno_from_negative_syscall_ret(ret));
2769 }
2770 Ok(())
2771}
2772
2773/// Lit l'**affinité CPU** courante d'une tâche (`sched_getaffinity(2)`) **dans**
2774/// `cpus` (buffer fourni, zéro allocation). `tid = None` cible la tâche appelante.
2775///
2776/// # Errors
2777///
2778/// - [`Errno::ESRCH`] : aucune tâche ne porte ce `tid`.
2779/// - [`Errno::EINVAL`] : taille de masque incohérente (ne se produit pas avec un
2780/// `CpuSet` de [`air_sys_types::system::CPU_SETSIZE`] bits).
2781pub fn get_cpu_affinity(tid: Option<Tid>, cpus: &mut CpuSet) -> Result<(), Errno> {
2782 let len = core::mem::size_of::<CpuSet>() as u64;
2783 let mask = core::ptr::from_mut(cpus).cast::<u8>() as u64;
2784 // SAFETY: `mask` pointe les `len` octets (= 128) du `CpuSet` **mutable** de
2785 // l'appelant ; le kernel y écrit au plus `len` octets (le bitmap d'affinité).
2786 // `CpuSet` est `#[repr(C)]` sans padding ⇒ tout motif d'octets est valide.
2787 let ret = unsafe { raw_syscall_sched_getaffinity(affinity_pid(tid), len, mask) };
2788 if ret < 0 {
2789 return Err(errno_from_negative_syscall_ret(ret));
2790 }
2791 Ok(())
2792}
2793
2794// ─────────────────────────────────────────────────────────────────────────
2795// exec / redirection — execve, execveat, dup3, fchdir, chdir.
2796//
2797// Surface pilotée par le contrat de la couche 1 (`air-process`) : lancer un
2798// programme (exec), rediriger ses stdin/stdout/stderr (dup3) et fixer son
2799// répertoire de travail (fchdir / chdir). Cf. `docs/specs/layer-0/
2800// family-process.md`, section « exec / redirection ».
2801// ─────────────────────────────────────────────────────────────────────────
2802
2803/// `AT_FDCWD` : base « répertoire courant » des opérations `*at` (sentinelle
2804/// kernel `-100`, typée [`DirFd::Cwd`] côté Air, cf. ADR-021 convention 1).
2805const AT_FDCWD: i32 = -100;
2806
2807/// Convertit une [`DirFd`] en l'entier attendu par l'ABI syscall (`AT_FDCWD`
2808/// pour [`DirFd::Cwd`], sinon le fd emprunté).
2809fn dirfd_to_raw(dirfd: DirFd<'_>) -> i32 {
2810 match dirfd {
2811 DirFd::Cwd => AT_FDCWD,
2812 DirFd::Fd(fd) => fd.as_raw_fd(),
2813 }
2814}
2815
2816/// Tableau de pointeurs C terminé par `NULL`, pour le marshalling d'`argv` /
2817/// `envp` vers `execve(2)` / `execveat(2)`.
2818///
2819/// Ces syscalls attendent un argument de type `char *const argv[]` : une suite
2820/// de pointeurs vers des chaînes C **terminée par un pointeur `NULL`**. Ce type
2821/// matérialise cette **frontière de marshalling**, qui est *fuzzée*
2822/// (`fuzz/fuzz_targets/fuzz_exec_argv.rs`) : construction depuis une liste
2823/// arbitraire (vide, longue), terminaison `NULL` toujours présente, ordre
2824/// préservé.
2825///
2826/// **Rejet des NUL embarqués — par construction.** Un argument C ne peut pas
2827/// contenir d'octet NUL (il y serait interprété comme terminateur). L'entrée
2828/// étant une tranche de [`CStr`] — type qui **garantit** l'absence de NUL
2829/// interne — l'invariant est assuré *en amont*, là où l'appelant construit ses
2830/// `&CStr` (typiquement via `CStr::from_bytes_with_nul` ou `CString::new`, qui
2831/// rejettent proprement un NUL embarqué). Aucune revalidation n'est donc
2832/// nécessaire ici (Principe 4 : « parse, don't validate »).
2833///
2834/// **Allocation.** La construction alloue un [`Vec`] de `items.len() + 1`
2835/// pointeurs : c'est l'exception d'allocation documentée d'ADR-021 §4 (buffer
2836/// dynamique *intrinsèque* — sa taille dépend du nombre d'arguments, inconnu à
2837/// la compilation). Les chaînes elles-mêmes ne sont **pas** copiées : les
2838/// pointeurs empruntent les [`CStr`] fournis (d'où la durée de vie `'a`).
2839pub struct CStrArray<'a> {
2840 /// Pointeurs vers chaque `CStr`, suivis d'un pointeur `NULL` final.
2841 /// Invariant : `ptrs.len() >= 1` et `ptrs[ptrs.len() - 1].is_null()`.
2842 ptrs: Vec<*const c_char>,
2843 /// Lie la durée de vie des pointeurs à celle de la tranche source.
2844 _borrow: PhantomData<&'a [&'a CStr]>,
2845}
2846
2847impl<'a> CStrArray<'a> {
2848 /// Construit le tableau `NULL`-terminé à partir de `items`.
2849 ///
2850 /// Une tranche **vide** produit un tableau ne contenant que le `NULL` final
2851 /// (`argv = { NULL }`, soit `argc == 0`) — accepté par le kernel.
2852 #[must_use]
2853 pub fn new(items: &'a [&'a CStr]) -> Self {
2854 // `+ 1` pour le terminateur NULL. `saturating_add` par discipline
2855 // (Principe 2) ; la capacité d'une tranche ne peut pas atteindre
2856 // `usize::MAX`, donc la saturation ne se déclenche jamais.
2857 let mut ptrs = Vec::with_capacity(items.len().saturating_add(1));
2858 for item in items {
2859 ptrs.push(item.as_ptr());
2860 }
2861 ptrs.push(core::ptr::null());
2862 Self {
2863 ptrs,
2864 _borrow: PhantomData,
2865 }
2866 }
2867
2868 /// Nombre d'arguments (hors terminateur `NULL`).
2869 #[must_use]
2870 pub fn len(&self) -> usize {
2871 // Invariant : `ptrs` contient toujours le `NULL` final en sus.
2872 self.ptrs.len().saturating_sub(1)
2873 }
2874
2875 /// Vrai si la liste est vide (le tableau ne contient que le `NULL` final).
2876 #[must_use]
2877 pub fn is_empty(&self) -> bool {
2878 self.len() == 0
2879 }
2880
2881 /// Pointeur vers le tableau de pointeurs, à transmettre au syscall.
2882 #[inline]
2883 fn as_ptr(&self) -> *const *const c_char {
2884 self.ptrs.as_ptr()
2885 }
2886}
2887
2888/// Remplace l'image mémoire du processus courant par l'exécutable situé à
2889/// `path`, avec les arguments `argv` et l'environnement `envp`.
2890///
2891/// Cf. `docs/specs/layer-0/family-process.md`. En cas de **succès, ne retourne
2892/// jamais** (le processus exécute désormais le nouveau binaire) — d'où le type
2893/// `Result<Infallible, Errno>` (ADR-021 convention 5). En cas d'échec, retourne
2894/// `Err(errno)`.
2895///
2896/// `argv` / `envp` sont des tranches de [`CStr`] ; le wrapper en construit les
2897/// tableaux de pointeurs C `NULL`-terminés via [`CStrArray`]. Par convention
2898/// POSIX, `argv[0]` est le nom du programme ; une tranche vide est acceptée par
2899/// le kernel (`argc == 0`).
2900///
2901/// Pour exécuter un binaire désigné par un **fd** (sans race sur le chemin),
2902/// préférer [`execveat`] avec [`ExecveatFlags::EMPTY_PATH`].
2903///
2904/// # Errors
2905///
2906/// - [`Errno::EACCES`] : `path` n'est pas exécutable, un répertoire du chemin
2907/// interdit la traversée, ou le système de fichiers est monté `noexec`.
2908/// - [`Errno::ENOENT`] : `path` (ou l'interpréteur `#!` d'un script) n'existe
2909/// pas.
2910/// - [`Errno::ENOEXEC`] : `path` existe mais n'est pas dans un format
2911/// exécutable reconnu (ni ELF, ni script avec `#!`).
2912/// - [`Errno::E2BIG`] : `argv` + `envp` dépassent la limite kernel (`ARG_MAX`).
2913/// - `ENOMEM`, `ETXTBSY`, `ELOOP`, … : cf. `execve(2)`.
2914pub fn execve(path: &CStr, argv: &[&CStr], envp: &[&CStr]) -> Result<Infallible, Errno> {
2915 // L'allocation des tableaux de pointeurs ([`CStrArray::new`]) a lieu **ici**.
2916 // Le cœur (syscall, sans allocation) est délégué à [`execve_prepared`], que
2917 // les appelants en chemin **post-fork** invoquent directement avec des
2918 // [`CStrArray`] matérialisés **avant** le fork (async-signal-safety).
2919 let c_argv = CStrArray::new(argv);
2920 let c_envp = CStrArray::new(envp);
2921 execve_prepared(path, &c_argv, &c_envp)
2922}
2923
2924/// Variante d'[`execve`] **sans allocation** : reçoit des tableaux de pointeurs
2925/// `argv`/`envp` **déjà construits** ([`CStrArray`]).
2926///
2927/// Destinée au chemin **post-fork / pré-`execve`** : l'appelant matérialise les
2928/// [`CStrArray`] dans le parent **avant** `clone3`/`fork`, puis les passe ici
2929/// depuis l'enfant. Cette fonction n'alloue rien — seul un syscall est émis —
2930/// donc elle est sûre dans la fenêtre fork→exec où aucune allocation (verrou
2931/// allocateur potentiellement tenu par un autre thread au moment du fork) n'est
2932/// permise. [`execve`] délègue à cette fonction après avoir bâti ses tableaux.
2933///
2934/// En cas de **succès, ne retourne jamais** (image mémoire remplacée). En cas
2935/// d'échec, retourne `Err(errno)`.
2936///
2937/// # Errors
2938///
2939/// Identiques à [`execve`] (`EACCES`, `ENOENT`, `ENOEXEC`, `E2BIG`, …).
2940pub fn execve_prepared(
2941 path: &CStr,
2942 argv: &CStrArray<'_>,
2943 envp: &CStrArray<'_>,
2944) -> Result<Infallible, Errno> {
2945 // SAFETY:
2946 // - `path` est un `CStr` valide NUL-terminé (le kernel le lit jusqu'au NUL).
2947 // - `argv`/`envp` exposent chacun un tableau de pointeurs C valide, terminé
2948 // par NULL, vivant pour toute la durée de l'appel ; les chaînes pointées
2949 // sont les `CStr` empruntés, vivants eux aussi (durée de vie liée par
2950 // `CStrArray<'a>`). Le kernel ne fait que **lire** ces données.
2951 // - Succès : le syscall ne revient pas (image remplacée). Échec : ret < 0.
2952 let ret = unsafe {
2953 raw_syscall_execve(
2954 path.as_ptr() as u64,
2955 argv.as_ptr() as u64,
2956 envp.as_ptr() as u64,
2957 )
2958 };
2959 // Seul un échec ramène ici : `ret` est toujours `< 0`. La conversion errno
2960 // impose elle-même `ret ∈ [-4095, -1]` (debug_assert interne).
2961 Err(errno_from_negative_syscall_ret(ret))
2962}
2963
2964#[cfg(target_arch = "x86_64")]
2965#[inline]
2966unsafe fn raw_syscall_execve(path: u64, argv: u64, envp: u64) -> i64 {
2967 let ret: i64;
2968 // SAFETY:
2969 // - SYS_execve (x86_64 = 59). Le kernel **lit** la chaîne `path` (jusqu'au
2970 // NUL) et les tableaux `argv`/`envp` (jusqu'au pointeur NULL, puis chaque
2971 // chaîne jusqu'à son NUL) ; il n'écrit aucune mémoire utilisateur →
2972 // `readonly` correct. Validité garantie par l'appelant.
2973 // - Succès : ne revient pas (image remplacée). Échec : ret = -errno.
2974 // - x86_64 ABI : numéro en RAX, args en RDI/RSI/RDX ; retour RAX ; clobbe
2975 // RCX/R11.
2976 unsafe {
2977 core::arch::asm!(
2978 "syscall",
2979 in("rax") 59_i64,
2980 in("rdi") path,
2981 in("rsi") argv,
2982 in("rdx") envp,
2983 lateout("rax") ret,
2984 lateout("rcx") _,
2985 lateout("r11") _,
2986 options(nostack, preserves_flags, readonly),
2987 );
2988 }
2989 ret
2990}
2991
2992#[cfg(target_arch = "aarch64")]
2993#[inline]
2994unsafe fn raw_syscall_execve(path: u64, argv: u64, envp: u64) -> i64 {
2995 let ret: i64;
2996 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_execve (aarch64 = 221),
2997 // numéro en X8, args en X0/X1/X2, retour en X0. Lecture seule → `readonly`.
2998 unsafe {
2999 core::arch::asm!(
3000 "svc 0",
3001 in("x8") 221_i64,
3002 inout("x0") path => ret,
3003 in("x1") argv,
3004 in("x2") envp,
3005 options(nostack, preserves_flags, readonly),
3006 );
3007 }
3008 ret
3009}
3010
3011/// Variante *at d'[`execve`] (préférée par ADR-021) : résout l'exécutable
3012/// relativement à `dirfd`, ou — avec [`ExecveatFlags::EMPTY_PATH`] et
3013/// `path == c""` — exécute **directement** le binaire désigné par `dirfd`
3014/// (un fd ouvert sur l'exécutable, typiquement avec `O_PATH`), éliminant toute
3015/// race entre l'ouverture/vérification et l'exec.
3016///
3017/// Cf. `docs/specs/layer-0/family-process.md`. Comme [`execve`], **ne retourne
3018/// jamais en cas de succès**.
3019///
3020/// # Errors
3021///
3022/// Mêmes erreurs qu'[`execve`], plus :
3023///
3024/// - [`Errno::EBADF`] : `dirfd` n'est ni `AT_FDCWD` ni un fd ouvert valide.
3025/// - [`Errno::ENOENT`] : `path` est vide sans [`ExecveatFlags::EMPTY_PATH`].
3026/// - [`Errno::ELOOP`] : composant final symbolique avec
3027/// [`ExecveatFlags::SYMLINK_NOFOLLOW`].
3028pub fn execveat(
3029 dirfd: DirFd<'_>,
3030 path: &CStr,
3031 argv: &[&CStr],
3032 envp: &[&CStr],
3033 flags: ExecveatFlags,
3034) -> Result<Infallible, Errno> {
3035 let c_argv = CStrArray::new(argv);
3036 let c_envp = CStrArray::new(envp);
3037 // SAFETY: identique à `execve` pour `path`/`argv`/`envp` (lecture seule),
3038 // plus :
3039 // - `dirfd` est `AT_FDCWD` ou un fd valide emprunté (résolution relative,
3040 // ou exécutable direct si `EMPTY_PATH`).
3041 // - `flags` est un masque `AT_*` valide (typé `ExecveatFlags`).
3042 let ret = unsafe {
3043 raw_syscall_execveat(
3044 dirfd_to_raw(dirfd),
3045 path.as_ptr() as u64,
3046 c_argv.as_ptr() as u64,
3047 c_envp.as_ptr() as u64,
3048 flags.bits(),
3049 )
3050 };
3051 // Seul un échec ramène ici : `ret` est toujours `< 0`.
3052 Err(errno_from_negative_syscall_ret(ret))
3053}
3054
3055#[cfg(target_arch = "x86_64")]
3056#[inline]
3057unsafe fn raw_syscall_execveat(dirfd: i32, path: u64, argv: u64, envp: u64, flags: i32) -> i64 {
3058 let ret: i64;
3059 // SAFETY:
3060 // - SYS_execveat (x86_64 = 322). Lecture seule de `path`/`argv`/`envp` (cf.
3061 // `raw_syscall_execve`) → `readonly`. `dirfd` (étendu en signe : AT_FDCWD
3062 // = -100 ou fd valide) et `flags` sont des scalaires.
3063 // - Succès : ne revient pas. Échec : ret = -errno.
3064 // - x86_64 ABI : args en RDI/RSI/RDX/R10/R8.
3065 unsafe {
3066 core::arch::asm!(
3067 "syscall",
3068 in("rax") 322_i64,
3069 in("rdi") i64::from(dirfd),
3070 in("rsi") path,
3071 in("rdx") argv,
3072 in("r10") envp,
3073 in("r8") i64::from(flags),
3074 lateout("rax") ret,
3075 lateout("rcx") _,
3076 lateout("r11") _,
3077 options(nostack, preserves_flags, readonly),
3078 );
3079 }
3080 ret
3081}
3082
3083#[cfg(target_arch = "aarch64")]
3084#[inline]
3085unsafe fn raw_syscall_execveat(dirfd: i32, path: u64, argv: u64, envp: u64, flags: i32) -> i64 {
3086 let ret: i64;
3087 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_execveat (aarch64 = 281),
3088 // numéro en X8, args en X0..X4, retour en X0. Lecture seule → `readonly`.
3089 unsafe {
3090 core::arch::asm!(
3091 "svc 0",
3092 in("x8") 281_i64,
3093 inout("x0") i64::from(dirfd) => ret,
3094 in("x1") path,
3095 in("x2") argv,
3096 in("x3") envp,
3097 in("x4") i64::from(flags),
3098 options(nostack, preserves_flags, readonly),
3099 );
3100 }
3101 ret
3102}
3103
3104/// Duplique le descripteur `old` vers le numéro `new` (`dup3(2)`), en fermant
3105/// atomiquement `new` s'il était ouvert. Retourne le nouveau descripteur,
3106/// possédé ([`OwnedFd`], fermé au `Drop`).
3107///
3108/// Cf. `docs/specs/layer-0/family-process.md`. Variante **moderne** de `dup2`
3109/// (ADR-021) : les drapeaux sont explicites ([`Dup3Flags`], seul `O_CLOEXEC`
3110/// est valide). Cas d'usage couche 1 : rediriger les flux standard d'un enfant
3111/// (`dup3(pipe, STDOUT_FILENO, …)`) juste avant l'`exec`.
3112///
3113/// À la différence de `dup2`, `dup3` **échoue** (`EINVAL`) si `old == new` :
3114/// pas de no-op silencieux. Le numéro `new` doit donc différer de `old`.
3115///
3116/// # Errors
3117///
3118/// - [`Errno::EINVAL`] : `flags` invalides, **ou** `old == new`.
3119/// - [`Errno::EBADF`] : `new` hors de la plage autorisée (`> RLIMIT_NOFILE`).
3120/// - [`Errno::EBUSY`] : course (rare) avec `open`/`dup` concurrents sur `new`.
3121/// - [`Errno::EINTR`] : interrompu par un signal (remonté tel quel, ADR-021
3122/// convention 2).
3123pub fn dup3(old: BorrowedFd<'_>, new: RawFd, flags: Dup3Flags) -> Result<OwnedFd, Errno> {
3124 // SAFETY: `dup3(2)` ne touche aucune mémoire utilisateur ; il installe une
3125 // copie de l'entrée de table de fd `old` au numéro `new` (fermant `new` au
3126 // préalable s'il était ouvert), de manière atomique. `flags` (typé) ne peut
3127 // valoir que `O_CLOEXEC`.
3128 let ret = unsafe { raw_syscall_dup3(old.as_raw_fd(), new, flags.bits()) };
3129 if ret < 0 {
3130 return Err(errno_from_negative_syscall_ret(ret));
3131 }
3132 // Succès : `ret == new` (le numéro demandé), donc tient dans un `i32`.
3133 #[allow(clippy::cast_possible_truncation)]
3134 let new_fd = ret as RawFd;
3135 // SAFETY: le kernel vient d'installer un fd valide au numéro `new`, dont
3136 // Air prend désormais la propriété exclusive (fermé par `OwnedFd::Drop`).
3137 Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
3138}
3139
3140#[cfg(target_arch = "x86_64")]
3141#[inline]
3142unsafe fn raw_syscall_dup3(oldfd: i32, newfd: i32, flags: i32) -> i64 {
3143 let ret: i64;
3144 // SAFETY: SYS_dup3 (x86_64 = 292) ne touche aucune mémoire utilisateur
3145 // (`readonly`). Args étendus en signe (fd ≥ 0 ici ; l'extension reste
3146 // correcte). x86_64 ABI : args en RDI/RSI/RDX.
3147 unsafe {
3148 core::arch::asm!(
3149 "syscall",
3150 in("rax") 292_i64,
3151 in("rdi") i64::from(oldfd),
3152 in("rsi") i64::from(newfd),
3153 in("rdx") i64::from(flags),
3154 lateout("rax") ret,
3155 lateout("rcx") _,
3156 lateout("r11") _,
3157 options(nostack, preserves_flags, readonly),
3158 );
3159 }
3160 ret
3161}
3162
3163#[cfg(target_arch = "aarch64")]
3164#[inline]
3165unsafe fn raw_syscall_dup3(oldfd: i32, newfd: i32, flags: i32) -> i64 {
3166 let ret: i64;
3167 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_dup3 (aarch64 = 24),
3168 // numéro en X8, args en X0/X1/X2, retour en X0. Lecture seule → `readonly`.
3169 unsafe {
3170 core::arch::asm!(
3171 "svc 0",
3172 in("x8") 24_i64,
3173 inout("x0") i64::from(oldfd) => ret,
3174 in("x1") i64::from(newfd),
3175 in("x2") i64::from(flags),
3176 options(nostack, preserves_flags, readonly),
3177 );
3178 }
3179 ret
3180}
3181
3182/// Change le répertoire de travail courant du processus pour celui référencé
3183/// par le descripteur `dir` (`fchdir(2)`).
3184///
3185/// Cf. `docs/specs/layer-0/family-process.md`. Variante par fd de [`chdir`] :
3186/// pas de race sur le chemin (le répertoire est déjà ouvert et épinglé). Cas
3187/// d'usage couche 1 : fixer le `current_dir` d'un enfant à partir d'un fd
3188/// vérifié, juste avant l'`exec`.
3189///
3190/// # Errors
3191///
3192/// - [`Errno::EBADF`] : `dir` n'est pas un descripteur ouvert valide.
3193/// - [`Errno::ENOTDIR`] : `dir` ne référence pas un répertoire.
3194/// - [`Errno::EACCES`] : permission de recherche refusée sur le répertoire.
3195pub fn fchdir(dir: BorrowedFd<'_>) -> Result<(), Errno> {
3196 // SAFETY: `fchdir(2)` ne touche aucune mémoire utilisateur ; il bascule le
3197 // répertoire courant du processus vers le répertoire référencé par `dir`.
3198 let ret = unsafe { raw_syscall_fchdir(dir.as_raw_fd()) };
3199 if ret < 0 {
3200 return Err(errno_from_negative_syscall_ret(ret));
3201 }
3202 Ok(())
3203}
3204
3205#[cfg(target_arch = "x86_64")]
3206#[inline]
3207unsafe fn raw_syscall_fchdir(fd: i32) -> i64 {
3208 let ret: i64;
3209 // SAFETY: SYS_fchdir (x86_64 = 81) ne touche aucune mémoire utilisateur
3210 // (`readonly`). x86_64 ABI : argument unique en RDI.
3211 unsafe {
3212 core::arch::asm!(
3213 "syscall",
3214 in("rax") 81_i64,
3215 in("rdi") i64::from(fd),
3216 lateout("rax") ret,
3217 lateout("rcx") _,
3218 lateout("r11") _,
3219 options(nostack, preserves_flags, readonly),
3220 );
3221 }
3222 ret
3223}
3224
3225#[cfg(target_arch = "aarch64")]
3226#[inline]
3227unsafe fn raw_syscall_fchdir(fd: i32) -> i64 {
3228 let ret: i64;
3229 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_fchdir (aarch64 = 50),
3230 // numéro en X8, argument en X0, retour en X0. Lecture seule → `readonly`.
3231 unsafe {
3232 core::arch::asm!(
3233 "svc 0",
3234 in("x8") 50_i64,
3235 inout("x0") i64::from(fd) => ret,
3236 options(nostack, preserves_flags, readonly),
3237 );
3238 }
3239 ret
3240}
3241
3242/// Change le répertoire de travail courant du processus pour `path`
3243/// (`chdir(2)`).
3244///
3245/// Cf. `docs/specs/layer-0/family-process.md`. Cas d'usage couche 1 : fixer le
3246/// `current_dir` d'un enfant juste avant l'`exec`. Préférer [`fchdir`] quand un
3247/// fd de répertoire est disponible (pas de race sur le chemin).
3248///
3249/// # Errors
3250///
3251/// - [`Errno::ENOENT`] : `path` n'existe pas.
3252/// - [`Errno::ENOTDIR`] : un composant de `path` n'est pas un répertoire.
3253/// - [`Errno::EACCES`] : permission de recherche refusée.
3254/// - [`Errno::ENAMETOOLONG`] : `path` trop long.
3255pub fn chdir(path: &CStr) -> Result<(), Errno> {
3256 // SAFETY: `chdir(2)` **lit** la chaîne `path` (jusqu'au NUL) et n'écrit
3257 // aucune mémoire utilisateur → `readonly`. `path` est un `CStr` valide.
3258 let ret = unsafe { raw_syscall_chdir(path.as_ptr() as u64) };
3259 if ret < 0 {
3260 return Err(errno_from_negative_syscall_ret(ret));
3261 }
3262 Ok(())
3263}
3264
3265/// Change le **répertoire racine** du processus vers `path` (`chroot(2)`) —
3266/// **descellement additif [ADR-085](../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md)**
3267/// pour la face libc. Confinement de processus (`CAP_SYS_CHROOT` requis).
3268///
3269/// # Errors
3270///
3271/// - [`Errno::EPERM`] : `CAP_SYS_CHROOT` manquant.
3272/// - [`Errno::ENOENT`] : `path` n'existe pas.
3273/// - [`Errno::ENOTDIR`] : un composant de `path` n'est pas un répertoire.
3274pub fn chroot(path: &CStr) -> Result<(), Errno> {
3275 // SAFETY: `chroot(2)` **lit** la chaîne `path` (jusqu'au NUL) et n'écrit aucune
3276 // mémoire utilisateur → `readonly`. `path` est un `CStr` valide.
3277 let ret = unsafe { raw_syscall_chroot(path.as_ptr() as u64) };
3278 if ret < 0 {
3279 return Err(errno_from_negative_syscall_ret(ret));
3280 }
3281 Ok(())
3282}
3283
3284#[cfg(target_arch = "x86_64")]
3285#[inline]
3286unsafe fn raw_syscall_chdir(path: u64) -> i64 {
3287 let ret: i64;
3288 // SAFETY: SYS_chdir (x86_64 = 80). Le kernel **lit** la chaîne `path`
3289 // (jusqu'au NUL), validité garantie par l'appelant ; aucune écriture
3290 // utilisateur → `readonly`. x86_64 ABI : argument unique en RDI.
3291 unsafe {
3292 core::arch::asm!(
3293 "syscall",
3294 in("rax") 80_i64,
3295 in("rdi") path,
3296 lateout("rax") ret,
3297 lateout("rcx") _,
3298 lateout("r11") _,
3299 options(nostack, preserves_flags, readonly),
3300 );
3301 }
3302 ret
3303}
3304
3305#[cfg(target_arch = "aarch64")]
3306#[inline]
3307unsafe fn raw_syscall_chdir(path: u64) -> i64 {
3308 let ret: i64;
3309 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_chdir (aarch64 = 49),
3310 // numéro en X8, argument (pointeur `path`) en X0, retour en X0. Lecture
3311 // seule → `readonly`.
3312 unsafe {
3313 core::arch::asm!(
3314 "svc 0",
3315 in("x8") 49_i64,
3316 inout("x0") path => ret,
3317 options(nostack, preserves_flags, readonly),
3318 );
3319 }
3320 ret
3321}
3322
3323#[cfg(target_arch = "x86_64")]
3324#[inline]
3325unsafe fn raw_syscall_chroot(path: u64) -> i64 {
3326 let ret: i64;
3327 // SAFETY: SYS_chroot (x86_64 = 161). Le kernel **lit** la chaîne `path` (jusqu'au
3328 // NUL), validité garantie par l'appelant ; aucune écriture utilisateur → `readonly`.
3329 // x86_64 ABI : argument unique en RDI.
3330 unsafe {
3331 core::arch::asm!(
3332 "syscall",
3333 in("rax") 161_i64,
3334 in("rdi") path,
3335 lateout("rax") ret,
3336 lateout("rcx") _,
3337 lateout("r11") _,
3338 options(nostack, preserves_flags, readonly),
3339 );
3340 }
3341 ret
3342}
3343
3344#[cfg(target_arch = "aarch64")]
3345#[inline]
3346unsafe fn raw_syscall_chroot(path: u64) -> i64 {
3347 let ret: i64;
3348 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_chroot (aarch64 = 51), numéro en
3349 // X8, argument (pointeur `path`) en X0, retour en X0. Lecture seule → `readonly`.
3350 unsafe {
3351 core::arch::asm!(
3352 "svc 0",
3353 in("x8") 51_i64,
3354 inout("x0") path => ret,
3355 options(nostack, preserves_flags, readonly),
3356 );
3357 }
3358 ret
3359}
3360
3361/// API de fuzzing **pure** (sans syscall) de la frontière de marshalling
3362/// `argv`/`envp` ([`CStrArray`]). Le harnais cargo-fuzz
3363/// (`fuzz/fuzz_targets/fuzz_exec_argv.rs`) y délègue ; l'oracle vit ici pour
3364/// accéder aux internes de [`CStrArray`] (le tableau de pointeurs) tout en
3365/// restant testé et couvert. Suit le pattern de [`crate::io_uring::fuzz_api`].
3366pub mod fuzz_api {
3367 use super::CStrArray;
3368 use alloc::ffi::CString;
3369 use alloc::vec::Vec;
3370 use core::ffi::{CStr, c_char};
3371
3372 /// Exerce la chaîne « octets bruts → `CString` → `CStrArray` » et la
3373 /// **parcourt** jusqu'au terminateur `NULL`, garantissant l'absence de
3374 /// panic / UB pour **toute** entrée (Principe 3 — frontière) :
3375 ///
3376 /// - **rejet propre des NUL embarqués** : `CString::new` rend `Err` (jamais
3377 /// panic) dès qu'un morceau contient un octet NUL ; ces morceaux sont
3378 /// simplement écartés ;
3379 /// - **totalité** : la construction du tableau ne panique jamais (liste
3380 /// vide, ou très longue) ;
3381 /// - **terminaison NULL & validité** : le parcours s'arrête sur le `NULL`
3382 /// final et relit chaque chaîne C (tout pointeur invalide ou toute
3383 /// terminaison manquante ferait crasher sous sanitizer/libFuzzer).
3384 ///
3385 /// L'oracle *structurel* exact (ordre, exactitude des pointeurs) est
3386 /// vérifié par les tests unitaires `cstr_array_*`.
3387 pub fn marshal_exec_args(chunks: Vec<Vec<u8>>) {
3388 // Frontière 1 — NUL embarqué rejeté proprement (Err, jamais panic).
3389 let owned: Vec<CString> = chunks
3390 .iter()
3391 .filter_map(|chunk| CString::new(chunk.as_slice()).ok())
3392 .collect();
3393 let refs: Vec<&CStr> = owned.iter().map(CString::as_c_str).collect();
3394
3395 // Frontière 2 — construction du tableau de pointeurs C NULL-terminé.
3396 let arr = CStrArray::new(&refs);
3397 let _ = arr.len();
3398 let _ = arr.is_empty();
3399
3400 // Parcours jusqu'au terminateur NULL en relisant chaque chaîne : tout
3401 // écart (pointeur invalide, terminaison absente) ferait crasher la
3402 // cible libFuzzer.
3403 let base = arr.as_ptr();
3404 let mut i = 0_usize;
3405 loop {
3406 // SAFETY: `base` pointe `refs.len() + 1` pointeurs ; on s'arrête au
3407 // premier NULL (présent par construction en position `refs.len()`),
3408 // donc `i` reste borné par `refs.len()`.
3409 let ptr: *const c_char = unsafe { *base.add(i) };
3410 if ptr.is_null() {
3411 break;
3412 }
3413 // SAFETY: `ptr` non NULL ⇒ i-ème chaîne C vivante (empruntée à
3414 // `owned`), NUL-terminée par construction de `CStr`.
3415 let s = unsafe { CStr::from_ptr(ptr) };
3416 let _ = s.to_bytes();
3417 i = i.saturating_add(1);
3418 }
3419 }
3420}
3421
3422// ─────────────────────────────────────────────────────────────────────────
3423// Runtime & libc — re-sceau `couche-0-v1.7` (ADR-051)
3424//
3425// 6 syscalls ajoutés pour le runtime Air (air-rt) et l'objectif libc :
3426// `getppid`, `set_tid_address`, `sched_yield`, `umask`, `getcwd`, `getrusage`.
3427// (Le 7ᵉ, `arch_prctl`, est x86_64-only et vit dans `crate::arch`.)
3428// Conventions ADR-021 : fonction dédiée typée, `Option`/newtypes, pas de
3429// sentinelle, zéro alloc sur le happy path.
3430// ─────────────────────────────────────────────────────────────────────────
3431
3432/// Retourne le PID du **parent** du processus appelant (`getppid(2)`).
3433///
3434/// Retourne `None` lorsque le kernel rend `0`, c.-à-d. quand le parent n'est
3435/// **pas visible** dans le *PID namespace* du processus (ré-parentage hors
3436/// namespace). Le `0` du kernel — qui n'est pas un PID valide — est ainsi
3437/// re-présenté en `Option` plutôt qu'exposé comme sentinelle (ADR-021 §1).
3438#[must_use]
3439pub fn getppid() -> Option<Pid> {
3440 let raw = raw_syscall_getppid();
3441 // PID : `pid_t` (i32) ; les 32 bits hauts sont nuls pour une valeur valide.
3442 #[allow(clippy::cast_possible_truncation)]
3443 let raw32 = raw as i32;
3444 // `try_from_raw` rend `None` pour `0` (parent hors namespace).
3445 Pid::try_from_raw(raw32)
3446}
3447
3448#[cfg(target_arch = "x86_64")]
3449#[inline]
3450fn raw_syscall_getppid() -> i64 {
3451 let ret: i64;
3452 // SAFETY: SYS_getppid (x86_64 = 110) ne touche aucune mémoire utilisateur ;
3453 // clobbe RCX/R11 comme tout `syscall` x86_64.
3454 unsafe {
3455 core::arch::asm!(
3456 "syscall",
3457 in("rax") 110_i64,
3458 lateout("rax") ret,
3459 lateout("rcx") _,
3460 lateout("r11") _,
3461 options(nostack, preserves_flags, readonly),
3462 );
3463 }
3464 ret
3465}
3466
3467#[cfg(target_arch = "aarch64")]
3468#[inline]
3469fn raw_syscall_getppid() -> i64 {
3470 let ret: i64;
3471 // SAFETY: SYS_getppid (aarch64 = 173) ne touche aucune mémoire utilisateur.
3472 unsafe {
3473 core::arch::asm!(
3474 "svc 0",
3475 in("x8") 173_i64,
3476 lateout("x0") ret,
3477 options(nostack, preserves_flags, readonly),
3478 );
3479 }
3480 ret
3481}
3482
3483/// Enregistre l'adresse `clear_child_tid` du thread appelant et retourne son TID
3484/// (`set_tid_address(2)`). À la **fin du thread**, le kernel écrit `0` à cette
3485/// adresse et émet un `FUTEX_WAKE` dessus — primitif de *join* (cf.
3486/// [`clone_thread`] et la famille [`futex`](crate::futex)).
3487///
3488/// `None` désenregistre (passe `NULL`). L'appel **ne peut pas échouer** et rend
3489/// toujours le TID appelant (`> 0`).
3490///
3491/// # Safety
3492///
3493/// Si `clear_child_tid` est `Some(addr)`, le kernel **conservera `addr`** et y
3494/// écrira à la terminaison du thread. L'appelant doit garantir que la mémoire
3495/// pointée reste **valide jusqu'à la fin du thread** (sinon écriture kernel dans
3496/// de la mémoire libérée). Même discipline que [`CloneArgs::child_tid`].
3497pub unsafe fn set_tid_address(clear_child_tid: Option<&core::sync::atomic::AtomicU32>) -> Tid {
3498 let ptr = clear_child_tid.map_or(0_usize, |a| core::ptr::from_ref(a) as usize);
3499 // SAFETY: l'appel délègue ; la liveness de `ptr` jusqu'à la mort du thread
3500 // relève du contrat `unsafe` de cette fonction.
3501 let raw = unsafe { raw_syscall_set_tid_address(ptr) };
3502 // Le kernel rend le TID appelant, strictement positif.
3503 #[allow(clippy::cast_possible_truncation)]
3504 let tid_raw = raw as i32;
3505 let nz = NonZeroI32::new(tid_raw).expect("set_tid_address : TID appelant > 0");
3506 Tid::from_nonzero(nz)
3507}
3508
3509#[cfg(target_arch = "x86_64")]
3510#[inline]
3511unsafe fn raw_syscall_set_tid_address(tidptr: usize) -> i64 {
3512 let ret: i64;
3513 // SAFETY: SYS_set_tid_address (x86_64 = 218). Le kernel **enregistre**
3514 // `tidptr` pour écriture à la mort du thread ; il n'écrit pas pendant
3515 // l'appel. La validité/liveness de `tidptr` relève du contrat de l'appelant.
3516 unsafe {
3517 core::arch::asm!(
3518 "syscall",
3519 in("rax") 218_i64,
3520 in("rdi") tidptr,
3521 lateout("rax") ret,
3522 lateout("rcx") _,
3523 lateout("r11") _,
3524 options(nostack, preserves_flags),
3525 );
3526 }
3527 ret
3528}
3529
3530#[cfg(target_arch = "aarch64")]
3531#[inline]
3532unsafe fn raw_syscall_set_tid_address(tidptr: usize) -> i64 {
3533 let ret: i64;
3534 // SAFETY: SYS_set_tid_address (aarch64 = 96). Voir la branche x86_64.
3535 unsafe {
3536 core::arch::asm!(
3537 "svc 0",
3538 in("x8") 96_i64,
3539 in("x0") tidptr,
3540 lateout("x0") ret,
3541 options(nostack, preserves_flags),
3542 );
3543 }
3544 ret
3545}
3546
3547/// Cède volontairement le CPU à un autre thread prêt (`sched_yield(2)`).
3548///
3549/// Sous Linux, `sched_yield` **réussit toujours** (retour `0`) ; la fonction est
3550/// donc totale.
3551pub fn sched_yield() {
3552 let _ = raw_syscall_sched_yield();
3553}
3554
3555#[cfg(target_arch = "x86_64")]
3556#[inline]
3557fn raw_syscall_sched_yield() -> i64 {
3558 let ret: i64;
3559 // SAFETY: SYS_sched_yield (x86_64 = 24) ne touche aucune mémoire utilisateur.
3560 unsafe {
3561 core::arch::asm!(
3562 "syscall",
3563 in("rax") 24_i64,
3564 lateout("rax") ret,
3565 lateout("rcx") _,
3566 lateout("r11") _,
3567 options(nostack, preserves_flags, readonly),
3568 );
3569 }
3570 ret
3571}
3572
3573#[cfg(target_arch = "aarch64")]
3574#[inline]
3575fn raw_syscall_sched_yield() -> i64 {
3576 let ret: i64;
3577 // SAFETY: SYS_sched_yield (aarch64 = 124) ne touche aucune mémoire utilisateur.
3578 unsafe {
3579 core::arch::asm!(
3580 "svc 0",
3581 in("x8") 124_i64,
3582 lateout("x0") ret,
3583 options(nostack, preserves_flags, readonly),
3584 );
3585 }
3586 ret
3587}
3588
3589/// Fixe le **masque de création de fichiers** du processus et retourne l'ancien
3590/// (`umask(2)`). Opération **totale** (ne peut pas échouer).
3591#[must_use]
3592pub fn umask(new_mask: Mode) -> Mode {
3593 let previous = raw_syscall_umask(u64::from(new_mask));
3594 // L'ancien masque tient dans `mode_t` (12 bits significatifs) ⇒ conversion
3595 // sûre, jamais lossy (Principe 2 : pas d'`as` lossy).
3596 u32::try_from(previous).expect("umask : ancien masque dans la plage mode_t")
3597}
3598
3599#[cfg(target_arch = "x86_64")]
3600#[inline]
3601fn raw_syscall_umask(mask: u64) -> i64 {
3602 let ret: i64;
3603 // SAFETY: SYS_umask (x86_64 = 95) ne touche aucune mémoire utilisateur ;
3604 // rend l'ancien masque.
3605 unsafe {
3606 core::arch::asm!(
3607 "syscall",
3608 in("rax") 95_i64,
3609 in("rdi") mask,
3610 lateout("rax") ret,
3611 lateout("rcx") _,
3612 lateout("r11") _,
3613 options(nostack, preserves_flags, readonly),
3614 );
3615 }
3616 ret
3617}
3618
3619#[cfg(target_arch = "aarch64")]
3620#[inline]
3621fn raw_syscall_umask(mask: u64) -> i64 {
3622 let ret: i64;
3623 // SAFETY: SYS_umask (aarch64 = 166). Voir la branche x86_64.
3624 unsafe {
3625 core::arch::asm!(
3626 "svc 0",
3627 in("x8") 166_i64,
3628 in("x0") mask,
3629 lateout("x0") ret,
3630 options(nostack, preserves_flags, readonly),
3631 );
3632 }
3633 ret
3634}
3635
3636/// Écrit le **répertoire courant** (chemin absolu) dans `buf` et retourne la
3637/// tranche d'octets correspondante, **sans** l'octet NUL terminal (`getcwd(2)`).
3638///
3639/// Le chemin est traité comme des **octets** (jamais supposé UTF-8 — Principe 3).
3640/// L'appelant fournit le buffer (zéro allocation, ADR-021 §4).
3641///
3642/// # Errors
3643///
3644/// - [`Errno::ERANGE`] si `buf` est trop petit pour contenir le chemin + NUL.
3645/// - Autres `Errno` propagés (p. ex. permissions de lecture d'un composant).
3646pub fn getcwd(buf: &mut [u8]) -> Result<&[u8], Errno> {
3647 let size = u64::try_from(buf.len()).expect("getcwd : taille de buffer dans u64");
3648 let ptr = buf.as_mut_ptr() as usize;
3649 // SAFETY: `ptr`/`size` décrivent `buf` (emprunt `&mut` valide) ; le kernel y
3650 // écrit au plus `size` octets.
3651 let ret = unsafe { raw_syscall_getcwd(ptr, size) };
3652 if ret < 0 {
3653 return Err(errno_from_negative_syscall_ret(ret));
3654 }
3655 // `getcwd(2)` rend la longueur **NUL compris** (≥ 1) ; on retourne les
3656 // octets sans le NUL final.
3657 let total = usize::try_from(ret).expect("getcwd : longueur non négative");
3658 let path_length = total
3659 .checked_sub(1)
3660 .expect("getcwd : longueur ≥ 1 (NUL terminal compris)");
3661 // TODO(ADR-029/Principe 3) : remplacer l'indexation directe `&buf[..path_length]`
3662 // par `buf.get(..path_length)` (slicing via `Option`, jamais d'indexation qui
3663 // panique). Ici `path_length < total ≤ buf.len()` ⇒ borné par construction, donc
3664 // sûr ; reste à convertir le `Option<&[u8]>` rendu en `Result` (p. ex.
3665 // `.expect("getcwd : tranche bornée par construction")` ou un bras d'erreur).
3666 Ok(&buf[..path_length])
3667}
3668
3669#[cfg(target_arch = "x86_64")]
3670#[inline]
3671unsafe fn raw_syscall_getcwd(buf: usize, size: u64) -> i64 {
3672 let ret: i64;
3673 // SAFETY: SYS_getcwd (x86_64 = 79) écrit jusqu'à `size` octets à `buf`.
3674 // Validité de `buf`/`size` : contrat de l'appelant. Pas de `readonly` (écrit
3675 // la mémoire pointée).
3676 unsafe {
3677 core::arch::asm!(
3678 "syscall",
3679 in("rax") 79_i64,
3680 in("rdi") buf,
3681 in("rsi") size,
3682 lateout("rax") ret,
3683 lateout("rcx") _,
3684 lateout("r11") _,
3685 options(nostack),
3686 );
3687 }
3688 ret
3689}
3690
3691#[cfg(target_arch = "aarch64")]
3692#[inline]
3693unsafe fn raw_syscall_getcwd(buf: usize, size: u64) -> i64 {
3694 let ret: i64;
3695 // SAFETY: SYS_getcwd (aarch64 = 17). Voir la branche x86_64.
3696 unsafe {
3697 core::arch::asm!(
3698 "svc 0",
3699 in("x8") 17_i64,
3700 in("x0") buf,
3701 in("x1") size,
3702 lateout("x0") ret,
3703 options(nostack),
3704 );
3705 }
3706 ret
3707}
3708
3709/// Renseigne les **statistiques de ressources** (`getrusage(2)`) pour la cible
3710/// [`RusageWho`] demandée.
3711///
3712/// # Errors
3713///
3714/// `getrusage(2)` ne peut échouer que sur `who` invalide (`EINVAL`) ou pointeur
3715/// invalide (`EFAULT`) — deux cas **rendus impossibles** par l'API typée
3716/// (`RusageWho` est un enum, le buffer est interne et valide). Le bras `Err` est
3717/// donc inatteignable via cette fonction (cf. `COVERAGE-EXCEPTIONS.md`,
3718/// STRUCTURAL/EFAULT-SAFE) ; la signature reste `Result` par honnêteté kernel.
3719pub fn getrusage(who: RusageWho) -> Result<Rusage, Errno> {
3720 let who_raw: i64 = match who {
3721 RusageWho::SelfProcess => 0,
3722 RusageWho::Children => -1,
3723 RusageWho::Thread => 1,
3724 };
3725 let mut usage = Rusage::default();
3726 let ptr = core::ptr::from_mut(&mut usage) as usize;
3727 // SAFETY: `ptr` désigne `usage` (local valide, taille `struct rusage`) ; le
3728 // kernel y écrit la structure. `who_raw` provient d'un enum ⇒ toujours valide.
3729 let ret = unsafe { raw_syscall_getrusage(who_raw, ptr) };
3730 if ret < 0 {
3731 return Err(errno_from_negative_syscall_ret(ret));
3732 }
3733 Ok(usage)
3734}
3735
3736#[cfg(target_arch = "x86_64")]
3737#[inline]
3738unsafe fn raw_syscall_getrusage(who: i64, usage: usize) -> i64 {
3739 let ret: i64;
3740 // SAFETY: SYS_getrusage (x86_64 = 98) écrit une `struct rusage` à `usage`.
3741 // Validité : contrat de l'appelant. Pas de `readonly` (écrit la mémoire).
3742 unsafe {
3743 core::arch::asm!(
3744 "syscall",
3745 in("rax") 98_i64,
3746 in("rdi") who,
3747 in("rsi") usage,
3748 lateout("rax") ret,
3749 lateout("rcx") _,
3750 lateout("r11") _,
3751 options(nostack),
3752 );
3753 }
3754 ret
3755}
3756
3757#[cfg(target_arch = "aarch64")]
3758#[inline]
3759unsafe fn raw_syscall_getrusage(who: i64, usage: usize) -> i64 {
3760 let ret: i64;
3761 // SAFETY: SYS_getrusage (aarch64 = 165). Voir la branche x86_64.
3762 unsafe {
3763 core::arch::asm!(
3764 "svc 0",
3765 in("x8") 165_i64,
3766 in("x0") who,
3767 in("x1") usage,
3768 lateout("x0") ret,
3769 options(nostack),
3770 );
3771 }
3772 ret
3773}
3774
3775#[cfg(test)]
3776mod tests {
3777 use super::*;
3778 use crate::test_support::{sfd, sraw};
3779
3780 // ── Affinité CPU (sched_set/getaffinity) ──────────────────────────────
3781 use core::num::NonZeroI32;
3782
3783 #[test]
3784 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
3785 fn cpu_affinity_round_trips_on_calling_task() {
3786 let mut original = CpuSet::new();
3787 get_cpu_affinity(None, &mut original).expect("get affinité courante");
3788 assert!(original.count() >= 1, "au moins un CPU en ligne");
3789 // Restreint à CPU 0 (toujours en ligne) puis relit.
3790 let mut only0 = CpuSet::new();
3791 assert!(only0.set(0));
3792 set_cpu_affinity(None, &only0).expect("set CPU 0");
3793 let mut read_back = CpuSet::new();
3794 get_cpu_affinity(None, &mut read_back).expect("get après set");
3795 assert!(read_back.contains(0));
3796 assert_eq!(read_back.count(), 1, "exactement CPU 0");
3797 // Restaure l'affinité d'origine (ne pas perturber le runner).
3798 set_cpu_affinity(None, &original).expect("restauration");
3799 }
3800
3801 #[test]
3802 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
3803 fn cpu_affinity_by_explicit_calling_tid() {
3804 // Branche `Some(tid)` d'`affinity_pid` (tâche appelante explicite).
3805 let tid = gettid();
3806 let mut current = CpuSet::new();
3807 get_cpu_affinity(Some(tid), &mut current).expect("get par tid");
3808 assert!(current.count() >= 1);
3809 set_cpu_affinity(Some(tid), ¤t).expect("set par tid (idempotent)");
3810 }
3811
3812 #[test]
3813 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
3814 fn empty_cpu_set_is_einval() {
3815 // Aucun CPU autorisé → le kernel refuse (EINVAL).
3816 let empty = CpuSet::new();
3817 assert!(matches!(set_cpu_affinity(None, &empty), Err(Errno::EINVAL)));
3818 }
3819
3820 #[test]
3821 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
3822 fn nonexistent_tid_is_esrch() {
3823 // `Tid` très improbable d'exister → ESRCH (couvre la branche d'erreur).
3824 let bogus = Tid::from_nonzero(NonZeroI32::new(0x3FFF_FFFF).expect("nz"));
3825 let mut cpus = CpuSet::new();
3826 assert!(matches!(
3827 get_cpu_affinity(Some(bogus), &mut cpus),
3828 Err(Errno::ESRCH)
3829 ));
3830 }
3831
3832 use crate::fs::dup_fd;
3833 use std::sync::{Arc, Barrier, Mutex};
3834 use std::thread;
3835
3836 /// Écrit le profil de couverture LLVM de l'enfant forké avant `exit_group`
3837 /// (qui court-circuite l'atexit LLVM). Sans flush, le corps des wrappers
3838 /// exercés dans l'enfant (clone3/prctl/…) apparaît non couvert.
3839 /// `LLVM_PROFILE_FILE` contient `%p` → un `.profraw` par enfant, fusionné.
3840 /// Aucun verrou I/O dans cette famille → le flush en fin d'enfant suffit.
3841 // Intentionnellement VIDE — **async-signal-safety**. Un enfant forké via
3842 // `clone3` hérite d'un espace d'adressage COPIÉ : si un thread frère tenait le
3843 // lock interne de `malloc` au moment du `clone3`, ce lock est copié VERROUILLÉ
3844 // (son détenteur n'existe pas dans l'enfant) ; `__llvm_profile_write_file`
3845 // **alloue** → le premier `malloc` de l'enfant deadlocke sur ce futex. On NE
3846 // flushe donc PAS dans l'enfant : ses lignes sont des exceptions **CHILD-EXIT**
3847 // (ADR-035), prouvées par le code de sortie observé via `waitid`. NE PAS
3848 // réintroduire de flush ici (flaky deadlock couche 0).
3849 fn flush_child_coverage() {}
3850
3851 /// Sortie d'un enfant forké de test : termine via `exit_group`. NE FLUSHE PLUS
3852 /// le profil (cf. [`flush_child_coverage`], désormais vide pour rester
3853 /// async-signal-safe). Les lignes de l'enfant sont des exceptions CHILD-EXIT.
3854 fn child_exit(status: i32) -> ! {
3855 flush_child_coverage();
3856 exit_group(status)
3857 }
3858
3859 /// Sérialise les tests qui **observent la fermeture d'un fd** (drop puis
3860 /// re-`borrow_raw` du même numéro pour vérifier `EBADF`).
3861 ///
3862 /// Défense en deux temps, complémentaire de [`high_unreusable_fd_target`] :
3863 /// le fd HAUT écarte la réutilisation par le churn de fd des AUTRES tests
3864 /// (qui allouent toujours des numéros BAS) ; ce verrou empêche les
3865 /// tests-frères de viser le MÊME numéro haut au même instant — sinon, l'un
3866 /// libère ce numéro pendant que l'autre le ré-alloue, et la course revient
3867 /// (déplacée sur le fd haut). Zéro dépendance externe (pas de `serial_test`),
3868 /// sans `--test-threads=1`. Cf. `docs/CI.md`.
3869 static FD_OBSERVATION_LOCK: Mutex<()> = Mutex::new(());
3870
3871 /// Prend [`FD_OBSERVATION_LOCK`] en ignorant l'empoisonnement : si un test
3872 /// du groupe a paniqué en tenant le verrou, on veut voir CET échec, pas une
3873 /// cascade d'erreurs « poisoned » sur les tests suivants.
3874 fn fd_observation_lock() -> std::sync::MutexGuard<'static, ()> {
3875 FD_OBSERVATION_LOCK
3876 .lock()
3877 .unwrap_or_else(std::sync::PoisonError::into_inner)
3878 }
3879
3880 /// Numéro de fd « haut », **non réutilisable**, pour observer une fermeture
3881 /// de fd sans flakiness sous instrumentation de couverture.
3882 ///
3883 /// Un `open`/`dup` concurrent alloue **toujours** le plus petit fd libre
3884 /// (numéros bas). Un fd proche de la limite douce `RLIMIT_NOFILE` ne peut
3885 /// donc jamais être réutilisé tant que le processus n'est pas en épuisement
3886 /// de fd (jamais en test). On vise `min(soft - 1, 4096)` : assez haut pour
3887 /// être hors d'atteinte de toute allocation concurrente, assez bas pour
3888 /// rester légal (< `soft`) et ne pas dimensionner inutilement la table de
3889 /// fd du kernel. Cf. `docs/CI.md` (course de réutilisation de fd).
3890 fn high_unreusable_fd_target() -> RawFd {
3891 const CAP: u64 = 4096;
3892 let soft = match getrlimit(Resource::NoFile) {
3893 Ok(Rlimit {
3894 soft: RlimitValue::Finite(n),
3895 ..
3896 }) => n,
3897 // Limite illimitée ou illisible : le plafond `CAP` reste sûr.
3898 _ => CAP.saturating_add(1),
3899 };
3900 let target = soft.saturating_sub(1).min(CAP);
3901 RawFd::try_from(target).unwrap_or(RawFd::MAX)
3902 }
3903
3904 // ── getpid ────────────────────────────────────────────────────────
3905
3906 #[test]
3907 fn getpid_returns_valid_pid() {
3908 let pid = getpid();
3909 assert!(pid.as_raw() > 0, "PID doit être strictement positif");
3910 }
3911
3912 #[test]
3913 fn getpid_is_consistent_across_calls() {
3914 // Invariant : `getpid` lit l'identité du processus appelant ; deux
3915 // appels consécutifs depuis le même processus doivent renvoyer la
3916 // même valeur.
3917 let p1 = getpid();
3918 let p2 = getpid();
3919 assert_eq!(p1, p2);
3920 }
3921
3922 // ── gettid ────────────────────────────────────────────────────────
3923
3924 #[test]
3925 fn gettid_returns_valid_tid() {
3926 let tid = gettid();
3927 assert!(tid.as_raw() > 0, "TID doit être strictement positif");
3928 }
3929
3930 #[test]
3931 fn gettid_is_consistent_within_thread() {
3932 let t1 = gettid();
3933 let t2 = gettid();
3934 assert_eq!(t1, t2, "le TID est stable pour un thread donné");
3935 }
3936
3937 #[test]
3938 fn gettid_distinguishes_distinct_threads() {
3939 // Invariant testé : deux threads vivants simultanément ont des
3940 // TIDs distincts. La barrière garantit que les deux threads sont
3941 // encore en vie au moment où chacun lit son TID — donc aucun TID
3942 // ne peut être recyclé par le kernel.
3943 let barrier = Arc::new(Barrier::new(2));
3944
3945 let b1 = Arc::clone(&barrier);
3946 let h1 = thread::spawn(move || {
3947 let tid = gettid();
3948 b1.wait();
3949 tid
3950 });
3951
3952 let b2 = Arc::clone(&barrier);
3953 let h2 = thread::spawn(move || {
3954 let tid = gettid();
3955 b2.wait();
3956 tid
3957 });
3958
3959 let tid1 = h1.join().expect("le thread 1 ne doit pas paniquer");
3960 let tid2 = h2.join().expect("le thread 2 ne doit pas paniquer");
3961 // Invariant kernel : deux threads vivants ont des TIDs distincts.
3962 assert_ne!(tid1, tid2);
3963 }
3964
3965 #[test]
3966 fn worker_thread_tid_differs_from_process_pid() {
3967 // Corollaire de l'invariant « gettid == getpid pour le thread
3968 // principal » : pour un thread *non* principal, gettid != getpid.
3969 // Le thread principal du processus est vérifié séparément par
3970 // l'intégration `tests/main_thread_invariant.rs` (binaire avec
3971 // `harness = false`, ce qui place le code sur le thread principal).
3972 let pid = getpid();
3973 let handle = thread::spawn(move || (gettid(), getpid()));
3974 let (worker_tid, worker_pid) = handle.join().expect("le worker ne doit pas paniquer");
3975 // Le PID est global au processus : tous les threads observent le même.
3976 assert_eq!(worker_pid, pid);
3977 // Un thread worker (non-principal) doit avoir un TID distinct du PID.
3978 assert_ne!(worker_tid.as_raw(), pid.as_raw());
3979 }
3980
3981 // ── clone3 / waitid : tests bout-en-bout ─────────────────────────
3982 //
3983 // Discipline de sortie de l'enfant (CRITIQUE, cf. exit_group ci-dessus) :
3984 // - Chaque test enfant termine via `child_exit(N)` (flush du profil de
3985 // couverture LLVM puis `exit_group(N)`) — un syscall direct, sans atexit
3986 // handler ni flush userspace (jamais `execve` dans cette PR ; le 5e test
3987 // de la spec — exec dans l'enfant — est déféré à la PR `execve`).
3988 // - On **ne** peut **pas** utiliser `std::process::exit` ici : dans
3989 // l'enfant d'un `clone3` fork issu du harnais de test multi-thread,
3990 // les atexit handlers (flush stdio, capture du harnais) deadlockent
3991 // sur des mutex hérités verrouillés par les threads du parent absents
3992 // dans l'enfant. Reproduit sur aarch64 (`futex_do_wait`), masqué sur
3993 // x86_64.
3994 // - Conséquence coverage : l'enfant n'écrit plus de `.profraw` (pas
3995 // d'atexit LLVM). Les rares lignes exécutées **uniquement** côté enfant
3996 // (le bras `CloneResult::Child` et la ligne `ret == 0` de `clone3`)
3997 // ne sont donc pas comptées par la couverture. Couverture éventuelle de
3998 // ces lignes : harnais dédié hors `cargo test` parallèle, ou exception
3999 // documentée (cf. docs/COVERAGE-EXCEPTIONS.md).
4000 //
4001 // Tests environnement-dépendants :
4002 // - Le test namespaces (NEWUSER) est skippé si AppArmor restreint les
4003 // user namespaces non privilégiés (cas Ubuntu 24.04 par défaut).
4004 // - Le test capabilities (NEWPID sans NEWUSER) est skippé si on est
4005 // root (auquel cas le syscall réussirait au lieu de retourner EPERM).
4006 // - Détails : `apparmor_restricts_userns()` et `is_root()` ci-dessous.
4007
4008 /// Vrai si `kernel.apparmor_restrict_unprivileged_userns` vaut 1.
4009 /// Sur Ubuntu 24.04+, ce sysctl est positionné par défaut et bloque
4010 /// `CLONE_NEWUSER` pour les processus non privilégiés (renvoie EPERM
4011 /// avant même l'exécution du syscall, via une hook AppArmor).
4012 fn apparmor_restricts_userns() -> bool {
4013 std::fs::read_to_string("/proc/sys/kernel/apparmor_restrict_unprivileged_userns")
4014 .map(|s| s.trim() == "1")
4015 .unwrap_or(false)
4016 }
4017
4018 /// Vrai si l'EUID courant est 0 (root). Lecture via `/proc/self/status`
4019 /// — pas de wrapper `geteuid` côté Air à ce stade. Les `.expect()`
4020 /// portent sur des invariants Linux solides (procfs présent, format
4021 /// stable de `/proc/PID/status`) ; leur branche d'échec est
4022 /// inatteignable en pratique.
4023 fn is_root() -> bool {
4024 let status = std::fs::read_to_string("/proc/self/status")
4025 .expect("/proc/self/status doit être lisible sur Linux");
4026 let uid_line = status
4027 .lines()
4028 .find(|l| l.starts_with("Uid:"))
4029 .expect("/proc/self/status doit contenir une ligne 'Uid:'");
4030 // Format : "Uid:\tREAL\tEFFECTIVE\tSAVED\tFS".
4031 let euid: u32 = uid_line
4032 .split_whitespace()
4033 .nth(2)
4034 .and_then(|s| s.parse().ok())
4035 .expect("EUID parsable dans /proc/self/status");
4036 euid == 0
4037 }
4038
4039 #[test]
4040 fn clone3_fork_exit_observed_by_waitid_exited() {
4041 let args = CloneArgs {
4042 flags: CloneFlags::empty(),
4043 exit_signal: Some(Signal::SIGCHLD),
4044 stack: None,
4045 child_tid: None,
4046 parent_tid: None,
4047 tls: None,
4048 };
4049 // SAFETY: fork classique (pas de CLONE_VM, pas de namespaces).
4050 // Aucune mémoire partagée, aucune capability requise.
4051 let result = unsafe { clone3(&args) }.expect("clone3 doit réussir");
4052 match result {
4053 CloneResult::Child => {
4054 // L'enfant écrit son profil LLVM (atexit) puis termine.
4055 child_exit(42);
4056 }
4057 CloneResult::Parent {
4058 child_pid,
4059 child_pidfd,
4060 } => {
4061 assert!(child_pidfd.is_none());
4062 let status = waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED)
4063 .expect("waitid doit réussir");
4064 let s = status.expect("événement EXITED disponible");
4065 assert_eq!(s.pid, child_pid);
4066 assert!(matches!(s.event, WaitEvent::Exited { code: 42 }));
4067 }
4068 }
4069 }
4070
4071 #[test]
4072 fn build_kernel_clone_args_maps_exit_signal_some_and_none() {
4073 // Branche `Some(sig)` : le signal est encodé sur les 8 bits de
4074 // poids faible de `clone_args.exit_signal`.
4075 let args_some = CloneArgs {
4076 flags: CloneFlags::empty(),
4077 exit_signal: Some(Signal::SIGCHLD),
4078 stack: None,
4079 child_tid: None,
4080 parent_tid: None,
4081 tls: None,
4082 };
4083 let k = build_kernel_clone_args(&args_some, 0);
4084 assert_eq!(
4085 k.exit_signal,
4086 u64::from(Signal::SIGCHLD.as_raw().unsigned_abs())
4087 );
4088
4089 // Branche `None` : 0 transmis au kernel — Air ne reproduit pas le
4090 // comportement de fork() qui force SIGCHLD ; l'appelant choisit.
4091 // Cette branche n'est pas testable bout-en-bout via clone3+waitid
4092 // sur Linux : avec `exit_signal=0`, le kernel détache l'enfant et
4093 // `waitid` retourne `ECHILD`. Cf. JOURNAL.md session
4094 // 2026-05-22 (suite clone3+waitid).
4095 let args_none = CloneArgs {
4096 flags: CloneFlags::empty(),
4097 exit_signal: None,
4098 stack: None,
4099 child_tid: None,
4100 parent_tid: None,
4101 tls: None,
4102 };
4103 let k = build_kernel_clone_args(&args_none, 0);
4104 assert_eq!(k.exit_signal, 0);
4105 }
4106
4107 #[test]
4108 fn build_kernel_clone_args_propagates_flags_and_pidfd_addr() {
4109 let args = CloneArgs {
4110 flags: CloneFlags::PIDFD | CloneFlags::NEWUSER,
4111 exit_signal: Some(Signal::SIGCHLD),
4112 stack: None,
4113 child_tid: None,
4114 parent_tid: None,
4115 tls: None,
4116 };
4117 let k = build_kernel_clone_args(&args, 0xdead_beef_u64);
4118 assert_eq!(k.flags, (CloneFlags::PIDFD | CloneFlags::NEWUSER).bits());
4119 assert_eq!(k.pidfd, 0xdead_beef_u64);
4120 // Champs non utilisés par cette PR : tous à zéro.
4121 assert_eq!(k.child_tid, 0);
4122 assert_eq!(k.parent_tid, 0);
4123 assert_eq!(k.stack, 0);
4124 assert_eq!(k.stack_size, 0);
4125 assert_eq!(k.tls, 0);
4126 assert_eq!(k.set_tid, 0);
4127 assert_eq!(k.set_tid_size, 0);
4128 assert_eq!(k.cgroup, 0);
4129 }
4130
4131 #[test]
4132 fn clone3_with_pidfd_yields_owned_pidfd_usable_for_waitid() {
4133 let args = CloneArgs {
4134 flags: CloneFlags::PIDFD,
4135 exit_signal: Some(Signal::SIGCHLD),
4136 stack: None,
4137 child_tid: None,
4138 parent_tid: None,
4139 tls: None,
4140 };
4141 // SAFETY: fork classique + CLONE_PIDFD ; pas de mémoire partagée.
4142 let result = unsafe { clone3(&args) }.expect("clone3 doit réussir");
4143 match result {
4144 CloneResult::Child => {
4145 child_exit(7);
4146 }
4147 CloneResult::Parent {
4148 child_pid,
4149 child_pidfd,
4150 } => {
4151 let pidfd = child_pidfd.expect("CLONE_PIDFD était demandé");
4152 let status = waitid(WaitTarget::PidFd(pidfd.as_fd()), WaitOptions::EXITED)
4153 .expect("waitid via pidfd doit réussir");
4154 let s = status.expect("événement EXITED disponible");
4155 assert_eq!(s.pid, child_pid);
4156 assert!(matches!(s.event, WaitEvent::Exited { code: 7 }));
4157 }
4158 }
4159 }
4160
4161 /// `pidfd_send_signal` avec un **`siginfo` explicite** (`Some(&info)`) : couvre
4162 /// le bras `Some(_)` du wrapper (jadis value-unreachable faute de constructeur
4163 /// `SignalInfo` public — désormais disponible via `SignalInfo::new_queue`,
4164 /// **sans aucun ajout de surface**).
4165 ///
4166 /// Le bras `Some(i) => …` est **exercé** dès qu'on passe `Some(&info)` (il
4167 /// marshale `info_ptr` **avant** le syscall). On envoie vers un pidfd **de
4168 /// soi-même** : `pidfd_send_signal` exige `si_signo == sig`, or `new_queue`
4169 /// (destiné à `rt_sigqueueinfo`, où le kernel **écrase** `si_signo`) le laisse
4170 /// à 0 ⇒ le kernel **rejette par `EINVAL` AVANT toute délivrance** — l'appel est
4171 /// donc **sûr même vers soi-même** (aucun signal n'est envoyé). Vérifié
4172 /// empiriquement (un envoi `SIGKILL`+`new_queue` ne tue pas la cible).
4173 #[test]
4174 #[cfg_attr(miri, ignore = "pidfd/signaux non supportés par Miri")]
4175 fn pidfd_send_signal_with_explicit_siginfo_exercises_some_arm() {
4176 use air_sys_types::{SignalInfo, SignalValue};
4177
4178 let self_pidfd = pidfd_open(getpid(), PidFdOpenFlags::empty()).expect("pidfd_open self");
4179 let info = SignalInfo::new_queue(SignalValue::Integer(0x5A5A));
4180 // ── CIBLE : bras `Some(&info)` (info_ptr non nul) marshalé. ──
4181 let result = pidfd_send_signal(self_pidfd.as_fd(), Signal::SIGUSR1, Some(&info));
4182 assert_eq!(
4183 result,
4184 Err(Errno::EINVAL),
4185 "siginfo explicite : bras Some exercé ; rejet EINVAL (si_signo=0 ≠ sig), \
4186 aucun signal délivré"
4187 );
4188 }
4189
4190 #[test]
4191 fn waitid_decodes_child_killed_by_signal() {
4192 // **Ce que ce test prouve.** `waitid` décode correctement un
4193 // événement enfant terminé par signal en `WaitEvent::Killed`
4194 // (branche `CLD_KILLED` ou `CLD_DUMPED` du dispatch
4195 // `siginfo_to_wait_event`). L'enfant produit l'événement en
4196 // s'auto-`abort()` — pour le kernel et pour `waitid`, le siginfo
4197 // résultant est strictement équivalent à « le parent a envoyé
4198 // SIGABRT » : `si_code = CLD_KILLED`/`CLD_DUMPED`,
4199 // `si_status = SIGABRT`. La branche du dispatch exercée est la
4200 // même que pour le scénario canonique.
4201 //
4202 // **Scénario canonique disponible depuis la PR pidfd_*.** Pour
4203 // une orchestration parent → enfant réelle (parent envoie un
4204 // signal qui tue l'enfant), voir le test
4205 // `pidfd_send_signal_canonical_parent_kills_child_with_sigkill`
4206 // ci-dessous. Ce test-ci reste utile : il exerce le décodage
4207 // `CLD_DUMPED` (SIGABRT dump-friendly) alors que le canonique
4208 // exerce `CLD_KILLED` (SIGKILL pur).
4209 let args = CloneArgs {
4210 flags: CloneFlags::empty(),
4211 exit_signal: Some(Signal::SIGCHLD),
4212 stack: None,
4213 child_tid: None,
4214 parent_tid: None,
4215 tls: None,
4216 };
4217 // SAFETY: fork classique, pas de mémoire partagée.
4218 let result = unsafe { clone3(&args) }.expect("clone3 doit réussir");
4219 match result {
4220 CloneResult::Child => {
4221 // SIGABRT raised; child dies. `abort()` bypasses atexit,
4222 // donc cette ligne n'incrémente PAS le compteur LLVM côté
4223 // child (trou de couverture acté dans JOURNAL).
4224 std::process::abort();
4225 }
4226 CloneResult::Parent { child_pid, .. } => {
4227 let status = waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED)
4228 .expect("waitid doit réussir");
4229 let s = status.expect("événement EXITED disponible");
4230 assert!(matches!(
4231 s.event,
4232 WaitEvent::Killed { signal, .. } if signal.as_raw() == Signal::SIGABRT.as_raw()
4233 ));
4234 }
4235 }
4236 }
4237
4238 #[test]
4239 fn waitid_dispatches_processgroup_and_anyprocessgroup() {
4240 // Exerce les bras `WaitTarget::ProcessGroup(_)` et
4241 // `WaitTarget::AnyProcessGroup` du dispatch interne de `waitid`.
4242 // NOHANG garantit un retour immédiat. **NOWAIT** (peek) est
4243 // crucial : sans lui, cet appel concurrent à d'autres tests qui
4244 // forkent et reapent leurs enfants risquerait de **consommer**
4245 // un événement appartenant à un autre test, lui faisant voir
4246 // `ECHILD`. NOWAIT garde l'événement waitable pour son
4247 // propriétaire légitime.
4248 let pid = getpid();
4249 let opts = WaitOptions::EXITED | WaitOptions::NOHANG | WaitOptions::NOWAIT;
4250 let _ = waitid(WaitTarget::ProcessGroup(pid), opts);
4251 let _ = waitid(WaitTarget::AnyProcessGroup, opts);
4252 }
4253
4254 #[test]
4255 fn waitid_nohang_returns_none_when_no_event_yet() {
4256 let args = CloneArgs {
4257 flags: CloneFlags::empty(),
4258 exit_signal: Some(Signal::SIGCHLD),
4259 stack: None,
4260 child_tid: None,
4261 parent_tid: None,
4262 tls: None,
4263 };
4264 // SAFETY: fork classique.
4265 let result = unsafe { clone3(&args) }.expect("clone3 doit réussir");
4266 match result {
4267 CloneResult::Child => {
4268 // Sleep long pour laisser au parent le temps de la fenêtre
4269 // NOHANG. 200 ms est largement suffisant sur n'importe
4270 // quelle CI réaliste.
4271 std::thread::sleep(std::time::Duration::from_millis(200));
4272 child_exit(0);
4273 }
4274 CloneResult::Parent { child_pid, .. } => {
4275 let status = waitid(
4276 WaitTarget::Pid(child_pid),
4277 WaitOptions::EXITED | WaitOptions::NOHANG,
4278 )
4279 .expect("waitid avec NOHANG doit réussir");
4280 assert!(status.is_none());
4281 // Cleanup : on attend l'enfant bloquant pour ne pas laisser
4282 // de zombie aux tests suivants.
4283 let _ = waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED)
4284 .expect("waitid bloquant final doit réussir");
4285 }
4286 }
4287 }
4288
4289 #[test]
4290 fn clone3_rejects_stack_argument() {
4291 // `clone3` est dédié au fork : une pile dédiée (création de thread) doit
4292 // être refusée en amont, sans toucher au kernel (Principe 4 : validation
4293 // amont). La création de thread avec pile passe par `clone_thread`.
4294 let dummy_buf: [u8; 16] = [0; 16];
4295 let mut buffer = dummy_buf;
4296 let args = CloneArgs {
4297 flags: CloneFlags::empty(),
4298 exit_signal: Some(Signal::SIGCHLD),
4299 stack: Some(air_sys_types::StackSpecification {
4300 addr: buffer.as_mut_ptr(),
4301 size: buffer.len(),
4302 }),
4303 child_tid: None,
4304 parent_tid: None,
4305 tls: None,
4306 };
4307 // SAFETY: le wrapper rejette `stack` avant tout syscall.
4308 let result = unsafe { clone3(&args) };
4309 assert_eq!(result.err(), Some(Errno::EINVAL));
4310 }
4311
4312 #[test]
4313 fn clone3_rejects_thread_creation_flags() {
4314 // CLONE_VM / CLONE_THREAD / CLONE_SIGHAND ne sont pas supportés
4315 // dans cette PR ; le wrapper doit les refuser en amont.
4316 let args = CloneArgs {
4317 flags: CloneFlags::VM,
4318 exit_signal: None,
4319 stack: None,
4320 child_tid: None,
4321 parent_tid: None,
4322 tls: None,
4323 };
4324 // SAFETY: refus en amont, sans syscall.
4325 let result = unsafe { clone3(&args) };
4326 assert_eq!(result.err(), Some(Errno::EINVAL));
4327 }
4328
4329 // ── clone_thread : création de thread (CLONE_VM) + join par CHILD_CLEARTID ─
4330 //
4331 // Marqueur écrit par l'enfant dans la VM partagée ; valeur arbitraire non
4332 // nulle, distincte de l'état initial (0) et d'un TID plausible.
4333 const THREAD_MARKER: u32 = 0xABCD_1234;
4334
4335 /// Point d'entrée du thread de test : écrit `THREAD_MARKER` dans le mot
4336 /// atomique pointé par `arg` (mémoire partagée via `CLONE_VM`).
4337 ///
4338 /// **Discipline côté enfant** : opération **brute**, async-signal-safe — une
4339 /// seule écriture atomique, **aucun** accès TLS ni allocation. Côté kernel,
4340 /// le trampoline `clone_thread` termine le thread via `SYS_exit` au retour.
4341 ///
4342 /// Cette fonction est **aussi** appelée directement côté parent par
4343 /// `thread_marker_entry_writes_marker_directly` (couverture de son corps :
4344 /// l'invocation enfant ne flushe pas le profil LLVM).
4345 extern "C" fn thread_marker_entry(arg: usize) {
4346 // SAFETY (test) : `arg` est l'adresse d'un `AtomicU32` vivant fourni par
4347 // l'appelant (parent), partagé via la VM commune. Alignement et validité
4348 // garantis par le site d'appel.
4349 let marker = unsafe { &*(arg as *const core::sync::atomic::AtomicU32) };
4350 marker.store(THREAD_MARKER, core::sync::atomic::Ordering::Release);
4351 }
4352
4353 #[test]
4354 fn thread_marker_entry_writes_marker_directly() {
4355 // Couvre le corps de `thread_marker_entry` côté parent (le profil LLVM
4356 // de son exécution enfant n'est pas flushable : SYS_exit direct).
4357 use core::sync::atomic::{AtomicU32, Ordering};
4358 let word = AtomicU32::new(0);
4359 let ptr = core::ptr::addr_of!(word) as usize;
4360 thread_marker_entry(ptr);
4361 assert_eq!(word.load(Ordering::Acquire), THREAD_MARKER);
4362 }
4363
4364 #[test]
4365 #[cfg_attr(miri, ignore = "clone3/futex non supportés par Miri")]
4366 fn clone_thread_spawns_runs_and_joins_via_child_cleartid() {
4367 use crate::futex::futex_wait;
4368 use crate::mem::{mmap_anonymous, munmap};
4369 use air_sys_types::mem::{MapFlags, ProtectionFlags};
4370 use air_sys_types::{FutexScope, TidReceiver};
4371 use core::sync::atomic::{AtomicU32, Ordering};
4372
4373 // Pile dédiée du thread : 64 KiB, page-alignée (mmap) ⇒ sommet aligné 16.
4374 const STACK_SIZE: usize = 64 * 1024;
4375 let mut stack = mmap_anonymous(
4376 STACK_SIZE,
4377 ProtectionFlags::READ | ProtectionFlags::WRITE,
4378 MapFlags::PRIVATE | MapFlags::STACK,
4379 )
4380 .expect("mmap pile thread");
4381
4382 // Marqueur partagé (VM commune) écrit par l'enfant, lu par le parent.
4383 let marker = AtomicU32::new(0);
4384 // Mot du join : `PARENT_SETTID` y écrit le TID (sans course, côté parent
4385 // avant retour) ; `CHILD_CLEARTID` le remet à 0 + `FUTEX_WAKE` à la mort
4386 // du thread.
4387 let tid_word = AtomicU32::new(0);
4388
4389 let flags = CloneFlags::VM
4390 | CloneFlags::FS
4391 | CloneFlags::FILES
4392 | CloneFlags::SIGHAND
4393 | CloneFlags::THREAD
4394 | CloneFlags::SYSVSEM
4395 | CloneFlags::PARENT_SETTID
4396 | CloneFlags::CHILD_CLEARTID;
4397
4398 let args = CloneArgs {
4399 flags,
4400 // CLONE_THREAD interdit tout signal de sortie.
4401 exit_signal: None,
4402 stack: Some(StackSpecification {
4403 addr: stack.as_mut_ptr(),
4404 size: STACK_SIZE,
4405 }),
4406 child_tid: Some(TidReceiver::new(&tid_word)),
4407 parent_tid: Some(TidReceiver::new(&tid_word)),
4408 tls: None,
4409 };
4410
4411 let marker_ptr = core::ptr::addr_of!(marker) as usize;
4412 // SAFETY: pile dédiée valide, non libérée avant le join ; `thread_marker_entry`
4413 // n'utilise ni TLS ni alloc et n'écrit qu'un mot atomique partagé vivant ;
4414 // `tid_word` reste vivant jusqu'à la fin du join.
4415 let tid = unsafe { clone_thread(&args, thread_marker_entry, marker_ptr) }
4416 .expect("clone_thread doit réussir");
4417 assert!(tid.as_raw() > 0, "TID enfant strictement positif");
4418
4419 // Join : le kernel a écrit le TID dans `tid_word` (PARENT_SETTID) ; il le
4420 // remet à 0 + `FUTEX_WAKE` (portée **partagée**) à la mort du thread.
4421 // Robustesse : `futex_wait` borné par un court timeout, puis relecture du
4422 // mot — le kernel écrit toujours 0, indépendamment du réveil ; la boucle
4423 // converge même si la portée du futex différait.
4424 loop {
4425 let v = tid_word.load(Ordering::Acquire);
4426 if v == 0 {
4427 break;
4428 }
4429 let _ = futex_wait(
4430 &tid_word,
4431 v,
4432 Some(core::time::Duration::from_millis(100)),
4433 FutexScope::Shared,
4434 );
4435 }
4436
4437 assert_eq!(
4438 marker.load(Ordering::Acquire),
4439 THREAD_MARKER,
4440 "l'enfant doit avoir écrit le marqueur dans la VM partagée"
4441 );
4442
4443 // La pile n'est libérée qu'après le join (thread mort) : pas d'UAF.
4444 munmap(stack).expect("munmap pile thread");
4445 }
4446
4447 #[test]
4448 #[cfg_attr(miri, ignore = "clone3 non supporté par Miri")]
4449 fn clone_thread_kernel_rejection_surfaces_errno() {
4450 // Couvre le bras d'erreur de `clone_thread` (échec AU SYSCALL, après la
4451 // validation amont). `CLONE_THREAD` sans `CLONE_SIGHAND` viole un
4452 // invariant *inconditionnel* de `copy_process`
4453 // (`THREAD ⟹ SIGHAND ⟹ VM`) : le kernel rend `EINVAL` **avant** de créer
4454 // quoi que ce soit — aucun thread n'est lancé, `entry` n'est jamais
4455 // exécuté. Déterministe sur tout Linux.
4456 use crate::mem::{mmap_anonymous, munmap};
4457 use air_sys_types::mem::{MapFlags, ProtectionFlags};
4458
4459 const STACK_SIZE: usize = 64 * 1024;
4460 let mut stack = mmap_anonymous(
4461 STACK_SIZE,
4462 ProtectionFlags::READ | ProtectionFlags::WRITE,
4463 MapFlags::PRIVATE | MapFlags::STACK,
4464 )
4465 .expect("mmap pile thread");
4466
4467 let args = CloneArgs {
4468 // VM présent (passe la validation amont) mais THREAD sans SIGHAND.
4469 flags: CloneFlags::VM | CloneFlags::THREAD,
4470 exit_signal: None,
4471 stack: Some(StackSpecification {
4472 addr: stack.as_mut_ptr(),
4473 size: STACK_SIZE,
4474 }),
4475 child_tid: None,
4476 parent_tid: None,
4477 tls: None,
4478 };
4479 // SAFETY: le kernel rejette la combinaison de drapeaux (EINVAL) avant
4480 // toute création de thread ; `entry` n'est jamais appelé. La pile reste
4481 // intacte et est libérée ensuite.
4482 let r = unsafe { clone_thread(&args, thread_marker_entry, 0) };
4483 assert_eq!(r.err(), Some(Errno::EINVAL));
4484
4485 munmap(stack).expect("munmap pile thread");
4486 }
4487
4488 /// Construit une `StackSpecification` à adresse/taille **contrôlées** pour
4489 /// les tests de validation (jamais déréférencée : rejet en amont).
4490 fn fake_stack(addr: usize, size: usize) -> StackSpecification {
4491 StackSpecification {
4492 addr: core::ptr::without_provenance_mut::<u8>(addr),
4493 size,
4494 }
4495 }
4496
4497 #[test]
4498 fn clone_thread_validation_rejects_bad_inputs() {
4499 let entry = thread_marker_entry;
4500 // (a) pile absente (avec CLONE_VM) → EINVAL.
4501 let a = CloneArgs {
4502 flags: CloneFlags::VM,
4503 exit_signal: None,
4504 stack: None,
4505 child_tid: None,
4506 parent_tid: None,
4507 tls: None,
4508 };
4509 assert_eq!(
4510 unsafe { clone_thread(&a, entry, 0) }.err(),
4511 Some(Errno::EINVAL),
4512 "pile absente"
4513 );
4514
4515 // (b) pile présente mais CLONE_VM absent → EINVAL.
4516 let b = CloneArgs {
4517 flags: CloneFlags::empty(),
4518 exit_signal: None,
4519 stack: Some(fake_stack(0x10000, 8192)),
4520 child_tid: None,
4521 parent_tid: None,
4522 tls: None,
4523 };
4524 assert_eq!(
4525 unsafe { clone_thread(&b, entry, 0) }.err(),
4526 Some(Errno::EINVAL),
4527 "CLONE_VM absent"
4528 );
4529
4530 // Base commune pour les cas (c)..(g) : CLONE_VM présent.
4531 let with_stack = |s: StackSpecification| CloneArgs {
4532 flags: CloneFlags::VM,
4533 exit_signal: None,
4534 stack: Some(s),
4535 child_tid: None,
4536 parent_tid: None,
4537 tls: None,
4538 };
4539
4540 // (c) adresse de pile nulle → EINVAL.
4541 let c = with_stack(fake_stack(0, 8192));
4542 assert_eq!(
4543 unsafe { clone_thread(&c, entry, 0) }.err(),
4544 Some(Errno::EINVAL),
4545 "addr nulle"
4546 );
4547
4548 // (d) taille < MIN_THREAD_STACK_SIZE → EINVAL.
4549 let d = with_stack(fake_stack(0x10000, 16));
4550 assert_eq!(
4551 unsafe { clone_thread(&d, entry, 0) }.err(),
4552 Some(Errno::EINVAL),
4553 "taille trop petite"
4554 );
4555
4556 // (e) `addr` non alignée sur 16 → EINVAL (1er opérande du `||`).
4557 let e = with_stack(fake_stack(0x10001, 8192));
4558 assert_eq!(
4559 unsafe { clone_thread(&e, entry, 0) }.err(),
4560 Some(Errno::EINVAL),
4561 "addr non alignée 16"
4562 );
4563
4564 // (f) `size` non alignée sur 16 (addr alignée) → EINVAL (2e opérande).
4565 let f = with_stack(fake_stack(0x10000, 8192 + 1));
4566 assert_eq!(
4567 unsafe { clone_thread(&f, entry, 0) }.err(),
4568 Some(Errno::EINVAL),
4569 "size non alignée 16"
4570 );
4571
4572 // (g) sommet `addr + size` en débordement d'adresse → EINVAL.
4573 let g = with_stack(fake_stack(0xFFFF_FFFF_FFFF_F000, 0x2000));
4574 assert_eq!(
4575 unsafe { clone_thread(&g, entry, 0) }.err(),
4576 Some(Errno::EINVAL),
4577 "débordement du sommet de pile"
4578 );
4579 }
4580
4581 #[test]
4582 fn build_kernel_clone_args_maps_thread_fields() {
4583 // Mapping des champs de thread : stack/stack_size, tls, child_tid, parent_tid.
4584 use air_sys_types::TidReceiver;
4585 use core::sync::atomic::AtomicU32;
4586 let word = AtomicU32::new(0);
4587 let recv = TidReceiver::new(&word);
4588 let args = CloneArgs {
4589 flags: CloneFlags::VM | CloneFlags::THREAD,
4590 exit_signal: None,
4591 stack: Some(fake_stack(0x4000, 0x8000)),
4592 child_tid: Some(recv),
4593 parent_tid: Some(recv),
4594 tls: Some(0xDEAD_0000),
4595 };
4596 let k = build_kernel_clone_args(&args, 0);
4597 assert_eq!(k.flags, (CloneFlags::VM | CloneFlags::THREAD).bits());
4598 assert_eq!(k.stack, 0x4000);
4599 assert_eq!(k.stack_size, 0x8000);
4600 assert_eq!(k.tls, 0xDEAD_0000);
4601 assert_eq!(k.child_tid, recv.as_ptr() as u64);
4602 assert_eq!(k.parent_tid, recv.as_ptr() as u64);
4603 // Champs hors périmètre : toujours nuls.
4604 assert_eq!(k.pidfd, 0);
4605 assert_eq!(k.set_tid, 0);
4606 assert_eq!(k.set_tid_size, 0);
4607 assert_eq!(k.cgroup, 0);
4608 }
4609
4610 #[test]
4611 fn clone3_with_new_user_namespace_runs_or_skips() {
4612 if apparmor_restricts_userns() {
4613 eprintln!(
4614 "SKIP clone3_with_new_user_namespace_runs_or_skips : \
4615 kernel.apparmor_restrict_unprivileged_userns=1 sur cet \
4616 environnement (Ubuntu 24.04+ par défaut)."
4617 );
4618 return;
4619 }
4620 let args = CloneArgs {
4621 flags: CloneFlags::NEWUSER,
4622 exit_signal: Some(Signal::SIGCHLD),
4623 stack: None,
4624 child_tid: None,
4625 parent_tid: None,
4626 tls: None,
4627 };
4628 // SAFETY: création de processus dans un user namespace ;
4629 // l'appelant n'a pas besoin de capability sur un système qui
4630 // autorise les userns non privilégiés.
4631 let result = unsafe { clone3(&args) }
4632 .expect("clone3 NEWUSER doit réussir sans restriction AppArmor");
4633 match result {
4634 CloneResult::Child => {
4635 child_exit(0);
4636 }
4637 CloneResult::Parent { child_pid, .. } => {
4638 let status = waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED)
4639 .expect("waitid doit réussir");
4640 let s = status.expect("événement disponible");
4641 assert!(matches!(s.event, WaitEvent::Exited { code: 0 }));
4642 }
4643 }
4644 }
4645
4646 #[test]
4647 fn clone3_newpid_without_caps_returns_eperm() {
4648 if is_root() {
4649 eprintln!(
4650 "SKIP clone3_newpid_without_caps_returns_eperm : \
4651 le test runner s'exécute en root, le scénario \
4652 « capabilities insuffisantes » est inapplicable."
4653 );
4654 return;
4655 }
4656 let args = CloneArgs {
4657 // NEWPID seul (sans NEWUSER) exige CAP_SYS_ADMIN dans le
4658 // namespace utilisateur courant ; un non-root est refusé.
4659 flags: CloneFlags::NEWPID,
4660 exit_signal: Some(Signal::SIGCHLD),
4661 stack: None,
4662 child_tid: None,
4663 parent_tid: None,
4664 tls: None,
4665 };
4666 // SAFETY: si EPERM est retourné, aucun fork n'a eu lieu. Le test
4667 // a été skippé en amont si on est root, donc la branche `Ok(_)`
4668 // est inatteignable sur un environnement conforme.
4669 let result = unsafe { clone3(&args) };
4670 assert_eq!(result.err(), Some(Errno::EPERM));
4671 }
4672
4673 #[test]
4674 fn waitid_invalid_options_returns_einval() {
4675 // Le kernel exige au moins un parmi EXITED/STOPPED/CONTINUED.
4676 let result = waitid(WaitTarget::AnyChild, WaitOptions::empty());
4677 assert_eq!(result.err(), Some(Errno::EINVAL));
4678 }
4679
4680 // ── siginfo_to_wait_event : dispatch testé sur KernelSiginfo synthétiques ─
4681 //
4682 // Voir docs/JOURNAL.md (session 2026-05-22, suite clone3+waitid) :
4683 // ces tests couvrent la **logique de dispatch** des 5 variantes de
4684 // WaitEvent. Le remplissage réel de `siginfo_t` par le kernel n'est
4685 // testé bout-en-bout que pour Exited et Killed (les deux tests
4686 // ci-dessus). Stopped/Continued/Trapped restent un trou de couverture
4687 // assumé pour ce périmètre.
4688
4689 fn make_siginfo(si_code: i32, si_status: i32) -> KernelSiginfo {
4690 let mut info = KernelSiginfo::zeroed();
4691 info.si_signo = Signal::SIGCHLD.as_raw();
4692 info.si_code = si_code;
4693 info.si_pid = 1234;
4694 info.si_uid = 1000;
4695 info.si_status = si_status;
4696 info
4697 }
4698
4699 #[test]
4700 fn dispatch_cld_exited() {
4701 let info = make_siginfo(CLD_EXITED, 42);
4702 assert_eq!(siginfo_to_wait_event(&info), WaitEvent::Exited { code: 42 });
4703 }
4704
4705 #[test]
4706 fn dispatch_cld_killed_no_dump() {
4707 let info = make_siginfo(CLD_KILLED, Signal::SIGKILL.as_raw());
4708 assert_eq!(
4709 siginfo_to_wait_event(&info),
4710 WaitEvent::Killed {
4711 signal: Signal::SIGKILL,
4712 core_dumped: false,
4713 }
4714 );
4715 }
4716
4717 #[test]
4718 fn dispatch_cld_dumped_sets_core_dumped_true() {
4719 let info = make_siginfo(CLD_DUMPED, Signal::SIGSEGV.as_raw());
4720 assert_eq!(
4721 siginfo_to_wait_event(&info),
4722 WaitEvent::Killed {
4723 signal: Signal::SIGSEGV,
4724 core_dumped: true,
4725 }
4726 );
4727 }
4728
4729 #[test]
4730 fn dispatch_cld_stopped() {
4731 let info = make_siginfo(CLD_STOPPED, Signal::SIGSTOP.as_raw());
4732 assert_eq!(
4733 siginfo_to_wait_event(&info),
4734 WaitEvent::Stopped {
4735 signal: Signal::SIGSTOP,
4736 }
4737 );
4738 }
4739
4740 #[test]
4741 fn dispatch_cld_trapped() {
4742 let info = make_siginfo(CLD_TRAPPED, Signal::SIGTRAP.as_raw());
4743 assert_eq!(
4744 siginfo_to_wait_event(&info),
4745 WaitEvent::Trapped {
4746 signal: Signal::SIGTRAP,
4747 }
4748 );
4749 }
4750
4751 #[test]
4752 fn dispatch_cld_continued() {
4753 // si_status est ignoré pour CLD_CONTINUED ; on passe 0.
4754 let info = make_siginfo(CLD_CONTINUED, 0);
4755 assert_eq!(siginfo_to_wait_event(&info), WaitEvent::Continued);
4756 }
4757
4758 #[test]
4759 #[should_panic(expected = "si_code inconnu")]
4760 fn dispatch_unknown_cld_code_panics() {
4761 // Couvre la branche `unreachable!` de siginfo_to_wait_event.
4762 // Cette branche ne peut être atteinte que si le kernel renseigne
4763 // un si_code hors {1..=6} — situation impossible en pratique mais
4764 // testée pour fermer la couverture du match.
4765 let info = make_siginfo(99, 0);
4766 let _ = siginfo_to_wait_event(&info);
4767 }
4768
4769 // ── errno_from_negative_syscall_ret : conversion brute → typée ─
4770
4771 #[test]
4772 fn errno_from_negative_syscall_ret_maps_known_codes() {
4773 assert_eq!(errno_from_negative_syscall_ret(-1), Errno::EPERM);
4774 assert_eq!(errno_from_negative_syscall_ret(-3), Errno::ESRCH);
4775 assert_eq!(errno_from_negative_syscall_ret(-4), Errno::EINTR);
4776 assert_eq!(errno_from_negative_syscall_ret(-9), Errno::EBADF);
4777 assert_eq!(errno_from_negative_syscall_ret(-10), Errno::ECHILD);
4778 assert_eq!(errno_from_negative_syscall_ret(-22), Errno::EINVAL);
4779 }
4780
4781 // Couvrent les deux opérandes du `debug_assert!(ret < 0 && ret > -4096)` :
4782 // `ret == 0` viole `ret < 0` ; `ret == -5000` viole `ret > -4096`.
4783 #[test]
4784 #[should_panic(expected = "ret < 0 && ret > -4096")]
4785 fn errno_from_negative_syscall_ret_panics_on_non_negative() {
4786 let _ = errno_from_negative_syscall_ret(0);
4787 }
4788
4789 #[test]
4790 #[should_panic(expected = "ret < 0 && ret > -4096")]
4791 fn errno_from_negative_syscall_ret_panics_below_errno_range() {
4792 let _ = errno_from_negative_syscall_ret(-5000);
4793 }
4794
4795 // ── pidfd_open / pidfd_send_signal / pidfd_getfd ─────────────────
4796 //
4797 // Stratégie de coverage post-fork (identique aux PRs précédentes) :
4798 // les enfants terminent par `exit_group(N)` quand c'est
4799 // possible (flush LLVM via atexit). Pour le test canonique SIGKILL,
4800 // la ligne `loop { sleep }` côté enfant reste non-couverte (SIGKILL
4801 // bypasse atexit), trou acté dans JOURNAL session clone3+waitid.
4802
4803 /// Vrai si Yama interdit le ptrace même sur un enfant direct (scope ≥ 2).
4804 /// Sur Ubuntu, ptrace_scope vaut 1 par défaut (parent → enfant
4805 /// autorisé), donc `pidfd_getfd` sur notre propre enfant passe.
4806 fn yama_blocks_ptrace_on_own_child() -> bool {
4807 let Ok(s) = std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope") else {
4808 return false;
4809 };
4810 let scope: u32 = s.trim().parse().unwrap_or(0);
4811 scope >= 2
4812 }
4813
4814 #[test]
4815 fn pidfd_open_returns_pidfd_usable_for_waitid_over_child_lifetime() {
4816 // Couvre : (a) `pidfd_open` happy path ; (b) le pidfd reste valide
4817 // pendant *et après* la mort du processus cible (jusqu'au reap).
4818 let args = CloneArgs {
4819 flags: CloneFlags::empty(),
4820 exit_signal: Some(Signal::SIGCHLD),
4821 stack: None,
4822 child_tid: None,
4823 parent_tid: None,
4824 tls: None,
4825 };
4826 // SAFETY: fork classique, pas de mémoire partagée.
4827 let result = unsafe { clone3(&args) }.expect("clone3 doit réussir");
4828 match result {
4829 CloneResult::Child => {
4830 // Petit délai : laisse au parent le temps d'ouvrir le
4831 // pidfd avant qu'on exit (pas strictement nécessaire — le
4832 // kernel garde l'entrée en process table jusqu'au reap —
4833 // mais rend la nature « post-mortem » du pidfd observable).
4834 std::thread::sleep(std::time::Duration::from_millis(20));
4835 child_exit(11);
4836 }
4837 CloneResult::Parent { child_pid, .. } => {
4838 let pidfd = pidfd_open(child_pid, PidFdOpenFlags::empty())
4839 .expect("pidfd_open sur enfant vivant doit réussir");
4840 let status = waitid(WaitTarget::PidFd(pidfd.as_fd()), WaitOptions::EXITED)
4841 .expect("waitid via pidfd post-mortem doit réussir");
4842 let s = status.expect("événement EXITED disponible");
4843 assert_eq!(s.pid, child_pid);
4844 assert!(matches!(s.event, WaitEvent::Exited { code: 11 }));
4845 }
4846 }
4847 }
4848
4849 #[test]
4850 fn pidfd_open_returns_esrch_for_nonexistent_pid() {
4851 // PID_MAX_LIMIT sur Linux 64-bit est typiquement 2^22 = 4 194 304.
4852 // 0x7FFF_FFFE (~2.1 G) est très au-delà : aucun processus ne peut
4853 // jamais avoir ce PID. Le kernel retourne ESRCH.
4854 let nonexistent = Pid::try_from_raw(0x7FFF_FFFE).expect("PID littéral valide");
4855 let result = pidfd_open(nonexistent, PidFdOpenFlags::empty());
4856 assert_eq!(result.err(), Some(Errno::ESRCH));
4857 }
4858
4859 #[test]
4860 fn pidfd_drop_closes_underlying_fd() {
4861 // **Ce que ce test prouve.** Le `Drop` du wrapper RAII — `PidFd` est un
4862 // `PidFd(OwnedFd)` sans `Drop` propre, son `Drop` EST donc celui
4863 // d'`OwnedFd` qui appelle `close(fd)` — ferme bien le descripteur
4864 // sous-jacent. Vérifié en post-drop : on tente `waitid` sur le numéro
4865 // fermé via `BorrowedFd::borrow_raw` ; le kernel doit retourner `EBADF`.
4866 //
4867 // **Immunité à la course de réutilisation de fd (sinon flaky sous
4868 // couverture).** Cargo teste en parallèle ; un autre thread peut
4869 // allouer un fd qui RÉUTILISE le numéro qu'on vient de fermer, dans la
4870 // fenêtre entre `drop` et l'observation — `waitid` ne verrait alors pas
4871 // `EBADF` mais le nouveau fd valide (faux négatif). L'instrumentation
4872 // de couverture élargit cette fenêtre et rendait le test flaky
4873 // (cf. `docs/CI.md`). Parade *par construction* : on duplique le pidfd
4874 // vers un numéro de fd HAUT (`high_unreusable_fd_target`), puis on
4875 // observe la fermeture de CE numéro. Une allocation concurrente prend
4876 // toujours le plus PETIT fd libre, jamais ce numéro haut → réutilisation
4877 // impossible — et `FD_OBSERVATION_LOCK` empêche les tests-frères de
4878 // viser ce même numéro haut au même instant. Pas de `--test-threads=1`.
4879 let _obs = fd_observation_lock();
4880 let pidfd = pidfd_open(getpid(), PidFdOpenFlags::empty())
4881 .expect("pidfd_open sur self doit réussir");
4882 let high = dup_fd(pidfd.as_fd(), high_unreusable_fd_target())
4883 .expect("dup_fd vers un fd haut doit réussir");
4884 // Le fd bas du pidfd peut être réutilisé après ce drop : sans effet, on
4885 // n'observe que le fd HAUT ci-dessous.
4886 drop(pidfd);
4887 let raw_fd: RawFd = high.as_raw_fd();
4888 // Ferme le fd HAUT : c'est le sujet de l'observation, et il est hors
4889 // d'atteinte de toute réutilisation concurrente.
4890 drop(high);
4891 // SAFETY: on re-borrow un fd qu'on sait fermé ET non réutilisable
4892 // (numéro haut). Usage strictement limité à la transmission au kernel
4893 // pour vérifier `EBADF` ; on ne lit ni n'écrit aucune ressource.
4894 let dangling = unsafe { BorrowedFd::borrow_raw(raw_fd) };
4895 let err = waitid(WaitTarget::PidFd(dangling), WaitOptions::EXITED)
4896 .expect_err("waitid sur fd fermé doit échouer");
4897 assert_eq!(err, Errno::EBADF);
4898 }
4899
4900 #[test]
4901 fn pidfd_send_signal_on_closed_pidfd_returns_ebadf() {
4902 // Couvre le chemin d'erreur de `pidfd_send_signal` (le scénario
4903 // canonique SIGKILL ci-dessous exerce seulement le chemin happy).
4904 // Immunité réutilisation de fd : même technique de fd HAUT que
4905 // `pidfd_drop_closes_underlying_fd` (cf. son commentaire).
4906 let _obs = fd_observation_lock();
4907 let pidfd = pidfd_open(getpid(), PidFdOpenFlags::empty())
4908 .expect("pidfd_open sur self doit réussir");
4909 let high = dup_fd(pidfd.as_fd(), high_unreusable_fd_target())
4910 .expect("dup_fd vers un fd haut doit réussir");
4911 drop(pidfd);
4912 let raw_fd: RawFd = high.as_raw_fd();
4913 drop(high);
4914 // SAFETY: re-borrow d'un fd fermé ET non réutilisable (numéro haut),
4915 // utilisé strictement pour vérifier le retour kernel `EBADF`.
4916 let dangling = unsafe { BorrowedFd::borrow_raw(raw_fd) };
4917 let err = pidfd_send_signal(dangling, Signal::SIGKILL, None)
4918 .expect_err("pidfd_send_signal sur fd fermé doit échouer");
4919 assert_eq!(err, Errno::EBADF);
4920 }
4921
4922 #[test]
4923 fn pidfd_getfd_on_closed_pidfd_returns_ebadf() {
4924 // Couvre le chemin d'erreur de `pidfd_getfd`. Même technique de fd HAUT
4925 // immunisée contre la réutilisation que les tests `EBADF` ci-dessus.
4926 let _obs = fd_observation_lock();
4927 let pidfd = pidfd_open(getpid(), PidFdOpenFlags::empty())
4928 .expect("pidfd_open sur self doit réussir");
4929 let high = dup_fd(pidfd.as_fd(), high_unreusable_fd_target())
4930 .expect("dup_fd vers un fd haut doit réussir");
4931 drop(pidfd);
4932 let raw_fd: RawFd = high.as_raw_fd();
4933 drop(high);
4934 // SAFETY: re-borrow d'un fd fermé ET non réutilisable (numéro haut).
4935 let dangling = unsafe { BorrowedFd::borrow_raw(raw_fd) };
4936 // `0` est un fd potentiellement ouvert côté cible (stdin) — peu importe
4937 // ici, le kernel rejette d'abord sur le pidfd fermé avec EBADF.
4938 let err = pidfd_getfd(dangling, 0, 0).expect_err("pidfd_getfd sur fd fermé doit échouer");
4939 assert_eq!(err, Errno::EBADF);
4940 }
4941
4942 #[test]
4943 fn pidfd_send_signal_canonical_parent_kills_child_with_sigkill() {
4944 // **Scénario canonique.** Le parent fork un enfant qui dort, lui
4945 // envoie SIGKILL via `pidfd_send_signal`, puis `waitid` observe
4946 // `WaitEvent::Killed { signal: SIGKILL, .. }`. C'est ce qui se
4947 // passe en production réelle (orchestration parent → enfant),
4948 // contrairement au scénario d'équivalence-kernel de
4949 // `waitid_decodes_child_killed_by_signal` (auto-abort).
4950 let args = CloneArgs {
4951 flags: CloneFlags::PIDFD,
4952 exit_signal: Some(Signal::SIGCHLD),
4953 stack: None,
4954 child_tid: None,
4955 parent_tid: None,
4956 tls: None,
4957 };
4958 // SAFETY: fork classique avec pidfd, pas de mémoire partagée.
4959 let result = unsafe { clone3(&args) }.expect("clone3 doit réussir");
4960 match result {
4961 CloneResult::Child => {
4962 // Le parent va nous tuer via SIGKILL. SIGKILL est
4963 // uncaught, donc on ne reprendra jamais d'ici. La ligne
4964 // n'est PAS couverte par LLVM (le profile n'est pas
4965 // flushé avant SIGKILL), trou acté.
4966 loop {
4967 std::thread::sleep(std::time::Duration::from_secs(60));
4968 }
4969 }
4970 CloneResult::Parent {
4971 child_pid,
4972 child_pidfd,
4973 } => {
4974 let pidfd = child_pidfd.expect("CLONE_PIDFD demandé");
4975 // Brève attente pour que l'enfant atteigne sa boucle
4976 // (réduit la probabilité d'envoyer SIGKILL avant que
4977 // l'enfant ait commencé son sleep — sans incidence sur
4978 // l'observation `waitid`).
4979 std::thread::sleep(std::time::Duration::from_millis(20));
4980 pidfd_send_signal(pidfd.as_fd(), Signal::SIGKILL, None)
4981 .expect("pidfd_send_signal SIGKILL doit réussir");
4982 let status = waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED)
4983 .expect("waitid doit réussir");
4984 let s = status.expect("événement disponible");
4985 assert!(matches!(
4986 s.event,
4987 WaitEvent::Killed { signal, .. } if signal.as_raw() == Signal::SIGKILL.as_raw()
4988 ));
4989 }
4990 }
4991 }
4992
4993 #[test]
4994 fn pidfd_getfd_duplicates_fd_from_child() {
4995 if yama_blocks_ptrace_on_own_child() {
4996 eprintln!(
4997 "SKIP pidfd_getfd_duplicates_fd_from_child : \
4998 kernel.yama.ptrace_scope >= 2 sur cet environnement \
4999 (blocage explicite, indépendant de parent → enfant)."
5000 );
5001 return;
5002 }
5003
5004 // On ouvre `/dev/null` AVANT le fork : parent et enfant héritent
5005 // tous deux du même fd (numéro identique, même entrée en file
5006 // table). Après le fork, le parent demande au kernel — via
5007 // `pidfd_getfd` ciblé sur l'enfant — un duplicata de CE numéro
5008 // de fd côté enfant. Le duplicata est un nouveau fd côté parent
5009 // pointant sur la même file que le fd cible.
5010 use std::os::fd::IntoRawFd;
5011 let placeholder = std::fs::File::open("/dev/null").expect("open /dev/null");
5012 let placeholder_raw: RawFd = placeholder.into_raw_fd();
5013
5014 let args = CloneArgs {
5015 flags: CloneFlags::PIDFD,
5016 exit_signal: Some(Signal::SIGCHLD),
5017 stack: None,
5018 child_tid: None,
5019 parent_tid: None,
5020 tls: None,
5021 };
5022 // SAFETY: fork classique avec pidfd.
5023 let result = unsafe { clone3(&args) }.expect("clone3 doit réussir");
5024 match result {
5025 CloneResult::Child => {
5026 // Sleep assez long pour que le parent ait le temps
5027 // d'invoquer pidfd_getfd avant qu'on exit (relâcherait
5028 // le fd côté enfant).
5029 std::thread::sleep(std::time::Duration::from_millis(80));
5030 child_exit(0);
5031 }
5032 CloneResult::Parent {
5033 child_pid,
5034 child_pidfd,
5035 } => {
5036 let pidfd = child_pidfd.expect("CLONE_PIDFD demandé");
5037 let dup: OwnedFd = pidfd_getfd(pidfd.as_fd(), placeholder_raw, 0)
5038 .expect("pidfd_getfd doit réussir");
5039 // `dup` est un fd côté parent pointant sur le même
5040 // `/dev/null` que celui qu'on avait ouvert avant fork.
5041 // Son numéro est nouveau (différent de `placeholder_raw`,
5042 // sauf collision improbable). Le simple fait que le
5043 // syscall ait réussi prouve la duplication ; on libère.
5044 let dup_raw = dup.as_raw_fd();
5045 assert!(dup_raw >= 0, "fd dupliqué doit être valide");
5046 drop(dup);
5047 // On nettoie aussi le `placeholder_raw` côté parent, qui
5048 // n'a jamais été wrappé dans un OwnedFd après le
5049 // `into_raw_fd()` initial.
5050 // SAFETY: `placeholder_raw` est un fd valide que nous
5051 // détenons (créé par `File::into_raw_fd`, transféré).
5052 drop(unsafe { OwnedFd::from_raw_fd(placeholder_raw) });
5053 let _ = waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED)
5054 .expect("waitid final pour reaper");
5055 }
5056 }
5057 }
5058
5059 // ── Groupes et sessions ───────────────────────────────────────────
5060
5061 #[test]
5062 fn getpgid_none_returns_positive_pgid() {
5063 let pgid = getpgid(None).expect("getpgid(None) doit réussir");
5064 assert!(pgid.as_raw() > 0);
5065 }
5066
5067 #[test]
5068 fn getpgid_self_equals_getpgid_none() {
5069 let self_pid = getpid();
5070 let via_none = getpgid(None).expect("getpgid(None)");
5071 let via_self = getpgid(Some(self_pid)).expect("getpgid(self)");
5072 assert_eq!(via_none, via_self);
5073 }
5074
5075 #[test]
5076 fn getsid_none_returns_positive_sid() {
5077 let sid = getsid(None).expect("getsid(None) doit réussir");
5078 assert!(sid.as_raw() > 0);
5079 }
5080
5081 #[test]
5082 fn getpgid_on_nonexistent_pid_returns_esrch() {
5083 // PID > PID_MAX_LIMIT (~2^22) ; couvre la branche d'erreur.
5084 let nope = Pid::try_from_raw(0x7FFF_FFFE).expect("pid littéral");
5085 let err = getpgid(Some(nope)).expect_err("getpgid PID inexistant");
5086 assert_eq!(err, Errno::ESRCH);
5087 }
5088
5089 #[test]
5090 fn getsid_on_nonexistent_pid_returns_esrch() {
5091 let nope = Pid::try_from_raw(0x7FFF_FFFE).expect("pid littéral");
5092 let err = getsid(Some(nope)).expect_err("getsid PID inexistant");
5093 assert_eq!(err, Errno::ESRCH);
5094 }
5095
5096 #[test]
5097 fn setsid_returns_eperm_when_caller_is_pgrp_leader() {
5098 // Le test runner cargo est lancé typiquement comme leader de son
5099 // process group ; setsid échoue alors avec EPERM. C'est le cas
5100 // canonique vérifié ici. Si l'environnement nous a placés dans
5101 // une situation où setsid réussit, on serait propriétaire d'une
5102 // nouvelle session — ce qui contaminerait les tests suivants.
5103 // On skippe proprement dans ce cas hypothétique.
5104 let self_pid = getpid();
5105 let pgid = getpgid(None).expect("getpgid");
5106 if self_pid != pgid {
5107 eprintln!(
5108 "SKIP setsid_returns_eperm_when_caller_is_pgrp_leader : \
5109 l'appelant n'est pas pgrp leader (pid={} != pgid={}), \
5110 scénario EPERM inapplicable",
5111 self_pid.as_raw(),
5112 pgid.as_raw()
5113 );
5114 return;
5115 }
5116 let err = setsid().expect_err("setsid sur pgrp leader doit échouer");
5117 assert_eq!(err, Errno::EPERM);
5118 }
5119
5120 #[test]
5121 fn setpgid_self_to_own_pgrp_is_idempotent() {
5122 // Place self dans son propre process group (déjà le cas). C'est
5123 // une opération idempotente qui exerce le wrapper sans changer
5124 // d'état observable.
5125 let our_pgid = getpgid(None).expect("getpgid");
5126 setpgid(None, Some(our_pgid)).expect("setpgid(None, our_pgid) doit réussir");
5127 }
5128
5129 #[test]
5130 fn setpgid_on_nonexistent_pid_returns_esrch() {
5131 // Couvre la branche d'erreur de setpgid.
5132 let nope = Pid::try_from_raw(0x7FFF_FFFE).expect("pid littéral");
5133 let err = setpgid(Some(nope), None).expect_err("setpgid PID inexistant");
5134 assert_eq!(err, Errno::ESRCH);
5135 }
5136
5137 #[test]
5138 fn setsid_succeeds_in_forked_child() {
5139 // Le test runner cargo n'est typiquement PAS pgrp leader → on ne
5140 // peut pas tester `setsid` happy-path directement (cf. test
5141 // `setsid_returns_eperm_when_caller_is_pgrp_leader`).
5142 // On fork donc un enfant : l'enfant hérite du pgrp du parent
5143 // (n'est pas leader), et `setsid()` y SUCCÈDE en créant une
5144 // nouvelle session avec lui-même comme leader+SID. L'enfant
5145 // encode le résultat dans son code de sortie.
5146 let args = CloneArgs {
5147 flags: CloneFlags::empty(),
5148 exit_signal: Some(Signal::SIGCHLD),
5149 stack: None,
5150 child_tid: None,
5151 parent_tid: None,
5152 tls: None,
5153 };
5154 // SAFETY: fork classique, pas de mémoire partagée.
5155 let result = unsafe { clone3(&args) }.expect("clone3");
5156 match result {
5157 CloneResult::Child => match setsid() {
5158 Ok(new_sid) => {
5159 // L'enfant doit voir new_sid == son propre PID (il
5160 // devient session leader). Encoder dans le code de
5161 // sortie permet au parent de vérifier.
5162 let child_pid = getpid();
5163 if new_sid == child_pid {
5164 child_exit(0);
5165 } else {
5166 child_exit(1);
5167 }
5168 }
5169 Err(_) => child_exit(2),
5170 },
5171 CloneResult::Parent { child_pid, .. } => {
5172 let status =
5173 waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED).expect("waitid");
5174 let s = status.expect("event");
5175 assert!(
5176 matches!(s.event, WaitEvent::Exited { code: 0 }),
5177 "setsid dans enfant doit réussir (event={:?})",
5178 s.event
5179 );
5180 }
5181 }
5182 }
5183
5184 #[test]
5185 fn setsid_returns_eperm_when_already_pgrp_leader_in_forked_child() {
5186 // Couvre la branche d'erreur (EPERM) de `setsid`. L'enfant fork'é
5187 // se rend pgrp leader (`setpgid(None, None)`) puis appelle
5188 // `setsid` qui DOIT échouer avec EPERM (un pgrp leader ne peut
5189 // pas créer une nouvelle session). Le code de sortie de l'enfant
5190 // encode le résultat.
5191 let args = CloneArgs {
5192 flags: CloneFlags::empty(),
5193 exit_signal: Some(Signal::SIGCHLD),
5194 stack: None,
5195 child_tid: None,
5196 parent_tid: None,
5197 tls: None,
5198 };
5199 // SAFETY: fork classique.
5200 let result = unsafe { clone3(&args) }.expect("clone3");
5201 match result {
5202 CloneResult::Child => {
5203 if setpgid(None, None).is_err() {
5204 child_exit(40);
5205 }
5206 match setsid() {
5207 Ok(_) => child_exit(41),
5208 Err(e) if e == Errno::EPERM => child_exit(0),
5209 Err(_) => child_exit(42),
5210 }
5211 }
5212 CloneResult::Parent { child_pid, .. } => {
5213 let status =
5214 waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED).expect("waitid");
5215 let s = status.expect("event");
5216 assert!(
5217 matches!(s.event, WaitEvent::Exited { code: 0 }),
5218 "setsid sur pgrp leader doit retourner EPERM, event={:?}",
5219 s.event
5220 );
5221 }
5222 }
5223 }
5224
5225 #[test]
5226 fn set_dumpable_round_trip_in_forked_child() {
5227 // `set_dumpable` est process-wide. Pour ne pas contaminer les
5228 // autres tests, on round-trippe dans un enfant fork'é qui meurt
5229 // immédiatement après vérification.
5230 let args = CloneArgs {
5231 flags: CloneFlags::empty(),
5232 exit_signal: Some(Signal::SIGCHLD),
5233 stack: None,
5234 child_tid: None,
5235 parent_tid: None,
5236 tls: None,
5237 };
5238 // SAFETY: fork classique.
5239 let result = unsafe { clone3(&args) }.expect("clone3");
5240 match result {
5241 CloneResult::Child => {
5242 // Round-trip : NotDumpable ↔ Dumpable. `SuidDumpable` (=2)
5243 // est déprécié et rejeté par les kernels Linux récents
5244 // (EINVAL), donc non testé ici — le wrapper le supporte
5245 // pour conformité spec mais sa branche kernel reste
5246 // hors test sur ces noyaux.
5247 if set_dumpable(DumpableMode::NotDumpable).is_err() {
5248 child_exit(10);
5249 }
5250 if !matches!(get_dumpable(), Ok(DumpableMode::NotDumpable)) {
5251 child_exit(11);
5252 }
5253 if set_dumpable(DumpableMode::Dumpable).is_err() {
5254 child_exit(12);
5255 }
5256 if !matches!(get_dumpable(), Ok(DumpableMode::Dumpable)) {
5257 child_exit(13);
5258 }
5259 child_exit(0);
5260 }
5261 CloneResult::Parent { child_pid, .. } => {
5262 let status =
5263 waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED).expect("waitid");
5264 let s = status.expect("event");
5265 assert!(
5266 matches!(s.event, WaitEvent::Exited { code: 0 }),
5267 "round-trip dumpable doit réussir, event={:?}",
5268 s.event
5269 );
5270 }
5271 }
5272 }
5273
5274 #[test]
5275 fn set_keep_caps_round_trip_in_forked_child() {
5276 // `set_keep_caps` est process-wide (touche les credentials du
5277 // process). Même technique : round-trip dans un enfant fork'é.
5278 let args = CloneArgs {
5279 flags: CloneFlags::empty(),
5280 exit_signal: Some(Signal::SIGCHLD),
5281 stack: None,
5282 child_tid: None,
5283 parent_tid: None,
5284 tls: None,
5285 };
5286 // SAFETY: fork classique.
5287 let result = unsafe { clone3(&args) }.expect("clone3");
5288 match result {
5289 CloneResult::Child => {
5290 if set_keep_caps(true).is_err() {
5291 child_exit(20);
5292 }
5293 if !matches!(get_keep_caps(), Ok(true)) {
5294 child_exit(21);
5295 }
5296 if set_keep_caps(false).is_err() {
5297 child_exit(22);
5298 }
5299 if !matches!(get_keep_caps(), Ok(false)) {
5300 child_exit(23);
5301 }
5302 child_exit(0);
5303 }
5304 CloneResult::Parent { child_pid, .. } => {
5305 let status =
5306 waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED).expect("waitid");
5307 let s = status.expect("event");
5308 assert!(
5309 matches!(s.event, WaitEvent::Exited { code: 0 }),
5310 "round-trip keep_caps doit réussir, event={:?}",
5311 s.event
5312 );
5313 }
5314 }
5315 }
5316
5317 // ── prctl ─────────────────────────────────────────────────────────
5318
5319 #[test]
5320 fn get_no_new_privs_returns_ok() {
5321 // Lecture seule — on ne SET PAS (irréversible côté process,
5322 // contaminerait les tests suivants). Le wrapper renvoie Ok(bool).
5323 let _value = get_no_new_privs().expect("get_no_new_privs doit réussir");
5324 }
5325
5326 #[test]
5327 fn get_dumpable_returns_known_mode() {
5328 // Lecture seule pour éviter de contaminer d'autres tests (le bit
5329 // dumpable est process-wide).
5330 let mode = get_dumpable().expect("get_dumpable doit réussir");
5331 assert!(matches!(
5332 mode,
5333 DumpableMode::NotDumpable | DumpableMode::Dumpable | DumpableMode::SuidDumpable
5334 ));
5335 }
5336
5337 #[test]
5338 fn thread_name_round_trip() {
5339 // PR_SET_NAME / PR_GET_NAME sont per-thread. cargo test exécute
5340 // chaque #[test] sur son propre worker thread, donc on peut
5341 // round-tripper sans contaminer les autres.
5342 let new_name = c"air-prctl-test";
5343 set_thread_name(new_name).expect("set_thread_name");
5344 let read_back = get_thread_name().expect("get_thread_name");
5345 assert_eq!(read_back.as_c_str(), new_name);
5346 }
5347
5348 #[test]
5349 fn thread_name_kernel_truncates_at_15_chars() {
5350 // Le kernel tronque à 15 caractères + NUL. On passe 20 caractères
5351 // et on vérifie que get_thread_name récupère les 15 premiers.
5352 let long_name = c"abcdefghijklmnopqrst"; // 20 caractères
5353 set_thread_name(long_name).expect("set_thread_name long");
5354 let read_back = get_thread_name().expect("get_thread_name long");
5355 assert_eq!(read_back.to_bytes(), b"abcdefghijklmno"); // 15 premiers
5356 }
5357
5358 #[test]
5359 fn parent_death_signal_round_trip() {
5360 // Per-thread sur Linux moderne. On utilise SIGKILL qui est déjà
5361 // dans le stub Signal ; round-trip Some/None/Some.
5362 set_parent_death_signal(Some(Signal::SIGKILL)).expect("set Some(SIGKILL)");
5363 let got = get_parent_death_signal().expect("get");
5364 assert_eq!(got, Some(Signal::SIGKILL));
5365
5366 set_parent_death_signal(None).expect("set None");
5367 let got = get_parent_death_signal().expect("get after clear");
5368 assert_eq!(got, None);
5369 }
5370
5371 #[test]
5372 fn set_timer_slack_accepts_zero_and_finite_values() {
5373 // 0 réinitialise à la valeur par défaut héritée. Toute autre
5374 // valeur est acceptée. On n'a pas get_timer_slack côté Air
5375 // (non listé par la spec) ; on vérifie juste l'absence d'erreur.
5376 set_timer_slack(0).expect("reset to default");
5377 set_timer_slack(50_000).expect("50 µs slack");
5378 }
5379
5380 #[test]
5381 fn cap_ambient_is_set_returns_ok_for_known_capability() {
5382 // Un thread non-root n'a aucune capability ambiante par défaut ;
5383 // le test vérifie que le wrapper retourne Ok(false), pas qu'il
5384 // donne une valeur particulière.
5385 let _value =
5386 cap_ambient_is_set(Capability::SysAdmin).expect("cap_ambient_is_set doit réussir");
5387 }
5388
5389 #[test]
5390 fn cap_ambient_lower_succeeds_unconditionally() {
5391 // PR_CAP_AMBIENT_LOWER clear le bit sans erreur même s'il n'était
5392 // pas positionné. Per-thread, donc safe.
5393 cap_ambient_lower(Capability::SysAdmin).expect("cap_ambient_lower doit réussir");
5394 }
5395
5396 #[test]
5397 fn cap_ambient_clear_all_succeeds() {
5398 // Per-thread, idempotent.
5399 cap_ambient_clear_all().expect("cap_ambient_clear_all doit réussir");
5400 }
5401
5402 #[test]
5403 fn cap_ambient_raise_non_permitted_returns_eperm() {
5404 if is_root() {
5405 eprintln!(
5406 "SKIP cap_ambient_raise_non_permitted_returns_eperm : \
5407 le runner s'exécute en root, scénario EPERM inapplicable."
5408 );
5409 return;
5410 }
5411 // SysAdmin n'est jamais dans permitted∩inheritable pour un user
5412 // non-root standard → raise échoue avec EPERM.
5413 let err = cap_ambient_raise(Capability::SysAdmin)
5414 .expect_err("cap_ambient_raise non-root doit échouer");
5415 assert_eq!(err, Errno::EPERM);
5416 }
5417
5418 // ── rlimits ───────────────────────────────────────────────────────
5419
5420 #[test]
5421 fn prlimit_no_op_round_trip_returns_current_value() {
5422 // Lit la valeur courante, la ré-écrit (no-op), vérifie qu'on
5423 // retrouve la même valeur. Exerce getrlimit ET setrlimit côtés
5424 // wrapper, sans modifier l'état process-wide.
5425 let original = prlimit(None, Resource::NoFile, None).expect("prlimit read");
5426 let _back = prlimit(None, Resource::NoFile, Some(original)).expect("prlimit noop set");
5427 let verified = prlimit(None, Resource::NoFile, None).expect("prlimit verify");
5428 assert_eq!(verified, original);
5429 }
5430
5431 #[test]
5432 fn getrlimit_legacy_alias_returns_same_as_prlimit() {
5433 let via_legacy = getrlimit(Resource::NoFile).expect("getrlimit");
5434 let via_prlimit = prlimit(None, Resource::NoFile, None).expect("prlimit");
5435 assert_eq!(via_legacy, via_prlimit);
5436 }
5437
5438 #[test]
5439 fn setrlimit_legacy_alias_lower_soft_round_trip_then_restore() {
5440 // setrlimit modifie l'état process-wide ; on restore en fin de
5441 // test. Le test exerce la branche `setrlimit -> prlimit(Some)`
5442 // et la lecture suivante.
5443 let original = getrlimit(Resource::NoFile).expect("get original");
5444 let lowered_soft = match original.soft {
5445 RlimitValue::Finite(n) => match n.checked_sub(1) {
5446 Some(m) if m > 0 => RlimitValue::Finite(m),
5447 _ => {
5448 eprintln!(
5449 "SKIP setrlimit_legacy_alias_lower_soft_round_trip_then_restore : \
5450 soft NoFile trop basse pour décrémenter"
5451 );
5452 return;
5453 }
5454 },
5455 RlimitValue::Infinity => {
5456 eprintln!(
5457 "SKIP setrlimit_legacy_alias_lower_soft_round_trip_then_restore : \
5458 soft NoFile est Infinity, scénario décrément inapplicable"
5459 );
5460 return;
5461 }
5462 };
5463
5464 let lowered = Rlimit {
5465 soft: lowered_soft,
5466 hard: original.hard,
5467 };
5468 setrlimit(Resource::NoFile, lowered).expect("setrlimit lowered");
5469 let verified = getrlimit(Resource::NoFile).expect("verify lowered");
5470 assert_eq!(verified.soft, lowered.soft);
5471
5472 // Restauration.
5473 setrlimit(Resource::NoFile, original).expect("restore original");
5474 }
5475
5476 #[test]
5477 fn setrlimit_raising_hard_limit_returns_eperm_for_non_root() {
5478 if is_root() {
5479 eprintln!(
5480 "SKIP setrlimit_raising_hard_limit_returns_eperm_for_non_root : \
5481 runner en root, scénario EPERM inapplicable."
5482 );
5483 return;
5484 }
5485 let original = getrlimit(Resource::NoFile).expect("get");
5486 let raised_hard = match original.hard {
5487 RlimitValue::Finite(h) => match h.checked_add(1) {
5488 Some(new_h) => RlimitValue::Finite(new_h),
5489 None => {
5490 eprintln!("SKIP : hard NoFile = u64::MAX, ne peut être incrémentée");
5491 return;
5492 }
5493 },
5494 RlimitValue::Infinity => {
5495 eprintln!("SKIP : hard NoFile = Infinity, impossible de la « relever »");
5496 return;
5497 }
5498 };
5499 let attempted = Rlimit {
5500 soft: original.soft,
5501 hard: raised_hard,
5502 };
5503 let err = setrlimit(Resource::NoFile, attempted)
5504 .expect_err("setrlimit raise hard doit échouer non-root");
5505 assert_eq!(err, Errno::EPERM);
5506 }
5507
5508 #[test]
5509 fn prlimit_on_nonexistent_pid_returns_esrch() {
5510 let nope = Pid::try_from_raw(0x7FFF_FFFE).expect("pid littéral");
5511 let err = prlimit(Some(nope), Resource::NoFile, None).expect_err("prlimit PID inexistant");
5512 assert_eq!(err, Errno::ESRCH);
5513 }
5514
5515 // ── capget / capset ────────────────────────────────────────────────
5516
5517 #[test]
5518 fn capget_current_thread_returns_ok() {
5519 let _set = capget(CapabilityTarget::CurrentThread).expect("capget(CurrentThread)");
5520 }
5521
5522 #[test]
5523 fn capget_via_process_target_returns_same_as_current_thread() {
5524 // Couvre le bras `CapabilityTarget::Process(_)` du helper de dispatch
5525 // `cap_target_to_kernel_pid` (les autres tests n'utilisent que
5526 // `CurrentThread` et `Thread`). Pour le thread principal du
5527 // processus, `Thread(getpid())` et `Process(getpid())` ciblent la
5528 // même tâche kernel — donc les capabilities retournées sont
5529 // identiques (à la nuance près que sur des threads worker, le
5530 // PID renvoie le thread principal).
5531 let self_pid = getpid();
5532 let via_current = capget(CapabilityTarget::CurrentThread).expect("current");
5533 let via_process = capget(CapabilityTarget::Process(self_pid)).expect("process");
5534 // On vérifie que l'opération réussit ; les capabilities entre
5535 // threads d'un même processus peuvent différer (CAP_AUDIT_WRITE
5536 // par exemple peut être thread-spécifique), donc on ne fait
5537 // qu'asserter la viabilité du chemin de dispatch.
5538 let _ = (via_current, via_process);
5539 }
5540
5541 #[test]
5542 fn rlim_value_infinity_round_trips_through_kernel_encoding() {
5543 // Couvre les deux branches `RlimitValue::Infinity` du codec
5544 // `rlim_value_to_kernel` / `rlim_value_from_kernel`. Sur Ubuntu
5545 // (et la plupart des distros), `Resource::Stack` a `hard =
5546 // Infinity` par défaut — donc lire+écrire+relire exerce les
5547 // deux conversions.
5548 let original = getrlimit(Resource::Stack).expect("getrlimit Stack");
5549 // Écrire la même valeur (no-op) : exerce `rlim_value_to_kernel`
5550 // pour les deux variantes (Finite pour soft, Infinity pour hard
5551 // si c'est le cas).
5552 setrlimit(Resource::Stack, original).expect("setrlimit no-op Stack");
5553 let verified = getrlimit(Resource::Stack).expect("re-read");
5554 assert_eq!(verified, original);
5555
5556 // Vérification directe que au moins une des deux limites est
5557 // Infinity dans l'environnement (sinon le test ne couvre pas la
5558 // branche visée).
5559 let touches_infinity = matches!(
5560 (original.soft, original.hard),
5561 (RlimitValue::Infinity, _) | (_, RlimitValue::Infinity)
5562 );
5563 if !touches_infinity {
5564 eprintln!(
5565 "SKIP rlim_value_infinity_round_trips_through_kernel_encoding : \
5566 Stack rlimit ne contient pas Infinity sur cet environnement \
5567 (soft={:?}, hard={:?}), la branche Infinity n'est pas couverte.",
5568 original.soft, original.hard
5569 );
5570 }
5571 }
5572
5573 #[test]
5574 fn capget_self_thread_equals_capget_current() {
5575 let self_tid = gettid();
5576 let via_current = capget(CapabilityTarget::CurrentThread).expect("capget current");
5577 let via_thread = capget(CapabilityTarget::Thread(self_tid)).expect("capget self thread");
5578 assert_eq!(via_current, via_thread);
5579 }
5580
5581 #[test]
5582 fn capset_no_op_with_current_values_succeeds() {
5583 let current = capget(CapabilityTarget::CurrentThread).expect("capget");
5584 // Réécrire les mêmes valeurs : opération autorisée pour tout
5585 // thread, n'élève rien.
5586 capset(CapabilityTarget::CurrentThread, ¤t).expect("capset no-op");
5587 }
5588
5589 #[test]
5590 fn capset_raise_non_permitted_returns_eperm() {
5591 if is_root() {
5592 eprintln!(
5593 "SKIP capset_raise_non_permitted_returns_eperm : \
5594 runner en root, scénario EPERM inapplicable."
5595 );
5596 return;
5597 }
5598 let current = capget(CapabilityTarget::CurrentThread).expect("capget");
5599 if current.permitted.contains(Capability::SysAdmin) {
5600 eprintln!(
5601 "SKIP : SysAdmin est déjà dans permitted (file capabilities ?), \
5602 scénario EPERM inapplicable."
5603 );
5604 return;
5605 }
5606 // Tente d'ajouter SysAdmin à effective alors qu'il n'est pas
5607 // dans permitted → kernel refuse (effective ⊆ permitted).
5608 let attempted = CapabilitySet {
5609 effective: current.effective.with(Capability::SysAdmin),
5610 permitted: current.permitted,
5611 inheritable: current.inheritable,
5612 };
5613 let err = capset(CapabilityTarget::CurrentThread, &attempted)
5614 .expect_err("capset élévation doit échouer non-root");
5615 assert_eq!(err, Errno::EPERM);
5616 }
5617
5618 // ── mask_to_words / words_to_mask : placement absolu lo/hi ────────
5619 //
5620 // Une éventuelle inversion lo/hi dans `mask_to_words` se compenserait
5621 // dans un simple round-trip via `words_to_mask`. Ces tests vérifient
5622 // le placement ABSOLU des bits dans chaque mot, indépendamment du
5623 // round-trip — ils catchent l'inversion.
5624
5625 #[test]
5626 fn mask_to_words_places_bit_0_in_lo() {
5627 let mask = CapabilityMask::from_bits(1_u64);
5628 let (lo, hi) = mask_to_words(mask);
5629 assert_eq!(lo, 1, "bit 0 doit aller en mot bas");
5630 assert_eq!(hi, 0, "bit 0 NE DOIT PAS aller en mot haut");
5631 }
5632
5633 #[test]
5634 fn mask_to_words_places_bit_32_in_hi() {
5635 let mask = CapabilityMask::from_bits(1_u64 << 32);
5636 let (lo, hi) = mask_to_words(mask);
5637 assert_eq!(lo, 0, "bit 32 NE DOIT PAS aller en mot bas");
5638 assert_eq!(hi, 1, "bit 32 doit aller en mot haut");
5639 }
5640
5641 #[test]
5642 fn words_to_mask_recombines_with_correct_placement() {
5643 // (lo=1, hi=0) doit donner bit 0 dans le mask, pas bit 32.
5644 let m1 = words_to_mask(1, 0);
5645 assert_eq!(m1.bits(), 1_u64);
5646
5647 // (lo=0, hi=1) doit donner bit 32 dans le mask, pas bit 0.
5648 let m2 = words_to_mask(0, 1);
5649 assert_eq!(m2.bits(), 1_u64 << 32);
5650
5651 // Combiné : bits dans les deux mots à la fois.
5652 let m3 = words_to_mask(0xFF, 0x01);
5653 assert_eq!(m3.bits(), (1_u64 << 32) | 0xFF_u64);
5654 }
5655
5656 #[test]
5657 fn mask_words_round_trip_preserves_known_capability() {
5658 // Sanity check : Capability::SysAdmin (bit 21, mot bas) survit
5659 // un round-trip complet.
5660 let original = CapabilityMask::empty().with(Capability::SysAdmin);
5661 let (lo, hi) = mask_to_words(original);
5662 let recovered = words_to_mask(lo, hi);
5663 assert_eq!(recovered, original);
5664 assert!(recovered.contains(Capability::SysAdmin));
5665 // SysAdmin = 21, donc mot bas exclusivement.
5666 assert_eq!(lo, 1_u32 << 21);
5667 assert_eq!(hi, 0);
5668 }
5669
5670 // ── Séparation de privilèges (privsep) ────────────────────────────────
5671 //
5672 // Les mutations de credentials sont **irréversibles** (saved-set) : on les
5673 // exécute dans des **enfants forkés** disposables (correctness via le code
5674 // de sortie observé par `waitid`). Les *getters* (read-only) et l'encodage
5675 // `Option → -1` sont couverts **en processus**. Tests privilégiés gardés par
5676 // `is_root()` (skip propre sinon → exception PRIVILEGE).
5677 //
5678 // Couverture des *setters* : un enfant qui **reste root** jusqu'au
5679 // `child_exit` voit son profil LLVM flushé (cf. `flush_child_coverage`), donc
5680 // leurs chemins **succès** sont comptabilisés ; leurs chemins **erreur**
5681 // (EPERM après réduction) sont prouvés fonctionnellement par l'enfant
5682 // « drop » (flush impossible une fois non-root → exception).
5683
5684 /// Forke un enfant (fork classique), exécute `body` dans l'enfant (qui
5685 /// retourne son **code de sortie** : `0` = succès, `N != 0` = étape `N`
5686 /// échouée), termine l'enfant via `child_exit` (flush de couverture), et
5687 /// retourne le code observé par le parent via `waitid`.
5688 fn run_child(body: impl FnOnce() -> i32) -> i32 {
5689 let args = CloneArgs {
5690 flags: CloneFlags::empty(),
5691 exit_signal: Some(Signal::SIGCHLD),
5692 stack: None,
5693 child_tid: None,
5694 parent_tid: None,
5695 tls: None,
5696 };
5697 // SAFETY: fork classique (pas de CLONE_VM, pas de namespaces) ; aucune
5698 // mémoire partagée, aucune capability requise pour le fork lui-même.
5699 match unsafe { clone3(&args) }.expect("clone3 doit réussir") {
5700 CloneResult::Child => child_exit(body()),
5701 CloneResult::Parent { child_pid, .. } => {
5702 let status = waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED)
5703 .expect("waitid")
5704 .expect("événement EXITED disponible");
5705 match status.event {
5706 WaitEvent::Exited { code } => code,
5707 other => panic!("événement de sortie inattendu : {other:?}"),
5708 }
5709 }
5710 }
5711 }
5712
5713 #[test]
5714 fn privsep_opt_encoding_round_trips() {
5715 // `None` → sentinelle `-1` ; `Some(id)` → sa valeur brute.
5716 assert_eq!(opt_gid_to_raw(None), u32::MAX);
5717 assert_eq!(opt_uid_to_raw(None), u32::MAX);
5718 assert_eq!(opt_gid_to_raw(Some(Gid::ROOT)), 0);
5719 assert_eq!(opt_uid_to_raw(Some(Uid::from_raw(1000))), 1000);
5720 assert_eq!(opt_gid_to_raw(Some(Gid::from_raw(u32::MAX))), u32::MAX);
5721 }
5722
5723 proptest::proptest! {
5724 /// Encodage `Option<Uid/Gid>` → `u32` : `None` ⇒ `-1`, `Some(v)` ⇒ `v`.
5725 #[test]
5726 fn prop_opt_encoding(value in proptest::option::of(0u32..=u32::MAX)) {
5727 let expected = value.unwrap_or(u32::MAX);
5728 proptest::prop_assert_eq!(opt_gid_to_raw(value.map(Gid::from_raw)), expected);
5729 proptest::prop_assert_eq!(opt_uid_to_raw(value.map(Uid::from_raw)), expected);
5730 }
5731 }
5732
5733 #[test]
5734 fn set_groups_too_many_is_einval() {
5735 // > NGROUPS_MAX ⇒ EINVAL **en amont** (Principe 4), sans privilège ni
5736 // syscall ni mutation : ne touche pas les credentials du test.
5737 let groups = vec![Gid::ROOT; NGROUPS_MAX + 1];
5738 assert_eq!(set_groups(&groups), Err(Errno::EINVAL));
5739 }
5740
5741 #[test]
5742 fn privsep_getters_read_own_credentials() {
5743 // Read-only : couvre les chemins **succès** de getres*/getgroups sans
5744 // privilège ni mutation. Buffer généreux (≤ 1024 groupes supplémentaires
5745 // — au-delà de tout utilisateur réel).
5746 let ru = get_resuid().expect("getresuid");
5747 let rg = get_resgid().expect("getresgid");
5748 // Un processus de test normal a real == effective.
5749 assert_eq!(ru.real, ru.effective);
5750 assert_eq!(rg.real, rg.effective);
5751 let mut buf = vec![Gid::ROOT; 1024];
5752 let groups = get_groups(&mut buf).expect("getgroups (≤ 1024 groupes)");
5753 assert!(groups.len() <= 1024);
5754 }
5755
5756 // Les corps d'enfant ci-dessous sont **sans branche locale** : conditions
5757 // combinées par `&` (ET bit-à-bit, non court-circuitant) et `==`/`is_ok`, de
5758 // sorte qu'un enfant qui largue ses privilèges (flush de couverture
5759 // impossible) n'introduise **aucune** branche non couverte. La correction est
5760 // prouvée par le code de sortie (`0` = succès) observé par le parent.
5761
5762 #[test]
5763 fn privsep_setters_succeed_as_root() {
5764 if !is_root() {
5765 return; // PRIVILEGE : nécessite CAP_SETUID/CAP_SETGID.
5766 }
5767 // Enfant qui **reste root** (uid 0) : son profil de couverture est flushé,
5768 // couvrant les chemins **succès** des setters (no-op root → root) et
5769 // l'erreur EINVAL de `get_groups` (buffer trop petit).
5770 let code = run_child(|| {
5771 let mut small = [Gid::ROOT; 1];
5772 let mut buf = [Gid::ROOT; 8];
5773 let ok = set_groups(&[]).is_ok()
5774 & set_groups(&[Gid::from_raw(1), Gid::from_raw(2)]).is_ok()
5775 & (get_groups(&mut small) == Err(Errno::EINVAL))
5776 & (get_groups(&mut buf).map(<[Gid]>::len) == Ok(2))
5777 & set_resgid(Some(Gid::ROOT), Some(Gid::ROOT), Some(Gid::ROOT)).is_ok()
5778 & set_resuid(Some(Uid::ROOT), Some(Uid::ROOT), Some(Uid::ROOT)).is_ok()
5779 & (get_resuid()
5780 == Ok(ResUid {
5781 real: Uid::ROOT,
5782 effective: Uid::ROOT,
5783 saved: Uid::ROOT,
5784 }))
5785 & (get_resgid()
5786 == Ok(ResGid {
5787 real: Gid::ROOT,
5788 effective: Gid::ROOT,
5789 saved: Gid::ROOT,
5790 }));
5791 i32::from(!ok)
5792 });
5793 assert_eq!(code, 0, "séquence privsep (root) échouée");
5794 }
5795
5796 #[test]
5797 fn privsep_drop_is_irreversible() {
5798 if !is_root() {
5799 return; // PRIVILEGE.
5800 }
5801 // `nobody`/`nogroup` (ids numériques standard, pas besoin d'entrée passwd).
5802 const NOBODY_UID: u32 = 65534;
5803 const NOBODY_GID: u32 = 65534;
5804 let code = run_child(|| {
5805 let gid = Gid::from_raw(NOBODY_GID);
5806 let uid = Uid::from_raw(NOBODY_UID);
5807 // Ordre **correct** du privsep : groupes → GID → UID.
5808 let dropped = set_groups(&[]).is_ok()
5809 & set_resgid(Some(gid), Some(gid), Some(gid)).is_ok()
5810 & set_resuid(Some(uid), Some(uid), Some(uid)).is_ok();
5811 // **Le cœur** : saved-set fixé ⇒ regain root impossible, et plus de
5812 // `CAP_SETGID` ⇒ pas de réajout de groupe ; tout figé sur la cible.
5813 let locked = (set_resuid(Some(Uid::ROOT), None, None) == Err(Errno::EPERM))
5814 & (set_resgid(Some(Gid::ROOT), None, None) == Err(Errno::EPERM))
5815 & (set_groups(&[Gid::ROOT]) == Err(Errno::EPERM))
5816 & (get_resuid()
5817 == Ok(ResUid {
5818 real: uid,
5819 effective: uid,
5820 saved: uid,
5821 }))
5822 & (get_resgid()
5823 == Ok(ResGid {
5824 real: gid,
5825 effective: gid,
5826 saved: gid,
5827 }));
5828 i32::from(!(dropped & locked))
5829 });
5830 assert_eq!(code, 0, "regain de privilège possible ou drop incorrect");
5831 }
5832
5833 #[test]
5834 fn privsep_resuid_none_changes_only_effective() {
5835 if !is_root() {
5836 return; // PRIVILEGE.
5837 }
5838 const NOBODY_UID: u32 = 65534;
5839 let code = run_child(|| {
5840 let uid = Uid::from_raw(NOBODY_UID);
5841 // `None` pour real et saved : seul l'effective change (root le peut).
5842 let ok = set_resuid(None, Some(uid), None).is_ok()
5843 & (get_resuid()
5844 == Ok(ResUid {
5845 real: Uid::ROOT,
5846 effective: uid,
5847 saved: Uid::ROOT,
5848 }));
5849 i32::from(!ok)
5850 });
5851 assert_eq!(code, 0, "None ne devrait changer que l'effective");
5852 }
5853
5854 // ── exec / redirection : execve, execveat, dup3, fchdir, chdir ────────
5855 //
5856 // Stratégie de test (cf. CLAUDE.md, rigueur couche 0) :
5857 // - `execve`/`execveat` ne peuvent PAS s'auto-tester dans le process de
5858 // test : un exec réussi REMPLACE l'image du process et n'en revient
5859 // jamais. On teste donc :
5860 // * les chemins d'ERREUR **en process** (ENOENT/EACCES/ENOEXEC) — l'exec
5861 // échoué retourne, ce qui couvre le corps complet du wrapper ;
5862 // * un exec RÉUSSI dans un **enfant forké** (`clone3`) qui exécute
5863 // `/usr/bin/true` (sortie 0) ou ouvre un fd dessus ; le parent vérifie
5864 // le code de sortie via `waitid`.
5865 // - `dup3`/`fchdir`/`chdir` sont testables directement. Les SUCCÈS de
5866 // `fchdir`/`chdir` mutent le cwd (état **process-global**) : ils sont donc
5867 // exécutés dans un enfant forké pour ne pas perturber les tests `fs`
5868 // concurrents qui résolvent des chemins relatifs depuis le cwd.
5869
5870 use air_sys_types::fd::AsFd;
5871
5872 // Vérification alloc-free du cwd dans les enfants forkés (cf. `fork_run`) :
5873 // `statx` remplit une structure sur la pile (aucun heap), donc utilisable
5874 // post-fork là où `std::fs::read_link` (qui alloue) ne l'est pas.
5875 use crate::fs::statx;
5876 use air_sys_types::fs::{StatxFlags, StatxMask};
5877
5878 // Création de fichier alloc-free dans un enfant forké (cf. `fork_run`) :
5879 // syscalls bruts au lieu de `std::fs::write`/`set_permissions` (qui allouent
5880 // ET dont le fd en écriture, ouvert côté parent, exposerait une course
5881 // `ETXTBSY` via héritage à un `clone3` concurrent).
5882 use crate::fs::{close, fchmodat, openat, write};
5883 use air_sys_types::fs::OpenFlags;
5884
5885 /// Forke via `clone3`, exécute `child` dans l'enfant (qui termine via
5886 /// `child_exit` avec le code retourné), et renvoie le code de sortie observé
5887 /// par le parent via `waitid`. `child` ne doit JAMAIS paniquer — un `panic`
5888 /// dans un enfant forké d'un harnais multi-thread corromprait le protocole
5889 /// de sortie enfant→parent ; il rapporte ses échecs par un code ≠ 0.
5890 ///
5891 /// **La closure `child` DOIT être async-signal-safe : aucune allocation,
5892 /// aucun verrou.** Le harnais de test est multi-threadé (tests `io_uring`
5893 /// concurrents : workers/SQPOLL). Si le `clone3` survient pendant qu'un
5894 /// thread sibling tient le **verrou d'allocateur de la glibc**, le premier
5895 /// `malloc` de l'enfant deadlock en `futex_wait` (l'état du verrou est copié
5896 /// *locked*, et le thread qui le détenait n'existe pas dans l'enfant) → le
5897 /// parent `waitid` à l'infini. La closure ne doit donc faire QUE des
5898 /// syscalls sur des données **pré-construites dans le parent avant le fork**
5899 /// (`execve_prepared`/[`execveat_prepared`] sur des `CStrArray` matérialisés,
5900 /// `chdir`/`fchdir`/`statx`, comparaisons d'`Errno`) : zéro `format!`,
5901 /// `String`, `Vec`, `CString::new`, `std::fs::*`, `File::open` ou `println!`.
5902 /// Tout setup allouant (création/écriture de fichier, ouverture de fd,
5903 /// marshalling argv/envp) se fait côté parent ; le nettoyage aussi.
5904 fn fork_run(child: impl FnOnce() -> i32) -> i32 {
5905 let args = CloneArgs {
5906 flags: CloneFlags::empty(),
5907 exit_signal: Some(Signal::SIGCHLD),
5908 stack: None,
5909 child_tid: None,
5910 parent_tid: None,
5911 tls: None,
5912 };
5913 // SAFETY: fork classique (pas de CLONE_VM ni de namespace) : aucune
5914 // mémoire partagée, aucune capability requise.
5915 match unsafe { clone3(&args) }.expect("clone3 doit réussir") {
5916 CloneResult::Child => child_exit(child()),
5917 CloneResult::Parent { child_pid, .. } => {
5918 let status = waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED)
5919 .expect("waitid doit réussir")
5920 .expect("événement EXITED disponible");
5921 match status.event {
5922 WaitEvent::Exited { code } => code,
5923 other => panic!("attendu Exited, obtenu : {other:?}"),
5924 }
5925 }
5926 }
5927 }
5928
5929 /// Invoque `execveat` **sans allocation**, depuis des [`CStrArray`]
5930 /// pré-construits dans le parent. Pendant qu'[`execveat`] (production)
5931 /// alloue ses tableaux argv/envp via [`CStrArray::new`], ce shim de test
5932 /// reproduit son cœur (le seul syscall) en sautant cette construction : il
5933 /// est donc utilisable depuis la closure de [`fork_run`], qui doit rester
5934 /// async-signal-safe. Renvoie toujours l'`Errno` d'échec — un succès remplace
5935 /// l'image du process et ne revient jamais.
5936 fn execveat_prepared(
5937 dirfd: DirFd<'_>,
5938 path: &CStr,
5939 argv: &CStrArray<'_>,
5940 envp: &CStrArray<'_>,
5941 flags: ExecveatFlags,
5942 ) -> Errno {
5943 // SAFETY: même contrat que `execveat` — `path` est un `CStr` valide
5944 // NUL-terminé ; `argv`/`envp` exposent des tableaux de pointeurs C
5945 // valides, NULL-terminés, vivants pour l'appel (matérialisés AVANT le
5946 // fork dans le parent) ; `dirfd` est `AT_FDCWD` ou un fd valide
5947 // emprunté ; `flags` est un masque `AT_*` typé. Le kernel ne fait que
5948 // lire ces données.
5949 let ret = unsafe {
5950 raw_syscall_execveat(
5951 dirfd_to_raw(dirfd),
5952 path.as_ptr() as u64,
5953 argv.as_ptr() as u64,
5954 envp.as_ptr() as u64,
5955 flags.bits(),
5956 )
5957 };
5958 errno_from_negative_syscall_ret(ret)
5959 }
5960
5961 // ── CStrArray (marshalling argv/envp, pur — aussi fuzzé) ──────────────
5962
5963 #[test]
5964 fn cstr_array_empty_contains_only_null() {
5965 let arr = CStrArray::new(&[]);
5966 assert_eq!(arr.len(), 0);
5967 assert!(arr.is_empty());
5968 // SAFETY: `as_ptr` renvoie un tableau valide d'au moins un pointeur
5969 // (le terminateur). On lit le premier élément.
5970 let first = unsafe { *arr.as_ptr() };
5971 assert!(first.is_null(), "tableau vide ⇒ uniquement le NULL final");
5972 }
5973
5974 #[test]
5975 fn cstr_array_preserves_order_and_null_terminates() {
5976 let items: [&CStr; 3] = [c"argv0", c"deux", c"trois"];
5977 let arr = CStrArray::new(&items);
5978 assert_eq!(arr.len(), 3);
5979 assert!(!arr.is_empty());
5980 let base = arr.as_ptr();
5981 for (i, item) in items.iter().enumerate() {
5982 // SAFETY: `i < 3` ⇒ dans les bornes du tableau (3 pointeurs + NULL).
5983 let ptr = unsafe { *base.add(i) };
5984 // SAFETY: `ptr` pointe la i-ème chaîne C, vivante (empruntée à
5985 // `items`), NUL-terminée par construction de `CStr`.
5986 let got = unsafe { CStr::from_ptr(ptr) };
5987 assert_eq!(got, *item, "ordre préservé en position {i}");
5988 }
5989 // Terminateur NULL en position 3.
5990 // SAFETY: position du terminateur, dans les bornes (4 éléments).
5991 let term = unsafe { *base.add(3) };
5992 assert!(term.is_null(), "terminaison NULL présente");
5993 }
5994
5995 #[test]
5996 fn fuzz_api_marshal_exec_args_is_total() {
5997 // Liste vide (le tableau ne contient que le NULL : boucle s'arrête en 0).
5998 fuzz_api::marshal_exec_args(vec![]);
5999 // Morceaux valides + un morceau à NUL embarqué (écarté proprement) +
6000 // un argument vide (valide). Couvre le parcours non vide.
6001 fuzz_api::marshal_exec_args(vec![
6002 b"prog".to_vec(),
6003 b"--flag".to_vec(),
6004 b"avec\0nul".to_vec(),
6005 b"".to_vec(),
6006 ]);
6007 }
6008
6009 // ── execve / execveat ────────────────────────────────────────────────
6010
6011 #[test]
6012 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6013 fn execve_enoent_on_missing_path() {
6014 // En process : un exec échoué RETOURNE, couvrant tout le corps du
6015 // wrapper (marshalling + syscall + conversion errno).
6016 let err = execve(c"/nonexistent/air-exec-test-xyz", &[c"x"], &[]).unwrap_err();
6017 assert_eq!(err, Errno::ENOENT);
6018 }
6019
6020 #[test]
6021 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6022 fn execve_eacces_on_non_executable_file() {
6023 // `/proc/self/status` est un fichier régulier sans aucun bit `x`
6024 // (toujours présent sur Linux, jamais ouvert en écriture ⇒ pas de
6025 // course `ETXTBSY`). L'exécution exige au moins un bit `x`, même pour
6026 // root (CAP_DAC_OVERRIDE n'accorde l'exécution que si un bit `x` existe)
6027 // ⇒ EACCES de façon déterministe.
6028 let err = execve(c"/proc/self/status", &[c"x"], &[]).unwrap_err();
6029 assert_eq!(err, Errno::EACCES);
6030 }
6031
6032 #[test]
6033 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6034 fn execve_enoexec_on_unknown_format() {
6035 // Préparation côté PARENT (allocation autorisée ; cf. la règle
6036 // async-signal-safe de `fork_run`) : on construit le `CString` du chemin
6037 // et les tableaux argv/envp. Le fichier au format invalide (ni ELF, ni
6038 // `#!`) est en revanche créé **dans l'enfant**, via des **syscalls bruts**
6039 // (`openat`/`write`/`fchmodat`/`close`) qui n'allouent pas : le fd en
6040 // écriture reste donc **privé à cet enfant mono-thread** (aucun `clone3`
6041 // concurrent ne peut l'hériter) et il est **fermé avant l'`execve`** ⇒
6042 // pas de course `ETXTBSY`. `std::fs::write`/`set_permissions` sont
6043 // proscrits ici : ils allouent (deadlock allocateur post-fork) et, si on
6044 // les déportait côté parent, leur fd en écriture rouvrirait la course
6045 // `ETXTBSY`. Le format invalide donne ENOEXEC, rapporté par le code de
6046 // sortie.
6047 let path = format!("/tmp/air-enoexec-{}", std::process::id());
6048 let cpath = CString::new(path.clone().into_bytes()).expect("chemin /tmp sans NUL");
6049 let argv: [&CStr; 1] = [c"x"];
6050 let argv_array = CStrArray::new(&argv);
6051 let envp: [&CStr; 0] = [];
6052 let envp_array = CStrArray::new(&envp);
6053
6054 let code = fork_run(|| {
6055 let bytes: &[u8] = b"\xff\xfe pas un binaire";
6056 // Les erreurs de setup sont ignorées (comme l'étaient les
6057 // `std::fs::write`/`set_permissions` de la version précédente) : un
6058 // échec de création laisserait l'`execve` renvoyer ENOENT/EACCES, et
6059 // l'assertion finale tomberait avec un message clair. `fchmodat`
6060 // force 0o755 indépendamment de l'umask (qui pourrait masquer le bit
6061 // `x` du propriétaire et donner EACCES au lieu d'ENOEXEC). Le fd en
6062 // écriture est fermé AVANT l'`execve` ⇒ pas de course `ETXTBSY`.
6063 // `.map` (combinateur de `core`) plutôt qu'un `if let` : la
6064 // discrimination Ok/Err vit dans la bibliothèque (hors couverture de
6065 // ce fichier), comme l'étaient les branches internes du
6066 // `std::fs::write` précédent — pas de branche `process.rs` ajoutée.
6067 let _ = openat(
6068 DirFd::Cwd,
6069 &cpath,
6070 OpenFlags::WRONLY | OpenFlags::CREAT | OpenFlags::TRUNC,
6071 0o755,
6072 )
6073 .map(|fd| {
6074 let _ = write(fd.as_fd(), bytes);
6075 let _ = fchmodat(DirFd::Cwd, &cpath, 0o755, 0);
6076 let _ = close(fd);
6077 });
6078 let err = execve_prepared(&cpath, &argv_array, &envp_array).unwrap_err();
6079 i32::from(err != Errno::ENOEXEC)
6080 });
6081
6082 let _ = std::fs::remove_file(&path); // nettoyage côté PARENT
6083 assert_eq!(code, 0, "execve d'un format inconnu doit donner ENOEXEC");
6084 }
6085
6086 #[test]
6087 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6088 fn execve_runs_binary_in_forked_child() {
6089 // Tableaux argv/envp matérialisés côté PARENT ; l'enfant ne fait qu'un
6090 // `execve_prepared` (alloc-free, async-signal-safe — cf. `fork_run`).
6091 let argv: [&CStr; 1] = [c"true"];
6092 let argv_array = CStrArray::new(&argv);
6093 let envp: [&CStr; 0] = [];
6094 let envp_array = CStrArray::new(&envp);
6095 let code = fork_run(|| {
6096 // Succès ⇒ `/usr/bin/true` s'exécute et sort 0 ; on n'arrive après
6097 // l'exec QUE s'il a échoué.
6098 let _ = execve_prepared(c"/usr/bin/true", &argv_array, &envp_array);
6099 1
6100 });
6101 assert_eq!(code, 0, "execve(/usr/bin/true) doit aboutir à un exit 0");
6102 }
6103
6104 #[test]
6105 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6106 fn execveat_enoent_on_missing_path() {
6107 // En process (échec retourné) : couvre le corps d'`execveat` et le bras
6108 // `DirFd::Cwd` de `dirfd_to_raw`.
6109 let err = execveat(
6110 DirFd::Cwd,
6111 c"/nonexistent/air-execveat-xyz",
6112 &[c"x"],
6113 &[],
6114 ExecveatFlags::empty(),
6115 )
6116 .unwrap_err();
6117 assert_eq!(err, Errno::ENOENT);
6118 }
6119
6120 #[test]
6121 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6122 fn execveat_empty_path_executes_open_fd() {
6123 // EMPTY_PATH : l'exécutable est désigné directement par le fd ouvert.
6124 // Ouverture du fd et tableaux argv/envp matérialisés côté PARENT ;
6125 // l'enfant ne fait qu'un `execveat_prepared` (alloc-free) sur le
6126 // `BorrowedFd` emprunté — cf. la règle async-signal-safe de `fork_run`.
6127 let file = std::fs::File::open("/usr/bin/true").expect("ouverture de /usr/bin/true");
6128 let fd = sfd(&file);
6129 let argv: [&CStr; 1] = [c"true"];
6130 let argv_array = CStrArray::new(&argv);
6131 let envp: [&CStr; 0] = [];
6132 let envp_array = CStrArray::new(&envp);
6133 let code = fork_run(|| {
6134 let _ = execveat_prepared(
6135 DirFd::Fd(fd),
6136 c"",
6137 &argv_array,
6138 &envp_array,
6139 ExecveatFlags::EMPTY_PATH,
6140 );
6141 1 // execveat a échoué
6142 });
6143 assert_eq!(
6144 code, 0,
6145 "execveat(EMPTY_PATH, fd /usr/bin/true) doit sortir 0"
6146 );
6147 }
6148
6149 #[test]
6150 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6151 fn execveat_relative_path_executes() {
6152 // Résolution relative : dirfd sur /usr/bin, chemin « true ». Couvre le
6153 // bras `DirFd::Fd` de `dirfd_to_raw`. Ouverture du dirfd et tableaux
6154 // argv/envp côté PARENT ; l'enfant ne fait qu'un `execveat_prepared`
6155 // (alloc-free) — cf. la règle async-signal-safe de `fork_run`.
6156 let dir = std::fs::File::open("/usr/bin").expect("ouverture de /usr/bin");
6157 let fd = sfd(&dir);
6158 let argv: [&CStr; 1] = [c"true"];
6159 let argv_array = CStrArray::new(&argv);
6160 let envp: [&CStr; 0] = [];
6161 let envp_array = CStrArray::new(&envp);
6162 let code = fork_run(|| {
6163 let _ = execveat_prepared(
6164 DirFd::Fd(fd),
6165 c"true",
6166 &argv_array,
6167 &envp_array,
6168 ExecveatFlags::empty(),
6169 );
6170 1
6171 });
6172 assert_eq!(code, 0, "execveat(dirfd=/usr/bin, \"true\") doit sortir 0");
6173 }
6174
6175 #[test]
6176 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6177 fn execveat_with_dir_fd_enoent() {
6178 // En process (échec retourné) : couvre le bras `DirFd::Fd` de
6179 // `dirfd_to_raw`. Chemin relatif inexistant sous un dirfd ouvert ⇒
6180 // ENOENT, sans remplacer l'image du process.
6181 let root = std::fs::File::open("/").expect("ouverture de /");
6182 let err = execveat(
6183 DirFd::Fd(sfd(&root)),
6184 c"air-execveat-no-such-relative",
6185 &[c"x"],
6186 &[],
6187 ExecveatFlags::empty(),
6188 )
6189 .unwrap_err();
6190 assert_eq!(err, Errno::ENOENT);
6191 }
6192
6193 #[test]
6194 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6195 fn execveat_prepared_returns_errno_in_process() {
6196 // Couvre le shim de test `execveat_prepared` **en process** : dans les
6197 // enfants forkés, un `execveat` réussi remplace l'image AVANT
6198 // `child_exit`, donc la couverture du shim n'y est jamais flushée. Ici,
6199 // un chemin inexistant fait RETOURNER le shim (ENOENT) — exactement le
6200 // même appel (tableaux pré-construits) que côté enfant, sans remplacer
6201 // l'image. Analogue de la couverture d'`execve_prepared` via `execve`.
6202 let argv: [&CStr; 1] = [c"x"];
6203 let argv_array = CStrArray::new(&argv);
6204 let envp: [&CStr; 0] = [];
6205 let envp_array = CStrArray::new(&envp);
6206 let err = execveat_prepared(
6207 DirFd::Cwd,
6208 c"/nonexistent/air-execveat-prepared-xyz",
6209 &argv_array,
6210 &envp_array,
6211 ExecveatFlags::empty(),
6212 );
6213 assert_eq!(err, Errno::ENOENT);
6214 }
6215
6216 // ── dup3 ──────────────────────────────────────────────────────────────
6217
6218 #[test]
6219 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6220 fn dup3_redirects_onto_target_and_owns_result() {
6221 // Sérialise l'usage d'un fd HAUT non réutilisable (cf. doc du lock).
6222 let _guard = fd_observation_lock();
6223 let target = high_unreusable_fd_target();
6224 let null = std::fs::OpenOptions::new()
6225 .read(true)
6226 .write(true)
6227 .open("/dev/null")
6228 .expect("ouverture de /dev/null");
6229 let dup = dup3(sfd(&null), target, Dup3Flags::CLOEXEC).expect("dup3 doit réussir");
6230 assert_eq!(
6231 dup.as_raw_fd(),
6232 target,
6233 "dup3 installe le nouveau fd au numéro demandé"
6234 );
6235 // `drop(dup)` ferme `target` ; `drop(null)` ferme l'original.
6236 }
6237
6238 #[test]
6239 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6240 fn dup3_same_old_new_is_einval() {
6241 let null = std::fs::File::open("/dev/null").expect("ouverture de /dev/null");
6242 let raw = sraw(&null);
6243 // `dup3` exige `old != new` (différence avec `dup2`) : EINVAL.
6244 let err = dup3(sfd(&null), raw, Dup3Flags::empty()).unwrap_err();
6245 assert_eq!(err, Errno::EINVAL);
6246 }
6247
6248 // ── fchdir / chdir ────────────────────────────────────────────────────
6249
6250 #[test]
6251 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6252 fn fchdir_error_on_non_directory() {
6253 // /dev/null n'est pas un répertoire ⇒ ENOTDIR (cwd inchangé : sûr en
6254 // process). Couvre le corps + le bras d'erreur de `fchdir`.
6255 let null = std::fs::File::open("/dev/null").expect("ouverture de /dev/null");
6256 let err = fchdir(sfd(&null)).unwrap_err();
6257 assert_eq!(err, Errno::ENOTDIR);
6258 }
6259
6260 #[test]
6261 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6262 fn chdir_error_on_missing_path() {
6263 // Chemin inexistant ⇒ ENOENT (cwd inchangé : sûr en process).
6264 let err = chdir(c"/nonexistent/air-chdir-test").unwrap_err();
6265 assert_eq!(err, Errno::ENOENT);
6266 }
6267
6268 #[test]
6269 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6270 fn chroot_root_noop_or_eperm() {
6271 // `chroot("/")` : sur un runner **privilégié** (CI root), la racine est déjà `/`
6272 // ⇒ **no-op inoffensif** (`Ok`) ; sinon `EPERM`. La ligne est exercée dans les
6273 // deux cas. Chemin **succès** = exception PRIVILEGE (`CAP_SYS_CHROOT`).
6274 let result = chroot(c"/");
6275 assert!(
6276 result.is_ok() || result == Err(Errno::EPERM),
6277 "chroot(/) doit rendre Ok (root) ou EPERM (non privilégié) : {result:?}"
6278 );
6279 // Chemin **inexistant** : `ENOENT` (root) ou `EPERM` (non-root) ⇒ toujours `Err`.
6280 assert!(chroot(c"/air/no/such/root-xyz").is_err());
6281 }
6282
6283 #[test]
6284 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6285 fn chdir_changes_cwd_in_forked_child() {
6286 // Mutation du cwd (process-global) ⇒ enfant forké. Vérifie le succès ET
6287 // l'effet réel **sans allocation** (async-signal-safe — cf. `fork_run`) :
6288 // après `chdir("/")`, le cwd (".") doit désigner la même inode (ino +
6289 // dev) que "/". `statx` remplit une structure sur la pile (zéro heap),
6290 // contrairement à `std::fs::read_link` qui alloue un `PathBuf`.
6291 // Combinateurs de `core` (`.ok()`/`.zip()`/`.is_some_and()`) plutôt
6292 // qu'un `match` : la discrimination Ok/Err des deux `statx` et le `&&` de
6293 // la comparaison vivent dans la bibliothèque (hors couverture de ce
6294 // fichier). Seul subsiste le `&&` du `chdir` (sa branche d'échec, comme
6295 // dans la version `read_link` précédente).
6296 let code = fork_run(|| {
6297 let same = chdir(c"/").is_ok()
6298 && statx(DirFd::Cwd, c".", StatxFlags::empty(), StatxMask::INO)
6299 .ok()
6300 .zip(statx(DirFd::Cwd, c"/", StatxFlags::empty(), StatxMask::INO).ok())
6301 .is_some_and(|(cwd, root)| {
6302 (cwd.ino, cwd.dev_major, cwd.dev_minor)
6303 == (root.ino, root.dev_major, root.dev_minor)
6304 });
6305 i32::from(!same)
6306 });
6307 assert_eq!(code, 0, "chdir(/) doit positionner le cwd à /");
6308 }
6309
6310 #[test]
6311 #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
6312 fn fchdir_changes_cwd_in_forked_child() {
6313 // Ouverture du fd sur "/" côté PARENT ; l'enfant ne fait que `fchdir`
6314 // puis vérifie l'effet via `statx` (alloc-free), sans `read_link` qui
6315 // allouerait — cf. la règle async-signal-safe de `fork_run`.
6316 let root = std::fs::File::open("/").expect("ouverture de /");
6317 let fd = sfd(&root);
6318 // Combinateurs de `core` (cf. `chdir_changes_cwd_in_forked_child`) :
6319 // pas de branche `process.rs` au-delà de l'échec de `fchdir`.
6320 let code = fork_run(|| {
6321 let same = fchdir(fd).is_ok()
6322 && statx(DirFd::Cwd, c".", StatxFlags::empty(), StatxMask::INO)
6323 .ok()
6324 .zip(statx(DirFd::Cwd, c"/", StatxFlags::empty(), StatxMask::INO).ok())
6325 .is_some_and(|(cwd, target)| {
6326 (cwd.ino, cwd.dev_major, cwd.dev_minor)
6327 == (target.ino, target.dev_major, target.dev_minor)
6328 });
6329 i32::from(!same)
6330 });
6331 assert_eq!(code, 0, "fchdir(fd /) doit positionner le cwd à /");
6332 }
6333}
6334
6335#[cfg(test)]
6336mod v1_7_runtime_libc_tests {
6337 use super::*;
6338
6339 #[test]
6340 fn getppid_returns_some_positive() {
6341 // Le parent du runner de test est toujours visible dans son namespace.
6342 let ppid = getppid().expect("parent visible dans le PID namespace");
6343 assert!(ppid.as_raw() > 0);
6344 }
6345
6346 #[test]
6347 fn sched_yield_is_callable() {
6348 // Fonction totale : ne panique pas, ne retourne rien.
6349 sched_yield();
6350 }
6351
6352 #[test]
6353 fn umask_roundtrip_restores_previous() {
6354 // umask rend l'ANCIEN masque. On pose 0o022, on lit (et restaure) :
6355 let previous = umask(0o022);
6356 let seen = umask(previous); // rend 0o022 (ce qu'on venait de poser) + restaure
6357 assert_eq!(seen, 0o022);
6358 // Filet : on remet bien l'état initial.
6359 let _ = umask(previous);
6360 }
6361
6362 #[test]
6363 fn getcwd_matches_std_current_dir() {
6364 let mut buf = [0_u8; 4096];
6365 let cwd = getcwd(&mut buf).expect("getcwd dans un buffer large");
6366 let expected = std::env::current_dir().expect("cwd std");
6367 assert_eq!(cwd, expected.as_os_str().as_encoded_bytes());
6368 }
6369
6370 #[test]
6371 fn getcwd_buffer_too_small_is_erange() {
6372 let mut tiny = [0_u8; 1];
6373 // ERANGE == 34 (Linux) ; l'enum Errno partiel ne l'expose pas forcément
6374 // en constante nommée → on le construit explicitement.
6375 let erange = Errno::from_nonzero(NonZeroI32::new(34).expect("34 != 0"));
6376 assert_eq!(getcwd(&mut tiny), Err(erange));
6377 }
6378
6379 #[test]
6380 fn getrusage_self_succeeds_and_fills() {
6381 let usage = getrusage(RusageWho::SelfProcess).expect("getrusage(SELF)");
6382 // Un processus en cours a forcément déclenché des fautes de page mineures.
6383 assert!(usage.ru_minflt >= 0);
6384 }
6385
6386 #[test]
6387 fn getrusage_children_and_thread_variants() {
6388 // Couvre les deux autres bras de l'enum `who`.
6389 let _ = getrusage(RusageWho::Children).expect("getrusage(CHILDREN)");
6390 let _ = getrusage(RusageWho::Thread).expect("getrusage(THREAD)");
6391 }
6392
6393 /// Emplacements partagés `'static` (VM commune via `CLONE_VM`) que l'enfant
6394 /// du test `None` de [`set_tid_address`] publie : le TID rendu par le syscall
6395 /// et un drapeau de fin. `None` ne fournit **aucun** mot kernel de fin de
6396 /// thread (pas de `clear_child_tid` ré-mis à 0) — l'enfant signale donc
6397 /// lui-même sa progression au parent via ces atomiques.
6398 struct SetTidNoneSlots {
6399 /// TID rendu par `set_tid_address(None)` (l'appelant = le thread enfant).
6400 tid: core::sync::atomic::AtomicI32,
6401 /// `0` tant que l'enfant n'a pas publié `tid` ; `1` une fois écrit.
6402 done: core::sync::atomic::AtomicU32,
6403 }
6404
6405 /// Point d'entrée du thread de test pour le chemin `None` de
6406 /// [`set_tid_address`] : interprète `arg` comme l'adresse d'un
6407 /// [`SetTidNoneSlots`] `'static` partagé, appelle `set_tid_address(None)`
6408 /// (qui **désenregistre** `clear_child_tid` et **rend** le TID appelant),
6409 /// publie ce TID, lève le drapeau `done`, réveille le parent, puis
6410 /// **retourne** — le trampoline [`clone_thread`] termine alors le thread
6411 /// (`SYS_exit`).
6412 ///
6413 /// **Discipline côté enfant** : async-signal-safe, **zéro glibc** — deux
6414 /// syscalls seulement (`set_tid_address`, `futex_wake`), aucun accès TLS,
6415 /// aucune allocation.
6416 extern "C" fn set_tid_address_none_entry(arg: usize) {
6417 use air_sys_types::{FutexScope, FutexWakeCount};
6418 use core::sync::atomic::Ordering;
6419 // SAFETY (test) : `arg` est l'adresse d'un `SetTidNoneSlots` `'static`
6420 // (`Box::leak`, jamais libéré) fourni par le parent et partagé via la VM
6421 // commune. Alignement et validité garantis par le site d'appel.
6422 let slots = unsafe { &*(arg as *const SetTidNoneSlots) };
6423 // SAFETY: thread possédé par Air (jamais réapé par glibc, contrairement à
6424 // un `std::thread` détaché) ; `set_tid_address(None)` désenregistre
6425 // `clear_child_tid` — sans effet ici car aucun `CHILD_CLEARTID` n'a été
6426 // enregistré pour ce thread (pas de mot de join kernel concurrent à
6427 // écraser). Le syscall est total : il rend le TID de l'appelant.
6428 let tid = unsafe { set_tid_address(None) };
6429 slots.tid.store(tid.as_raw(), Ordering::SeqCst);
6430 // `Release` : apparie le `Acquire` du parent — `tid` est visible avant
6431 // que `done` ne passe à 1.
6432 slots.done.store(1, Ordering::Release);
6433 // Réveille le parent éventuellement endormi dans `futex_wait` sur `done`
6434 // (le timeout du parent le ferait converger même sans ce réveil).
6435 let _ = crate::futex::futex_wake(&slots.done, FutexWakeCount::ALL, FutexScope::Shared);
6436 // Retour → trampoline `clone_thread` → `SYS_exit`.
6437 }
6438
6439 #[test]
6440 #[cfg_attr(miri, ignore = "clone3/futex non supportés par Miri")]
6441 fn set_tid_address_none_returns_caller_tid() {
6442 use crate::futex::futex_wait;
6443 use crate::mem::mmap_anonymous;
6444 use air_sys_types::FutexScope;
6445 use air_sys_types::mem::{MapFlags, ProtectionFlags};
6446 use core::sync::atomic::{AtomicI32, AtomicU32, Ordering};
6447 use core::time::Duration;
6448
6449 // Test SOUND du chemin `None` : on prouve que `set_tid_address(None)`
6450 // rend le TID de l'appelant **sur un thread possédé par Air**
6451 // (`clone_thread`, couche 0), **sans pthread glibc**. L'ancienne version
6452 // spawnait un `std::thread` détaché et y appelait `set_tid_address(None)`,
6453 // ce qui mettait à NULL le `clear_child_tid` que glibc utilise pour réaper
6454 // ses threads détachés — détournant le teardown glibc et pendant par
6455 // intermittence sous charge ARM (cohabitation Air/glibc unsound, même
6456 // classe de bug que le frère `_some`). Ici aucun `CHILD_CLEARTID` n'est
6457 // enregistré (pas de conflit), et l'attente parent est **bornée** par une
6458 // deadline (échec rapide et diagnostique, jamais un hang de 30 min).
6459
6460 // `None` ne fournit AUCUN mot kernel de fin de thread : l'enfant publie
6461 // lui-même `tid` + `done` dans des emplacements `'static` partagés
6462 // (`Box::leak`, jamais libérés ⇒ le kernel/enfant peut y écrire sans
6463 // risque d'UAF), et le parent attend `done` via un `futex_wait` borné.
6464 let slots: &'static SetTidNoneSlots = Box::leak(Box::new(SetTidNoneSlots {
6465 tid: AtomicI32::new(0),
6466 done: AtomicU32::new(0),
6467 }));
6468 let slots_ptr = core::ptr::from_ref(slots) as usize;
6469
6470 // Pile dédiée du thread : 64 KiB, page-alignée (mmap) ⇒ sommet aligné 16.
6471 const STACK_SIZE: usize = 64 * 1024;
6472 let mut stack = mmap_anonymous(
6473 STACK_SIZE,
6474 ProtectionFlags::READ | ProtectionFlags::WRITE,
6475 MapFlags::PRIVATE | MapFlags::STACK,
6476 )
6477 .expect("mmap pile thread");
6478 let stack_ptr = stack.as_mut_ptr();
6479
6480 // PAS de `CHILD_CLEARTID`/`PARENT_SETTID` : on n'enregistre aucun mot de
6481 // join kernel (il entrerait en conflit avec le `set_tid_address(None)` du
6482 // child).
6483 let flags = CloneFlags::VM
6484 | CloneFlags::FS
6485 | CloneFlags::FILES
6486 | CloneFlags::SIGHAND
6487 | CloneFlags::THREAD
6488 | CloneFlags::SYSVSEM;
6489
6490 let args = CloneArgs {
6491 flags,
6492 // CLONE_THREAD interdit tout signal de sortie.
6493 exit_signal: None,
6494 stack: Some(StackSpecification {
6495 addr: stack_ptr,
6496 size: STACK_SIZE,
6497 }),
6498 child_tid: None,
6499 parent_tid: None,
6500 tls: None,
6501 };
6502
6503 // SAFETY: pile dédiée valide et **jamais libérée** (voir le `forget`
6504 // ci-dessous) → vivante au-delà de la durée de vie du thread ;
6505 // `set_tid_address_none_entry` n'utilise ni TLS ni alloc et n'opère que
6506 // sur `slots` (`'static`, partagé via la VM commune) ; aucun mot de join
6507 // kernel n'est enregistré.
6508 let child_tid = unsafe { clone_thread(&args, set_tid_address_none_entry, slots_ptr) }
6509 .expect("clone_thread doit réussir");
6510 assert!(child_tid.as_raw() > 0, "TID enfant strictement positif");
6511
6512 // Fuite **délibérée** de la pile : `set_tid_address(None)` ne laisse aucun
6513 // mot kernel pour savoir quand le thread a fini d'utiliser sa pile (le
6514 // trampoline s'exécute encore dessus juste avant `SYS_exit`) → la libérer
6515 // serait un UAF. `forget` neutralise le `Drop` (munmap) de `Mapping` :
6516 // la région reste mappée à vie. Le leak de pile (test-only) est la
6517 // discipline déjà retenue pour ce motif (cf. frère `_some`).
6518 core::mem::forget(stack);
6519
6520 // Attente **bornée** de la publication : `futex_wait` borné à 100 ms ; la
6521 // deadline absolue (50 réveils ⇒ ≤ 5 s) garantit un échec **rapide**
6522 // plutôt qu'un hang. Le child fait `store(done, 1)` + `futex_wake` ; un
6523 // réveil manqué est rattrapé par le re-poll après timeout — la boucle
6524 // converge dans tous les cas.
6525 const MAX_WAITS: u32 = 50; // 50 × 100 ms = 5 s de borne.
6526 let mut waits = 0_u32;
6527 while slots.done.load(Ordering::Acquire) == 0 {
6528 let _ = futex_wait(
6529 &slots.done,
6530 0,
6531 Some(Duration::from_millis(100)),
6532 FutexScope::Shared,
6533 );
6534 waits = waits.saturating_add(1);
6535 assert!(
6536 waits < MAX_WAITS,
6537 "l'enfant doit publier son TID via set_tid_address(None) \
6538 (deadline {MAX_WAITS} × 100 ms expirée — publication non observée)"
6539 );
6540 }
6541
6542 // `set_tid_address(None)` rend le TID de l'appelant (le thread enfant) :
6543 // c'est exactement le TID rendu par `clone_thread` au parent.
6544 let observed = slots.tid.load(Ordering::Acquire);
6545 assert!(
6546 observed > 0,
6547 "set_tid_address(None) rend le TID appelant (> 0)"
6548 );
6549 assert_eq!(
6550 observed,
6551 child_tid.as_raw(),
6552 "le TID rendu par set_tid_address(None) doit être celui du thread enfant"
6553 );
6554 }
6555
6556 /// Point d'entrée du thread de test pour le chemin `Some` de
6557 /// [`set_tid_address`] : interprète `arg` comme l'adresse d'un `AtomicU32`
6558 /// `'static` partagé (VM commune via `CLONE_VM`), y **enregistre**
6559 /// `clear_child_tid`, puis **retourne** — le trampoline [`clone_thread`]
6560 /// termine alors le thread (`SYS_exit`) et le kernel remet le mot à `0` +
6561 /// `FUTEX_WAKE`.
6562 ///
6563 /// **Discipline côté enfant** : async-signal-safe, **zéro glibc** — un seul
6564 /// syscall (`set_tid_address`), aucun accès TLS, aucune allocation.
6565 extern "C" fn set_tid_address_some_entry(arg: usize) {
6566 // SAFETY (test) : `arg` est l'adresse d'un `AtomicU32` `'static`
6567 // (`Box::leak`, jamais libéré) fourni par le parent et partagé via la VM
6568 // commune. Alignement et validité garantis par le site d'appel.
6569 let slot = unsafe { &*(arg as *const core::sync::atomic::AtomicU32) };
6570 // SAFETY: `slot` est `'static` : le kernel pourra y écrire `0` à la mort
6571 // du thread sans risque d'usage-après-libération (contrat de
6572 // `set_tid_address`). Aucun mot de join kernel (`CHILD_CLEARTID`) n'a été
6573 // enregistré pour ce thread → pas de conflit avec ce `clear_child_tid`.
6574 let _ = unsafe { set_tid_address(Some(slot)) };
6575 // Retour → trampoline `clone_thread` → `SYS_exit` → clear + FUTEX_WAKE.
6576 }
6577
6578 #[test]
6579 #[cfg_attr(miri, ignore = "clone3/futex non supportés par Miri")]
6580 fn set_tid_address_some_is_cleared_by_kernel_on_thread_exit() {
6581 use crate::futex::futex_wait;
6582 use crate::mem::{mmap_anonymous, munmap};
6583 use air_sys_types::FutexScope;
6584 use air_sys_types::mem::{MapFlags, ProtectionFlags};
6585 use core::sync::atomic::{AtomicU32, Ordering};
6586 use core::time::Duration;
6587
6588 // Test SOUND du chemin `Some` : on prouve la sémantique kernel
6589 // (`set_tid_address(Some(ptr))` ⇒ le kernel remet `ptr` à 0 +
6590 // `FUTEX_WAKE` à la sortie du thread) **sans pthread glibc**. L'ancienne
6591 // version spawnait un `std::thread` détaché et écrasait son
6592 // `clear_child_tid` — celui que glibc utilise pour réaper ses threads
6593 // détachés —, ce qui détournait le teardown glibc et pendait par
6594 // intermittence sous charge ARM (cohabitation Air/glibc unsound). Ici le
6595 // thread est créé par `clone_thread` (couche 0), **sans** `CHILD_CLEARTID`
6596 // (aucun mot de join kernel concurrent), et l'attente parent est **bornée**
6597 // par une deadline (échec rapide et diagnostique, jamais un hang).
6598
6599 // Pile dédiée du thread : 64 KiB, page-alignée (mmap) ⇒ sommet aligné 16.
6600 const STACK_SIZE: usize = 64 * 1024;
6601 let mut stack = mmap_anonymous(
6602 STACK_SIZE,
6603 ProtectionFlags::READ | ProtectionFlags::WRITE,
6604 MapFlags::PRIVATE | MapFlags::STACK,
6605 )
6606 .expect("mmap pile thread");
6607
6608 // `slot` à adresse **stable** et durée de vie `'static` (`Box::leak`) :
6609 // l'enfant y enregistre `clear_child_tid` et le kernel y écrit `0` à la
6610 // mort du thread. Le leak (test-only) garantit qu'aucun chemin — y compris
6611 // une deadline expirée qui dépile par `panic!` — ne libère la mémoire
6612 // pendant que le kernel peut encore y écrire (pas d'UAF).
6613 let slot: &'static AtomicU32 = Box::leak(Box::new(AtomicU32::new(0xFFFF_FFFF)));
6614 let slot_ptr = core::ptr::from_ref(slot) as usize;
6615
6616 // PAS de `CHILD_CLEARTID`/`PARENT_SETTID` : on n'enregistre aucun mot de
6617 // join kernel (il entrerait en conflit avec le `set_tid_address` du child).
6618 let flags = CloneFlags::VM
6619 | CloneFlags::FS
6620 | CloneFlags::FILES
6621 | CloneFlags::SIGHAND
6622 | CloneFlags::THREAD
6623 | CloneFlags::SYSVSEM;
6624
6625 let args = CloneArgs {
6626 flags,
6627 // CLONE_THREAD interdit tout signal de sortie.
6628 exit_signal: None,
6629 stack: Some(StackSpecification {
6630 addr: stack.as_mut_ptr(),
6631 size: STACK_SIZE,
6632 }),
6633 child_tid: None,
6634 parent_tid: None,
6635 tls: None,
6636 };
6637
6638 // SAFETY: pile dédiée valide (libérée seulement après la mort du thread) ;
6639 // `set_tid_address_some_entry` n'utilise ni TLS ni alloc et n'opère que sur
6640 // `slot` (`'static`, partagé via la VM commune) ; aucun mot de join kernel
6641 // n'est enregistré.
6642 let tid = unsafe { clone_thread(&args, set_tid_address_some_entry, slot_ptr) }
6643 .expect("clone_thread doit réussir");
6644 assert!(tid.as_raw() > 0, "TID enfant strictement positif");
6645
6646 // Attente **bornée** : le kernel remet `slot` à 0 (+ `FUTEX_WAKE`) à la
6647 // mort du thread. `futex_wait` est borné par 100 ms ; la deadline absolue
6648 // (50 réveils ⇒ ≤ 5 s) garantit un échec **rapide** plutôt qu'un hang de
6649 // 30 min. Si la portée du futex différait, le timeout fait re-poller le
6650 // mot — le kernel écrit toujours 0, donc la boucle converge.
6651 const MAX_WAITS: u32 = 50; // 50 × 100 ms = 5 s de borne.
6652 let mut waits = 0_u32;
6653 loop {
6654 let v = slot.load(Ordering::Acquire);
6655 if v == 0 {
6656 break;
6657 }
6658 let _ = futex_wait(
6659 slot,
6660 v,
6661 Some(Duration::from_millis(100)),
6662 FutexScope::Shared,
6663 );
6664 waits = waits.saturating_add(1);
6665 assert!(
6666 waits < MAX_WAITS,
6667 "clear_child_tid doit être remis à 0 à la sortie du thread \
6668 (deadline {MAX_WAITS} × 100 ms expirée — sémantique kernel \
6669 `set_tid_address(Some)` non observée)"
6670 );
6671 }
6672
6673 // Le mot a été remis à 0 ⇒ le thread est mort : la pile n'est plus
6674 // utilisée, on peut la libérer sans UAF.
6675 munmap(stack).expect("munmap pile thread");
6676 }
6677}