ADR-156 — air-sandbox cible l’ABI Landlock v7 : on expose tout ce que le noyau offre
Statut : Accepté (2026-08-13, BDFL). Prolonge
ADR-112, qui a créé air-sandbox, et sert le seuil d’exécution
d’ADR-046 D6.
Catégorie : Sécurité + contrat d’API. Décide jusqu’où la cage d’Air suit le noyau, et selon quelle règle.
Contexte
air-sandbox expose aujourd’hui une fraction de ce que Landlock sait faire. L’inventaire,
constaté le 2026-08-13 :
| ABI | Noyau | Ce qu’elle apporte | Exposé par air-sandbox ? |
|---|---|---|---|
| v1 | 5.13 | droits de base par chemin | ✅ |
| v2 | 5.19 | REFER — déplacement inter-répertoires | ❌ |
| v3 | 6.2 | TRUNCATE | ✅ (via read_write_file) |
| v4 | 6.7 | ports TCP bind/connect | ✅ |
| v5 | 6.10 | IOCTL_DEV — ioctl borné par chemin | ✅ depuis le 2026-08-13 |
| v6 | 6.12 | scoping : sockets UNIX abstraits, signaux | ✅ depuis le 2026-08-13 |
| v7 | ~6.15 | journalisation/audit (à confirmer) | ❌ |
| v8 | (constatée) | LANDLOCK_RESTRICT_SELF_TSYNC — la restriction s’applique à tous les fils | ❌ |
Le plancher d’Air est 6.12, c’est-à-dire v6 : tout ce qui précède est donc acquis, et le manque n’est plus une prudence — c’est un retard.
Mesuré le 2026-08-13, pas recopié. Un noyau 7.0 de développement annonce une ABI v8, et non v7 — la sonde et l’en-tête noyau installé le disent tous deux. D1 prévoyait ce cas : la cible se déplace avec le noyau, sans nouvel ADR. Le tableau est donc daté par construction, et c’est la sonde qui fait foi, jamais lui.
Un piège relevé au passage :
/usr/include/linux/landlock.h(celui de la libc) était périmé — deux champs, seize octets — là où l’en-tête noyau en porte trois, vingt-quatre. Prendre la disposition dans le mauvais fichier aurait fait écrire unscopedque le noyau n’aurait jamais lu.v8 est un enjeu réel pour Air, et pas une curiosité :
TSYNCapplique la restriction à tous les fils du processus. Sans lui, une cage posée sur un seul fil est une cage qu’on contourne en changeant de fil. À instruire dans son propre incrément.
Ce que le retard coûte, mesuré
deny_all() ne gère que les droits v1. Or un droit non géré est un droit non
restreint — c’est le mécanisme exact de l’écart EXECUTE trouvé le 2026-08-12, où
handled_access_fs était l’union des droits des règles présentes et où l’exécution n’était
donc bornée nulle part.
Conséquence : une cage qui se présente comme une fermeture totale ne restreint aujourd’hui
ni TRUNCATE, ni REFER, ni IOCTL_DEV. Le commentaire qui justifiait ce gel disait que
« ce qui reste hors couverture … n’ouvre aucune porte à un worker qui, précisément, ne peut
plus rien ouvrir ». L’argument vaut pour ce qu’un worker ouvrirait ; il ne vaut pas pour
les descripteurs qu’il a hérités — un worker en tient (canal de médiation, cibles de
redirection, maître de PTY).
Ce n’est pas une faille démontrée : pour les profils en service, seccomp borne le verbe et referme la question en pratique. C’est un écart entre ce que la cage annonce et ce qu’elle gère, et cet écart est exactement ce qu’ADR-112 existait pour supprimer.
La règle qui décide
On n’expose pas ce dont on a besoin ; on expose ce que le noyau offre. Air fournit une plateforme : un développeur qui écrit pour Air doit trouver toute la capacité de confinement d’un noyau 6.x ou 7.x, pas la part qu’Air a consommée jusqu’ici. C’est la doctrine d’API du projet — l’API est une anticipation, jamais une réaction.
Décision
D1 — La cible est l’ABI v7
air-sandbox expose tous les droits et mécanismes de Landlock jusqu’à v7 inclus :
REFER (v2), le scoping des sockets UNIX abstraits et des signaux (v6), et la
journalisation d’audit (v7).
La cible se déplace avec le noyau : une ABI v8 ouvrira un incrément de plus, sans nouvel ADR — la règle est posée ici une fois pour toutes.
D2 — Ce qui est au plancher est exigé ; ce qui est au-dessus est sondé
Le plancher d’Air est 6.12, donc v6. Jusqu’à v6, l’absence d’un droit est une erreur de plateforme : Air refuse, il ne dégrade pas (ADR-004 — aucun repli vers les noyaux antérieurs).
Au-delà (v7 et suivantes), la composition est au mieux-disant, décidée par la sonde
landlock_supported_abi() — qui existe déjà en couche 0 et qu’air-sandbox utilise déjà
pour le ruleset réseau. Ce qui manque est absent, pas silencieusement ignoré : l’appelant
peut le constater.
D3 — deny_all() gère tout ce que l’ABI constatée permet de gérer
Une fermeture totale doit fermer tout ce que le noyau sait fermer. Le jeu figé en v1 disparaît au profit d’un jeu calculé à partir de la version sondée.
C’est la correction directe de l’écart mesuré ci-dessus, et l’application de la règle qui a
manqué à EXECUTE : ce qui n’est pas géré n’est pas restreint.
D4 — Le scoping (v6) est exposé comme un mécanisme à part
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET et LANDLOCK_SCOPE_SIGNAL ne sont pas des droits par
chemin : ils bornent ce qu’un domaine peut joindre hors du système de fichiers. Ils
reçoivent donc leur propre surface, et non un AirPathAccess de plus.
Leur intérêt pour Air est direct : un socket UNIX abstrait n’a pas de chemin, donc aucune règle Landlock de fichier ne le borne — c’était jusqu’ici une voie de communication latérale entre services confinés que la cage ne voyait pas.
D5 — L’audit (v7) est opt-in, jamais un défaut
Les drapeaux de journalisation de landlock_restrict_self disent au noyau ce qu’il doit
tracer d’un refus. Utiles au diagnostic, ils ne changent aucune décision d’accès : ils
sont exposés, et leur activation appartient à l’appelant.
Conséquences
La surface d’air-sandbox s’élargit : nouveaux constructeurs d’accès, surface de
scoping, drapeaux d’audit. C’est une évolution additive de la couche 1 — donc conforme
à la directive de re-sceau unique, dont elle rejoint l’inventaire.
deny_all() devient strictement plus fermant sur tout noyau ≥ 6.12. Aucun appelant ne
perd de droit : ce qui était permis parce que non géré cesse de l’être, ce qui est le sens
même d’une fermeture totale. Un service qui en dépendait sans le savoir le découvrira — et
c’est le but.
Un test doit exister par ABI, et il doit mordre : un droit annoncé géré et non posé est précisément le défaut qu’ADR-112 et cet ADR-ci existent pour empêcher. Le motif est connu : neutraliser la règle, vérifier que le test tombe.
Le coût est nommé : les ABI ≥ v7 ne sont pas garanties par le plancher. Le code doit donc porter deux chemins — exigé jusqu’à v6, sondé au-delà — et cette asymétrie est un endroit où deux lecteurs peuvent diverger. Elle est le prix de la règle « tout ce que le noyau offre » sur un plancher qui, lui, ne bouge pas.
Alternatives rejetées
- N’exposer que ce qu’Air consomme. Rejeté par la règle qui décide : Air est une plateforme, et un développeur doit y trouver la capacité du noyau, pas le sous-ensemble qu’Air a utilisé. C’est la doctrine d’API du projet.
- Attendre un besoin. Rejeté pour la même raison — et parce que l’écart de
deny_allest déjà là, mesuré, indépendamment de tout besoin futur. - Relever le plancher noyau à 6.15 pour garantir v7. Rejeté : le plancher sert le userland d’Air et se décide sur d’autres critères (io_uring, cgroups v2, eBPF). Le tirer pour une commodité de cage inverserait la dépendance.
- Dégrader en silence quand un droit manque. Rejeté : c’est ainsi qu’on obtient une cage qui semble poser une politique. Ce qui manque doit être constatable par l’appelant.
À vérifier à la mise en œuvre
Points factuels, qui conditionnent le code sans remettre la décision en cause :
- Le contenu exact de l’ABI v7 et le noyau qui l’apporte, dans
Documentation/userspace-api/landlock.rstde la version ciblée. Le tableau ci-dessus donne v7 ≈ 6.15 pour la journalisation d’audit : à confirmer, pas à recopier d’ici ; - Le comportement de
landlock_create_rulesetquand on lui demande de gérer un droit inconnu —EINVALattendu, à re-constater plutôt qu’à supposer, puisque toute la composition au mieux-disant repose dessus ; - Les droits que le scoping (v6) exige de déclarer au
create_ruleset, sa granularité, et son interaction avec AirCom — dont les anneaux voyagent par descripteur et non par socket abstrait.
Licence du document : MPL 2.0