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

ADR-035 — Taxonomie des exceptions de couverture

Statut : Accepté (2026-06-14). Companion d’ADR-031 (mesure de couverture en root) et de la politique de couverture couche 0 (Principe 1) ; n’amende ni l’un ni l’autre, il en grave la taxonomie d’exceptions.

Catégorie : Méthode (gouvernance technique / CI).

Contexte

La couche 0 vise 100 % de couverture (lignes + branches) hors exceptions documentées (Principe 1). Le registre docs/COVERAGE-EXCEPTIONS.md énumère chaque ligne/branche de production non couverte et la justifie. Au fil de l’implémentation, six catégories d’exception sont apparues : trois « canoniques » (STRUCTURAL, PRIVILEGE, FEATURE-KERNEL) et trois introduites en cours de route (EFAULT-SAFE, CHILD-EXIT, TEST-HARNESS), plus une étiquette VALUE-UNREACHABLE. L’audit 053 a montré que cette prolifération brouillait l’invariant « couvrable VIDE » : certaines entrées « inatteignables » étaient en réalité atteignables-mais-non-outillées (donc à couvrir, pas à exempter — cf. le cas Sqpoll réglé au 046).

Décision

1. Quatre catégories d’exception, plus une étiquette de dette

Canoniques — branche/ligne qu’aucun test légitime ne peut atteindre :

  • STRUCTURALinatteignable par construction : un invariant garantit que le bras n’est jamais pris (longueur ≤ borne, bid < count, free-list, sémantique kernel per-process, joker #[non_exhaustive] dont toutes les variantes réelles sont mappées, garde défensive permanente du Principe 5…). Absorbe les anciennes étiquettes :
    • EFAULT-SAFE (bras EFAULT/EBADF exclu car buffers/sorties valides par construction) — cas particulier de STRUCTURAL ; le mot-clé reste autorisé dans la justification, pas comme catégorie ;
    • VALUE-UNREACHABLE permanente (joker exhaustif, garde défensive) — idem.
  • PRIVILEGE — exige une capability / condition de privilège absente sur le runner (CAP_*, RLIMIT_*, Yama ptrace_scope, cpuset cgroup…). Le bras opposé est mort dans cet environnement. (Mesure root : cf. ADR-031.)
  • FEATURE-KERNEL — une feature kernel est présente sur les exécuteurs (≥ 6.12) ⇒ le bras de repli (kernel ancien / feature absente) est mort.

Limite d’instrumentation (le code s’exécute mais n’est pas mesurable) :

  • CHILD-EXIT — code réellement exécuté dans un processus enfant forké dont le profil LLVM ne peut pas être écrit : l’enfant a largué ses privilèges (écriture .profraw refusée à un non-root), s’est confiné (seccomp-strict tue / Landlock refuse l’O_RDWR), ou s’est abort()é (bypass de l’atexit). Modèle fork+flush d’ADR-031. Preuve d’exécution : le code de sortie observé par le parent via waitid. Distincte de STRUCTURAL (ce n’est pas inatteignable) et de PRIVILEGE (le privilège n’est pas le frein — l’instrumentation l’est).

Dette outillée (à résorber, jamais permanente) :

  • DEFERRED-TOOLING — branche réellement atteignable mais non encore couverte faute d’outillage (un primitif manquant, un harnais à écrire). À couvrir, jamais exemption permanente. Chaque entrée nomme l’outillage requis et la condition de levée. Une exception « atteignable-mais-dure » doit être ici (jamais déguisée en STRUCTURAL).

2. Le code de test n’est pas une exception production

Les bras de code de test (court-circuit d’assert!, matches! de validation, gardes de harnais) ne figurent pas au registre production. S’ils nuisent à la lisibilité d’une mesure, ils sont consignés en annexe « résidus internes aux tests », exclus du décompte. (L’ancienne étiquette TEST-HARNESS est retirée du registre.)

3. Invariant « couvrable VIDE »

L’ensemble « couvrable » = les branches atteignables non couvertes hors exception légitime. Il doit rester VIDE : toute branche atteignable est soit couverte par un test, soit en DEFERRED-TOOLING avec un plan de levée. Une entrée STRUCTURAL/PRIVILEGE/FEATURE-KERNEL/CHILD-EXIT atteste, elle, d’une impossibilité (de construction, de privilège, de feature, d’instrumentation), pas d’une dette.

Conséquences

  • Registre simplifié : 4 catégories + 1 étiquette de dette, légende et récap alignés. EFAULT-SAFE/VALUE-UNREACHABLE repliées dans STRUCTURAL ; TEST-HARNESS en annexe.
  • Discipline anti-complaisance : une branche atteignable ne peut plus être « rangée » en STRUCTURAL ; elle va en DEFERRED-TOOLING (visible, à couvrir) ou est couverte. Application immédiate au 055 : les 3 candidates (fs::try_lock contention, net::get_so_error async, process::pidfd_send_signal Some) sont couvertes par de vrais tests ⇒ DEFERRED-TOOLING vide.
  • Pas d’amendement à ADR-031 (mesure root) ni au Principe 1 (objectif 100 %) : ADR-035 ne fait que nommer les exceptions légitimes et bannir les fausses.
  • Revue humaine du registre inchangée ; le plancher CI (--fail-under-lines 96) reste l’anti-régression brut.

Alternatives écartées

  • Garder les 6 catégories : prolifération sans valeur ; EFAULT-SAFE et VALUE-UNREACHABLE permanente sont indiscernables de STRUCTURAL à l’usage.
  • Supprimer toute catégorie non canonique : perdrait CHILD-EXIT (limite d’instrumentation réelle, bien distincte) et DEFERRED-TOOLING (le filet anti-complaisance qui force la couverture des atteignables).
  • Exempter les branches atteignables-mais-dures en STRUCTURAL : exactement le travers que cet ADR interdit (cf. Sqpoll/046).

Références

  • ADR-031 (mesure de couverture en root — modèle fork+flush dont découle CHILD-EXIT).
  • Principe 1 (couverture 100 % couches 0/1), docs/COVERAGE-EXCEPTIONS.md.
  • Audit 053 (~/Code/air-relay/053-consolidation-flaky-exceptions.out) et son application 055.

Amendement — catégorie TARGET-ONLY (2026-06-29, RFC ADR-015)

Statut : Proposé (à ratifier BDFL). Companion d’ADR-052 (runtime air-runtime couche 1) et d’ADR-049. Étend la taxonomie d’une cinquième catégorie ; n’amende ni ADR-031 ni le Principe 1.

Contexte

Le runtime userland (air-runtime, ADR-052) contient du code dont la sémantique n’est correcte que sur la cible *-linux-air : programmer le registre TLS (set_fs/tpidr_el0), créer un thread avec CLONE_SETTLS pointant un bloc TLS d’Air, appliquer les relocations static-PIE, lire le self-pointer du TCB. Exécuter ces lignes sous le gate de couverture — qui tourne sur *-linux-gnu (glibc) — est unsound : mêler le TLS d’Air et celui de glibc corrompt l’état par-thread (c’est l’unsoundness #151, la raison d’être même du runtime). Ce code ne peut donc pas tourner du tout sous le gate, et encore moins être mesuré.

Ce n’est ni STRUCTURAL (le code est atteignable et s’exécute — sur la bonne cible), ni CHILD-EXIT (là le code tourne dans le run du gate mais le profil n’est pas écrit ; ici il ne tourne pas dans le run du gate tout court), ni DEFERRED-TOOLING (ce n’est pas une dette d’outillage à résorber par un test hôte : aucun test hôte ne pourra jamais l’exécuter sainement).

Décision — TARGET-ONLY (limite d’environnement de gate)

  • TARGET-ONLY — code sound uniquement sur *-linux-air, donc ni exécuté ni mesuré par le gate de couverture (*-linux-gnu). Preuve de correction : un selftest dédié sur *-linux-air (binaire rt/crates/airrt-selftest), validé par code de sortie sur les 2 arches (x86_64 sur carbon, aarch64 sur raspi/CI ARM) — même esprit de preuve-par-exit-code que CHILD-EXIT, mais le frein est l’environnement de cible du gate, pas l’instrumentation.

  • Granularité = fichier. Le code TARGET-ONLY est isolé dans des fichiers dédiés, retirés de la mesure par --ignore-filename-regex (précédent : le code généré _capnp.rs), jamais par une exception ligne-à-ligne. Où une ligne TARGET-ONLY isolée vit dans un fichier par ailleurs mesuré (p. ex. l’applier de relocation), elle est extraite dans un fichier dédié pour rester ignorable par fichier. Fichiers concernés (à date) : air-runtime/src/{thread_control_block, thread_local_storage,thread,start}.rs, le module de primitives target-only d’air-thread, l’applier de reloc.

  • Le reste de la crate reste mesuré à 100 %. Retirer --exclude air-runtime (exclusion en bloc du squelette) au profit de l’--ignore-filename-regex ciblé : les modules host-testables (env/args/parser de reloc/machinerie fork) retombent sous le 100 % lignes+branches.

  • Discipline anti-complaisance. TARGET-ONLY n’est pas un refuge pour du code « dur à tester » (cela reste DEFERRED-TOOLING) : il est réservé au code structurellement insondable sur la cible du gate (unsoundness TLS Air/glibc). Chaque fichier TARGET-ONLY : commentaire in-code justifiant, nom du selftest qui le prouve, et condition de levéeréversible : la couverture sera réactivée le jour où une toolchain Air-native mesurera cargo llvm-cov directement sur *-linux-air (cf. note « cohabitation toolchain », PR #161).

Invariant « couvrable VIDE » (inchangé)

Une entrée TARGET-ONLY atteste une impossibilité (cible du gate unsound), au même titre que STRUCTURAL/PRIVILEGE/FEATURE-KERNEL/CHILD-EXIT — pas une dette. L’ensemble « couvrable » reste VIDE. Récap : 5 catégories (STRUCTURAL, PRIVILEGE, FEATURE-KERNEL, CHILD-EXIT, TARGET-ONLY)

  • 1 étiquette de dette (DEFERRED-TOOLING).

Alternatives écartées

  • Garder --exclude air-runtime en bloc : perd la mesure des modules host-testables de la crate (env/args/reloc-parser/fork) → moins rigoureux ; masque la frontière host/target.
  • Laisser le mécanisme dans rt/ (hors-arbre, déjà exclu) : contredit ADR-052 D4 (la logique de runtime est couche 1) ; le code couche 1 ne serait jamais ni porté ni mesuré.
  • Exception ligne-à-ligne plutôt que fichier : fragile et bruité ; la granularité fichier (code target-only isolé) est nette et auditable (modèle _capnp.rs).

Licence du document : MPL 2.0.