Skip to main content

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_CAPBSET_READ: i32 = 23;
1910const PR_CAPBSET_DROP: i32 = 24;
1911const PR_SET_TIMERSLACK: i32 = 29;
1912const PR_SET_NO_NEW_PRIVS: i32 = 38;
1913const PR_GET_NO_NEW_PRIVS: i32 = 39;
1914const PR_CAP_AMBIENT: i32 = 47;
1915
1916// Sous-opérations de PR_CAP_AMBIENT (arg2).
1917const PR_CAP_AMBIENT_IS_SET: u64 = 1;
1918const PR_CAP_AMBIENT_RAISE: u64 = 2;
1919const PR_CAP_AMBIENT_LOWER: u64 = 3;
1920const PR_CAP_AMBIENT_CLEAR_ALL: u64 = 4;
1921
1922/// Définit le signal envoyé au processus courant à la mort de son parent.
1923///
1924/// `None` désactive la notification (le kernel envoie 0, traité comme
1925/// « aucun signal »).
1926///
1927/// # Errors
1928///
1929/// - [`Errno::EINVAL`] : signal hors plage.
1930pub fn set_parent_death_signal(signal: Option<Signal>) -> Result<(), Errno> {
1931    let sig_arg: u64 = signal.map_or(0_u64, |s| {
1932        // s.as_raw() > 0 par construction (NonZeroI32). Cast sign-safe.
1933        #[allow(clippy::cast_sign_loss)]
1934        {
1935            s.as_raw() as u64
1936        }
1937    });
1938    // SAFETY: PR_SET_PDEATHSIG — pas d'accès mémoire utilisateur.
1939    let ret = unsafe { raw_syscall_prctl(PR_SET_PDEATHSIG, sig_arg, 0, 0, 0) };
1940    if ret < 0 {
1941        return Err(errno_from_negative_syscall_ret(ret));
1942    }
1943    Ok(())
1944}
1945
1946/// Retourne le signal envoyé au processus à la mort du parent.
1947/// `Ok(None)` si la notification est désactivée.
1948///
1949/// # Errors
1950///
1951/// - [`Errno::EINVAL`] : ne se produit pas pour PR_GET_PDEATHSIG en
1952///   pratique.
1953pub fn get_parent_death_signal() -> Result<Option<Signal>, Errno> {
1954    // Le kernel écrit le signal dans `*(int *)arg2`.
1955    let mut out: i32 = 0;
1956    let out_ptr: *mut i32 = &mut out;
1957    // SAFETY: PR_GET_PDEATHSIG écrit un int à arg2 ; `out` est local
1958    // valide pour toute la durée du syscall.
1959    let ret = unsafe { raw_syscall_prctl(PR_GET_PDEATHSIG, out_ptr as u64, 0, 0, 0) };
1960    if ret < 0 {
1961        return Err(errno_from_negative_syscall_ret(ret));
1962    }
1963    Ok(Signal::try_from_raw(out))
1964}
1965
1966/// Active le bit `no_new_privs` du processus.
1967///
1968/// **Opération irréversible.** Une fois positionné, ce bit ne peut pas
1969/// être effacé pour la durée de vie du processus — toute tentative
1970/// (`set_no_new_privs` après coup, ou via execve d'un binaire SUID) est
1971/// neutralisée par le kernel. Conséquence pour les tests : exécuter
1972/// `set_no_new_privs()` dans un test contamine **tous les tests
1973/// ultérieurs** dans la même process. Cette PR ne teste donc que
1974/// `get_no_new_privs()` ; le set est laissé à la PR `family-security`
1975/// (seccomp) qui s'exécute dans un processus enfant fork'é.
1976///
1977/// # Errors
1978///
1979/// - [`Errno::EINVAL`] : ne se produit pas en pratique.
1980pub fn set_no_new_privs() -> Result<(), Errno> {
1981    // arg2 doit être 1 selon le contrat kernel ; toute autre valeur
1982    // retourne EINVAL.
1983    // SAFETY: PR_SET_NO_NEW_PRIVS — pas d'accès mémoire utilisateur.
1984    let ret = unsafe { raw_syscall_prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
1985    if ret < 0 {
1986        return Err(errno_from_negative_syscall_ret(ret));
1987    }
1988    Ok(())
1989}
1990
1991/// Indique si le bit `no_new_privs` est positionné pour le processus.
1992///
1993/// # Errors
1994///
1995/// - Aucune en pratique pour cette opération.
1996pub fn get_no_new_privs() -> Result<bool, Errno> {
1997    // SAFETY: PR_GET_NO_NEW_PRIVS — pas d'accès mémoire utilisateur ; la
1998    // valeur est retournée comme code de retour du syscall (0 ou 1).
1999    let ret = unsafe { raw_syscall_prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) };
2000    if ret < 0 {
2001        return Err(errno_from_negative_syscall_ret(ret));
2002    }
2003    Ok(ret != 0)
2004}
2005
2006/// Définit le nom du thread (max 15 caractères + NUL).
2007///
2008/// Le kernel tronque silencieusement à 15 caractères si `name` est plus
2009/// long. Le nom est utilisé par `/proc/PID/task/TID/comm` et exposé par
2010/// `top`/`ps -L`.
2011///
2012/// # Errors
2013///
2014/// - `EFAULT` : pointeur invalide ; ne se produit pas via l'API safe.
2015pub fn set_thread_name(name: &CStr) -> Result<(), Errno> {
2016    let ptr = name.as_ptr();
2017    // SAFETY: PR_SET_NAME lit la C-string à arg2 (kernel tronque à 16
2018    // octets). `name` est un &CStr valide pour la durée du call.
2019    let ret = unsafe { raw_syscall_prctl(PR_SET_NAME, ptr as u64, 0, 0, 0) };
2020    if ret < 0 {
2021        return Err(errno_from_negative_syscall_ret(ret));
2022    }
2023    Ok(())
2024}
2025
2026/// Retourne le nom du thread courant.
2027///
2028/// # Allocation
2029///
2030/// Convention 4 ADR-021 exception explicite : « la sémantique exige un
2031/// type owned ». Le kernel écrit ≤ 16 octets dans un buffer transient ;
2032/// le caller récupère une `CString` qui peut sortir du frame. Allocation
2033/// bornée à 16 octets (un seul `Box<[u8]>`), happy-path.
2034///
2035/// # Errors
2036///
2037/// - Aucune en pratique pour cette opération.
2038pub fn get_thread_name() -> Result<CString, Errno> {
2039    let mut buffer: [u8; 16] = [0; 16];
2040    let ptr: *mut u8 = buffer.as_mut_ptr();
2041    // SAFETY: PR_GET_NAME écrit exactement 16 octets dans `*(char *)arg2` ;
2042    // `buffer` est local valide pour la durée du call. Le kernel garantit la
2043    // terminaison NUL (en tronquant si nécessaire).
2044    let ret = unsafe { raw_syscall_prctl(PR_GET_NAME, ptr as u64, 0, 0, 0) };
2045    if ret < 0 {
2046        return Err(errno_from_negative_syscall_ret(ret));
2047    }
2048    // Le buffer est garanti contenir au moins un NUL (le kernel tronque
2049    // à 15 caractères + NUL). On extrait la C-string jusqu'au premier NUL.
2050    let length = buffer.iter().position(|&b| b == 0).unwrap_or(16);
2051    // CString::new exige absence de NUL interne ; on passe les bytes
2052    // *avant* le NUL trouvé.
2053    Ok(CString::new(&buffer[..length])
2054        .expect("buffer kernel sans NUL interne avant le terminateur"))
2055}
2056
2057/// Définit le mode `dumpable` du processus.
2058///
2059/// # Errors
2060///
2061/// - [`Errno::EINVAL`] : valeur hors plage (ne se produit pas via l'API
2062///   typée).
2063pub fn set_dumpable(mode: DumpableMode) -> Result<(), Errno> {
2064    let mode_arg = u64::from(mode as u32);
2065    // SAFETY: PR_SET_DUMPABLE — pas d'accès mémoire utilisateur.
2066    let ret = unsafe { raw_syscall_prctl(PR_SET_DUMPABLE, mode_arg, 0, 0, 0) };
2067    if ret < 0 {
2068        return Err(errno_from_negative_syscall_ret(ret));
2069    }
2070    Ok(())
2071}
2072
2073/// Retourne le mode `dumpable` actuel.
2074///
2075/// # Errors
2076///
2077/// - Aucune en pratique pour cette opération.
2078pub fn get_dumpable() -> Result<DumpableMode, Errno> {
2079    // SAFETY: PR_GET_DUMPABLE — pas d'accès mémoire utilisateur ;
2080    // valeur retournée comme code de retour.
2081    let ret = unsafe { raw_syscall_prctl(PR_GET_DUMPABLE, 0, 0, 0, 0) };
2082    if ret < 0 {
2083        return Err(errno_from_negative_syscall_ret(ret));
2084    }
2085    match ret {
2086        0 => Ok(DumpableMode::NotDumpable),
2087        1 => Ok(DumpableMode::Dumpable),
2088        2 => Ok(DumpableMode::SuidDumpable),
2089        // Le kernel garantit 0, 1, ou 2. Sortie hors plage = bug kernel.
2090        other => unreachable!("PR_GET_DUMPABLE a retourné {other}, hors plage [0,2]"),
2091    }
2092}
2093
2094/// Définit le bit `keep_caps` : si vrai, les capabilities `permitted`
2095/// sont conservées lors d'un changement d'UID effectif vers non-zéro.
2096///
2097/// # Errors
2098///
2099/// - [`Errno::EINVAL`] : valeur invalide (ne se produit pas via l'API typée).
2100pub fn set_keep_caps(keep: bool) -> Result<(), Errno> {
2101    let arg: u64 = u64::from(keep);
2102    // SAFETY: PR_SET_KEEPCAPS — pas d'accès mémoire utilisateur.
2103    let ret = unsafe { raw_syscall_prctl(PR_SET_KEEPCAPS, arg, 0, 0, 0) };
2104    if ret < 0 {
2105        return Err(errno_from_negative_syscall_ret(ret));
2106    }
2107    Ok(())
2108}
2109
2110/// Indique si le bit `keep_caps` est positionné.
2111///
2112/// # Errors
2113///
2114/// - Aucune en pratique pour cette opération.
2115pub fn get_keep_caps() -> Result<bool, Errno> {
2116    // SAFETY: PR_GET_KEEPCAPS — pas d'accès mémoire utilisateur.
2117    let ret = unsafe { raw_syscall_prctl(PR_GET_KEEPCAPS, 0, 0, 0, 0) };
2118    if ret < 0 {
2119        return Err(errno_from_negative_syscall_ret(ret));
2120    }
2121    Ok(ret != 0)
2122}
2123
2124/// Définit la « timer slack » du processus (nanosecondes).
2125///
2126/// Le timer slack permet au kernel d'agréger les timers proches pour
2127/// réduire les wake-ups. `0` réinitialise à la valeur par défaut héritée.
2128///
2129/// # Errors
2130///
2131/// - Aucune en pratique.
2132pub fn set_timer_slack(slack_ns: u64) -> Result<(), Errno> {
2133    // SAFETY: PR_SET_TIMERSLACK — pas d'accès mémoire utilisateur.
2134    let ret = unsafe { raw_syscall_prctl(PR_SET_TIMERSLACK, slack_ns, 0, 0, 0) };
2135    if ret < 0 {
2136        return Err(errno_from_negative_syscall_ret(ret));
2137    }
2138    Ok(())
2139}
2140
2141/// Ajoute `cap` à l'ensemble ambient du thread.
2142///
2143/// L'ensemble ambient est transmis à `execve`, contrairement aux
2144/// inheritable seuls. Exige `cap` dans `permitted ∩ inheritable`.
2145///
2146/// # Errors
2147///
2148/// - [`Errno::EPERM`] : `cap` n'est pas dans permitted ∩ inheritable, ou
2149///   `SECBIT_NO_CAP_AMBIENT_RAISE` est positionné.
2150/// - [`Errno::EINVAL`] : valeur de capability inconnue côté kernel.
2151pub fn cap_ambient_raise(cap: Capability) -> Result<(), Errno> {
2152    // SAFETY: PR_CAP_AMBIENT/RAISE — pas d'accès mémoire utilisateur.
2153    let ret = unsafe {
2154        raw_syscall_prctl(
2155            PR_CAP_AMBIENT,
2156            PR_CAP_AMBIENT_RAISE,
2157            u64::from(cap.as_raw()),
2158            0,
2159            0,
2160        )
2161    };
2162    if ret < 0 {
2163        return Err(errno_from_negative_syscall_ret(ret));
2164    }
2165    Ok(())
2166}
2167
2168/// Retire `cap` de l'ensemble ambient.
2169///
2170/// # Errors
2171///
2172/// - [`Errno::EINVAL`] : valeur de capability inconnue côté kernel.
2173pub fn cap_ambient_lower(cap: Capability) -> Result<(), Errno> {
2174    // SAFETY: PR_CAP_AMBIENT/LOWER — pas d'accès mémoire utilisateur.
2175    let ret = unsafe {
2176        raw_syscall_prctl(
2177            PR_CAP_AMBIENT,
2178            PR_CAP_AMBIENT_LOWER,
2179            u64::from(cap.as_raw()),
2180            0,
2181            0,
2182        )
2183    };
2184    if ret < 0 {
2185        return Err(errno_from_negative_syscall_ret(ret));
2186    }
2187    Ok(())
2188}
2189
2190/// Vide totalement l'ensemble ambient du thread.
2191///
2192/// # Errors
2193///
2194/// - Aucune en pratique.
2195pub fn cap_ambient_clear_all() -> Result<(), Errno> {
2196    // SAFETY: PR_CAP_AMBIENT/CLEAR_ALL — pas d'accès mémoire utilisateur.
2197    let ret = unsafe { raw_syscall_prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) };
2198    if ret < 0 {
2199        return Err(errno_from_negative_syscall_ret(ret));
2200    }
2201    Ok(())
2202}
2203
2204/// Indique si `cap` est dans l'ensemble ambient.
2205///
2206/// # Errors
2207///
2208/// - [`Errno::EINVAL`] : valeur de capability inconnue côté kernel.
2209pub fn cap_ambient_is_set(cap: Capability) -> Result<bool, Errno> {
2210    // SAFETY: PR_CAP_AMBIENT/IS_SET — pas d'accès mémoire utilisateur ;
2211    // valeur retournée comme code de retour (0 ou 1).
2212    let ret = unsafe {
2213        raw_syscall_prctl(
2214            PR_CAP_AMBIENT,
2215            PR_CAP_AMBIENT_IS_SET,
2216            u64::from(cap.as_raw()),
2217            0,
2218            0,
2219        )
2220    };
2221    if ret < 0 {
2222        return Err(errno_from_negative_syscall_ret(ret));
2223    }
2224    Ok(ret != 0)
2225}
2226
2227/// Teste si `cap` est présente dans le **bounding set** du processus
2228/// (`prctl(PR_CAPBSET_READ)`). Non destructif.
2229///
2230/// # Errors
2231///
2232/// - [`Errno::EINVAL`] : `cap` n'est pas une capability reconnue du kernel.
2233pub fn capbset_read(cap: Capability) -> Result<bool, Errno> {
2234    // SAFETY: PR_CAPBSET_READ — pas d'accès mémoire utilisateur ; résultat (0/1)
2235    // rendu comme code de retour.
2236    let ret = unsafe { raw_syscall_prctl(PR_CAPBSET_READ, u64::from(cap.as_raw()), 0, 0, 0) };
2237    if ret < 0 {
2238        return Err(errno_from_negative_syscall_ret(ret));
2239    }
2240    Ok(ret != 0)
2241}
2242
2243/// Retire `cap` du **bounding set** du processus (`prctl(PR_CAPBSET_DROP)`).
2244///
2245/// **Irréversible** : une capability retirée du bounding set ne peut plus jamais être
2246/// ajoutée à l'ensemble *permitted* d'un descendant, **même** par un processus root.
2247/// Durcissement de moindre autorité du monitor (ADR-122 §6) : après avoir réduit ses
2248/// ensembles à la cible, le monitor largue du bounding set tout ce qu'il n'utilisera
2249/// jamais, se privant définitivement de la capacité de le regagner.
2250///
2251/// # Errors
2252///
2253/// - [`Errno::EPERM`] : l'appelant n'a pas `CAP_SETPCAP`.
2254/// - [`Errno::EINVAL`] : `cap` n'est pas une capability reconnue du kernel.
2255pub fn capbset_drop(cap: Capability) -> Result<(), Errno> {
2256    // SAFETY: PR_CAPBSET_DROP — pas d'accès mémoire utilisateur ; arg2 = numéro de cap.
2257    let ret = unsafe { raw_syscall_prctl(PR_CAPBSET_DROP, u64::from(cap.as_raw()), 0, 0, 0) };
2258    if ret < 0 {
2259        return Err(errno_from_negative_syscall_ret(ret));
2260    }
2261    Ok(())
2262}
2263
2264#[cfg(target_arch = "x86_64")]
2265#[inline]
2266unsafe fn raw_syscall_prctl(op: i32, arg2: u64, arg3: u64, arg4: u64, arg5: u64) -> i64 {
2267    let ret: i64;
2268    // SAFETY: SYS_prctl (x86_64 = 157). Selon `op`, le kernel peut lire
2269    // ou écrire à des pointeurs passés via arg2..arg5 ; les fonctions
2270    // d'opération individuelles (au-dessus) garantissent la validité de
2271    // ces pointeurs lorsqu'elles en passent. Sans pointeur, pas
2272    // d'accès mémoire utilisateur.
2273    //
2274    // **Pas de `readonly`** : certaines opérations (PR_GET_NAME,
2275    // PR_GET_PDEATHSIG) font écrire le kernel à un pointeur user.
2276    unsafe {
2277        core::arch::asm!(
2278            "syscall",
2279            in("rax") 157_i64,
2280            in("rdi") i64::from(op),
2281            in("rsi") arg2,
2282            in("rdx") arg3,
2283            in("r10") arg4,
2284            in("r8") arg5,
2285            lateout("rax") ret,
2286            lateout("rcx") _,
2287            lateout("r11") _,
2288            options(nostack, preserves_flags),
2289        );
2290    }
2291    ret
2292}
2293
2294#[cfg(target_arch = "aarch64")]
2295#[inline]
2296unsafe fn raw_syscall_prctl(op: i32, arg2: u64, arg3: u64, arg4: u64, arg5: u64) -> i64 {
2297    let ret: i64;
2298    // SAFETY: SYS_prctl (aarch64 = 167). Mêmes considérations que x86_64.
2299    unsafe {
2300        core::arch::asm!(
2301            "svc 0",
2302            in("x8") 167_i64,
2303            inout("x0") i64::from(op) => ret,
2304            in("x1") arg2,
2305            in("x2") arg3,
2306            in("x3") arg4,
2307            in("x4") arg5,
2308            options(nostack, preserves_flags),
2309        );
2310    }
2311    ret
2312}
2313
2314// ─────────────────────────────────────────────────────────────────────────
2315// rlimits (sous-section 6 de family-process.md).
2316//
2317// `prlimit64` est le syscall préféré ; `getrlimit`/`setrlimit` legacy
2318// sont exposés mais routés au plus simple. La représentation kernel de
2319// `struct rlimit64` est { u64 rlim_cur ; u64 rlim_max }, identique à
2320// `struct rlimit` sur LP64. On la traduit en `Rlimit` via mapping
2321// `RLIM_INFINITY <-> RlimitValue::Infinity`.
2322// ─────────────────────────────────────────────────────────────────────────
2323
2324#[repr(C)]
2325#[derive(Default, Clone, Copy)]
2326struct KernelRlimit64 {
2327    rlim_cur: u64,
2328    rlim_max: u64,
2329}
2330
2331const RLIM_INFINITY: u64 = u64::MAX;
2332
2333fn rlim_value_to_kernel(v: RlimitValue) -> u64 {
2334    match v {
2335        RlimitValue::Finite(n) => n,
2336        RlimitValue::Infinity => RLIM_INFINITY,
2337    }
2338}
2339
2340fn rlim_value_from_kernel(n: u64) -> RlimitValue {
2341    if n == RLIM_INFINITY {
2342        RlimitValue::Infinity
2343    } else {
2344        RlimitValue::Finite(n)
2345    }
2346}
2347
2348fn rlimit_to_kernel(r: Rlimit) -> KernelRlimit64 {
2349    KernelRlimit64 {
2350        rlim_cur: rlim_value_to_kernel(r.soft),
2351        rlim_max: rlim_value_to_kernel(r.hard),
2352    }
2353}
2354
2355fn rlimit_from_kernel(k: KernelRlimit64) -> Rlimit {
2356    Rlimit {
2357        soft: rlim_value_from_kernel(k.rlim_cur),
2358        hard: rlim_value_from_kernel(k.rlim_max),
2359    }
2360}
2361
2362/// Retourne la `Rlimit` courante du processus pour `resource`.
2363///
2364/// Air recommande [`prlimit`] qui est plus expressif (cible un autre
2365/// processus, opération atomique) ; `getrlimit` est exposé pour
2366/// compatibilité.
2367///
2368/// # Errors
2369///
2370/// - [`Errno::EINVAL`] : ressource inconnue (impossible via l'API typée).
2371pub fn getrlimit(resource: Resource) -> Result<Rlimit, Errno> {
2372    // Délègue à prlimit(self, resource, None) pour ne pas dupliquer
2373    // l'asm! et bénéficier de la même conversion.
2374    prlimit(None, resource, None)
2375}
2376
2377/// Définit la `Rlimit` courante du processus pour `resource`.
2378///
2379/// # Errors
2380///
2381/// - [`Errno::EINVAL`] : valeurs incohérentes (soft > hard, …).
2382/// - [`Errno::EPERM`] : tentative de relever la hard limit sans
2383///   `CAP_SYS_RESOURCE`.
2384pub fn setrlimit(resource: Resource, limit: Rlimit) -> Result<(), Errno> {
2385    // Délègue à prlimit(self, resource, Some(limit)) ; on ignore l'ancienne
2386    // valeur retournée.
2387    let _ = prlimit(None, resource, Some(limit))?;
2388    Ok(())
2389}
2390
2391/// `prlimit64` : lit et/ou modifie atomiquement la `Rlimit` d'un
2392/// processus.
2393///
2394/// `pid = None` désigne le processus courant. Retourne **toujours** la
2395/// limite *avant* modification (équivalent kernel "old_limit").
2396///
2397/// # Errors
2398///
2399/// - [`Errno::EINVAL`] : valeurs incohérentes.
2400/// - [`Errno::EPERM`] : tentative de relever la hard limit sans
2401///   `CAP_SYS_RESOURCE`, ou ciblage d'un autre processus sans droits.
2402/// - [`Errno::ESRCH`] : `pid` n'existe pas.
2403pub fn prlimit(
2404    pid: Option<Pid>,
2405    resource: Resource,
2406    new_limit: Option<Rlimit>,
2407) -> Result<Rlimit, Errno> {
2408    let pid_arg = pid.map_or(0_i32, |p| p.as_raw());
2409
2410    let new_buf: Option<KernelRlimit64> = new_limit.map(rlimit_to_kernel);
2411    let new_ptr: u64 = match &new_buf {
2412        Some(b) => {
2413            let p: *const KernelRlimit64 = b;
2414            p as u64
2415        }
2416        None => 0,
2417    };
2418
2419    let mut old_buf = KernelRlimit64::default();
2420    let old_ptr: *mut KernelRlimit64 = &mut old_buf;
2421
2422    // SAFETY: prlimit64(2) lit `new_limit` si non-null, écrit `old_limit`
2423    // si non-null. Nous fournissons toujours `old_ptr` valide ; `new_ptr`
2424    // est null ou pointe sur `new_buf` valide pour la durée du call.
2425    let ret = unsafe { raw_syscall_prlimit64(pid_arg, resource as u32, new_ptr, old_ptr as u64) };
2426    if ret < 0 {
2427        return Err(errno_from_negative_syscall_ret(ret));
2428    }
2429    Ok(rlimit_from_kernel(old_buf))
2430}
2431
2432#[cfg(target_arch = "x86_64")]
2433#[inline]
2434unsafe fn raw_syscall_prlimit64(pid: i32, resource: u32, new_limit: u64, old_limit: u64) -> i64 {
2435    let ret: i64;
2436    // SAFETY: SYS_prlimit64 (x86_64 = 302). Le kernel lit `new_limit`
2437    // (16 octets) si non-null ; écrit `old_limit` (16 octets) si non-null.
2438    // Pas de `readonly` (écriture sur `*old_limit`).
2439    unsafe {
2440        core::arch::asm!(
2441            "syscall",
2442            in("rax") 302_i64,
2443            in("rdi") i64::from(pid),
2444            in("rsi") i64::from(resource),
2445            in("rdx") new_limit,
2446            in("r10") old_limit,
2447            lateout("rax") ret,
2448            lateout("rcx") _,
2449            lateout("r11") _,
2450            options(nostack, preserves_flags),
2451        );
2452    }
2453    ret
2454}
2455
2456#[cfg(target_arch = "aarch64")]
2457#[inline]
2458unsafe fn raw_syscall_prlimit64(pid: i32, resource: u32, new_limit: u64, old_limit: u64) -> i64 {
2459    let ret: i64;
2460    // SAFETY: SYS_prlimit64 (aarch64 = 261). Mêmes considérations.
2461    unsafe {
2462        core::arch::asm!(
2463            "svc 0",
2464            in("x8") 261_i64,
2465            inout("x0") i64::from(pid) => ret,
2466            in("x1") i64::from(resource),
2467            in("x2") new_limit,
2468            in("x3") old_limit,
2469            options(nostack, preserves_flags),
2470        );
2471    }
2472    ret
2473}
2474
2475// ─────────────────────────────────────────────────────────────────────────
2476// capget / capset (sous-section 7 de family-process.md).
2477//
2478// Version 3 capabilities (kernel ≥ 2.6.25) : header + array de 2 data
2479// structs ; chaque data struct contient `(effective, permitted,
2480// inheritable)` en `u32`. Index 0 = bits 0-31, index 1 = bits 32-63.
2481//
2482// La conversion `CapabilityMask (u64) ↔ (lo, hi)` est faite par les
2483// helpers privés `mask_to_words` / `words_to_mask` ci-dessous, testés
2484// explicitement pour qu'une éventuelle inversion lo/hi soit détectée
2485// (cf. JOURNAL session 2026-05-24).
2486// ─────────────────────────────────────────────────────────────────────────
2487
2488const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
2489
2490#[repr(C)]
2491struct KernelCapHeader {
2492    version: u32,
2493    pid: i32,
2494}
2495
2496#[repr(C)]
2497#[derive(Default, Clone, Copy)]
2498struct KernelCapData {
2499    effective: u32,
2500    permitted: u32,
2501    inheritable: u32,
2502}
2503
2504fn mask_to_words(mask: CapabilityMask) -> (u32, u32) {
2505    let bits = mask.bits();
2506    // Troncature exacte : `bits & 0xFFFF_FFFF` fit dans u32 par
2507    // construction.
2508    #[allow(clippy::cast_possible_truncation)]
2509    let lo = (bits & 0xFFFF_FFFF) as u32;
2510    // Troncature exacte : après `>> 32`, la valeur résiduelle est dans
2511    // `[0, u32::MAX]`.
2512    #[allow(clippy::cast_possible_truncation)]
2513    let hi = (bits >> 32) as u32;
2514    (lo, hi)
2515}
2516
2517fn words_to_mask(lo: u32, hi: u32) -> CapabilityMask {
2518    let bits = (u64::from(hi) << 32) | u64::from(lo);
2519    CapabilityMask::from_bits(bits)
2520}
2521
2522fn cap_target_to_kernel_pid(target: CapabilityTarget) -> i32 {
2523    match target {
2524        CapabilityTarget::CurrentThread => 0,
2525        CapabilityTarget::Thread(tid) => tid.as_raw(),
2526        CapabilityTarget::Process(pid) => pid.as_raw(),
2527    }
2528}
2529
2530/// Lit les capabilities du thread cible.
2531///
2532/// # Errors
2533///
2534/// - [`Errno::EINVAL`] : version capabilities non supportée par le kernel
2535///   (Air utilise version 3, supportée depuis 2.6.25).
2536/// - [`Errno::EPERM`] : ciblage d'un thread sans droits (rare en lecture).
2537pub fn capget(target: CapabilityTarget) -> Result<CapabilitySet, Errno> {
2538    let mut header = KernelCapHeader {
2539        version: LINUX_CAPABILITY_VERSION_3,
2540        pid: cap_target_to_kernel_pid(target),
2541    };
2542    let mut data: [KernelCapData; 2] = [KernelCapData::default(); 2];
2543    let header_ptr: *mut KernelCapHeader = &mut header;
2544    let data_ptr: *mut KernelCapData = data.as_mut_ptr();
2545    // SAFETY: capget(2) lit `*header` et écrit deux `KernelCapData`
2546    // consécutives à `data_ptr` ; les deux pointeurs sont locaux et
2547    // valides pour la durée du call.
2548    let ret = unsafe { raw_syscall_capget(header_ptr as u64, data_ptr as u64) };
2549    if ret < 0 {
2550        return Err(errno_from_negative_syscall_ret(ret));
2551    }
2552    Ok(CapabilitySet {
2553        effective: words_to_mask(data[0].effective, data[1].effective),
2554        permitted: words_to_mask(data[0].permitted, data[1].permitted),
2555        inheritable: words_to_mask(data[0].inheritable, data[1].inheritable),
2556    })
2557}
2558
2559/// Écrit les capabilities du thread cible.
2560///
2561/// # Errors
2562///
2563/// - [`Errno::EINVAL`] : version capabilities ou layout invalides.
2564/// - [`Errno::EPERM`] : tentative d'élever une capability hors permitted,
2565///   ou modification d'un autre thread sans droits.
2566pub fn capset(target: CapabilityTarget, set: &CapabilitySet) -> Result<(), Errno> {
2567    let mut header = KernelCapHeader {
2568        version: LINUX_CAPABILITY_VERSION_3,
2569        pid: cap_target_to_kernel_pid(target),
2570    };
2571    let (e_lo, e_hi) = mask_to_words(set.effective);
2572    let (p_lo, p_hi) = mask_to_words(set.permitted);
2573    let (i_lo, i_hi) = mask_to_words(set.inheritable);
2574    let data: [KernelCapData; 2] = [
2575        KernelCapData {
2576            effective: e_lo,
2577            permitted: p_lo,
2578            inheritable: i_lo,
2579        },
2580        KernelCapData {
2581            effective: e_hi,
2582            permitted: p_hi,
2583            inheritable: i_hi,
2584        },
2585    ];
2586    let header_ptr: *mut KernelCapHeader = &mut header;
2587    let data_ptr: *const KernelCapData = data.as_ptr();
2588    // SAFETY: capset(2) lit `*header` et lit deux `KernelCapData`
2589    // consécutives à `data_ptr`. Tout est local et valide pour la durée
2590    // du call.
2591    let ret = unsafe { raw_syscall_capset(header_ptr as u64, data_ptr as u64) };
2592    if ret < 0 {
2593        return Err(errno_from_negative_syscall_ret(ret));
2594    }
2595    Ok(())
2596}
2597
2598#[cfg(target_arch = "x86_64")]
2599#[inline]
2600unsafe fn raw_syscall_capget(header: u64, data: u64) -> i64 {
2601    let ret: i64;
2602    // SAFETY: SYS_capget (x86_64 = 125). Le kernel lit `*header` (8 octets)
2603    // et écrit 2 × 12 octets à `data`. Pas de `readonly` (écriture).
2604    unsafe {
2605        core::arch::asm!(
2606            "syscall",
2607            in("rax") 125_i64,
2608            in("rdi") header,
2609            in("rsi") data,
2610            lateout("rax") ret,
2611            lateout("rcx") _,
2612            lateout("r11") _,
2613            options(nostack, preserves_flags),
2614        );
2615    }
2616    ret
2617}
2618
2619#[cfg(target_arch = "x86_64")]
2620#[inline]
2621unsafe fn raw_syscall_capset(header: u64, data: u64) -> i64 {
2622    let ret: i64;
2623    // SAFETY: SYS_capset (x86_64 = 126). Lecture seule de `*header` et
2624    // `*data` côté user ; `readonly` correct.
2625    unsafe {
2626        core::arch::asm!(
2627            "syscall",
2628            in("rax") 126_i64,
2629            in("rdi") header,
2630            in("rsi") data,
2631            lateout("rax") ret,
2632            lateout("rcx") _,
2633            lateout("r11") _,
2634            options(nostack, preserves_flags, readonly),
2635        );
2636    }
2637    ret
2638}
2639
2640#[cfg(target_arch = "aarch64")]
2641#[inline]
2642unsafe fn raw_syscall_capget(header: u64, data: u64) -> i64 {
2643    let ret: i64;
2644    // SAFETY: SYS_capget (aarch64 = 90).
2645    unsafe {
2646        core::arch::asm!(
2647            "svc 0",
2648            in("x8") 90_i64,
2649            inout("x0") header => ret,
2650            in("x1") data,
2651            options(nostack, preserves_flags),
2652        );
2653    }
2654    ret
2655}
2656
2657#[cfg(target_arch = "aarch64")]
2658#[inline]
2659unsafe fn raw_syscall_capset(header: u64, data: u64) -> i64 {
2660    let ret: i64;
2661    // SAFETY: SYS_capset (aarch64 = 91). `readonly` car lecture seule.
2662    unsafe {
2663        core::arch::asm!(
2664            "svc 0",
2665            in("x8") 91_i64,
2666            inout("x0") header => ret,
2667            in("x1") data,
2668            options(nostack, preserves_flags, readonly),
2669        );
2670    }
2671    ret
2672}
2673
2674// ─────────────────────────────────────────────────────────────────────────
2675// Helper interne de conversion `errno`.
2676// ─────────────────────────────────────────────────────────────────────────
2677
2678/// Convertit une valeur de retour de syscall strictement négative en
2679/// [`Errno`]. Le kernel borne les errno à `[-4095, -1]` (`MAX_ERRNO`).
2680fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
2681    debug_assert!(ret < 0 && ret > -4096);
2682    // `ret.wrapping_neg()` est donc dans `[1, 4095]` — toujours fit in i32.
2683    #[allow(clippy::cast_possible_truncation)]
2684    let raw = ret.wrapping_neg() as i32;
2685    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
2686    Errno::from_nonzero(nz)
2687}
2688
2689// ─────────────────────────────────────────────────────────────────────────
2690// Affinité CPU — sched_setaffinity / sched_getaffinity
2691// Cf. `docs/specs/layer-0/family-process-affinity.md`.
2692// ─────────────────────────────────────────────────────────────────────────
2693
2694#[cfg(target_arch = "x86_64")]
2695#[inline]
2696unsafe fn raw_syscall_sched_setaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2697    let ret: i64;
2698    // SAFETY:
2699    // - SYS_sched_setaffinity (x86_64 = 203). Le kernel **lit** `len` octets à
2700    //   `mask` (jamais d'écriture) → `readonly` correct ; validité du buffer
2701    //   garantie par l'appelant (`CpuSet` local, 128 octets).
2702    // - x86_64 ABI : numéro en RAX, args en RDI/RSI/RDX ; retour RAX ; clobbe
2703    //   RCX/R11.
2704    unsafe {
2705        core::arch::asm!(
2706            "syscall",
2707            in("rax") 203_i64,
2708            in("rdi") i64::from(pid),
2709            in("rsi") len,
2710            in("rdx") mask,
2711            lateout("rax") ret,
2712            lateout("rcx") _,
2713            lateout("r11") _,
2714            options(nostack, preserves_flags, readonly),
2715        );
2716    }
2717    ret
2718}
2719
2720#[cfg(target_arch = "aarch64")]
2721#[inline]
2722unsafe fn raw_syscall_sched_setaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2723    let ret: i64;
2724    // SAFETY: voir version x86_64 ; aarch64 ABI : numéro en X8 (= 122), args en
2725    // X0/X1/X2, retour en X0. Le kernel **lit** `mask` → `readonly`.
2726    unsafe {
2727        core::arch::asm!(
2728            "svc 0",
2729            in("x8") 122_i64,
2730            inout("x0") i64::from(pid) => ret,
2731            in("x1") len,
2732            in("x2") mask,
2733            options(nostack, preserves_flags, readonly),
2734        );
2735    }
2736    ret
2737}
2738
2739#[cfg(target_arch = "x86_64")]
2740#[inline]
2741unsafe fn raw_syscall_sched_getaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2742    let ret: i64;
2743    // SAFETY:
2744    // - SYS_sched_getaffinity (x86_64 = 204). Le kernel **écrit** jusqu'à `len`
2745    //   octets à `mask` → **pas de `readonly`** ; validité/taille du buffer
2746    //   garanties par l'appelant (`CpuSet` local, 128 octets).
2747    // - Retourne le nombre d'octets écrits (≥ 0) ou `-errno`.
2748    unsafe {
2749        core::arch::asm!(
2750            "syscall",
2751            in("rax") 204_i64,
2752            in("rdi") i64::from(pid),
2753            in("rsi") len,
2754            in("rdx") mask,
2755            lateout("rax") ret,
2756            lateout("rcx") _,
2757            lateout("r11") _,
2758            options(nostack, preserves_flags),
2759        );
2760    }
2761    ret
2762}
2763
2764#[cfg(target_arch = "aarch64")]
2765#[inline]
2766unsafe fn raw_syscall_sched_getaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2767    let ret: i64;
2768    // SAFETY: voir version x86_64 ; aarch64 ABI : numéro en X8 (= 123), args en
2769    // X0/X1/X2, retour en X0. Le kernel **écrit** dans `mask` → pas de `readonly`.
2770    unsafe {
2771        core::arch::asm!(
2772            "svc 0",
2773            in("x8") 123_i64,
2774            inout("x0") i64::from(pid) => ret,
2775            in("x1") len,
2776            in("x2") mask,
2777            options(nostack, preserves_flags),
2778        );
2779    }
2780    ret
2781}
2782
2783/// Identifiant de tâche pour les opérations d'affinité : `None` = **tâche
2784/// appelante** (sentinelle kernel `0` typée en `Option`, ADR-021 conv. 1).
2785fn affinity_pid(tid: Option<Tid>) -> i32 {
2786    // `0` = la tâche appelante (jamais exposé comme entier magique).
2787    tid.map_or(0, Tid::as_raw)
2788}
2789
2790/// Fixe l'**affinité CPU** d'une tâche (`sched_setaffinity(2)`) : la tâche ne sera
2791/// ordonnancée que sur les CPU présents dans `cpus`. `tid = None` cible la tâche
2792/// appelante. Débloque `air-thread::cpu_affinity` (couche 1).
2793///
2794/// # Errors
2795///
2796/// - [`Errno::EINVAL`] : `cpus` ne contient **aucun** CPU autorisé/en ligne.
2797/// - [`Errno::ESRCH`] : aucune tâche ne porte ce `tid`.
2798/// - [`Errno::EPERM`] : privilèges insuffisants pour changer l'affinité de `tid`.
2799pub fn set_cpu_affinity(tid: Option<Tid>, cpus: &CpuSet) -> Result<(), Errno> {
2800    let bytes = cpus.as_bytes();
2801    let len = bytes.len() as u64;
2802    let mask = bytes.as_ptr() as u64;
2803    // SAFETY: `mask` pointe les `len` octets (= 128) du `CpuSet` de l'appelant,
2804    // valides en lecture pour la durée de l'appel ; le kernel ne les écrit pas.
2805    let ret = unsafe { raw_syscall_sched_setaffinity(affinity_pid(tid), len, mask) };
2806    if ret < 0 {
2807        return Err(errno_from_negative_syscall_ret(ret));
2808    }
2809    Ok(())
2810}
2811
2812/// Lit l'**affinité CPU** courante d'une tâche (`sched_getaffinity(2)`) **dans**
2813/// `cpus` (buffer fourni, zéro allocation). `tid = None` cible la tâche appelante.
2814///
2815/// # Errors
2816///
2817/// - [`Errno::ESRCH`] : aucune tâche ne porte ce `tid`.
2818/// - [`Errno::EINVAL`] : taille de masque incohérente (ne se produit pas avec un
2819///   `CpuSet` de [`air_sys_types::system::CPU_SETSIZE`] bits).
2820pub fn get_cpu_affinity(tid: Option<Tid>, cpus: &mut CpuSet) -> Result<(), Errno> {
2821    let len = core::mem::size_of::<CpuSet>() as u64;
2822    let mask = core::ptr::from_mut(cpus).cast::<u8>() as u64;
2823    // SAFETY: `mask` pointe les `len` octets (= 128) du `CpuSet` **mutable** de
2824    // l'appelant ; le kernel y écrit au plus `len` octets (le bitmap d'affinité).
2825    // `CpuSet` est `#[repr(C)]` sans padding ⇒ tout motif d'octets est valide.
2826    let ret = unsafe { raw_syscall_sched_getaffinity(affinity_pid(tid), len, mask) };
2827    if ret < 0 {
2828        return Err(errno_from_negative_syscall_ret(ret));
2829    }
2830    Ok(())
2831}
2832
2833// ─────────────────────────────────────────────────────────────────────────
2834// exec / redirection — execve, execveat, dup3, fchdir, chdir.
2835//
2836// Surface pilotée par le contrat de la couche 1 (`air-process`) : lancer un
2837// programme (exec), rediriger ses stdin/stdout/stderr (dup3) et fixer son
2838// répertoire de travail (fchdir / chdir). Cf. `docs/specs/layer-0/
2839// family-process.md`, section « exec / redirection ».
2840// ─────────────────────────────────────────────────────────────────────────
2841
2842/// `AT_FDCWD` : base « répertoire courant » des opérations `*at` (sentinelle
2843/// kernel `-100`, typée [`DirFd::Cwd`] côté Air, cf. ADR-021 convention 1).
2844const AT_FDCWD: i32 = -100;
2845
2846/// Convertit une [`DirFd`] en l'entier attendu par l'ABI syscall (`AT_FDCWD`
2847/// pour [`DirFd::Cwd`], sinon le fd emprunté).
2848fn dirfd_to_raw(dirfd: DirFd<'_>) -> i32 {
2849    match dirfd {
2850        DirFd::Cwd => AT_FDCWD,
2851        DirFd::Fd(fd) => fd.as_raw_fd(),
2852    }
2853}
2854
2855/// Tableau de pointeurs C terminé par `NULL`, pour le marshalling d'`argv` /
2856/// `envp` vers `execve(2)` / `execveat(2)`.
2857///
2858/// Ces syscalls attendent un argument de type `char *const argv[]` : une suite
2859/// de pointeurs vers des chaînes C **terminée par un pointeur `NULL`**. Ce type
2860/// matérialise cette **frontière de marshalling**, qui est *fuzzée*
2861/// (`fuzz/fuzz_targets/fuzz_exec_argv.rs`) : construction depuis une liste
2862/// arbitraire (vide, longue), terminaison `NULL` toujours présente, ordre
2863/// préservé.
2864///
2865/// **Rejet des NUL embarqués — par construction.** Un argument C ne peut pas
2866/// contenir d'octet NUL (il y serait interprété comme terminateur). L'entrée
2867/// étant une tranche de [`CStr`] — type qui **garantit** l'absence de NUL
2868/// interne — l'invariant est assuré *en amont*, là où l'appelant construit ses
2869/// `&CStr` (typiquement via `CStr::from_bytes_with_nul` ou `CString::new`, qui
2870/// rejettent proprement un NUL embarqué). Aucune revalidation n'est donc
2871/// nécessaire ici (Principe 4 : « parse, don't validate »).
2872///
2873/// **Allocation.** La construction alloue un [`Vec`] de `items.len() + 1`
2874/// pointeurs : c'est l'exception d'allocation documentée d'ADR-021 §4 (buffer
2875/// dynamique *intrinsèque* — sa taille dépend du nombre d'arguments, inconnu à
2876/// la compilation). Les chaînes elles-mêmes ne sont **pas** copiées : les
2877/// pointeurs empruntent les [`CStr`] fournis (d'où la durée de vie `'a`).
2878pub struct CStrArray<'a> {
2879    /// Pointeurs vers chaque `CStr`, suivis d'un pointeur `NULL` final.
2880    /// Invariant : `ptrs.len() >= 1` et `ptrs[ptrs.len() - 1].is_null()`.
2881    ptrs: Vec<*const c_char>,
2882    /// Lie la durée de vie des pointeurs à celle de la tranche source.
2883    _borrow: PhantomData<&'a [&'a CStr]>,
2884}
2885
2886impl<'a> CStrArray<'a> {
2887    /// Construit le tableau `NULL`-terminé à partir de `items`.
2888    ///
2889    /// Une tranche **vide** produit un tableau ne contenant que le `NULL` final
2890    /// (`argv = { NULL }`, soit `argc == 0`) — accepté par le kernel.
2891    #[must_use]
2892    pub fn new(items: &'a [&'a CStr]) -> Self {
2893        // `+ 1` pour le terminateur NULL. `saturating_add` par discipline
2894        // (Principe 2) ; la capacité d'une tranche ne peut pas atteindre
2895        // `usize::MAX`, donc la saturation ne se déclenche jamais.
2896        let mut ptrs = Vec::with_capacity(items.len().saturating_add(1));
2897        for item in items {
2898            ptrs.push(item.as_ptr());
2899        }
2900        ptrs.push(core::ptr::null());
2901        Self {
2902            ptrs,
2903            _borrow: PhantomData,
2904        }
2905    }
2906
2907    /// Nombre d'arguments (hors terminateur `NULL`).
2908    #[must_use]
2909    pub fn len(&self) -> usize {
2910        // Invariant : `ptrs` contient toujours le `NULL` final en sus.
2911        self.ptrs.len().saturating_sub(1)
2912    }
2913
2914    /// Vrai si la liste est vide (le tableau ne contient que le `NULL` final).
2915    #[must_use]
2916    pub fn is_empty(&self) -> bool {
2917        self.len() == 0
2918    }
2919
2920    /// Pointeur vers le tableau de pointeurs, à transmettre au syscall.
2921    #[inline]
2922    fn as_ptr(&self) -> *const *const c_char {
2923        self.ptrs.as_ptr()
2924    }
2925}
2926
2927/// Remplace l'image mémoire du processus courant par l'exécutable situé à
2928/// `path`, avec les arguments `argv` et l'environnement `envp`.
2929///
2930/// Cf. `docs/specs/layer-0/family-process.md`. En cas de **succès, ne retourne
2931/// jamais** (le processus exécute désormais le nouveau binaire) — d'où le type
2932/// `Result<Infallible, Errno>` (ADR-021 convention 5). En cas d'échec, retourne
2933/// `Err(errno)`.
2934///
2935/// `argv` / `envp` sont des tranches de [`CStr`] ; le wrapper en construit les
2936/// tableaux de pointeurs C `NULL`-terminés via [`CStrArray`]. Par convention
2937/// POSIX, `argv[0]` est le nom du programme ; une tranche vide est acceptée par
2938/// le kernel (`argc == 0`).
2939///
2940/// Pour exécuter un binaire désigné par un **fd** (sans race sur le chemin),
2941/// préférer [`execveat`] avec [`ExecveatFlags::EMPTY_PATH`].
2942///
2943/// # Errors
2944///
2945/// - [`Errno::EACCES`] : `path` n'est pas exécutable, un répertoire du chemin
2946///   interdit la traversée, ou le système de fichiers est monté `noexec`.
2947/// - [`Errno::ENOENT`] : `path` (ou l'interpréteur `#!` d'un script) n'existe
2948///   pas.
2949/// - [`Errno::ENOEXEC`] : `path` existe mais n'est pas dans un format
2950///   exécutable reconnu (ni ELF, ni script avec `#!`).
2951/// - [`Errno::E2BIG`] : `argv` + `envp` dépassent la limite kernel (`ARG_MAX`).
2952/// - `ENOMEM`, `ETXTBSY`, `ELOOP`, … : cf. `execve(2)`.
2953pub fn execve(path: &CStr, argv: &[&CStr], envp: &[&CStr]) -> Result<Infallible, Errno> {
2954    // L'allocation des tableaux de pointeurs ([`CStrArray::new`]) a lieu **ici**.
2955    // Le cœur (syscall, sans allocation) est délégué à [`execve_prepared`], que
2956    // les appelants en chemin **post-fork** invoquent directement avec des
2957    // [`CStrArray`] matérialisés **avant** le fork (async-signal-safety).
2958    let c_argv = CStrArray::new(argv);
2959    let c_envp = CStrArray::new(envp);
2960    execve_prepared(path, &c_argv, &c_envp)
2961}
2962
2963/// Variante d'[`execve`] **sans allocation** : reçoit des tableaux de pointeurs
2964/// `argv`/`envp` **déjà construits** ([`CStrArray`]).
2965///
2966/// Destinée au chemin **post-fork / pré-`execve`** : l'appelant matérialise les
2967/// [`CStrArray`] dans le parent **avant** `clone3`/`fork`, puis les passe ici
2968/// depuis l'enfant. Cette fonction n'alloue rien — seul un syscall est émis —
2969/// donc elle est sûre dans la fenêtre fork→exec où aucune allocation (verrou
2970/// allocateur potentiellement tenu par un autre thread au moment du fork) n'est
2971/// permise. [`execve`] délègue à cette fonction après avoir bâti ses tableaux.
2972///
2973/// En cas de **succès, ne retourne jamais** (image mémoire remplacée). En cas
2974/// d'échec, retourne `Err(errno)`.
2975///
2976/// # Errors
2977///
2978/// Identiques à [`execve`] (`EACCES`, `ENOENT`, `ENOEXEC`, `E2BIG`, …).
2979pub fn execve_prepared(
2980    path: &CStr,
2981    argv: &CStrArray<'_>,
2982    envp: &CStrArray<'_>,
2983) -> Result<Infallible, Errno> {
2984    // SAFETY:
2985    // - `path` est un `CStr` valide NUL-terminé (le kernel le lit jusqu'au NUL).
2986    // - `argv`/`envp` exposent chacun un tableau de pointeurs C valide, terminé
2987    //   par NULL, vivant pour toute la durée de l'appel ; les chaînes pointées
2988    //   sont les `CStr` empruntés, vivants eux aussi (durée de vie liée par
2989    //   `CStrArray<'a>`). Le kernel ne fait que **lire** ces données.
2990    // - Succès : le syscall ne revient pas (image remplacée). Échec : ret < 0.
2991    let ret = unsafe {
2992        raw_syscall_execve(
2993            path.as_ptr() as u64,
2994            argv.as_ptr() as u64,
2995            envp.as_ptr() as u64,
2996        )
2997    };
2998    // Seul un échec ramène ici : `ret` est toujours `< 0`. La conversion errno
2999    // impose elle-même `ret ∈ [-4095, -1]` (debug_assert interne).
3000    Err(errno_from_negative_syscall_ret(ret))
3001}
3002
3003#[cfg(target_arch = "x86_64")]
3004#[inline]
3005unsafe fn raw_syscall_execve(path: u64, argv: u64, envp: u64) -> i64 {
3006    let ret: i64;
3007    // SAFETY:
3008    // - SYS_execve (x86_64 = 59). Le kernel **lit** la chaîne `path` (jusqu'au
3009    //   NUL) et les tableaux `argv`/`envp` (jusqu'au pointeur NULL, puis chaque
3010    //   chaîne jusqu'à son NUL) ; il n'écrit aucune mémoire utilisateur →
3011    //   `readonly` correct. Validité garantie par l'appelant.
3012    // - Succès : ne revient pas (image remplacée). Échec : ret = -errno.
3013    // - x86_64 ABI : numéro en RAX, args en RDI/RSI/RDX ; retour RAX ; clobbe
3014    //   RCX/R11.
3015    unsafe {
3016        core::arch::asm!(
3017            "syscall",
3018            in("rax") 59_i64,
3019            in("rdi") path,
3020            in("rsi") argv,
3021            in("rdx") envp,
3022            lateout("rax") ret,
3023            lateout("rcx") _,
3024            lateout("r11") _,
3025            options(nostack, preserves_flags, readonly),
3026        );
3027    }
3028    ret
3029}
3030
3031#[cfg(target_arch = "aarch64")]
3032#[inline]
3033unsafe fn raw_syscall_execve(path: u64, argv: u64, envp: u64) -> i64 {
3034    let ret: i64;
3035    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_execve (aarch64 = 221),
3036    // numéro en X8, args en X0/X1/X2, retour en X0. Lecture seule → `readonly`.
3037    unsafe {
3038        core::arch::asm!(
3039            "svc 0",
3040            in("x8") 221_i64,
3041            inout("x0") path => ret,
3042            in("x1") argv,
3043            in("x2") envp,
3044            options(nostack, preserves_flags, readonly),
3045        );
3046    }
3047    ret
3048}
3049
3050/// Variante *at d'[`execve`] (préférée par ADR-021) : résout l'exécutable
3051/// relativement à `dirfd`, ou — avec [`ExecveatFlags::EMPTY_PATH`] et
3052/// `path == c""` — exécute **directement** le binaire désigné par `dirfd`
3053/// (un fd ouvert sur l'exécutable, typiquement avec `O_PATH`), éliminant toute
3054/// race entre l'ouverture/vérification et l'exec.
3055///
3056/// Cf. `docs/specs/layer-0/family-process.md`. Comme [`execve`], **ne retourne
3057/// jamais en cas de succès**.
3058///
3059/// # Errors
3060///
3061/// Mêmes erreurs qu'[`execve`], plus :
3062///
3063/// - [`Errno::EBADF`] : `dirfd` n'est ni `AT_FDCWD` ni un fd ouvert valide.
3064/// - [`Errno::ENOENT`] : `path` est vide sans [`ExecveatFlags::EMPTY_PATH`].
3065/// - [`Errno::ELOOP`] : composant final symbolique avec
3066///   [`ExecveatFlags::SYMLINK_NOFOLLOW`].
3067pub fn execveat(
3068    dirfd: DirFd<'_>,
3069    path: &CStr,
3070    argv: &[&CStr],
3071    envp: &[&CStr],
3072    flags: ExecveatFlags,
3073) -> Result<Infallible, Errno> {
3074    let c_argv = CStrArray::new(argv);
3075    let c_envp = CStrArray::new(envp);
3076    // SAFETY: identique à `execve` pour `path`/`argv`/`envp` (lecture seule),
3077    // plus :
3078    // - `dirfd` est `AT_FDCWD` ou un fd valide emprunté (résolution relative,
3079    //   ou exécutable direct si `EMPTY_PATH`).
3080    // - `flags` est un masque `AT_*` valide (typé `ExecveatFlags`).
3081    let ret = unsafe {
3082        raw_syscall_execveat(
3083            dirfd_to_raw(dirfd),
3084            path.as_ptr() as u64,
3085            c_argv.as_ptr() as u64,
3086            c_envp.as_ptr() as u64,
3087            flags.bits(),
3088        )
3089    };
3090    // Seul un échec ramène ici : `ret` est toujours `< 0`.
3091    Err(errno_from_negative_syscall_ret(ret))
3092}
3093
3094#[cfg(target_arch = "x86_64")]
3095#[inline]
3096unsafe fn raw_syscall_execveat(dirfd: i32, path: u64, argv: u64, envp: u64, flags: i32) -> i64 {
3097    let ret: i64;
3098    // SAFETY:
3099    // - SYS_execveat (x86_64 = 322). Lecture seule de `path`/`argv`/`envp` (cf.
3100    //   `raw_syscall_execve`) → `readonly`. `dirfd` (étendu en signe : AT_FDCWD
3101    //   = -100 ou fd valide) et `flags` sont des scalaires.
3102    // - Succès : ne revient pas. Échec : ret = -errno.
3103    // - x86_64 ABI : args en RDI/RSI/RDX/R10/R8.
3104    unsafe {
3105        core::arch::asm!(
3106            "syscall",
3107            in("rax") 322_i64,
3108            in("rdi") i64::from(dirfd),
3109            in("rsi") path,
3110            in("rdx") argv,
3111            in("r10") envp,
3112            in("r8") i64::from(flags),
3113            lateout("rax") ret,
3114            lateout("rcx") _,
3115            lateout("r11") _,
3116            options(nostack, preserves_flags, readonly),
3117        );
3118    }
3119    ret
3120}
3121
3122#[cfg(target_arch = "aarch64")]
3123#[inline]
3124unsafe fn raw_syscall_execveat(dirfd: i32, path: u64, argv: u64, envp: u64, flags: i32) -> i64 {
3125    let ret: i64;
3126    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_execveat (aarch64 = 281),
3127    // numéro en X8, args en X0..X4, retour en X0. Lecture seule → `readonly`.
3128    unsafe {
3129        core::arch::asm!(
3130            "svc 0",
3131            in("x8") 281_i64,
3132            inout("x0") i64::from(dirfd) => ret,
3133            in("x1") path,
3134            in("x2") argv,
3135            in("x3") envp,
3136            in("x4") i64::from(flags),
3137            options(nostack, preserves_flags, readonly),
3138        );
3139    }
3140    ret
3141}
3142
3143/// Duplique le descripteur `old` vers le numéro `new` (`dup3(2)`), en fermant
3144/// atomiquement `new` s'il était ouvert. Retourne le nouveau descripteur,
3145/// possédé ([`OwnedFd`], fermé au `Drop`).
3146///
3147/// Cf. `docs/specs/layer-0/family-process.md`. Variante **moderne** de `dup2`
3148/// (ADR-021) : les drapeaux sont explicites ([`Dup3Flags`], seul `O_CLOEXEC`
3149/// est valide). Cas d'usage couche 1 : rediriger les flux standard d'un enfant
3150/// (`dup3(pipe, STDOUT_FILENO, …)`) juste avant l'`exec`.
3151///
3152/// À la différence de `dup2`, `dup3` **échoue** (`EINVAL`) si `old == new` :
3153/// pas de no-op silencieux. Le numéro `new` doit donc différer de `old`.
3154///
3155/// # Errors
3156///
3157/// - [`Errno::EINVAL`] : `flags` invalides, **ou** `old == new`.
3158/// - [`Errno::EBADF`] : `new` hors de la plage autorisée (`> RLIMIT_NOFILE`).
3159/// - [`Errno::EBUSY`] : course (rare) avec `open`/`dup` concurrents sur `new`.
3160/// - [`Errno::EINTR`] : interrompu par un signal (remonté tel quel, ADR-021
3161///   convention 2).
3162pub fn dup3(old: BorrowedFd<'_>, new: RawFd, flags: Dup3Flags) -> Result<OwnedFd, Errno> {
3163    // SAFETY: `dup3(2)` ne touche aucune mémoire utilisateur ; il installe une
3164    // copie de l'entrée de table de fd `old` au numéro `new` (fermant `new` au
3165    // préalable s'il était ouvert), de manière atomique. `flags` (typé) ne peut
3166    // valoir que `O_CLOEXEC`.
3167    let ret = unsafe { raw_syscall_dup3(old.as_raw_fd(), new, flags.bits()) };
3168    if ret < 0 {
3169        return Err(errno_from_negative_syscall_ret(ret));
3170    }
3171    // Succès : `ret == new` (le numéro demandé), donc tient dans un `i32`.
3172    #[allow(clippy::cast_possible_truncation)]
3173    let new_fd = ret as RawFd;
3174    // SAFETY: le kernel vient d'installer un fd valide au numéro `new`, dont
3175    // Air prend désormais la propriété exclusive (fermé par `OwnedFd::Drop`).
3176    Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
3177}
3178
3179#[cfg(target_arch = "x86_64")]
3180#[inline]
3181unsafe fn raw_syscall_dup3(oldfd: i32, newfd: i32, flags: i32) -> i64 {
3182    let ret: i64;
3183    // SAFETY: SYS_dup3 (x86_64 = 292) ne touche aucune mémoire utilisateur
3184    // (`readonly`). Args étendus en signe (fd ≥ 0 ici ; l'extension reste
3185    // correcte). x86_64 ABI : args en RDI/RSI/RDX.
3186    unsafe {
3187        core::arch::asm!(
3188            "syscall",
3189            in("rax") 292_i64,
3190            in("rdi") i64::from(oldfd),
3191            in("rsi") i64::from(newfd),
3192            in("rdx") i64::from(flags),
3193            lateout("rax") ret,
3194            lateout("rcx") _,
3195            lateout("r11") _,
3196            options(nostack, preserves_flags, readonly),
3197        );
3198    }
3199    ret
3200}
3201
3202#[cfg(target_arch = "aarch64")]
3203#[inline]
3204unsafe fn raw_syscall_dup3(oldfd: i32, newfd: i32, flags: i32) -> i64 {
3205    let ret: i64;
3206    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_dup3 (aarch64 = 24),
3207    // numéro en X8, args en X0/X1/X2, retour en X0. Lecture seule → `readonly`.
3208    unsafe {
3209        core::arch::asm!(
3210            "svc 0",
3211            in("x8") 24_i64,
3212            inout("x0") i64::from(oldfd) => ret,
3213            in("x1") i64::from(newfd),
3214            in("x2") i64::from(flags),
3215            options(nostack, preserves_flags, readonly),
3216        );
3217    }
3218    ret
3219}
3220
3221/// Change le répertoire de travail courant du processus pour celui référencé
3222/// par le descripteur `dir` (`fchdir(2)`).
3223///
3224/// Cf. `docs/specs/layer-0/family-process.md`. Variante par fd de [`chdir`] :
3225/// pas de race sur le chemin (le répertoire est déjà ouvert et épinglé). Cas
3226/// d'usage couche 1 : fixer le `current_dir` d'un enfant à partir d'un fd
3227/// vérifié, juste avant l'`exec`.
3228///
3229/// # Errors
3230///
3231/// - [`Errno::EBADF`] : `dir` n'est pas un descripteur ouvert valide.
3232/// - [`Errno::ENOTDIR`] : `dir` ne référence pas un répertoire.
3233/// - [`Errno::EACCES`] : permission de recherche refusée sur le répertoire.
3234pub fn fchdir(dir: BorrowedFd<'_>) -> Result<(), Errno> {
3235    // SAFETY: `fchdir(2)` ne touche aucune mémoire utilisateur ; il bascule le
3236    // répertoire courant du processus vers le répertoire référencé par `dir`.
3237    let ret = unsafe { raw_syscall_fchdir(dir.as_raw_fd()) };
3238    if ret < 0 {
3239        return Err(errno_from_negative_syscall_ret(ret));
3240    }
3241    Ok(())
3242}
3243
3244#[cfg(target_arch = "x86_64")]
3245#[inline]
3246unsafe fn raw_syscall_fchdir(fd: i32) -> i64 {
3247    let ret: i64;
3248    // SAFETY: SYS_fchdir (x86_64 = 81) ne touche aucune mémoire utilisateur
3249    // (`readonly`). x86_64 ABI : argument unique en RDI.
3250    unsafe {
3251        core::arch::asm!(
3252            "syscall",
3253            in("rax") 81_i64,
3254            in("rdi") i64::from(fd),
3255            lateout("rax") ret,
3256            lateout("rcx") _,
3257            lateout("r11") _,
3258            options(nostack, preserves_flags, readonly),
3259        );
3260    }
3261    ret
3262}
3263
3264#[cfg(target_arch = "aarch64")]
3265#[inline]
3266unsafe fn raw_syscall_fchdir(fd: i32) -> i64 {
3267    let ret: i64;
3268    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_fchdir (aarch64 = 50),
3269    // numéro en X8, argument en X0, retour en X0. Lecture seule → `readonly`.
3270    unsafe {
3271        core::arch::asm!(
3272            "svc 0",
3273            in("x8") 50_i64,
3274            inout("x0") i64::from(fd) => ret,
3275            options(nostack, preserves_flags, readonly),
3276        );
3277    }
3278    ret
3279}
3280
3281/// Change le répertoire de travail courant du processus pour `path`
3282/// (`chdir(2)`).
3283///
3284/// Cf. `docs/specs/layer-0/family-process.md`. Cas d'usage couche 1 : fixer le
3285/// `current_dir` d'un enfant juste avant l'`exec`. Préférer [`fchdir`] quand un
3286/// fd de répertoire est disponible (pas de race sur le chemin).
3287///
3288/// # Errors
3289///
3290/// - [`Errno::ENOENT`] : `path` n'existe pas.
3291/// - [`Errno::ENOTDIR`] : un composant de `path` n'est pas un répertoire.
3292/// - [`Errno::EACCES`] : permission de recherche refusée.
3293/// - [`Errno::ENAMETOOLONG`] : `path` trop long.
3294pub fn chdir(path: &CStr) -> Result<(), Errno> {
3295    // SAFETY: `chdir(2)` **lit** la chaîne `path` (jusqu'au NUL) et n'écrit
3296    // aucune mémoire utilisateur → `readonly`. `path` est un `CStr` valide.
3297    let ret = unsafe { raw_syscall_chdir(path.as_ptr() as u64) };
3298    if ret < 0 {
3299        return Err(errno_from_negative_syscall_ret(ret));
3300    }
3301    Ok(())
3302}
3303
3304/// Change le **répertoire racine** du processus vers `path` (`chroot(2)`) —
3305/// **descellement additif [ADR-085](../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md)**
3306/// pour la face libc. Confinement de processus (`CAP_SYS_CHROOT` requis).
3307///
3308/// # Errors
3309///
3310/// - [`Errno::EPERM`] : `CAP_SYS_CHROOT` manquant.
3311/// - [`Errno::ENOENT`] : `path` n'existe pas.
3312/// - [`Errno::ENOTDIR`] : un composant de `path` n'est pas un répertoire.
3313pub fn chroot(path: &CStr) -> Result<(), Errno> {
3314    // SAFETY: `chroot(2)` **lit** la chaîne `path` (jusqu'au NUL) et n'écrit aucune
3315    // mémoire utilisateur → `readonly`. `path` est un `CStr` valide.
3316    let ret = unsafe { raw_syscall_chroot(path.as_ptr() as u64) };
3317    if ret < 0 {
3318        return Err(errno_from_negative_syscall_ret(ret));
3319    }
3320    Ok(())
3321}
3322
3323#[cfg(target_arch = "x86_64")]
3324#[inline]
3325unsafe fn raw_syscall_chdir(path: u64) -> i64 {
3326    let ret: i64;
3327    // SAFETY: SYS_chdir (x86_64 = 80). Le kernel **lit** la chaîne `path`
3328    // (jusqu'au NUL), validité garantie par l'appelant ; aucune écriture
3329    // utilisateur → `readonly`. x86_64 ABI : argument unique en RDI.
3330    unsafe {
3331        core::arch::asm!(
3332            "syscall",
3333            in("rax") 80_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_chdir(path: u64) -> i64 {
3347    let ret: i64;
3348    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_chdir (aarch64 = 49),
3349    // numéro en X8, argument (pointeur `path`) en X0, retour en X0. Lecture
3350    // seule → `readonly`.
3351    unsafe {
3352        core::arch::asm!(
3353            "svc 0",
3354            in("x8") 49_i64,
3355            inout("x0") path => ret,
3356            options(nostack, preserves_flags, readonly),
3357        );
3358    }
3359    ret
3360}
3361
3362#[cfg(target_arch = "x86_64")]
3363#[inline]
3364unsafe fn raw_syscall_chroot(path: u64) -> i64 {
3365    let ret: i64;
3366    // SAFETY: SYS_chroot (x86_64 = 161). Le kernel **lit** la chaîne `path` (jusqu'au
3367    // NUL), validité garantie par l'appelant ; aucune écriture utilisateur → `readonly`.
3368    // x86_64 ABI : argument unique en RDI.
3369    unsafe {
3370        core::arch::asm!(
3371            "syscall",
3372            in("rax") 161_i64,
3373            in("rdi") path,
3374            lateout("rax") ret,
3375            lateout("rcx") _,
3376            lateout("r11") _,
3377            options(nostack, preserves_flags, readonly),
3378        );
3379    }
3380    ret
3381}
3382
3383#[cfg(target_arch = "aarch64")]
3384#[inline]
3385unsafe fn raw_syscall_chroot(path: u64) -> i64 {
3386    let ret: i64;
3387    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_chroot (aarch64 = 51), numéro en
3388    // X8, argument (pointeur `path`) en X0, retour en X0. Lecture seule → `readonly`.
3389    unsafe {
3390        core::arch::asm!(
3391            "svc 0",
3392            in("x8") 51_i64,
3393            inout("x0") path => ret,
3394            options(nostack, preserves_flags, readonly),
3395        );
3396    }
3397    ret
3398}
3399
3400/// API de fuzzing **pure** (sans syscall) de la frontière de marshalling
3401/// `argv`/`envp` ([`CStrArray`]). Le harnais cargo-fuzz
3402/// (`fuzz/fuzz_targets/fuzz_exec_argv.rs`) y délègue ; l'oracle vit ici pour
3403/// accéder aux internes de [`CStrArray`] (le tableau de pointeurs) tout en
3404/// restant testé et couvert. Suit le pattern de [`crate::io_uring::fuzz_api`].
3405pub mod fuzz_api {
3406    use super::CStrArray;
3407    use alloc::ffi::CString;
3408    use alloc::vec::Vec;
3409    use core::ffi::{CStr, c_char};
3410
3411    /// Exerce la chaîne « octets bruts → `CString` → `CStrArray` » et la
3412    /// **parcourt** jusqu'au terminateur `NULL`, garantissant l'absence de
3413    /// panic / UB pour **toute** entrée (Principe 3 — frontière) :
3414    ///
3415    /// - **rejet propre des NUL embarqués** : `CString::new` rend `Err` (jamais
3416    ///   panic) dès qu'un morceau contient un octet NUL ; ces morceaux sont
3417    ///   simplement écartés ;
3418    /// - **totalité** : la construction du tableau ne panique jamais (liste
3419    ///   vide, ou très longue) ;
3420    /// - **terminaison NULL & validité** : le parcours s'arrête sur le `NULL`
3421    ///   final et relit chaque chaîne C (tout pointeur invalide ou toute
3422    ///   terminaison manquante ferait crasher sous sanitizer/libFuzzer).
3423    ///
3424    /// L'oracle *structurel* exact (ordre, exactitude des pointeurs) est
3425    /// vérifié par les tests unitaires `cstr_array_*`.
3426    pub fn marshal_exec_args(chunks: Vec<Vec<u8>>) {
3427        // Frontière 1 — NUL embarqué rejeté proprement (Err, jamais panic).
3428        let owned: Vec<CString> = chunks
3429            .iter()
3430            .filter_map(|chunk| CString::new(chunk.as_slice()).ok())
3431            .collect();
3432        let refs: Vec<&CStr> = owned.iter().map(CString::as_c_str).collect();
3433
3434        // Frontière 2 — construction du tableau de pointeurs C NULL-terminé.
3435        let arr = CStrArray::new(&refs);
3436        let _ = arr.len();
3437        let _ = arr.is_empty();
3438
3439        // Parcours jusqu'au terminateur NULL en relisant chaque chaîne : tout
3440        // écart (pointeur invalide, terminaison absente) ferait crasher la
3441        // cible libFuzzer.
3442        let base = arr.as_ptr();
3443        let mut i = 0_usize;
3444        loop {
3445            // SAFETY: `base` pointe `refs.len() + 1` pointeurs ; on s'arrête au
3446            // premier NULL (présent par construction en position `refs.len()`),
3447            // donc `i` reste borné par `refs.len()`.
3448            let ptr: *const c_char = unsafe { *base.add(i) };
3449            if ptr.is_null() {
3450                break;
3451            }
3452            // SAFETY: `ptr` non NULL ⇒ i-ème chaîne C vivante (empruntée à
3453            // `owned`), NUL-terminée par construction de `CStr`.
3454            let s = unsafe { CStr::from_ptr(ptr) };
3455            let _ = s.to_bytes();
3456            i = i.saturating_add(1);
3457        }
3458    }
3459}
3460
3461// ─────────────────────────────────────────────────────────────────────────
3462// Runtime & libc — re-sceau `couche-0-v1.7` (ADR-051)
3463//
3464// 6 syscalls ajoutés pour le runtime Air (air-rt) et l'objectif libc :
3465// `getppid`, `set_tid_address`, `sched_yield`, `umask`, `getcwd`, `getrusage`.
3466// (Le 7ᵉ, `arch_prctl`, est x86_64-only et vit dans `crate::arch`.)
3467// Conventions ADR-021 : fonction dédiée typée, `Option`/newtypes, pas de
3468// sentinelle, zéro alloc sur le happy path.
3469// ─────────────────────────────────────────────────────────────────────────
3470
3471/// Retourne le PID du **parent** du processus appelant (`getppid(2)`).
3472///
3473/// Retourne `None` lorsque le kernel rend `0`, c.-à-d. quand le parent n'est
3474/// **pas visible** dans le *PID namespace* du processus (ré-parentage hors
3475/// namespace). Le `0` du kernel — qui n'est pas un PID valide — est ainsi
3476/// re-présenté en `Option` plutôt qu'exposé comme sentinelle (ADR-021 §1).
3477#[must_use]
3478pub fn getppid() -> Option<Pid> {
3479    let raw = raw_syscall_getppid();
3480    // PID : `pid_t` (i32) ; les 32 bits hauts sont nuls pour une valeur valide.
3481    #[allow(clippy::cast_possible_truncation)]
3482    let raw32 = raw as i32;
3483    // `try_from_raw` rend `None` pour `0` (parent hors namespace).
3484    Pid::try_from_raw(raw32)
3485}
3486
3487#[cfg(target_arch = "x86_64")]
3488#[inline]
3489fn raw_syscall_getppid() -> i64 {
3490    let ret: i64;
3491    // SAFETY: SYS_getppid (x86_64 = 110) ne touche aucune mémoire utilisateur ;
3492    // clobbe RCX/R11 comme tout `syscall` x86_64.
3493    unsafe {
3494        core::arch::asm!(
3495            "syscall",
3496            in("rax") 110_i64,
3497            lateout("rax") ret,
3498            lateout("rcx") _,
3499            lateout("r11") _,
3500            options(nostack, preserves_flags, readonly),
3501        );
3502    }
3503    ret
3504}
3505
3506#[cfg(target_arch = "aarch64")]
3507#[inline]
3508fn raw_syscall_getppid() -> i64 {
3509    let ret: i64;
3510    // SAFETY: SYS_getppid (aarch64 = 173) ne touche aucune mémoire utilisateur.
3511    unsafe {
3512        core::arch::asm!(
3513            "svc 0",
3514            in("x8") 173_i64,
3515            lateout("x0") ret,
3516            options(nostack, preserves_flags, readonly),
3517        );
3518    }
3519    ret
3520}
3521
3522/// Enregistre l'adresse `clear_child_tid` du thread appelant et retourne son TID
3523/// (`set_tid_address(2)`). À la **fin du thread**, le kernel écrit `0` à cette
3524/// adresse et émet un `FUTEX_WAKE` dessus — primitif de *join* (cf.
3525/// [`clone_thread`] et la famille [`futex`](crate::futex)).
3526///
3527/// `None` désenregistre (passe `NULL`). L'appel **ne peut pas échouer** et rend
3528/// toujours le TID appelant (`> 0`).
3529///
3530/// # Safety
3531///
3532/// Si `clear_child_tid` est `Some(addr)`, le kernel **conservera `addr`** et y
3533/// écrira à la terminaison du thread. L'appelant doit garantir que la mémoire
3534/// pointée reste **valide jusqu'à la fin du thread** (sinon écriture kernel dans
3535/// de la mémoire libérée). Même discipline que [`CloneArgs::child_tid`].
3536pub unsafe fn set_tid_address(clear_child_tid: Option<&core::sync::atomic::AtomicU32>) -> Tid {
3537    let ptr = clear_child_tid.map_or(0_usize, |a| core::ptr::from_ref(a) as usize);
3538    // SAFETY: l'appel délègue ; la liveness de `ptr` jusqu'à la mort du thread
3539    // relève du contrat `unsafe` de cette fonction.
3540    let raw = unsafe { raw_syscall_set_tid_address(ptr) };
3541    // Le kernel rend le TID appelant, strictement positif.
3542    #[allow(clippy::cast_possible_truncation)]
3543    let tid_raw = raw as i32;
3544    let nz = NonZeroI32::new(tid_raw).expect("set_tid_address : TID appelant > 0");
3545    Tid::from_nonzero(nz)
3546}
3547
3548#[cfg(target_arch = "x86_64")]
3549#[inline]
3550unsafe fn raw_syscall_set_tid_address(tidptr: usize) -> i64 {
3551    let ret: i64;
3552    // SAFETY: SYS_set_tid_address (x86_64 = 218). Le kernel **enregistre**
3553    // `tidptr` pour écriture à la mort du thread ; il n'écrit pas pendant
3554    // l'appel. La validité/liveness de `tidptr` relève du contrat de l'appelant.
3555    unsafe {
3556        core::arch::asm!(
3557            "syscall",
3558            in("rax") 218_i64,
3559            in("rdi") tidptr,
3560            lateout("rax") ret,
3561            lateout("rcx") _,
3562            lateout("r11") _,
3563            options(nostack, preserves_flags),
3564        );
3565    }
3566    ret
3567}
3568
3569#[cfg(target_arch = "aarch64")]
3570#[inline]
3571unsafe fn raw_syscall_set_tid_address(tidptr: usize) -> i64 {
3572    let ret: i64;
3573    // SAFETY: SYS_set_tid_address (aarch64 = 96). Voir la branche x86_64.
3574    unsafe {
3575        core::arch::asm!(
3576            "svc 0",
3577            in("x8") 96_i64,
3578            in("x0") tidptr,
3579            lateout("x0") ret,
3580            options(nostack, preserves_flags),
3581        );
3582    }
3583    ret
3584}
3585
3586/// Cède volontairement le CPU à un autre thread prêt (`sched_yield(2)`).
3587///
3588/// Sous Linux, `sched_yield` **réussit toujours** (retour `0`) ; la fonction est
3589/// donc totale.
3590pub fn sched_yield() {
3591    let _ = raw_syscall_sched_yield();
3592}
3593
3594#[cfg(target_arch = "x86_64")]
3595#[inline]
3596fn raw_syscall_sched_yield() -> i64 {
3597    let ret: i64;
3598    // SAFETY: SYS_sched_yield (x86_64 = 24) ne touche aucune mémoire utilisateur.
3599    unsafe {
3600        core::arch::asm!(
3601            "syscall",
3602            in("rax") 24_i64,
3603            lateout("rax") ret,
3604            lateout("rcx") _,
3605            lateout("r11") _,
3606            options(nostack, preserves_flags, readonly),
3607        );
3608    }
3609    ret
3610}
3611
3612#[cfg(target_arch = "aarch64")]
3613#[inline]
3614fn raw_syscall_sched_yield() -> i64 {
3615    let ret: i64;
3616    // SAFETY: SYS_sched_yield (aarch64 = 124) ne touche aucune mémoire utilisateur.
3617    unsafe {
3618        core::arch::asm!(
3619            "svc 0",
3620            in("x8") 124_i64,
3621            lateout("x0") ret,
3622            options(nostack, preserves_flags, readonly),
3623        );
3624    }
3625    ret
3626}
3627
3628/// Fixe le **masque de création de fichiers** du processus et retourne l'ancien
3629/// (`umask(2)`). Opération **totale** (ne peut pas échouer).
3630#[must_use]
3631pub fn umask(new_mask: Mode) -> Mode {
3632    let previous = raw_syscall_umask(u64::from(new_mask));
3633    // L'ancien masque tient dans `mode_t` (12 bits significatifs) ⇒ conversion
3634    // sûre, jamais lossy (Principe 2 : pas d'`as` lossy).
3635    u32::try_from(previous).expect("umask : ancien masque dans la plage mode_t")
3636}
3637
3638#[cfg(target_arch = "x86_64")]
3639#[inline]
3640fn raw_syscall_umask(mask: u64) -> i64 {
3641    let ret: i64;
3642    // SAFETY: SYS_umask (x86_64 = 95) ne touche aucune mémoire utilisateur ;
3643    // rend l'ancien masque.
3644    unsafe {
3645        core::arch::asm!(
3646            "syscall",
3647            in("rax") 95_i64,
3648            in("rdi") mask,
3649            lateout("rax") ret,
3650            lateout("rcx") _,
3651            lateout("r11") _,
3652            options(nostack, preserves_flags, readonly),
3653        );
3654    }
3655    ret
3656}
3657
3658#[cfg(target_arch = "aarch64")]
3659#[inline]
3660fn raw_syscall_umask(mask: u64) -> i64 {
3661    let ret: i64;
3662    // SAFETY: SYS_umask (aarch64 = 166). Voir la branche x86_64.
3663    unsafe {
3664        core::arch::asm!(
3665            "svc 0",
3666            in("x8") 166_i64,
3667            in("x0") mask,
3668            lateout("x0") ret,
3669            options(nostack, preserves_flags, readonly),
3670        );
3671    }
3672    ret
3673}
3674
3675/// Écrit le **répertoire courant** (chemin absolu) dans `buf` et retourne la
3676/// tranche d'octets correspondante, **sans** l'octet NUL terminal (`getcwd(2)`).
3677///
3678/// Le chemin est traité comme des **octets** (jamais supposé UTF-8 — Principe 3).
3679/// L'appelant fournit le buffer (zéro allocation, ADR-021 §4).
3680///
3681/// # Errors
3682///
3683/// - [`Errno::ERANGE`] si `buf` est trop petit pour contenir le chemin + NUL.
3684/// - Autres `Errno` propagés (p. ex. permissions de lecture d'un composant).
3685pub fn getcwd(buf: &mut [u8]) -> Result<&[u8], Errno> {
3686    let size = u64::try_from(buf.len()).expect("getcwd : taille de buffer dans u64");
3687    let ptr = buf.as_mut_ptr() as usize;
3688    // SAFETY: `ptr`/`size` décrivent `buf` (emprunt `&mut` valide) ; le kernel y
3689    // écrit au plus `size` octets.
3690    let ret = unsafe { raw_syscall_getcwd(ptr, size) };
3691    if ret < 0 {
3692        return Err(errno_from_negative_syscall_ret(ret));
3693    }
3694    // `getcwd(2)` rend la longueur **NUL compris** (≥ 1) ; on retourne les
3695    // octets sans le NUL final.
3696    let total = usize::try_from(ret).expect("getcwd : longueur non négative");
3697    let path_length = total
3698        .checked_sub(1)
3699        .expect("getcwd : longueur ≥ 1 (NUL terminal compris)");
3700    // TODO(ADR-029/Principe 3) : remplacer l'indexation directe `&buf[..path_length]`
3701    // par `buf.get(..path_length)` (slicing via `Option`, jamais d'indexation qui
3702    // panique). Ici `path_length < total ≤ buf.len()` ⇒ borné par construction, donc
3703    // sûr ; reste à convertir le `Option<&[u8]>` rendu en `Result` (p. ex.
3704    // `.expect("getcwd : tranche bornée par construction")` ou un bras d'erreur).
3705    Ok(&buf[..path_length])
3706}
3707
3708#[cfg(target_arch = "x86_64")]
3709#[inline]
3710unsafe fn raw_syscall_getcwd(buf: usize, size: u64) -> i64 {
3711    let ret: i64;
3712    // SAFETY: SYS_getcwd (x86_64 = 79) écrit jusqu'à `size` octets à `buf`.
3713    // Validité de `buf`/`size` : contrat de l'appelant. Pas de `readonly` (écrit
3714    // la mémoire pointée).
3715    unsafe {
3716        core::arch::asm!(
3717            "syscall",
3718            in("rax") 79_i64,
3719            in("rdi") buf,
3720            in("rsi") size,
3721            lateout("rax") ret,
3722            lateout("rcx") _,
3723            lateout("r11") _,
3724            options(nostack),
3725        );
3726    }
3727    ret
3728}
3729
3730#[cfg(target_arch = "aarch64")]
3731#[inline]
3732unsafe fn raw_syscall_getcwd(buf: usize, size: u64) -> i64 {
3733    let ret: i64;
3734    // SAFETY: SYS_getcwd (aarch64 = 17). Voir la branche x86_64.
3735    unsafe {
3736        core::arch::asm!(
3737            "svc 0",
3738            in("x8") 17_i64,
3739            in("x0") buf,
3740            in("x1") size,
3741            lateout("x0") ret,
3742            options(nostack),
3743        );
3744    }
3745    ret
3746}
3747
3748/// Renseigne les **statistiques de ressources** (`getrusage(2)`) pour la cible
3749/// [`RusageWho`] demandée.
3750///
3751/// # Errors
3752///
3753/// `getrusage(2)` ne peut échouer que sur `who` invalide (`EINVAL`) ou pointeur
3754/// invalide (`EFAULT`) — deux cas **rendus impossibles** par l'API typée
3755/// (`RusageWho` est un enum, le buffer est interne et valide). Le bras `Err` est
3756/// donc inatteignable via cette fonction (cf. `COVERAGE-EXCEPTIONS.md`,
3757/// STRUCTURAL/EFAULT-SAFE) ; la signature reste `Result` par honnêteté kernel.
3758pub fn getrusage(who: RusageWho) -> Result<Rusage, Errno> {
3759    let who_raw: i64 = match who {
3760        RusageWho::SelfProcess => 0,
3761        RusageWho::Children => -1,
3762        RusageWho::Thread => 1,
3763    };
3764    let mut usage = Rusage::default();
3765    let ptr = core::ptr::from_mut(&mut usage) as usize;
3766    // SAFETY: `ptr` désigne `usage` (local valide, taille `struct rusage`) ; le
3767    // kernel y écrit la structure. `who_raw` provient d'un enum ⇒ toujours valide.
3768    let ret = unsafe { raw_syscall_getrusage(who_raw, ptr) };
3769    if ret < 0 {
3770        return Err(errno_from_negative_syscall_ret(ret));
3771    }
3772    Ok(usage)
3773}
3774
3775#[cfg(target_arch = "x86_64")]
3776#[inline]
3777unsafe fn raw_syscall_getrusage(who: i64, usage: usize) -> i64 {
3778    let ret: i64;
3779    // SAFETY: SYS_getrusage (x86_64 = 98) écrit une `struct rusage` à `usage`.
3780    // Validité : contrat de l'appelant. Pas de `readonly` (écrit la mémoire).
3781    unsafe {
3782        core::arch::asm!(
3783            "syscall",
3784            in("rax") 98_i64,
3785            in("rdi") who,
3786            in("rsi") usage,
3787            lateout("rax") ret,
3788            lateout("rcx") _,
3789            lateout("r11") _,
3790            options(nostack),
3791        );
3792    }
3793    ret
3794}
3795
3796#[cfg(target_arch = "aarch64")]
3797#[inline]
3798unsafe fn raw_syscall_getrusage(who: i64, usage: usize) -> i64 {
3799    let ret: i64;
3800    // SAFETY: SYS_getrusage (aarch64 = 165). Voir la branche x86_64.
3801    unsafe {
3802        core::arch::asm!(
3803            "svc 0",
3804            in("x8") 165_i64,
3805            in("x0") who,
3806            in("x1") usage,
3807            lateout("x0") ret,
3808            options(nostack),
3809        );
3810    }
3811    ret
3812}
3813
3814#[cfg(test)]
3815mod tests;
3816
3817#[cfg(test)]
3818mod v1_7_runtime_libc_tests;