Skip to main content

air_sys_syscall/io_uring/
sandbox.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! **Confinement d'un ring** (Temps 3f) — sous-module
6//! `air-sys-syscall::io_uring::sandbox`. Matérialise la décision de soundness
7//! **S3** : un ring qui ne peut émettre que les opérations explicitement mises
8//! en liste blanche, **imposé par le kernel**.
9//!
10//! Référence normative : `docs/specs/layer-0/io-uring-3f-sandbox.md`.
11//!
12//! ## Pourquoi
13//!
14//! io_uring exécute des opérations (`read`, `openat2`, `connect`…) qui **ne
15//! passent pas par l'interface syscall classique** : un filtre **seccomp** qui
16//! bloque le syscall `openat2` n'empêche **pas** un `IORING_OP_OPENAT2` soumis
17//! via le ring — voie de contournement historique des bacs à sable. La réponse :
18//! **restreindre le ring lui-même**. Les restrictions io_uring **complètent**
19//! seccomp/Landlock (`family-security`), en défense en profondeur ; elles ne les
20//! remplacent pas. C'est la brique io_uring du modèle de capabilities d'Air
21//! (ADR-001 AirCom, ADR-010 entitlements signés) — la couche 0 fournit le
22//! **mécanisme**, la **politique** vit en couche 5.
23//!
24//! ## Le flux en trois temps (imposé kernel, reflété par l'API)
25//!
26//! 1. **Créer désactivé** : [`IoUringBuilder::restrict`](super::IoUringBuilder::restrict)
27//!    crée le ring `IORING_SETUP_R_DISABLED` et applique la liste blanche
28//!    (`REGISTER_RESTRICTIONS`) — possible **uniquement** tant qu'il est désactivé.
29//! 2. **Activer** : [`IoUring::enable`](super::IoUring::enable) (`REGISTER_ENABLE_RINGS`).
30//! 3. **Immuable ensuite** : après `enable`, les restrictions ne peuvent plus être
31//!    assouplies (imposé kernel) ; toute soumission **avant** `enable` est refusée.
32//!
33//! **Default-deny** : dès qu'un opcode est mis en liste blanche, le kernel refuse
34//! **tout le reste** (`-EACCES`).
35//!
36//! ```
37//! use air_sys_syscall::io_uring::{IoUringOpcode, RegisterOp, RestrictionSet, SqeFlagSet};
38//!
39//! // Un service AirCom « réseau seul » : le ring ne pourra jamais émettre
40//! // openat2, unlinkat, etc. — garanti kernel.
41//! let policy = RestrictionSet::new()
42//!     .allow_op(IoUringOpcode::Socket)
43//!     .allow_op(IoUringOpcode::Connect)
44//!     .allow_op(IoUringOpcode::Send)
45//!     .allow_op(IoUringOpcode::Recv)
46//!     .allow_op(IoUringOpcode::Close)
47//!     .allow_register(RegisterOp::ProvidedBuffers)
48//!     .require_sqe_flags(SqeFlagSet::new().fixed_file());
49//! assert_eq!(policy.as_slice().len(), 7);
50//! // let mut ring = IoUringBuilder::new(entries).restrict(policy.as_slice()).build()?;
51//! // ring.enable()?;  // à partir d'ici, tout opcode hors liste échoue.
52//! ```
53
54use super::{IoUringOpcode, Restriction, SubmitOptions};
55use alloc::vec::Vec;
56
57// ───────────────────────────────────────────────────────────────────────────
58// RestrictionSet — liste blanche default-deny (§3.1)
59// ───────────────────────────────────────────────────────────────────────────
60
61/// Liste blanche de ce qu'un ring confiné pourra faire (décision **S3**).
62/// **Default-deny** : ce qui n'est pas explicitement autorisé est refusé par le
63/// kernel. Construite par combinateurs, puis passée à
64/// [`IoUringBuilder::restrict`](super::IoUringBuilder::restrict) via
65/// [`as_slice`](Self::as_slice).
66///
67/// `Default` = ensemble **vide** : aucune restriction (le ring n'est **pas**
68/// confiné). Le confinement commence dès la **première** entrée (`allow_*` /
69/// `require_*`).
70#[derive(Debug, Clone, Default)]
71pub struct RestrictionSet {
72    restrictions: Vec<Restriction>,
73}
74
75impl RestrictionSet {
76    /// Ensemble vide (équivaut à [`Default`]).
77    #[must_use]
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Autorise un **opcode de soumission** (`RESTRICTION_SQE_OP`). Dès le premier
83    /// `allow_op`, **seuls** les opcodes listés passent (default-deny).
84    #[must_use]
85    pub fn allow_op(mut self, op: IoUringOpcode) -> Self {
86        self.restrictions.push(Restriction::AllowOp(op));
87        self
88    }
89
90    /// Autorise un **register opcode** (`RESTRICTION_REGISTER_OP`).
91    #[must_use]
92    pub fn allow_register(mut self, op: RegisterOp) -> Self {
93        self.restrictions
94            .push(Restriction::AllowRegister(op.number()));
95        self
96    }
97
98    /// Déclare les **drapeaux SQE autorisés** (`RESTRICTION_SQE_FLAGS_ALLOWED`) :
99    /// un SQE ne peut porter que des drapeaux de cet ensemble.
100    #[must_use]
101    pub fn allow_sqe_flags(mut self, flags: SqeFlagSet) -> Self {
102        self.restrictions
103            .push(Restriction::SqeFlagsAllowed(flags.into_options()));
104        self
105    }
106
107    /// Déclare les **drapeaux SQE requis** sur **chaque** soumission
108    /// (`RESTRICTION_SQE_FLAGS_REQUIRED`) : un SQE sans ces drapeaux est refusé.
109    /// Combiné à [`SqeFlagSet::fixed_file`], impose l'usage de FD **enregistrés**
110    /// (interdit les FD bruts — moindre privilège).
111    #[must_use]
112    pub fn require_sqe_flags(mut self, flags: SqeFlagSet) -> Self {
113        self.restrictions
114            .push(Restriction::SqeFlagsRequired(flags.into_options()));
115        self
116    }
117
118    /// **Point d'intégration couche 5** (ADR-010). Un composant de confiance
119    /// (`air-launchd`/`air-trust`) traduit les **entitlements signés** du
120    /// manifeste d'un `.airservice`/`.airapp` en l'ensemble d'opcodes que le ring
121    /// pourra émettre, puis appelle ceci. La **politique** (quel entitlement ⇒
122    /// quels opcodes) vit en couche 5 ; la couche 0 ne fournit que le mécanisme
123    /// **default-deny**. Équivaut à un `allow_op` répété sur `allowed_ops`.
124    #[must_use]
125    pub fn from_entitlements<I>(allowed_ops: I) -> Self
126    where
127        I: IntoIterator<Item = IoUringOpcode>,
128    {
129        allowed_ops.into_iter().fold(Self::new(), Self::allow_op)
130    }
131
132    /// Vue des restrictions, à passer à
133    /// [`IoUringBuilder::restrict`](super::IoUringBuilder::restrict).
134    #[must_use]
135    pub fn as_slice(&self) -> &[Restriction] {
136        &self.restrictions
137    }
138
139    /// Nombre de restrictions accumulées.
140    #[must_use]
141    pub fn len(&self) -> usize {
142        self.restrictions.len()
143    }
144
145    /// `true` si aucune restriction (ring **non** confiné).
146    #[must_use]
147    pub fn is_empty(&self) -> bool {
148        self.restrictions.is_empty()
149    }
150}
151
152// ───────────────────────────────────────────────────────────────────────────
153// RegisterOp — register-ops autorisables (§4, RESTRICTION_REGISTER_OP)
154// ───────────────────────────────────────────────────────────────────────────
155
156/// Register opcode qu'un ring confiné peut être autorisé à effectuer
157/// (`RESTRICTION_REGISTER_OP`). **Newtype typé** plutôt qu'un `u8` magique
158/// (ADR-021 conv. 3 : chaque opération est une variante nommée). `#[non_exhaustive]`
159/// : de futurs kernels peuvent en ajouter.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161#[non_exhaustive]
162pub enum RegisterOp {
163    /// `IORING_REGISTER_BUFFERS2` (15) — buffers enregistrés (Temps 3a).
164    Buffers,
165    /// `IORING_REGISTER_FILES2` (13) — table de FD fixes (Temps 3a).
166    Files,
167    /// `IORING_REGISTER_EVENTFD` (4) — eventfd de complétion (Temps 3a).
168    Eventfd,
169    /// `IORING_REGISTER_PERSONALITY` (9) — identité enregistrée (Temps 3a).
170    Personality,
171    /// `IORING_REGISTER_PBUF_RING` (22) — buffers fournis ring-mapped (Temps 3b).
172    ProvidedBuffers,
173    /// `IORING_REGISTER_RING_FDS` (20) — enregistrement du ring fd (Temps 3a/3e).
174    RingFd,
175    /// `IORING_REGISTER_SYNC_CANCEL` (24) — annulation synchrone (Temps 2c/3d).
176    SyncCancel,
177    /// `IORING_REGISTER_PROBE` (8) — introspection des opcodes (Temps 1).
178    Probe,
179}
180
181impl RegisterOp {
182    /// Numéro kernel `IORING_REGISTER_*` (placé dans le champ `register_op` de
183    /// `io_uring_restriction`). Style aligné sur `opcode_number` : littéraux `u8`
184    /// (tous < 256), nom kernel en doc de variante.
185    fn number(self) -> u8 {
186        match self {
187            RegisterOp::Buffers => 15,
188            RegisterOp::Files => 13,
189            RegisterOp::Eventfd => 4,
190            RegisterOp::Personality => 9,
191            RegisterOp::ProvidedBuffers => 22,
192            RegisterOp::RingFd => 20,
193            RegisterOp::SyncCancel => 24,
194            RegisterOp::Probe => 8,
195        }
196    }
197}
198
199// ───────────────────────────────────────────────────────────────────────────
200// SqeFlagSet — drapeaux IOSQE_* pour les restrictions de drapeaux (§4)
201// ───────────────────────────────────────────────────────────────────────────
202
203/// Ensemble de drapeaux `IOSQE_*` pour les restrictions de drapeaux SQE
204/// ([`RestrictionSet::allow_sqe_flags`] / [`RestrictionSet::require_sqe_flags`]).
205/// `Default` = aucun drapeau. Builder chaînable. Partage la représentation des
206/// drapeaux avec [`SubmitOptions`](super::SubmitOptions).
207#[derive(Debug, Clone, Copy, Default)]
208pub struct SqeFlagSet {
209    options: SubmitOptions,
210}
211
212impl SqeFlagSet {
213    /// Ensemble vide (équivaut à [`Default`]).
214    #[must_use]
215    pub fn new() -> Self {
216        Self::default()
217    }
218
219    /// `IOSQE_FIXED_FILE` : le `fd` du SQE est un **index de slot fixe**. En
220    /// `require_sqe_flags`, **interdit les FD bruts** (force les FD enregistrés).
221    #[must_use]
222    pub fn fixed_file(mut self) -> Self {
223        self.options = self.options.fixed_file();
224        self
225    }
226
227    /// `IOSQE_IO_DRAIN` : draine les opérations précédentes avant celle-ci.
228    #[must_use]
229    pub fn drain(mut self) -> Self {
230        self.options = self.options.drain();
231        self
232    }
233
234    /// `IOSQE_ASYNC` : force l'exécution sur le pool io-wq.
235    #[must_use]
236    pub fn force_async(mut self) -> Self {
237        self.options = self.options.force_async();
238        self
239    }
240
241    /// `IOSQE_BUFFER_SELECT` : sélection automatique d'un buffer fourni (Temps 3b).
242    #[must_use]
243    pub fn buffer_select(mut self) -> Self {
244        self.options = self.options.buffer_select();
245        self
246    }
247
248    /// `IOSQE_CQE_SKIP_SUCCESS` : pas de CQE en cas de succès.
249    #[must_use]
250    pub fn skip_cqe_on_success(mut self) -> Self {
251        self.options = self.options.skip_cqe_on_success();
252        self
253    }
254
255    /// Conversion interne vers la représentation [`SubmitOptions`] portée par
256    /// [`Restriction`].
257    fn into_options(self) -> SubmitOptions {
258        self.options
259    }
260}
261
262// ───────────────────────────────────────────────────────────────────────────
263// Tests
264// ───────────────────────────────────────────────────────────────────────────
265//
266// Intégration kernel (io_uring non modélisé par Miri) → `#[cfg_attr(miri, ignore)]`.
267// La preuve de sécurité (§8) est `openat2_refused_on_network_only_ring`.
268
269#[cfg(test)]
270mod tests;