Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spec couche 0 — Famille process

Spécification technique — Version 1.0

Vue d’ensemble de la famille

Le module air-sys-syscall::process expose les primitives kernel de gestion des processus, threads, et processus enfants. C’est l’une des familles fondamentales de la couche 0 : tout ce qui concerne le cycle de vie d’un processus (création, attente, terminaison), l’identité (PIDs, TIDs), les capabilities, et les rlimits.

Périmètre de la famille.

Dix-huit syscalls wrappés, organisés en sous-catégories :

  1. Identité : getpid, gettid.
  2. Création de processus : clone3, execve, execveat.
  3. Attente et terminaison : waitid, exit_group, pidfd_open, pidfd_send_signal, pidfd_getfd.
  4. Groupes et sessions : setpgid, getpgid, setsid, getsid.
  5. Contrôle de processus : prctl (opérations exposées individuellement).
  6. Limites de ressources : getrlimit, setrlimit, prlimit.
  7. Capabilities : capget, capset.

Caractéristiques transverses de la famille.

  1. Préférence pour les variantes modernes. clone3 plutôt que clone, pidfd_open plutôt que d’utiliser les PIDs nus pour les opérations post-fork. Les syscalls legacy sont listés dans UNSUPPORTED.md avec justification.

  2. Newtypes systématiques pour les identifiants. Pid, Tid, PidFd sont des newtypes typés. Pas de i32 brut pour un PID.

  3. Sémantique d’ownership claire. PidFd est RAII : ferme automatiquement le FD à la destruction. Les FDs reçus par les opérations sont toujours retournés comme OwnedFd.

  4. Validation amont systématique. Conformément au Principe d’ingénierie 4, toute fonction publique valide ses paramètres au début et retourne Result ou Option en cas de violation.

Sous-section 1 : Identité

getpid

Signature.

#![allow(unused)]
fn main() {
pub fn getpid() -> Pid;
}

Syscall sous-jacent. getpid (x86_64 n°39, ARM64 n°172). Page man getpid(2).

Comportement.

Retourne le PID du processus appelant. Ne peut pas échouer. POSIX garantit qu’un PID retourné est strictement positif.

Type de retour.

Pid est un newtype sur NonZeroI32. La fonction est totale (pas de Result).

Performance. Négligeable (~30-50 ns via vDSO sur les kernels récents).

Tests.

  • Test nominal : getpid() retourne un Pid valide.
  • Test cohérence : deux appels successifs retournent la même valeur.

gettid

Signature.

#![allow(unused)]
fn main() {
pub fn gettid() -> Tid;
}

Syscall sous-jacent. gettid (x86_64 n°186, ARM64 n°178). Page man gettid(2).

Comportement.

Retourne le TID du thread appelant. Sur Linux, chaque thread a un TID distinct ; le thread principal d’un processus a un TID égal au PID du processus.

Type de retour.

Tid est un newtype distinct de Pid pour empêcher les confusions par type.

Performance. ~30-50 ns.

Tests.

  • Test thread principal : gettid() égale getpid() pour le thread principal.
  • Test multi-thread : deux threads d’un même processus ont des TIDs distincts.

Sous-section 2 : Création de processus

clone3

Signature.

#![allow(unused)]
fn main() {
pub unsafe fn clone3(args: &CloneArgs) -> Result<CloneResult, Errno>;

pub enum CloneResult {
    Parent { child_pid: Pid, child_pidfd: Option<PidFd> },
    Child,
}

#[derive(Debug, Default)]
pub struct CloneArgs {
    pub flags: CloneFlags,
    pub pidfd: Option<PidFdReceiver>,
    pub child_tid: Option<TidReceiver>,
    pub parent_tid: Option<TidReceiver>,
    pub exit_signal: Option<Signal>,
    pub stack: Option<StackSpecification>,
    pub tls: Option<u64>,
    pub set_tid: Option<&[Pid]>,
    pub cgroup: Option<RawFd>,
}

bitflags! {
    pub struct CloneFlags: u64 {
        const VM = 0x00000100;
        const FS = 0x00000200;
        const FILES = 0x00000400;
        const SIGHAND = 0x00000800;
        const PIDFD = 0x00001000;
        const PTRACE = 0x00002000;
        const VFORK = 0x00004000;
        const PARENT = 0x00008000;
        const THREAD = 0x00010000;
        const NEWNS = 0x00020000;
        const SYSVSEM = 0x00040000;
        const SETTLS = 0x00080000;
        const PARENT_SETTID = 0x00100000;
        const CHILD_CLEARTID = 0x00200000;
        const DETACHED = 0x00400000;
        const UNTRACED = 0x00800000;
        const CHILD_SETTID = 0x01000000;
        const NEWCGROUP = 0x02000000;
        const NEWUTS = 0x04000000;
        const NEWIPC = 0x08000000;
        const NEWUSER = 0x10000000;
        const NEWPID = 0x20000000;
        const NEWNET = 0x40000000;
        const IO = 0x80000000;
        const CLEAR_SIGHAND = 0x100000000;
        const INTO_CGROUP = 0x200000000;
        const NEWTIME = 0x400000000;
    }
}

pub struct StackSpecification {
    pub addr: *mut u8,
    pub size: usize,
}
}

