Spec couche 0 — Module io_uring, Temps 3f : confinement (sandbox)
Spécification technique — Version 1.0 (cible kernel : Linux 6.12 LTS)
Position. Le Temps 3f rend opérationnelle la décision de soundness S3 du document maître : confiner un ring pour qu’il ne puisse émettre que des opérations explicitement autorisées. Sous-module
air-sys-syscall::io_uring::sandbox. Repose sur le flagIORING_SETUP_R_DISABLED(Temps 1), et les register opcodesREGISTER_RESTRICTIONS(11) etENABLE_RINGS(12). C’est la brique io_uring du modèle de capabilities d’Air (ADR-001 AirCom, ADR-010 entitlements signés), en défense en profondeur avecfamily-security(seccomp/Landlock).
1. Pourquoi confiner un ring ?
io_uring exécute des opérations (read, openat2, connect…) qui, soumises
par le ring, ne passent pas par l’interface syscall classique. Un filtre
seccomp qui bloque le syscall openat2 n’empêche pas un IORING_OP_OPENAT2
soumis via le ring. Historiquement, c’est une voie de contournement des bacs à
sable. La réponse propre : restreindre le ring lui-même.
Un ring confiné ne peut soumettre que les opcodes, register-ops et drapeaux SQE explicitement mis en liste blanche, et le kernel l’impose. Même un service compromis ne peut pas élargir ce que son ring autorise.
C’est la matérialisation, au niveau io_uring, des entitlements déclarés dans
le manifeste signé d’un .airservice/.airapp (ADR-010), et le pendant
kernel des capabilities AirCom (ADR-001).
2. Le mécanisme en trois temps
- Créer désactivé : le ring est créé avec
IORING_SETUP_R_DISABLED— il existe mais ne peut rien soumettre. - Restreindre :
REGISTER_RESTRICTIONSapplique une liste de restrictions (liste blanche). Possible uniquement tant que le ring est désactivé. - Activer :
REGISTER_ENABLE_RINGSactive le ring. Après activation, les restrictions sont immuables — on ne peut plus les assouplir.
L’ordre est imposé par le kernel et reflété par l’API : on ne peut pas restreindre un ring déjà actif, ni soumettre avant activation.
3. API
3.1 RestrictionSet
#![allow(unused)]
fn main() {
/// Liste blanche de ce qu'un ring confiné pourra faire. Default-deny : ce qui
/// n'est pas explicitement autorisé est refusé par le kernel.
#[derive(Debug, Clone, Default)]
pub struct RestrictionSet { /* ... */ }
impl RestrictionSet {
pub fn new() -> Self;
/// Autorise un opcode de soumission (RESTRICTION_SQE_OP).
pub fn allow_op(self, op: IoUringOpcode) -> Self;
/// Autorise un register opcode (RESTRICTION_REGISTER_OP).
pub fn allow_register(self, op: RegisterOp) -> Self;
/// Drapeaux SQE autorisés (RESTRICTION_SQE_FLAGS_ALLOWED).
pub fn allow_sqe_flags(self, flags: SqeFlagSet) -> Self;
/// Drapeaux SQE requis sur CHAQUE soumission (RESTRICTION_SQE_FLAGS_REQUIRED).
pub fn require_sqe_flags(self, flags: SqeFlagSet) -> Self;
/// Conversion depuis un jeu d'entitlements (couche supérieure).
/// Fournie comme point d'intégration ; la politique vit en couche 5.
pub fn from_entitlements(/* … */) -> Self;
}
}
3.2 Intégration au builder (Temps 1)
#![allow(unused)]
fn main() {
// IoUringBuilder::restrict(&[Restriction]) → R_DISABLED + REGISTER_RESTRICTIONS.
let mut ring = IoUringBuilder::new(entries)
.restrict(restrictions.as_slice()) // ring créé désactivé
.build()?;
ring.enable()?; // REGISTER_ENABLE_RINGS ; immuable ensuite
// À partir d'ici, toute soumission hors liste blanche échoue (-EACCES/-EINVAL).
}
build()avecrestrictnon vide laisse le ring désactivé ;enable()(Temps 1 §5.2) l’active. Toute tentative desubmit_*avantenable()⇒ erreur. Toute tentative de re-restreindre aprèsenable()⇒ erreur.- Sémantique default-deny : si
RestrictionSetcontient au moins unallow_op, seuls les opcodes listés sont permis (le reste est refusé par le kernel). Idem pour les register-ops.
4. Les quatre types de restriction
Type (io_uring_register_restriction_op) | RestrictionSet | Effet |
|---|---|---|
RESTRICTION_SQE_OP (1) | allow_op | liste blanche des opcodes soumissibles |
RESTRICTION_REGISTER_OP (0) | allow_register | liste blanche des register-ops |
RESTRICTION_SQE_FLAGS_ALLOWED (2) | allow_sqe_flags | drapeaux SQE autorisés |
RESTRICTION_SQE_FLAGS_REQUIRED (3) | require_sqe_flags | drapeaux imposés à chaque SQE |
Exemple — un service AirCom « réseau seul » :
#![allow(unused)]
fn main() {
let r = RestrictionSet::new()
.allow_op(IoUringOpcode::Socket)
.allow_op(IoUringOpcode::Connect)
.allow_op(IoUringOpcode::Send)
.allow_op(IoUringOpcode::Receive)
.allow_op(IoUringOpcode::SendZeroCopy)
.allow_op(IoUringOpcode::Close);
// Le ring ne pourra jamais émettre openat2, unlinkat, etc. — garanti kernel.
}
5. Lien avec le modèle de sécurité d’Air
- ADR-010 (entitlements signés) : le manifeste d’un
.airservicedéclare ses entitlements ; un composant de couche 5 (air-launchd/air-trust) traduit ces entitlements enRestrictionSetet fournit au service un ring déjà confiné. La couche 0 fournit le mécanisme (RestrictionSet,restrict,enable) ; la politique vit en couche supérieure — séparation nette. - ADR-001 (AirCom) : un service ne reçoit que les capabilities (FD) de son manifeste, et son ring ne peut émettre que les opérations correspondantes — double verrou (FD non falsifiables + ring restreint).
family-security(seccomp/Landlock) : les restrictions io_uring complètent seccomp/Landlock, elles ne les remplacent pas. seccomp filtre les syscalls, Landlock les accès FS, les restrictions io_uring les opérations du ring — défense en profondeur. C’est précisément ce qui ferme la voie de contournement évoquée en §1.- Personality (Temps 3a) : combinée aux restrictions, permet d’exécuter des ops sous une identité contrôlée — moindre privilège.
Note hors périmètre. Un filtrage encore plus fin par programme BPF (
IORING_REGISTER_BPF_FILTER) est postérieur à 6.12 ; il pourra enrichir ce confinement plus tard, par détection runtime, sans amendement.
6. Récapitulatif
| Élément | Façade |
|---|---|
SETUP_R_DISABLED | IoUringBuilder::restrict (crée désactivé) |
REGISTER_RESTRICTIONS (11) | IoUringBuilder::restrict (applique la liste) |
REGISTER_ENABLE_RINGS (12) | IoUring::enable |
4 restriction_op | RestrictionSet::{allow_op, allow_register, allow_sqe_flags, require_sqe_flags} |
7. Types ajoutés / partagés
Nouveaux : RestrictionSet, RegisterOp (énumération des register-ops
autorisables), SqeFlagSet. Réutilise : Restriction et IoUringOpcode (déclarés
au Temps 1), IoUringBuilder::restrict, IoUring::enable. Point d’intégration
from_entitlements (type d’entitlements défini en couche supérieure).
8. Stratégie de tests
- Intégration : créer un ring
R_DISABLED, appliquer une liste blanche,enable(), vérifier qu’un opcode autorisé passe et qu’un opcode non autorisé échoue (-EACCES/-EINVAL) ; idem register-ops ;require_sqe_flagsimposé (une soumission sans le drapeau échoue) ; soumission avantenablerefusée ; re-restriction aprèsenablerefusée (immuabilité). - Sécurité : test démontrant qu’un
openat2soumis via un ring « réseau seul » est refusé même si le process a le droit d’ouvrir le fichier (preuve de la défense en profondeur vs seccomp). - Property-based : pour tout sous-ensemble d’opcodes autorisés, exactement ceux-là passent.
- Couverture 100 % lignes + branches.
9. Décisions de fond émergées au Temps 3f
- Confinement = mécanisme couche 0, politique couche 5 —
RestrictionSetet le fluxrestrict → enablesont neutres ; la traduction des entitlements signés (ADR-010) vit en couche supérieure. - Default-deny — dès qu’une liste blanche d’opcodes est posée, tout le reste est refusé par le kernel ; pas de demi-mesure.
- Immuabilité après
enable— garantie par le kernel et reflétée par l’API ; impossible d’assouplir un ring confiné. - Défense en profondeur explicite — les restrictions io_uring complètent seccomp/Landlock et ferment la voie de contournement du filtre syscall.
- BPF filtering noté mais hors périmètre (post-6.12).
10. Travail à reprendre
Spec suivante et dernière du module : io-uring-4-raw.md (accès niveau 1 au
protocole d’anneau : RawSubmissionQueueEntry/RawCompletionQueueEntry, manipulation directe des SQE/CQE,
opérations non encore wrappées, # Safety extensif). Traduction anglaise globale
après validation des documents français.
Licence du document : MPL 2.0
Statut : Spécification technique du Temps 3f (confinement) du module air-sys-syscall::io_uring, cible kernel 6.12 LTS.