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// `unshare` — une seule opération, dédiée ([ADR-186] D2, [ADR-021] §3).
2316//
2317// `unshare(2)` multiplexe des usages sans rapport entre eux : espaces de noms, table
2318// des descripteurs, attributs `fs`… Aucun `unshare(flags)` générique n'est exposé.
2319// Seule l'opération qui **interroge le noyau sur le groupe de fils** l'est ; toute
2320// autre viendra, le cas échéant, avec sa propre fonction et sa propre raison.
2321//
2322// [ADR-186]: ../../../docs/adrs/ADR-186-fil-unique-avant-fork-verifie-par-le-noyau-fr.md
2323// [ADR-021]: ../../../docs/adrs/ADR-021-conventions-couche-0-fr.md
2324// ─────────────────────────────────────────────────────────────────────────
2325
2326/// `CLONE_THREAD` (`linux/sched.h`).
2327const CLONE_THREAD: u64 = 0x0001_0000;
2328
2329/// Demande au **noyau** si le processus appelant est **seul dans son groupe de fils** :
2330/// `unshare(CLONE_THREAD)` ([ADR-186] D1).
2331///
2332/// Quand l'appelant est seul, il n'y a rien à défaire : l'appel réussit et **ne change
2333/// rien**. Quand le groupe compte une autre tâche, le noyau refuse avec `EINVAL`
2334/// (`unshare(2)`, « *CLONE_THREAD … was specified in flags, and the caller is
2335/// multithreaded* »). C'est le noyau qui compte, sans lire aucun chemin : ni `/proc`,
2336/// ni Landlock n'interviennent.
2337///
2338/// **Ce que le noyau compte comme « une autre tâche »** — mesuré le 2026-09-26
2339/// (ADR-186 §1.3) :
2340/// - tout fil ordinaire vivant ;
2341/// - un fil de travail d'io_uring (`iou-wrk-…`), qui ne disparaît plus une fois créé,
2342///   même anneau fermé — alors qu'il n'exécute jamais de code utilisateur ;
2343/// - un fil qui vient de se terminer, un court instant après son `join`.
2344///
2345/// La réponse ne se trompe donc que dans un sens : elle peut dire « plusieurs » quand
2346/// aucun fil utilisateur ne vit, jamais « seul » quand il y en a un.
2347///
2348/// # Errors
2349///
2350/// - [`Errno::EINVAL`] : le groupe de fils compte au moins une autre tâche (voir
2351///   ci-dessus). C'est la réponse « non », pas une panne.
2352/// - Toute autre erreur est celle du noyau ou d'un filtre seccomp (par exemple un
2353///   filtre qui rend `EPERM`) : elle ne dit **rien** du nombre de fils.
2354///
2355/// [ADR-186]: ../../../docs/adrs/ADR-186-fil-unique-avant-fork-verifie-par-le-noyau-fr.md
2356pub fn unshare_thread_group() -> Result<(), Errno> {
2357    // SAFETY: `unshare(CLONE_THREAD)` ne lit ni n'écrit aucune mémoire utilisateur ; seul
2358    // l'entier `CLONE_THREAD` est transmis.
2359    let ret = unsafe { raw_syscall_unshare(CLONE_THREAD) };
2360    if ret < 0 {
2361        return Err(errno_from_negative_syscall_ret(ret));
2362    }
2363    Ok(())
2364}
2365
2366#[cfg(target_arch = "x86_64")]
2367#[inline]
2368unsafe fn raw_syscall_unshare(flags: u64) -> i64 {
2369    let ret: i64;
2370    // SAFETY: SYS_unshare (x86_64 = 272). Un seul argument entier, aucun pointeur :
2371    // pas d'accès mémoire utilisateur.
2372    unsafe {
2373        core::arch::asm!(
2374            "syscall",
2375            in("rax") 272_i64,
2376            in("rdi") flags,
2377            lateout("rax") ret,
2378            lateout("rcx") _,
2379            lateout("r11") _,
2380            options(nostack, preserves_flags),
2381        );
2382    }
2383    ret
2384}
2385
2386#[cfg(target_arch = "aarch64")]
2387#[inline]
2388unsafe fn raw_syscall_unshare(flags: u64) -> i64 {
2389    let ret: i64;
2390    // SAFETY: SYS_unshare (aarch64 = 97). Mêmes considérations que x86_64.
2391    unsafe {
2392        core::arch::asm!(
2393            "svc 0",
2394            in("x8") 97_i64,
2395            inout("x0") flags => ret,
2396            options(nostack, preserves_flags),
2397        );
2398    }
2399    ret
2400}
2401
2402// ─────────────────────────────────────────────────────────────────────────
2403// rlimits (sous-section 6 de family-process.md).
2404//
2405// `prlimit64` est le syscall préféré ; `getrlimit`/`setrlimit` legacy
2406// sont exposés mais routés au plus simple. La représentation kernel de
2407// `struct rlimit64` est { u64 rlim_cur ; u64 rlim_max }, identique à
2408// `struct rlimit` sur LP64. On la traduit en `Rlimit` via mapping
2409// `RLIM_INFINITY <-> RlimitValue::Infinity`.
2410// ─────────────────────────────────────────────────────────────────────────
2411
2412#[repr(C)]
2413#[derive(Default, Clone, Copy)]
2414struct KernelRlimit64 {
2415    rlim_cur: u64,
2416    rlim_max: u64,
2417}
2418
2419const RLIM_INFINITY: u64 = u64::MAX;
2420
2421fn rlim_value_to_kernel(v: RlimitValue) -> u64 {
2422    match v {
2423        RlimitValue::Finite(n) => n,
2424        RlimitValue::Infinity => RLIM_INFINITY,
2425    }
2426}
2427
2428fn rlim_value_from_kernel(n: u64) -> RlimitValue {
2429    if n == RLIM_INFINITY {
2430        RlimitValue::Infinity
2431    } else {
2432        RlimitValue::Finite(n)
2433    }
2434}
2435
2436fn rlimit_to_kernel(r: Rlimit) -> KernelRlimit64 {
2437    KernelRlimit64 {
2438        rlim_cur: rlim_value_to_kernel(r.soft),
2439        rlim_max: rlim_value_to_kernel(r.hard),
2440    }
2441}
2442
2443fn rlimit_from_kernel(k: KernelRlimit64) -> Rlimit {
2444    Rlimit {
2445        soft: rlim_value_from_kernel(k.rlim_cur),
2446        hard: rlim_value_from_kernel(k.rlim_max),
2447    }
2448}
2449
2450/// Retourne la `Rlimit` courante du processus pour `resource`.
2451///
2452/// Air recommande [`prlimit`] qui est plus expressif (cible un autre
2453/// processus, opération atomique) ; `getrlimit` est exposé pour
2454/// compatibilité.
2455///
2456/// # Errors
2457///
2458/// - [`Errno::EINVAL`] : ressource inconnue (impossible via l'API typée).
2459pub fn getrlimit(resource: Resource) -> Result<Rlimit, Errno> {
2460    // Délègue à prlimit(self, resource, None) pour ne pas dupliquer
2461    // l'asm! et bénéficier de la même conversion.
2462    prlimit(None, resource, None)
2463}
2464
2465/// Définit la `Rlimit` courante du processus pour `resource`.
2466///
2467/// # Errors
2468///
2469/// - [`Errno::EINVAL`] : valeurs incohérentes (soft > hard, …).
2470/// - [`Errno::EPERM`] : tentative de relever la hard limit sans
2471///   `CAP_SYS_RESOURCE`.
2472pub fn setrlimit(resource: Resource, limit: Rlimit) -> Result<(), Errno> {
2473    // Délègue à prlimit(self, resource, Some(limit)) ; on ignore l'ancienne
2474    // valeur retournée.
2475    let _ = prlimit(None, resource, Some(limit))?;
2476    Ok(())
2477}
2478
2479/// `prlimit64` : lit et/ou modifie atomiquement la `Rlimit` d'un
2480/// processus.
2481///
2482/// `pid = None` désigne le processus courant. Retourne **toujours** la
2483/// limite *avant* modification (équivalent kernel "old_limit").
2484///
2485/// # Errors
2486///
2487/// - [`Errno::EINVAL`] : valeurs incohérentes.
2488/// - [`Errno::EPERM`] : tentative de relever la hard limit sans
2489///   `CAP_SYS_RESOURCE`, ou ciblage d'un autre processus sans droits.
2490/// - [`Errno::ESRCH`] : `pid` n'existe pas.
2491pub fn prlimit(
2492    pid: Option<Pid>,
2493    resource: Resource,
2494    new_limit: Option<Rlimit>,
2495) -> Result<Rlimit, Errno> {
2496    let pid_arg = pid.map_or(0_i32, |p| p.as_raw());
2497
2498    let new_buf: Option<KernelRlimit64> = new_limit.map(rlimit_to_kernel);
2499    let new_ptr: u64 = match &new_buf {
2500        Some(b) => {
2501            let p: *const KernelRlimit64 = b;
2502            p as u64
2503        }
2504        None => 0,
2505    };
2506
2507    let mut old_buf = KernelRlimit64::default();
2508    let old_ptr: *mut KernelRlimit64 = &mut old_buf;
2509
2510    // SAFETY: prlimit64(2) lit `new_limit` si non-null, écrit `old_limit`
2511    // si non-null. Nous fournissons toujours `old_ptr` valide ; `new_ptr`
2512    // est null ou pointe sur `new_buf` valide pour la durée du call.
2513    let ret = unsafe { raw_syscall_prlimit64(pid_arg, resource as u32, new_ptr, old_ptr as u64) };
2514    if ret < 0 {
2515        return Err(errno_from_negative_syscall_ret(ret));
2516    }
2517    Ok(rlimit_from_kernel(old_buf))
2518}
2519
2520#[cfg(target_arch = "x86_64")]
2521#[inline]
2522unsafe fn raw_syscall_prlimit64(pid: i32, resource: u32, new_limit: u64, old_limit: u64) -> i64 {
2523    let ret: i64;
2524    // SAFETY: SYS_prlimit64 (x86_64 = 302). Le kernel lit `new_limit`
2525    // (16 octets) si non-null ; écrit `old_limit` (16 octets) si non-null.
2526    // Pas de `readonly` (écriture sur `*old_limit`).
2527    unsafe {
2528        core::arch::asm!(
2529            "syscall",
2530            in("rax") 302_i64,
2531            in("rdi") i64::from(pid),
2532            in("rsi") i64::from(resource),
2533            in("rdx") new_limit,
2534            in("r10") old_limit,
2535            lateout("rax") ret,
2536            lateout("rcx") _,
2537            lateout("r11") _,
2538            options(nostack, preserves_flags),
2539        );
2540    }
2541    ret
2542}
2543
2544#[cfg(target_arch = "aarch64")]
2545#[inline]
2546unsafe fn raw_syscall_prlimit64(pid: i32, resource: u32, new_limit: u64, old_limit: u64) -> i64 {
2547    let ret: i64;
2548    // SAFETY: SYS_prlimit64 (aarch64 = 261). Mêmes considérations.
2549    unsafe {
2550        core::arch::asm!(
2551            "svc 0",
2552            in("x8") 261_i64,
2553            inout("x0") i64::from(pid) => ret,
2554            in("x1") i64::from(resource),
2555            in("x2") new_limit,
2556            in("x3") old_limit,
2557            options(nostack, preserves_flags),
2558        );
2559    }
2560    ret
2561}
2562
2563// ─────────────────────────────────────────────────────────────────────────
2564// capget / capset (sous-section 7 de family-process.md).
2565//
2566// Version 3 capabilities (kernel ≥ 2.6.25) : header + array de 2 data
2567// structs ; chaque data struct contient `(effective, permitted,
2568// inheritable)` en `u32`. Index 0 = bits 0-31, index 1 = bits 32-63.
2569//
2570// La conversion `CapabilityMask (u64) ↔ (lo, hi)` est faite par les
2571// helpers privés `mask_to_words` / `words_to_mask` ci-dessous, testés
2572// explicitement pour qu'une éventuelle inversion lo/hi soit détectée
2573// (cf. JOURNAL session 2026-05-24).
2574// ─────────────────────────────────────────────────────────────────────────
2575
2576const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
2577
2578#[repr(C)]
2579struct KernelCapHeader {
2580    version: u32,
2581    pid: i32,
2582}
2583
2584#[repr(C)]
2585#[derive(Default, Clone, Copy)]
2586struct KernelCapData {
2587    effective: u32,
2588    permitted: u32,
2589    inheritable: u32,
2590}
2591
2592fn mask_to_words(mask: CapabilityMask) -> (u32, u32) {
2593    let bits = mask.bits();
2594    // Troncature exacte : `bits & 0xFFFF_FFFF` fit dans u32 par
2595    // construction.
2596    #[allow(clippy::cast_possible_truncation)]
2597    let lo = (bits & 0xFFFF_FFFF) as u32;
2598    // Troncature exacte : après `>> 32`, la valeur résiduelle est dans
2599    // `[0, u32::MAX]`.
2600    #[allow(clippy::cast_possible_truncation)]
2601    let hi = (bits >> 32) as u32;
2602    (lo, hi)
2603}
2604
2605fn words_to_mask(lo: u32, hi: u32) -> CapabilityMask {
2606    let bits = (u64::from(hi) << 32) | u64::from(lo);
2607    CapabilityMask::from_bits(bits)
2608}
2609
2610fn cap_target_to_kernel_pid(target: CapabilityTarget) -> i32 {
2611    match target {
2612        CapabilityTarget::CurrentThread => 0,
2613        CapabilityTarget::Thread(tid) => tid.as_raw(),
2614        CapabilityTarget::Process(pid) => pid.as_raw(),
2615    }
2616}
2617
2618/// Lit les capabilities du thread cible.
2619///
2620/// # Errors
2621///
2622/// - [`Errno::EINVAL`] : version capabilities non supportée par le kernel
2623///   (Air utilise version 3, supportée depuis 2.6.25).
2624/// - [`Errno::EPERM`] : ciblage d'un thread sans droits (rare en lecture).
2625pub fn capget(target: CapabilityTarget) -> Result<CapabilitySet, Errno> {
2626    let mut header = KernelCapHeader {
2627        version: LINUX_CAPABILITY_VERSION_3,
2628        pid: cap_target_to_kernel_pid(target),
2629    };
2630    let mut data: [KernelCapData; 2] = [KernelCapData::default(); 2];
2631    let header_ptr: *mut KernelCapHeader = &mut header;
2632    let data_ptr: *mut KernelCapData = data.as_mut_ptr();
2633    // SAFETY: capget(2) lit `*header` et écrit deux `KernelCapData`
2634    // consécutives à `data_ptr` ; les deux pointeurs sont locaux et
2635    // valides pour la durée du call.
2636    let ret = unsafe { raw_syscall_capget(header_ptr as u64, data_ptr as u64) };
2637    if ret < 0 {
2638        return Err(errno_from_negative_syscall_ret(ret));
2639    }
2640    Ok(CapabilitySet {
2641        effective: words_to_mask(data[0].effective, data[1].effective),
2642        permitted: words_to_mask(data[0].permitted, data[1].permitted),
2643        inheritable: words_to_mask(data[0].inheritable, data[1].inheritable),
2644    })
2645}
2646
2647/// Écrit les capabilities du thread cible.
2648///
2649/// # Errors
2650///
2651/// - [`Errno::EINVAL`] : version capabilities ou layout invalides.
2652/// - [`Errno::EPERM`] : tentative d'élever une capability hors permitted,
2653///   ou modification d'un autre thread sans droits.
2654pub fn capset(target: CapabilityTarget, set: &CapabilitySet) -> Result<(), Errno> {
2655    let mut header = KernelCapHeader {
2656        version: LINUX_CAPABILITY_VERSION_3,
2657        pid: cap_target_to_kernel_pid(target),
2658    };
2659    let (e_lo, e_hi) = mask_to_words(set.effective);
2660    let (p_lo, p_hi) = mask_to_words(set.permitted);
2661    let (i_lo, i_hi) = mask_to_words(set.inheritable);
2662    let data: [KernelCapData; 2] = [
2663        KernelCapData {
2664            effective: e_lo,
2665            permitted: p_lo,
2666            inheritable: i_lo,
2667        },
2668        KernelCapData {
2669            effective: e_hi,
2670            permitted: p_hi,
2671            inheritable: i_hi,
2672        },
2673    ];
2674    let header_ptr: *mut KernelCapHeader = &mut header;
2675    let data_ptr: *const KernelCapData = data.as_ptr();
2676    // SAFETY: capset(2) lit `*header` et lit deux `KernelCapData`
2677    // consécutives à `data_ptr`. Tout est local et valide pour la durée
2678    // du call.
2679    let ret = unsafe { raw_syscall_capset(header_ptr as u64, data_ptr as u64) };
2680    if ret < 0 {
2681        return Err(errno_from_negative_syscall_ret(ret));
2682    }
2683    Ok(())
2684}
2685
2686#[cfg(target_arch = "x86_64")]
2687#[inline]
2688unsafe fn raw_syscall_capget(header: u64, data: u64) -> i64 {
2689    let ret: i64;
2690    // SAFETY: SYS_capget (x86_64 = 125). Le kernel lit `*header` (8 octets)
2691    // et écrit 2 × 12 octets à `data`. Pas de `readonly` (écriture).
2692    unsafe {
2693        core::arch::asm!(
2694            "syscall",
2695            in("rax") 125_i64,
2696            in("rdi") header,
2697            in("rsi") data,
2698            lateout("rax") ret,
2699            lateout("rcx") _,
2700            lateout("r11") _,
2701            options(nostack, preserves_flags),
2702        );
2703    }
2704    ret
2705}
2706
2707#[cfg(target_arch = "x86_64")]
2708#[inline]
2709unsafe fn raw_syscall_capset(header: u64, data: u64) -> i64 {
2710    let ret: i64;
2711    // SAFETY: SYS_capset (x86_64 = 126). Lecture seule de `*header` et
2712    // `*data` côté user ; `readonly` correct.
2713    unsafe {
2714        core::arch::asm!(
2715            "syscall",
2716            in("rax") 126_i64,
2717            in("rdi") header,
2718            in("rsi") data,
2719            lateout("rax") ret,
2720            lateout("rcx") _,
2721            lateout("r11") _,
2722            options(nostack, preserves_flags, readonly),
2723        );
2724    }
2725    ret
2726}
2727
2728#[cfg(target_arch = "aarch64")]
2729#[inline]
2730unsafe fn raw_syscall_capget(header: u64, data: u64) -> i64 {
2731    let ret: i64;
2732    // SAFETY: SYS_capget (aarch64 = 90).
2733    unsafe {
2734        core::arch::asm!(
2735            "svc 0",
2736            in("x8") 90_i64,
2737            inout("x0") header => ret,
2738            in("x1") data,
2739            options(nostack, preserves_flags),
2740        );
2741    }
2742    ret
2743}
2744
2745#[cfg(target_arch = "aarch64")]
2746#[inline]
2747unsafe fn raw_syscall_capset(header: u64, data: u64) -> i64 {
2748    let ret: i64;
2749    // SAFETY: SYS_capset (aarch64 = 91). `readonly` car lecture seule.
2750    unsafe {
2751        core::arch::asm!(
2752            "svc 0",
2753            in("x8") 91_i64,
2754            inout("x0") header => ret,
2755            in("x1") data,
2756            options(nostack, preserves_flags, readonly),
2757        );
2758    }
2759    ret
2760}
2761
2762// ─────────────────────────────────────────────────────────────────────────
2763// Helper interne de conversion `errno`.
2764// ─────────────────────────────────────────────────────────────────────────
2765
2766/// Convertit une valeur de retour de syscall strictement négative en
2767/// [`Errno`]. Le kernel borne les errno à `[-4095, -1]` (`MAX_ERRNO`).
2768fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
2769    debug_assert!(ret < 0 && ret > -4096);
2770    // `ret.wrapping_neg()` est donc dans `[1, 4095]` — toujours fit in i32.
2771    #[allow(clippy::cast_possible_truncation)]
2772    let raw = ret.wrapping_neg() as i32;
2773    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
2774    Errno::from_nonzero(nz)
2775}
2776
2777// ─────────────────────────────────────────────────────────────────────────
2778// Affinité CPU — sched_setaffinity / sched_getaffinity
2779// Cf. `docs/specs/layer-0/family-process-affinity.md`.
2780// ─────────────────────────────────────────────────────────────────────────
2781
2782#[cfg(target_arch = "x86_64")]
2783#[inline]
2784unsafe fn raw_syscall_sched_setaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2785    let ret: i64;
2786    // SAFETY:
2787    // - SYS_sched_setaffinity (x86_64 = 203). Le kernel **lit** `len` octets à
2788    //   `mask` (jamais d'écriture) → `readonly` correct ; validité du buffer
2789    //   garantie par l'appelant (`CpuSet` local, 128 octets).
2790    // - x86_64 ABI : numéro en RAX, args en RDI/RSI/RDX ; retour RAX ; clobbe
2791    //   RCX/R11.
2792    unsafe {
2793        core::arch::asm!(
2794            "syscall",
2795            in("rax") 203_i64,
2796            in("rdi") i64::from(pid),
2797            in("rsi") len,
2798            in("rdx") mask,
2799            lateout("rax") ret,
2800            lateout("rcx") _,
2801            lateout("r11") _,
2802            options(nostack, preserves_flags, readonly),
2803        );
2804    }
2805    ret
2806}
2807
2808#[cfg(target_arch = "aarch64")]
2809#[inline]
2810unsafe fn raw_syscall_sched_setaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2811    let ret: i64;
2812    // SAFETY: voir version x86_64 ; aarch64 ABI : numéro en X8 (= 122), args en
2813    // X0/X1/X2, retour en X0. Le kernel **lit** `mask` → `readonly`.
2814    unsafe {
2815        core::arch::asm!(
2816            "svc 0",
2817            in("x8") 122_i64,
2818            inout("x0") i64::from(pid) => ret,
2819            in("x1") len,
2820            in("x2") mask,
2821            options(nostack, preserves_flags, readonly),
2822        );
2823    }
2824    ret
2825}
2826
2827#[cfg(target_arch = "x86_64")]
2828#[inline]
2829unsafe fn raw_syscall_sched_getaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2830    let ret: i64;
2831    // SAFETY:
2832    // - SYS_sched_getaffinity (x86_64 = 204). Le kernel **écrit** jusqu'à `len`
2833    //   octets à `mask` → **pas de `readonly`** ; validité/taille du buffer
2834    //   garanties par l'appelant (`CpuSet` local, 128 octets).
2835    // - Retourne le nombre d'octets écrits (≥ 0) ou `-errno`.
2836    unsafe {
2837        core::arch::asm!(
2838            "syscall",
2839            in("rax") 204_i64,
2840            in("rdi") i64::from(pid),
2841            in("rsi") len,
2842            in("rdx") mask,
2843            lateout("rax") ret,
2844            lateout("rcx") _,
2845            lateout("r11") _,
2846            options(nostack, preserves_flags),
2847        );
2848    }
2849    ret
2850}
2851
2852#[cfg(target_arch = "aarch64")]
2853#[inline]
2854unsafe fn raw_syscall_sched_getaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2855    let ret: i64;
2856    // SAFETY: voir version x86_64 ; aarch64 ABI : numéro en X8 (= 123), args en
2857    // X0/X1/X2, retour en X0. Le kernel **écrit** dans `mask` → pas de `readonly`.
2858    unsafe {
2859        core::arch::asm!(
2860            "svc 0",
2861            in("x8") 123_i64,
2862            inout("x0") i64::from(pid) => ret,
2863            in("x1") len,
2864            in("x2") mask,
2865            options(nostack, preserves_flags),
2866        );
2867    }
2868    ret
2869}
2870
2871/// Identifiant de tâche pour les opérations d'affinité : `None` = **tâche
2872/// appelante** (sentinelle kernel `0` typée en `Option`, ADR-021 conv. 1).
2873fn affinity_pid(tid: Option<Tid>) -> i32 {
2874    // `0` = la tâche appelante (jamais exposé comme entier magique).
2875    tid.map_or(0, Tid::as_raw)
2876}
2877
2878/// Fixe l'**affinité CPU** d'une tâche (`sched_setaffinity(2)`) : la tâche ne sera
2879/// ordonnancée que sur les CPU présents dans `cpus`. `tid = None` cible la tâche
2880/// appelante. Débloque `air-thread::cpu_affinity` (couche 1).
2881///
2882/// # Errors
2883///
2884/// - [`Errno::EINVAL`] : `cpus` ne contient **aucun** CPU autorisé/en ligne.
2885/// - [`Errno::ESRCH`] : aucune tâche ne porte ce `tid`.
2886/// - [`Errno::EPERM`] : privilèges insuffisants pour changer l'affinité de `tid`.
2887pub fn set_cpu_affinity(tid: Option<Tid>, cpus: &CpuSet) -> Result<(), Errno> {
2888    let bytes = cpus.as_bytes();
2889    let len = bytes.len() as u64;
2890    let mask = bytes.as_ptr() as u64;
2891    // SAFETY: `mask` pointe les `len` octets (= 128) du `CpuSet` de l'appelant,
2892    // valides en lecture pour la durée de l'appel ; le kernel ne les écrit pas.
2893    let ret = unsafe { raw_syscall_sched_setaffinity(affinity_pid(tid), len, mask) };
2894    if ret < 0 {
2895        return Err(errno_from_negative_syscall_ret(ret));
2896    }
2897    Ok(())
2898}
2899
2900/// Lit l'**affinité CPU** courante d'une tâche (`sched_getaffinity(2)`) **dans**
2901/// `cpus` (buffer fourni, zéro allocation). `tid = None` cible la tâche appelante.
2902///
2903/// # Errors
2904///
2905/// - [`Errno::ESRCH`] : aucune tâche ne porte ce `tid`.
2906/// - [`Errno::EINVAL`] : taille de masque incohérente (ne se produit pas avec un
2907///   `CpuSet` de [`air_sys_types::system::CPU_SETSIZE`] bits).
2908pub fn get_cpu_affinity(tid: Option<Tid>, cpus: &mut CpuSet) -> Result<(), Errno> {
2909    let len = core::mem::size_of::<CpuSet>() as u64;
2910    let mask = core::ptr::from_mut(cpus).cast::<u8>() as u64;
2911    // SAFETY: `mask` pointe les `len` octets (= 128) du `CpuSet` **mutable** de
2912    // l'appelant ; le kernel y écrit au plus `len` octets (le bitmap d'affinité).
2913    // `CpuSet` est `#[repr(C)]` sans padding ⇒ tout motif d'octets est valide.
2914    let ret = unsafe { raw_syscall_sched_getaffinity(affinity_pid(tid), len, mask) };
2915    if ret < 0 {
2916        return Err(errno_from_negative_syscall_ret(ret));
2917    }
2918    Ok(())
2919}
2920
2921// ─────────────────────────────────────────────────────────────────────────
2922// exec / redirection — execve, execveat, dup3, fchdir, chdir.
2923//
2924// Surface pilotée par le contrat de la couche 1 (`air-process`) : lancer un
2925// programme (exec), rediriger ses stdin/stdout/stderr (dup3) et fixer son
2926// répertoire de travail (fchdir / chdir). Cf. `docs/specs/layer-0/
2927// family-process.md`, section « exec / redirection ».
2928// ─────────────────────────────────────────────────────────────────────────
2929
2930/// `AT_FDCWD` : base « répertoire courant » des opérations `*at` (sentinelle
2931/// kernel `-100`, typée [`DirFd::Cwd`] côté Air, cf. ADR-021 convention 1).
2932const AT_FDCWD: i32 = -100;
2933
2934/// Convertit une [`DirFd`] en l'entier attendu par l'ABI syscall (`AT_FDCWD`
2935/// pour [`DirFd::Cwd`], sinon le fd emprunté).
2936fn dirfd_to_raw(dirfd: DirFd<'_>) -> i32 {
2937    match dirfd {
2938        DirFd::Cwd => AT_FDCWD,
2939        DirFd::Fd(fd) => fd.as_raw_fd(),
2940    }
2941}
2942
2943/// Tableau de pointeurs C terminé par `NULL`, pour le marshalling d'`argv` /
2944/// `envp` vers `execve(2)` / `execveat(2)`.
2945///
2946/// Ces syscalls attendent un argument de type `char *const argv[]` : une suite
2947/// de pointeurs vers des chaînes C **terminée par un pointeur `NULL`**. Ce type
2948/// matérialise cette **frontière de marshalling**, qui est *fuzzée*
2949/// (`fuzz/fuzz_targets/fuzz_exec_argv.rs`) : construction depuis une liste
2950/// arbitraire (vide, longue), terminaison `NULL` toujours présente, ordre
2951/// préservé.
2952///
2953/// **Rejet des NUL embarqués — par construction.** Un argument C ne peut pas
2954/// contenir d'octet NUL (il y serait interprété comme terminateur). L'entrée
2955/// étant une tranche de [`CStr`] — type qui **garantit** l'absence de NUL
2956/// interne — l'invariant est assuré *en amont*, là où l'appelant construit ses
2957/// `&CStr` (typiquement via `CStr::from_bytes_with_nul` ou `CString::new`, qui
2958/// rejettent proprement un NUL embarqué). Aucune revalidation n'est donc
2959/// nécessaire ici (Principe 4 : « parse, don't validate »).
2960///
2961/// **Allocation.** La construction alloue un [`Vec`] de `items.len() + 1`
2962/// pointeurs : c'est l'exception d'allocation documentée d'ADR-021 §4 (buffer
2963/// dynamique *intrinsèque* — sa taille dépend du nombre d'arguments, inconnu à
2964/// la compilation). Les chaînes elles-mêmes ne sont **pas** copiées : les
2965/// pointeurs empruntent les [`CStr`] fournis (d'où la durée de vie `'a`).
2966pub struct CStrArray<'a> {
2967    /// Pointeurs vers chaque `CStr`, suivis d'un pointeur `NULL` final.
2968    /// Invariant : `ptrs.len() >= 1` et `ptrs[ptrs.len() - 1].is_null()`.
2969    ptrs: Vec<*const c_char>,
2970    /// Lie la durée de vie des pointeurs à celle de la tranche source.
2971    _borrow: PhantomData<&'a [&'a CStr]>,
2972}
2973
2974impl<'a> CStrArray<'a> {
2975    /// Construit le tableau `NULL`-terminé à partir de `items`.
2976    ///
2977    /// Une tranche **vide** produit un tableau ne contenant que le `NULL` final
2978    /// (`argv = { NULL }`, soit `argc == 0`) — accepté par le kernel.
2979    #[must_use]
2980    pub fn new(items: &'a [&'a CStr]) -> Self {
2981        // `+ 1` pour le terminateur NULL. `saturating_add` par discipline
2982        // (Principe 2) ; la capacité d'une tranche ne peut pas atteindre
2983        // `usize::MAX`, donc la saturation ne se déclenche jamais.
2984        let mut ptrs = Vec::with_capacity(items.len().saturating_add(1));
2985        for item in items {
2986            ptrs.push(item.as_ptr());
2987        }
2988        ptrs.push(core::ptr::null());
2989        Self {
2990            ptrs,
2991            _borrow: PhantomData,
2992        }
2993    }
2994
2995    /// Nombre d'arguments (hors terminateur `NULL`).
2996    #[must_use]
2997    pub fn len(&self) -> usize {
2998        // Invariant : `ptrs` contient toujours le `NULL` final en sus.
2999        self.ptrs.len().saturating_sub(1)
3000    }
3001
3002    /// Vrai si la liste est vide (le tableau ne contient que le `NULL` final).
3003    #[must_use]
3004    pub fn is_empty(&self) -> bool {
3005        self.len() == 0
3006    }
3007
3008    /// Pointeur vers le tableau de pointeurs, à transmettre au syscall.
3009    #[inline]
3010    fn as_ptr(&self) -> *const *const c_char {
3011        self.ptrs.as_ptr()
3012    }
3013}
3014
3015/// Remplace l'image mémoire du processus courant par l'exécutable situé à
3016/// `path`, avec les arguments `argv` et l'environnement `envp`.
3017///
3018/// Cf. `docs/specs/layer-0/family-process.md`. En cas de **succès, ne retourne
3019/// jamais** (le processus exécute désormais le nouveau binaire) — d'où le type
3020/// `Result<Infallible, Errno>` (ADR-021 convention 5). En cas d'échec, retourne
3021/// `Err(errno)`.
3022///
3023/// `argv` / `envp` sont des tranches de [`CStr`] ; le wrapper en construit les
3024/// tableaux de pointeurs C `NULL`-terminés via [`CStrArray`]. Par convention
3025/// POSIX, `argv[0]` est le nom du programme ; une tranche vide est acceptée par
3026/// le kernel (`argc == 0`).
3027///
3028/// Pour exécuter un binaire désigné par un **fd** (sans race sur le chemin),
3029/// préférer [`execveat`] avec [`ExecveatFlags::EMPTY_PATH`].
3030///
3031/// # Errors
3032///
3033/// - [`Errno::EACCES`] : `path` n'est pas exécutable, un répertoire du chemin
3034///   interdit la traversée, ou le système de fichiers est monté `noexec`.
3035/// - [`Errno::ENOENT`] : `path` (ou l'interpréteur `#!` d'un script) n'existe
3036///   pas.
3037/// - [`Errno::ENOEXEC`] : `path` existe mais n'est pas dans un format
3038///   exécutable reconnu (ni ELF, ni script avec `#!`).
3039/// - [`Errno::E2BIG`] : `argv` + `envp` dépassent la limite kernel (`ARG_MAX`).
3040/// - `ENOMEM`, `ETXTBSY`, `ELOOP`, … : cf. `execve(2)`.
3041pub fn execve(path: &CStr, argv: &[&CStr], envp: &[&CStr]) -> Result<Infallible, Errno> {
3042    // L'allocation des tableaux de pointeurs ([`CStrArray::new`]) a lieu **ici**.
3043    // Le cœur (syscall, sans allocation) est délégué à [`execve_prepared`], que
3044    // les appelants en chemin **post-fork** invoquent directement avec des
3045    // [`CStrArray`] matérialisés **avant** le fork (async-signal-safety).
3046    let c_argv = CStrArray::new(argv);
3047    let c_envp = CStrArray::new(envp);
3048    execve_prepared(path, &c_argv, &c_envp)
3049}
3050
3051/// Variante d'[`execve`] **sans allocation** : reçoit des tableaux de pointeurs
3052/// `argv`/`envp` **déjà construits** ([`CStrArray`]).
3053///
3054/// Destinée au chemin **post-fork / pré-`execve`** : l'appelant matérialise les
3055/// [`CStrArray`] dans le parent **avant** `clone3`/`fork`, puis les passe ici
3056/// depuis l'enfant. Cette fonction n'alloue rien — seul un syscall est émis —
3057/// donc elle est sûre dans la fenêtre fork→exec où aucune allocation (verrou
3058/// allocateur potentiellement tenu par un autre thread au moment du fork) n'est
3059/// permise. [`execve`] délègue à cette fonction après avoir bâti ses tableaux.
3060///
3061/// En cas de **succès, ne retourne jamais** (image mémoire remplacée). En cas
3062/// d'échec, retourne `Err(errno)`.
3063///
3064/// # Errors
3065///
3066/// Identiques à [`execve`] (`EACCES`, `ENOENT`, `ENOEXEC`, `E2BIG`, …).
3067pub fn execve_prepared(
3068    path: &CStr,
3069    argv: &CStrArray<'_>,
3070    envp: &CStrArray<'_>,
3071) -> Result<Infallible, Errno> {
3072    // SAFETY:
3073    // - `path` est un `CStr` valide NUL-terminé (le kernel le lit jusqu'au NUL).
3074    // - `argv`/`envp` exposent chacun un tableau de pointeurs C valide, terminé
3075    //   par NULL, vivant pour toute la durée de l'appel ; les chaînes pointées
3076    //   sont les `CStr` empruntés, vivants eux aussi (durée de vie liée par
3077    //   `CStrArray<'a>`). Le kernel ne fait que **lire** ces données.
3078    // - Succès : le syscall ne revient pas (image remplacée). Échec : ret < 0.
3079    let ret = unsafe {
3080        raw_syscall_execve(
3081            path.as_ptr() as u64,
3082            argv.as_ptr() as u64,
3083            envp.as_ptr() as u64,
3084        )
3085    };
3086    // Seul un échec ramène ici : `ret` est toujours `< 0`. La conversion errno
3087    // impose elle-même `ret ∈ [-4095, -1]` (debug_assert interne).
3088    Err(errno_from_negative_syscall_ret(ret))
3089}
3090
3091#[cfg(target_arch = "x86_64")]
3092#[inline]
3093unsafe fn raw_syscall_execve(path: u64, argv: u64, envp: u64) -> i64 {
3094    let ret: i64;
3095    // SAFETY:
3096    // - SYS_execve (x86_64 = 59). Le kernel **lit** la chaîne `path` (jusqu'au
3097    //   NUL) et les tableaux `argv`/`envp` (jusqu'au pointeur NULL, puis chaque
3098    //   chaîne jusqu'à son NUL) ; il n'écrit aucune mémoire utilisateur →
3099    //   `readonly` correct. Validité garantie par l'appelant.
3100    // - Succès : ne revient pas (image remplacée). Échec : ret = -errno.
3101    // - x86_64 ABI : numéro en RAX, args en RDI/RSI/RDX ; retour RAX ; clobbe
3102    //   RCX/R11.
3103    unsafe {
3104        core::arch::asm!(
3105            "syscall",
3106            in("rax") 59_i64,
3107            in("rdi") path,
3108            in("rsi") argv,
3109            in("rdx") envp,
3110            lateout("rax") ret,
3111            lateout("rcx") _,
3112            lateout("r11") _,
3113            options(nostack, preserves_flags, readonly),
3114        );
3115    }
3116    ret
3117}
3118
3119#[cfg(target_arch = "aarch64")]
3120#[inline]
3121unsafe fn raw_syscall_execve(path: u64, argv: u64, envp: u64) -> i64 {
3122    let ret: i64;
3123    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_execve (aarch64 = 221),
3124    // numéro en X8, args en X0/X1/X2, retour en X0. Lecture seule → `readonly`.
3125    unsafe {
3126        core::arch::asm!(
3127            "svc 0",
3128            in("x8") 221_i64,
3129            inout("x0") path => ret,
3130            in("x1") argv,
3131            in("x2") envp,
3132            options(nostack, preserves_flags, readonly),
3133        );
3134    }
3135    ret
3136}
3137
3138/// Variante *at d'[`execve`] (préférée par ADR-021) : résout l'exécutable
3139/// relativement à `dirfd`, ou — avec [`ExecveatFlags::EMPTY_PATH`] et
3140/// `path == c""` — exécute **directement** le binaire désigné par `dirfd`
3141/// (un fd ouvert sur l'exécutable, typiquement avec `O_PATH`), éliminant toute
3142/// race entre l'ouverture/vérification et l'exec.
3143///
3144/// Cf. `docs/specs/layer-0/family-process.md`. Comme [`execve`], **ne retourne
3145/// jamais en cas de succès**.
3146///
3147/// # Errors
3148///
3149/// Mêmes erreurs qu'[`execve`], plus :
3150///
3151/// - [`Errno::EBADF`] : `dirfd` n'est ni `AT_FDCWD` ni un fd ouvert valide.
3152/// - [`Errno::ENOENT`] : `path` est vide sans [`ExecveatFlags::EMPTY_PATH`].
3153/// - [`Errno::ELOOP`] : composant final symbolique avec
3154///   [`ExecveatFlags::SYMLINK_NOFOLLOW`].
3155pub fn execveat(
3156    dirfd: DirFd<'_>,
3157    path: &CStr,
3158    argv: &[&CStr],
3159    envp: &[&CStr],
3160    flags: ExecveatFlags,
3161) -> Result<Infallible, Errno> {
3162    let c_argv = CStrArray::new(argv);
3163    let c_envp = CStrArray::new(envp);
3164    // SAFETY: identique à `execve` pour `path`/`argv`/`envp` (lecture seule),
3165    // plus :
3166    // - `dirfd` est `AT_FDCWD` ou un fd valide emprunté (résolution relative,
3167    //   ou exécutable direct si `EMPTY_PATH`).
3168    // - `flags` est un masque `AT_*` valide (typé `ExecveatFlags`).
3169    let ret = unsafe {
3170        raw_syscall_execveat(
3171            dirfd_to_raw(dirfd),
3172            path.as_ptr() as u64,
3173            c_argv.as_ptr() as u64,
3174            c_envp.as_ptr() as u64,
3175            flags.bits(),
3176        )
3177    };
3178    // Seul un échec ramène ici : `ret` est toujours `< 0`.
3179    Err(errno_from_negative_syscall_ret(ret))
3180}
3181
3182#[cfg(target_arch = "x86_64")]
3183#[inline]
3184unsafe fn raw_syscall_execveat(dirfd: i32, path: u64, argv: u64, envp: u64, flags: i32) -> i64 {
3185    let ret: i64;
3186    // SAFETY:
3187    // - SYS_execveat (x86_64 = 322). Lecture seule de `path`/`argv`/`envp` (cf.
3188    //   `raw_syscall_execve`) → `readonly`. `dirfd` (étendu en signe : AT_FDCWD
3189    //   = -100 ou fd valide) et `flags` sont des scalaires.
3190    // - Succès : ne revient pas. Échec : ret = -errno.
3191    // - x86_64 ABI : args en RDI/RSI/RDX/R10/R8.
3192    unsafe {
3193        core::arch::asm!(
3194            "syscall",
3195            in("rax") 322_i64,
3196            in("rdi") i64::from(dirfd),
3197            in("rsi") path,
3198            in("rdx") argv,
3199            in("r10") envp,
3200            in("r8") i64::from(flags),
3201            lateout("rax") ret,
3202            lateout("rcx") _,
3203            lateout("r11") _,
3204            options(nostack, preserves_flags, readonly),
3205        );
3206    }
3207    ret
3208}
3209
3210#[cfg(target_arch = "aarch64")]
3211#[inline]
3212unsafe fn raw_syscall_execveat(dirfd: i32, path: u64, argv: u64, envp: u64, flags: i32) -> i64 {
3213    let ret: i64;
3214    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_execveat (aarch64 = 281),
3215    // numéro en X8, args en X0..X4, retour en X0. Lecture seule → `readonly`.
3216    unsafe {
3217        core::arch::asm!(
3218            "svc 0",
3219            in("x8") 281_i64,
3220            inout("x0") i64::from(dirfd) => ret,
3221            in("x1") path,
3222            in("x2") argv,
3223            in("x3") envp,
3224            in("x4") i64::from(flags),
3225            options(nostack, preserves_flags, readonly),
3226        );
3227    }
3228    ret
3229}
3230
3231/// Duplique le descripteur `old` vers le numéro `new` (`dup3(2)`), en fermant
3232/// atomiquement `new` s'il était ouvert. Retourne le nouveau descripteur,
3233/// possédé ([`OwnedFd`], fermé au `Drop`).
3234///
3235/// Cf. `docs/specs/layer-0/family-process.md`. Variante **moderne** de `dup2`
3236/// (ADR-021) : les drapeaux sont explicites ([`Dup3Flags`], seul `O_CLOEXEC`
3237/// est valide). Cas d'usage couche 1 : rediriger les flux standard d'un enfant
3238/// (`dup3(pipe, STDOUT_FILENO, …)`) juste avant l'`exec`.
3239///
3240/// À la différence de `dup2`, `dup3` **échoue** (`EINVAL`) si `old == new` :
3241/// pas de no-op silencieux. Le numéro `new` doit donc différer de `old`.
3242///
3243/// # Errors
3244///
3245/// - [`Errno::EINVAL`] : `flags` invalides, **ou** `old == new`.
3246/// - [`Errno::EBADF`] : `new` hors de la plage autorisée (`> RLIMIT_NOFILE`).
3247/// - [`Errno::EBUSY`] : course (rare) avec `open`/`dup` concurrents sur `new`.
3248/// - [`Errno::EINTR`] : interrompu par un signal (remonté tel quel, ADR-021
3249///   convention 2).
3250pub fn dup3(old: BorrowedFd<'_>, new: RawFd, flags: Dup3Flags) -> Result<OwnedFd, Errno> {
3251    // SAFETY: `dup3(2)` ne touche aucune mémoire utilisateur ; il installe une
3252    // copie de l'entrée de table de fd `old` au numéro `new` (fermant `new` au
3253    // préalable s'il était ouvert), de manière atomique. `flags` (typé) ne peut
3254    // valoir que `O_CLOEXEC`.
3255    let ret = unsafe { raw_syscall_dup3(old.as_raw_fd(), new, flags.bits()) };
3256    if ret < 0 {
3257        return Err(errno_from_negative_syscall_ret(ret));
3258    }
3259    // Succès : `ret == new` (le numéro demandé), donc tient dans un `i32`.
3260    #[allow(clippy::cast_possible_truncation)]
3261    let new_fd = ret as RawFd;
3262    // SAFETY: le kernel vient d'installer un fd valide au numéro `new`, dont
3263    // Air prend désormais la propriété exclusive (fermé par `OwnedFd::Drop`).
3264    Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
3265}
3266
3267#[cfg(target_arch = "x86_64")]
3268#[inline]
3269unsafe fn raw_syscall_dup3(oldfd: i32, newfd: i32, flags: i32) -> i64 {
3270    let ret: i64;
3271    // SAFETY: SYS_dup3 (x86_64 = 292) ne touche aucune mémoire utilisateur
3272    // (`readonly`). Args étendus en signe (fd ≥ 0 ici ; l'extension reste
3273    // correcte). x86_64 ABI : args en RDI/RSI/RDX.
3274    unsafe {
3275        core::arch::asm!(
3276            "syscall",
3277            in("rax") 292_i64,
3278            in("rdi") i64::from(oldfd),
3279            in("rsi") i64::from(newfd),
3280            in("rdx") i64::from(flags),
3281            lateout("rax") ret,
3282            lateout("rcx") _,
3283            lateout("r11") _,
3284            options(nostack, preserves_flags, readonly),
3285        );
3286    }
3287    ret
3288}
3289
3290#[cfg(target_arch = "aarch64")]
3291#[inline]
3292unsafe fn raw_syscall_dup3(oldfd: i32, newfd: i32, flags: i32) -> i64 {
3293    let ret: i64;
3294    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_dup3 (aarch64 = 24),
3295    // numéro en X8, args en X0/X1/X2, retour en X0. Lecture seule → `readonly`.
3296    unsafe {
3297        core::arch::asm!(
3298            "svc 0",
3299            in("x8") 24_i64,
3300            inout("x0") i64::from(oldfd) => ret,
3301            in("x1") i64::from(newfd),
3302            in("x2") i64::from(flags),
3303            options(nostack, preserves_flags, readonly),
3304        );
3305    }
3306    ret
3307}
3308
3309/// Change le répertoire de travail courant du processus pour celui référencé
3310/// par le descripteur `dir` (`fchdir(2)`).
3311///
3312/// Cf. `docs/specs/layer-0/family-process.md`. Variante par fd de [`chdir`] :
3313/// pas de race sur le chemin (le répertoire est déjà ouvert et épinglé). Cas
3314/// d'usage couche 1 : fixer le `current_dir` d'un enfant à partir d'un fd
3315/// vérifié, juste avant l'`exec`.
3316///
3317/// # Errors
3318///
3319/// - [`Errno::EBADF`] : `dir` n'est pas un descripteur ouvert valide.
3320/// - [`Errno::ENOTDIR`] : `dir` ne référence pas un répertoire.
3321/// - [`Errno::EACCES`] : permission de recherche refusée sur le répertoire.
3322pub fn fchdir(dir: BorrowedFd<'_>) -> Result<(), Errno> {
3323    // SAFETY: `fchdir(2)` ne touche aucune mémoire utilisateur ; il bascule le
3324    // répertoire courant du processus vers le répertoire référencé par `dir`.
3325    let ret = unsafe { raw_syscall_fchdir(dir.as_raw_fd()) };
3326    if ret < 0 {
3327        return Err(errno_from_negative_syscall_ret(ret));
3328    }
3329    Ok(())
3330}
3331
3332#[cfg(target_arch = "x86_64")]
3333#[inline]
3334unsafe fn raw_syscall_fchdir(fd: i32) -> i64 {
3335    let ret: i64;
3336    // SAFETY: SYS_fchdir (x86_64 = 81) ne touche aucune mémoire utilisateur
3337    // (`readonly`). x86_64 ABI : argument unique en RDI.
3338    unsafe {
3339        core::arch::asm!(
3340            "syscall",
3341            in("rax") 81_i64,
3342            in("rdi") i64::from(fd),
3343            lateout("rax") ret,
3344            lateout("rcx") _,
3345            lateout("r11") _,
3346            options(nostack, preserves_flags, readonly),
3347        );
3348    }
3349    ret
3350}
3351
3352#[cfg(target_arch = "aarch64")]
3353#[inline]
3354unsafe fn raw_syscall_fchdir(fd: i32) -> i64 {
3355    let ret: i64;
3356    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_fchdir (aarch64 = 50),
3357    // numéro en X8, argument en X0, retour en X0. Lecture seule → `readonly`.
3358    unsafe {
3359        core::arch::asm!(
3360            "svc 0",
3361            in("x8") 50_i64,
3362            inout("x0") i64::from(fd) => ret,
3363            options(nostack, preserves_flags, readonly),
3364        );
3365    }
3366    ret
3367}
3368
3369/// Change le répertoire de travail courant du processus pour `path`
3370/// (`chdir(2)`).
3371///
3372/// Cf. `docs/specs/layer-0/family-process.md`. Cas d'usage couche 1 : fixer le
3373/// `current_dir` d'un enfant juste avant l'`exec`. Préférer [`fchdir`] quand un
3374/// fd de répertoire est disponible (pas de race sur le chemin).
3375///
3376/// # Errors
3377///
3378/// - [`Errno::ENOENT`] : `path` n'existe pas.
3379/// - [`Errno::ENOTDIR`] : un composant de `path` n'est pas un répertoire.
3380/// - [`Errno::EACCES`] : permission de recherche refusée.
3381/// - [`Errno::ENAMETOOLONG`] : `path` trop long.
3382pub fn chdir(path: &CStr) -> Result<(), Errno> {
3383    // SAFETY: `chdir(2)` **lit** la chaîne `path` (jusqu'au NUL) et n'écrit
3384    // aucune mémoire utilisateur → `readonly`. `path` est un `CStr` valide.
3385    let ret = unsafe { raw_syscall_chdir(path.as_ptr() as u64) };
3386    if ret < 0 {
3387        return Err(errno_from_negative_syscall_ret(ret));
3388    }
3389    Ok(())
3390}
3391
3392/// Change le **répertoire racine** du processus vers `path` (`chroot(2)`) —
3393/// **descellement additif [ADR-085](../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md)**
3394/// pour la face libc. Confinement de processus (`CAP_SYS_CHROOT` requis).
3395///
3396/// # Errors
3397///
3398/// - [`Errno::EPERM`] : `CAP_SYS_CHROOT` manquant.
3399/// - [`Errno::ENOENT`] : `path` n'existe pas.
3400/// - [`Errno::ENOTDIR`] : un composant de `path` n'est pas un répertoire.
3401pub fn chroot(path: &CStr) -> Result<(), Errno> {
3402    // SAFETY: `chroot(2)` **lit** la chaîne `path` (jusqu'au NUL) et n'écrit aucune
3403    // mémoire utilisateur → `readonly`. `path` est un `CStr` valide.
3404    let ret = unsafe { raw_syscall_chroot(path.as_ptr() as u64) };
3405    if ret < 0 {
3406        return Err(errno_from_negative_syscall_ret(ret));
3407    }
3408    Ok(())
3409}
3410
3411#[cfg(target_arch = "x86_64")]
3412#[inline]
3413unsafe fn raw_syscall_chdir(path: u64) -> i64 {
3414    let ret: i64;
3415    // SAFETY: SYS_chdir (x86_64 = 80). Le kernel **lit** la chaîne `path`
3416    // (jusqu'au NUL), validité garantie par l'appelant ; aucune écriture
3417    // utilisateur → `readonly`. x86_64 ABI : argument unique en RDI.
3418    unsafe {
3419        core::arch::asm!(
3420            "syscall",
3421            in("rax") 80_i64,
3422            in("rdi") path,
3423            lateout("rax") ret,
3424            lateout("rcx") _,
3425            lateout("r11") _,
3426            options(nostack, preserves_flags, readonly),
3427        );
3428    }
3429    ret
3430}
3431
3432#[cfg(target_arch = "aarch64")]
3433#[inline]
3434unsafe fn raw_syscall_chdir(path: u64) -> i64 {
3435    let ret: i64;
3436    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_chdir (aarch64 = 49),
3437    // numéro en X8, argument (pointeur `path`) en X0, retour en X0. Lecture
3438    // seule → `readonly`.
3439    unsafe {
3440        core::arch::asm!(
3441            "svc 0",
3442            in("x8") 49_i64,
3443            inout("x0") path => ret,
3444            options(nostack, preserves_flags, readonly),
3445        );
3446    }
3447    ret
3448}
3449
3450#[cfg(target_arch = "x86_64")]
3451#[inline]
3452unsafe fn raw_syscall_chroot(path: u64) -> i64 {
3453    let ret: i64;
3454    // SAFETY: SYS_chroot (x86_64 = 161). Le kernel **lit** la chaîne `path` (jusqu'au
3455    // NUL), validité garantie par l'appelant ; aucune écriture utilisateur → `readonly`.
3456    // x86_64 ABI : argument unique en RDI.
3457    unsafe {
3458        core::arch::asm!(
3459            "syscall",
3460            in("rax") 161_i64,
3461            in("rdi") path,
3462            lateout("rax") ret,
3463            lateout("rcx") _,
3464            lateout("r11") _,
3465            options(nostack, preserves_flags, readonly),
3466        );
3467    }
3468    ret
3469}
3470
3471#[cfg(target_arch = "aarch64")]
3472#[inline]
3473unsafe fn raw_syscall_chroot(path: u64) -> i64 {
3474    let ret: i64;
3475    // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_chroot (aarch64 = 51), numéro en
3476    // X8, argument (pointeur `path`) en X0, retour en X0. Lecture seule → `readonly`.
3477    unsafe {
3478        core::arch::asm!(
3479            "svc 0",
3480            in("x8") 51_i64,
3481            inout("x0") path => ret,
3482            options(nostack, preserves_flags, readonly),
3483        );
3484    }
3485    ret
3486}
3487
3488/// API de fuzzing **pure** (sans syscall) de la frontière de marshalling
3489/// `argv`/`envp` ([`CStrArray`]). Le harnais cargo-fuzz
3490/// (`fuzz/fuzz_targets/fuzz_exec_argv.rs`) y délègue ; l'oracle vit ici pour
3491/// accéder aux internes de [`CStrArray`] (le tableau de pointeurs) tout en
3492/// restant testé et couvert. Suit le pattern de [`crate::io_uring::fuzz_api`].
3493pub mod fuzz_api {
3494    use super::CStrArray;
3495    use alloc::ffi::CString;
3496    use alloc::vec::Vec;
3497    use core::ffi::{CStr, c_char};
3498
3499    /// Exerce la chaîne « octets bruts → `CString` → `CStrArray` » et la
3500    /// **parcourt** jusqu'au terminateur `NULL`, garantissant l'absence de
3501    /// panic / UB pour **toute** entrée (Principe 3 — frontière) :
3502    ///
3503    /// - **rejet propre des NUL embarqués** : `CString::new` rend `Err` (jamais
3504    ///   panic) dès qu'un morceau contient un octet NUL ; ces morceaux sont
3505    ///   simplement écartés ;
3506    /// - **totalité** : la construction du tableau ne panique jamais (liste
3507    ///   vide, ou très longue) ;
3508    /// - **terminaison NULL & validité** : le parcours s'arrête sur le `NULL`
3509    ///   final et relit chaque chaîne C (tout pointeur invalide ou toute
3510    ///   terminaison manquante ferait crasher sous sanitizer/libFuzzer).
3511    ///
3512    /// L'oracle *structurel* exact (ordre, exactitude des pointeurs) est
3513    /// vérifié par les tests unitaires `cstr_array_*`.
3514    pub fn marshal_exec_args(chunks: Vec<Vec<u8>>) {
3515        // Frontière 1 — NUL embarqué rejeté proprement (Err, jamais panic).
3516        let owned: Vec<CString> = chunks
3517            .iter()
3518            .filter_map(|chunk| CString::new(chunk.as_slice()).ok())
3519            .collect();
3520        let refs: Vec<&CStr> = owned.iter().map(CString::as_c_str).collect();
3521
3522        // Frontière 2 — construction du tableau de pointeurs C NULL-terminé.
3523        let arr = CStrArray::new(&refs);
3524        let _ = arr.len();
3525        let _ = arr.is_empty();
3526
3527        // Parcours jusqu'au terminateur NULL en relisant chaque chaîne : tout
3528        // écart (pointeur invalide, terminaison absente) ferait crasher la
3529        // cible libFuzzer.
3530        let base = arr.as_ptr();
3531        let mut i = 0_usize;
3532        loop {
3533            // SAFETY: `base` pointe `refs.len() + 1` pointeurs ; on s'arrête au
3534            // premier NULL (présent par construction en position `refs.len()`),
3535            // donc `i` reste borné par `refs.len()`.
3536            let ptr: *const c_char = unsafe { *base.add(i) };
3537            if ptr.is_null() {
3538                break;
3539            }
3540            // SAFETY: `ptr` non NULL ⇒ i-ème chaîne C vivante (empruntée à
3541            // `owned`), NUL-terminée par construction de `CStr`.
3542            let s = unsafe { CStr::from_ptr(ptr) };
3543            let _ = s.to_bytes();
3544            i = i.saturating_add(1);
3545        }
3546    }
3547}
3548
3549// ─────────────────────────────────────────────────────────────────────────
3550// Runtime & libc — re-sceau `couche-0-v1.7` (ADR-051)
3551//
3552// 6 syscalls ajoutés pour le runtime Air (air-rt) et l'objectif libc :
3553// `getppid`, `set_tid_address`, `sched_yield`, `umask`, `getcwd`, `getrusage`.
3554// (Le 7ᵉ, `arch_prctl`, est x86_64-only et vit dans `crate::arch`.)
3555// Conventions ADR-021 : fonction dédiée typée, `Option`/newtypes, pas de
3556// sentinelle, zéro alloc sur le happy path.
3557// ─────────────────────────────────────────────────────────────────────────
3558
3559/// Retourne le PID du **parent** du processus appelant (`getppid(2)`).
3560///
3561/// Retourne `None` lorsque le kernel rend `0`, c.-à-d. quand le parent n'est
3562/// **pas visible** dans le *PID namespace* du processus (ré-parentage hors
3563/// namespace). Le `0` du kernel — qui n'est pas un PID valide — est ainsi
3564/// re-présenté en `Option` plutôt qu'exposé comme sentinelle (ADR-021 §1).
3565#[must_use]
3566pub fn getppid() -> Option<Pid> {
3567    let raw = raw_syscall_getppid();
3568    // PID : `pid_t` (i32) ; les 32 bits hauts sont nuls pour une valeur valide.
3569    #[allow(clippy::cast_possible_truncation)]
3570    let raw32 = raw as i32;
3571    // `try_from_raw` rend `None` pour `0` (parent hors namespace).
3572    Pid::try_from_raw(raw32)
3573}
3574
3575#[cfg(target_arch = "x86_64")]
3576#[inline]
3577fn raw_syscall_getppid() -> i64 {
3578    let ret: i64;
3579    // SAFETY: SYS_getppid (x86_64 = 110) ne touche aucune mémoire utilisateur ;
3580    // clobbe RCX/R11 comme tout `syscall` x86_64.
3581    unsafe {
3582        core::arch::asm!(
3583            "syscall",
3584            in("rax") 110_i64,
3585            lateout("rax") ret,
3586            lateout("rcx") _,
3587            lateout("r11") _,
3588            options(nostack, preserves_flags, readonly),
3589        );
3590    }
3591    ret
3592}
3593
3594#[cfg(target_arch = "aarch64")]
3595#[inline]
3596fn raw_syscall_getppid() -> i64 {
3597    let ret: i64;
3598    // SAFETY: SYS_getppid (aarch64 = 173) ne touche aucune mémoire utilisateur.
3599    unsafe {
3600        core::arch::asm!(
3601            "svc 0",
3602            in("x8") 173_i64,
3603            lateout("x0") ret,
3604            options(nostack, preserves_flags, readonly),
3605        );
3606    }
3607    ret
3608}
3609
3610/// Enregistre l'adresse `clear_child_tid` du thread appelant et retourne son TID
3611/// (`set_tid_address(2)`). À la **fin du thread**, le kernel écrit `0` à cette
3612/// adresse et émet un `FUTEX_WAKE` dessus — primitif de *join* (cf.
3613/// [`clone_thread`] et la famille [`futex`](crate::futex)).
3614///
3615/// `None` désenregistre (passe `NULL`). L'appel **ne peut pas échouer** et rend
3616/// toujours le TID appelant (`> 0`).
3617///
3618/// # Safety
3619///
3620/// Si `clear_child_tid` est `Some(addr)`, le kernel **conservera `addr`** et y
3621/// écrira à la terminaison du thread. L'appelant doit garantir que la mémoire
3622/// pointée reste **valide jusqu'à la fin du thread** (sinon écriture kernel dans
3623/// de la mémoire libérée). Même discipline que [`CloneArgs::child_tid`].
3624pub unsafe fn set_tid_address(clear_child_tid: Option<&core::sync::atomic::AtomicU32>) -> Tid {
3625    let ptr = clear_child_tid.map_or(0_usize, |a| core::ptr::from_ref(a) as usize);
3626    // SAFETY: l'appel délègue ; la liveness de `ptr` jusqu'à la mort du thread
3627    // relève du contrat `unsafe` de cette fonction.
3628    let raw = unsafe { raw_syscall_set_tid_address(ptr) };
3629    // Le kernel rend le TID appelant, strictement positif.
3630    #[allow(clippy::cast_possible_truncation)]
3631    let tid_raw = raw as i32;
3632    let nz = NonZeroI32::new(tid_raw).expect("set_tid_address : TID appelant > 0");
3633    Tid::from_nonzero(nz)
3634}
3635
3636#[cfg(target_arch = "x86_64")]
3637#[inline]
3638unsafe fn raw_syscall_set_tid_address(tidptr: usize) -> i64 {
3639    let ret: i64;
3640    // SAFETY: SYS_set_tid_address (x86_64 = 218). Le kernel **enregistre**
3641    // `tidptr` pour écriture à la mort du thread ; il n'écrit pas pendant
3642    // l'appel. La validité/liveness de `tidptr` relève du contrat de l'appelant.
3643    unsafe {
3644        core::arch::asm!(
3645            "syscall",
3646            in("rax") 218_i64,
3647            in("rdi") tidptr,
3648            lateout("rax") ret,
3649            lateout("rcx") _,
3650            lateout("r11") _,
3651            options(nostack, preserves_flags),
3652        );
3653    }
3654    ret
3655}
3656
3657#[cfg(target_arch = "aarch64")]
3658#[inline]
3659unsafe fn raw_syscall_set_tid_address(tidptr: usize) -> i64 {
3660    let ret: i64;
3661    // SAFETY: SYS_set_tid_address (aarch64 = 96). Voir la branche x86_64.
3662    unsafe {
3663        core::arch::asm!(
3664            "svc 0",
3665            in("x8") 96_i64,
3666            in("x0") tidptr,
3667            lateout("x0") ret,
3668            options(nostack, preserves_flags),
3669        );
3670    }
3671    ret
3672}
3673
3674/// Cède volontairement le CPU à un autre thread prêt (`sched_yield(2)`).
3675///
3676/// Sous Linux, `sched_yield` **réussit toujours** (retour `0`) ; la fonction est
3677/// donc totale.
3678pub fn sched_yield() {
3679    let _ = raw_syscall_sched_yield();
3680}
3681
3682#[cfg(target_arch = "x86_64")]
3683#[inline]
3684fn raw_syscall_sched_yield() -> i64 {
3685    let ret: i64;
3686    // SAFETY: SYS_sched_yield (x86_64 = 24) ne touche aucune mémoire utilisateur.
3687    unsafe {
3688        core::arch::asm!(
3689            "syscall",
3690            in("rax") 24_i64,
3691            lateout("rax") ret,
3692            lateout("rcx") _,
3693            lateout("r11") _,
3694            options(nostack, preserves_flags, readonly),
3695        );
3696    }
3697    ret
3698}
3699
3700#[cfg(target_arch = "aarch64")]
3701#[inline]
3702fn raw_syscall_sched_yield() -> i64 {
3703    let ret: i64;
3704    // SAFETY: SYS_sched_yield (aarch64 = 124) ne touche aucune mémoire utilisateur.
3705    unsafe {
3706        core::arch::asm!(
3707            "svc 0",
3708            in("x8") 124_i64,
3709            lateout("x0") ret,
3710            options(nostack, preserves_flags, readonly),
3711        );
3712    }
3713    ret
3714}
3715
3716/// Fixe le **masque de création de fichiers** du processus et retourne l'ancien
3717/// (`umask(2)`). Opération **totale** (ne peut pas échouer).
3718#[must_use]
3719pub fn umask(new_mask: Mode) -> Mode {
3720    let previous = raw_syscall_umask(u64::from(new_mask));
3721    // L'ancien masque tient dans `mode_t` (12 bits significatifs) ⇒ conversion
3722    // sûre, jamais lossy (Principe 2 : pas d'`as` lossy).
3723    u32::try_from(previous).expect("umask : ancien masque dans la plage mode_t")
3724}
3725
3726#[cfg(target_arch = "x86_64")]
3727#[inline]
3728fn raw_syscall_umask(mask: u64) -> i64 {
3729    let ret: i64;
3730    // SAFETY: SYS_umask (x86_64 = 95) ne touche aucune mémoire utilisateur ;
3731    // rend l'ancien masque.
3732    unsafe {
3733        core::arch::asm!(
3734            "syscall",
3735            in("rax") 95_i64,
3736            in("rdi") mask,
3737            lateout("rax") ret,
3738            lateout("rcx") _,
3739            lateout("r11") _,
3740            options(nostack, preserves_flags, readonly),
3741        );
3742    }
3743    ret
3744}
3745
3746#[cfg(target_arch = "aarch64")]
3747#[inline]
3748fn raw_syscall_umask(mask: u64) -> i64 {
3749    let ret: i64;
3750    // SAFETY: SYS_umask (aarch64 = 166). Voir la branche x86_64.
3751    unsafe {
3752        core::arch::asm!(
3753            "svc 0",
3754            in("x8") 166_i64,
3755            in("x0") mask,
3756            lateout("x0") ret,
3757            options(nostack, preserves_flags, readonly),
3758        );
3759    }
3760    ret
3761}
3762
3763/// Écrit le **répertoire courant** (chemin absolu) dans `buf` et retourne la
3764/// tranche d'octets correspondante, **sans** l'octet NUL terminal (`getcwd(2)`).
3765///
3766/// Le chemin est traité comme des **octets** (jamais supposé UTF-8 — Principe 3).
3767/// L'appelant fournit le buffer (zéro allocation, ADR-021 §4).
3768///
3769/// # Errors
3770///
3771/// - [`Errno::ERANGE`] si `buf` est trop petit pour contenir le chemin + NUL.
3772/// - Autres `Errno` propagés (p. ex. permissions de lecture d'un composant).
3773pub fn getcwd(buf: &mut [u8]) -> Result<&[u8], Errno> {
3774    let size = u64::try_from(buf.len()).expect("getcwd : taille de buffer dans u64");
3775    let ptr = buf.as_mut_ptr() as usize;
3776    // SAFETY: `ptr`/`size` décrivent `buf` (emprunt `&mut` valide) ; le kernel y
3777    // écrit au plus `size` octets.
3778    let ret = unsafe { raw_syscall_getcwd(ptr, size) };
3779    if ret < 0 {
3780        return Err(errno_from_negative_syscall_ret(ret));
3781    }
3782    // `getcwd(2)` rend la longueur **NUL compris** (≥ 1) ; on retourne les
3783    // octets sans le NUL final.
3784    let total = usize::try_from(ret).expect("getcwd : longueur non négative");
3785    let path_length = total
3786        .checked_sub(1)
3787        .expect("getcwd : longueur ≥ 1 (NUL terminal compris)");
3788    // TODO(ADR-029/Principe 3) : remplacer l'indexation directe `&buf[..path_length]`
3789    // par `buf.get(..path_length)` (slicing via `Option`, jamais d'indexation qui
3790    // panique). Ici `path_length < total ≤ buf.len()` ⇒ borné par construction, donc
3791    // sûr ; reste à convertir le `Option<&[u8]>` rendu en `Result` (p. ex.
3792    // `.expect("getcwd : tranche bornée par construction")` ou un bras d'erreur).
3793    Ok(&buf[..path_length])
3794}
3795
3796#[cfg(target_arch = "x86_64")]
3797#[inline]
3798unsafe fn raw_syscall_getcwd(buf: usize, size: u64) -> i64 {
3799    let ret: i64;
3800    // SAFETY: SYS_getcwd (x86_64 = 79) écrit jusqu'à `size` octets à `buf`.
3801    // Validité de `buf`/`size` : contrat de l'appelant. Pas de `readonly` (écrit
3802    // la mémoire pointée).
3803    unsafe {
3804        core::arch::asm!(
3805            "syscall",
3806            in("rax") 79_i64,
3807            in("rdi") buf,
3808            in("rsi") size,
3809            lateout("rax") ret,
3810            lateout("rcx") _,
3811            lateout("r11") _,
3812            options(nostack),
3813        );
3814    }
3815    ret
3816}
3817
3818#[cfg(target_arch = "aarch64")]
3819#[inline]
3820unsafe fn raw_syscall_getcwd(buf: usize, size: u64) -> i64 {
3821    let ret: i64;
3822    // SAFETY: SYS_getcwd (aarch64 = 17). Voir la branche x86_64.
3823    unsafe {
3824        core::arch::asm!(
3825            "svc 0",
3826            in("x8") 17_i64,
3827            in("x0") buf,
3828            in("x1") size,
3829            lateout("x0") ret,
3830            options(nostack),
3831        );
3832    }
3833    ret
3834}
3835
3836/// Renseigne les **statistiques de ressources** (`getrusage(2)`) pour la cible
3837/// [`RusageWho`] demandée.
3838///
3839/// # Errors
3840///
3841/// `getrusage(2)` ne peut échouer que sur `who` invalide (`EINVAL`) ou pointeur
3842/// invalide (`EFAULT`) — deux cas **rendus impossibles** par l'API typée
3843/// (`RusageWho` est un enum, le buffer est interne et valide). Le bras `Err` est
3844/// donc inatteignable via cette fonction (cf. `COVERAGE-EXCEPTIONS.md`,
3845/// STRUCTURAL/EFAULT-SAFE) ; la signature reste `Result` par honnêteté kernel.
3846pub fn getrusage(who: RusageWho) -> Result<Rusage, Errno> {
3847    let who_raw: i64 = match who {
3848        RusageWho::SelfProcess => 0,
3849        RusageWho::Children => -1,
3850        RusageWho::Thread => 1,
3851    };
3852    let mut usage = Rusage::default();
3853    let ptr = core::ptr::from_mut(&mut usage) as usize;
3854    // SAFETY: `ptr` désigne `usage` (local valide, taille `struct rusage`) ; le
3855    // kernel y écrit la structure. `who_raw` provient d'un enum ⇒ toujours valide.
3856    let ret = unsafe { raw_syscall_getrusage(who_raw, ptr) };
3857    if ret < 0 {
3858        return Err(errno_from_negative_syscall_ret(ret));
3859    }
3860    Ok(usage)
3861}
3862
3863#[cfg(target_arch = "x86_64")]
3864#[inline]
3865unsafe fn raw_syscall_getrusage(who: i64, usage: usize) -> i64 {
3866    let ret: i64;
3867    // SAFETY: SYS_getrusage (x86_64 = 98) écrit une `struct rusage` à `usage`.
3868    // Validité : contrat de l'appelant. Pas de `readonly` (écrit la mémoire).
3869    unsafe {
3870        core::arch::asm!(
3871            "syscall",
3872            in("rax") 98_i64,
3873            in("rdi") who,
3874            in("rsi") usage,
3875            lateout("rax") ret,
3876            lateout("rcx") _,
3877            lateout("r11") _,
3878            options(nostack),
3879        );
3880    }
3881    ret
3882}
3883
3884#[cfg(target_arch = "aarch64")]
3885#[inline]
3886unsafe fn raw_syscall_getrusage(who: i64, usage: usize) -> i64 {
3887    let ret: i64;
3888    // SAFETY: SYS_getrusage (aarch64 = 165). Voir la branche x86_64.
3889    unsafe {
3890        core::arch::asm!(
3891            "svc 0",
3892            in("x8") 165_i64,
3893            in("x0") who,
3894            in("x1") usage,
3895            lateout("x0") ret,
3896            options(nostack),
3897        );
3898    }
3899    ret
3900}
3901
3902#[cfg(test)]
3903mod tests;
3904
3905#[cfg(test)]
3906mod v1_7_runtime_libc_tests;