Syscall sous-jacent. clone3 (x86_64 n°435, ARM64 n°435). Page man clone(2). Disponible depuis Linux 5.3 (septembre 2019).

Préconditions (Safety).

L’API est unsafe parce que clone3 peut créer un thread partageant la mémoire avec le parent (CLONE_VM), ce qui demande une gestion attentive de l’aliasing mémoire que Rust ne peut pas vérifier.

La documentation # Safety exige :

  • Pour les usages “fork classique” (création d’un processus enfant indépendant), pas de précondition particulière. Le wrapper Air pourra fournir un helper safe fork_process qui appelle clone3 avec les flags appropriés.

  • Pour les usages avec CLONE_VM, l’appelant doit garantir que la mémoire partagée est manipulée de manière thread-safe par les deux côtés.

  • Pour les usages avec CLONE_NEWUSER, CLONE_NEWNS, etc. (namespaces), l’appelant doit avoir les capabilities nécessaires.

Comportement.

clone3 est l’opération moderne unifiée pour créer processus, threads, et toute combinaison entre les deux. Le comportement précis dépend des flags :

  • Sans flags spéciaux : équivalent à fork(). Crée un processus enfant indépendant.
  • Avec CLONE_VM | CLONE_THREAD | CLONE_SIGHAND : crée un thread dans le processus courant.
  • Avec CLONE_NEW* : crée le processus dans de nouveaux namespaces.
  • Avec CLONE_PIDFD : retourne aussi un pidfd pour le nouveau processus, évitant les races sur le PID recyclé.

Le wrapper Air retourne un CloneResult qui distingue clairement le côté parent du côté enfant. Pour le parent, le PID de l’enfant est retourné, plus optionnellement un PidFd si CLONE_PIDFD était demandé.

Pattern recommandé.

#![allow(unused)]
fn main() {
let mut args = CloneArgs::default();
args.flags = CloneFlags::PIDFD;
args.exit_signal = Some(Signal::SIGCHLD);

// SAFETY: pas de mémoire partagée, fork classique avec pidfd
let result = unsafe { clone3(&args)? };

match result {
    CloneResult::Parent { child_pid, child_pidfd } => {
        let pidfd = child_pidfd.expect("requested PIDFD");
        // Attendre l'enfant via waitid sur le pidfd
    }
    CloneResult::Child => {
        // Code enfant
        // Probablement execve ici
        unreachable!("execve should not return");
    }
}
}

Erreurs.

  • EAGAIN : limites système atteintes (nombre maximum de processus).
  • EINVAL : combinaison de flags invalide.
  • ENOMEM : mémoire kernel insuffisante.
  • EPERM : capabilities insuffisantes pour les flags demandés.
  • EUSERS : trop de namespaces utilisateurs.

Performance.

clone3 avec fork classique : ~50-100 µs. Le coût principal est la duplication des tables de pages (copy-on-write).

clone3 pour création de thread : ~20-50 µs.

clone3 avec namespaces : variable, ~100-500 µs selon les namespaces demandés.

Portabilité.

Strictement Linux. Pas d’équivalent direct sur les autres Unix (qui ont fork/vfork/pthread_create séparés).

Tests.

  • Test fork classique : créer un processus enfant qui exit immédiatement, waitid, vérifier code de sortie.
  • Test pidfd : créer avec CLONE_PIDFD, vérifier que le pidfd est valide.
  • Test exec dans enfant : fork puis execve, vérifier que le binaire externe est exécuté.
  • Test namespaces : créer dans nouveau PID namespace, vérifier l’isolation.
  • Test capabilities : tenter sans privilèges, vérifier EPERM.

execve

Signature.

