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

COVERAGE-EXCEPTIONS.md

Exceptions de couverture légitimes pour la couche 0 d’Air.

Chaque entrée correspond à une branche ou à des lignes de production qui ne peuvent pas être atteintes via l’API safe ou en environnement CI non-root, et qui ne constituent pas une dette de qualité.

Taxonomie gravée par ADR-035 (companion d’ADR-031) :

  • STRUCTURAL : branche logiquement inatteignable par construction (invariant garanti : longueur ≤ borne, sémantique kernel per-process, free-list, bid < count…). Inclut l’ancienne EFAULT-SAFE (bras EFAULT/EBADF exclu car buffers/sorties valides par construction — le mot-clé « EFAULT-SAFE » reste dans les justifications) et l’ancienne VALUE-UNREACHABLE permanente (joker #[non_exhaustive], garde défensive permanente).
  • PRIVILEGE : nécessite une capability / condition de privilège (CAP_*, RLIMIT_*, Yama…) absente sur le runner.
  • FEATURE-KERNEL : feature/option kernel présente ⇒ le bras de repli (kernel ancien / feature absente) est mort.
  • CHILD-EXIT : code exécuté dans un enfant forké mais profil LLVM non flushable (priv-drop / seccomp-strict / landlock-lock / abort) — limite d’instrumentation du modèle fork+flush d’ADR-031 ; preuve par le code de sortie observé via waitid.
  • DEFERRED-TOOLING : branche réellement atteignable non encore outillée — à couvrir, jamais exemption permanente ; chaque entrée nomme l’outillage requis.
  • TARGET-ONLY (amendement ADR-035, 2026-06-29 ; couche 1) : code sound uniquement sur *-linux-air (registre TLS, TCB, relocation static-PIE, spawn CLONE_SETTLS) — ni exécuté ni mesuré par le gate (*-linux-gnu, glibc : unsound, #151). Granularité fichier (--ignore-filename-regex) ; preuve par le selftest rt/ on-target (exit code, 2 arches). Exception légitime, pas une dette. Cf. section « air-runtime — couche 1 » ci-dessous. Hors décompte du récapitulatif production couche 0.

Règle (ADR-035) : les bras de code de test (court-circuit d’assert!, gardes de harnais) ne figurent PAS au registre production — voir l’annexe en fin de fichier. Invariant : l’ensemble « couvrable » (branches atteignables non couvertes hors exception légitime) reste VIDE.

Application de la taxonomie (055, 2026-06-14). EFAULT-SAFE et VALUE-UNREACHABLE (permanente) reclassées en STRUCTURAL ; TEST-HARNESS sorti du registre production (→ annexe) ; 3 candidates « atteignables-mais- dures » désormais COUVERTES par de vrais tests, leurs exceptions supprimées (fs::try_lock contention, net::get_so_error async, process::pidfd_send_signal Some) ⇒ DEFERRED-TOOLING défini mais VIDE.


Méthodologie de mesure des branches — artefact [True:0, False:0] (élucidé)

cargo +nightly llvm-cov --workspace --branch (tous targets) colle, sur des lignes pourtant couvertes, des régions de branche fantômes Branch (L:C): [True: 0, False: 0] au coordonnées identiques à une branche réelle couverte. Investigation (session 3f) :

  • Cause prouvée. Elles proviennent exclusivement du binaire de test d’intégration (crates/air-sys-syscall/tests/main_thread_invariant.rs), qui lie la lib sans #[cfg(test)] et n’exécute que getpid/gettid. Son instanciation (hash de coverage-map différent de celle compilée cfg(test) pour les tests unitaires) contribue un [0,0] pour chaque branche qu’il n’exécute pas ; llvm-cov ne peut pas le fusionner avec les compteurs réels et liste les deux.
  • Preuve reproductible. --workspace → 36 shadows sur process.rs ; --workspace --lib (exclut le binaire d’intégration) → 0 shadow, 60 régions réelles. Chaque shadow [0,0] partageait sa coordonnée exacte avec une branche couverte (36/36) — une branche source réelle ne peut pas être à la fois couverte et jamais atteinte.
  • Décision de mesure. La barrière branches se mesure cargo +nightly llvm-cov --workspace --lib --branch (artefact-free). Les lignes se mesurent --workspace (les compteurs de lignes fusionnent correctement, pas de shadow). Ce ne sont donc pas des entrées d’exception à énumérer : c’est une correction de commande de mesure. Le binaire d’intégration garde sa propre couverture (getpid/gettid) hors métrique de branche.

Note — cohabitation toolchain (exceptions CHILD-EXIT temporaires)

Plusieurs familles (process, signal, security) exercent des wrappers couche 0 uniquement dans des enfants forkés (clone3). Le profil LLVM de ces enfants n’est plus flushé : flush_child_coverage est désormais vide. Raison : un enfant forké hérite d’un espace d’adressage copié, et __llvm_profile_write_file alloue — si un thread frère tenait le lock interne de malloc au moment du clone3, ce lock est copié verrouillé (détenteur absent dans l’enfant) → le malloc de l’enfant deadlocke (flaky couche 0 qui faisait pendre le gate). L’enfant doit donc rester async-signal-safe. Les lignes qu’il exécute sont des exceptions CHILD-EXIT, prouvées par le code de sortie observé par le parent via waitid.

C’est un artefact de la cohabitation entre le code bas-niveau Air — qui forke via clone3 brut (couche 0, kernel = bible, sans la danse atfork d’une libc) — et glibc, dont l’allocateur sert au runtime de couverture ; cohabitation imposée par une toolchain rustc/LLVM qui n’est pas Air-compatible. Le fork() d’une libc, lui, ferait l’atfork (verrouiller l’allocateur avant le clone, le réinitialiser dans l’enfant) ; clone3 brut, non.

Réactivable plus tard. Quand la libc Air sera prête et qu’on aura recompilé une toolchain Rust/LLVM/clang/clang++ sur la libc Air (probablement avec d’autres bibliothèques que la seule libc — à voir le moment venu), Air maîtrisera l’allocateur tiré en couverture (fork-safe, ou flush async-signal-safe) → la couverture de ces lignes pourra être rétablie. Donc : exception maintenant, couverture totale plus tard. (cf. mémoire projet « coverage CHILD-EXIT cohabitation toolchain ».)


fs — famille filesystem

Branche / LignesCatégorieJustification
open_by_handle_at — corps complet (chemin succès, l.2453-2495)PRIVILEGECAP_DAC_READ_SEARCH requis ; absent en CI non-root. Le chemin erreur (EPERM) est couvert par name_to_handle_at_enoent.
openat2 — repli return openat(...) sur ENOSYS (l.620) et branche if err == ENOSYS (l.655)FEATURE-KERNELopenat2(2) (Linux 5.6+) est présent sur les 2 exécuteurs → le repli vers openat n’est jamais pris. Atteignable seulement sur un kernel < 5.6.
close — branche d’erreur if ret < 0 => return Err (l.725)STRUCTURALclose d’un OwnedFd valide consommé : seuls EINTR (non retenté, convention 2 ADR-021) ou EBADF (impossible, fd possédé valide) ; non provoquable via l’API safe.
readlinkat — branche if n < cap côté faux + chemin de croissance du buffer (l.1642, 1646-1657)STRUCTURALLe buffer initial fait PATH_MAX (4096) ; un kernel Linux refuse de créer un symlink dont la cible dépasse PATH_MAX-1 (4095). Donc readlinkat retourne toujours n ≤ 4095 < 4096 → la troncature/croissance est inatteignable par construction sur les FS cibles (ext4/tmpfs).
name_to_handle_at_ok_or_eopnotsupp — bras Err(EOPNOTSUPP)/Err(e) => panic! (l.3507-3511) et 2ᵉ/3ᵉ opérandes du `assert!(…
Garde de skip if ret < 0 { return; } du test seals_* quand memfd_create/seals indisponible (l.3467)FEATURE-KERNELSkip pris uniquement si memfd/F_SEAL absents ; présents sur les exécuteurs.

fs — extension inotify (fs::inotify)

Décodeur pur (air-sys-types::fs : InotifyEvents/InotifyEvent) couvert à 100 % lignes + branches, incl. property-based + fuzz (fuzz_inotify_parse). Wrappers (fs::inotify) : intégration kernel réelle (CREATE/MODIFY/MOVED_FROM↔ MOVED_TO corrélés par cookie/DELETE/IGNORED, bon name ; remove_watch ; EAGAIN NONBLOCK ; add_watch ENOENT ; remove_watch EINVAL). Résidus :

Branche / LignesCatégorieJustification
inotify_init — bras d’erreur if ret < 0 => return ErrSTRUCTURALN’échoue qu’en épuisement de ressources (EMFILE/ENFILE/ENOMEM, max_user_instances) — condition système non provoquable de façon déterministe en CI (cf. spec §Tests). Le chemin succès est couvert ; le décodage d’errno est testé par ailleurs.
Bras Err des i32::try_from(ret).map_err(EINVAL) de inotify_init (fd) et add_watch (wd)STRUCTURALConversion défensive i64 → i32 (Principe 2, pas d’as nu) : ret ≥ 0 provient d’un FD/wd kernel, toujours ≤ i32::MAX → le bras d’erreur est inatteignable par construction.

ipc — famille IPC

Branche / LignesCatégorieJustification
Branches d’erreur (if ret < 0 => return Err) de eventfd2 (l.194) et pipe2 (l.243)STRUCTURALAvec des EventFdFlags/PipeFlags typés et valides, les seuls échecs possibles sont EFAULT (exclu, aucun pointeur user pour eventfd2) ou l’épuisement de fd (EMFILE/ENFILE, condition de ressource, pas un défaut de code). Les branches d’erreur atteignables sont testées : EventFd::readEAGAIN, EventFd::write(u64::MAX)EINVAL, vmsplice fd invalide→EBADF.
Garde de skip if test_memfd_create(...) < 0 { return; } du test tee_einval_on_non_pipeFEATURE-KERNELLe bras return (skip) n’est pris que si memfd_create est indisponible. Sur les 2 exécuteurs cibles memfd_create est présent → seul le bras « continuer » est exercé ; le skip relève d’un kernel sans memfd.

mem — famille mémoire

Branche / LignesCatégorieJustification
Mapping::drop / munmap — branche d’erreur if ret < 0 (l.133-134)STRUCTURALraw_munmap n’échoue (EINVAL) que sur une adresse/longueur invalide ; un Mapping/munmap(Mapping) part toujours d’un mapping vivant valide → inatteignable via l’API safe. Le chemin succès est couvert (munmap_explicit_succeeds).
munlockall — branche d’erreur if ret < 0 (l.670)STRUCTURALmunlockall(2) ne prend aucun pointeur utilisateur et ne nécessite aucun privilège sur Linux moderne (≥ 2.6.9) : il réussit toujours. Le chemin succès est couvert (munlockall_succeeds_or_eperm).
memfd_secret — branche d’erreur if ret < 0 (l.757, ENOSYS)FEATURE-KERNELCONFIG_SECRETMEM présent sur les 2 exécuteurs → memfd_secret réussit, l’arme ENOSYS n’est prise que sur un kernel < 5.14 sans secretmem. Test memfd_secret_ok_or_enosys (les deux issues).
mmap_fixed — chemin succès et bras fd = Some— (COUVERT)mmap_fixed_over_memfd_succeeds (MAP_FIXED par-dessus un memfd) et mmap_fixed_noreplace_eexist_on_occupied_addr (chemin d’erreur ? → EEXIST).
mlockall — corps de retour succès Ok(()) (l.651)PRIVILEGEPris seulement si CAP_IPC_LOCK / RLIMIT_MEMLOCK l’autorise. Divergence d’environnement constatée : couvert sur speedy (mlockall réussit), non pris sur raspi-srv-2 (mlockall → EPERM, hôte plus restreint). L’arme d’erreur est couverte sur les 2 arches par mlockall_einval_on_empty_flags (EINVAL).
Tests « Ok ou restreint » (mlock_munlock_pair, mlock2_onfault, mlockall_current_or_eperm, munlockall_succeeds_or_eperm, memfd_secret_ok_or_enosys, process_vm_readv/writev_own_memory) : le bras non pris selon le privilège de l’hôte (Ok sur hôte permissif, Err(EPERM/ENOMEM/EACCES/ENOSYS) sur hôte restreint)PRIVILEGECes tests prennent exactement un bras selon l’environnement (CAP_IPC_LOCK, RLIMIT_MEMLOCK, Yama ptrace_scope, secretmem). L’autre bras est donc structurellement mort sur cet hôte. Divergence attendue : sur speedy le bras Ok est pris (l’Err est mort), sur raspi plusieurs Err sont pris (l’Ok est mort). Le cœur de chaque test (l’opération elle-même) est exercé dans les deux cas.

net — famille réseau

Branche / LignesCatégorieJustification
raw_to_socket_addr — branche if len < 3 (AF_UNIX, l.164)STRUCTURALInatteignable : len < 2 (l.150) et len == 2 (l.161) sont déjà traités au-dessus → en ce point len ≥ 3 toujours. Les gardes de troncature len < 16 (IPv4) et len < 28 (IPv6) sont testées (raw_to_socket_addr_rejects_truncated_*).
parse_scm_rights — 2ᵉ borne du while interne d’extraction des FDs && fd_off+4 <= cmsg_buf.len() (l.278)STRUCTURALDéfense en profondeur contre un cmsg malformé annonçant plus de FDs que le buffer n’en contient. Le kernel fournit toujours des données de contrôle bien formées (end ≤ cmsg_buf.len()), donc la 1ʳᵉ borne (fd_off+4 <= end) sort de la boucle avant. Les autres branches du parseur sont couvertes (type/level/len ≠ SCM_RIGHTS, aligned == 0, dépassement de buffer).
get_so_error — bras None (closure) de NonZeroI32::new(val).ok_or_else(…) (net.rs)STRUCTURALDéfense en profondeur (Principe 5) : dans cette closure, val != 0 est déjà garanti par le else du if val == 0NonZeroI32::new(val) est donc toujours Some, la closure d’erreur EINVAL est inatteignable par construction. (Le bras Some(erreur) — erreur socket asynchrone — est désormais couvert par un vrai test : connect non bloquant vers un port loopback fermé → ECONNREFUSED via SO_ERROR, boucle bornée — voir get_so_error_some_on_refused_nonblocking_connect.)
Tests « variante connue » : if let SocketAddr::Ipv4/Ipv6/Unix(..) = getsockname(..) (l.2403/2426/2443/2478)STRUCTURALgetsockname sur un socket lié en IPv4/IPv6/Unix retourne déterministe­ment la variante correspondante ; le bras else { panic } est inatteignable par construction.
Test connect_ipv4_econnrefused — 2ᵉ opérande de err == ECONNREFUSED || err == EACCES (l.2505)STRUCTURALSur les 2 exécuteurs, connect(127.0.0.1:1)ECONNREFUSED (1ᵉʳ opérande vrai → court-circuit) ; le || EACCES couvre un hôte au pare-feu plus strict.
Test sockopt_ipv6_v6only_set_on_ipv6_socketif let Ok(fd) = socket(Ipv6, ..) (l.2349)FEATURE-KERNELLe bras else (création de socket IPv6 échoue) n’est pris que sur un hôte sans IPv6. Présent sur les 2 exécuteurs → seul le bras Ok est exercé.
Test accept4_eagain_on_nonblocking_without_connections — garde de skip if ret < 0 (fcntl F_SETFL) (l.2585)FEATURE-KERNELLe skip n’est pris que si fcntl(F_SETFL) échoue (jamais sur les kernels cibles). Le cœur du test (accept4 non-bloquant → EAGAIN) est exercé.
getpeername — closure None (EAFNOSUPPORT) de raw_to_socket_addr(..).ok_or_else(..) (net.rs, l.1060)STRUCTURALInatteignable par construction : getpeername(2) réussit sur un socket créé via l’API safe, dont le domaine ∈ {Unix(1), Ipv4(2), Ipv6(10)} (SocketDomain) — les trois familles que raw_to_socket_addr décode toujours en Some. Les gardes de troncature internes (len < 16/< 28) sont testées séparément (raw_to_socket_addr_rejects_truncated_*).
get_so_bindtodevice — closure map_err (EINVAL) de CString::new(&buffer[..nul_pos]) (net.rs, l.1390)STRUCTURALInatteignable par construction : nul_pos est la position du premier NUL (ou length à défaut), donc buffer[..nul_pos] ne contient aucun NUL interne → CString::new retourne toujours Ok. Le chemin succès est couvert (get_so_bindtodevice_unbound_returns_empty).

security — famille sécurité

Branche / LignesCatégorieJustification
Corps des wrappers exécutés dans l’enfant : raw_syscall_landlock_restrict_self (l.236-253), LandlockRuleset::restrict_self (l.420-424), seccomp_set_mode_strict (l.620-627), helper de test raw_exitCHILD-EXITExécutés dans un enfant forké. Le profil LLVM de l’enfant n’est plus flushé (flush_child_coverage est vide — async-signal-safety, § Note cohabitation toolchain). Auparavant un flush était tenté avant le verrou (le verrou interdit ensuite tout open/write), mais ce flush alloue → risque de deadlock après clone3. Ces lignes restent prouvées par le code de sortie observé via waitid. Couverture réactivable avec une toolchain Air-compatible.
Cohabitation toolchain — setup des wrappers seccomp/landlock exercé uniquement dans des enfants forkés (avant le verrou), désormais non flushé : seccomp_set_mode_filter, raw_syscall_seccomp, landlock_create_ruleset, landlock_add_rule, add_rule_path_beneath, raw_syscall_landlock_create_ruleset, raw_syscall_landlock_add_rule, run_in_forked_childCHILD-EXITExécutés dans un enfant forké (clone3) ; flush_child_coverage est vide (async-signal-safety — § Note cohabitation toolchain). Preuve d’exécution = code de sortie via waitid. Couverture réactivable avec une toolchain Air-compatible.
Bras défensifs / d’échec des fermetures enfant des tests fork (set_no_new_privs().is_err() => return, path_fd < 0 => return 14, Err(_) => code) et bras parent correspondants Exited { code: ≠0 } / other => panic!CHILD-EXITNe sont pris que si la séquence échoue côté enfant — jamais sur un noyau sain. Le return N est le canal de communication enfant→parent (un panic/expect dans l’enfant forké corromprait le protocole waitid), ils ne peuvent donc pas être remplacés.
Bras Err(Errno::ENOSYS) => 42 (+ Exited { code: 42 }) des tests fork seccomp/landlockFEATURE-KERNELPris uniquement si seccomp/Landlock est absent du noyau. Sur les noyaux cibles Air (Landlock + seccomp activés, vérifié sur les 2 exécuteurs) ils ne s’exécutent pas. Les sondes read-only (landlock_supported_abi, landlock_create_ruleset, as_fd) assument, elles, la capacité présente (pas de bras mort).
Branches CLONE_NEWUSER dans clone3FEATURE-KERNELkernel.apparmor_restrict_unprivileged_userns=1 sur Ubuntu 24.04+. Le code production de clone3 ne contient pas de branchement sur les bits CLONE_NEW* — seul le test environnement-dépendant est en lecture seule.

Note couverture (historique). Un spike avait introduit un flush explicite du profil LLVM de l’enfant (security.rs 40,8 %/10 % → 84,9 %/60 %), couvrant le setup des wrappers (create_ruleset, add_rule, filtre allow-all, no_new_privs). Ce flush a été retiré (flush_child_coverage vide) car il alloue dans un enfant forké → deadlock possible (§ Note cohabitation toolchain). Ce setup est donc redevenu CHILD-EXIT (entrée « Cohabitation toolchain » ci-dessus), prouvé par le code de sortie. Couverture réactivable avec une toolchain Air-compatible.


process — famille processus

Branche / LignesCatégorieJustification
Branches d’erreur des wrappers prctl simples — set_parent_death_signal/get_parent_death_signal, get_no_new_privs, set_thread_name/get_thread_name, set_dumpable/get_dumpable, set_keep_caps/get_keep_caps, set_timer_slack, cap_ambient_is_set/cap_ambient_raise/cap_ambient_lower/cap_ambient_clear_all, capget (noms complets cités pour le rapprochement mécanique du gate)FEATURE-KERNELCes appels ne peuvent pas échouer sur un kernel sain : l’entrée est validée en amont par les newtypes typés (Signal, DumpableMode, Capability, bool) → pas d’EINVAL ; aucun pointeur user pour les setters scalaires → pas d’EFAULT. La logique de décodage Errno elle-même est couverte via le helper commun errno_from_negative_syscall_ret testé en isolation.
Bras child_exit(N) d’échec/alternatif des fermetures enfant des tests fork (codes ≠ 0), gardes défensives (is_err() => …) et branches « impossibles » associées (ex. if new_sid == child_pid)CHILD-EXITLe profil LLVM de l’enfant n’est plus flushé (flush_child_coverage est vide — async-signal-safety, cf. § Note cohabitation toolchain ; le setup enfant est désormais CHILD-EXIT, cf. l’entrée « Cohabitation toolchain » ci-dessous). Restent ici les bras d’échec/alternatifs, jamais pris sur un environnement sain — et qui ne peuvent pas être remplacés par panic/expect (corromprait le protocole de sortie enfant→parent via waitid).
Cohabitation toolchain — corps des wrappers exercés uniquement dans des enfants forkés (privsep / sessions / cwd), désormais non flushés : setsid, raw_syscall_setsid, raw_syscall_setresuid, raw_syscall_setresgid, raw_syscall_setgroups, get_groups, chdir, fchdirCHILD-EXITExécutés dans un enfant forké (clone3) ; flush_child_coverage est vide (async-signal-safety — § Note cohabitation toolchain). Preuve d’exécution = code de sortie via waitid. Couverture réactivable avec une toolchain Air-compatible.
exit_group (l.448) et raw_syscall_exit_group (l.457-466)CHILD-EXITPrimitive de sortie appelée par child_exit après le flush du profil (et ne retournant jamais : -> !) ; ses lignes ne peuvent donc pas être comptabilisées. Exécution prouvée par le code de sortie observé via waitid.
Ligne std::process::abort() côté enfant (test waitid_decodes_child_killed_by_signal)CHILD-EXITabort() lève SIGABRT et bypasse l’atexit LLVM ; impossible de flusher avant (c’est précisément ce que le test provoque).
Branches de skip if is_root() { … } des tests de capabilitiesPRIVILEGELe bras root n’est pris que si le runner est root (scénario EPERM inapplicable) ; non pris sur les exécuteurs CI non-root.
cap_ambient_raise chemin succèsPRIVILEGECAP_SETPCAP + la capability visée requises. Le chemin erreur EPERM est couvert.
capset chemin succèsPRIVILEGECAP_SETPCAP/CAP_SETUID requis. Le chemin erreur EPERM est couvert.
debug_assert!(ret < 0 && ret > -4096) de errno_from_negative_syscall_ret (l.1939)— (COUVERT)Les deux côtés faux sont désormais couverts par 2 tests #[should_panic] (ret = 0 ; ret = -5000). Plus une exception.
Branches de détection d’environnement des helpers de test : if apparmor_restricts_userns() (l.2354), if yama_blocks_ptrace_on_own_child() (l.2697), let Ok(s) = read_to_string("/proc/sys/kernel/…") (l.2529)FEATURE-KERNELCôté non pris = configuration kernel opposée (AppArmor désactivé / Yama bloquant / sysctl absent). Sur les 2 exécuteurs cibles (Ubuntu, AppArmor userns=1, Yama=1, sysctls présents) un seul côté est atteignable. L’autre relève d’un kernel différemment configuré.
Gardes structurelles des harnais de test dont un seul côté est logiquement atteignable : match … Killed { signal, .. } if signal.as_raw() == SIGKILL (l.2264, 2689), if self_pid != pgid (l.2805), Some(m) if m > 0 => Finite(m) (l.3133), if !touches_infinity (l.3250)CHILD-EXITBras d’assertion d’un harnais : le côté « faux » correspond à un résultat que le test garantit ne pas se produire (signal ≠ SIGKILL après un kill(SIGKILL), pgid déjà égal, rlim ≤ 0, valeur infinie inattendue). Le remplacer par un assert perdrait l’aiguillage de validation ; il n’est pas réductible sans réécrire le test.
Garde du matches! de test CloneResult::Parent { … } if child_pid.as_raw() == 1234 (air-sys-types/process, l.768)STRUCTURALBras d’assertion : le côté faux (PID ≠ 1234) ne se produit pas dans le test qui fixe le PID à 1234. Pid/Tid/Signal::try_from_raw (gardes > 0 / ≤ NSIG) sont par ailleurs testés explicitement (valeurs négatives, nulles, hors borne).
privsep — bras erreur if ret < 0 { return Err(…) } de set_groups (l.1111), set_resgid (l.1177), set_resuid (l.1205)PRIVILEGEL’erreur de ces setters est EPERM, atteignable uniquement pour un appelant non privilégié (après réduction). Elle est exercée fonctionnellement par l’enfant forké privsep_drop_is_irreversible (regain root/gid → EPERM, et réajout de groupe → EPERM), prouvée par le code de sortie observé via waitid — mais cet enfant a largué ses privilèges, donc son profil LLVM n’est pas flushable (écriture du .profraw refusée à un non-root). Les chemins succès sont exercés par l’enfant privsep_setters_succeed_as_root mais, le flush enfant ayant été retiré (async-signal-safety — § Note cohabitation toolchain), ils sont désormais CHILD-EXIT (cf. l’entrée « Cohabitation toolchain » de cette famille), prouvés par le code de sortie. Réactivable avec une toolchain Air-compatible.
privsep — bras erreur if ret < 0 { return Err(…) } de get_resgid (l.1232), get_resuid (l.1261)STRUCTURALgetresgid/getresuid ne peuvent échouer qu’avec EFAULT (pointeur invalide) ; les trois sorties sont des variables de pile vivantes et alignées → l’échec est inatteignable par construction. Le chemin succès est couvert (privsep_getters_read_own_credentials, en processus).
privsep — corps des enfants privsep_drop_is_irreversible / privsep_resuid_none_changes_only_effective (sans branche locale : conditions combinées par &) ; lignes de skip if !is_root() { return } des 3 tests privilégiés ; bras other => panic! de run_childCHILD-EXIT / PRIVILEGELes enfants qui larguent leurs privilèges ne peuvent pas flusher leur profil (écriture .profraw refusée à un non-root) ; leurs lignes ne sont donc pas comptabilisées bien qu’exécutées (correction prouvée par le code de sortie). Le return de skip n’est pris que hors root (le gate de couverture tourne en root). Le bras panic de run_child n’est pris que si waitid rend autre chose qu’Exited — jamais sur un fork sain.
getrusage — bras d’erreur if ret < 0 { return Err(…) } (l.3669)STRUCTURALgetrusage(2) ne peut échouer que sur who invalide (EINVAL) ou pointeur invalide (EFAULT) — exclus par construction : RusageWho est un enum typé (3 valeurs valides) et le buffer usage est une variable de pile vivante et alignée (taille struct rusage). Le bras d’erreur est donc inatteignable via l’API safe ; le chemin succès est couvert (getrusage_self_succeeds_and_fills + variantes Children/Thread). Le décodage d’errno (errno_from_negative_syscall_ret) est couvert par ailleurs.

signal — famille signaux

Branche / LignesCatégorieJustification
Défense en profondeur SignalMask::{contains,with,without} — branches None => false/self (air-sys-types, l.330/346/360)STRUCTURALInatteignables tant que Signal::try_from_raw borne à [1, 64]. Gardées par Principe 5 (défense en profondeur).
unwrap_nz — bras None => panic!("unreachable…") (air-sys-types, l.142) et garde if n <= 0 || n > NSIGSTRUCTURALConst fn de construction des constantes Signal::SIGxxx : après la garde [1, NSIG], NonZeroI32::new(n) est toujours Some → le bras None est inatteignable (présent pour l’exhaustivité du match const). Les valeurs hors borne sont rejetées à la compilation ; les tests runtime #[should_panic] (n = 0, n = 65/100) couvrent la garde côté panic.
SignalMask::from_signals — bras None de 1_u64.checked_shl(bit_index) (air-sys-types, l.307)STRUCTURALbit_index = signal - 1 ≤ NSIG - 1 = 63 (signal borné [1, 64] par try_from_raw) → checked_shl(≤ 63) est toujours Some ; le décalage ne déborde jamais.
Branches d’erreur (if ret < 0 => return Err) de SignalFd::update_mask (l.107), signalfd_create (l.143), rt_sigprocmask (l.244), install_fatal_handler (l.509), restore_handler (l.539)STRUCTURALAvec un fd OwnedFd valide, un mask/pointeur valides, sigsetsize=8, des flags typés et un FatalSignal typé (Segv/Bus/Fpe/Ill), les seuls échecs possibles seraient EFAULT (pointeur invalide, exclu par construction), EBADF (fd possédé valide, exclu) ou l’épuisement de fd (EMFILE/ENFILE pour signalfd_create, condition de ressource, pas un défaut de code). Le chemin succès est couvert ; le décodage Errno lui-même l’est via errno_from_negative_syscall_ret (tests dédiés). Les branches d’erreur réellement atteignables sans privilège sont testées : SignalFd::readEAGAIN, tgkill/kill/rt_sigqueueinfoESRCH.
async_handler (ADR-066) — bras d’erreur if ret < 0 => return Err de async_handler::restore ; bras d’erreur if ret < 0 de rt_sigpendingSTRUCTURALrestore repasse au noyau une disposition valide capturée par un install réussi sur le même signal → pas d’EINVAL/EFAULT (pointeur local vivant). rt_sigpending écrit un sigset_t dans un buffer local vivant avec sigsetsize=8 fixé → seul EFAULT (exclu). Le chemin succès des deux est couvert en thread (async_handler::install réel + SIG_DFL/SIG_IGN via auto-tgkill, pas de fork ; rt_sigpending : SIGUSR1 bloqué+généré → pendant, drainé via signalfd) ; le bras d’erreur d’async_handler::install est, lui, testé (install(SIGKILL) → EINVAL). Décodage Errno couvert via errno_from_negative_syscall_ret.
Bras défensifs/alternatifs des fermetures enfant des tests fork (if …is_err() => child_exit(N), gardes de match Ok(e) if e.signal == … => child_exit(0) dont le côté faux n’est jamais pris) — kill/rt_sigqueueinfo/install+restore en enfant forkéCHILD-EXITLe profil LLVM de l’enfant n’est plus flushé (flush_child_coverage est vide — async-signal-safety, § Note cohabitation toolchain ; le setup enfant est désormais CHILD-EXIT, cf. l’entrée « Cohabitation toolchain » ci-dessous). Restent ici les bras d’échec/alternatifs, jamais pris sur un noyau sain, et qui ne peuvent pas être remplacés par panic/expect (corromprait le protocole de sortie enfant→parent via waitid).
Cohabitation toolchain — wrappers signaux exercés uniquement dans des enfants forkés, désormais non flushés : current_signal_mask, block_signals, unblock_signals, set_signal_mask, signalfd_create, wait_for_signal, kill, tgkill, rt_sigqueueinfo, install_fatal_handler, restore_handler, raw_syscall_rt_sigprocmask, raw_syscall_rt_sigaction, raw_syscall_kill, raw_syscall_tgkill, raw_syscall_rt_sigqueueinfo, raw_syscall_signalfd4, raw_syscall_readCHILD-EXITExécutés dans un enfant forké (clone3) ; flush_child_coverage est vide (async-signal-safety — § Note cohabitation toolchain). Preuve d’exécution = code de sortie via waitid. Couverture réactivable avec une toolchain Air-compatible.

system — famille système

Branche / LignesCatégorieJustification
sethostname chemin succèsPRIVILEGECAP_SYS_ADMIN requis. Le chemin erreur EPERM est couvert par sethostname_without_cap_sys_admin_returns_eperm.
uname — branche ret < 0 (l.219/220)STRUCTURALuname(2) ne peut échouer qu’avec EFAULT sur un pointeur invalide ; l’API safe fournit toujours une &mut KernelUtsname valide. Branche inatteignable par construction.
sysinfo — branche ret < 0 (l.265/266)STRUCTURALidem uname : seul EFAULT possible, exclu par construction.
getrandom — branche ret < 0 (l.174/175)STRUCTURALAvec une slice valide et des GetrandomFlags typés, les seuls échecs seraient EFAULT (exclu par construction) ou EAGAIN/EINTR lors d’un blocage sur pool d’entropie non initialisé — ne se produit pas sur un système démarré (et jamais avec GRND_INSECURE).

time — famille temps

Branche / LignesCatégorieJustification
clock_settime chemin succès avec modification effectivePRIVILEGECAP_SYS_TIME requis. Le chemin erreur EPERM est couvert par clock_settime_realtime_current_time_is_ok_or_eperm.

io_uring — famille io_uring (Temps 1, cœur)

Le cœur Temps 1 ne soumet qu’une opération : NOP (mono-shot, traitée inline par le kernel — le CQE est posté avant le retour de io_uring_enter). Plusieurs chemins de production ne sont donc atteignables qu’avec des opérations à complétion asynchrone (POLL/READ/TIMEOUT) ou multishot, dont la soumission relève de Temps ultérieurs (3d multishot, 3f sandbox). Les chemins correspondants sont prouvés au niveau unitaire là où c’est possible (logique slab, décodage CQE) ; leur câblage de bout en bout viendra avec l’API qui les rend atteignables.

Branche / LignesCatégorieJustification
map_rings/ring_sizes — bras else du layout deux mmaps (mod.rs l.313 ; Some(mmap_file(…)) l.338-344)FEATURE-KERNELIORING_FEAT_SINGLE_MMAP (Linux ≥ 5.4) est présent sur les 2 exécuteurs (kernel 6.12) → single_mmap est toujours vrai, le layout hérité à deux mmaps (SQ et CQ séparés) n’est jamais pris. Atteignable seulement sur un kernel < 5.4.
Attente bloquante bornée — chemin succès : wait_bounded Ok(()) (mod.rs l.536-537), wait_completion reboucle après enter réussi (l.863), wait_completion_timeout Ok(self.harvest_ready()) (l.915-916)STRUCTURALCes bras ne sont pris que si l’io_uring_enter(GETEVENTS, min_complete=1) réussit alors qu’aucune complétion non périmée n’était déjà moissonnée. En Temps 1 le seul op soumissible (NOP) est synchrone : son CQE est déjà présent au premier harvest_ready, avant tout enter d’attente. Atteignable avec un op à complétion asynchrone (POLL/READ/TIMEOUT), soumissible en Temps ≥ 3. Les chemins d’erreur (EINTR/ETIME/EAGAIN) sont couverts par le simulateur.
harvest_ready — bras SlotOutcome::More (mod.rs l.709-716)STRUCTURALLe CQE IORING_CQE_F_MORE n’est produit que par une opération multishot ; sa soumission relève du Temps 3d (multishot). La logique slab sous-jacente (slot maintenu vivant sur has_more) est couverte par slab::tests::multishot_completion_keeps_slot_alive.
opcode_number — bras _ => u8::MAX (mod.rs l.1057)STRUCTURALIoUringOpcode est #[non_exhaustive] : les 55 variantes actuelles sont toutes mappées explicitement (couvert par opcode_number_maps_all_retained_opcodes). Le joker ne garde que d’éventuelles variantes futures — value-unreachable aujourd’hui.
SubmissionRing::prepare — garde if space_left() == 0 { return None } (ring.rs l.191)STRUCTURALsubmit_nop vérifie self.sq.space_left() == 0 (→ EBUSY) avant d’appeler prepare (couvert par submit_nop_on_full_sq_returns_ebusy), puis dénote le retour par .expect("place SQ vérifiée"). La garde None de prepare est donc inatteignable via l’API : aucun appelant n’invoque prepare sans pré-vérifier la place.
InflightSlab::reserve_inner — bras else => unreachable!() du let Free { next_free } = slot.state (slab.rs)STRUCTURALInvariant : free_head ne référence que des slots Free (maintenu par reserve/reserve_multishot/complete/release, qui n’y remettent un slot qu’en l’état Free). Le else ne peut être pris sans violer l’invariant de la free-list — inatteignable par construction. (Cœur commun reserve_inner introduit au Temps 3d ; même invariant.)

io_uring — Temps 2a (opérations filesystem)

23 façades FS livrées (fs_ops.rs), couvertes de bout en bout en root par les round-trips d’intégration (kernel 6.12) + property-based + Miri (ownership). Les accesseurs de Completion (into_vectored_result/into_statx/into_xattr_result/ opened_fd) sont couverts chemins succès et erreur et bras défensifs. Résidu unique :

Branche / LignesCatégorieJustification
Gardes de débordement u32 sur la longueur de buffer : len_u32(buffer.len())? de submit_read/submit_write et len_u32(value.len())? de submit_setxattr/submit_getxattr/submit_fsetxattr/submit_fgetxattr — bras Err(EINVAL) (fs_ops.rs)STRUCTURALLe bras d’erreur n’est pris que pour un buffer > 4 GiB (usize > u32::MAX), non allouable de façon déterministe en CI (RAM + temps). La validation est nécessaire (le champ len du SQE est un u32, Principe 4) ; son chemin succès est couvert par tous les round-trips. Les autres gardes de longueur sont couvertes car testables sans allocation géante : sync_file_range/fadvise (u32::try_from(nbytes/length) sur un u64 > u32::MAX) et readv/writev (buffers.len() > IOV_MAX), par length_overflow_validations_reject_before_submission.

io_uring — Temps 2b (opérations réseau)

12 façades réseau livrées (net_ops.rs), couvertes en root par les round-trips d’intégration (AF_UNIX socketpair + TCP loopback) : send/recv, passage de FD SCM_RIGHTS (même-objet vérifié), MSG_CTRUNC sans fuite, socket/bind/listen/ connect/accept, cycle zero-copy à deux complétions (résultat→NOTIF) + échec non supporté, proptest, Miri (ownership zero-copy + msghdr). Les 5 accesseurs (into_socket_fd/accepted_fd/into_accept_result/into_receive_message_result/ into_zero_copy_buffer, + zero_copy_copied) sont couverts succès + erreur + bras défensifs. Résidu unique :

Branche / LignesCatégorieJustification
Gardes de débordement u32 sur la longueur de buffer : len_u32(buffer.len())? de submit_send/submit_receive/submit_send_zero_copy — bras Err(EINVAL) (net_ops.rs l.272/299/420)STRUCTURALIdentique au résidu 2a : pris seulement pour un buffer > 4 GiB (usize > u32::MAX), non allouable en CI. Le champ len du SQE est un u32 (Principe 4) ; le chemin succès est couvert par tous les round-trips send/recv et le property-based. Aucune autre branche/direction d’erreur de production 2b n’est rouge (ensemble « couvrable » vide) : EBUSY, validations, control.is_empty(), et les directions res<0 des 5 accesseurs sont toutes exercées.

io_uring — Temps 3a (registration : ressources fixes)

21 register opcodes + 3 variantes « direct » + read/write fixe + fixed_fd_install livrés (registration.rs), couverts en root par les round-trips d’intégration (kernel 6.17 : FixedFdTable sparse + openat2/socket/accept direct + read/write via FixedSlot + fixed_fd_install, RegisteredBuffers register/mmap/update/ clone_from + read/write fixed round-trip, eventfd notification, personality, io-wq affinité/max-workers, horloge, ring fd enregistré), le simulateur (tous les bras ? Err des façades register), les bornes (set/clear/update/ slice/set_alloc_range hors borne), et les doctests compile_fail (un FixedSlot/RegisteredBufferSlice ne survit pas au désenregistrement). Branches : 100 % (ensemble couvrable vide). Résidu lignes unique :

Branche / LignesCatégorieJustification
register_napi — corps succès Ok(NapiConfig { … }) et unregister_napiOk(()) (registration.rs), + le bras Ok du test register_napi_round_trip_when_availableFEATURE-KERNELIORING_REGISTER_NAPI (busy-poll réseau, CONFIG_NET_RX_BUSY_POLL) rend EINVAL sur les exécuteurs (pas de NAPI exposé au ring) → seul le bras Err(EINVAL) est pris. Le chemin d’erreur est couvert (intégration + simulateur). Le chemin succès n’est atteignable que sur un hôte où NAPI est exposé. Le test prend exactement un bras selon l’environnement.

Écart spec/kernel signalé (pas une exception de couverture). register_clock(ClockSource::Realtime) rend EINVAL : io_uring 6.12/6.17 n’accepte que MONOTONIC/BOOTTIME pour l’horloge des timeouts. Le type ClockSource::Realtime reste exposé (surface validée non modifiée) ; son rejet est testé (register_clock_all_sources) et documenté, jamais corrigé en silence (ADR-032).

io_uring — Temps 3b (buffers fournis ring-mapped)

Sous-module provided.rs : ProvidedBufferRing (register/unregister/status, modes app et kernel_mmap), submit_receive_provided/submit_read_provided (+ bundle), guard RAII ProvidedBuffer (réappro à la libération), mode incrémental (BUF_MORE ⇒ pas de réappro). Couvert en root par les round-trips d’intégration (recv loopback + id/octets, pénurie -ENOBUFS + réappro + resoumission, kernel_mmap, status(), read_provided sur pipe, bundle), des Completion synthétiques déterministes (branches BUF_MORE/réappro/None sans dépendre du timing kernel), un test property-style (256 cycles checkout→return : available ≤ count, id rendu == id demandé, aucun buffer perdu), le simulateur (erreurs register/status/unregister + count non puissance de 2), et un doctest compile_fail (désenregistrement interdit sous un ProvidedBuffer vivant). Lignes 99,6 % / couvrable vide. Résidu :

Branche / LignesCatégorieJustification
Bras Err des ? sur MmapRegion::new_anonymous (anneau app) et mmap_file (anneau kernel) dans register (provided.rs)STRUCTURALUn mmap(MAP_ANONYMOUS) / mmap du fd ring à l’offset IORING_OFF_PBUF_RING d’une petite région valide ne peut échouer que par ENOMEM (ressource, pas un défaut de code) — non provoquable de façon déterministe en CI. Le simulateur de syscalls ne couvre que setup/enter/register (pas mmap). Le chemin succès est couvert (modes app et kernel testés).
Bras défensifs is_some_and(…) == false de checkout/return_buffer (bid ≥ countout_flags.get_mut rend None) (provided.rs)STRUCTURALLe kernel ne sélectionne que des bid < count (id d’un buffer du groupe) ; un bid hors borne est inatteignable via l’API (l’id vient toujours d’un cqe->flags du même groupe). Garde défensive conservée (Principe 5), jamais prise.

io_uring — Temps 3c (opérations liées)

Sous-module linked.rs : LinkedChainBuilder (first/then/then_hard/ with_link_timeout/submit), staging + réservation atomique + publication unique. Lignes de production 100 % (--lib --branch --show-missing-lines vide). Couvert en root par : chaîne de nops (ordre), soft link rompu sur erreur (-ECANCELED), piège du short read rompant une chaîne soft (confirmé sur kernel réel), hard link survivant à l’erreur, link_timeout expirant (maillon annulé) et non expirant (timeout annulé), skip_cqe_on_success (seule la finale arrive), atomicité (slab plein ⇒ EBUSY sans publication, rollback), erreur de staging du LINK_TIMEOUT, with_link_timeout sans maillon (EINVAL), property-style (longueurs 1..6, ordre = soumission), accesseurs ChainTokens (sous Miri). Résidu :

Branche / LignesCatégorieJustification
SubmissionRing::prepare — garde if space_left()==0 { return None } (ring.rs)STRUCTURALPré-existant (déjà tabulé section io_uring Temps 1) : tout appelant pré-vérifie space_left avant prepare (EBUSY), puis dénote par .expect. Inatteignable via l’API ; resurfacé par l’ajout des helpers de staging.

io_uring — Temps 3d (opérations multishot)

Sous-module multishot.rs : submit_accept_multishot[_direct], submit_receive_multishot, submit_read_multishot, submit_poll_multishot, submit_timeout_multishot, cancel_multishot. multishot.rs : lignes 100 % / branches 100 % — ensemble couvrable VIDE. Couvert en root par : accept multishot (flux F_MORE de N connexions + cancel terminal -ECANCELED, slot vivant tant que F_MORE puis libéré), accept multishot direct (connexion dans un slot fixe auto-alloué), recv multishot (flux de buffers fournis), pénurie -ENOBUFS terminante + réappro + resoumission, read multishot (pipe + buffers fournis), poll multishot (événement + cancel), timeout multishot (ticks + cancel), back-pressure EBUSY (SQ pleine), op mono-coup → multishot_token() == None. Le cycle de vie S1 multishot (slot survit aux F_MORE, libéré à la terminale, génération anti-CQE-tardif) est prouvé sous Miri par le test pur du slab (multishot_slot_survives_until_final_then_filters_stale).

Aucune exception propre au sous-module. Le seul résidu de slab.rs (reserve_inner unreachable!) est pré-existant (cf. section io_uring Temps 1, STRUCTURAL).

io_uring — Temps 3e (usage multi-thread)

Sous-module shared.rs : LockedIoUring (verrou, Send + Sync), RingPool (ATTACH_WQ + ring fds enregistrés + routage msg_ring par RingHandle), SqpollIoUring (SETUP_SQPOLL, réveil NEED_WAKEUP auto). Sûreté prouvée à trois niveaux : IoUring: !Sync (doctest compile_fail) + LockedIoUring: Send + Sync (doctest compile-pass + assertion statique) ; loom (mod loom_proof, --cfg loom) sur la discipline de verrou (exclusion mutuelle, aucune mise à jour perdue, aucun accès gardé hors verrou) ; tests d’intégration concurrents réels (4 threads sur LockedIoUring, thread-per-core, routage inter-ring msg_ring, SQPOLL réveil + rafale éveillée + propagation d’erreur enter). IoUring reçoit son unsafe impl Send (ADR-022 D6, justifié // SAFETY:). Les deux bras d’erreur d’io_uring_enter de SqpollIoUring (réveil SQ_WAKEUP dans submit, attente GETEVENTS dans submit_and_wait) sont couverts par le simulateur de syscalls : submit/submit_and_wait empruntent la même couture instrumentable syscall::enter que les autres syscalls io_uring, sur laquelle on injecte -EINTR ; l’errno transite par le même errno_from_negative_syscall_ret que le vrai kernel et remonte tel quel (ADR-021 conv. 2). L’ancienne exception EFAULT-SAFE posée sur ces bras est donc retirée. shared.rs lignes 95,6 %, branches 100 %. Résidus :

Branche / LignesCatégorieJustification
Bras Err(e) => { assert!(matches!(e, EPERM)); return } des 5 tests SqpollIoUring (fallback si SETUP_SQPOLL refusé)PRIVILEGESQPOLL est autorisé sans privilège sur les exécuteurs (kernel ≥ 6.17, depuis 5.13) → seul le bras Ok est pris ; le bras Err (EPERM) relève d’un hôte/cgroup restreignant SQPOLL. Prend exactement un bras selon l’environnement.

io_uring — Temps 3f (confinement / sandbox)

Sous-module sandbox.rs : RestrictionSet (liste blanche default-deny), RegisterOp, SqeFlagSet ; flux restrict → enable (S3) câblé dans mod.rs::apply_restrictions/encode_restriction. sandbox.rs 100 % lignes + branches (aucune branche ; matchs exhaustifs couverts). La preuve de sécurité (openat2 refusé sur un ring « réseau seul » alors que le process a le droit d’ouvrir le fichier) est verte, ainsi que : opcode autorisé qui passe, default-deny, require_sqe_flags imposé, soumission avant enable refusée (EBADFD), immuabilité après enable (refus kernel), propagation d’erreur REGISTER_RESTRICTIONS (simulateur), property-based (tout sous-ensemble encode exactement ses opcodes). Résidu unique :

Branche / LignesCatégorieJustification
Bras Err du u32::try_from(encoded.len()).map_err(EINVAL)? dans apply_restrictions (mod.rs)STRUCTURALConversion défensive usize → u32 du nombre de restrictions (Principe 2, pas d’as nu). Une liste blanche ne peut pas approcher 2³² entrées (chaque io_uring_restriction fait 16 o ⇒ 64 Gio) : le bras d’erreur est inatteignable par construction. Le chemin succès et le bras if ret < 0 (échec REGISTER_RESTRICTIONS) sont couverts (simulateur).

io_uring — Temps 4 (accès brut niveau 1)

Sous-module raw.rs (surface publique : RawSubmissionQueueEntry/ RawCompletionQueueEntry/RawOpcode/RAW_USER_DATA_TAG + accesseurs ; méthodes raw_get_submission_queue_entry/raw_peek_completion_queue_entry/ raw_advance_completion_queue + capacités, dans mod.rs). Couverture : 100 % lignes + branches sur le code de production / couvrable vide — à un unique artefact d’instrumentation près (ligne ci-dessous). Les chemins unsafe sont exercés par des tests d’intégration kernel (NOP brut → raw_peek/raw_advance ; op brute vs wrapper niveau 2 ; coexistence slab + brut sans collision ; SQ pleine → None ; raw_advance borné ; moisson gérée qui s’efface devant une brute en tête). La règle du tag bit 63 repose sur le slab dont la génération est désormais bornée à 31 bits (GENERATION_MASK, slab.rs) — le bit 63 du user_data géré est donc toujours nul (asserté par les tests de coexistence). Le debug_assert! du tag manquant est couvert par un test #[should_panic]. Le décodage brut de données kernel externes est fuzzé (fuzz_api::decode_raw_cqe, cible io_uring_4_raw_cqe, sous #[cfg(fuzzing)]).

Branche / LignesCatégorieJustification
debug_assert_raw_tags} fermant if cfg!(debug_assertions) (mod.rs, l.1013)STRUCTURALArtefact d’instrumentation LLVM : ce } porte une région à count == 0 alors que le corps de la boucle est prouvé exécuté (mesure : 308 itérations via les tests raw_nop_*). Région dégénérée du if cfg!-constant, analogue aux ombres [True:0, False:0] documentées en tête de registre — non couvrable par aucun test (la purge raw_pending.clear() qui suit, l.1014, est couverte).

Comme tout le module, l’intégration io_uring n’est pas modélisable par Miri (#[cfg_attr(miri, ignore)]) ; Miri valide les chemins purs (layout, accesseurs, slab/génération). Les seuls résidus de branches de raw.rs (≈ 80 % sur le fichier) sont des bras d’assert!/should_panic de tests, jamais du code de production (qui n’a aucune branche dans raw.rs).


device — famille périphériques

Le décodage pur (uevent, input_event) est couvert à 100 % (lignes + branches), incl. property-based. Les chemins de succès des ioctls EVIOC* et de recvmsg (réception d’un vrai uevent) sont couverts par le test d’intégration uinput (uinput_evdev_and_uevent_round_trip) exécuté en root sur les exécuteurs. Résidus :

Branche / LignesCatégorieJustification
uevent_socket_open — branche d’erreur if ret < 0 de socket (l.187)STRUCTURALsocket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT) ne requiert aucun privilège et n’échoue qu’en épuisement de ressources (EMFILE/ENOMEM) non provoquable de façon déterministe en test. Le chemin succès est couvert ; le helper errno_from_negative_syscall_ret est testé séparément.
uevent_socket_open — branche d’erreur if ret < 0 de setsockopt(SO_PASSCRED) (l.211)STRUCTURALsetsockopt(SOL_SOCKET, SO_PASSCRED, 1) sur un socket netlink valide ne peut échouer (option universellement supportée) ; branche d’erreur non provoquable. Chemin succès couvert par uevent_open_userspace_sets_passcred.
uevent_socket_open — branche d’erreur if ret < 0 de bind (l.230)STRUCTURALbind sur un sockaddr_nl bien formé (groupes KERNEL/USERSPACE) réussit ; l’échec exigerait un nl_groups réservé/privilégié non émis par l’API. Chemin succès couvert.
read_u32_native / credential_uid — bras None des try_into().ok()? sur des tranches déjà bornéesSTRUCTURALLa tranche passée à try_into::<[u8;4]> provient d’un get(off..off+4) réussi : la longueur est exactement 4 par construction, le Err de try_into est inatteignable. Défense en profondeur (Principe 5).

ebpf — famille eBPF / perf

air-sys-types::ebpf couvert à 100 % (lignes + branches), incl. proptest (round-trip des miroirs BpfInstruction/PerfEventAttr, repli Other). air-sys-syscall::ebpf couvert à 100 % hors exceptions ci-dessous : un harnais d’intégration privilégié réel (exécuté en root sur speedy) exerce les chemins de succès — création/élément/batch/gel de cartes, chargement de programme (valide + invalide → log du vérifieur, + champs optionnels fentry/freplace), test_run, programme compteur assemblé à la main avec BPF_PSEUDO_MAP_FD attaché via bpf_raw_tracepoint_open (compteur vérifié), attache/détache cgroup v2 + bpf_link_create/update/detach + prog_query, épinglage bpffs pin/get, btf_load + carte typée par BTF, introspection *_get_next_id/*_get_fd_by_id (prog/map/btf/link), perf_event_open (software/tracepoint/matériel + portées ProcessOnCpu/AllProcessesOnCpu/Cgroup), tous les ioctls perf dont le pont perf_event_set_bpf_program et query_bpf. Fuzzing : cible fuzz_bpf_obj_info (décode borné des champs réécrits par le kernel dans bpf_attr).

Les fausses exceptions « PRIVILEGE / dette de test » de la première version ont été retirées : les chemins qu’elles couvraient sont désormais exercés par le harnais. Ne restent ci-dessous que des exceptions structurelles réelles.

Branche / LignesCatégorieJustification
Attr::put_u32/put_u64/put_name et perf_event_query_bpf — bras None des if let Some(slot)/(Some, Some) (l.171, 178, 196, 1203) ; Attr::read_u32 bras None => 0 (l.187)STRUCTURALDéfense en profondeur (Principe 5) : tous les offsets utilisés en production sont des constantes ≤ 188 dans un tampon de 192 octets ; get_mut/get ne retournent jamais None. Inatteignable par construction (mais conservé : un futur offset erroné est borné, jamais d’OOB). Le bras None de read_u32 est prouvé sûr par le fuzz fuzz_bpf_obj_info (offset hors borne → 0).
*_get_next_id (prog/map/btf/link) — bras return Err(err) du cas non-ENOENT (l.766-767, branche if err == ENOENT côté faux l.764)STRUCTURAL*_GET_NEXT_ID ne peut retourner que ENOENT (fin d’itération, → Ok(None), couvert) ou, uniquement en non-privilégié, EPERM. Sur l’exécuteur root, seul ENOENT survient ; le bras d’erreur générique n’est atteignable que sans CAP_BPF (couvert sur l’arche non-root raspi). Conservé par robustesse.

poll — famille poll

Branche / LignesCatégorieJustification
ppoll — bras d’erreur if ret < 0 => return Err(errno_from_negative_syscall_ret(ret)) (poll.rs, l.107)STRUCTURALppoll(2) ne retourne < 0 que sur EINTR (signal pendant l’attente — non injectable de façon déterministe en CI, même précédent que waitid), EFAULT (pointeur invalide — exclu : la slice PollFd et le mot sigset proviennent de références vivantes) ou EINVAL (timespec invalide — exclu par la conversion défensive interne : tv_nsec < 1e9, tv_sec ≥ 0 clampé). Un fd invalide n’erre pas : le kernel pose POLLNVAL dans revents et compte l’entrée. Les chemins succès (fd prêt / POLLIN), expiration (Ok(0)), sondage (Duration::ZERO), multi-fd, masque non-null et timeout NULL sont couverts ; le décodage d’errno l’est via errno_from_negative_syscall_ret_maps_eintr.

futex — famille futex (ADR-048, re-sceau couche-0-v1.6)

Branche / LignesCatégorieJustification
futex_wake — bras d’erreur if ret < 0 => return Err(errno_from_negative_syscall_ret(ret)) (futex.rs, l.168)STRUCTURALFUTEX_WAKE sur un &AtomicU32 valide ne peut échouer que par EFAULT (pointeur invalide — exclu par construction : word est une référence vivante) : FUTEX_WAKE n’utilise que l’adresse comme clé de file, sans lire/écrire le contenu du mot, et ne prend aucun timeout/uaddr2/val3. Le bras d’erreur est donc inatteignable via l’API safe. Le chemin succès (compte de réveils, dont 0 sans waiter) est couvert ; le décodage d’errno l’est via errno_from_negative_syscall_ret_maps_known. (futex_wait n’a PAS d’exception : son bras ret < 0 est couvert par EAGAIN (valeur divergente) et ETIMEDOUT (échéance courte) — déterministes. EINTR partagerait ce bras mais n’est pas injectable sans trampoline de restorer de signal x86_64 ; il est documenté ici sans test dédié, même précédent que ppoll.)
futex_wake — bras Err du u32::try_from(ret).map_err(|_| Errno::EINVAL) (futex.rs, l.175)STRUCTURALConversion défensive i64 → u32 (Principe 2, pas d’as nu) : ret ≥ 0 est le nombre de waiters réveillés, toujours ≤ INT_MAX (FutexWakeCount borne le val à INT_MAX par construction) → le bras d’erreur est inatteignable. Le chemin succès est couvert (test multi-thread futex_wake_releases_one_waiter).

arch — famille architecture (ADR-051, re-sceau couche-0-v1.7)

Branche / LignesCatégorieJustification
get_fs — bras d’erreur if ret < 0 { return Err(…) } (arch.rs, l.110)STRUCTURALarch_prctl(ARCH_GET_FS, &mut base) ne peut échouer que par EFAULT (pointeur invalide — exclu : base est une variable de pile vivante et alignée) sur un kernel x86_64 conforme. Le bras d’erreur est inatteignable via l’API safe. Le chemin succès est couvert (get_fs_returns_nonzero_base) ; le décodage d’errno (errno_from_negative_syscall_ret) l’est par le bras d’erreur atteignable de set_fs (set_fs_non_canonical_is_rejected_without_changing_fs, adresse non canonique → EINVAL/EPERM). (arch_prctl est x86_64-only ; sur aarch64 le module arch est vide — aucune ligne à couvrir.)

terminal — famille terminal (ADR-060, termios/tty)

Le groupe Session / job-control (tranche 5) exige, pour son chemin nominal, un terminal de contrôle — que le kernel n’accorde qu’à un leader de session. Le cycle complet est donc validé dans un enfant forké setsid (test session_job_control_full_cycle_in_forked_child), qui encode chaque étape dans son code de sortie (0 = tout le cycle a réussi). Toutes les branches d’erreur (if result < 0 => Err) sont couvertes dans le parent sur /dev/null (ENOTTY) ; le bras Ok(None) de tcgetpgrp est lui aussi couvert dans le parent (un maître /dev/ptmx neuf n’a aucun groupe de premier plan → TIOCGPGRP rend 0Ok(None)) — donc aucune exception pour tcgetpgrp. Résidu :

Branche / LignesCatégorieJustification
Bras succès (Ok(…) + if result < 0 côté faux) de tcsetpgrp (l.558), session_id (l.586), set_ctty (l.611), clear_ctty (l.628) — 4 lignes + 4 branchesCHILD-EXITChemin nominal exécuté uniquement dans l’enfant forké setsid (le kernel refuse ces ioctls hors terminal de contrôle → ENOTTY côté parent, branche Err couverte). Validé par le code de sortie de l’enfant (fork_run, test session_job_control_full_cycle_in_forked_child) : setsidset_cttysession_id == SIDtcsetpgrptcgetpgrp == Some(SID)clear_ctty. Le profil LLVM de l’enfant n’est pas flushé (async-signal-safety — § Note cohabitation toolchain). Couverture réactivable avec une toolchain Air-compatible.

air-runtime — couche 1, TARGET-ONLY (ADR-035 § amendement 2026-06-29)

Le runtime userland air-runtime (objet couche 1, ADR-052 / ADR-049) est mesuré depuis l’étape 6 phase B (retrait de --exclude air-runtime). Ses 4 fichiers TARGET-ONLYthread_control_block.rs, thread_local_storage.rs, thread.rs, start.rs — sont retirés par fichier (--ignore-filename-regex, précédent _capnp.rs) car sound uniquement sur *-linux-air (programmation du registre TLS, self-pointer du TCB, spawn CLONE_SETTLS, bootstrap crt0). Les modules host-testables (args.rs, env.rs, lib.rs façade AirRuntime, parseur de reloc.rs, machinerie d’fork.rs) sont mesurés à 100 %.

Restent, dans reloc.rs et fork.rs (fichiers mesurés, NON ignorés), quelques lignes isolées TARGET-ONLY : orchestrations qui parcourent l’auxv/pile réel·le du kernel ou écrivent en mémoire de l’image — non exerçables sur l’hôte du gate (*-linux-gnu) sans corrompre la GOT/le TLS (unsoundness #151). Toute la logique de parsing/arithmétique en amont est extraite en helpers couverts à 100 % sur images ELF synthétiques ; ne restent target-only que les corps d’orchestration ci-dessous. Preuve on-target : le selftest rt/crates/airrt-selftest (exit 42, 2 arches — carbon x86_64 + raspi aarch64). Hors décompte du récapitulatif production couche 0.

Branche / LignesCatégorieJustification
apply_relative_relocationsreloc.rs corps (l.415-427)TARGET-ONLYL’écriture de relocation (target as *mut usize).write(value) ne peut pas s’exécuter sur l’hôte : le binaire de test est lié à glibc et déjà relocalisé par ld.so → ré-écrire la GOT/.data.rel.ro corromprait le processus. Les couples (cible, valeur) rendus par RelativeRelocations sont couverts à 100 % sur image synthétique ; seule l’écriture est target-only (prouvée on-target, selftest rt/).
relocate_selfreloc.rs corps (l.438-473)TARGET-ONLYOrchestration static-PIE : parcourt l’auxv/pile réel·le du kernel (AT_PHDR/PT_DYNAMIC/.rela.dyn) puis applique les relocations. L’appeler sur l’hôte corromprait la GOT (cf. ci-dessus) ; le module de test ne l’exécute jamais. Tous ses helpers (auxiliary_vector_address, read_program_header_table, scan_program_headers, read_relocation_table, RelativeRelocations) sont couverts à 100 %. Prouvée on-target (selftest rt/, exit 42).
protect_relroreloc.rs corps (l.581-617)TARGET-ONLYDurcissement RELRO : parcourt l’auxv réel (PT_GNU_RELRO, AT_PAGESZ) puis appelle air_memory::raw_mapping::protect_range (mprotect) sur la plage chargée — non exerçable sur l’hôte (reprotéger des pages de l’image de test = SIGSEGV). Le calcul de plage (scan_relro_segment/read_page_size) est couvert à 100 % ; seul l’appel mprotect final est target-only. Prouvée on-target (selftest rt/).
run_childfork.rs corps (l.194-201)TARGET-ONLYHook enfant post-fork/clone3 : invoque les hooks child (testés via invoke_child_handlers) puis enchaîne sur reset_after_fork_in_child (reset bas niveau TARGET-ONLY ci-dessous). N’a de sens que dans l’enfant mono-thread async-signal-safe sur *-linux-air ; l’exécuter sur le gate mêlerait les TLS Air/glibc (#151). Prouvée on-target (selftest rt/).
reset_after_fork_in_childfork.rs corps (l.221-263)TARGET-ONLYReset post-fork : relit le tid de l’enfant (gettid, host-testable en isolation) et l’écrit dans le TCB au registre TLS via ThreadControlBlock::refresh_current_tid — l’écriture du TCB est unsound sur l’hôte du gate (registre %fs programmé par glibc, #151) ; appelle aussi air_alloc::reset_after_fork() (ADR-056 D8 — remet le verrou de l’arène globale d’aplomb dans l’enfant ; le mécanisme de reset est host-testé à 100 % côté air-alloc, seul son invocation dans ce chemin enfant est target-only). N’a de sens que dans l’enfant sur *-linux-air. Prouvée on-target (selftest rt/).

Annexe — résidus internes aux tests (NON suivis comme exceptions production)

Conformément à ADR-035, les bras de code de test (court-circuit d’assertion, garde de harnais) ne sont pas des exceptions de couverture production — ils sont listés ici pour mémoire seulement et exclus du décompte. Le code de production correspondant est couvert (100 % lignes pour linked.rs/shared.rs).

Résidu (test)Pourquoi non couvert
4 sous-branches de linked.rs::tests (court-circuit d’assertions a == -62 || a >= 0, matches! du résultat LINK_TIMEOUT)Bras d’assertion de test ; la production de linked.rs est 100 % lignes, toutes ses branches métier exercées.
Retour false (timeout) du thread pair de ring_pool_routes_msg_ring_between_peers (shared.rs::tests)Garde-fou de harnais (boucle bornée 2000×1 ms) ; le msg_ring arrive bien avant → seul true est pris.

Récapitulatif par catégorie (production)

Décompte recompté à partir des lignes réelles du registre (les figures antérieures avaient dérivé — exactement le travers signalé par l’audit 053). Décompte désormais outillé et vérifié en CI par cargo xtask audit-exceptions, qui recompte ce tableau à partir des rangées et échoue sur toute divergence (c’est ce gate qui a corrigé la dérive CHILD-EXIT 9 → 8 antérieure). Décompte CHILD-EXIT actuel : 11 rangées CHILD-EXIT + 1 rangée combinée CHILD-EXIT / PRIVILEGE = 12 (les 3 rangées « Cohabitation toolchain » — process, signal, security — viennent du retrait du flush enfant, async-signal-safety ; la rangée terminal — session / job-control ADR-060 tranche 5 — valide son cycle nominal dans un enfant forké setsid).

Catégorie (ADR-035)Nombre d’entrées
STRUCTURAL (inclut ex-EFAULT-SAFE + ex-VALUE-UNREACHABLE permanente)47
FEATURE-KERNEL13
PRIVILEGE10
CHILD-EXIT12
DEFERRED-TOOLING0
Total (production)81

(Hors décompte : 2 pseudo-lignes — (COUVERT) — déjà couvertes, conservées pour mémoire — et l’annexe « résidus internes aux tests » ci-dessus. La catégorie combinée CHILD-EXIT / PRIVILEGE est comptée sous CHILD-EXIT.)

Rappel méthodologie : la métrique de branches se mesure cargo +nightly llvm-cov --workspace --lib --branch (artefact-free) ; voir la section « artefact [True:0, False:0] » en tête de fichier. « 100 % hors exceptions / couvrable VIDE » = chaque ligne/branche atteignable rouge ⊆ une entrée ci-dessus, vérifié par réconciliation ligne-à-ligne sur les 2 arches (x86_64 speedy, aarch64 raspi). DEFERRED-TOOLING est vide : aucune branche atteignable n’est exemptée (les 3 ex-candidates sont couvertes par de vrais tests).


Code GÉNÉRÉ exclu de la mesure (capnpc, ADR-040)

Les fichiers Rust générés par le compilateur de schéma Cap’n Proto (*_capnp.rs, crate air-config-schema) sont committés (politique « code généré committé », ADR-040 addendum) mais exclus de la mesure de couverture — au même titre que xtask (--exclude xtask). Raisons :

  • C’est de la sortie d’outil, pas du code Air écrit à la main (accesseurs/setters générés en masse, majoritairement non appelés) ; sa correction relève du codegen capnpc (version pinnée), pas de tests Air.
  • Le code généré réellement utilisé est exercé indirectement par les tests de binding d’air-config-compile (round-trip source→artefact→relecture).

Mécanisme : cargo llvm-cov … --ignore-filename-regex '_capnp\.rs$' dans la barrière (cargo xtask barrier) et la CI (.github/workflows/ci.yml), lignes et branches. Exclusion globale (pas une exception ligne-à-ligne).

Famille signal (libc) — pause : attente bloquante non injectable (BLOCKING)

AirSignalManager::pause (air-signal) et pause_impl (air-libc-signal::suspend) suspendent le thread jusqu’à un signal capté (ppoll(0 fd, attente infinie, masque courant)), puis rendent -1/EINTR. Le chemin ne rend jamais sans qu’un signal soit délivré à un thread muni d’un handler : l’exercer en test exigerait un harnais de délivrance de signal, or air-poll documente déjà cet EINTR comme non injectable (cf. crates/air-poll/src/lib.rs : « EINTR — non injectable »). Appeler pause() en test bloquerait indéfiniment le harnais.

  • Correct par construction : composition mince sur ppoll (couche 0, testée) ; aucune logique propre hormis le mapping d’erreur.
  • Aucun additif couche 0 : pause réutilise ppoll existant.

Absorbé par le plancher de couverture workspace (cargo llvm-cov … --fail-under-lines 96) : ~8 lignes non couvertes, négligeables. Catégorie BLOCKING (analogue au récap CHILD-EXIT : chemin réel non exerçable en process).


Licence : MPL 2.0 Dernière mise à jour : 2026-07-02 (famille terminal — session / job-control, ADR-060 tranche 5 : 1 rangée CHILD-EXIT (tcsetpgrp/session_id/set_ctty/clear_ctty, cycle nominal validé en enfant forké setsid) ; récap CHILD-EXIT 11 → 12, total 80 → 81). Antérieur — 2026-06-30 (étape 6 phase B : air-runtime (couche 1) bascule en mesure per-fichier — section TARGET-ONLY air-runtime ajoutée (5 entrées : reloc.rs ×3, fork.rs ×2), catégorie TARGET-ONLY gravée (ADR-035 § amendement), hors décompte production couche 0). Antérieur — 2026-06-28 : ajout famille futex (ADR-048, re-sceau couche-0-v1.6) — 2 entrées STRUCTURAL futex_wake ; récap 73 → 75). Antérieur — 2026-06-25 : code généré *_capnp.rs exclu de la couverture, ADR-040). Antérieur — 055 : taxonomie ADR-035 appliquée (EFAULT-SAFE/VALUE-UNREACHABLE→STRUCTURAL, TEST-HARNESS→annexe, 3 candidates couvertes ; récap recompté).