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;