#![allow(unused)]
fn main() {
pub fn execve(
    path: &CStr,
    argv: &[&CStr],
    envp: &[&CStr],
) -> Result<Infallible, Errno>;
}

Syscall sous-jacent. execve (x86_64 n°59, ARM64 n°221). Page man execve(2).

Préconditions.

argv et envp sont des tableaux de chaînes C-compatibles. Le wrapper convertit en *const *const c_char en interne.

Comportement.

Remplace l’image mémoire du processus courant par celle du binaire à path. En cas de succès, ne retourne jamais (le processus exécute désormais le nouveau binaire). En cas d’échec, retourne avec une Errno.

Type de retour.

Result<Infallible, Errno> : Infallible est l’idiome Rust pour “n’arrive jamais en succès”. L’utilisateur peut faire let _: Infallible = execve(...)?; puis avoir un unreachable!().

Erreurs.

  • E2BIG : argv/envp trop grands.
  • EACCES : permissions insuffisantes pour exécuter path.
  • ENOENT : path n’existe pas.
  • ENOEXEC : path n’est pas un exécutable valide.
  • ENOMEM : mémoire insuffisante.
  • ETXTBSY : le binaire est ouvert en écriture par un autre processus.

Performance.

Variable selon la taille du binaire et la complexité du loader. Typiquement ~1-5 ms pour un binaire simple, plus pour des binaires liés dynamiquement avec beaucoup de bibliothèques.

Tests.

  • Test nominal : fork puis execve d’un binaire simple (/bin/true), waitid.
  • Test ENOENT : execve sur chemin inexistant.
  • Test EACCES : execve sur fichier non exécutable.

execveat

Signature.

#![allow(unused)]
fn main() {
pub fn execveat(
    dirfd: DirFd,
    path: &CStr,
    argv: &[&CStr],
    envp: &[&CStr],
    flags: ExecveatFlags,
) -> Result<Infallible, Errno>;

bitflags! {
    pub struct ExecveatFlags: i32 {
        const EMPTY_PATH = 0x1000;
        const SYMLINK_NOFOLLOW = 0x100;
    }
}
}

Syscall sous-jacent. execveat (x86_64 n°322, ARM64 n°281). Disponible depuis Linux 3.19.

Comportement.

Variante de execve qui prend un FD de répertoire (pour résolution de path relative) ou un FD direct vers le binaire (avec EMPTY_PATH).

Cas d’usage principal : exécuter un binaire référencé par un FD ouvert précédemment, sans race sur le chemin filesystem.

#![allow(unused)]
fn main() {
let bin_fd = openat(DirFd::Cwd, c"/usr/bin/myapp", OpenFlags::PATH | OpenFlags::CLOEXEC, Mode::empty())?;
// Plus tard, après vérifications éventuelles :
execveat(DirFd::Fd(bin_fd.as_fd()), c"", &argv, &envp, ExecveatFlags::EMPTY_PATH)?;
}

Tests.

  • Test EMPTY_PATH : openat sur binaire, execveat avec FD.
  • Test path relatif : execveat avec dirfd + path.

Sous-section 3 : Attente et terminaison

waitid

Signature.

