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 {
271 use super::{RegisterOp, RestrictionSet, SqeFlagSet};
272 use crate::io_uring::{
273 IoUringBuilder, IoUringOpcode, Restriction, SubmitOptions, encode_restriction,
274 opcode_number, raw, syscall,
275 };
276 use air_sys_types::Errno;
277 use air_sys_types::fs::{DirFd, OpenHow};
278 use air_sys_types::net::{SocketDomain, SocketType};
279 use core::num::NonZeroU32;
280 use std::ffi::CString;
281
282 fn nz(n: u32) -> NonZeroU32 {
283 NonZeroU32::new(n).expect("n ≠ 0")
284 }
285
286 /// Liste candidate pour la propriété.
287 const CANDIDATE_OPS: &[IoUringOpcode] = &[
288 IoUringOpcode::Nop,
289 IoUringOpcode::Socket,
290 IoUringOpcode::Connect,
291 IoUringOpcode::Send,
292 IoUringOpcode::Recv,
293 IoUringOpcode::Close,
294 IoUringOpcode::Openat2,
295 IoUringOpcode::Read,
296 ];
297
298 const ALL_REGISTER_OPS: &[RegisterOp] = &[
299 RegisterOp::Buffers,
300 RegisterOp::Files,
301 RegisterOp::Eventfd,
302 RegisterOp::Personality,
303 RegisterOp::ProvidedBuffers,
304 RegisterOp::RingFd,
305 RegisterOp::SyncCancel,
306 RegisterOp::Probe,
307 ];
308
309 fn cpath(p: &std::path::Path) -> CString {
310 CString::new(p.as_os_str().as_encoded_bytes()).expect("chemin sans NUL")
311 }
312
313 // ── Unitaires (sans kernel) ───────────────────────────────────────────
314
315 #[test]
316 fn restriction_set_is_default_deny_and_accumulates() {
317 // Vide par défaut : ring non confiné.
318 let empty = RestrictionSet::new();
319 assert!(empty.is_empty());
320 assert_eq!(empty.len(), 0);
321 assert!(empty.as_slice().is_empty());
322
323 // Chaque combinateur ajoute exactement une restriction, dans l'ordre.
324 let set = RestrictionSet::new()
325 .allow_op(IoUringOpcode::Socket)
326 .allow_register(RegisterOp::ProvidedBuffers)
327 .allow_sqe_flags(SqeFlagSet::new().force_async())
328 .require_sqe_flags(SqeFlagSet::new().fixed_file());
329 assert!(!set.is_empty());
330 assert_eq!(set.len(), 4);
331
332 // `from_entitlements` = `allow_op` répété (point d'intégration couche 5).
333 let from_ent =
334 RestrictionSet::from_entitlements([IoUringOpcode::Send, IoUringOpcode::Recv]);
335 assert_eq!(from_ent.len(), 2);
336 }
337
338 #[test]
339 fn register_op_numbers_match_kernel_uapi() {
340 // Exhaustif : chaque variante → son numéro `IORING_REGISTER_*` (6.12).
341 let expected: &[(RegisterOp, u8)] = &[
342 (RegisterOp::Buffers, 15),
343 (RegisterOp::Files, 13),
344 (RegisterOp::Eventfd, 4),
345 (RegisterOp::Personality, 9),
346 (RegisterOp::ProvidedBuffers, 22),
347 (RegisterOp::RingFd, 20),
348 (RegisterOp::SyncCancel, 24),
349 (RegisterOp::Probe, 8),
350 ];
351 for &(op, number) in expected {
352 assert_eq!(op.number(), number, "{op:?}");
353 }
354 assert_eq!(ALL_REGISTER_OPS.len(), expected.len(), "liste exhaustive");
355 }
356
357 #[test]
358 fn sqe_flag_set_maps_to_submit_options() {
359 // SqeFlagSet partage la représentation des drapeaux avec SubmitOptions.
360 let from_set = SqeFlagSet::new()
361 .fixed_file()
362 .drain()
363 .force_async()
364 .buffer_select()
365 .skip_cqe_on_success()
366 .into_options();
367 let direct = SubmitOptions::default()
368 .fixed_file()
369 .drain()
370 .force_async()
371 .buffer_select()
372 .skip_cqe_on_success();
373 assert_eq!(from_set, direct);
374 }
375
376 proptest::proptest! {
377 /// **Propriété (§8)** : pour tout sous-ensemble d'opcodes autorisés, la
378 /// liste blanche encode **exactement** ces opcodes (un `RESTRICTION_SQE_OP`
379 /// par opcode, dans l'ordre, sans extra) — ni plus, ni moins. Le comportement
380 /// kernel default-deny est, lui, prouvé par les tests d'intégration et de
381 /// sécurité ci-dessous (un opcode hors liste **échoue** réellement).
382 #[test]
383 fn whitelisted_ops_encode_exactly_those(
384 mask in proptest::collection::vec(proptest::bool::ANY, CANDIDATE_OPS.len())
385 ) {
386 let subset: Vec<IoUringOpcode> = CANDIDATE_OPS
387 .iter()
388 .zip(&mask)
389 .filter(|(_, keep)| **keep)
390 .map(|(op, _)| *op)
391 .collect();
392 let set = RestrictionSet::from_entitlements(subset.iter().copied());
393 proptest::prop_assert_eq!(set.len(), subset.len());
394 for (restriction, expected) in set.as_slice().iter().zip(&subset) {
395 let encoded = encode_restriction(*restriction);
396 proptest::prop_assert_eq!(encoded.opcode, raw::IORING_RESTRICTION_SQE_OP);
397 proptest::prop_assert_eq!(encoded.op_or_flags, opcode_number(*expected));
398 }
399 }
400 }
401
402 // ── Intégration kernel ────────────────────────────────────────────────
403
404 /// Tous les types de restriction à la fois : `build()` applique réellement
405 /// `REGISTER_RESTRICTIONS` (4 variantes encodées) ; après `enable`, l'opcode
406 /// autorisé (Nop) passe.
407 #[test]
408 #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
409 fn confined_ring_with_all_restriction_kinds_runs_whitelisted_op() {
410 let policy = RestrictionSet::new()
411 .allow_op(IoUringOpcode::Nop)
412 .allow_register(RegisterOp::Probe)
413 .allow_sqe_flags(SqeFlagSet::new().force_async())
414 .require_sqe_flags(SqeFlagSet::new()); // aucun drapeau imposé
415 let mut ring = IoUringBuilder::new(nz(8))
416 .restrict(policy.as_slice())
417 .build()
418 .expect("ring R_DISABLED + REGISTER_RESTRICTIONS");
419 ring.enable().expect("enable du ring confiné");
420 // Nop est en liste blanche → passe.
421 let tok = ring.submit_nop().expect("submit_nop autorisé");
422 ring.submit_and_wait(1).expect("submit_and_wait");
423 let completion = ring.wait_completion().expect("completion");
424 assert_eq!(completion.token(), tok);
425 completion.completed().expect("nop autorisé réussit");
426 assert_eq!(ring.in_flight(), 0);
427 }
428
429 /// **Preuve de sécurité (§8)** : un `openat2` soumis via un ring « réseau
430 /// seul » est **refusé** par le kernel, **alors même que le process a le droit
431 /// d'ouvrir le fichier**. Ferme la voie de contournement de seccomp (§1).
432 #[test]
433 #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
434 fn openat2_refused_on_network_only_ring() {
435 // Fichier que le process PEUT ouvrir hors ring (preuve que le refus vient
436 // bien de la restriction du ring, pas d'un défaut de permission FS).
437 let path = std::env::temp_dir().join(format!("air-iou3f-{}", std::process::id()));
438 std::fs::write(&path, b"secret").expect("création fixture");
439 assert!(
440 std::fs::File::open(&path).is_ok(),
441 "le process a bien le droit d'ouvrir le fichier hors ring"
442 );
443
444 // Ring « réseau seul » : openat2 n'est PAS en liste blanche.
445 let policy = RestrictionSet::new()
446 .allow_op(IoUringOpcode::Socket)
447 .allow_op(IoUringOpcode::Connect)
448 .allow_op(IoUringOpcode::Send)
449 .allow_op(IoUringOpcode::Recv)
450 .allow_op(IoUringOpcode::Close);
451 let mut ring = IoUringBuilder::new(nz(8))
452 .restrict(policy.as_slice())
453 .build()
454 .expect("ring réseau seul");
455 ring.enable().expect("enable");
456
457 // Un opcode autorisé (socket) passe — le ring est fonctionnel.
458 let sock_tok = ring
459 .submit_socket(SocketDomain::Ipv4, SocketType::Stream, 0)
460 .expect("submit_socket autorisé");
461 ring.submit_and_wait(1).expect("submit_and_wait socket");
462 let sock = ring.wait_completion().expect("completion socket");
463 assert_eq!(sock.token(), sock_tok);
464 sock.completed().expect("socket autorisé réussit");
465
466 // openat2 (hors liste) → REFUSÉ par le kernel, malgré le droit FS.
467 let open_tok = ring
468 .submit_openat2(DirFd::Cwd, cpath(&path), OpenHow::default())
469 .expect("submit_openat2 (la soumission elle-même est acceptée)");
470 ring.submit_and_wait(1).expect("submit_and_wait openat2");
471 let opened = ring.wait_completion().expect("completion openat2");
472 assert_eq!(opened.token(), open_tok);
473 let err = opened
474 .completed()
475 .expect_err("openat2 doit être REFUSÉ sur un ring réseau seul");
476 assert!(
477 matches!(err, Errno::EACCES | Errno::EINVAL),
478 "refus default-deny attendu (EACCES/EINVAL), obtenu {err:?}"
479 );
480
481 let _ = std::fs::remove_file(&path);
482 }
483
484 /// `require_sqe_flags` imposé : une soumission **sans** le drapeau requis est
485 /// refusée par le kernel.
486 #[test]
487 #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
488 fn required_sqe_flag_missing_is_refused() {
489 let policy = RestrictionSet::new()
490 .allow_op(IoUringOpcode::Nop)
491 .require_sqe_flags(SqeFlagSet::new().fixed_file());
492 let mut ring = IoUringBuilder::new(nz(8))
493 .restrict(policy.as_slice())
494 .build()
495 .expect("ring");
496 ring.enable().expect("enable");
497 // Nop SANS IOSQE_FIXED_FILE alors qu'il est requis → refusé.
498 let tok = ring.submit_nop().expect("submit_nop (soumission acceptée)");
499 ring.submit_and_wait(1).expect("submit_and_wait");
500 let completion = ring.wait_completion().expect("completion");
501 assert_eq!(completion.token(), tok);
502 let err = completion
503 .completed()
504 .expect_err("drapeau requis manquant → refus");
505 assert!(
506 matches!(err, Errno::EACCES | Errno::EINVAL),
507 "refus drapeau requis attendu, obtenu {err:?}"
508 );
509 }
510
511 /// Soumission **avant** `enable` : refusée (le ring `R_DISABLED` ne soumet rien).
512 #[test]
513 #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
514 fn submission_before_enable_is_refused() {
515 let policy = RestrictionSet::new().allow_op(IoUringOpcode::Nop);
516 let mut ring = IoUringBuilder::new(nz(8))
517 .restrict(policy.as_slice())
518 .build()
519 .expect("ring R_DISABLED");
520 // PAS de enable(). La préparation du SQE réussit ; la soumission échoue.
521 ring.submit_nop().expect("préparation du SQE");
522 let err = ring
523 .submit()
524 .expect_err("soumission sur un ring désactivé doit échouer");
525 // EBADFD (77) sur ring désactivé : pas un Errno nommé → on vérifie le code.
526 assert_eq!(
527 err.as_raw(),
528 77,
529 "ring désactivé : EBADFD attendu, obtenu {err:?}"
530 );
531 }
532
533 /// **Immuabilité** après `enable` : l'API ne propose aucune re-restriction
534 /// (garantie par construction — `restrict` n'est que sur le builder) ; on
535 /// prouve ici que le **kernel** la refuse aussi (défense en profondeur) en
536 /// retentant `REGISTER_RESTRICTIONS` sur un ring déjà activé.
537 #[test]
538 #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
539 fn restrictions_are_immutable_after_enable() {
540 let policy = RestrictionSet::new().allow_op(IoUringOpcode::Nop);
541 let mut ring = IoUringBuilder::new(nz(8))
542 .restrict(policy.as_slice())
543 .build()
544 .expect("ring");
545 ring.enable().expect("enable");
546
547 // Re-restriction d'un ring ACTIF, au niveau register brut.
548 let entry = encode_restriction(Restriction::AllowOp(IoUringOpcode::Read));
549 let arr = [entry];
550 // SAFETY: `arr` est un tableau d'une `io_uring_restriction` valide et
551 // vivante pour la durée de l'appel ; `fd_raw()` est le ring fd possédé.
552 // On ATTEND un échec (ring déjà activé) — aucune mutation d'état effective.
553 let ret = unsafe {
554 syscall::register(
555 ring.fd_raw(),
556 raw::IORING_REGISTER_RESTRICTIONS,
557 arr.as_ptr() as u64,
558 1,
559 )
560 };
561 assert!(
562 ret < 0,
563 "re-restriction d'un ring déjà activé doit être refusée par le kernel"
564 );
565 }
566
567 /// Couverture du bras d'erreur de `apply_restrictions` : si
568 /// `REGISTER_RESTRICTIONS` échoue (ici injecté), `build()` remonte l'`Errno`
569 /// et le ring `R_DISABLED` se ferme proprement (pas de fuite de FD).
570 #[test]
571 #[cfg_attr(miri, ignore = "io_uring_setup réel requis (register injecté)")]
572 fn build_propagates_register_restrictions_error() {
573 syscall::sim::clear();
574 // `setup` réussit (vrai syscall) ; `register` échoue (injecté -EINVAL).
575 syscall::sim::inject(syscall::sim::Syscall::Register, -22);
576 let policy = RestrictionSet::new().allow_op(IoUringOpcode::Nop);
577 let result = IoUringBuilder::new(nz(8))
578 .restrict(policy.as_slice())
579 .build();
580 assert_eq!(result.err(), Some(Errno::EINVAL));
581 syscall::sim::clear();
582 }
583}