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/--branchsont préfixées desudo -n -E env "PATH=$PATH"(préserveHOME/CARGO_HOMEet lePATHcargo).sudo -néchoue franchement (pas de prompt) si le runner n’a pasNOPASSWD→ signal clair. - Une étape finale
if: always()rétablit la propriété detarget/à l’utilisateur du runner (sudo chown -R), afin que legit clean(non-root) du run suivant ne bute pas sur des fichiers root. - Prérequis infra : l’utilisateur des deux runners doit disposer du
sudosans 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 premiersudo -n(diagnostic explicite). - Propriété de
target/: normalisée après coup ; lechownfinal est enif: 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.