#![allow(unused)]
fn main() {
pub fn waitid(
    target: WaitTarget,
    options: WaitOptions,
) -> Result<Option<WaitStatus>, Errno>;

pub enum WaitTarget {
    AnyChild,
    Pid(Pid),
    ProcessGroup(Pid),
    AnyProcessGroup,
    PidFd(BorrowedFd<'_>),
}

bitflags! {
    pub struct WaitOptions: i32 {
        const EXITED = 4;
        const STOPPED = 2;
        const CONTINUED = 8;
        const NOHANG = 1;
        const NOWAIT = 0x01000000;
    }
}

pub struct WaitStatus {
    pub pid: Pid,
    pub uid: u32,
    pub event: WaitEvent,
}

pub enum WaitEvent {
    Exited { code: i32 },
    Killed { signal: Signal, core_dumped: bool },
    Stopped { signal: Signal },
    Continued,
    Trapped { signal: Signal },
}
}

Syscall sous-jacent. waitid (x86_64 n°247, ARM64 n°95). Page man waitid(2).

Préconditions.

WaitOptions doit contenir au moins un de EXITED, STOPPED, ou CONTINUED (sinon waitid ne sait pas quels événements rapporter).

Comportement.

Attend qu’un événement survienne sur le processus ou groupe ciblé. Plus puissant que wait ou waitpid parce qu’il peut attendre sur un pidfd directement (évitant les races) et discriminer les types d’événements.

Le wrapper retourne Result<Option<WaitStatus>, Errno>. Ok(None) indique que NOHANG était demandé et qu’aucun événement n’était disponible.

Pattern avec pidfd.

#![allow(unused)]
fn main() {
let mut args = CloneArgs::default();
args.flags = CloneFlags::PIDFD;
// SAFETY: fork classique avec pidfd
let result = unsafe { clone3(&args)? };

if let CloneResult::Parent { child_pidfd: Some(pidfd), .. } = result {
    let status = waitid(WaitTarget::PidFd(pidfd.as_fd()), WaitOptions::EXITED)?;
    match status {
        Some(WaitStatus { event: WaitEvent::Exited { code }, .. }) => {
            println!("Child exited with code {}", code);
        }
        _ => { /* autres cas */ }
    }
}
}

Erreurs.

  • ECHILD : pas d’enfant à attendre.
  • EINTR : interrompu par un signal (conformément à la convention 2 d’ADR-021, remonté à l’appelant).
  • EINVAL : options invalides.

Performance.

Variable. Avec NOHANG, retour immédiat ~1 µs. Sans NOHANG, bloquant jusqu’à événement.

exit_group

Signature.

#![allow(unused)]
fn main() {
pub fn exit_group(status: i32) -> !;
}

Syscall sous-jacent. exit_group (x86_64 n°231, ARM64 n°94). Page man exit_group(2).

Comportement.

Termine tous les threads du processus appelant avec le code de sortie spécifié. Ne retourne jamais.

Type de retour.

! (never type) : la fonction ne retourne jamais.

Performance. Négligeable côté appelant (le processus disparaît).

pidfd_open

Signature.

#![allow(unused)]
fn main() {
pub fn pidfd_open(pid: Pid, flags: PidFdOpenFlags) -> Result<PidFd, Errno>;

bitflags! {
    pub struct PidFdOpenFlags: u32 {
        const NONBLOCK = 0x800;
    }
}

pub struct PidFd { /* possède OwnedFd interne */ }

impl PidFd {
    pub fn as_fd(&self) -> BorrowedFd<'_>;
    pub fn into_fd(self) -> OwnedFd;
}
}

Syscall sous-jacent. pidfd_open (x86_64 n°434, ARM64 n°434). Disponible depuis Linux 5.3.

Comportement.

Ouvre un FD qui référence un processus par PID. Le FD reste valide même si le PID est recyclé après mort du processus original (le FD ne se “transfère” pas à un nouveau processus).

Le pidfd peut ensuite être utilisé pour :

  • waitid (attendre la fin du processus).
  • pidfd_send_signal (envoyer un signal sans race sur le PID).
  • pidfd_getfd (récupérer un FD du processus cible).
  • Surveillance via poll/epoll (le FD devient lisible quand le processus se termine).

Erreurs.

  • EINVAL : flags invalides.
  • ESRCH : processus inexistant.
  • ENOMEM : mémoire kernel insuffisante.

Performance. ~5-10 µs.

Tests.

  • Test nominal : fork enfant, pidfd_open sur son PID, waitid sur le pidfd.
  • Test ESRCH : pidfd_open sur PID inexistant.
  • Test post-mortem : créer pidfd, tuer processus, vérifier que le pidfd reste valide jusqu’à waitid.

pidfd_send_signal

Signature.

#![allow(unused)]
fn main() {
pub fn pidfd_send_signal(
    pidfd: BorrowedFd<'_>,
    signal: Signal,
    info: Option<&SignalInfo>,
) -> Result<(), Errno>;
}

Syscall sous-jacent. pidfd_send_signal (x86_64 n°424, ARM64 n°424). Disponible depuis Linux 5.1.

Comportement.

Envoie un signal au processus référencé par le pidfd. Pas de race sur le PID recyclé : si le processus original est mort, le signal n’est pas délivré à un éventuel nouveau processus avec le même PID.

À préférer à kill(pid, sig) quand un pidfd est disponible.

Erreurs.

  • EBADF : pidfd invalide.
  • EINVAL : signal invalide.
  • EPERM : permissions insuffisantes.
  • ESRCH : le processus référencé n’existe plus.

pidfd_getfd

Signature.

#![allow(unused)]
fn main() {
pub fn pidfd_getfd(
    pidfd: BorrowedFd<'_>,
    target_fd: RawFd,
    flags: u32,
) -> Result<OwnedFd, Errno>;
}

Syscall sous-jacent. pidfd_getfd (x86_64 n°438, ARM64 n°438). Disponible depuis Linux 5.6.

Comportement.

Récupère un duplicata d’un FD ouvert dans un autre processus. Nécessite CAP_SYS_PTRACE ou être le même utilisateur que le processus cible.

Cas d’usage spécialisé : debuggers, outils de surveillance, transfert de ressources entre processus coopérant.

Sous-section 4 : Groupes et sessions

setpgid, getpgid, setsid, getsid

Signatures.

#![allow(unused)]
fn main() {
pub fn setpgid(pid: Option<Pid>, pgid: Option<Pid>) -> Result<(), Errno>;
pub fn getpgid(pid: Option<Pid>) -> Result<Pid, Errno>;
pub fn setsid() -> Result<Pid, Errno>;
pub fn getsid(pid: Option<Pid>) -> Result<Pid, Errno>;
}

Syscalls sous-jacents. setpgid (x86_64 n°109, ARM64 n°154), getpgid (n°121, n°155), setsid (n°112, n°157), getsid (n°124, n°156).

Préconditions.

Option<Pid> permet de signifier “le processus courant” via None (au lieu de la sentinelle 0 du kernel). Cohérent avec la convention 1 de l’ADR-021.

Comportement.

Gestion des process groups et sessions Unix. Usage rare dans les applications modernes mais nécessaire pour les shells, les démons, et certaines applications terminal.

Performance. Négligeable (~1-2 µs).

Sous-section 5 : prctl (opérations exposées individuellement)

prctl est un syscall multiplexé : 60+ opérations distinctes selon le premier argument. Conformément à la convention 3 de l’ADR-021, Air ne wrappe pas prctl génériquement mais expose chaque opération comme fonction dédiée typée.

Opérations exposées

#![allow(unused)]
fn main() {
// PR_SET_PDEATHSIG / PR_GET_PDEATHSIG
pub fn set_parent_death_signal(signal: Option<Signal>) -> Result<(), Errno>;
pub fn get_parent_death_signal() -> Result<Option<Signal>, Errno>;

// PR_SET_NO_NEW_PRIVS / PR_GET_NO_NEW_PRIVS
pub fn set_no_new_privs() -> Result<(), Errno>;
pub fn get_no_new_privs() -> Result<bool, Errno>;

// PR_SET_NAME / PR_GET_NAME (nom du thread, max 16 octets)
pub fn set_thread_name(name: &CStr) -> Result<(), Errno>;
pub fn get_thread_name() -> Result<CString, Errno>;

// PR_SET_DUMPABLE / PR_GET_DUMPABLE
pub fn set_dumpable(dumpable: DumpableMode) -> Result<(), Errno>;
pub fn get_dumpable() -> Result<DumpableMode, Errno>;

pub enum DumpableMode {
    NotDumpable,
    Dumpable,
    SuidDumpable,
}

// PR_SET_KEEPCAPS / PR_GET_KEEPCAPS
pub fn set_keep_caps(keep: bool) -> Result<(), Errno>;
pub fn get_keep_caps() -> Result<bool, Errno>;

// PR_SET_TIMERSLACK
pub fn set_timer_slack(slack_ns: u64) -> Result<(), Errno>;

// PR_CAP_AMBIENT (avec sous-opérations)
pub fn cap_ambient_raise(cap: Capability) -> Result<(), Errno>;
pub fn cap_ambient_lower(cap: Capability) -> Result<(), Errno>;
pub fn cap_ambient_clear_all() -> Result<(), Errno>;
pub fn cap_ambient_is_set(cap: Capability) -> Result<bool, Errno>;

// PR_SET_SECCOMP_MODE_FILTER (alternative au syscall seccomp direct)
// Non exposé ici, préférer seccomp_load_filter dans la famille security.
}

Cette liste couvre les opérations prctl les plus utilisées. Les opérations rares (PR_TASK_PERF_EVENTS, PR_GET_TID_ADDRESS, etc.) sont listées dans UNSUPPORTED.md et peuvent être ajoutées au cas par cas si Air en a besoin.

Sous-section 6 : Limites de ressources

getrlimit, setrlimit, prlimit

Signatures.

#![allow(unused)]
fn main() {
pub fn getrlimit(resource: Resource) -> Result<Rlimit, Errno>;
pub fn setrlimit(resource: Resource, limit: Rlimit) -> Result<(), Errno>;
pub fn prlimit(
    pid: Option<Pid>,
    resource: Resource,
    new_limit: Option<Rlimit>,
) -> Result<Rlimit, Errno>;

pub enum Resource {
    Cpu,
    FileSize,
    Data,
    Stack,
    Core,
    Rss,
    NProc,
    NoFile,
    MemLock,
    As,
    Locks,
    SigPending,
    MsgQueue,
    Nice,
    RtPrio,
    RtTime,
}

pub struct Rlimit {
    pub soft: RlimitValue,
    pub hard: RlimitValue,
}

pub enum RlimitValue {
    Finite(u64),
    Infinity,
}
}

Syscalls sous-jacents. getrlimit (x86_64 n°97, ARM64 n°163), setrlimit (n°160, n°164), prlimit64 (n°302, n°261).

Comportement.

Lit et modifie les limites de ressources d’un processus. prlimit est plus puissant : permet d’opérer sur un autre processus (avec privilèges appropriés) et fait les deux opérations (lecture ancienne valeur + écriture nouvelle) en un seul appel atomique.

Recommandation Air. Préférer prlimit partout. getrlimit et setrlimit sont exposés pour compatibilité mais sont essentiellement des cas particuliers de prlimit.

Erreurs.

  • EFAULT : ne devrait pas arriver via l’API safe.
  • EINVAL : ressource invalide ou limite invalide.
  • EPERM : tentative de relever la hard limit sans privilèges.
  • ESRCH : processus inexistant (pour prlimit).

Sous-section 7 : Capabilities

capget, capset

Signatures.

#![allow(unused)]
fn main() {
pub fn capget(target: CapabilityTarget) -> Result<CapabilitySet, Errno>;
pub fn capset(target: CapabilityTarget, set: &CapabilitySet) -> Result<(), Errno>;

pub enum CapabilityTarget {
    CurrentThread,
    Thread(Tid),
    Process(Pid),
}

pub struct CapabilitySet {
    pub effective: CapabilityMask,
    pub permitted: CapabilityMask,
    pub inheritable: CapabilityMask,
}

#[derive(Debug, Clone, Copy)]
pub struct CapabilityMask(u64);

#[derive(Debug, Clone, Copy)]
pub enum Capability {
    Chown,
    DacOverride,
    DacReadSearch,
    Fowner,
    Fsetid,
    Kill,
    Setgid,
    Setuid,
    SysAdmin,
    SysBoot,
    SysChroot,
    SysModule,
    SysNice,
    SysPtrace,
    SysRawio,
    SysResource,
    SysTime,
    NetAdmin,
    NetBindService,
    NetRaw,
    // ... autres capabilities Linux
}
}

Syscalls sous-jacents. capget (x86_64 n°125, ARM64 n°90), capset (n°126, n°91).

Comportement.

Gestion des capabilities Linux : permissions fines remplaçant le modèle binaire root/non-root.

Trois ensembles par thread :

  • Effective : capabilities actuellement utilisables.
  • Permitted : capabilities que le thread peut activer.
  • Inheritable : capabilities transmises à travers execve.

Performance. ~1-2 µs.

Tests.

  • Test capget courant : lire les capabilities, vérifier cohérence.
  • Test capset drop : retirer une capability, vérifier qu’elle disparaît.
  • Test EPERM : tenter d’élever une capability non permitted, vérifier l’erreur.

Sous-section 8 : exec / redirection (extension couche 1)

Extension additive pilotée par le contrat de la couche 1 (air-process) : au-delà de la création de processus (clone3) et de l’exec (execve/execveat, sous-section 2), lancer un programme exige de marshaller ses arguments, de rediriger ses flux standard (dup3), de fixer son répertoire de travail (fchdir/chdir) et de terminer proprement l’enfant si l’exec échoue (exit_group, sous-section 3, promu en primitive publique).

Marshalling argv / envpCStrArray

execve/execveat attendent des tableaux char *const argv[] terminés par un pointeur NULL. Le type CStrArray<'a> matérialise cette frontière de marshalling à partir d’une tranche de CStr Rust sûrs :

#![allow(unused)]
fn main() {
pub struct CStrArray<'a> { /* Vec<*const c_char> NULL-terminé, emprunte 'a */ }

impl<'a> CStrArray<'a> {
    pub fn new(items: &'a [&'a CStr]) -> Self; // tranche vide → { NULL }
    pub fn len(&self) -> usize;                // hors terminateur NULL
    pub fn is_empty(&self) -> bool;
}
}

