air_sys_syscall/process.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Wrappers de la famille `process`.
6//!
7//! Cf. `docs/specs/layer-0/family-process.md`. Couvre l'identité (`getpid`,
8//! `gettid`), la création/attente (`clone3`, `waitid`, `exit_group`), l'`exec`
9//! et la redirection (`execve`, `execveat`, `dup3`, `fchdir`, `chdir`), les
10//! groupes/sessions, `prctl` (opérations individuelles), les rlimits, les
11//! capabilities et l'affinité CPU.
12//!
13//! Les wrappers exécutent les syscalls Linux directement via
14//! `core::arch::asm!` ; la libc n'est pas dans le chemin d'appel
15//! (cf. CLAUDE.md : « variantes modernes préférées, syscalls non wrappés
16//! listés dans UNSUPPORTED.md »).
17
18use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
19use alloc::vec::Vec;
20use core::convert::Infallible;
21use core::ffi::c_char;
22use core::marker::PhantomData;
23use core::num::NonZeroI32;
24
25use alloc::ffi::CString;
26use core::ffi::CStr;
27
28use air_sys_types::fs::{DirFd, Mode};
29use air_sys_types::system::CpuSet;
30use air_sys_types::{
31 Capability, CapabilityMask, CapabilitySet, CapabilityTarget, CloneArgs, CloneFlags,
32 CloneResult, DumpableMode, Dup3Flags, Errno, ExecveatFlags, Gid, Pid, PidFd, PidFdOpenFlags,
33 ResGid, ResUid, Resource, Rlimit, RlimitValue, Rusage, RusageWho, Signal, SignalInfo,
34 StackSpecification, Tid, Uid, WaitEvent, WaitOptions, WaitStatus, WaitTarget,
35};
36
37#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
38compile_error!(
39 "air-sys-syscall ne supporte que x86_64 et aarch64 (cf. ADR-014 catalogue matériel)."
40);
41
42/// Retourne le PID du processus appelant.
43///
44/// Fonction **totale** : le syscall `getpid(2)` ne peut pas échouer et POSIX
45/// garantit que le PID retourné est strictement positif. C'est pourquoi la
46/// signature retourne `Pid` directement, sans `Result`.
47///
48/// # Performance
49///
50/// Très peu coûteux (~30-50 ns via vDSO sur les kernels récents) ; ici on
51/// appelle le syscall en direct sans passer par la vDSO, donc on est du
52/// côté haut de cette fourchette.
53#[must_use]
54pub fn getpid() -> Pid {
55 let raw = raw_syscall_getpid();
56 // POSIX/Linux : `getpid` retourne un `pid_t` (i32) strictement positif.
57 // La troncature de i64 à i32 est intentionnelle ; les 32 bits hauts sont
58 // garantis nuls pour un PID valide.
59 #[allow(clippy::cast_possible_truncation)]
60 let pid_raw = raw as i32;
61 // Construction *vérifiée* du `NonZeroI32` : un `new_unchecked` ici
62 // (Principe 5 : pas d'optimisation avant mesure, et plus grave : si l'asm!
63 // au-dessus est jamais mal câblé, `new_unchecked(0)` est un UB silencieux,
64 // alors qu'un `.expect()` rend la violation d'invariant bruyante et
65 // diagnostiquable). La branche d'échec n'est pas exécutable sur un kernel
66 // POSIX conforme.
67 let nz = NonZeroI32::new(pid_raw)
68 .expect("kernel a violé l'invariant POSIX : getpid doit retourner > 0");
69 Pid::from_nonzero(nz)
70}
71
72/// Retourne le TID du thread appelant.
73///
74/// Fonction **totale** : le syscall `gettid(2)` ne peut pas échouer et le
75/// kernel garantit un TID strictement positif. Sur Linux, chaque thread a un
76/// TID distinct ; le thread principal d'un processus a `gettid() == getpid()`.
77///
78/// Le type de retour [`Tid`] est **distinct** de [`Pid`] pour empêcher par
79/// typage la confusion entre identifiant de thread et identifiant de
80/// processus (cf. ADR-021 convention 1, et Principe d'ingénierie 7).
81#[must_use]
82pub fn gettid() -> Tid {
83 let raw = raw_syscall_gettid();
84 // Même invariant que getpid : TID strictement positif, fits in i32.
85 #[allow(clippy::cast_possible_truncation)]
86 let tid_raw = raw as i32;
87 // Construction *vérifiée* (cf. justification dans `getpid` ci-dessus).
88 let nz = NonZeroI32::new(tid_raw)
89 .expect("kernel a violé l'invariant POSIX : gettid doit retourner > 0");
90 Tid::from_nonzero(nz)
91}
92
93// ── Helpers spécifiques à l'architecture ─────────────────────────────────
94//
95// Une fonction par couple (syscall, architecture). Pas de wrapper générique
96// (cf. convention 3 ADR-021 : pas de wrapper générique pour les syscalls
97// multiplexés ; ici les helpers sont privés et chaque syscall a sa propre
98// fonction dédiée, donc l'esprit de la convention est respecté).
99
100#[cfg(target_arch = "x86_64")]
101#[inline]
102fn raw_syscall_getpid() -> i64 {
103 let ret: i64;
104 // SAFETY:
105 // - SYS_getpid (x86_64 = 39) ne touche à aucune mémoire utilisateur.
106 // - L'ABI syscall x86_64 prend le numéro de syscall dans RAX, retourne
107 // en RAX, et clobbe RCX (RIP de retour) et R11 (RFLAGS sauvegardé).
108 // Les autres registres caller-saved sont préservés.
109 // - `nostack` est correct car le syscall n'utilise pas la pile.
110 // - `readonly` est correct car ce syscall ne modifie aucune mémoire
111 // accessible au programme.
112 // - `preserves_flags` est correct car le kernel restaure RFLAGS via R11
113 // au retour (le clobber explicite de R11 ci-dessus le couvre).
114 unsafe {
115 core::arch::asm!(
116 "syscall",
117 in("rax") 39_i64,
118 lateout("rax") ret,
119 lateout("rcx") _,
120 lateout("r11") _,
121 options(nostack, preserves_flags, readonly),
122 );
123 }
124 ret
125}
126
127#[cfg(target_arch = "x86_64")]
128#[inline]
129fn raw_syscall_gettid() -> i64 {
130 let ret: i64;
131 // SAFETY: identique à `raw_syscall_getpid` ci-dessus, avec SYS_gettid
132 // (x86_64 = 186) à la place. Aucune mémoire utilisateur n'est touchée.
133 unsafe {
134 core::arch::asm!(
135 "syscall",
136 in("rax") 186_i64,
137 lateout("rax") ret,
138 lateout("rcx") _,
139 lateout("r11") _,
140 options(nostack, preserves_flags, readonly),
141 );
142 }
143 ret
144}
145
146#[cfg(target_arch = "aarch64")]
147#[inline]
148fn raw_syscall_getpid() -> i64 {
149 let ret: i64;
150 // SAFETY:
151 // - SYS_getpid (aarch64 = 172) ne touche à aucune mémoire utilisateur.
152 // - L'ABI syscall aarch64 prend le numéro dans X8 et retourne en X0.
153 // Les registres caller-saved en dehors de X0 sont préservés.
154 // - `nostack`, `readonly`, `preserves_flags` : mêmes justifications que
155 // pour la branche x86_64.
156 unsafe {
157 core::arch::asm!(
158 "svc 0",
159 in("x8") 172_i64,
160 lateout("x0") ret,
161 options(nostack, preserves_flags, readonly),
162 );
163 }
164 ret
165}
166
167#[cfg(target_arch = "aarch64")]
168#[inline]
169fn raw_syscall_gettid() -> i64 {
170 let ret: i64;
171 // SAFETY: identique à `raw_syscall_getpid` ci-dessus, avec SYS_gettid
172 // (aarch64 = 178) à la place. Aucune mémoire utilisateur n'est touchée.
173 unsafe {
174 core::arch::asm!(
175 "svc 0",
176 in("x8") 178_i64,
177 lateout("x0") ret,
178 options(nostack, preserves_flags, readonly),
179 );
180 }
181 ret
182}
183
184// ─────────────────────────────────────────────────────────────────────────
185// clone3
186// ─────────────────────────────────────────────────────────────────────────
187
188/// Structure d'arguments transmise au kernel (`struct clone_args` de
189/// `linux/sched.h`). Représente le ABI Linux 5.7+ (88 octets).
190///
191/// Les champs sont des `u64` même quand ils encodent des pointeurs (les
192/// adresses kernel n'utilisent que les premiers 48 bits sur x86_64/aarch64,
193/// mais l'ABI réserve 64 bits pour forward-compat).
194#[repr(C)]
195#[derive(Default)]
196struct KernelCloneArgs {
197 flags: u64,
198 pidfd: u64,
199 child_tid: u64,
200 parent_tid: u64,
201 exit_signal: u64,
202 stack: u64,
203 stack_size: u64,
204 tls: u64,
205 set_tid: u64,
206 set_tid_size: u64,
207 cgroup: u64,
208}
209
210/// Construit la structure ABI kernel à partir de [`CloneArgs`] Air.
211///
212/// Extrait en fonction privée pour pouvoir tester unitairement le mapping
213/// `exit_signal` (notamment la branche `None` qui ne peut pas être
214/// observée bout-en-bout via fork — le kernel détache l'enfant quand
215/// `exit_signal = 0` et `waitid` retourne alors `ECHILD`).
216fn build_kernel_clone_args(args: &CloneArgs, pidfd_addr: u64) -> KernelCloneArgs {
217 let exit_signal_u64: u64 = args.exit_signal.map_or(0, |sig| {
218 // sig.as_raw() > 0 par construction (NonZeroI32) → cast sign-safe.
219 #[allow(clippy::cast_sign_loss)]
220 {
221 sig.as_raw() as u64
222 }
223 });
224
225 // Pile : `None` → 0/0 (fork classique) ; `Some` → base + taille (thread).
226 // `clone3` valide que `stack == None` (fork) ; `clone_thread` exige `Some`.
227 let (stack_addr, stack_size) = match args.stack {
228 Some(s) => (s.addr as u64, s.size as u64),
229 None => (0, 0),
230 };
231
232 KernelCloneArgs {
233 flags: args.flags.bits(),
234 pidfd: pidfd_addr,
235 child_tid: args.child_tid.map_or(0, |r| r.as_ptr() as u64),
236 parent_tid: args.parent_tid.map_or(0, |r| r.as_ptr() as u64),
237 exit_signal: exit_signal_u64,
238 stack: stack_addr,
239 stack_size,
240 tls: args.tls.unwrap_or(0),
241 set_tid: 0,
242 set_tid_size: 0,
243 cgroup: 0,
244 }
245}
246
247/// Crée un nouveau processus via le syscall `clone3(2)`.
248///
249/// Cf. `docs/specs/layer-0/family-process.md` section `clone3`. La fonction
250/// retourne **deux fois** en cas de succès : une fois côté parent (avec le
251/// PID enfant, et un `PidFd` si [`CloneFlags::PIDFD`] était positionné),
252/// une fois côté enfant (avec [`CloneResult::Child`]). Le `match` sur le
253/// résultat est le seul moyen sûr de discriminer.
254///
255/// # Périmètre
256///
257/// `clone3` est dédié à la **création de processus** style fork ; la création
258/// de **thread** passe par [`clone_thread`] (modèle de retour distinct, cf. sa
259/// doc et la note d'implémentation au-dessus de `clone_thread`) :
260///
261/// - `args.flags` peut contenir tous les bits hors [`CloneFlags::VM`] /
262/// [`CloneFlags::THREAD`] / [`CloneFlags::SIGHAND`] : ces drapeaux de
263/// création de thread sont refusés ici avec [`Errno::EINVAL`] (leur retour
264/// `CloneResult::Child` serait *unsound* sur une pile neuve — voir
265/// `clone_thread`).
266/// - `args.stack` doit être `None` (refusé sinon avec [`Errno::EINVAL`] ; une
267/// pile dédiée n'a de sens que pour un thread → `clone_thread`).
268/// - `args.exit_signal` est typiquement `Some(Signal::SIGCHLD)`.
269///
270/// # Safety
271///
272/// `clone3` peut créer un thread partageant la mémoire avec le parent
273/// (`CLONE_VM`), situation que Rust ne peut pas vérifier. L'appelant doit
274/// donc garantir, selon le scénario :
275///
276/// - **Fork classique** (sans `CLONE_VM`) — aucune précondition
277/// particulière sur la mémoire. L'enfant possède une copie CoW de
278/// l'espace d'adressage du parent ; tous les invariants Rust restent
279/// valables des deux côtés.
280///
281/// - **Création de thread** (`CLONE_VM | CLONE_THREAD | CLONE_SIGHAND`)
282/// — non géré par `clone3` ; l'appel échoue avec [`Errno::EINVAL`]. Utiliser
283/// [`clone_thread`], qui exécute le thread via un trampoline assembleur (pas
284/// de retour Rust sur pile neuve) et expose le *join* par
285/// `CLONE_CHILD_CLEARTID`.
286///
287/// - **Namespaces** (`CLONE_NEWUSER`, `CLONE_NEWNS`, `CLONE_NEWPID`, …)
288/// — l'appelant doit posséder les capabilities nécessaires
289/// (typiquement `CAP_SYS_ADMIN`) ; à défaut, le syscall retourne
290/// [`Errno::EPERM`]. Sur Ubuntu 24.04, les user namespaces non
291/// privilégiés sont restreints par AppArmor
292/// (`kernel.apparmor_restrict_unprivileged_userns=1`) et l'appel
293/// échoue avec [`Errno::EPERM`] même côté unprivileged.
294///
295/// - **« Retourne deux fois »** — après succès, le syscall est exécuté
296/// côté parent **et** côté enfant ; les deux retours apparaissent ici
297/// comme `Ok(_)` distincts. L'enfant doit terminer son travail
298/// (typiquement par `std::process::exit` ou un futur `exit_group`
299/// wrapper) ; revenir « plus loin » dans le code appelant produit
300/// généralement un comportement surprenant pour l'utilisateur.
301///
302/// # Errors
303///
304/// - [`Errno::EINVAL`] : combinaison de flags refusée par cette PR
305/// (création de thread) ou par le kernel (configuration invalide).
306/// - [`Errno::EPERM`] : capabilities insuffisantes (NEWUSER refusé,
307/// AppArmor, …).
308/// - `EAGAIN` (constante non encore définie dans le stub partiel `Errno`)
309/// : limites système atteintes (nombre de processus).
310/// - `ENOMEM` (idem) : mémoire kernel insuffisante.
311/// - `EUSERS` (idem) : trop de namespaces utilisateurs créés.
312/// - `ENOSYS` (idem) : kernel antérieur à 5.3 (`clone3` indisponible).
313///
314/// Les constantes nommées en prose ci-dessus seront promues en
315/// `pub const` au fil des PRs qui les rencontrent réellement
316/// (cf. note « stub partiel » en tête du module `errno`).
317pub unsafe fn clone3(args: &CloneArgs) -> Result<CloneResult, Errno> {
318 // Refus explicite des cas non testés (Principe 4 : valider en amont).
319 if args.stack.is_some() {
320 return Err(Errno::EINVAL);
321 }
322 if args
323 .flags
324 .intersects(CloneFlags::VM | CloneFlags::THREAD | CloneFlags::SIGHAND)
325 {
326 return Err(Errno::EINVAL);
327 }
328
329 let want_pidfd = args.flags.contains(CloneFlags::PIDFD);
330 let mut pidfd_storage: i32 = -1;
331
332 let pidfd_addr: u64 = if want_pidfd {
333 // Adresse du i32 que le kernel renseignera côté parent en cas de succès.
334 let ptr: *mut i32 = &mut pidfd_storage;
335 ptr as u64
336 } else {
337 0
338 };
339
340 let kernel_args = build_kernel_clone_args(args, pidfd_addr);
341
342 let args_ptr: *const KernelCloneArgs = &kernel_args;
343 // size_of::<KernelCloneArgs>() = 88. Air ne cible que des cibles
344 // 64-bit (cf. ADR-014) ; `usize == u64`, donc le cast est exact.
345 #[allow(clippy::cast_lossless)]
346 let args_size: u64 = core::mem::size_of::<KernelCloneArgs>() as u64;
347
348 // SAFETY:
349 // - args_ptr pointe sur `kernel_args` (local valide pour toute la fonction).
350 // - args_size est exactement size_of::<KernelCloneArgs>().
351 // - Si CLONE_PIDFD est positionné, pidfd_addr pointe sur `pidfd_storage`
352 // qui est local valide ; le kernel y écrit côté parent uniquement.
353 // - Aucune autre adresse mémoire utilisateur n'est passée au kernel
354 // (parent_tid/child_tid/stack/set_tid/cgroup sont à 0).
355 // - Sémantique « retourne deux fois » : voir doc de la fonction.
356 let ret = unsafe { raw_syscall_clone3(args_ptr as u64, args_size) };
357
358 if ret < 0 {
359 return Err(errno_from_negative_syscall_ret(ret));
360 }
361
362 if ret == 0 {
363 // Côté enfant : ne pas lire `pidfd_storage` (le kernel n'a écrit que
364 // côté parent ; la copie CoW du child contient la valeur initiale).
365 return Ok(CloneResult::Child);
366 }
367
368 // Côté parent : `ret` est le PID enfant, borné par `PID_MAX_LIMIT`
369 // (= 2^22 sur 64-bit Linux), bien en deçà de `i32::MAX`. Troncature
370 // intentionnelle, sûre par construction.
371 #[allow(clippy::cast_possible_truncation)]
372 let child_pid_raw = ret as i32;
373 let child_pid = Pid::try_from_raw(child_pid_raw)
374 .expect("clone3 retour parent : PID strictement positif (POSIX)");
375
376 let child_pidfd = if want_pidfd {
377 let raw_fd = pidfd_storage;
378 // SAFETY: le kernel a écrit un fd valide dans `pidfd_storage` côté
379 // parent après succès de clone3 avec CLONE_PIDFD. Air en prend
380 // ownership ; OwnedFd::Drop fermera le fd.
381 let owned = unsafe { OwnedFd::from_raw_fd(raw_fd) };
382 Some(PidFd::from_owned_fd(owned))
383 } else {
384 None
385 };
386
387 Ok(CloneResult::Parent {
388 child_pid,
389 child_pidfd,
390 })
391}
392
393#[cfg(target_arch = "x86_64")]
394#[inline]
395unsafe fn raw_syscall_clone3(args_ptr: u64, args_size: u64) -> i64 {
396 let ret: i64;
397 // SAFETY:
398 // - SYS_clone3 (x86_64 = 435). Le kernel lit `args_size` octets à
399 // l'adresse `args_ptr` (validité garantie par l'appelant) ; il peut
400 // écrire à des sous-pointeurs (pidfd/parent_tid/child_tid) si
401 // demandés. L'appelant a respecté ces préconditions.
402 // - L'ABI syscall x86_64 prend le numéro dans RAX, arg1/arg2 dans
403 // RDI/RSI ; retourne en RAX ; clobbe RCX (RIP) et R11 (RFLAGS).
404 // - **Pas de `readonly`** : clone3 écrit dans la mémoire utilisateur
405 // (output pointers) ; `readonly` serait incorrect ici.
406 // - Sémantique « retourne deux fois » : le syscall revient côté
407 // parent (rax = pid > 0) et côté enfant (rax = 0). Les deux
408 // retours sortent normalement de cette fonction.
409 unsafe {
410 core::arch::asm!(
411 "syscall",
412 in("rax") 435_i64,
413 in("rdi") args_ptr,
414 in("rsi") args_size,
415 lateout("rax") ret,
416 lateout("rcx") _,
417 lateout("r11") _,
418 options(nostack, preserves_flags),
419 );
420 }
421 ret
422}
423
424#[cfg(target_arch = "aarch64")]
425#[inline]
426unsafe fn raw_syscall_clone3(args_ptr: u64, args_size: u64) -> i64 {
427 let ret: i64;
428 // SAFETY: voir version x86_64 ; aarch64 syscall ABI : numéro en X8,
429 // arg1/arg2 en X0/X1, retour en X0. svc 0 effectue le syscall.
430 // Comme x86_64, **pas de `readonly`** (clone3 écrit en mémoire user).
431 unsafe {
432 core::arch::asm!(
433 "svc 0",
434 in("x8") 435_i64,
435 inout("x0") args_ptr => ret,
436 in("x1") args_size,
437 options(nostack, preserves_flags),
438 );
439 }
440 ret
441}
442
443// ─────────────────────────────────────────────────────────────────────────
444// clone_thread — création d'un thread (CLONE_VM) via clone3 + trampoline asm.
445//
446// Pourquoi une fonction DISTINCTE de `clone3` (et non un simple
447// `CloneResult::Child` étendu) ? Parce que le modèle « clone3 retourne deux
448// fois dans du Rust » est **unsound** pour un thread doté d'une pile NEUVE :
449// après le syscall, l'enfant reprend l'exécution avec `sp` = sommet de la pile
450// fournie. Laisser le code Rust « revenir » (épilogue de fonction : restauration
451// de registres callee-saved + `ret`/`blr x30`) lirait des **ordures** sur cette
452// pile vierge → crash. La discipline universelle (glibc/musl) est donc :
453// l'enfant ne **revient jamais** dans le code Rust appelant ; un trampoline en
454// assembleur appelle directement un point d'entrée `extern "C"` fourni, puis
455// termine le thread via `SYS_exit`. C'est ce que fait `clone_thread`.
456//
457// `clone3` reste, lui, dédié au **fork** (pas de pile neuve, l'enfant partage la
458// copie CoW de la pile parent : `CloneResult::Child` y est sound) et **rejette**
459// `stack`/`CLONE_VM` en amont.
460// ─────────────────────────────────────────────────────────────────────────
461
462/// Taille minimale (octets) acceptée pour la pile d'un thread `clone_thread`.
463/// Une page suffit largement au point d'entrée minimal ; les piles réelles sont
464/// dimensionnées par l'appelant (la libc Air, couche 1).
465const MIN_THREAD_STACK_SIZE: usize = 4096;
466
467/// Crée un **thread** (mémoire partagée, `CLONE_VM`) via `clone3(2)`.
468///
469/// Contrairement à [`clone3`] (réservé au fork), `clone_thread` ne « retourne
470/// pas deux fois » : le nouveau thread exécute le point d'entrée `entry(arg)`
471/// **sur sa pile dédiée** via un trampoline assembleur, puis — si `entry`
472/// revient — termine le **seul** thread courant par `SYS_exit` (jamais
473/// `exit_group`). Seul le **parent** revient de cette fonction, avec le [`Tid`]
474/// du nouveau thread.
475///
476/// # Join (terminaison observable)
477///
478/// Les threads `CLONE_THREAD` ne sont **pas** *waitable* via `waitid`. Le *join*
479/// se fait par `CLONE_CHILD_CLEARTID` : positionner [`CloneFlags::CHILD_CLEARTID`]
480/// et renseigner [`CloneArgs::child_tid`] sur un mot [`core::sync::atomic::AtomicU32`]
481/// partagé. À la mort du thread, le kernel remet ce mot à `0` et émet un
482/// `FUTEX_WAKE` (de portée **partagée** — sans `FUTEX_PRIVATE_FLAG`) dessus. Pour
483/// initialiser le mot avec le TID **sans course**, ajouter
484/// [`CloneFlags::PARENT_SETTID`] avec [`CloneArgs::parent_tid`] sur le **même**
485/// mot (écrit par le kernel, côté parent, avant le retour). Le parent joint
486/// alors en bouclant `futex_wait` jusqu'à lire `0` (cf. la famille
487/// [`futex`](crate::futex)).
488///
489/// # Errors
490///
491/// - [`Errno::EINVAL`] si la validation amont échoue : `stack` absent,
492/// [`CloneFlags::VM`] absent, adresse de pile nulle, taille `< 4096`, `addr`
493/// ou `size` non alignés sur 16, ou sommet de pile en débordement. Le kernel
494/// peut aussi rendre `EINVAL` pour une combinaison de drapeaux incohérente
495/// (p. ex. `CLONE_THREAD` avec un `exit_signal`).
496/// - `EAGAIN`/`ENOMEM` (stub partiel `Errno`) : limites système / mémoire kernel.
497///
498/// # Safety
499///
500/// Hautement `unsafe`. L'appelant doit garantir :
501///
502/// - **Pile.** `stack` désigne une région mémoire valide, possédée, exclusivement
503/// réservée au nouveau thread, d'au moins `size` octets, et **non libérée**
504/// tant que le thread n'est pas joint (sinon use-after-free de la pile).
505/// - **Point d'entrée.** `entry` ne doit exécuter que des opérations sûres dans
506/// ce contexte naissant : **aucune** variable `thread_local` (sauf
507/// [`CloneArgs::tls`] fourni avec un bloc TLS valide via [`CloneFlags::SETTLS`]),
508/// pas d'`unwind`/panic traversant le trampoline, pas de dépendance à un état
509/// d'initialisation propre au thread. Les écritures en mémoire **partagée**
510/// (VM commune) doivent être thread-safe (`Atomic*`).
511/// - **`arg`.** Sa signification est définie par `entry` ; s'il encode un
512/// pointeur, la cible doit rester vivante et correctement synchronisée.
513/// - **Join.** `child_tid`/`parent_tid` (si fournis) pointent un `AtomicU32`
514/// vivant jusqu'à la fin du *join*.
515pub unsafe fn clone_thread(
516 args: &CloneArgs,
517 entry: extern "C" fn(usize),
518 arg: usize,
519) -> Result<Tid, Errno> {
520 // ── Validation amont (Principe 4) ───────────────────────────────────
521 let stack: StackSpecification = args.stack.ok_or(Errno::EINVAL)?;
522 // Un thread partage l'espace d'adressage : CLONE_VM est obligatoire (sinon
523 // la pile neuve ne sert à rien et le join par mot partagé est impossible).
524 if !args.flags.contains(CloneFlags::VM) {
525 return Err(Errno::EINVAL);
526 }
527 if stack.addr.is_null() {
528 return Err(Errno::EINVAL);
529 }
530 if stack.size < MIN_THREAD_STACK_SIZE {
531 return Err(Errno::EINVAL);
532 }
533 let base = stack.addr.addr();
534 // Alignement 16 de la base ET de la taille ⇒ sommet (`base + size`) aligné 16
535 // (exigence ABI x86_64/aarch64 au point d'entrée). `& 0xF` : pas d'arithmétique.
536 if base & 0xF != 0 || stack.size & 0xF != 0 {
537 return Err(Errno::EINVAL);
538 }
539 // Sommet de pile sans débordement d'adresse (Principe 2 : pas d'« + » nu).
540 base.checked_add(stack.size).ok_or(Errno::EINVAL)?;
541
542 // Un thread n'a pas de pidfd : pidfd_addr = 0 (CLONE_PIDFD non pris en charge
543 // par ce chemin ; il appartient au fork).
544 let kernel_args = build_kernel_clone_args(args, 0);
545 let args_ptr: *const KernelCloneArgs = &kernel_args;
546 // size_of::<KernelCloneArgs>() = 88 ; cibles 64-bit uniquement (ADR-014).
547 #[allow(clippy::cast_lossless)]
548 let args_size: u64 = core::mem::size_of::<KernelCloneArgs>() as u64;
549
550 // SAFETY:
551 // - `args_ptr` pointe `kernel_args` (local vivant pour toute la fonction) ;
552 // `args_size` est exactement sa taille. Le kernel lit cette structure et,
553 // via `child_tid`/`parent_tid`, peut écrire les mots `AtomicU32` fournis
554 // (validité garantie par le contrat `unsafe` de cette fonction).
555 // - Le côté ENFANT ne revient pas ici (le trampoline appelle `entry(arg)`
556 // sur la pile neuve puis `SYS_exit`) : aucun retour Rust sur pile vierge.
557 // - Le côté PARENT reçoit `ret` (TID > 0, ou -errno) et le rend normalement.
558 let ret = unsafe { raw_syscall_clone3_thread(args_ptr as u64, args_size, entry, arg) };
559
560 if ret < 0 {
561 return Err(errno_from_negative_syscall_ret(ret));
562 }
563 // `ret == 0` est inobservable ici (l'enfant ne revient jamais dans ce code).
564 // Le parent reçoit le TID enfant, borné par `PID_MAX_LIMIT` ≪ `i32::MAX`.
565 #[allow(clippy::cast_possible_truncation)]
566 let tid_raw = ret as i32;
567 let tid = Tid::try_from_raw(tid_raw)
568 .expect("clone_thread retour parent : TID strictement positif (POSIX)");
569 Ok(tid)
570}
571
572#[cfg(target_arch = "x86_64")]
573#[inline]
574unsafe fn raw_syscall_clone3_thread(
575 args_ptr: u64,
576 args_size: u64,
577 entry: extern "C" fn(usize),
578 arg: usize,
579) -> i64 {
580 let ret: i64;
581 // SAFETY:
582 // - SYS_clone3 (x86_64 = 435) crée le thread ; le kernel lit `args_size`
583 // octets à `args_ptr` (struct clone_args valide) et règle `sp` de l'enfant
584 // au sommet de la pile (`stack`/`stack_size` du struct).
585 // - « Retourne deux fois » : côté PARENT `rax` = TID (> 0) ou -errno → on
586 // branche en `2:` et rend `rax` à Rust. Côté ENFANT `rax` = 0 : on NE
587 // revient PAS dans le code Rust (sa pile est vierge). On place `arg` dans
588 // rdi puis on `call` `entry` (ABI C) sur la pile neuve. Si `entry` revient,
589 // on termine le SEUL thread courant via SYS_exit (x86_64 = 60), jamais
590 // exit_group. `ud2` est un garde-fou inatteignable.
591 // - Clobbers du PARENT uniquement (l'enfant ne rend jamais la main au
592 // compilateur) : rax (ret), rcx et r11 (clobbés par `syscall`), drapeaux
593 // (pas de `preserves_flags`). `entry`/`arg` sont **épinglés** sur des
594 // registres callee-saved (r12/r13) que `syscall` ne touche pas : un
595 // registre callee-saved choisi par le compilateur (`in(reg)`) pourrait
596 // être rcx/r11 et serait écrasé par le `syscall` AVANT l'usage enfant
597 // (l'allocateur suppose les `lateout` produits *après* les `in` lus).
598 // `inout("rdi")` documente que rdi (args_ptr puis arg) est écrasé. Pas de
599 // `nostack` : le `call` enfant pousse sur la pile (neuve).
600 unsafe {
601 core::arch::asm!(
602 "syscall",
603 "test rax, rax",
604 "jnz 2f",
605 "mov rdi, r13",
606 "call r12",
607 "mov rax, 60", // SYS_exit (x86_64 = 60) : termine CE thread.
608 "xor edi, edi",
609 "syscall",
610 "ud2",
611 "2:",
612 inout("rax") 435_i64 => ret, // SYS_clone3 (x86_64 = 435).
613 inout("rdi") args_ptr => _,
614 in("rsi") args_size,
615 in("r12") entry,
616 in("r13") arg,
617 lateout("rcx") _,
618 lateout("r11") _,
619 );
620 }
621 ret
622}
623
624#[cfg(target_arch = "aarch64")]
625#[inline]
626unsafe fn raw_syscall_clone3_thread(
627 args_ptr: u64,
628 args_size: u64,
629 entry: extern "C" fn(usize),
630 arg: usize,
631) -> i64 {
632 let ret: i64;
633 // SAFETY: voir la version x86_64. ABI aarch64 : SYS_clone3 (aarch64 = 435)
634 // en X8, args en X0/X1, retour en X0. Côté ENFANT (`x0 == 0`) : `arg` → x0,
635 // `blr` `entry` sur la pile neuve, puis SYS_exit (aarch64 = 93) — jamais
636 // exit_group. `udf #0` garde-fou inatteignable. Clobbers du PARENT
637 // uniquement : x0 (ret) ; `x8` est `in` (réécrit côté enfant, consommé).
638 // `entry`/`arg` sont **épinglés** sur des registres callee-saved (x21/x20)
639 // intacts jusqu'à leur usage enfant (symétrie avec x86_64). Pas de `nostack`
640 // (`blr` enfant utilise la pile).
641 unsafe {
642 core::arch::asm!(
643 "svc 0",
644 "cbnz x0, 2f",
645 "mov x0, x20",
646 "blr x21",
647 "mov x8, #93", // SYS_exit (aarch64 = 93) : termine CE thread.
648 "mov x0, #0",
649 "svc 0",
650 "udf #0",
651 "2:",
652 in("x8") 435_i64, // SYS_clone3 (aarch64 = 435).
653 inout("x0") args_ptr => ret,
654 in("x1") args_size,
655 in("x21") entry,
656 in("x20") arg,
657 );
658 }
659 ret
660}
661
662// ─────────────────────────────────────────────────────────────────────────
663// exit_group — terminaison immédiate du processus, sans cleanup userspace.
664//
665// Helper **interne** (`pub(crate)`, hors API publique gelée) : c'est la
666// discipline de sortie correcte pour l'enfant d'un `clone3` fork dans un
667// programme multi-thread. `std::process::exit` exécute les atexit handlers
668// (flush stdio, machinerie de capture du harnais de test…) qui peuvent
669// **deadlocker** sur des mutex hérités verrouillés par les threads du parent
670// — inexistants dans l'enfant. Observé sur aarch64 (`futex_do_wait`) là où
671// x86_64 masquait le bug. `exit_group` est un syscall direct : aucun verrou,
672// aucun atexit, terminaison atomique de tous les threads du processus.
673// ─────────────────────────────────────────────────────────────────────────
674
675/// Termine immédiatement le processus appelant (tous ses threads) avec le
676/// code `status`, via le syscall `child_exit(2)`.
677///
678/// Ne retourne jamais (`!`). N'exécute **aucun** atexit handler, ne flush
679/// **aucun** buffer userspace : c'est précisément ce qui la rend sûre comme
680/// sortie d'un enfant forké d'un programme multi-thread (cf. note ci-dessus).
681///
682/// Primitive publique (spec `family-process`, sous-section 3) : c'est le
683/// chemin de sortie correct pour l'**enfant d'un `clone3` fork** dont l'`exec`
684/// a échoué — terminer immédiatement, sans réveiller la machinerie userspace
685/// héritée du parent. `air-process` (couche 1) l'utilise après un `execve`
686/// raté dans l'enfant. Le code de sortie observé par `waitid`/`wait` est
687/// `status & 0xFF`.
688pub fn exit_group(status: i32) -> ! {
689 // SAFETY: `exit_group(2)` ne lit ni n'écrit aucune mémoire utilisateur ;
690 // il termine le processus et ne retourne pas. `options(noreturn)` est
691 // donc correct.
692 unsafe { raw_syscall_exit_group(status) }
693}
694
695#[cfg(target_arch = "x86_64")]
696#[inline]
697unsafe fn raw_syscall_exit_group(status: i32) -> ! {
698 // SAFETY: SYS_exit_group (x86_64 = 231) ne touche aucune mémoire
699 // utilisateur et ne retourne jamais. `noreturn` autorisé.
700 unsafe {
701 core::arch::asm!(
702 "syscall",
703 in("rax") 231_i64,
704 in("rdi") i64::from(status),
705 options(nostack, noreturn),
706 );
707 }
708}
709
710#[cfg(target_arch = "aarch64")]
711#[inline]
712unsafe fn raw_syscall_exit_group(status: i32) -> ! {
713 // SAFETY: SYS_exit_group (aarch64 = 94) ne touche aucune mémoire
714 // utilisateur et ne retourne jamais. `noreturn` autorisé.
715 unsafe {
716 core::arch::asm!(
717 "svc 0",
718 in("x8") 94_i64,
719 in("x0") i64::from(status),
720 options(nostack, noreturn),
721 );
722 }
723}
724
725// ─────────────────────────────────────────────────────────────────────────
726// waitid
727// ─────────────────────────────────────────────────────────────────────────
728
729// `idtype` du syscall waitid (cf. `linux/wait.h`).
730const P_ALL: i64 = 0;
731const P_PID: i64 = 1;
732const P_PGID: i64 = 2;
733const P_PIDFD: i64 = 3;
734
735// `si_code` valeurs pour SIGCHLD (cf. `linux/signal.h`).
736const CLD_EXITED: i32 = 1;
737const CLD_KILLED: i32 = 2;
738const CLD_DUMPED: i32 = 3;
739const CLD_TRAPPED: i32 = 4;
740const CLD_STOPPED: i32 = 5;
741const CLD_CONTINUED: i32 = 6;
742
743/// Représentation locale du `siginfo_t` Linux pour SIGCHLD.
744///
745/// Layout vérifié sur x86_64 et aarch64 (deux seules architectures Air,
746/// cf. ADR-014) : 128 octets, union 8-octets alignée à l'offset 16. Les
747/// champs `si_pid`/`si_uid`/`si_status` sont au début de la variante
748/// `_sigchld` de l'union ; les offsets reflètent cette disposition.
749#[repr(C)]
750#[derive(Clone, Copy)]
751struct KernelSiginfo {
752 si_signo: i32,
753 si_errno: i32,
754 si_code: i32,
755 _pad0: i32,
756 si_pid: i32,
757 si_uid: u32,
758 si_status: i32,
759 _trailing: [u8; 100],
760}
761
762impl KernelSiginfo {
763 fn zeroed() -> Self {
764 Self {
765 si_signo: 0,
766 si_errno: 0,
767 si_code: 0,
768 _pad0: 0,
769 si_pid: 0,
770 si_uid: 0,
771 si_status: 0,
772 _trailing: [0u8; 100],
773 }
774 }
775}
776
777const _: () = {
778 // Vérifie à la compilation que KernelSiginfo fait bien 128 octets : si le
779 // layout C devait dériver (futur portage), cette assertion explose.
780 assert!(core::mem::size_of::<KernelSiginfo>() == 128);
781};
782
783/// Attend un événement sur un processus enfant via le syscall `waitid(2)`.
784///
785/// Cf. `docs/specs/layer-0/family-process.md` section `waitid`. Retourne :
786///
787/// - `Ok(Some(status))` si un événement a été reçu.
788/// - `Ok(None)` si [`WaitOptions::NOHANG`] était positionné et qu'aucun
789/// événement n'était disponible (kernel renvoie une `siginfo` zéroée).
790/// - `Err(Errno::EINTR)` si l'attente a été interrompue par un signal :
791/// **non retried automatiquement** (convention 2 ADR-021). L'appelant
792/// qui veut retry écrit la boucle explicitement.
793///
794/// # Errors
795///
796/// - [`Errno::ECHILD`] : aucun enfant à attendre dans la cible.
797/// - [`Errno::EINTR`] : interrompu par un signal, voir ci-dessus.
798/// - [`Errno::EINVAL`] : options invalides (aucun parmi
799/// `EXITED`/`STOPPED`/`CONTINUED` positionné) ou cible invalide.
800pub fn waitid(target: WaitTarget<'_>, options: WaitOptions) -> Result<Option<WaitStatus>, Errno> {
801 use air_sys_types::fd::AsRawFd;
802
803 let (idtype, id_i64) = match target {
804 WaitTarget::AnyChild => (P_ALL, 0_i64),
805 WaitTarget::Pid(p) => (P_PID, i64::from(p.as_raw())),
806 WaitTarget::ProcessGroup(p) => (P_PGID, i64::from(p.as_raw())),
807 WaitTarget::AnyProcessGroup => (P_PGID, 0_i64),
808 WaitTarget::PidFd(fd) => (P_PIDFD, i64::from(fd.as_raw_fd())),
809 };
810
811 let mut info = KernelSiginfo::zeroed();
812 let info_ptr: *mut KernelSiginfo = &mut info;
813
814 let options_i64 = i64::from(options.bits());
815
816 // SAFETY:
817 // - info_ptr pointe sur un KernelSiginfo local (128 octets, alignement 4) que
818 // le kernel renseigne au plus jusqu'à 128 octets — taille fixe et
819 // exactement celle qu'attend l'ABI Linux pour `siginfo_t`.
820 // - rusage = 0 : on n'expose pas le 5e argument optionnel.
821 let ret = unsafe { raw_syscall_waitid(idtype, id_i64, info_ptr as u64, options_i64) };
822
823 if ret < 0 {
824 return Err(errno_from_negative_syscall_ret(ret));
825 }
826
827 // Cas NOHANG sans événement : le kernel laisse `siginfo` zéroé
828 // (en particulier si_signo == 0, parce qu'un événement waitid réel
829 // a toujours si_signo = SIGCHLD = 17 sur x86_64/aarch64).
830 if info.si_signo == 0 {
831 return Ok(None);
832 }
833
834 let pid = Pid::try_from_raw(info.si_pid)
835 .expect("waitid : si_pid positif pour un événement enfant réel");
836 let event = siginfo_to_wait_event(&info);
837
838 Ok(Some(WaitStatus {
839 pid,
840 uid: info.si_uid,
841 event,
842 }))
843}
844
845/// Helper privé : convertit un `KernelSiginfo` rempli par le kernel en
846/// [`WaitEvent`] typé. Extrait du corps de [`waitid`] pour pouvoir être
847/// testé unitairement avec des `KernelSiginfo` synthétisés en mémoire — ce qui
848/// permet de couvrir les 5 variantes (Exited, Killed, Stopped, Continued,
849/// Trapped) sans dépendre de scénarios kernel réels.
850///
851/// **Limite connue** : ce test sur KernelSiginfo synthétisés couvre la *logique
852/// de dispatch* mais ne valide pas le *remplissage* effectif de
853/// `siginfo_t` par le kernel pour chaque code `CLD_*`. Voir `docs/JOURNAL.md`,
854/// session 2026-05-22 (suite : clone3+waitid), trou de couverture acté.
855fn siginfo_to_wait_event(info: &KernelSiginfo) -> WaitEvent {
856 match info.si_code {
857 CLD_EXITED => WaitEvent::Exited {
858 code: info.si_status,
859 },
860 CLD_KILLED => WaitEvent::Killed {
861 signal: Signal::try_from_raw(info.si_status)
862 .expect("CLD_KILLED : si_status est un numéro de signal valide"),
863 core_dumped: false,
864 },
865 CLD_DUMPED => WaitEvent::Killed {
866 signal: Signal::try_from_raw(info.si_status)
867 .expect("CLD_DUMPED : si_status est un numéro de signal valide"),
868 core_dumped: true,
869 },
870 CLD_TRAPPED => WaitEvent::Trapped {
871 signal: Signal::try_from_raw(info.si_status)
872 .expect("CLD_TRAPPED : si_status est un numéro de signal valide"),
873 },
874 CLD_STOPPED => WaitEvent::Stopped {
875 signal: Signal::try_from_raw(info.si_status)
876 .expect("CLD_STOPPED : si_status est un numéro de signal valide"),
877 },
878 CLD_CONTINUED => WaitEvent::Continued,
879 unknown => unreachable!("kernel a renseigné un si_code inconnu pour SIGCHLD : {unknown}"),
880 }
881}
882
883#[cfg(target_arch = "x86_64")]
884#[inline]
885unsafe fn raw_syscall_waitid(idtype: i64, id: i64, infop: u64, options: i64) -> i64 {
886 let ret: i64;
887 // SAFETY:
888 // - SYS_waitid (x86_64 = 247). Le kernel écrit dans `infop` au plus
889 // 128 octets — validité garantie par l'appelant (KernelSiginfo local).
890 // - rusage (5e arg) à 0 : non utilisé.
891 // - x86_64 syscall ABI : numéro en RAX, args en RDI/RSI/RDX/R10/R8/R9 ;
892 // retour en RAX ; clobbe RCX et R11.
893 // - **Pas de `readonly`** : le kernel écrit dans `*infop`.
894 unsafe {
895 core::arch::asm!(
896 "syscall",
897 in("rax") 247_i64,
898 in("rdi") idtype,
899 in("rsi") id,
900 in("rdx") infop,
901 in("r10") options,
902 in("r8") 0_i64,
903 lateout("rax") ret,
904 lateout("rcx") _,
905 lateout("r11") _,
906 options(nostack, preserves_flags),
907 );
908 }
909 ret
910}
911
912#[cfg(target_arch = "aarch64")]
913#[inline]
914unsafe fn raw_syscall_waitid(idtype: i64, id: i64, infop: u64, options: i64) -> i64 {
915 let ret: i64;
916 // SAFETY: voir version x86_64 ; aarch64 syscall ABI :
917 // numéro en X8 (= 95), args en X0..X4 (5 args, le 5e = rusage = 0).
918 unsafe {
919 core::arch::asm!(
920 "svc 0",
921 in("x8") 95_i64,
922 inout("x0") idtype => ret,
923 in("x1") id,
924 in("x2") infop,
925 in("x3") options,
926 in("x4") 0_i64,
927 options(nostack, preserves_flags),
928 );
929 }
930 ret
931}
932
933// ─────────────────────────────────────────────────────────────────────────
934// pidfd_open
935// ─────────────────────────────────────────────────────────────────────────
936
937/// Ouvre un pidfd qui référence le processus identifié par `pid`.
938///
939/// Cf. `docs/specs/layer-0/family-process.md`, sous-section 3,
940/// `pidfd_open`. Le pidfd retourné reste valide même après la mort du
941/// processus original (pas de race sur le PID recyclé). Il peut être
942/// utilisé par [`waitid`] (variant `WaitTarget::PidFd`),
943/// [`pidfd_send_signal`], [`pidfd_getfd`], et par les syscalls
944/// `poll`/`epoll` pour détecter la mort du processus.
945///
946/// # Errors
947///
948/// - [`Errno::EINVAL`] : un bit invalide dans `flags`.
949/// - [`Errno::ESRCH`] : aucun processus n'a ce PID.
950/// - `ENOMEM` (stub partiel `Errno`) : mémoire kernel insuffisante.
951pub fn pidfd_open(pid: Pid, flags: PidFdOpenFlags) -> Result<PidFd, Errno> {
952 // SAFETY:
953 // - `pidfd_open(2)` ne lit ni n'écrit aucune mémoire utilisateur :
954 // il prend deux scalaires et retourne un fd.
955 // - L'ABI x86_64 / aarch64 est documentée dans
956 // `raw_syscall_pidfd_open` (clobbers, options).
957 let ret = unsafe { raw_syscall_pidfd_open(pid.as_raw(), flags.bits()) };
958 if ret < 0 {
959 return Err(errno_from_negative_syscall_ret(ret));
960 }
961 // `ret` est un fd kernel valide. Linux borne les fd à `i32::MAX`
962 // (`/proc/sys/fs/file-max` n'excède pas cette limite par construction
963 // dans le kernel), donc la troncature est exacte.
964 #[allow(clippy::cast_possible_truncation)]
965 let new_fd: RawFd = ret as RawFd;
966 // SAFETY: le kernel vient d'allouer un fd valide et nous en a
967 // transféré la propriété ; Air le wrappe immédiatement dans
968 // `OwnedFd` puis dans `PidFd`. La fermeture est garantie par Drop.
969 let owned = unsafe { OwnedFd::from_raw_fd(new_fd) };
970 Ok(PidFd::from_owned_fd(owned))
971}
972
973#[cfg(target_arch = "x86_64")]
974#[inline]
975unsafe fn raw_syscall_pidfd_open(pid: i32, flags: u32) -> i64 {
976 let ret: i64;
977 // SAFETY:
978 // - SYS_pidfd_open (x86_64 = 434) ne touche à aucune mémoire
979 // utilisateur ; il ne fait que retourner un fd.
980 // - ABI syscall x86_64 : numéro en RAX, arg1/arg2 en RDI/RSI ;
981 // retour en RAX ; clobbe RCX (RIP) et R11 (RFLAGS).
982 // - `readonly` correct : pas d'écriture en mémoire utilisateur.
983 unsafe {
984 core::arch::asm!(
985 "syscall",
986 in("rax") 434_i64,
987 in("rdi") i64::from(pid),
988 in("rsi") i64::from(flags),
989 lateout("rax") ret,
990 lateout("rcx") _,
991 lateout("r11") _,
992 options(nostack, preserves_flags, readonly),
993 );
994 }
995 ret
996}
997
998#[cfg(target_arch = "aarch64")]
999#[inline]
1000unsafe fn raw_syscall_pidfd_open(pid: i32, flags: u32) -> i64 {
1001 let ret: i64;
1002 // SAFETY: ABI aarch64 ; numéro en X8, args en X0/X1, retour en X0.
1003 unsafe {
1004 core::arch::asm!(
1005 "svc 0",
1006 in("x8") 434_i64,
1007 inout("x0") i64::from(pid) => ret,
1008 in("x1") i64::from(flags),
1009 options(nostack, preserves_flags, readonly),
1010 );
1011 }
1012 ret
1013}
1014
1015// ─────────────────────────────────────────────────────────────────────────
1016// pidfd_send_signal
1017// ─────────────────────────────────────────────────────────────────────────
1018
1019/// Envoie un signal au processus référencé par `pidfd`.
1020///
1021/// Cf. `docs/specs/layer-0/family-process.md`, sous-section 3,
1022/// `pidfd_send_signal`. À préférer à un futur `kill(pid, sig)` quand un
1023/// pidfd est disponible : pas de race sur le PID recyclé.
1024///
1025/// # `info`
1026///
1027/// Si `Some(info)`, le kernel utilise le `siginfo_t` fourni (sémantique
1028/// `rt_sigqueueinfo`). Si `None`, le kernel synthétise un `siginfo_t`
1029/// avec `si_code = SI_USER`, équivalent à un `kill(2)` standard.
1030///
1031/// **Note périmètre.** Cette PR n'expose **aucun constructeur public**
1032/// pour [`SignalInfo`] (stub partiel) ; depuis l'extérieur de la crate,
1033/// seul `None` est passable. Le bras `Some(_)` du wrapper reste
1034/// type-reachable mais value-unreachable jusqu'à la PR `family-signal`.
1035///
1036/// # Errors
1037///
1038/// - [`Errno::EBADF`] : pidfd invalide.
1039/// - [`Errno::EINVAL`] : signal invalide.
1040/// - [`Errno::EPERM`] : permissions insuffisantes pour signaler cette cible.
1041/// - [`Errno::ESRCH`] : le processus référencé n'existe plus.
1042pub fn pidfd_send_signal(
1043 pidfd: BorrowedFd<'_>,
1044 signal: Signal,
1045 info: Option<&SignalInfo>,
1046) -> Result<(), Errno> {
1047 let info_ptr: u64 = match info {
1048 Some(i) => {
1049 let p: *const SignalInfo = i;
1050 p as u64
1051 }
1052 None => 0,
1053 };
1054
1055 // SAFETY:
1056 // - `pidfd_send_signal(2)` lit `*info` si le pointeur est non nul ;
1057 // il ne **modifie** aucune mémoire utilisateur (`readonly` OK).
1058 // - Si `info_ptr == 0` (cas `None`), le kernel synthétise un siginfo.
1059 // - L'appelant garantit que `pidfd` est ouvert (BorrowedFd typé).
1060 // - `flags` (5e arg) est réservé : doit valoir 0.
1061 let ret = unsafe {
1062 raw_syscall_pidfd_send_signal(pidfd.as_raw_fd(), signal.as_raw(), info_ptr, 0_u32)
1063 };
1064 if ret < 0 {
1065 return Err(errno_from_negative_syscall_ret(ret));
1066 }
1067 Ok(())
1068}
1069
1070#[cfg(target_arch = "x86_64")]
1071#[inline]
1072unsafe fn raw_syscall_pidfd_send_signal(pidfd: i32, signal: i32, info: u64, flags: u32) -> i64 {
1073 let ret: i64;
1074 // SAFETY:
1075 // - SYS_pidfd_send_signal (x86_64 = 424). Lecture seulement de
1076 // `*info` (et seulement si non-null) ; pas d'écriture en mémoire
1077 // utilisateur, d'où `readonly`.
1078 // - ABI x86_64 : args en RDI/RSI/RDX/R10 ; retour en RAX ; clobbe
1079 // RCX et R11.
1080 unsafe {
1081 core::arch::asm!(
1082 "syscall",
1083 in("rax") 424_i64,
1084 in("rdi") i64::from(pidfd),
1085 in("rsi") i64::from(signal),
1086 in("rdx") info,
1087 in("r10") i64::from(flags),
1088 lateout("rax") ret,
1089 lateout("rcx") _,
1090 lateout("r11") _,
1091 options(nostack, preserves_flags, readonly),
1092 );
1093 }
1094 ret
1095}
1096
1097#[cfg(target_arch = "aarch64")]
1098#[inline]
1099unsafe fn raw_syscall_pidfd_send_signal(pidfd: i32, signal: i32, info: u64, flags: u32) -> i64 {
1100 let ret: i64;
1101 // SAFETY: ABI aarch64 (numéro en X8, args en X0..X3).
1102 unsafe {
1103 core::arch::asm!(
1104 "svc 0",
1105 in("x8") 424_i64,
1106 inout("x0") i64::from(pidfd) => ret,
1107 in("x1") i64::from(signal),
1108 in("x2") info,
1109 in("x3") i64::from(flags),
1110 options(nostack, preserves_flags, readonly),
1111 );
1112 }
1113 ret
1114}
1115
1116// ─────────────────────────────────────────────────────────────────────────
1117// pidfd_getfd
1118// ─────────────────────────────────────────────────────────────────────────
1119
1120/// Duplique un FD ouvert dans le processus référencé par `pidfd`.
1121///
1122/// Cf. `docs/specs/layer-0/family-process.md`, sous-section 3,
1123/// `pidfd_getfd`. Cas d'usage spécialisé : debuggers, outils de
1124/// surveillance, transfert de ressources entre processus coopérant.
1125///
1126/// Le FD retourné est **propriété de l'appelant** ([`OwnedFd`]) ; il
1127/// pointe sur la même entrée du file table que `target_fd` côté
1128/// processus cible. La fermeture côté cible ne ferme pas la copie locale,
1129/// et réciproquement.
1130///
1131/// # Errors
1132///
1133/// - [`Errno::EBADF`] : `pidfd` invalide, ou `target_fd` non ouvert
1134/// dans le processus cible.
1135/// - [`Errno::EPERM`] : pas de `CAP_SYS_PTRACE` et pas le même
1136/// utilisateur que la cible. Sur Ubuntu, Yama `ptrace_scope ≥ 2`
1137/// peut également bloquer.
1138/// - [`Errno::EINVAL`] : `flags` non nul (réservé).
1139/// - [`Errno::ESRCH`] : le processus référencé n'existe plus.
1140pub fn pidfd_getfd(pidfd: BorrowedFd<'_>, target_fd: RawFd, flags: u32) -> Result<OwnedFd, Errno> {
1141 // SAFETY:
1142 // - `pidfd_getfd(2)` ne touche aucune mémoire utilisateur : il
1143 // alloue côté kernel une nouvelle entrée dans la file table de
1144 // l'appelant pointant sur la même file que `target_fd` côté cible.
1145 // - `flags` est réservé (doit être 0 sur kernel actuel).
1146 let ret = unsafe { raw_syscall_pidfd_getfd(pidfd.as_raw_fd(), target_fd, flags) };
1147 if ret < 0 {
1148 return Err(errno_from_negative_syscall_ret(ret));
1149 }
1150 #[allow(clippy::cast_possible_truncation)]
1151 let new_fd: RawFd = ret as RawFd;
1152 // SAFETY: kernel vient de transférer la propriété d'un fd valide.
1153 Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
1154}
1155
1156#[cfg(target_arch = "x86_64")]
1157#[inline]
1158unsafe fn raw_syscall_pidfd_getfd(pidfd: i32, target_fd: i32, flags: u32) -> i64 {
1159 let ret: i64;
1160 // SAFETY: SYS_pidfd_getfd (x86_64 = 438) ne lit ni n'écrit en
1161 // mémoire utilisateur (`readonly`).
1162 unsafe {
1163 core::arch::asm!(
1164 "syscall",
1165 in("rax") 438_i64,
1166 in("rdi") i64::from(pidfd),
1167 in("rsi") i64::from(target_fd),
1168 in("rdx") i64::from(flags),
1169 lateout("rax") ret,
1170 lateout("rcx") _,
1171 lateout("r11") _,
1172 options(nostack, preserves_flags, readonly),
1173 );
1174 }
1175 ret
1176}
1177
1178#[cfg(target_arch = "aarch64")]
1179#[inline]
1180unsafe fn raw_syscall_pidfd_getfd(pidfd: i32, target_fd: i32, flags: u32) -> i64 {
1181 let ret: i64;
1182 // SAFETY: ABI aarch64 ; numéro en X8 (= 438), args en X0..X2.
1183 unsafe {
1184 core::arch::asm!(
1185 "svc 0",
1186 in("x8") 438_i64,
1187 inout("x0") i64::from(pidfd) => ret,
1188 in("x1") i64::from(target_fd),
1189 in("x2") i64::from(flags),
1190 options(nostack, preserves_flags, readonly),
1191 );
1192 }
1193 ret
1194}
1195
1196// ─────────────────────────────────────────────────────────────────────────
1197// Groupes et sessions (sous-section 4 de family-process.md).
1198// ─────────────────────────────────────────────────────────────────────────
1199
1200/// Place `pid` dans le process group `pgid`.
1201///
1202/// Cf. `docs/specs/layer-0/family-process.md` sous-section 4. Sémantique
1203/// POSIX standard ; cas d'usage : shells, démons, applications terminal.
1204///
1205/// `pid = None` désigne le processus courant (cf. ADR-021 convention 1,
1206/// remplaçant la sentinelle kernel `0`). `pgid = None` désigne « créer un
1207/// nouveau process group dont le leader est `pid` » (sentinelle `0` côté
1208/// kernel).
1209///
1210/// # Errors
1211///
1212/// - [`Errno::EINVAL`] : `pgid` ne désigne pas un process group valide
1213/// dans la session courante.
1214/// - [`Errno::EPERM`] : `pid` n'est pas l'appelant ou un de ses enfants,
1215/// ou tentative de déplacer un processus hors session.
1216/// - [`Errno::ESRCH`] : `pid` n'existe pas.
1217pub fn setpgid(pid: Option<Pid>, pgid: Option<Pid>) -> Result<(), Errno> {
1218 let pid_arg = pid.map_or(0_i32, |p| p.as_raw());
1219 let pgid_arg = pgid.map_or(0_i32, |p| p.as_raw());
1220 // SAFETY: setpgid(2) ne touche aucune mémoire utilisateur ; lecture
1221 // des deux pid_t scalaires.
1222 let ret = unsafe { raw_syscall_setpgid(pid_arg, pgid_arg) };
1223 if ret < 0 {
1224 return Err(errno_from_negative_syscall_ret(ret));
1225 }
1226 Ok(())
1227}
1228
1229/// Retourne le process group ID de `pid` (ou du processus courant si `None`).
1230///
1231/// # Errors
1232///
1233/// - [`Errno::ESRCH`] : `pid` n'existe pas.
1234pub fn getpgid(pid: Option<Pid>) -> Result<Pid, Errno> {
1235 let pid_arg = pid.map_or(0_i32, |p| p.as_raw());
1236 // SAFETY: getpgid(2) ne touche aucune mémoire utilisateur.
1237 let ret = unsafe { raw_syscall_getpgid(pid_arg) };
1238 if ret < 0 {
1239 return Err(errno_from_negative_syscall_ret(ret));
1240 }
1241 // PGID retourné est un pid_t > 0 (process group ID = pid du leader,
1242 // qui est lui-même positif). Troncature exacte.
1243 #[allow(clippy::cast_possible_truncation)]
1244 let raw = ret as i32;
1245 Ok(Pid::try_from_raw(raw).expect("kernel : pgid strictement positif"))
1246}
1247
1248/// Crée une nouvelle session avec l'appelant comme session leader, et le
1249/// place dans un nouveau process group dont il est le leader. Retourne le
1250/// nouveau session ID.
1251///
1252/// # Errors
1253///
1254/// - [`Errno::EPERM`] : l'appelant est déjà process group leader (le test
1255/// runner cargo est typiquement un pgrp leader → `setsid` échoue, ce
1256/// qui est exploité par les tests).
1257pub fn setsid() -> Result<Pid, Errno> {
1258 // SAFETY: setsid(2) ne touche aucune mémoire utilisateur.
1259 let ret = unsafe { raw_syscall_setsid() };
1260 if ret < 0 {
1261 return Err(errno_from_negative_syscall_ret(ret));
1262 }
1263 #[allow(clippy::cast_possible_truncation)]
1264 let raw = ret as i32;
1265 Ok(Pid::try_from_raw(raw).expect("kernel : nouveau SID strictement positif"))
1266}
1267
1268/// Retourne le session ID de `pid` (ou du processus courant si `None`).
1269///
1270/// # Errors
1271///
1272/// - [`Errno::EPERM`] : `pid` est dans une session différente de l'appelant
1273/// et le kernel refuse de divulguer (rare).
1274/// - [`Errno::ESRCH`] : `pid` n'existe pas.
1275pub fn getsid(pid: Option<Pid>) -> Result<Pid, Errno> {
1276 let pid_arg = pid.map_or(0_i32, |p| p.as_raw());
1277 // SAFETY: getsid(2) ne touche aucune mémoire utilisateur.
1278 let ret = unsafe { raw_syscall_getsid(pid_arg) };
1279 if ret < 0 {
1280 return Err(errno_from_negative_syscall_ret(ret));
1281 }
1282 #[allow(clippy::cast_possible_truncation)]
1283 let raw = ret as i32;
1284 Ok(Pid::try_from_raw(raw).expect("kernel : SID strictement positif"))
1285}
1286
1287// ─────────────────────────────────────────────────────────────────────────
1288// Séparation de privilèges (privsep) — setgroups/getgroups + setres*id/getres*id
1289//
1290// Spec : `docs/specs/layer-0/family-process-privsep.md`.
1291//
1292// Opérations **privilégiées** (`CAP_SETUID`/`CAP_SETGID` ou root). Prérequis
1293// d'un `drop_privileges` correct (`air-process`, couche 1) : larguer les
1294// **groupes supplémentaires** ET fixer le ***saved-set*-id** — sans quoi un
1295// `setuid`/`setgid` simple laisse regagner les privilèges.
1296// ─────────────────────────────────────────────────────────────────────────
1297
1298/// `NGROUPS_MAX` (Linux) : nombre maximal de groupes supplémentaires. Au-delà,
1299/// `setgroups(2)` rend `EINVAL` ; on le valide **en amont** (Principe 4).
1300const NGROUPS_MAX: usize = 65536;
1301
1302/// Encode un `Option<Gid>` pour `setresgid` : `None` ⇒ `(gid_t)-1` (composante
1303/// **inchangée**), `Some(g)` ⇒ sa valeur brute. La sentinelle `-1` n'est jamais
1304/// exposée à l'appelant (ADR-021 conv. 1).
1305fn opt_gid_to_raw(gid: Option<Gid>) -> u32 {
1306 gid.map_or(u32::MAX, Gid::as_raw)
1307}
1308
1309/// Encode un `Option<Uid>` pour `setresuid` (`None` ⇒ `(uid_t)-1`). Voir
1310/// [`opt_gid_to_raw`].
1311fn opt_uid_to_raw(uid: Option<Uid>) -> u32 {
1312 uid.map_or(u32::MAX, Uid::as_raw)
1313}
1314
1315/// Remplace la liste des **groupes supplémentaires** du processus (`setgroups`).
1316///
1317/// Pour une réduction de privilèges, passer **`&[]`** (largage total) tant qu'on
1318/// est privilégié : la liste vide est la valeur **normale** du privsep, pas une
1319/// sentinelle. Le layout de `&[Gid]` coïncide avec `&[gid_t]` (`Gid` est
1320/// `#[repr(transparent)]`), donc aucune copie n'est nécessaire.
1321///
1322/// # Errors
1323///
1324/// - [`Errno::EINVAL`] : plus de `NGROUPS_MAX` (65536) groupes (validé en amont).
1325/// - [`Errno::EPERM`] : `CAP_SETGID` absent.
1326/// - [`Errno::EFAULT`] : `groups` hors de l'espace d'adressage (n'arrive pas avec
1327/// une tranche Rust valide).
1328///
1329/// # Examples
1330///
1331/// ```no_run
1332/// use air_sys_syscall::process::set_groups;
1333/// // Largage total des groupes supplémentaires (étape d'un privsep).
1334/// set_groups(&[]).expect("CAP_SETGID requis");
1335/// ```
1336pub fn set_groups(groups: &[Gid]) -> Result<(), Errno> {
1337 // Validation amont (Principe 4) : borne kernel.
1338 if groups.len() > NGROUPS_MAX {
1339 return Err(Errno::EINVAL);
1340 }
1341 let size = groups.len();
1342 let list_ptr = groups.as_ptr() as u64;
1343 // SAFETY: setgroups(2) **lit** `size` entrées `gid_t` à `list_ptr` et n'écrit
1344 // aucune mémoire utilisateur. `groups` est une tranche Rust valide pour la
1345 // durée de l'appel ; `Gid` est `#[repr(transparent)]` sur `u32` (= `gid_t`),
1346 // donc le pointeur et le compte décrivent un tableau de `gid_t` correct.
1347 // `size ≤ NGROUPS_MAX` est validé ci-dessus.
1348 let ret = unsafe { raw_syscall_setgroups(size, list_ptr) };
1349 if ret < 0 {
1350 return Err(errno_from_negative_syscall_ret(ret));
1351 }
1352 Ok(())
1353}
1354
1355/// Lit la liste courante des **groupes supplémentaires** (`getgroups`) dans le
1356/// `buffer` fourni — **zéro allocation** — et retourne la sous-tranche remplie.
1357///
1358/// Vérification défensive de `drop_privileges` (Principe 5 : confirmer la
1359/// réduction). Le `buffer` doit pouvoir contenir tous les groupes courants.
1360///
1361/// # Errors
1362///
1363/// - [`Errno::EINVAL`] : `buffer` trop petit pour la liste complète.
1364/// - [`Errno::EFAULT`] : `buffer` invalide (n'arrive pas avec une tranche Rust).
1365///
1366/// # Examples
1367///
1368/// ```no_run
1369/// use air_sys_syscall::process::get_groups;
1370/// use air_sys_types::Gid;
1371/// let mut buf = [Gid::from_raw(0); 64];
1372/// let groups = get_groups(&mut buf).expect("getgroups");
1373/// assert!(groups.len() <= 64);
1374/// ```
1375pub fn get_groups(buffer: &mut [Gid]) -> Result<&[Gid], Errno> {
1376 let size = buffer.len();
1377 let list_ptr = buffer.as_mut_ptr() as u64;
1378 // SAFETY: getgroups(2) **écrit** jusqu'à `size` entrées `gid_t` à `list_ptr`
1379 // et retourne le nombre écrit. `buffer` est une tranche mutable valide pour
1380 // la durée de l'appel ; `Gid` est `#[repr(transparent)]` sur `gid_t`.
1381 let ret = unsafe { raw_syscall_getgroups(size, list_ptr) };
1382 if ret < 0 {
1383 return Err(errno_from_negative_syscall_ret(ret));
1384 }
1385 // `ret` = nombre de groupes écrits (`0 ≤ ret ≤ size`). Conversion exacte.
1386 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1387 let count = ret as usize;
1388 // `count ≤ size` garanti par le kernel ; `get` évite toute indexation
1389 // panic-able (le bras `None` est structurellement inatteignable).
1390 buffer.get(..count).ok_or(Errno::EINVAL)
1391}
1392
1393/// Fixe **real + effective + saved** GID en un seul appel (`setresgid`).
1394///
1395/// À effectuer **avant** [`set_resuid`] (on ne peut plus changer de GID une fois
1396/// l'UID réduit). `None` pour une composante = « **inchangée** » (sentinelle
1397/// kernel `(gid_t)-1` typée, ADR-021 conv. 1). Fixer le *saved-set-gid* est ce
1398/// qui interdit le **retour** au GID privilégié.
1399///
1400/// # Errors
1401///
1402/// - [`Errno::EPERM`] : changement non autorisé (sans `CAP_SETGID`).
1403/// - [`Errno::EINVAL`] : un des GID n'est pas valide.
1404pub fn set_resgid(
1405 real: Option<Gid>,
1406 effective: Option<Gid>,
1407 saved: Option<Gid>,
1408) -> Result<(), Errno> {
1409 let real = opt_gid_to_raw(real);
1410 let effective = opt_gid_to_raw(effective);
1411 let saved = opt_gid_to_raw(saved);
1412 // SAFETY: setresgid(2) ne touche aucune mémoire utilisateur (3 scalaires
1413 // `gid_t`). `(gid_t)-1` laisse la composante inchangée.
1414 let ret = unsafe { raw_syscall_setresgid(real, effective, saved) };
1415 if ret < 0 {
1416 return Err(errno_from_negative_syscall_ret(ret));
1417 }
1418 Ok(())
1419}
1420
1421/// Fixe **real + effective + saved** UID en un seul appel (`setresuid`).
1422///
1423/// À effectuer **après** [`set_resgid`] (et après [`set_groups`]). `None` =
1424/// composante inchangée (`(uid_t)-1` typé). Fixer le *saved-set-uid* rend la
1425/// réduction de privilèges **irréversible** — garantie qu'un `setuid` simple ne
1426/// donne pas de façon fiable.
1427///
1428/// # Errors
1429///
1430/// - [`Errno::EPERM`] : changement non autorisé (sans `CAP_SETUID`).
1431/// - [`Errno::EINVAL`] : un des UID n'est pas valide.
1432pub fn set_resuid(
1433 real: Option<Uid>,
1434 effective: Option<Uid>,
1435 saved: Option<Uid>,
1436) -> Result<(), Errno> {
1437 let real = opt_uid_to_raw(real);
1438 let effective = opt_uid_to_raw(effective);
1439 let saved = opt_uid_to_raw(saved);
1440 // SAFETY: setresuid(2) ne touche aucune mémoire utilisateur (3 scalaires
1441 // `uid_t`). `(uid_t)-1` laisse la composante inchangée.
1442 let ret = unsafe { raw_syscall_setresuid(real, effective, saved) };
1443 if ret < 0 {
1444 return Err(errno_from_negative_syscall_ret(ret));
1445 }
1446 Ok(())
1447}
1448
1449/// Lit les trois composantes GID **réelle / effective / sauvegardée**
1450/// (`getresgid`) — vérification défensive (confirmer qu'on ne peut plus revenir
1451/// en arrière).
1452///
1453/// # Errors
1454///
1455/// - [`Errno::EFAULT`] : pointeurs invalides (n'arrive pas : pile locale).
1456pub fn get_resgid() -> Result<ResGid, Errno> {
1457 let mut real: u32 = 0;
1458 let mut effective: u32 = 0;
1459 let mut saved: u32 = 0;
1460 // SAFETY: getresgid(2) **écrit** trois `gid_t` aux pointeurs fournis. Les
1461 // trois variables locales sont vivantes, alignées et exclusivement
1462 // empruntées (`from_mut`) pour la durée de l'appel.
1463 let ret = unsafe {
1464 raw_syscall_getresgid(
1465 core::ptr::from_mut(&mut real) as u64,
1466 core::ptr::from_mut(&mut effective) as u64,
1467 core::ptr::from_mut(&mut saved) as u64,
1468 )
1469 };
1470 if ret < 0 {
1471 return Err(errno_from_negative_syscall_ret(ret));
1472 }
1473 Ok(ResGid {
1474 real: Gid::from_raw(real),
1475 effective: Gid::from_raw(effective),
1476 saved: Gid::from_raw(saved),
1477 })
1478}
1479
1480/// Lit les trois composantes UID **réelle / effective / sauvegardée**
1481/// (`getresuid`) — vérification défensive.
1482///
1483/// # Errors
1484///
1485/// - [`Errno::EFAULT`] : pointeurs invalides (n'arrive pas : pile locale).
1486pub fn get_resuid() -> Result<ResUid, Errno> {
1487 let mut real: u32 = 0;
1488 let mut effective: u32 = 0;
1489 let mut saved: u32 = 0;
1490 // SAFETY: getresuid(2) **écrit** trois `uid_t` aux pointeurs fournis. Voir
1491 // [`get_resgid`] pour les préconditions (variables pile vivantes/alignées).
1492 let ret = unsafe {
1493 raw_syscall_getresuid(
1494 core::ptr::from_mut(&mut real) as u64,
1495 core::ptr::from_mut(&mut effective) as u64,
1496 core::ptr::from_mut(&mut saved) as u64,
1497 )
1498 };
1499 if ret < 0 {
1500 return Err(errno_from_negative_syscall_ret(ret));
1501 }
1502 Ok(ResUid {
1503 real: Uid::from_raw(real),
1504 effective: Uid::from_raw(effective),
1505 saved: Uid::from_raw(saved),
1506 })
1507}
1508
1509#[cfg(target_arch = "x86_64")]
1510#[inline]
1511unsafe fn raw_syscall_setpgid(pid: i32, pgid: i32) -> i64 {
1512 let ret: i64;
1513 // SAFETY: SYS_setpgid (x86_64 = 109) — pas d'accès mémoire utilisateur ;
1514 // ABI x86_64 standard (RAX=nr, RDI/RSI=args ; clobbers RCX/R11).
1515 unsafe {
1516 core::arch::asm!(
1517 "syscall",
1518 in("rax") 109_i64,
1519 in("rdi") i64::from(pid),
1520 in("rsi") i64::from(pgid),
1521 lateout("rax") ret,
1522 lateout("rcx") _,
1523 lateout("r11") _,
1524 options(nostack, preserves_flags, readonly),
1525 );
1526 }
1527 ret
1528}
1529
1530#[cfg(target_arch = "x86_64")]
1531#[inline]
1532unsafe fn raw_syscall_getpgid(pid: i32) -> i64 {
1533 let ret: i64;
1534 // SAFETY: SYS_getpgid (x86_64 = 121) — pas d'accès mémoire utilisateur.
1535 unsafe {
1536 core::arch::asm!(
1537 "syscall",
1538 in("rax") 121_i64,
1539 in("rdi") i64::from(pid),
1540 lateout("rax") ret,
1541 lateout("rcx") _,
1542 lateout("r11") _,
1543 options(nostack, preserves_flags, readonly),
1544 );
1545 }
1546 ret
1547}
1548
1549#[cfg(target_arch = "x86_64")]
1550#[inline]
1551unsafe fn raw_syscall_setsid() -> i64 {
1552 let ret: i64;
1553 // SAFETY: SYS_setsid (x86_64 = 112) — pas d'accès mémoire utilisateur,
1554 // pas d'argument.
1555 unsafe {
1556 core::arch::asm!(
1557 "syscall",
1558 in("rax") 112_i64,
1559 lateout("rax") ret,
1560 lateout("rcx") _,
1561 lateout("r11") _,
1562 options(nostack, preserves_flags, readonly),
1563 );
1564 }
1565 ret
1566}
1567
1568#[cfg(target_arch = "x86_64")]
1569#[inline]
1570unsafe fn raw_syscall_getsid(pid: i32) -> i64 {
1571 let ret: i64;
1572 // SAFETY: SYS_getsid (x86_64 = 124) — pas d'accès mémoire utilisateur.
1573 unsafe {
1574 core::arch::asm!(
1575 "syscall",
1576 in("rax") 124_i64,
1577 in("rdi") i64::from(pid),
1578 lateout("rax") ret,
1579 lateout("rcx") _,
1580 lateout("r11") _,
1581 options(nostack, preserves_flags, readonly),
1582 );
1583 }
1584 ret
1585}
1586
1587#[cfg(target_arch = "aarch64")]
1588#[inline]
1589unsafe fn raw_syscall_setpgid(pid: i32, pgid: i32) -> i64 {
1590 let ret: i64;
1591 // SAFETY: SYS_setpgid (aarch64 = 154) — ABI standard, no mem access.
1592 unsafe {
1593 core::arch::asm!(
1594 "svc 0",
1595 in("x8") 154_i64,
1596 inout("x0") i64::from(pid) => ret,
1597 in("x1") i64::from(pgid),
1598 options(nostack, preserves_flags, readonly),
1599 );
1600 }
1601 ret
1602}
1603
1604#[cfg(target_arch = "aarch64")]
1605#[inline]
1606unsafe fn raw_syscall_getpgid(pid: i32) -> i64 {
1607 let ret: i64;
1608 // SAFETY: SYS_getpgid (aarch64 = 155).
1609 unsafe {
1610 core::arch::asm!(
1611 "svc 0",
1612 in("x8") 155_i64,
1613 inout("x0") i64::from(pid) => ret,
1614 options(nostack, preserves_flags, readonly),
1615 );
1616 }
1617 ret
1618}
1619
1620#[cfg(target_arch = "aarch64")]
1621#[inline]
1622unsafe fn raw_syscall_setsid() -> i64 {
1623 let ret: i64;
1624 // SAFETY: SYS_setsid (aarch64 = 157).
1625 unsafe {
1626 core::arch::asm!(
1627 "svc 0",
1628 in("x8") 157_i64,
1629 lateout("x0") ret,
1630 options(nostack, preserves_flags, readonly),
1631 );
1632 }
1633 ret
1634}
1635
1636#[cfg(target_arch = "aarch64")]
1637#[inline]
1638unsafe fn raw_syscall_getsid(pid: i32) -> i64 {
1639 let ret: i64;
1640 // SAFETY: SYS_getsid (aarch64 = 156).
1641 unsafe {
1642 core::arch::asm!(
1643 "svc 0",
1644 in("x8") 156_i64,
1645 inout("x0") i64::from(pid) => ret,
1646 options(nostack, preserves_flags, readonly),
1647 );
1648 }
1649 ret
1650}
1651
1652// ─────────────────────────────────────────────────────────────────────────
1653// Helpers syscall bruts — privsep (numéros vérifiés sur l'uapi 6.12 par arche :
1654// x86_64 = unistd_64.h, aarch64 = asm-generic/unistd.h).
1655// ─────────────────────────────────────────────────────────────────────────
1656
1657#[cfg(target_arch = "x86_64")]
1658#[inline]
1659unsafe fn raw_syscall_setgroups(size: usize, list: u64) -> i64 {
1660 let ret: i64;
1661 // SAFETY: SYS_setgroups (x86_64 = 116). **Lit** `size` `gid_t` à `list` ;
1662 // n'écrit aucune mémoire utilisateur (`readonly`).
1663 unsafe {
1664 core::arch::asm!(
1665 "syscall",
1666 in("rax") 116_i64,
1667 in("rdi") size,
1668 in("rsi") list,
1669 lateout("rax") ret,
1670 lateout("rcx") _,
1671 lateout("r11") _,
1672 options(nostack, preserves_flags, readonly),
1673 );
1674 }
1675 ret
1676}
1677
1678#[cfg(target_arch = "aarch64")]
1679#[inline]
1680unsafe fn raw_syscall_setgroups(size: usize, list: u64) -> i64 {
1681 let ret: i64;
1682 // SAFETY: SYS_setgroups (aarch64 = 159). Lit la liste ; n'écrit rien.
1683 unsafe {
1684 core::arch::asm!(
1685 "svc 0",
1686 in("x8") 159_i64,
1687 inout("x0") size => ret,
1688 in("x1") list,
1689 options(nostack, preserves_flags, readonly),
1690 );
1691 }
1692 ret
1693}
1694
1695#[cfg(target_arch = "x86_64")]
1696#[inline]
1697unsafe fn raw_syscall_getgroups(size: usize, list: u64) -> i64 {
1698 let ret: i64;
1699 // SAFETY: SYS_getgroups (x86_64 = 115). **Écrit** jusqu'à `size` `gid_t` à
1700 // `list` (pas de `readonly`). Retourne le nombre écrit ou `-errno`.
1701 unsafe {
1702 core::arch::asm!(
1703 "syscall",
1704 in("rax") 115_i64,
1705 in("rdi") size,
1706 in("rsi") list,
1707 lateout("rax") ret,
1708 lateout("rcx") _,
1709 lateout("r11") _,
1710 options(nostack, preserves_flags),
1711 );
1712 }
1713 ret
1714}
1715
1716#[cfg(target_arch = "aarch64")]
1717#[inline]
1718unsafe fn raw_syscall_getgroups(size: usize, list: u64) -> i64 {
1719 let ret: i64;
1720 // SAFETY: SYS_getgroups (aarch64 = 158). Écrit la liste (pas de `readonly`).
1721 unsafe {
1722 core::arch::asm!(
1723 "svc 0",
1724 in("x8") 158_i64,
1725 inout("x0") size => ret,
1726 in("x1") list,
1727 options(nostack, preserves_flags),
1728 );
1729 }
1730 ret
1731}
1732
1733#[cfg(target_arch = "x86_64")]
1734#[inline]
1735unsafe fn raw_syscall_setresgid(real: u32, effective: u32, saved: u32) -> i64 {
1736 let ret: i64;
1737 // SAFETY: SYS_setresgid (x86_64 = 119). Trois scalaires `gid_t` ; aucun
1738 // accès mémoire utilisateur (`readonly`).
1739 unsafe {
1740 core::arch::asm!(
1741 "syscall",
1742 in("rax") 119_i64,
1743 in("rdi") i64::from(real),
1744 in("rsi") i64::from(effective),
1745 in("rdx") i64::from(saved),
1746 lateout("rax") ret,
1747 lateout("rcx") _,
1748 lateout("r11") _,
1749 options(nostack, preserves_flags, readonly),
1750 );
1751 }
1752 ret
1753}
1754
1755#[cfg(target_arch = "aarch64")]
1756#[inline]
1757unsafe fn raw_syscall_setresgid(real: u32, effective: u32, saved: u32) -> i64 {
1758 let ret: i64;
1759 // SAFETY: SYS_setresgid (aarch64 = 149). Trois scalaires ; pas d'accès mém.
1760 unsafe {
1761 core::arch::asm!(
1762 "svc 0",
1763 in("x8") 149_i64,
1764 inout("x0") i64::from(real) => ret,
1765 in("x1") i64::from(effective),
1766 in("x2") i64::from(saved),
1767 options(nostack, preserves_flags, readonly),
1768 );
1769 }
1770 ret
1771}
1772
1773#[cfg(target_arch = "x86_64")]
1774#[inline]
1775unsafe fn raw_syscall_setresuid(real: u32, effective: u32, saved: u32) -> i64 {
1776 let ret: i64;
1777 // SAFETY: SYS_setresuid (x86_64 = 117). Trois scalaires `uid_t` ; `readonly`.
1778 unsafe {
1779 core::arch::asm!(
1780 "syscall",
1781 in("rax") 117_i64,
1782 in("rdi") i64::from(real),
1783 in("rsi") i64::from(effective),
1784 in("rdx") i64::from(saved),
1785 lateout("rax") ret,
1786 lateout("rcx") _,
1787 lateout("r11") _,
1788 options(nostack, preserves_flags, readonly),
1789 );
1790 }
1791 ret
1792}
1793
1794#[cfg(target_arch = "aarch64")]
1795#[inline]
1796unsafe fn raw_syscall_setresuid(real: u32, effective: u32, saved: u32) -> i64 {
1797 let ret: i64;
1798 // SAFETY: SYS_setresuid (aarch64 = 147). Trois scalaires ; pas d'accès mém.
1799 unsafe {
1800 core::arch::asm!(
1801 "svc 0",
1802 in("x8") 147_i64,
1803 inout("x0") i64::from(real) => ret,
1804 in("x1") i64::from(effective),
1805 in("x2") i64::from(saved),
1806 options(nostack, preserves_flags, readonly),
1807 );
1808 }
1809 ret
1810}
1811
1812#[cfg(target_arch = "x86_64")]
1813#[inline]
1814unsafe fn raw_syscall_getresgid(real: u64, effective: u64, saved: u64) -> i64 {
1815 let ret: i64;
1816 // SAFETY: SYS_getresgid (x86_64 = 120). **Écrit** trois `gid_t` aux trois
1817 // pointeurs (pas de `readonly`).
1818 unsafe {
1819 core::arch::asm!(
1820 "syscall",
1821 in("rax") 120_i64,
1822 in("rdi") real,
1823 in("rsi") effective,
1824 in("rdx") saved,
1825 lateout("rax") ret,
1826 lateout("rcx") _,
1827 lateout("r11") _,
1828 options(nostack, preserves_flags),
1829 );
1830 }
1831 ret
1832}
1833
1834#[cfg(target_arch = "aarch64")]
1835#[inline]
1836unsafe fn raw_syscall_getresgid(real: u64, effective: u64, saved: u64) -> i64 {
1837 let ret: i64;
1838 // SAFETY: SYS_getresgid (aarch64 = 150). Écrit trois `gid_t` (pas readonly).
1839 unsafe {
1840 core::arch::asm!(
1841 "svc 0",
1842 in("x8") 150_i64,
1843 inout("x0") real => ret,
1844 in("x1") effective,
1845 in("x2") saved,
1846 options(nostack, preserves_flags),
1847 );
1848 }
1849 ret
1850}
1851
1852#[cfg(target_arch = "x86_64")]
1853#[inline]
1854unsafe fn raw_syscall_getresuid(real: u64, effective: u64, saved: u64) -> i64 {
1855 let ret: i64;
1856 // SAFETY: SYS_getresuid (x86_64 = 118). **Écrit** trois `uid_t` aux trois
1857 // pointeurs (pas de `readonly`).
1858 unsafe {
1859 core::arch::asm!(
1860 "syscall",
1861 in("rax") 118_i64,
1862 in("rdi") real,
1863 in("rsi") effective,
1864 in("rdx") saved,
1865 lateout("rax") ret,
1866 lateout("rcx") _,
1867 lateout("r11") _,
1868 options(nostack, preserves_flags),
1869 );
1870 }
1871 ret
1872}
1873
1874#[cfg(target_arch = "aarch64")]
1875#[inline]
1876unsafe fn raw_syscall_getresuid(real: u64, effective: u64, saved: u64) -> i64 {
1877 let ret: i64;
1878 // SAFETY: SYS_getresuid (aarch64 = 148). Écrit trois `uid_t` (pas readonly).
1879 unsafe {
1880 core::arch::asm!(
1881 "svc 0",
1882 in("x8") 148_i64,
1883 inout("x0") real => ret,
1884 in("x1") effective,
1885 in("x2") saved,
1886 options(nostack, preserves_flags),
1887 );
1888 }
1889 ret
1890}
1891
1892// ─────────────────────────────────────────────────────────────────────────
1893// prctl (sous-section 5 de family-process.md).
1894//
1895// Convention 3 ADR-021 : `prctl(op, args...)` n'est PAS exposé. Chaque
1896// opération est exposée comme fonction dédiée typée. Le helper
1897// `raw_syscall_prctl` est strictement interne au module.
1898// ─────────────────────────────────────────────────────────────────────────
1899
1900// Opérations `prctl(2)` exposées par cette PR (cf. `include/uapi/linux/prctl.h`).
1901const PR_SET_PDEATHSIG: i32 = 1;
1902const PR_GET_PDEATHSIG: i32 = 2;
1903const PR_GET_DUMPABLE: i32 = 3;
1904const PR_SET_DUMPABLE: i32 = 4;
1905const PR_GET_KEEPCAPS: i32 = 7;
1906const PR_SET_KEEPCAPS: i32 = 8;
1907const PR_SET_NAME: i32 = 15;
1908const PR_GET_NAME: i32 = 16;
1909const PR_CAPBSET_READ: i32 = 23;
1910const PR_CAPBSET_DROP: i32 = 24;
1911const PR_SET_TIMERSLACK: i32 = 29;
1912const PR_SET_NO_NEW_PRIVS: i32 = 38;
1913const PR_GET_NO_NEW_PRIVS: i32 = 39;
1914const PR_CAP_AMBIENT: i32 = 47;
1915
1916// Sous-opérations de PR_CAP_AMBIENT (arg2).
1917const PR_CAP_AMBIENT_IS_SET: u64 = 1;
1918const PR_CAP_AMBIENT_RAISE: u64 = 2;
1919const PR_CAP_AMBIENT_LOWER: u64 = 3;
1920const PR_CAP_AMBIENT_CLEAR_ALL: u64 = 4;
1921
1922/// Définit le signal envoyé au processus courant à la mort de son parent.
1923///
1924/// `None` désactive la notification (le kernel envoie 0, traité comme
1925/// « aucun signal »).
1926///
1927/// # Errors
1928///
1929/// - [`Errno::EINVAL`] : signal hors plage.
1930pub fn set_parent_death_signal(signal: Option<Signal>) -> Result<(), Errno> {
1931 let sig_arg: u64 = signal.map_or(0_u64, |s| {
1932 // s.as_raw() > 0 par construction (NonZeroI32). Cast sign-safe.
1933 #[allow(clippy::cast_sign_loss)]
1934 {
1935 s.as_raw() as u64
1936 }
1937 });
1938 // SAFETY: PR_SET_PDEATHSIG — pas d'accès mémoire utilisateur.
1939 let ret = unsafe { raw_syscall_prctl(PR_SET_PDEATHSIG, sig_arg, 0, 0, 0) };
1940 if ret < 0 {
1941 return Err(errno_from_negative_syscall_ret(ret));
1942 }
1943 Ok(())
1944}
1945
1946/// Retourne le signal envoyé au processus à la mort du parent.
1947/// `Ok(None)` si la notification est désactivée.
1948///
1949/// # Errors
1950///
1951/// - [`Errno::EINVAL`] : ne se produit pas pour PR_GET_PDEATHSIG en
1952/// pratique.
1953pub fn get_parent_death_signal() -> Result<Option<Signal>, Errno> {
1954 // Le kernel écrit le signal dans `*(int *)arg2`.
1955 let mut out: i32 = 0;
1956 let out_ptr: *mut i32 = &mut out;
1957 // SAFETY: PR_GET_PDEATHSIG écrit un int à arg2 ; `out` est local
1958 // valide pour toute la durée du syscall.
1959 let ret = unsafe { raw_syscall_prctl(PR_GET_PDEATHSIG, out_ptr as u64, 0, 0, 0) };
1960 if ret < 0 {
1961 return Err(errno_from_negative_syscall_ret(ret));
1962 }
1963 Ok(Signal::try_from_raw(out))
1964}
1965
1966/// Active le bit `no_new_privs` du processus.
1967///
1968/// **Opération irréversible.** Une fois positionné, ce bit ne peut pas
1969/// être effacé pour la durée de vie du processus — toute tentative
1970/// (`set_no_new_privs` après coup, ou via execve d'un binaire SUID) est
1971/// neutralisée par le kernel. Conséquence pour les tests : exécuter
1972/// `set_no_new_privs()` dans un test contamine **tous les tests
1973/// ultérieurs** dans la même process. Cette PR ne teste donc que
1974/// `get_no_new_privs()` ; le set est laissé à la PR `family-security`
1975/// (seccomp) qui s'exécute dans un processus enfant fork'é.
1976///
1977/// # Errors
1978///
1979/// - [`Errno::EINVAL`] : ne se produit pas en pratique.
1980pub fn set_no_new_privs() -> Result<(), Errno> {
1981 // arg2 doit être 1 selon le contrat kernel ; toute autre valeur
1982 // retourne EINVAL.
1983 // SAFETY: PR_SET_NO_NEW_PRIVS — pas d'accès mémoire utilisateur.
1984 let ret = unsafe { raw_syscall_prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
1985 if ret < 0 {
1986 return Err(errno_from_negative_syscall_ret(ret));
1987 }
1988 Ok(())
1989}
1990
1991/// Indique si le bit `no_new_privs` est positionné pour le processus.
1992///
1993/// # Errors
1994///
1995/// - Aucune en pratique pour cette opération.
1996pub fn get_no_new_privs() -> Result<bool, Errno> {
1997 // SAFETY: PR_GET_NO_NEW_PRIVS — pas d'accès mémoire utilisateur ; la
1998 // valeur est retournée comme code de retour du syscall (0 ou 1).
1999 let ret = unsafe { raw_syscall_prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) };
2000 if ret < 0 {
2001 return Err(errno_from_negative_syscall_ret(ret));
2002 }
2003 Ok(ret != 0)
2004}
2005
2006/// Définit le nom du thread (max 15 caractères + NUL).
2007///
2008/// Le kernel tronque silencieusement à 15 caractères si `name` est plus
2009/// long. Le nom est utilisé par `/proc/PID/task/TID/comm` et exposé par
2010/// `top`/`ps -L`.
2011///
2012/// # Errors
2013///
2014/// - `EFAULT` : pointeur invalide ; ne se produit pas via l'API safe.
2015pub fn set_thread_name(name: &CStr) -> Result<(), Errno> {
2016 let ptr = name.as_ptr();
2017 // SAFETY: PR_SET_NAME lit la C-string à arg2 (kernel tronque à 16
2018 // octets). `name` est un &CStr valide pour la durée du call.
2019 let ret = unsafe { raw_syscall_prctl(PR_SET_NAME, ptr as u64, 0, 0, 0) };
2020 if ret < 0 {
2021 return Err(errno_from_negative_syscall_ret(ret));
2022 }
2023 Ok(())
2024}
2025
2026/// Retourne le nom du thread courant.
2027///
2028/// # Allocation
2029///
2030/// Convention 4 ADR-021 exception explicite : « la sémantique exige un
2031/// type owned ». Le kernel écrit ≤ 16 octets dans un buffer transient ;
2032/// le caller récupère une `CString` qui peut sortir du frame. Allocation
2033/// bornée à 16 octets (un seul `Box<[u8]>`), happy-path.
2034///
2035/// # Errors
2036///
2037/// - Aucune en pratique pour cette opération.
2038pub fn get_thread_name() -> Result<CString, Errno> {
2039 let mut buffer: [u8; 16] = [0; 16];
2040 let ptr: *mut u8 = buffer.as_mut_ptr();
2041 // SAFETY: PR_GET_NAME écrit exactement 16 octets dans `*(char *)arg2` ;
2042 // `buffer` est local valide pour la durée du call. Le kernel garantit la
2043 // terminaison NUL (en tronquant si nécessaire).
2044 let ret = unsafe { raw_syscall_prctl(PR_GET_NAME, ptr as u64, 0, 0, 0) };
2045 if ret < 0 {
2046 return Err(errno_from_negative_syscall_ret(ret));
2047 }
2048 // Le buffer est garanti contenir au moins un NUL (le kernel tronque
2049 // à 15 caractères + NUL). On extrait la C-string jusqu'au premier NUL.
2050 let length = buffer.iter().position(|&b| b == 0).unwrap_or(16);
2051 // CString::new exige absence de NUL interne ; on passe les bytes
2052 // *avant* le NUL trouvé.
2053 Ok(CString::new(&buffer[..length])
2054 .expect("buffer kernel sans NUL interne avant le terminateur"))
2055}
2056
2057/// Définit le mode `dumpable` du processus.
2058///
2059/// # Errors
2060///
2061/// - [`Errno::EINVAL`] : valeur hors plage (ne se produit pas via l'API
2062/// typée).
2063pub fn set_dumpable(mode: DumpableMode) -> Result<(), Errno> {
2064 let mode_arg = u64::from(mode as u32);
2065 // SAFETY: PR_SET_DUMPABLE — pas d'accès mémoire utilisateur.
2066 let ret = unsafe { raw_syscall_prctl(PR_SET_DUMPABLE, mode_arg, 0, 0, 0) };
2067 if ret < 0 {
2068 return Err(errno_from_negative_syscall_ret(ret));
2069 }
2070 Ok(())
2071}
2072
2073/// Retourne le mode `dumpable` actuel.
2074///
2075/// # Errors
2076///
2077/// - Aucune en pratique pour cette opération.
2078pub fn get_dumpable() -> Result<DumpableMode, Errno> {
2079 // SAFETY: PR_GET_DUMPABLE — pas d'accès mémoire utilisateur ;
2080 // valeur retournée comme code de retour.
2081 let ret = unsafe { raw_syscall_prctl(PR_GET_DUMPABLE, 0, 0, 0, 0) };
2082 if ret < 0 {
2083 return Err(errno_from_negative_syscall_ret(ret));
2084 }
2085 match ret {
2086 0 => Ok(DumpableMode::NotDumpable),
2087 1 => Ok(DumpableMode::Dumpable),
2088 2 => Ok(DumpableMode::SuidDumpable),
2089 // Le kernel garantit 0, 1, ou 2. Sortie hors plage = bug kernel.
2090 other => unreachable!("PR_GET_DUMPABLE a retourné {other}, hors plage [0,2]"),
2091 }
2092}
2093
2094/// Définit le bit `keep_caps` : si vrai, les capabilities `permitted`
2095/// sont conservées lors d'un changement d'UID effectif vers non-zéro.
2096///
2097/// # Errors
2098///
2099/// - [`Errno::EINVAL`] : valeur invalide (ne se produit pas via l'API typée).
2100pub fn set_keep_caps(keep: bool) -> Result<(), Errno> {
2101 let arg: u64 = u64::from(keep);
2102 // SAFETY: PR_SET_KEEPCAPS — pas d'accès mémoire utilisateur.
2103 let ret = unsafe { raw_syscall_prctl(PR_SET_KEEPCAPS, arg, 0, 0, 0) };
2104 if ret < 0 {
2105 return Err(errno_from_negative_syscall_ret(ret));
2106 }
2107 Ok(())
2108}
2109
2110/// Indique si le bit `keep_caps` est positionné.
2111///
2112/// # Errors
2113///
2114/// - Aucune en pratique pour cette opération.
2115pub fn get_keep_caps() -> Result<bool, Errno> {
2116 // SAFETY: PR_GET_KEEPCAPS — pas d'accès mémoire utilisateur.
2117 let ret = unsafe { raw_syscall_prctl(PR_GET_KEEPCAPS, 0, 0, 0, 0) };
2118 if ret < 0 {
2119 return Err(errno_from_negative_syscall_ret(ret));
2120 }
2121 Ok(ret != 0)
2122}
2123
2124/// Définit la « timer slack » du processus (nanosecondes).
2125///
2126/// Le timer slack permet au kernel d'agréger les timers proches pour
2127/// réduire les wake-ups. `0` réinitialise à la valeur par défaut héritée.
2128///
2129/// # Errors
2130///
2131/// - Aucune en pratique.
2132pub fn set_timer_slack(slack_ns: u64) -> Result<(), Errno> {
2133 // SAFETY: PR_SET_TIMERSLACK — pas d'accès mémoire utilisateur.
2134 let ret = unsafe { raw_syscall_prctl(PR_SET_TIMERSLACK, slack_ns, 0, 0, 0) };
2135 if ret < 0 {
2136 return Err(errno_from_negative_syscall_ret(ret));
2137 }
2138 Ok(())
2139}
2140
2141/// Ajoute `cap` à l'ensemble ambient du thread.
2142///
2143/// L'ensemble ambient est transmis à `execve`, contrairement aux
2144/// inheritable seuls. Exige `cap` dans `permitted ∩ inheritable`.
2145///
2146/// # Errors
2147///
2148/// - [`Errno::EPERM`] : `cap` n'est pas dans permitted ∩ inheritable, ou
2149/// `SECBIT_NO_CAP_AMBIENT_RAISE` est positionné.
2150/// - [`Errno::EINVAL`] : valeur de capability inconnue côté kernel.
2151pub fn cap_ambient_raise(cap: Capability) -> Result<(), Errno> {
2152 // SAFETY: PR_CAP_AMBIENT/RAISE — pas d'accès mémoire utilisateur.
2153 let ret = unsafe {
2154 raw_syscall_prctl(
2155 PR_CAP_AMBIENT,
2156 PR_CAP_AMBIENT_RAISE,
2157 u64::from(cap.as_raw()),
2158 0,
2159 0,
2160 )
2161 };
2162 if ret < 0 {
2163 return Err(errno_from_negative_syscall_ret(ret));
2164 }
2165 Ok(())
2166}
2167
2168/// Retire `cap` de l'ensemble ambient.
2169///
2170/// # Errors
2171///
2172/// - [`Errno::EINVAL`] : valeur de capability inconnue côté kernel.
2173pub fn cap_ambient_lower(cap: Capability) -> Result<(), Errno> {
2174 // SAFETY: PR_CAP_AMBIENT/LOWER — pas d'accès mémoire utilisateur.
2175 let ret = unsafe {
2176 raw_syscall_prctl(
2177 PR_CAP_AMBIENT,
2178 PR_CAP_AMBIENT_LOWER,
2179 u64::from(cap.as_raw()),
2180 0,
2181 0,
2182 )
2183 };
2184 if ret < 0 {
2185 return Err(errno_from_negative_syscall_ret(ret));
2186 }
2187 Ok(())
2188}
2189
2190/// Vide totalement l'ensemble ambient du thread.
2191///
2192/// # Errors
2193///
2194/// - Aucune en pratique.
2195pub fn cap_ambient_clear_all() -> Result<(), Errno> {
2196 // SAFETY: PR_CAP_AMBIENT/CLEAR_ALL — pas d'accès mémoire utilisateur.
2197 let ret = unsafe { raw_syscall_prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) };
2198 if ret < 0 {
2199 return Err(errno_from_negative_syscall_ret(ret));
2200 }
2201 Ok(())
2202}
2203
2204/// Indique si `cap` est dans l'ensemble ambient.
2205///
2206/// # Errors
2207///
2208/// - [`Errno::EINVAL`] : valeur de capability inconnue côté kernel.
2209pub fn cap_ambient_is_set(cap: Capability) -> Result<bool, Errno> {
2210 // SAFETY: PR_CAP_AMBIENT/IS_SET — pas d'accès mémoire utilisateur ;
2211 // valeur retournée comme code de retour (0 ou 1).
2212 let ret = unsafe {
2213 raw_syscall_prctl(
2214 PR_CAP_AMBIENT,
2215 PR_CAP_AMBIENT_IS_SET,
2216 u64::from(cap.as_raw()),
2217 0,
2218 0,
2219 )
2220 };
2221 if ret < 0 {
2222 return Err(errno_from_negative_syscall_ret(ret));
2223 }
2224 Ok(ret != 0)
2225}
2226
2227/// Teste si `cap` est présente dans le **bounding set** du processus
2228/// (`prctl(PR_CAPBSET_READ)`). Non destructif.
2229///
2230/// # Errors
2231///
2232/// - [`Errno::EINVAL`] : `cap` n'est pas une capability reconnue du kernel.
2233pub fn capbset_read(cap: Capability) -> Result<bool, Errno> {
2234 // SAFETY: PR_CAPBSET_READ — pas d'accès mémoire utilisateur ; résultat (0/1)
2235 // rendu comme code de retour.
2236 let ret = unsafe { raw_syscall_prctl(PR_CAPBSET_READ, u64::from(cap.as_raw()), 0, 0, 0) };
2237 if ret < 0 {
2238 return Err(errno_from_negative_syscall_ret(ret));
2239 }
2240 Ok(ret != 0)
2241}
2242
2243/// Retire `cap` du **bounding set** du processus (`prctl(PR_CAPBSET_DROP)`).
2244///
2245/// **Irréversible** : une capability retirée du bounding set ne peut plus jamais être
2246/// ajoutée à l'ensemble *permitted* d'un descendant, **même** par un processus root.
2247/// Durcissement de moindre autorité du monitor (ADR-122 §6) : après avoir réduit ses
2248/// ensembles à la cible, le monitor largue du bounding set tout ce qu'il n'utilisera
2249/// jamais, se privant définitivement de la capacité de le regagner.
2250///
2251/// # Errors
2252///
2253/// - [`Errno::EPERM`] : l'appelant n'a pas `CAP_SETPCAP`.
2254/// - [`Errno::EINVAL`] : `cap` n'est pas une capability reconnue du kernel.
2255pub fn capbset_drop(cap: Capability) -> Result<(), Errno> {
2256 // SAFETY: PR_CAPBSET_DROP — pas d'accès mémoire utilisateur ; arg2 = numéro de cap.
2257 let ret = unsafe { raw_syscall_prctl(PR_CAPBSET_DROP, u64::from(cap.as_raw()), 0, 0, 0) };
2258 if ret < 0 {
2259 return Err(errno_from_negative_syscall_ret(ret));
2260 }
2261 Ok(())
2262}
2263
2264#[cfg(target_arch = "x86_64")]
2265#[inline]
2266unsafe fn raw_syscall_prctl(op: i32, arg2: u64, arg3: u64, arg4: u64, arg5: u64) -> i64 {
2267 let ret: i64;
2268 // SAFETY: SYS_prctl (x86_64 = 157). Selon `op`, le kernel peut lire
2269 // ou écrire à des pointeurs passés via arg2..arg5 ; les fonctions
2270 // d'opération individuelles (au-dessus) garantissent la validité de
2271 // ces pointeurs lorsqu'elles en passent. Sans pointeur, pas
2272 // d'accès mémoire utilisateur.
2273 //
2274 // **Pas de `readonly`** : certaines opérations (PR_GET_NAME,
2275 // PR_GET_PDEATHSIG) font écrire le kernel à un pointeur user.
2276 unsafe {
2277 core::arch::asm!(
2278 "syscall",
2279 in("rax") 157_i64,
2280 in("rdi") i64::from(op),
2281 in("rsi") arg2,
2282 in("rdx") arg3,
2283 in("r10") arg4,
2284 in("r8") arg5,
2285 lateout("rax") ret,
2286 lateout("rcx") _,
2287 lateout("r11") _,
2288 options(nostack, preserves_flags),
2289 );
2290 }
2291 ret
2292}
2293
2294#[cfg(target_arch = "aarch64")]
2295#[inline]
2296unsafe fn raw_syscall_prctl(op: i32, arg2: u64, arg3: u64, arg4: u64, arg5: u64) -> i64 {
2297 let ret: i64;
2298 // SAFETY: SYS_prctl (aarch64 = 167). Mêmes considérations que x86_64.
2299 unsafe {
2300 core::arch::asm!(
2301 "svc 0",
2302 in("x8") 167_i64,
2303 inout("x0") i64::from(op) => ret,
2304 in("x1") arg2,
2305 in("x2") arg3,
2306 in("x3") arg4,
2307 in("x4") arg5,
2308 options(nostack, preserves_flags),
2309 );
2310 }
2311 ret
2312}
2313
2314// ─────────────────────────────────────────────────────────────────────────
2315// rlimits (sous-section 6 de family-process.md).
2316//
2317// `prlimit64` est le syscall préféré ; `getrlimit`/`setrlimit` legacy
2318// sont exposés mais routés au plus simple. La représentation kernel de
2319// `struct rlimit64` est { u64 rlim_cur ; u64 rlim_max }, identique à
2320// `struct rlimit` sur LP64. On la traduit en `Rlimit` via mapping
2321// `RLIM_INFINITY <-> RlimitValue::Infinity`.
2322// ─────────────────────────────────────────────────────────────────────────
2323
2324#[repr(C)]
2325#[derive(Default, Clone, Copy)]
2326struct KernelRlimit64 {
2327 rlim_cur: u64,
2328 rlim_max: u64,
2329}
2330
2331const RLIM_INFINITY: u64 = u64::MAX;
2332
2333fn rlim_value_to_kernel(v: RlimitValue) -> u64 {
2334 match v {
2335 RlimitValue::Finite(n) => n,
2336 RlimitValue::Infinity => RLIM_INFINITY,
2337 }
2338}
2339
2340fn rlim_value_from_kernel(n: u64) -> RlimitValue {
2341 if n == RLIM_INFINITY {
2342 RlimitValue::Infinity
2343 } else {
2344 RlimitValue::Finite(n)
2345 }
2346}
2347
2348fn rlimit_to_kernel(r: Rlimit) -> KernelRlimit64 {
2349 KernelRlimit64 {
2350 rlim_cur: rlim_value_to_kernel(r.soft),
2351 rlim_max: rlim_value_to_kernel(r.hard),
2352 }
2353}
2354
2355fn rlimit_from_kernel(k: KernelRlimit64) -> Rlimit {
2356 Rlimit {
2357 soft: rlim_value_from_kernel(k.rlim_cur),
2358 hard: rlim_value_from_kernel(k.rlim_max),
2359 }
2360}
2361
2362/// Retourne la `Rlimit` courante du processus pour `resource`.
2363///
2364/// Air recommande [`prlimit`] qui est plus expressif (cible un autre
2365/// processus, opération atomique) ; `getrlimit` est exposé pour
2366/// compatibilité.
2367///
2368/// # Errors
2369///
2370/// - [`Errno::EINVAL`] : ressource inconnue (impossible via l'API typée).
2371pub fn getrlimit(resource: Resource) -> Result<Rlimit, Errno> {
2372 // Délègue à prlimit(self, resource, None) pour ne pas dupliquer
2373 // l'asm! et bénéficier de la même conversion.
2374 prlimit(None, resource, None)
2375}
2376
2377/// Définit la `Rlimit` courante du processus pour `resource`.
2378///
2379/// # Errors
2380///
2381/// - [`Errno::EINVAL`] : valeurs incohérentes (soft > hard, …).
2382/// - [`Errno::EPERM`] : tentative de relever la hard limit sans
2383/// `CAP_SYS_RESOURCE`.
2384pub fn setrlimit(resource: Resource, limit: Rlimit) -> Result<(), Errno> {
2385 // Délègue à prlimit(self, resource, Some(limit)) ; on ignore l'ancienne
2386 // valeur retournée.
2387 let _ = prlimit(None, resource, Some(limit))?;
2388 Ok(())
2389}
2390
2391/// `prlimit64` : lit et/ou modifie atomiquement la `Rlimit` d'un
2392/// processus.
2393///
2394/// `pid = None` désigne le processus courant. Retourne **toujours** la
2395/// limite *avant* modification (équivalent kernel "old_limit").
2396///
2397/// # Errors
2398///
2399/// - [`Errno::EINVAL`] : valeurs incohérentes.
2400/// - [`Errno::EPERM`] : tentative de relever la hard limit sans
2401/// `CAP_SYS_RESOURCE`, ou ciblage d'un autre processus sans droits.
2402/// - [`Errno::ESRCH`] : `pid` n'existe pas.
2403pub fn prlimit(
2404 pid: Option<Pid>,
2405 resource: Resource,
2406 new_limit: Option<Rlimit>,
2407) -> Result<Rlimit, Errno> {
2408 let pid_arg = pid.map_or(0_i32, |p| p.as_raw());
2409
2410 let new_buf: Option<KernelRlimit64> = new_limit.map(rlimit_to_kernel);
2411 let new_ptr: u64 = match &new_buf {
2412 Some(b) => {
2413 let p: *const KernelRlimit64 = b;
2414 p as u64
2415 }
2416 None => 0,
2417 };
2418
2419 let mut old_buf = KernelRlimit64::default();
2420 let old_ptr: *mut KernelRlimit64 = &mut old_buf;
2421
2422 // SAFETY: prlimit64(2) lit `new_limit` si non-null, écrit `old_limit`
2423 // si non-null. Nous fournissons toujours `old_ptr` valide ; `new_ptr`
2424 // est null ou pointe sur `new_buf` valide pour la durée du call.
2425 let ret = unsafe { raw_syscall_prlimit64(pid_arg, resource as u32, new_ptr, old_ptr as u64) };
2426 if ret < 0 {
2427 return Err(errno_from_negative_syscall_ret(ret));
2428 }
2429 Ok(rlimit_from_kernel(old_buf))
2430}
2431
2432#[cfg(target_arch = "x86_64")]
2433#[inline]
2434unsafe fn raw_syscall_prlimit64(pid: i32, resource: u32, new_limit: u64, old_limit: u64) -> i64 {
2435 let ret: i64;
2436 // SAFETY: SYS_prlimit64 (x86_64 = 302). Le kernel lit `new_limit`
2437 // (16 octets) si non-null ; écrit `old_limit` (16 octets) si non-null.
2438 // Pas de `readonly` (écriture sur `*old_limit`).
2439 unsafe {
2440 core::arch::asm!(
2441 "syscall",
2442 in("rax") 302_i64,
2443 in("rdi") i64::from(pid),
2444 in("rsi") i64::from(resource),
2445 in("rdx") new_limit,
2446 in("r10") old_limit,
2447 lateout("rax") ret,
2448 lateout("rcx") _,
2449 lateout("r11") _,
2450 options(nostack, preserves_flags),
2451 );
2452 }
2453 ret
2454}
2455
2456#[cfg(target_arch = "aarch64")]
2457#[inline]
2458unsafe fn raw_syscall_prlimit64(pid: i32, resource: u32, new_limit: u64, old_limit: u64) -> i64 {
2459 let ret: i64;
2460 // SAFETY: SYS_prlimit64 (aarch64 = 261). Mêmes considérations.
2461 unsafe {
2462 core::arch::asm!(
2463 "svc 0",
2464 in("x8") 261_i64,
2465 inout("x0") i64::from(pid) => ret,
2466 in("x1") i64::from(resource),
2467 in("x2") new_limit,
2468 in("x3") old_limit,
2469 options(nostack, preserves_flags),
2470 );
2471 }
2472 ret
2473}
2474
2475// ─────────────────────────────────────────────────────────────────────────
2476// capget / capset (sous-section 7 de family-process.md).
2477//
2478// Version 3 capabilities (kernel ≥ 2.6.25) : header + array de 2 data
2479// structs ; chaque data struct contient `(effective, permitted,
2480// inheritable)` en `u32`. Index 0 = bits 0-31, index 1 = bits 32-63.
2481//
2482// La conversion `CapabilityMask (u64) ↔ (lo, hi)` est faite par les
2483// helpers privés `mask_to_words` / `words_to_mask` ci-dessous, testés
2484// explicitement pour qu'une éventuelle inversion lo/hi soit détectée
2485// (cf. JOURNAL session 2026-05-24).
2486// ─────────────────────────────────────────────────────────────────────────
2487
2488const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
2489
2490#[repr(C)]
2491struct KernelCapHeader {
2492 version: u32,
2493 pid: i32,
2494}
2495
2496#[repr(C)]
2497#[derive(Default, Clone, Copy)]
2498struct KernelCapData {
2499 effective: u32,
2500 permitted: u32,
2501 inheritable: u32,
2502}
2503
2504fn mask_to_words(mask: CapabilityMask) -> (u32, u32) {
2505 let bits = mask.bits();
2506 // Troncature exacte : `bits & 0xFFFF_FFFF` fit dans u32 par
2507 // construction.
2508 #[allow(clippy::cast_possible_truncation)]
2509 let lo = (bits & 0xFFFF_FFFF) as u32;
2510 // Troncature exacte : après `>> 32`, la valeur résiduelle est dans
2511 // `[0, u32::MAX]`.
2512 #[allow(clippy::cast_possible_truncation)]
2513 let hi = (bits >> 32) as u32;
2514 (lo, hi)
2515}
2516
2517fn words_to_mask(lo: u32, hi: u32) -> CapabilityMask {
2518 let bits = (u64::from(hi) << 32) | u64::from(lo);
2519 CapabilityMask::from_bits(bits)
2520}
2521
2522fn cap_target_to_kernel_pid(target: CapabilityTarget) -> i32 {
2523 match target {
2524 CapabilityTarget::CurrentThread => 0,
2525 CapabilityTarget::Thread(tid) => tid.as_raw(),
2526 CapabilityTarget::Process(pid) => pid.as_raw(),
2527 }
2528}
2529
2530/// Lit les capabilities du thread cible.
2531///
2532/// # Errors
2533///
2534/// - [`Errno::EINVAL`] : version capabilities non supportée par le kernel
2535/// (Air utilise version 3, supportée depuis 2.6.25).
2536/// - [`Errno::EPERM`] : ciblage d'un thread sans droits (rare en lecture).
2537pub fn capget(target: CapabilityTarget) -> Result<CapabilitySet, Errno> {
2538 let mut header = KernelCapHeader {
2539 version: LINUX_CAPABILITY_VERSION_3,
2540 pid: cap_target_to_kernel_pid(target),
2541 };
2542 let mut data: [KernelCapData; 2] = [KernelCapData::default(); 2];
2543 let header_ptr: *mut KernelCapHeader = &mut header;
2544 let data_ptr: *mut KernelCapData = data.as_mut_ptr();
2545 // SAFETY: capget(2) lit `*header` et écrit deux `KernelCapData`
2546 // consécutives à `data_ptr` ; les deux pointeurs sont locaux et
2547 // valides pour la durée du call.
2548 let ret = unsafe { raw_syscall_capget(header_ptr as u64, data_ptr as u64) };
2549 if ret < 0 {
2550 return Err(errno_from_negative_syscall_ret(ret));
2551 }
2552 Ok(CapabilitySet {
2553 effective: words_to_mask(data[0].effective, data[1].effective),
2554 permitted: words_to_mask(data[0].permitted, data[1].permitted),
2555 inheritable: words_to_mask(data[0].inheritable, data[1].inheritable),
2556 })
2557}
2558
2559/// Écrit les capabilities du thread cible.
2560///
2561/// # Errors
2562///
2563/// - [`Errno::EINVAL`] : version capabilities ou layout invalides.
2564/// - [`Errno::EPERM`] : tentative d'élever une capability hors permitted,
2565/// ou modification d'un autre thread sans droits.
2566pub fn capset(target: CapabilityTarget, set: &CapabilitySet) -> Result<(), Errno> {
2567 let mut header = KernelCapHeader {
2568 version: LINUX_CAPABILITY_VERSION_3,
2569 pid: cap_target_to_kernel_pid(target),
2570 };
2571 let (e_lo, e_hi) = mask_to_words(set.effective);
2572 let (p_lo, p_hi) = mask_to_words(set.permitted);
2573 let (i_lo, i_hi) = mask_to_words(set.inheritable);
2574 let data: [KernelCapData; 2] = [
2575 KernelCapData {
2576 effective: e_lo,
2577 permitted: p_lo,
2578 inheritable: i_lo,
2579 },
2580 KernelCapData {
2581 effective: e_hi,
2582 permitted: p_hi,
2583 inheritable: i_hi,
2584 },
2585 ];
2586 let header_ptr: *mut KernelCapHeader = &mut header;
2587 let data_ptr: *const KernelCapData = data.as_ptr();
2588 // SAFETY: capset(2) lit `*header` et lit deux `KernelCapData`
2589 // consécutives à `data_ptr`. Tout est local et valide pour la durée
2590 // du call.
2591 let ret = unsafe { raw_syscall_capset(header_ptr as u64, data_ptr as u64) };
2592 if ret < 0 {
2593 return Err(errno_from_negative_syscall_ret(ret));
2594 }
2595 Ok(())
2596}
2597
2598#[cfg(target_arch = "x86_64")]
2599#[inline]
2600unsafe fn raw_syscall_capget(header: u64, data: u64) -> i64 {
2601 let ret: i64;
2602 // SAFETY: SYS_capget (x86_64 = 125). Le kernel lit `*header` (8 octets)
2603 // et écrit 2 × 12 octets à `data`. Pas de `readonly` (écriture).
2604 unsafe {
2605 core::arch::asm!(
2606 "syscall",
2607 in("rax") 125_i64,
2608 in("rdi") header,
2609 in("rsi") data,
2610 lateout("rax") ret,
2611 lateout("rcx") _,
2612 lateout("r11") _,
2613 options(nostack, preserves_flags),
2614 );
2615 }
2616 ret
2617}
2618
2619#[cfg(target_arch = "x86_64")]
2620#[inline]
2621unsafe fn raw_syscall_capset(header: u64, data: u64) -> i64 {
2622 let ret: i64;
2623 // SAFETY: SYS_capset (x86_64 = 126). Lecture seule de `*header` et
2624 // `*data` côté user ; `readonly` correct.
2625 unsafe {
2626 core::arch::asm!(
2627 "syscall",
2628 in("rax") 126_i64,
2629 in("rdi") header,
2630 in("rsi") data,
2631 lateout("rax") ret,
2632 lateout("rcx") _,
2633 lateout("r11") _,
2634 options(nostack, preserves_flags, readonly),
2635 );
2636 }
2637 ret
2638}
2639
2640#[cfg(target_arch = "aarch64")]
2641#[inline]
2642unsafe fn raw_syscall_capget(header: u64, data: u64) -> i64 {
2643 let ret: i64;
2644 // SAFETY: SYS_capget (aarch64 = 90).
2645 unsafe {
2646 core::arch::asm!(
2647 "svc 0",
2648 in("x8") 90_i64,
2649 inout("x0") header => ret,
2650 in("x1") data,
2651 options(nostack, preserves_flags),
2652 );
2653 }
2654 ret
2655}
2656
2657#[cfg(target_arch = "aarch64")]
2658#[inline]
2659unsafe fn raw_syscall_capset(header: u64, data: u64) -> i64 {
2660 let ret: i64;
2661 // SAFETY: SYS_capset (aarch64 = 91). `readonly` car lecture seule.
2662 unsafe {
2663 core::arch::asm!(
2664 "svc 0",
2665 in("x8") 91_i64,
2666 inout("x0") header => ret,
2667 in("x1") data,
2668 options(nostack, preserves_flags, readonly),
2669 );
2670 }
2671 ret
2672}
2673
2674// ─────────────────────────────────────────────────────────────────────────
2675// Helper interne de conversion `errno`.
2676// ─────────────────────────────────────────────────────────────────────────
2677
2678/// Convertit une valeur de retour de syscall strictement négative en
2679/// [`Errno`]. Le kernel borne les errno à `[-4095, -1]` (`MAX_ERRNO`).
2680fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
2681 debug_assert!(ret < 0 && ret > -4096);
2682 // `ret.wrapping_neg()` est donc dans `[1, 4095]` — toujours fit in i32.
2683 #[allow(clippy::cast_possible_truncation)]
2684 let raw = ret.wrapping_neg() as i32;
2685 let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
2686 Errno::from_nonzero(nz)
2687}
2688
2689// ─────────────────────────────────────────────────────────────────────────
2690// Affinité CPU — sched_setaffinity / sched_getaffinity
2691// Cf. `docs/specs/layer-0/family-process-affinity.md`.
2692// ─────────────────────────────────────────────────────────────────────────
2693
2694#[cfg(target_arch = "x86_64")]
2695#[inline]
2696unsafe fn raw_syscall_sched_setaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2697 let ret: i64;
2698 // SAFETY:
2699 // - SYS_sched_setaffinity (x86_64 = 203). Le kernel **lit** `len` octets à
2700 // `mask` (jamais d'écriture) → `readonly` correct ; validité du buffer
2701 // garantie par l'appelant (`CpuSet` local, 128 octets).
2702 // - x86_64 ABI : numéro en RAX, args en RDI/RSI/RDX ; retour RAX ; clobbe
2703 // RCX/R11.
2704 unsafe {
2705 core::arch::asm!(
2706 "syscall",
2707 in("rax") 203_i64,
2708 in("rdi") i64::from(pid),
2709 in("rsi") len,
2710 in("rdx") mask,
2711 lateout("rax") ret,
2712 lateout("rcx") _,
2713 lateout("r11") _,
2714 options(nostack, preserves_flags, readonly),
2715 );
2716 }
2717 ret
2718}
2719
2720#[cfg(target_arch = "aarch64")]
2721#[inline]
2722unsafe fn raw_syscall_sched_setaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2723 let ret: i64;
2724 // SAFETY: voir version x86_64 ; aarch64 ABI : numéro en X8 (= 122), args en
2725 // X0/X1/X2, retour en X0. Le kernel **lit** `mask` → `readonly`.
2726 unsafe {
2727 core::arch::asm!(
2728 "svc 0",
2729 in("x8") 122_i64,
2730 inout("x0") i64::from(pid) => ret,
2731 in("x1") len,
2732 in("x2") mask,
2733 options(nostack, preserves_flags, readonly),
2734 );
2735 }
2736 ret
2737}
2738
2739#[cfg(target_arch = "x86_64")]
2740#[inline]
2741unsafe fn raw_syscall_sched_getaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2742 let ret: i64;
2743 // SAFETY:
2744 // - SYS_sched_getaffinity (x86_64 = 204). Le kernel **écrit** jusqu'à `len`
2745 // octets à `mask` → **pas de `readonly`** ; validité/taille du buffer
2746 // garanties par l'appelant (`CpuSet` local, 128 octets).
2747 // - Retourne le nombre d'octets écrits (≥ 0) ou `-errno`.
2748 unsafe {
2749 core::arch::asm!(
2750 "syscall",
2751 in("rax") 204_i64,
2752 in("rdi") i64::from(pid),
2753 in("rsi") len,
2754 in("rdx") mask,
2755 lateout("rax") ret,
2756 lateout("rcx") _,
2757 lateout("r11") _,
2758 options(nostack, preserves_flags),
2759 );
2760 }
2761 ret
2762}
2763
2764#[cfg(target_arch = "aarch64")]
2765#[inline]
2766unsafe fn raw_syscall_sched_getaffinity(pid: i32, len: u64, mask: u64) -> i64 {
2767 let ret: i64;
2768 // SAFETY: voir version x86_64 ; aarch64 ABI : numéro en X8 (= 123), args en
2769 // X0/X1/X2, retour en X0. Le kernel **écrit** dans `mask` → pas de `readonly`.
2770 unsafe {
2771 core::arch::asm!(
2772 "svc 0",
2773 in("x8") 123_i64,
2774 inout("x0") i64::from(pid) => ret,
2775 in("x1") len,
2776 in("x2") mask,
2777 options(nostack, preserves_flags),
2778 );
2779 }
2780 ret
2781}
2782
2783/// Identifiant de tâche pour les opérations d'affinité : `None` = **tâche
2784/// appelante** (sentinelle kernel `0` typée en `Option`, ADR-021 conv. 1).
2785fn affinity_pid(tid: Option<Tid>) -> i32 {
2786 // `0` = la tâche appelante (jamais exposé comme entier magique).
2787 tid.map_or(0, Tid::as_raw)
2788}
2789
2790/// Fixe l'**affinité CPU** d'une tâche (`sched_setaffinity(2)`) : la tâche ne sera
2791/// ordonnancée que sur les CPU présents dans `cpus`. `tid = None` cible la tâche
2792/// appelante. Débloque `air-thread::cpu_affinity` (couche 1).
2793///
2794/// # Errors
2795///
2796/// - [`Errno::EINVAL`] : `cpus` ne contient **aucun** CPU autorisé/en ligne.
2797/// - [`Errno::ESRCH`] : aucune tâche ne porte ce `tid`.
2798/// - [`Errno::EPERM`] : privilèges insuffisants pour changer l'affinité de `tid`.
2799pub fn set_cpu_affinity(tid: Option<Tid>, cpus: &CpuSet) -> Result<(), Errno> {
2800 let bytes = cpus.as_bytes();
2801 let len = bytes.len() as u64;
2802 let mask = bytes.as_ptr() as u64;
2803 // SAFETY: `mask` pointe les `len` octets (= 128) du `CpuSet` de l'appelant,
2804 // valides en lecture pour la durée de l'appel ; le kernel ne les écrit pas.
2805 let ret = unsafe { raw_syscall_sched_setaffinity(affinity_pid(tid), len, mask) };
2806 if ret < 0 {
2807 return Err(errno_from_negative_syscall_ret(ret));
2808 }
2809 Ok(())
2810}
2811
2812/// Lit l'**affinité CPU** courante d'une tâche (`sched_getaffinity(2)`) **dans**
2813/// `cpus` (buffer fourni, zéro allocation). `tid = None` cible la tâche appelante.
2814///
2815/// # Errors
2816///
2817/// - [`Errno::ESRCH`] : aucune tâche ne porte ce `tid`.
2818/// - [`Errno::EINVAL`] : taille de masque incohérente (ne se produit pas avec un
2819/// `CpuSet` de [`air_sys_types::system::CPU_SETSIZE`] bits).
2820pub fn get_cpu_affinity(tid: Option<Tid>, cpus: &mut CpuSet) -> Result<(), Errno> {
2821 let len = core::mem::size_of::<CpuSet>() as u64;
2822 let mask = core::ptr::from_mut(cpus).cast::<u8>() as u64;
2823 // SAFETY: `mask` pointe les `len` octets (= 128) du `CpuSet` **mutable** de
2824 // l'appelant ; le kernel y écrit au plus `len` octets (le bitmap d'affinité).
2825 // `CpuSet` est `#[repr(C)]` sans padding ⇒ tout motif d'octets est valide.
2826 let ret = unsafe { raw_syscall_sched_getaffinity(affinity_pid(tid), len, mask) };
2827 if ret < 0 {
2828 return Err(errno_from_negative_syscall_ret(ret));
2829 }
2830 Ok(())
2831}
2832
2833// ─────────────────────────────────────────────────────────────────────────
2834// exec / redirection — execve, execveat, dup3, fchdir, chdir.
2835//
2836// Surface pilotée par le contrat de la couche 1 (`air-process`) : lancer un
2837// programme (exec), rediriger ses stdin/stdout/stderr (dup3) et fixer son
2838// répertoire de travail (fchdir / chdir). Cf. `docs/specs/layer-0/
2839// family-process.md`, section « exec / redirection ».
2840// ─────────────────────────────────────────────────────────────────────────
2841
2842/// `AT_FDCWD` : base « répertoire courant » des opérations `*at` (sentinelle
2843/// kernel `-100`, typée [`DirFd::Cwd`] côté Air, cf. ADR-021 convention 1).
2844const AT_FDCWD: i32 = -100;
2845
2846/// Convertit une [`DirFd`] en l'entier attendu par l'ABI syscall (`AT_FDCWD`
2847/// pour [`DirFd::Cwd`], sinon le fd emprunté).
2848fn dirfd_to_raw(dirfd: DirFd<'_>) -> i32 {
2849 match dirfd {
2850 DirFd::Cwd => AT_FDCWD,
2851 DirFd::Fd(fd) => fd.as_raw_fd(),
2852 }
2853}
2854
2855/// Tableau de pointeurs C terminé par `NULL`, pour le marshalling d'`argv` /
2856/// `envp` vers `execve(2)` / `execveat(2)`.
2857///
2858/// Ces syscalls attendent un argument de type `char *const argv[]` : une suite
2859/// de pointeurs vers des chaînes C **terminée par un pointeur `NULL`**. Ce type
2860/// matérialise cette **frontière de marshalling**, qui est *fuzzée*
2861/// (`fuzz/fuzz_targets/fuzz_exec_argv.rs`) : construction depuis une liste
2862/// arbitraire (vide, longue), terminaison `NULL` toujours présente, ordre
2863/// préservé.
2864///
2865/// **Rejet des NUL embarqués — par construction.** Un argument C ne peut pas
2866/// contenir d'octet NUL (il y serait interprété comme terminateur). L'entrée
2867/// étant une tranche de [`CStr`] — type qui **garantit** l'absence de NUL
2868/// interne — l'invariant est assuré *en amont*, là où l'appelant construit ses
2869/// `&CStr` (typiquement via `CStr::from_bytes_with_nul` ou `CString::new`, qui
2870/// rejettent proprement un NUL embarqué). Aucune revalidation n'est donc
2871/// nécessaire ici (Principe 4 : « parse, don't validate »).
2872///
2873/// **Allocation.** La construction alloue un [`Vec`] de `items.len() + 1`
2874/// pointeurs : c'est l'exception d'allocation documentée d'ADR-021 §4 (buffer
2875/// dynamique *intrinsèque* — sa taille dépend du nombre d'arguments, inconnu à
2876/// la compilation). Les chaînes elles-mêmes ne sont **pas** copiées : les
2877/// pointeurs empruntent les [`CStr`] fournis (d'où la durée de vie `'a`).
2878pub struct CStrArray<'a> {
2879 /// Pointeurs vers chaque `CStr`, suivis d'un pointeur `NULL` final.
2880 /// Invariant : `ptrs.len() >= 1` et `ptrs[ptrs.len() - 1].is_null()`.
2881 ptrs: Vec<*const c_char>,
2882 /// Lie la durée de vie des pointeurs à celle de la tranche source.
2883 _borrow: PhantomData<&'a [&'a CStr]>,
2884}
2885
2886impl<'a> CStrArray<'a> {
2887 /// Construit le tableau `NULL`-terminé à partir de `items`.
2888 ///
2889 /// Une tranche **vide** produit un tableau ne contenant que le `NULL` final
2890 /// (`argv = { NULL }`, soit `argc == 0`) — accepté par le kernel.
2891 #[must_use]
2892 pub fn new(items: &'a [&'a CStr]) -> Self {
2893 // `+ 1` pour le terminateur NULL. `saturating_add` par discipline
2894 // (Principe 2) ; la capacité d'une tranche ne peut pas atteindre
2895 // `usize::MAX`, donc la saturation ne se déclenche jamais.
2896 let mut ptrs = Vec::with_capacity(items.len().saturating_add(1));
2897 for item in items {
2898 ptrs.push(item.as_ptr());
2899 }
2900 ptrs.push(core::ptr::null());
2901 Self {
2902 ptrs,
2903 _borrow: PhantomData,
2904 }
2905 }
2906
2907 /// Nombre d'arguments (hors terminateur `NULL`).
2908 #[must_use]
2909 pub fn len(&self) -> usize {
2910 // Invariant : `ptrs` contient toujours le `NULL` final en sus.
2911 self.ptrs.len().saturating_sub(1)
2912 }
2913
2914 /// Vrai si la liste est vide (le tableau ne contient que le `NULL` final).
2915 #[must_use]
2916 pub fn is_empty(&self) -> bool {
2917 self.len() == 0
2918 }
2919
2920 /// Pointeur vers le tableau de pointeurs, à transmettre au syscall.
2921 #[inline]
2922 fn as_ptr(&self) -> *const *const c_char {
2923 self.ptrs.as_ptr()
2924 }
2925}
2926
2927/// Remplace l'image mémoire du processus courant par l'exécutable situé à
2928/// `path`, avec les arguments `argv` et l'environnement `envp`.
2929///
2930/// Cf. `docs/specs/layer-0/family-process.md`. En cas de **succès, ne retourne
2931/// jamais** (le processus exécute désormais le nouveau binaire) — d'où le type
2932/// `Result<Infallible, Errno>` (ADR-021 convention 5). En cas d'échec, retourne
2933/// `Err(errno)`.
2934///
2935/// `argv` / `envp` sont des tranches de [`CStr`] ; le wrapper en construit les
2936/// tableaux de pointeurs C `NULL`-terminés via [`CStrArray`]. Par convention
2937/// POSIX, `argv[0]` est le nom du programme ; une tranche vide est acceptée par
2938/// le kernel (`argc == 0`).
2939///
2940/// Pour exécuter un binaire désigné par un **fd** (sans race sur le chemin),
2941/// préférer [`execveat`] avec [`ExecveatFlags::EMPTY_PATH`].
2942///
2943/// # Errors
2944///
2945/// - [`Errno::EACCES`] : `path` n'est pas exécutable, un répertoire du chemin
2946/// interdit la traversée, ou le système de fichiers est monté `noexec`.
2947/// - [`Errno::ENOENT`] : `path` (ou l'interpréteur `#!` d'un script) n'existe
2948/// pas.
2949/// - [`Errno::ENOEXEC`] : `path` existe mais n'est pas dans un format
2950/// exécutable reconnu (ni ELF, ni script avec `#!`).
2951/// - [`Errno::E2BIG`] : `argv` + `envp` dépassent la limite kernel (`ARG_MAX`).
2952/// - `ENOMEM`, `ETXTBSY`, `ELOOP`, … : cf. `execve(2)`.
2953pub fn execve(path: &CStr, argv: &[&CStr], envp: &[&CStr]) -> Result<Infallible, Errno> {
2954 // L'allocation des tableaux de pointeurs ([`CStrArray::new`]) a lieu **ici**.
2955 // Le cœur (syscall, sans allocation) est délégué à [`execve_prepared`], que
2956 // les appelants en chemin **post-fork** invoquent directement avec des
2957 // [`CStrArray`] matérialisés **avant** le fork (async-signal-safety).
2958 let c_argv = CStrArray::new(argv);
2959 let c_envp = CStrArray::new(envp);
2960 execve_prepared(path, &c_argv, &c_envp)
2961}
2962
2963/// Variante d'[`execve`] **sans allocation** : reçoit des tableaux de pointeurs
2964/// `argv`/`envp` **déjà construits** ([`CStrArray`]).
2965///
2966/// Destinée au chemin **post-fork / pré-`execve`** : l'appelant matérialise les
2967/// [`CStrArray`] dans le parent **avant** `clone3`/`fork`, puis les passe ici
2968/// depuis l'enfant. Cette fonction n'alloue rien — seul un syscall est émis —
2969/// donc elle est sûre dans la fenêtre fork→exec où aucune allocation (verrou
2970/// allocateur potentiellement tenu par un autre thread au moment du fork) n'est
2971/// permise. [`execve`] délègue à cette fonction après avoir bâti ses tableaux.
2972///
2973/// En cas de **succès, ne retourne jamais** (image mémoire remplacée). En cas
2974/// d'échec, retourne `Err(errno)`.
2975///
2976/// # Errors
2977///
2978/// Identiques à [`execve`] (`EACCES`, `ENOENT`, `ENOEXEC`, `E2BIG`, …).
2979pub fn execve_prepared(
2980 path: &CStr,
2981 argv: &CStrArray<'_>,
2982 envp: &CStrArray<'_>,
2983) -> Result<Infallible, Errno> {
2984 // SAFETY:
2985 // - `path` est un `CStr` valide NUL-terminé (le kernel le lit jusqu'au NUL).
2986 // - `argv`/`envp` exposent chacun un tableau de pointeurs C valide, terminé
2987 // par NULL, vivant pour toute la durée de l'appel ; les chaînes pointées
2988 // sont les `CStr` empruntés, vivants eux aussi (durée de vie liée par
2989 // `CStrArray<'a>`). Le kernel ne fait que **lire** ces données.
2990 // - Succès : le syscall ne revient pas (image remplacée). Échec : ret < 0.
2991 let ret = unsafe {
2992 raw_syscall_execve(
2993 path.as_ptr() as u64,
2994 argv.as_ptr() as u64,
2995 envp.as_ptr() as u64,
2996 )
2997 };
2998 // Seul un échec ramène ici : `ret` est toujours `< 0`. La conversion errno
2999 // impose elle-même `ret ∈ [-4095, -1]` (debug_assert interne).
3000 Err(errno_from_negative_syscall_ret(ret))
3001}
3002
3003#[cfg(target_arch = "x86_64")]
3004#[inline]
3005unsafe fn raw_syscall_execve(path: u64, argv: u64, envp: u64) -> i64 {
3006 let ret: i64;
3007 // SAFETY:
3008 // - SYS_execve (x86_64 = 59). Le kernel **lit** la chaîne `path` (jusqu'au
3009 // NUL) et les tableaux `argv`/`envp` (jusqu'au pointeur NULL, puis chaque
3010 // chaîne jusqu'à son NUL) ; il n'écrit aucune mémoire utilisateur →
3011 // `readonly` correct. Validité garantie par l'appelant.
3012 // - Succès : ne revient pas (image remplacée). Échec : ret = -errno.
3013 // - x86_64 ABI : numéro en RAX, args en RDI/RSI/RDX ; retour RAX ; clobbe
3014 // RCX/R11.
3015 unsafe {
3016 core::arch::asm!(
3017 "syscall",
3018 in("rax") 59_i64,
3019 in("rdi") path,
3020 in("rsi") argv,
3021 in("rdx") envp,
3022 lateout("rax") ret,
3023 lateout("rcx") _,
3024 lateout("r11") _,
3025 options(nostack, preserves_flags, readonly),
3026 );
3027 }
3028 ret
3029}
3030
3031#[cfg(target_arch = "aarch64")]
3032#[inline]
3033unsafe fn raw_syscall_execve(path: u64, argv: u64, envp: u64) -> i64 {
3034 let ret: i64;
3035 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_execve (aarch64 = 221),
3036 // numéro en X8, args en X0/X1/X2, retour en X0. Lecture seule → `readonly`.
3037 unsafe {
3038 core::arch::asm!(
3039 "svc 0",
3040 in("x8") 221_i64,
3041 inout("x0") path => ret,
3042 in("x1") argv,
3043 in("x2") envp,
3044 options(nostack, preserves_flags, readonly),
3045 );
3046 }
3047 ret
3048}
3049
3050/// Variante *at d'[`execve`] (préférée par ADR-021) : résout l'exécutable
3051/// relativement à `dirfd`, ou — avec [`ExecveatFlags::EMPTY_PATH`] et
3052/// `path == c""` — exécute **directement** le binaire désigné par `dirfd`
3053/// (un fd ouvert sur l'exécutable, typiquement avec `O_PATH`), éliminant toute
3054/// race entre l'ouverture/vérification et l'exec.
3055///
3056/// Cf. `docs/specs/layer-0/family-process.md`. Comme [`execve`], **ne retourne
3057/// jamais en cas de succès**.
3058///
3059/// # Errors
3060///
3061/// Mêmes erreurs qu'[`execve`], plus :
3062///
3063/// - [`Errno::EBADF`] : `dirfd` n'est ni `AT_FDCWD` ni un fd ouvert valide.
3064/// - [`Errno::ENOENT`] : `path` est vide sans [`ExecveatFlags::EMPTY_PATH`].
3065/// - [`Errno::ELOOP`] : composant final symbolique avec
3066/// [`ExecveatFlags::SYMLINK_NOFOLLOW`].
3067pub fn execveat(
3068 dirfd: DirFd<'_>,
3069 path: &CStr,
3070 argv: &[&CStr],
3071 envp: &[&CStr],
3072 flags: ExecveatFlags,
3073) -> Result<Infallible, Errno> {
3074 let c_argv = CStrArray::new(argv);
3075 let c_envp = CStrArray::new(envp);
3076 // SAFETY: identique à `execve` pour `path`/`argv`/`envp` (lecture seule),
3077 // plus :
3078 // - `dirfd` est `AT_FDCWD` ou un fd valide emprunté (résolution relative,
3079 // ou exécutable direct si `EMPTY_PATH`).
3080 // - `flags` est un masque `AT_*` valide (typé `ExecveatFlags`).
3081 let ret = unsafe {
3082 raw_syscall_execveat(
3083 dirfd_to_raw(dirfd),
3084 path.as_ptr() as u64,
3085 c_argv.as_ptr() as u64,
3086 c_envp.as_ptr() as u64,
3087 flags.bits(),
3088 )
3089 };
3090 // Seul un échec ramène ici : `ret` est toujours `< 0`.
3091 Err(errno_from_negative_syscall_ret(ret))
3092}
3093
3094#[cfg(target_arch = "x86_64")]
3095#[inline]
3096unsafe fn raw_syscall_execveat(dirfd: i32, path: u64, argv: u64, envp: u64, flags: i32) -> i64 {
3097 let ret: i64;
3098 // SAFETY:
3099 // - SYS_execveat (x86_64 = 322). Lecture seule de `path`/`argv`/`envp` (cf.
3100 // `raw_syscall_execve`) → `readonly`. `dirfd` (étendu en signe : AT_FDCWD
3101 // = -100 ou fd valide) et `flags` sont des scalaires.
3102 // - Succès : ne revient pas. Échec : ret = -errno.
3103 // - x86_64 ABI : args en RDI/RSI/RDX/R10/R8.
3104 unsafe {
3105 core::arch::asm!(
3106 "syscall",
3107 in("rax") 322_i64,
3108 in("rdi") i64::from(dirfd),
3109 in("rsi") path,
3110 in("rdx") argv,
3111 in("r10") envp,
3112 in("r8") i64::from(flags),
3113 lateout("rax") ret,
3114 lateout("rcx") _,
3115 lateout("r11") _,
3116 options(nostack, preserves_flags, readonly),
3117 );
3118 }
3119 ret
3120}
3121
3122#[cfg(target_arch = "aarch64")]
3123#[inline]
3124unsafe fn raw_syscall_execveat(dirfd: i32, path: u64, argv: u64, envp: u64, flags: i32) -> i64 {
3125 let ret: i64;
3126 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_execveat (aarch64 = 281),
3127 // numéro en X8, args en X0..X4, retour en X0. Lecture seule → `readonly`.
3128 unsafe {
3129 core::arch::asm!(
3130 "svc 0",
3131 in("x8") 281_i64,
3132 inout("x0") i64::from(dirfd) => ret,
3133 in("x1") path,
3134 in("x2") argv,
3135 in("x3") envp,
3136 in("x4") i64::from(flags),
3137 options(nostack, preserves_flags, readonly),
3138 );
3139 }
3140 ret
3141}
3142
3143/// Duplique le descripteur `old` vers le numéro `new` (`dup3(2)`), en fermant
3144/// atomiquement `new` s'il était ouvert. Retourne le nouveau descripteur,
3145/// possédé ([`OwnedFd`], fermé au `Drop`).
3146///
3147/// Cf. `docs/specs/layer-0/family-process.md`. Variante **moderne** de `dup2`
3148/// (ADR-021) : les drapeaux sont explicites ([`Dup3Flags`], seul `O_CLOEXEC`
3149/// est valide). Cas d'usage couche 1 : rediriger les flux standard d'un enfant
3150/// (`dup3(pipe, STDOUT_FILENO, …)`) juste avant l'`exec`.
3151///
3152/// À la différence de `dup2`, `dup3` **échoue** (`EINVAL`) si `old == new` :
3153/// pas de no-op silencieux. Le numéro `new` doit donc différer de `old`.
3154///
3155/// # Errors
3156///
3157/// - [`Errno::EINVAL`] : `flags` invalides, **ou** `old == new`.
3158/// - [`Errno::EBADF`] : `new` hors de la plage autorisée (`> RLIMIT_NOFILE`).
3159/// - [`Errno::EBUSY`] : course (rare) avec `open`/`dup` concurrents sur `new`.
3160/// - [`Errno::EINTR`] : interrompu par un signal (remonté tel quel, ADR-021
3161/// convention 2).
3162pub fn dup3(old: BorrowedFd<'_>, new: RawFd, flags: Dup3Flags) -> Result<OwnedFd, Errno> {
3163 // SAFETY: `dup3(2)` ne touche aucune mémoire utilisateur ; il installe une
3164 // copie de l'entrée de table de fd `old` au numéro `new` (fermant `new` au
3165 // préalable s'il était ouvert), de manière atomique. `flags` (typé) ne peut
3166 // valoir que `O_CLOEXEC`.
3167 let ret = unsafe { raw_syscall_dup3(old.as_raw_fd(), new, flags.bits()) };
3168 if ret < 0 {
3169 return Err(errno_from_negative_syscall_ret(ret));
3170 }
3171 // Succès : `ret == new` (le numéro demandé), donc tient dans un `i32`.
3172 #[allow(clippy::cast_possible_truncation)]
3173 let new_fd = ret as RawFd;
3174 // SAFETY: le kernel vient d'installer un fd valide au numéro `new`, dont
3175 // Air prend désormais la propriété exclusive (fermé par `OwnedFd::Drop`).
3176 Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
3177}
3178
3179#[cfg(target_arch = "x86_64")]
3180#[inline]
3181unsafe fn raw_syscall_dup3(oldfd: i32, newfd: i32, flags: i32) -> i64 {
3182 let ret: i64;
3183 // SAFETY: SYS_dup3 (x86_64 = 292) ne touche aucune mémoire utilisateur
3184 // (`readonly`). Args étendus en signe (fd ≥ 0 ici ; l'extension reste
3185 // correcte). x86_64 ABI : args en RDI/RSI/RDX.
3186 unsafe {
3187 core::arch::asm!(
3188 "syscall",
3189 in("rax") 292_i64,
3190 in("rdi") i64::from(oldfd),
3191 in("rsi") i64::from(newfd),
3192 in("rdx") i64::from(flags),
3193 lateout("rax") ret,
3194 lateout("rcx") _,
3195 lateout("r11") _,
3196 options(nostack, preserves_flags, readonly),
3197 );
3198 }
3199 ret
3200}
3201
3202#[cfg(target_arch = "aarch64")]
3203#[inline]
3204unsafe fn raw_syscall_dup3(oldfd: i32, newfd: i32, flags: i32) -> i64 {
3205 let ret: i64;
3206 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_dup3 (aarch64 = 24),
3207 // numéro en X8, args en X0/X1/X2, retour en X0. Lecture seule → `readonly`.
3208 unsafe {
3209 core::arch::asm!(
3210 "svc 0",
3211 in("x8") 24_i64,
3212 inout("x0") i64::from(oldfd) => ret,
3213 in("x1") i64::from(newfd),
3214 in("x2") i64::from(flags),
3215 options(nostack, preserves_flags, readonly),
3216 );
3217 }
3218 ret
3219}
3220
3221/// Change le répertoire de travail courant du processus pour celui référencé
3222/// par le descripteur `dir` (`fchdir(2)`).
3223///
3224/// Cf. `docs/specs/layer-0/family-process.md`. Variante par fd de [`chdir`] :
3225/// pas de race sur le chemin (le répertoire est déjà ouvert et épinglé). Cas
3226/// d'usage couche 1 : fixer le `current_dir` d'un enfant à partir d'un fd
3227/// vérifié, juste avant l'`exec`.
3228///
3229/// # Errors
3230///
3231/// - [`Errno::EBADF`] : `dir` n'est pas un descripteur ouvert valide.
3232/// - [`Errno::ENOTDIR`] : `dir` ne référence pas un répertoire.
3233/// - [`Errno::EACCES`] : permission de recherche refusée sur le répertoire.
3234pub fn fchdir(dir: BorrowedFd<'_>) -> Result<(), Errno> {
3235 // SAFETY: `fchdir(2)` ne touche aucune mémoire utilisateur ; il bascule le
3236 // répertoire courant du processus vers le répertoire référencé par `dir`.
3237 let ret = unsafe { raw_syscall_fchdir(dir.as_raw_fd()) };
3238 if ret < 0 {
3239 return Err(errno_from_negative_syscall_ret(ret));
3240 }
3241 Ok(())
3242}
3243
3244#[cfg(target_arch = "x86_64")]
3245#[inline]
3246unsafe fn raw_syscall_fchdir(fd: i32) -> i64 {
3247 let ret: i64;
3248 // SAFETY: SYS_fchdir (x86_64 = 81) ne touche aucune mémoire utilisateur
3249 // (`readonly`). x86_64 ABI : argument unique en RDI.
3250 unsafe {
3251 core::arch::asm!(
3252 "syscall",
3253 in("rax") 81_i64,
3254 in("rdi") i64::from(fd),
3255 lateout("rax") ret,
3256 lateout("rcx") _,
3257 lateout("r11") _,
3258 options(nostack, preserves_flags, readonly),
3259 );
3260 }
3261 ret
3262}
3263
3264#[cfg(target_arch = "aarch64")]
3265#[inline]
3266unsafe fn raw_syscall_fchdir(fd: i32) -> i64 {
3267 let ret: i64;
3268 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_fchdir (aarch64 = 50),
3269 // numéro en X8, argument en X0, retour en X0. Lecture seule → `readonly`.
3270 unsafe {
3271 core::arch::asm!(
3272 "svc 0",
3273 in("x8") 50_i64,
3274 inout("x0") i64::from(fd) => ret,
3275 options(nostack, preserves_flags, readonly),
3276 );
3277 }
3278 ret
3279}
3280
3281/// Change le répertoire de travail courant du processus pour `path`
3282/// (`chdir(2)`).
3283///
3284/// Cf. `docs/specs/layer-0/family-process.md`. Cas d'usage couche 1 : fixer le
3285/// `current_dir` d'un enfant juste avant l'`exec`. Préférer [`fchdir`] quand un
3286/// fd de répertoire est disponible (pas de race sur le chemin).
3287///
3288/// # Errors
3289///
3290/// - [`Errno::ENOENT`] : `path` n'existe pas.
3291/// - [`Errno::ENOTDIR`] : un composant de `path` n'est pas un répertoire.
3292/// - [`Errno::EACCES`] : permission de recherche refusée.
3293/// - [`Errno::ENAMETOOLONG`] : `path` trop long.
3294pub fn chdir(path: &CStr) -> Result<(), Errno> {
3295 // SAFETY: `chdir(2)` **lit** la chaîne `path` (jusqu'au NUL) et n'écrit
3296 // aucune mémoire utilisateur → `readonly`. `path` est un `CStr` valide.
3297 let ret = unsafe { raw_syscall_chdir(path.as_ptr() as u64) };
3298 if ret < 0 {
3299 return Err(errno_from_negative_syscall_ret(ret));
3300 }
3301 Ok(())
3302}
3303
3304/// Change le **répertoire racine** du processus vers `path` (`chroot(2)`) —
3305/// **descellement additif [ADR-085](../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md)**
3306/// pour la face libc. Confinement de processus (`CAP_SYS_CHROOT` requis).
3307///
3308/// # Errors
3309///
3310/// - [`Errno::EPERM`] : `CAP_SYS_CHROOT` manquant.
3311/// - [`Errno::ENOENT`] : `path` n'existe pas.
3312/// - [`Errno::ENOTDIR`] : un composant de `path` n'est pas un répertoire.
3313pub fn chroot(path: &CStr) -> Result<(), Errno> {
3314 // SAFETY: `chroot(2)` **lit** la chaîne `path` (jusqu'au NUL) et n'écrit aucune
3315 // mémoire utilisateur → `readonly`. `path` est un `CStr` valide.
3316 let ret = unsafe { raw_syscall_chroot(path.as_ptr() as u64) };
3317 if ret < 0 {
3318 return Err(errno_from_negative_syscall_ret(ret));
3319 }
3320 Ok(())
3321}
3322
3323#[cfg(target_arch = "x86_64")]
3324#[inline]
3325unsafe fn raw_syscall_chdir(path: u64) -> i64 {
3326 let ret: i64;
3327 // SAFETY: SYS_chdir (x86_64 = 80). Le kernel **lit** la chaîne `path`
3328 // (jusqu'au NUL), validité garantie par l'appelant ; aucune écriture
3329 // utilisateur → `readonly`. x86_64 ABI : argument unique en RDI.
3330 unsafe {
3331 core::arch::asm!(
3332 "syscall",
3333 in("rax") 80_i64,
3334 in("rdi") path,
3335 lateout("rax") ret,
3336 lateout("rcx") _,
3337 lateout("r11") _,
3338 options(nostack, preserves_flags, readonly),
3339 );
3340 }
3341 ret
3342}
3343
3344#[cfg(target_arch = "aarch64")]
3345#[inline]
3346unsafe fn raw_syscall_chdir(path: u64) -> i64 {
3347 let ret: i64;
3348 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_chdir (aarch64 = 49),
3349 // numéro en X8, argument (pointeur `path`) en X0, retour en X0. Lecture
3350 // seule → `readonly`.
3351 unsafe {
3352 core::arch::asm!(
3353 "svc 0",
3354 in("x8") 49_i64,
3355 inout("x0") path => ret,
3356 options(nostack, preserves_flags, readonly),
3357 );
3358 }
3359 ret
3360}
3361
3362#[cfg(target_arch = "x86_64")]
3363#[inline]
3364unsafe fn raw_syscall_chroot(path: u64) -> i64 {
3365 let ret: i64;
3366 // SAFETY: SYS_chroot (x86_64 = 161). Le kernel **lit** la chaîne `path` (jusqu'au
3367 // NUL), validité garantie par l'appelant ; aucune écriture utilisateur → `readonly`.
3368 // x86_64 ABI : argument unique en RDI.
3369 unsafe {
3370 core::arch::asm!(
3371 "syscall",
3372 in("rax") 161_i64,
3373 in("rdi") path,
3374 lateout("rax") ret,
3375 lateout("rcx") _,
3376 lateout("r11") _,
3377 options(nostack, preserves_flags, readonly),
3378 );
3379 }
3380 ret
3381}
3382
3383#[cfg(target_arch = "aarch64")]
3384#[inline]
3385unsafe fn raw_syscall_chroot(path: u64) -> i64 {
3386 let ret: i64;
3387 // SAFETY: voir version x86_64 ; aarch64 ABI : SYS_chroot (aarch64 = 51), numéro en
3388 // X8, argument (pointeur `path`) en X0, retour en X0. Lecture seule → `readonly`.
3389 unsafe {
3390 core::arch::asm!(
3391 "svc 0",
3392 in("x8") 51_i64,
3393 inout("x0") path => ret,
3394 options(nostack, preserves_flags, readonly),
3395 );
3396 }
3397 ret
3398}
3399
3400/// API de fuzzing **pure** (sans syscall) de la frontière de marshalling
3401/// `argv`/`envp` ([`CStrArray`]). Le harnais cargo-fuzz
3402/// (`fuzz/fuzz_targets/fuzz_exec_argv.rs`) y délègue ; l'oracle vit ici pour
3403/// accéder aux internes de [`CStrArray`] (le tableau de pointeurs) tout en
3404/// restant testé et couvert. Suit le pattern de [`crate::io_uring::fuzz_api`].
3405pub mod fuzz_api {
3406 use super::CStrArray;
3407 use alloc::ffi::CString;
3408 use alloc::vec::Vec;
3409 use core::ffi::{CStr, c_char};
3410
3411 /// Exerce la chaîne « octets bruts → `CString` → `CStrArray` » et la
3412 /// **parcourt** jusqu'au terminateur `NULL`, garantissant l'absence de
3413 /// panic / UB pour **toute** entrée (Principe 3 — frontière) :
3414 ///
3415 /// - **rejet propre des NUL embarqués** : `CString::new` rend `Err` (jamais
3416 /// panic) dès qu'un morceau contient un octet NUL ; ces morceaux sont
3417 /// simplement écartés ;
3418 /// - **totalité** : la construction du tableau ne panique jamais (liste
3419 /// vide, ou très longue) ;
3420 /// - **terminaison NULL & validité** : le parcours s'arrête sur le `NULL`
3421 /// final et relit chaque chaîne C (tout pointeur invalide ou toute
3422 /// terminaison manquante ferait crasher sous sanitizer/libFuzzer).
3423 ///
3424 /// L'oracle *structurel* exact (ordre, exactitude des pointeurs) est
3425 /// vérifié par les tests unitaires `cstr_array_*`.
3426 pub fn marshal_exec_args(chunks: Vec<Vec<u8>>) {
3427 // Frontière 1 — NUL embarqué rejeté proprement (Err, jamais panic).
3428 let owned: Vec<CString> = chunks
3429 .iter()
3430 .filter_map(|chunk| CString::new(chunk.as_slice()).ok())
3431 .collect();
3432 let refs: Vec<&CStr> = owned.iter().map(CString::as_c_str).collect();
3433
3434 // Frontière 2 — construction du tableau de pointeurs C NULL-terminé.
3435 let arr = CStrArray::new(&refs);
3436 let _ = arr.len();
3437 let _ = arr.is_empty();
3438
3439 // Parcours jusqu'au terminateur NULL en relisant chaque chaîne : tout
3440 // écart (pointeur invalide, terminaison absente) ferait crasher la
3441 // cible libFuzzer.
3442 let base = arr.as_ptr();
3443 let mut i = 0_usize;
3444 loop {
3445 // SAFETY: `base` pointe `refs.len() + 1` pointeurs ; on s'arrête au
3446 // premier NULL (présent par construction en position `refs.len()`),
3447 // donc `i` reste borné par `refs.len()`.
3448 let ptr: *const c_char = unsafe { *base.add(i) };
3449 if ptr.is_null() {
3450 break;
3451 }
3452 // SAFETY: `ptr` non NULL ⇒ i-ème chaîne C vivante (empruntée à
3453 // `owned`), NUL-terminée par construction de `CStr`.
3454 let s = unsafe { CStr::from_ptr(ptr) };
3455 let _ = s.to_bytes();
3456 i = i.saturating_add(1);
3457 }
3458 }
3459}
3460
3461// ─────────────────────────────────────────────────────────────────────────
3462// Runtime & libc — re-sceau `couche-0-v1.7` (ADR-051)
3463//
3464// 6 syscalls ajoutés pour le runtime Air (air-rt) et l'objectif libc :
3465// `getppid`, `set_tid_address`, `sched_yield`, `umask`, `getcwd`, `getrusage`.
3466// (Le 7ᵉ, `arch_prctl`, est x86_64-only et vit dans `crate::arch`.)
3467// Conventions ADR-021 : fonction dédiée typée, `Option`/newtypes, pas de
3468// sentinelle, zéro alloc sur le happy path.
3469// ─────────────────────────────────────────────────────────────────────────
3470
3471/// Retourne le PID du **parent** du processus appelant (`getppid(2)`).
3472///
3473/// Retourne `None` lorsque le kernel rend `0`, c.-à-d. quand le parent n'est
3474/// **pas visible** dans le *PID namespace* du processus (ré-parentage hors
3475/// namespace). Le `0` du kernel — qui n'est pas un PID valide — est ainsi
3476/// re-présenté en `Option` plutôt qu'exposé comme sentinelle (ADR-021 §1).
3477#[must_use]
3478pub fn getppid() -> Option<Pid> {
3479 let raw = raw_syscall_getppid();
3480 // PID : `pid_t` (i32) ; les 32 bits hauts sont nuls pour une valeur valide.
3481 #[allow(clippy::cast_possible_truncation)]
3482 let raw32 = raw as i32;
3483 // `try_from_raw` rend `None` pour `0` (parent hors namespace).
3484 Pid::try_from_raw(raw32)
3485}
3486
3487#[cfg(target_arch = "x86_64")]
3488#[inline]
3489fn raw_syscall_getppid() -> i64 {
3490 let ret: i64;
3491 // SAFETY: SYS_getppid (x86_64 = 110) ne touche aucune mémoire utilisateur ;
3492 // clobbe RCX/R11 comme tout `syscall` x86_64.
3493 unsafe {
3494 core::arch::asm!(
3495 "syscall",
3496 in("rax") 110_i64,
3497 lateout("rax") ret,
3498 lateout("rcx") _,
3499 lateout("r11") _,
3500 options(nostack, preserves_flags, readonly),
3501 );
3502 }
3503 ret
3504}
3505
3506#[cfg(target_arch = "aarch64")]
3507#[inline]
3508fn raw_syscall_getppid() -> i64 {
3509 let ret: i64;
3510 // SAFETY: SYS_getppid (aarch64 = 173) ne touche aucune mémoire utilisateur.
3511 unsafe {
3512 core::arch::asm!(
3513 "svc 0",
3514 in("x8") 173_i64,
3515 lateout("x0") ret,
3516 options(nostack, preserves_flags, readonly),
3517 );
3518 }
3519 ret
3520}
3521
3522/// Enregistre l'adresse `clear_child_tid` du thread appelant et retourne son TID
3523/// (`set_tid_address(2)`). À la **fin du thread**, le kernel écrit `0` à cette
3524/// adresse et émet un `FUTEX_WAKE` dessus — primitif de *join* (cf.
3525/// [`clone_thread`] et la famille [`futex`](crate::futex)).
3526///
3527/// `None` désenregistre (passe `NULL`). L'appel **ne peut pas échouer** et rend
3528/// toujours le TID appelant (`> 0`).
3529///
3530/// # Safety
3531///
3532/// Si `clear_child_tid` est `Some(addr)`, le kernel **conservera `addr`** et y
3533/// écrira à la terminaison du thread. L'appelant doit garantir que la mémoire
3534/// pointée reste **valide jusqu'à la fin du thread** (sinon écriture kernel dans
3535/// de la mémoire libérée). Même discipline que [`CloneArgs::child_tid`].
3536pub unsafe fn set_tid_address(clear_child_tid: Option<&core::sync::atomic::AtomicU32>) -> Tid {
3537 let ptr = clear_child_tid.map_or(0_usize, |a| core::ptr::from_ref(a) as usize);
3538 // SAFETY: l'appel délègue ; la liveness de `ptr` jusqu'à la mort du thread
3539 // relève du contrat `unsafe` de cette fonction.
3540 let raw = unsafe { raw_syscall_set_tid_address(ptr) };
3541 // Le kernel rend le TID appelant, strictement positif.
3542 #[allow(clippy::cast_possible_truncation)]
3543 let tid_raw = raw as i32;
3544 let nz = NonZeroI32::new(tid_raw).expect("set_tid_address : TID appelant > 0");
3545 Tid::from_nonzero(nz)
3546}
3547
3548#[cfg(target_arch = "x86_64")]
3549#[inline]
3550unsafe fn raw_syscall_set_tid_address(tidptr: usize) -> i64 {
3551 let ret: i64;
3552 // SAFETY: SYS_set_tid_address (x86_64 = 218). Le kernel **enregistre**
3553 // `tidptr` pour écriture à la mort du thread ; il n'écrit pas pendant
3554 // l'appel. La validité/liveness de `tidptr` relève du contrat de l'appelant.
3555 unsafe {
3556 core::arch::asm!(
3557 "syscall",
3558 in("rax") 218_i64,
3559 in("rdi") tidptr,
3560 lateout("rax") ret,
3561 lateout("rcx") _,
3562 lateout("r11") _,
3563 options(nostack, preserves_flags),
3564 );
3565 }
3566 ret
3567}
3568
3569#[cfg(target_arch = "aarch64")]
3570#[inline]
3571unsafe fn raw_syscall_set_tid_address(tidptr: usize) -> i64 {
3572 let ret: i64;
3573 // SAFETY: SYS_set_tid_address (aarch64 = 96). Voir la branche x86_64.
3574 unsafe {
3575 core::arch::asm!(
3576 "svc 0",
3577 in("x8") 96_i64,
3578 in("x0") tidptr,
3579 lateout("x0") ret,
3580 options(nostack, preserves_flags),
3581 );
3582 }
3583 ret
3584}
3585
3586/// Cède volontairement le CPU à un autre thread prêt (`sched_yield(2)`).
3587///
3588/// Sous Linux, `sched_yield` **réussit toujours** (retour `0`) ; la fonction est
3589/// donc totale.
3590pub fn sched_yield() {
3591 let _ = raw_syscall_sched_yield();
3592}
3593
3594#[cfg(target_arch = "x86_64")]
3595#[inline]
3596fn raw_syscall_sched_yield() -> i64 {
3597 let ret: i64;
3598 // SAFETY: SYS_sched_yield (x86_64 = 24) ne touche aucune mémoire utilisateur.
3599 unsafe {
3600 core::arch::asm!(
3601 "syscall",
3602 in("rax") 24_i64,
3603 lateout("rax") ret,
3604 lateout("rcx") _,
3605 lateout("r11") _,
3606 options(nostack, preserves_flags, readonly),
3607 );
3608 }
3609 ret
3610}
3611
3612#[cfg(target_arch = "aarch64")]
3613#[inline]
3614fn raw_syscall_sched_yield() -> i64 {
3615 let ret: i64;
3616 // SAFETY: SYS_sched_yield (aarch64 = 124) ne touche aucune mémoire utilisateur.
3617 unsafe {
3618 core::arch::asm!(
3619 "svc 0",
3620 in("x8") 124_i64,
3621 lateout("x0") ret,
3622 options(nostack, preserves_flags, readonly),
3623 );
3624 }
3625 ret
3626}
3627
3628/// Fixe le **masque de création de fichiers** du processus et retourne l'ancien
3629/// (`umask(2)`). Opération **totale** (ne peut pas échouer).
3630#[must_use]
3631pub fn umask(new_mask: Mode) -> Mode {
3632 let previous = raw_syscall_umask(u64::from(new_mask));
3633 // L'ancien masque tient dans `mode_t` (12 bits significatifs) ⇒ conversion
3634 // sûre, jamais lossy (Principe 2 : pas d'`as` lossy).
3635 u32::try_from(previous).expect("umask : ancien masque dans la plage mode_t")
3636}
3637
3638#[cfg(target_arch = "x86_64")]
3639#[inline]
3640fn raw_syscall_umask(mask: u64) -> i64 {
3641 let ret: i64;
3642 // SAFETY: SYS_umask (x86_64 = 95) ne touche aucune mémoire utilisateur ;
3643 // rend l'ancien masque.
3644 unsafe {
3645 core::arch::asm!(
3646 "syscall",
3647 in("rax") 95_i64,
3648 in("rdi") mask,
3649 lateout("rax") ret,
3650 lateout("rcx") _,
3651 lateout("r11") _,
3652 options(nostack, preserves_flags, readonly),
3653 );
3654 }
3655 ret
3656}
3657
3658#[cfg(target_arch = "aarch64")]
3659#[inline]
3660fn raw_syscall_umask(mask: u64) -> i64 {
3661 let ret: i64;
3662 // SAFETY: SYS_umask (aarch64 = 166). Voir la branche x86_64.
3663 unsafe {
3664 core::arch::asm!(
3665 "svc 0",
3666 in("x8") 166_i64,
3667 in("x0") mask,
3668 lateout("x0") ret,
3669 options(nostack, preserves_flags, readonly),
3670 );
3671 }
3672 ret
3673}
3674
3675/// Écrit le **répertoire courant** (chemin absolu) dans `buf` et retourne la
3676/// tranche d'octets correspondante, **sans** l'octet NUL terminal (`getcwd(2)`).
3677///
3678/// Le chemin est traité comme des **octets** (jamais supposé UTF-8 — Principe 3).
3679/// L'appelant fournit le buffer (zéro allocation, ADR-021 §4).
3680///
3681/// # Errors
3682///
3683/// - [`Errno::ERANGE`] si `buf` est trop petit pour contenir le chemin + NUL.
3684/// - Autres `Errno` propagés (p. ex. permissions de lecture d'un composant).
3685pub fn getcwd(buf: &mut [u8]) -> Result<&[u8], Errno> {
3686 let size = u64::try_from(buf.len()).expect("getcwd : taille de buffer dans u64");
3687 let ptr = buf.as_mut_ptr() as usize;
3688 // SAFETY: `ptr`/`size` décrivent `buf` (emprunt `&mut` valide) ; le kernel y
3689 // écrit au plus `size` octets.
3690 let ret = unsafe { raw_syscall_getcwd(ptr, size) };
3691 if ret < 0 {
3692 return Err(errno_from_negative_syscall_ret(ret));
3693 }
3694 // `getcwd(2)` rend la longueur **NUL compris** (≥ 1) ; on retourne les
3695 // octets sans le NUL final.
3696 let total = usize::try_from(ret).expect("getcwd : longueur non négative");
3697 let path_length = total
3698 .checked_sub(1)
3699 .expect("getcwd : longueur ≥ 1 (NUL terminal compris)");
3700 // TODO(ADR-029/Principe 3) : remplacer l'indexation directe `&buf[..path_length]`
3701 // par `buf.get(..path_length)` (slicing via `Option`, jamais d'indexation qui
3702 // panique). Ici `path_length < total ≤ buf.len()` ⇒ borné par construction, donc
3703 // sûr ; reste à convertir le `Option<&[u8]>` rendu en `Result` (p. ex.
3704 // `.expect("getcwd : tranche bornée par construction")` ou un bras d'erreur).
3705 Ok(&buf[..path_length])
3706}
3707
3708#[cfg(target_arch = "x86_64")]
3709#[inline]
3710unsafe fn raw_syscall_getcwd(buf: usize, size: u64) -> i64 {
3711 let ret: i64;
3712 // SAFETY: SYS_getcwd (x86_64 = 79) écrit jusqu'à `size` octets à `buf`.
3713 // Validité de `buf`/`size` : contrat de l'appelant. Pas de `readonly` (écrit
3714 // la mémoire pointée).
3715 unsafe {
3716 core::arch::asm!(
3717 "syscall",
3718 in("rax") 79_i64,
3719 in("rdi") buf,
3720 in("rsi") size,
3721 lateout("rax") ret,
3722 lateout("rcx") _,
3723 lateout("r11") _,
3724 options(nostack),
3725 );
3726 }
3727 ret
3728}
3729
3730#[cfg(target_arch = "aarch64")]
3731#[inline]
3732unsafe fn raw_syscall_getcwd(buf: usize, size: u64) -> i64 {
3733 let ret: i64;
3734 // SAFETY: SYS_getcwd (aarch64 = 17). Voir la branche x86_64.
3735 unsafe {
3736 core::arch::asm!(
3737 "svc 0",
3738 in("x8") 17_i64,
3739 in("x0") buf,
3740 in("x1") size,
3741 lateout("x0") ret,
3742 options(nostack),
3743 );
3744 }
3745 ret
3746}
3747
3748/// Renseigne les **statistiques de ressources** (`getrusage(2)`) pour la cible
3749/// [`RusageWho`] demandée.
3750///
3751/// # Errors
3752///
3753/// `getrusage(2)` ne peut échouer que sur `who` invalide (`EINVAL`) ou pointeur
3754/// invalide (`EFAULT`) — deux cas **rendus impossibles** par l'API typée
3755/// (`RusageWho` est un enum, le buffer est interne et valide). Le bras `Err` est
3756/// donc inatteignable via cette fonction (cf. `COVERAGE-EXCEPTIONS.md`,
3757/// STRUCTURAL/EFAULT-SAFE) ; la signature reste `Result` par honnêteté kernel.
3758pub fn getrusage(who: RusageWho) -> Result<Rusage, Errno> {
3759 let who_raw: i64 = match who {
3760 RusageWho::SelfProcess => 0,
3761 RusageWho::Children => -1,
3762 RusageWho::Thread => 1,
3763 };
3764 let mut usage = Rusage::default();
3765 let ptr = core::ptr::from_mut(&mut usage) as usize;
3766 // SAFETY: `ptr` désigne `usage` (local valide, taille `struct rusage`) ; le
3767 // kernel y écrit la structure. `who_raw` provient d'un enum ⇒ toujours valide.
3768 let ret = unsafe { raw_syscall_getrusage(who_raw, ptr) };
3769 if ret < 0 {
3770 return Err(errno_from_negative_syscall_ret(ret));
3771 }
3772 Ok(usage)
3773}
3774
3775#[cfg(target_arch = "x86_64")]
3776#[inline]
3777unsafe fn raw_syscall_getrusage(who: i64, usage: usize) -> i64 {
3778 let ret: i64;
3779 // SAFETY: SYS_getrusage (x86_64 = 98) écrit une `struct rusage` à `usage`.
3780 // Validité : contrat de l'appelant. Pas de `readonly` (écrit la mémoire).
3781 unsafe {
3782 core::arch::asm!(
3783 "syscall",
3784 in("rax") 98_i64,
3785 in("rdi") who,
3786 in("rsi") usage,
3787 lateout("rax") ret,
3788 lateout("rcx") _,
3789 lateout("r11") _,
3790 options(nostack),
3791 );
3792 }
3793 ret
3794}
3795
3796#[cfg(target_arch = "aarch64")]
3797#[inline]
3798unsafe fn raw_syscall_getrusage(who: i64, usage: usize) -> i64 {
3799 let ret: i64;
3800 // SAFETY: SYS_getrusage (aarch64 = 165). Voir la branche x86_64.
3801 unsafe {
3802 core::arch::asm!(
3803 "svc 0",
3804 in("x8") 165_i64,
3805 in("x0") who,
3806 in("x1") usage,
3807 lateout("x0") ret,
3808 options(nostack),
3809 );
3810 }
3811 ret
3812}
3813
3814#[cfg(test)]
3815mod tests;
3816
3817#[cfg(test)]
3818mod v1_7_runtime_libc_tests;