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-031 — Mesure de couverture en root sur les runners self-hosted

Statut : Accepté (2026-06-11). Companion d’ADR-014 (cibles tier-1) et de la politique de couverture couche 0 (Principe 1) ; n’amende pas ces documents, en précise la mise en œuvre CI.

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

Contexte

La couche 0 vise 100 % de couverture (lignes + branches) hors exceptions documentées (Principe 1). La CI applique un plancher anti-régression sur la couverture brute (cargo llvm-cov --workspace --fail-under-lines 96, cf. docs/CI.md), l’invariant « 100 % hors exceptions » restant revu humainement via docs/COVERAGE-EXCEPTIONS.md.

Jusqu’ici, le job test-coverage s’exécutait en tant que l’utilisateur du runner self-hosted (non-root). Les huit premières familles ont une surface majoritairement non privilégiée : leurs quelques chemins exigeant une capability Linux étaient inscrits en exceptions PRIVILEGE, et le plancher 96 % absorbait ces lignes rouges.

La famille ebpf rompt cet équilibre. Le syscall bpf() exige CAP_BPF/CAP_PERFMON pour quasiment toute opération (création de carte, chargement de programme, attache, introspection…). En non-root, seuls les chemins d’erreur sont atteignables → plafond intrinsèque de couverture ~74 % pour air-sys-syscall::ebpf. Mesuré en non-root, le total workspace tombe à 95,5 %, sous le plancher de 96 % → la CI échoue, alors que la famille est réellement couverte à 99,4 % quand le harnais d’intégration privilégié s’exécute (mesure root sur speedy).

Documenter les ~177 lignes privilégiées en exceptions ne résout pas le problème : le plancher est numérique, et la politique couche 0 (cf. prompt de finalisation ebpf) refuse les fausses exceptions « privilège » pour des chemins réellement atteignables en root sur les exécuteurs. Toute la documentation de mesure (*.out, JOURNAL.md) mesure d’ailleurs déjà « en root ».

Décision

Le job test-coverage mesure la couverture EN ROOT sur les runners self-hosted (air-x86_64 = speedy, air-aarch64 = raspi-srv-2), via sudo -n (non-interactif), de sorte que les tests d’intégration privilégiés (bpf(), uinput, perf, cgroup, bpffs) s’exécutent réellement sous instrumentation.

  • Les étapes cargo llvm-cov clean / --fail-under-lines / --branch sont préfixées de sudo -n -E env "PATH=$PATH" (préserve HOME/CARGO_HOME et le PATH cargo). sudo -n échoue franchement (pas de prompt) si le runner n’a pas NOPASSWD → signal clair.
  • Une étape finale if: always() rétablit la propriété de target/ à l’utilisateur du runner (sudo chown -R), afin que le git clean (non-root) du run suivant ne bute pas sur des fichiers root.
  • Prérequis infra : l’utilisateur des deux runners doit disposer du sudo sans mot de passe (NOPASSWD). C’est une configuration des machines (hors dépôt).

Le plancher reste 96 % lignes / 78 % branches : avec la mesure root, les familles privilégiées atteignent leur couverture réelle, et le plancher conserve sa marge anti-régression. Les exceptions légitimes restantes sont structurelles (gardes de borne défensives, branches EFAULT/per-process inatteignables), pas « privilège ».

Conséquences

Positives.

  • La CI valide la couverture réelle des familles privilégiées (ebpf à 99,4 %), au lieu d’un plafond non-root artificiel.
  • Plus de fausses exceptions « PRIVILEGE » pour masquer des chemins atteignables en root : la politique couche 0 (Principe 1) est tenue mécaniquement.
  • Alignement CI ↔ méthode de mesure déjà employée partout (« en root »).

Coûts / risques.

  • Modèle de confiance. Exécuter le build/tests en root sur les runners signifie qu’un code de PR s’exécute avec privilège. Acceptable tant que le dépôt est privé et mono-mainteneur (même posture que la dette « protections de branche inactives », cf. JOURNAL) : aucune PR externe n’atteint les runners. À revisiter impérativement avant l’ouverture publique ou l’arrivée de contributeurs externes (p. ex. : runners éphémères/conteneurisés, ou job privilégié réservé aux PR de mainteneurs, le fork CI restant non-root).
  • Dépendance infra : les deux runners doivent garder NOPASSWD. Sans lui, l’arche concernée échoue au premier sudo -n (diagnostic explicite).
  • Propriété de target/ : normalisée après coup ; le chown final est en if: always() pour survivre à un échec de mesure.

Alternatives écartées

  • Exclure la surface privilégiée du gate (#[coverage(off)] / --ignore-filename-regex) : cesse de mesurer ces lignes — divergence avec la méthode des huit familles, masque d’éventuelles régressions.
  • Abaisser le plancher (96 → 95) : affaiblit le gate global pour toutes les familles afin d’accommoder une seule famille privilégiée.
  • Garder la mesure non-root + exceptions : impossible, le plancher est numérique ; et contredit le refus des fausses exceptions privilège.

Références

  • Principe d’ingénierie 1 (couverture couche 0).
  • ADR-014 (cibles tier-1 x86_64 + aarch64 ⇒ CI sur les deux runners).
  • docs/CI.md (seuils, baseline), docs/COVERAGE-EXCEPTIONS.md.
  • Dette « protections de branche inactives » (même modèle de confiance privé / mono-mainteneur), docs/JOURNAL.md.

Licence du document : MPL 2.0.