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