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-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.0couche-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.1 revient 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 exigeCouche 0Couche 1 (avant)
Cascade de mort PR_SET_PDEATHSIG (§D1)set_parent_death_signalabsent
Purge des descripteurs hérités, anneaux io_uring compris (§D8)close_rangeabsent
Monitor root borné à {SETUID,SETGID,CHOWN,KILL} ([ADR-122] §6)capbset_read/capbset_drop/capsetabsent
Canal RPC lisible/écrivable sous la cage pre-auth (§D2/D8)read/writesend/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 :

  1. drop_privileges se 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 appelle getresuid/getresgid/getgroups, absents des profils pre-auth et sftp-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.
  2. AirLandlockPolicy::new() ne pose aucune cage. Une politique sans règle n’a aucun accès à gérer ; apply la 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 un fork (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/2 sont préservés d’office (un openat récupérant le descripteur 0 est 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 de drop_privileges qui change d’identité et largue tout. Séquence : bounding set → capset (permitted + effective = cible, inheritable vidé) → ambient vidé → no_new_privsvérification défensive.

    Ordre inversé par rapport à [ADR-122] §6, qui énumère capset avant le bounding set. C’est une nécessité mécanique, pas un changement de politique : PR_CAPBSET_DROP exige CAP_SETPCAP dans l’ensemble effective — que capset vers {SETUID,SETGID,CHOWN,KILL} retire précisément. Dans l’ordre de l’ADR, l’étape 2 échouerait EPERM. 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 que send/recv sur un seqpacket connecté (un appel = un datagramme), mais par write(2)/read(2).

    Pourquoi ne pas simplement autoriser sendto/recvfrom dans le profil pre-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 que write(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 porter MSG_NOSIGNAL, donc un pair disparu produit SIGPIPE ou EPIPE selon 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 par table_matches_kernel_headers), ajoutés aux profils pre-auth et sftp-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) et IOCTL_DEV (v4) en sont exclus délibérément : landlock_create_ruleset échoue EINVAL sur 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 de new() 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.1 est posé par le superviseur.
  • Aucun retrait, aucune signature modifiée. La surface couche-1-v3.0 reste 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/recvfrom dans le profil pre-auth plutôt qu’ajouter write_packet/read_packet. Rejeté : élargit la surface d’attaque du processus le plus exposé pour un service que write(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 Capability en 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’un close_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 0monitor::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 dans etat-avancement.md.

Licence du document : MPL 2.0