C’est la frontière fuzzée (fuzz/fuzz_targets/fuzz_exec_argv.rs, via process::fuzz_api::marshal_exec_args) :

  • rejet propre des NUL embarqués : un argument C ne peut pas contenir d’octet NUL ; l’invariant est garanti en amont, au point où l’appelant construit ses &CStr (CStr::from_bytes_with_nul / CString::new rejettent un NUL embarqué) — pas de revalidation ici (« parse, don’t validate ») ;
  • totalité : construction sans panic pour toute liste (vide, longue) ;
  • terminaison NULL systématique, ordre préservé.

L’allocation du tableau de pointeurs est l’exception ADR-021 §4 (buffer dynamique intrinsèque : taille = nombre d’arguments, inconnu à la compilation). Les chaînes elles-mêmes ne sont pas copiées.

dup3

Signature.

#![allow(unused)]
fn main() {
pub fn dup3(old: BorrowedFd<'_>, new: RawFd, flags: Dup3Flags) -> Result<OwnedFd, Errno>;

bitflags! { pub struct Dup3Flags: i32 { const CLOEXEC = 0o2_000_000; } }
}

Syscall sous-jacent. dup3 (x86_64 n°292, ARM64 n°24). Page man dup3(2).

Comportement. Variante moderne de dup2 (préférée, ADR-021) : duplique old vers le numéro new en fermant atomiquement new s’il était ouvert, et exige des drapeaux explicites (O_CLOEXEC est le seul valide). Cas d’usage couche 1 : rediriger les flux standard d’un enfant (dup3(pipe, STDOUT_FILENO, …)) juste avant l’exec. À la différence de dup2, échoue avec EINVAL si old == new (pas de no-op silencieux). Le new retourné est possédé (OwnedFd, fermé au Drop).

Erreurs. EINVAL (flags invalides ou old == new), EBADF (new hors plage RLIMIT_NOFILE), EBUSY (course rare), EINTR (remonté tel quel, convention 2 ADR-021).

Tests. Redirection réussie sur un fd haut non réutilisable (résultat possédé) ; old == newEINVAL.

fchdir, chdir

Signatures.

#![allow(unused)]
fn main() {
pub fn fchdir(dir: BorrowedFd<'_>) -> Result<(), Errno>;
pub fn chdir(path: &CStr) -> Result<(), Errno>;
}

Syscalls sous-jacents. fchdir (x86_64 n°81, ARM64 n°50), chdir (x86_64 n°80, ARM64 n°49).

Comportement. Fixent le répertoire de travail courant du processus. Cas d’usage couche 1 : current_dir d’un enfant juste avant l’exec. Préférer fchdir quand un fd de répertoire est disponible (pas de race sur le chemin).

Erreurs. fchdir : EBADF, ENOTDIR (le fd n’est pas un répertoire), EACCES. chdir : ENOENT, ENOTDIR, EACCES, ENAMETOOLONG.

Tests. Erreurs en process (fchdir sur /dev/nullENOTDIR ; chdir sur chemin inexistant → ENOENT) ; succès dans un enfant forké (le cwd est un état process-global : l’isolation par fork évite de perturber les tests fs concurrents), vérifiés via /proc/self/cwd.

Récapitulatif de la famille process

Fonctions exposées :

CatégorieFonctions principales
Identitégetpid, gettid
Créationclone3, execve, execveat
Attentewaitid, exit_group
Redirection / cwddup3, fchdir, chdir
Pidfdpidfd_open, pidfd_send_signal, pidfd_getfd
Groupessetpgid, getpgid, setsid, getsid
prctl~12 opérations individuelles
Limitesgetrlimit, setrlimit, prlimit
Capabilitiescapget, capset

Total : 21 syscalls wrappés, plus les opérations prctl individuelles. (Le marshalling argv/envp est exposé via le type CStrArray.)

Syscalls non-wrappés (listés dans UNSUPPORTED.md) :

  • fork, vfork : remplacés par clone3.
  • clone (sans le 3) : remplacé par clone3.
  • wait, wait3, wait4, waitpid : remplacés par waitid.
  • kill : exposé dans la famille signal, pas dans process.
  • tkill : deprecated, remplacé par tgkill (dans famille signal).
  • _exit : remplacé par exit_group.
  • dup, dup2 : remplacés par dup3 (drapeaux explicites, refus du no-op old == new).
  • Opérations prctl rares non listées ci-dessus.

Types ajoutés à air-sys-types

  • Pid, Tid
  • PidFd, PidFdReceiver, TidReceiver
  • WaitTarget, WaitOptions, WaitStatus, WaitEvent
  • CloneFlags, CloneArgs, CloneResult
  • Signal, SignalMask (utilisés aussi en famille signal)
  • Capability, CapabilityMask, CapabilitySet, CapabilityTarget
  • Resource, Rlimit, RlimitValue
  • DumpableMode
  • ExecveatFlags, Dup3Flags, PidFdOpenFlags
  • CStrArray (marshalling argv/envp, exposé par air-sys-syscall::process)

Soit ~20 types pour cette famille.

Décisions de fond émergées dans la famille process

1. Pid et Tid comme newtypes distincts.

Empêche par typage la confusion entre PID de processus et TID de thread. Application directe du Principe 7.

2. Option<Pid> pour “processus courant”.

Au lieu de la sentinelle kernel 0 qui signifie “le processus appelant”, Air utilise None. Plus clair, plus sûr, application de la convention 1 de l’ADR-021.

3. PidFd RAII avec ownership clair.

Le pidfd est encapsulé dans un type qui ferme automatiquement le FD à la destruction. Pas de fuite possible.

4. clone3 unsafe mais avec helper safe à venir.

L’API de bas niveau reste unsafe parce que CLONE_VM peut violer les invariants Rust. Un helper safe fork_process couvrant le cas commun (fork classique sans VM partagée) sera exposé en couche 1.

5. prctl exposé opération par opération.

Refus du multiplexé générique. Chaque opération prctl est une fonction dédiée typée. Coût en volume de code compensé par la clarté.


Licence du document : MPL 2.0 Statut : Spécification technique du module air-sys-syscall::process (couche 0).

Sous-section 7 : runtime & libc (ADR-051, re-sceau couche-0-v1.7)

Six syscalls ajoutés pour le runtime Air et l’objectif libc (cf. ADR-051). Consommateurs : les objets couche 1AirRuntime/ThreadControlBlock/ThreadLocalStorage et air-process — jamais le runtime en direct (seule la couche 1 consomme la couche 0, ADR-052). Le 7ᵉ syscall du lot, arch_prctl, est x86_64-only et vit dans la famille arch (family-arch.md).

getppid

#![allow(unused)]
fn main() {
pub fn getppid() -> Option<Pid>;
}

Syscall sous-jacent. getppid (x86_64 n°110, ARM64 n°173). getppid(2). Retourne None quand le kernel rend 0 (parent non visible dans le PID namespace) — la sentinelle 0 est re-présentée en Option (ADR-021 §1).

set_tid_address

#![allow(unused)]
fn main() {
pub unsafe fn set_tid_address(clear_child_tid: Option<&AtomicU32>) -> Tid;
}

Syscall sous-jacent. set_tid_address (x86_64 n°218, ARM64 n°96). Enregistre clear_child_tid (mot mis à 0 + FUTEX_WAKE à la mort du thread — primitif de join) et rend le TID appelant. unsafe : la mémoire pointée doit rester valide jusqu’à la fin du thread (même contrat que CloneArgs::child_tid).

sched_yield

#![allow(unused)]
fn main() {
pub fn sched_yield();
}

Syscall sous-jacent. sched_yield (x86_64 n°24, ARM64 n°124). Cède le CPU. Fonction totale (réussit toujours sous Linux).

umask

#![allow(unused)]
fn main() {
pub fn umask(new_mask: Mode) -> Mode;
}

Syscall sous-jacent. umask (x86_64 n°95, ARM64 n°166). Fixe le masque de création de fichiers, rend l’ancien. Fonction totale.

getcwd

#![allow(unused)]
fn main() {
pub fn getcwd(buf: &mut [u8]) -> Result<&[u8], Errno>;
}

Syscall sous-jacent. getcwd (x86_64 n°79, ARM64 n°17). Écrit le chemin absolu du répertoire courant dans buf (octets, jamais supposé UTF-8 — Principe 3 ; buffer fourni, zéro alloc — ADR-021 §4) et rend la tranche sans le NUL final. ERANGE si buf trop petit.

getrusage

#![allow(unused)]
fn main() {
pub fn getrusage(who: RusageWho) -> Result<Rusage, Errno>;
}

Syscall sous-jacent. getrusage (x86_64 n°98, ARM64 n°165). Statistiques de ressources pour RusageWho::{SelfProcess, Children, Thread}. Rusage est #[repr(C)] fidèle au kernel (2 Timeval + 14 long, 144 octets). Le bras d’erreur est inatteignable via l’API typée (exception STRUCTURAL, cf. COVERAGE-EXCEPTIONS.md).