ADR-125 — Descellement additif couche-1-v3.1 : les primitives de topologie privsep
Statut : Accepté (2026-07-28). RFC de structure (ADR-015). Applique le modèle de re-sceau du sceau couche 1 (ADR-062) sous la délégation additive du superviseur (ADR-065). Implémente les prérequis couche 1 d’ADR-124 §D1/D7/D8 et d’ADR-122 §6.
Catégorie : Descellement ADDITIF de la couche 1 scellée
(couche-1-v3.0 → couche-1-v3.1). Aucun retrait, aucun changement de
signature : toute surface antérieure reste valide telle quelle.
Le tag n’est pas posé par cet ADR. Conformément au modèle de re-sceau, la pose du tag
couche-1-v3.1revient au superviseur, après revue.
Contexte
ADR-124 §D1 décrit une topologie
de trois étages (listener → monitor → enfant pré-auth) dont chaque transition exige
une opération que la couche 1 ne savait pas nommer :
| Ce que la topologie exige | Couche 0 | Couche 1 (avant) |
|---|---|---|
Cascade de mort PR_SET_PDEATHSIG (§D1) | set_parent_death_signal ✔ | absent |
Purge des descripteurs hérités, anneaux io_uring compris (§D8) | close_range ✔ | absent |
Monitor root borné à {SETUID,SETGID,CHOWN,KILL} ([ADR-122] §6) | capbset_read/capbset_drop/capset ✔ | absent |
Canal RPC lisible/écrivable sous la cage pre-auth (§D2/D8) | read/write ✔ | send/recv seulement (⇒ sendto/recvfrom) |
air-sshd est couche 2 : le layering (ADR-077)
lui interdit d’appeler la couche 0 en direct. Sans ces additifs, la topologie
d’ADR-124 n’est pas exprimable — c’est ce qui motive ce descellement.
Deux découvertes faites en implémentant motivent deux additifs supplémentaires, qui ne relèvent pas du confort mais de la correction :
drop_privilegesse tuait sous sa propre cage. Depuis [ADR-123], la cage air-sandbox est posée au milieu de la séquence de largage (étape 6), et la vérification défensive (étape 7) s’exécute donc déjà confinée. Or elle appellegetresuid/getresgid/getgroups, absents des profilspre-authetsftp-worker— l’allow-list étant fail-closed (KILL_PROCESS), toute composition « largage + cage » se faisait tuer par seccomp. Personne ne l’avait vu : ADR-123 était livré sans appelant.AirLandlockPolicy::new()ne pose aucune cage. Une politique sans règle n’a aucun accès à gérer ;applyla court-circuite et aucun ruleset n’est posé. Or ADR-124 §D7 exige « Landlock VIDE (aucun fichier) » pour l’étage pré-auth. « Rien à restreindre » et « tout est restreint » se ressemblent dans le code et sont opposés dans les faits.
Décision
Descellement additif couche-1-v3.1, sur trois crates scellées.
air-process — les primitives de topologie
-
set_parent_death_signal(Option<Signal>)/parent_death_signal()— arme et relit la cascade de mort. La documentation grave les deux pièges : le réglage n’est pas hérité par unfork(c’est à l’enfant de le poser), et la course parent-mort-avant-armement doit être refermée par l’appelant (relecture du parent). -
close_inherited_fds_except(&[BorrowedFd])— ferme tout descripteur ≥ 3 sauf ceux nommés.0/1/2sont préservés d’office (unopenatrécupérant le descripteur0est une classe de faille connue). Alloc-free (tampon de tri sur la pile, ≤ 16 entrées) : utilisable dans la fenêtre post-fork. -
reduce_capabilities_to(&[Capability])— confine un processus qui reste root, à distinguer dedrop_privilegesqui change d’identité et largue tout. Séquence : bounding set →capset(permitted + effective = cible, inheritable vidé) → ambient vidé →no_new_privs→ vérification défensive.Ordre inversé par rapport à [ADR-122] §6, qui énumère
capsetavant le bounding set. C’est une nécessité mécanique, pas un changement de politique :PR_CAPBSET_DROPexigeCAP_SETPCAPdans l’ensemble effective — quecapsetvers{SETUID,SETGID,CHOWN,KILL}retire précisément. Dans l’ordre de l’ADR, l’étape 2 échoueraitEPERM. L’état final atteint est exactement celui décrit par ADR-122 §6.
air-socket — l’I/O que la cage autorise
-
AirUnixSeqpacket::write_packet/read_packet— même sémantique quesend/recvsur un seqpacket connecté (un appel = un datagramme), mais parwrite(2)/read(2).Pourquoi ne pas simplement autoriser
sendto/recvfromdans le profilpre-auth? Parce que ce serait élargir l’allow-list du processus le plus exposé du démon — celui qui parse le protocole SSH d’un pair non authentifié — alors quewrite(2)donne la même chose gratuitement. C’est le choix d’OpenSSH pour la même raison. La documentation grave le piège associé :write(2)ne peut pas porterMSG_NOSIGNAL, donc un pair disparu produitSIGPIPEouEPIPEselon la disposition du signal — dans les deux cas fail-closed, ce que le privsep recherche.
air-sandbox — deux corrections de confinement
-
AirSyscall::{Getresuid, Getresgid, Getgroups}(numéros x86_64 et aarch64, revérifiés contre les en-têtes du noyau partable_matches_kernel_headers), ajoutés aux profilspre-authetsftp-worker.Élargissement d’allow-list, donc à justifier. Coût en surface d’attaque : nul — trois lectures pures des identifiants du processus lui-même, sans effet de bord, ne révélant rien que
getuid/getgid(déjà permis) ne révèle. Sans elles, la composition « largage + cage » d’ADR-123 est inutilisable. Une garde de non-régression (both_profiles_allow_the_identity_readback) empêche leur retrait silencieux. -
AirLandlockPolicy::deny_all()— une cage totale : aucun chemin autorisé, mais tous les droits par chemin de l’ABI Landlock v1 déclarés gérés, donc effectivement refusés.REFER(v2),TRUNCATE(v3) etIOCTL_DEV(v4) en sont exclus délibérément :landlock_create_rulesetéchoueEINVALsur un droit inconnu du noyau, et la pose de cage est fail-closed — exiger un droit post-v1 tuerait le worker sur un noyau parfaitement valide. La documentation denew()est corrigée en conséquence (« point de départ d’un enchaînement d’allow, pas une cage »).
Ce que cet ADR ne fait pas
- Aucun tag.
couche-1-v3.1est posé par le superviseur. - Aucun retrait, aucune signature modifiée. La surface
couche-1-v3.0reste valide à l’identique ; c’est ce qui rend ce descellement additif. - Aucune modification de la couche 0. Voir la limite ci-dessous.
Limite connue — le bounding set n’est réduit qu’à 20/41
reduce_capabilities_to ne peut larguer du bounding set que les capabilities que la
couche 0 sait nommer : l’énumération air_sys_types::Capability en compte 20,
le noyau 41 (CAP_LAST_CAP = 40). Y subsistent donc, entre autres,
CAP_SETPCAP, CAP_SETFCAP, CAP_MKNOD, CAP_BPF, CAP_AUDIT_*, CAP_MAC_*.
Ce que cela ne remet pas en cause. Les ensembles permitted et effective sont
réduits exactement à la cible (masque de bits ⇒ exhaustif), et un ensemble
permitted ne peut jamais grandir : le monitor ne peut donc plus utiliser
aucune de ces capabilities, CAP_SETPCAP compris. Le bounding set résiduel ne compte
que pour ce qu’un descendant pourrait acquérir via un binaire à capabilities de
fichier lors d’un execve — or le monitor n’exec jamais, et le worker post-auth
(V2.5) posera no_new_privs.
Ce que cela laisse ouvert. L’objectif littéral d’ADR-122 §6 (« bounding set = la
cible ») n’est pas atteint. Le combler exige un additif couche 0 — compléter
Capability — qui n’est pas dans le mandat de cet ADR : c’est un descellement de
la couche 0, remonté au superviseur (cf. rapport V2.2).
Alternatives rejetées
- Autoriser
sendto/recvfromdans le profilpre-authplutôt qu’ajouterwrite_packet/read_packet. Rejeté : élargit la surface d’attaque du processus le plus exposé pour un service quewrite(2)rend déjà. - Sortir la cage de
drop_privileges(larguer, puis poser la cage depuis la couche 2) pour éviter d’élargir les profils. Rejeté : défait l’apport d’ADR-123 — l’atomicité de la séquence est précisément ce qui la rend impossible à se tromper. - Compléter
Capabilityen couche 0 dans le même incrément. Rejeté : hors mandat (descellement d’une autre couche scellée), et sans urgence puisque permitted est exact. Remonté au superviseur. - Un
close_fds_from(first)plutôt qu’unclose_inherited_fds_except(keep). Rejeté : obligerait chaque appelant à ranger ses descripteurs conservés sous un seuil — une discipline fragile, au moment précis où l’on ne veut pas se tromper.
Conséquences
air-sshd(couche 2) exprime la topologie d’ADR-124 §D1 sans jamais toucher la couche 0 —monitor::topology, incrément V2.2.- La composition « largage + cage » d’ADR-123 devient utilisable, et l’est
effectivement (premier appelant : l’étage pré-auth d’
air-sshd). - Trois crates scellées gagnent de la surface ⇒ re-sceau
couche-1-v3.1à poser par le superviseur. - Enregistrement :
SUMMARY.md,registre-adrs-fr.md, note dansetat-avancement.md.
Licence du document : MPL 2.0