air_sys_syscall/io_uring/mod.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//! Module `air-sys-syscall::io_uring` — façade typée d'io_uring (cible 6.12).
6//!
7//! Niveau d'abstraction 2 (ADR-022, Décision 1) : soumission/complétion typée.
8//! Le niveau 1 (anneaux bruts) vivra dans [`raw`]. Les buffers suivent le
9//! modèle de transfert d'ownership (ADR-022, Décision 3), garés dans un slab
10//! pré-alloué (S1) ; le téardown est sûr par `Drop` quiescent +
11//! [`IoUring::shutdown`] (S2).
12//!
13//! **Périmètre — Temps 1 (cœur).** Cycle de vie du ring (setup flags, mmaps SQ/
14//! CQ/SQE, négociation des features), slab d'opérations en vol (S1), protocole
15//! d'anneau acquire/release (§3.2 de la spec), soumission/complétion,
16//! [`Completion`], téardown sûr (S2), introspection (probe + capabilities). Les
17//! opérations métier `submit_*` (Temps 2a–2d) et les sous-modules
18//! ([`shared`], [`multishot`], [`sandbox`], registration/provided/linked/cmd)
19//! relèvent de PRs ultérieures.
20//!
21//! Référence normative : `docs/specs/layer-0/io-uring-1-core.md` (contrat) et
22//! `docs/specs/layer-0/io-uring-0-inventaire.md` §7 (squelette).
23//!
24//! Les trois syscalls sous-jacents — `io_uring_setup` (425), `io_uring_enter`
25//! (426), `io_uring_register` (427), numéros identiques x86_64/ARM64 — sont
26//! appelés directement via `core::arch::asm!` (aucune dépendance externe).
27
28use crate::mem::{Mapping, mmap_file};
29use air_sys_types::Errno;
30use air_sys_types::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
31use air_sys_types::mem::{MapFlags, ProtectionFlags};
32use alloc::boxed::Box;
33use alloc::vec::Vec;
34use core::marker::PhantomData;
35use core::num::NonZeroU32;
36use core::time::Duration;
37
38pub mod multishot;
39pub mod raw;
40pub mod sandbox;
41pub mod shared;
42
43mod async_ops;
44mod cmd;
45mod fs_ops;
46mod linked;
47mod net_ops;
48mod owned;
49mod provided;
50mod registration;
51mod ring;
52mod slab;
53mod syscall;
54
55use owned::OwnedOp;
56use ring::{CompletionRing, SubmissionRing};
57use slab::InflightSlab;
58
59// Types de données purs (bitflags, opcodes) : définis dans `air-sys-types`,
60// re-exportés ici pour offrir une façade `air_sys_syscall::io_uring::*` unique.
61pub use air_sys_types::io_uring::{CompletionFlags, IoUringOpcode, SetupFlags, UringCmdFlags};
62
63// Type d'attente `futex_waitv` (porte une `MmapRegion`) : défini avec ses
64// façades dans `async_ops`, re-exporté sur la façade `io_uring`.
65pub use async_ops::FutexWaiter;
66
67// Trait des commandes passthrough `URING_CMD` (Temps 2d), défini avec ses
68// façades dans `cmd`, re-exporté sur la façade `io_uring`.
69pub use cmd::UringCommand;
70
71// Surface du Temps 3a (registration : ressources fixes), définie dans
72// `registration`, re-exportée sur la façade `io_uring`.
73pub use registration::{
74 ClockSource, FixedFdTable, FixedSlot, FixedSlotTarget, NapiConfig, Personality,
75 RegisteredBufferSlice, RegisteredBuffers, WorkQueueWorkerLimits,
76};
77
78// Surface du Temps 3b (buffers fournis ring-mapped), définie dans `provided`,
79// re-exportée sur la façade `io_uring`.
80pub use provided::{
81 ProvidedBuffer, ProvidedBufferRing, ProvidedBufferRingOptions, ProvidedBufferRingStatus,
82};
83
84// Surface du Temps 3c (opérations liées), définie dans `linked`, re-exportée sur
85// la façade `io_uring`.
86pub use linked::{ChainTokens, LinkedChainBuilder};
87
88// Surface du Temps 3e (usage multi-thread), définie dans `shared`, re-exportée
89// sur la façade `io_uring`.
90pub use shared::{LockedIoUring, RingHandle, RingPool, SqpollIoUring};
91
92// Surface du Temps 3f (confinement / sandbox), définie dans `sandbox`,
93// re-exportée sur la façade `io_uring`.
94pub use sandbox::{RegisterOp, RestrictionSet, SqeFlagSet};
95
96// Surface du Temps 4 (accès brut niveau 1), définie dans `raw`, re-exportée sur
97// la façade `io_uring`. `raw` reste par ailleurs le module des miroirs ABI.
98pub use raw::{RAW_USER_DATA_TAG, RawCompletionQueueEntry, RawOpcode, RawSubmissionQueueEntry};
99
100// ---------------------------------------------------------------------------
101// Construction & cycle de vie
102// ---------------------------------------------------------------------------
103
104/// Anneau io_uring : FD + mmaps (SQ/CQ/SQE) + slab d'opérations en vol (S1) +
105/// capabilities négociées.
106///
107/// `Send` mais **pas** `Sync` (ADR-022, Décision 6) : un reactor par thread.
108/// Pour le multi-thread, voir [`shared`]. L'invariant de sûreté (S1/S2) : tant
109/// qu'une opération est en vol, son buffer vit dans le slot du slab et le ring
110/// ne peut être détruit sans quiescence — aucune écriture kernel ne tombe sur
111/// de la mémoire libérée.
112pub struct IoUring {
113 /// FD du ring. `Option` pour la **discipline de téardown** : libéré (close)
114 /// après quiescence, ou *fuité* (`forget`) si la quiescence échoue.
115 fd: Option<OwnedFd>,
116 /// Mmap de l'anneau SQ (et de la CQ si `SINGLE_MMAP`). `Option` : voir `fd`.
117 sq_ring_map: Option<Mapping>,
118 /// Mmap du tableau de SQE. `Option` : voir `fd`.
119 sqes_map: Option<Mapping>,
120 /// Mmap de l'anneau CQ, ou `None` si partagée avec `sq_ring_map`
121 /// (`IORING_FEAT_SINGLE_MMAP`, toujours présent en 6.12).
122 cq_ring_map: Option<Mapping>,
123 /// Pointeurs/cache de l'anneau de soumission (dans `sq_ring_map`).
124 sq: SubmissionRing,
125 /// Pointeurs/cache de l'anneau de complétion.
126 cq: CompletionRing,
127 /// Slab d'opérations en vol (S1).
128 slab: InflightSlab,
129 /// Features négociées au setup (axe G).
130 capabilities: IoUringCapabilities,
131 /// Opcodes supportés par le kernel courant, indexés par numéro `IORING_OP_*`
132 /// (mis en cache à la construction via `IORING_REGISTER_PROBE`, §9).
133 supported_ops: [bool; 256],
134 /// Options appliquées à la prochaine soumission ([`IoUring::with`]).
135 pending_options: SubmitOptions,
136 /// Personality appliquée à la prochaine soumission ([`IoUring::with_personality`]) :
137 /// `0` = credentials du process (aucune personality). Posée dans
138 /// `sqe.personality` puis remise à `0` (Temps 3a, registration §6).
139 pending_personality: u16,
140 /// Index du ring fd enregistré ([`IoUring::register_ring_fd`], Temps 3a §4),
141 /// ou `None`. Quand présent, les `io_uring_enter` suivants l'utilisent avec
142 /// `IORING_ENTER_REGISTERED_RING` (pas de résolution de FD).
143 enter_ring_index: Option<u32>,
144 /// `true` une fois le téardown effectué (par `shutdown` ou un `Drop`
145 /// précédent) : empêche toute double libération (reminder S2).
146 disposed: bool,
147 /// Indices (`tail`) des SQE préparés via [`IoUring::raw_get_submission_queue_entry`]
148 /// (Temps 4) depuis la dernière soumission, pour la **vérification debug** du
149 /// tag `user_data` (§5). Vide tant que l'accès brut n'est pas utilisé (aucune
150 /// allocation sur le happy path niveau 2) ; purgé à chaque `submit`.
151 raw_pending: Vec<u32>,
152 /// Marqueur `Send + !Sync` : `Cell<()>` est `Send` mais pas `Sync`, ce qui
153 /// impose le « un reactor par thread » de la Décision 6 sans verrou.
154 _not_sync: PhantomData<core::cell::Cell<()>>,
155}
156
157// SAFETY: `IoUring` est **`Send`** (ADR-022 Décision 6) : il peut être *déplacé*
158// d'un thread à l'autre. Ses pointeurs bruts (`SubmissionRing`/`CompletionRing`)
159// désignent des zones **mmappées** valides pour toute la durée de vie du ring
160// (process-global, indépendantes du thread) ; le FD est process-global ; le slab
161// S1 et les payloads possédés (`OwnedFd`/`Vec`/`Box`) sont tous `Send`. Déplacer
162// le ring **transfère l'ownership exclusif** — `Send` concerne le transfert, pas
163// le partage. Le partage concurrent (`Sync`) reste **interdit** : `_not_sync`
164// (`PhantomData<Cell<()>>`) garde `IoUring: !Sync`, car le protocole d'ordering
165// du Temps 1 ne synchronise pas userspace↔userspace (cf. `shared` §1). Un seul
166// thread accède donc au ring à un instant donné (Send + !Sync = thread-per-core).
167unsafe impl Send for IoUring {}
168
169/// Construit un [`IoUring`] : applique setup flags et restrictions (S3) avant
170/// l'activation, puis `io_uring_setup(2)`.
171pub struct IoUringBuilder {
172 entries: NonZeroU32,
173 cq_entries: Option<NonZeroU32>,
174 max_inflight: Option<NonZeroU32>,
175 flags: SetupFlags,
176 sqpoll_idle: Option<Duration>,
177 sqpoll_cpu: Option<u32>,
178 attach_wq_fd: Option<i32>,
179 restrictions: Vec<Restriction>,
180}
181
182impl IoUringBuilder {
183 /// Démarre un builder pour `entries` SQE (arrondi par le kernel à la
184 /// puissance de 2 supérieure).
185 #[must_use]
186 pub fn new(entries: NonZeroU32) -> Self {
187 Self {
188 entries,
189 cq_entries: None,
190 max_inflight: None,
191 flags: SetupFlags::empty(),
192 sqpoll_idle: None,
193 sqpoll_cpu: None,
194 attach_wq_fd: None,
195 restrictions: Vec::new(),
196 }
197 }
198
199 /// Fixe explicitement la profondeur de la CQ (`IORING_SETUP_CQSIZE`).
200 #[must_use]
201 pub fn with_completion_queue_entries(mut self, entries: NonZeroU32) -> Self {
202 self.cq_entries = Some(entries);
203 self
204 }
205
206 /// Capacité du slab d'opérations en vol (S1). Défaut : `cq_entries`.
207 #[must_use]
208 pub fn max_inflight(mut self, n: NonZeroU32) -> Self {
209 self.max_inflight = Some(n);
210 self
211 }
212
213 /// Active des flags de setup (cf. [`SetupFlags`]).
214 #[must_use]
215 pub fn with_flags(mut self, flags: SetupFlags) -> Self {
216 self.flags |= flags;
217 self
218 }
219
220 /// Durée d'inactivité avant que le thread `SQPOLL` ne s'endorme
221 /// (`SETUP_SQPOLL`, Temps 3e).
222 #[must_use]
223 pub fn with_sqpoll_idle(mut self, idle: Duration) -> Self {
224 self.sqpoll_idle = Some(idle);
225 self
226 }
227
228 /// Épingle le thread `SQPOLL` sur un CPU (`SETUP_SQ_AFF`).
229 #[must_use]
230 pub fn with_sqpoll_cpu(mut self, cpu: u32) -> Self {
231 self.sqpoll_cpu = Some(cpu);
232 self
233 }
234
235 /// Partage le pool io-wq d'un ring existant (`SETUP_ATTACH_WQ`).
236 #[must_use]
237 pub fn attach_work_queue(mut self, other: &IoUring) -> Self {
238 self.attach_wq_fd = Some(other.fd_raw());
239 self
240 }
241
242 /// Crée le ring désactivé (`R_DISABLED`) et applique des restrictions (S3,
243 /// Temps 3f). Doit être suivi de [`IoUring::enable`].
244 #[must_use]
245 pub fn restrict(mut self, restrictions: &[Restriction]) -> Self {
246 self.restrictions = restrictions.to_vec();
247 self
248 }
249
250 /// Finalise : traduit la config en `io_uring_params`, appelle
251 /// `io_uring_setup(2)`, mmappe les anneaux (§3.1), alloue le slab (§4) et
252 /// lit les features. Si `restrict` a été utilisé, le ring est créé
253 /// désactivé et doit être activé via [`IoUring::enable`].
254 ///
255 /// # Errors
256 ///
257 /// - [`Errno::EINVAL`] : paramètres ou combinaison de flags invalides
258 /// (remontés tels quels, sans masquage).
259 /// - [`Errno::ENOMEM`] : mémoire insuffisante pour les anneaux/le slab.
260 /// - [`Errno::EPERM`] : `SQPOLL`/affinité sans privilège selon la config.
261 /// - [`Errno::EFAULT`] : pointeur de paramètres invalide.
262 /// - [`Errno::ENOSYS`] : io_uring indisponible (kernel ancien, sandbox,
263 /// container durci) — **pas** de fallback caché (cf. ADR-022 D10).
264 pub fn build(self) -> Result<IoUring, Errno> {
265 let mut params = raw::IoUringParams {
266 flags: self.flags.bits(),
267 ..raw::IoUringParams::default()
268 };
269 if let Some(cq) = self.cq_entries {
270 params.flags |= raw::IORING_SETUP_CQSIZE;
271 params.cq_entries = cq.get();
272 }
273 if let Some(idle) = self.sqpoll_idle {
274 params.sq_thread_idle = u32::try_from(idle.as_millis()).unwrap_or(u32::MAX);
275 }
276 if let Some(cpu) = self.sqpoll_cpu {
277 params.sq_thread_cpu = cpu;
278 }
279 if let Some(wq_fd) = self.attach_wq_fd {
280 params.flags |= raw::IORING_SETUP_ATTACH_WQ;
281 params.wq_fd = u32::from_ne_bytes(wq_fd.to_ne_bytes());
282 }
283 if !self.restrictions.is_empty() {
284 params.flags |= SetupFlags::R_DISABLED.bits();
285 }
286
287 let params_ptr = core::ptr::from_mut(&mut params) as u64;
288 // SAFETY: `params_ptr` pointe une `IoUringParams` valide en lecture/
289 // écriture pour la durée de l'appel ; `entries` est un scalaire.
290 let ret = unsafe { syscall::setup(self.entries.get(), params_ptr) };
291 if ret < 0 {
292 return Err(raw::errno_from_negative_syscall_ret(ret));
293 }
294 let raw_fd = i32::try_from(ret).map_err(|_| Errno::EINVAL)?;
295 // SAFETY: `io_uring_setup` a retourné un FD frais possédé.
296 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
297
298 if !self.restrictions.is_empty() {
299 // Ring créé `R_DISABLED` ci-dessus : applique la liste blanche S3
300 // (`REGISTER_RESTRICTIONS`, Temps 3f, sous-module `sandbox`) **tant
301 // qu'il est désactivé** (le kernel refuse sinon). En cas d'échec, `fd`
302 // (OwnedFd local) se ferme proprement au retour — aucune fuite. Le
303 // ring reste désactivé : l'appelant l'active via `IoUring::enable`,
304 // après quoi les restrictions sont **immuables** (imposé kernel).
305 apply_restrictions(&fd, &self.restrictions)?;
306 }
307
308 let mappings = map_rings(&fd, ¶ms, self.flags)?;
309 let slab_capacity = self.max_inflight.map_or(params.cq_entries, NonZeroU32::get);
310 let slab_capacity = NonZeroU32::new(slab_capacity).ok_or(Errno::EINVAL)?;
311 let supported_ops = probe_supported_ops(fd.as_raw_fd());
312
313 Ok(IoUring {
314 fd: Some(fd),
315 sq_ring_map: Some(mappings.sq_ring),
316 sqes_map: Some(mappings.sqes),
317 cq_ring_map: mappings.cq_ring,
318 sq: mappings.sq,
319 cq: mappings.cq,
320 slab: InflightSlab::with_capacity(slab_capacity),
321 capabilities: IoUringCapabilities {
322 features: params.features,
323 },
324 supported_ops,
325 pending_options: SubmitOptions::default(),
326 pending_personality: 0,
327 enter_ring_index: None,
328 disposed: false,
329 raw_pending: Vec::new(),
330 _not_sync: PhantomData,
331 })
332 }
333}
334
335/// Mappings et holders d'anneaux résultant de la construction.
336struct RingMappings {
337 sq_ring: Mapping,
338 sqes: Mapping,
339 cq_ring: Option<Mapping>,
340 sq: SubmissionRing,
341 cq: CompletionRing,
342}
343
344/// mmappe les trois zones (§3.1) et construit les holders de pointeurs.
345/// Tailles de mmap des trois zones, dérivées des `params` retournés par le
346/// kernel. **Décode PUR** (aucun syscall) — frontière de décode de données
347/// externes (Principe 3), fuzzée via [`fuzz_api`]. Arithmétique **checked** :
348/// toute donnée kernel incohérente (offsets/entrées énormes) ⇒ `EINVAL`, jamais
349/// d'overflow ni de panic.
350struct RingSizes {
351 single_mmap: bool,
352 sq_map: usize,
353 cq_map: usize,
354 sqes: usize,
355 /// Taille d'un SQE en octets (64, ou 128 si `SETUP_SQE128`).
356 sqe_size: usize,
357}
358
359fn ring_sizes(params: &raw::IoUringParams, flags: SetupFlags) -> Result<RingSizes, Errno> {
360 let single_mmap = params.features & raw::IORING_FEAT_SINGLE_MMAP != 0;
361 let sqe_size: usize = if flags.contains(SetupFlags::SQE128) {
362 128
363 } else {
364 64
365 };
366 let cqe_size: usize = if flags.contains(SetupFlags::CQE32) {
367 32
368 } else {
369 16
370 };
371
372 let sq_entries = usize_of(params.sq_entries);
373 let cq_entries = usize_of(params.cq_entries);
374 let sq_ring_sz = usize_of(params.sq_off.array)
375 .checked_add(sq_entries.checked_mul(4).ok_or(Errno::EINVAL)?)
376 .ok_or(Errno::EINVAL)?;
377 let cq_ring_sz = usize_of(params.cq_off.cqes)
378 .checked_add(cq_entries.checked_mul(cqe_size).ok_or(Errno::EINVAL)?)
379 .ok_or(Errno::EINVAL)?;
380 let sqes = sq_entries.checked_mul(sqe_size).ok_or(Errno::EINVAL)?;
381
382 let (sq_map, cq_map) = if single_mmap {
383 let m = sq_ring_sz.max(cq_ring_sz);
384 (m, m)
385 } else {
386 (sq_ring_sz, cq_ring_sz)
387 };
388 Ok(RingSizes {
389 single_mmap,
390 sq_map,
391 cq_map,
392 sqes,
393 sqe_size,
394 })
395}
396
397fn map_rings(
398 fd: &OwnedFd,
399 params: &raw::IoUringParams,
400 flags: SetupFlags,
401) -> Result<RingMappings, Errno> {
402 let sizes = ring_sizes(params, flags)?;
403 let single_mmap = sizes.single_mmap;
404
405 let prot = ProtectionFlags::READ | ProtectionFlags::WRITE;
406 let map = MapFlags::SHARED | MapFlags::POPULATE;
407
408 let mut sq_ring = mmap_file(fd.as_fd(), sizes.sq_map, raw::IORING_OFF_SQ_RING, prot, map)?;
409 let mut cq_ring = if single_mmap {
410 None
411 } else {
412 Some(mmap_file(
413 fd.as_fd(),
414 sizes.cq_map,
415 raw::IORING_OFF_CQ_RING,
416 prot,
417 map,
418 )?)
419 };
420 let mut sqes = mmap_file(fd.as_fd(), sizes.sqes, raw::IORING_OFF_SQES, prot, map)?;
421
422 let sq_base = sq_ring.as_mut_ptr();
423 let cq_base = cq_ring.as_mut().map_or(sq_base, Mapping::as_mut_ptr);
424 let sqes_base = sqes.as_mut_ptr();
425 let sq_mask = params.sq_entries.wrapping_sub(1);
426 let cq_mask = params.cq_entries.wrapping_sub(1);
427
428 // SAFETY: les trois bases pointent des mmaps fraîches dimensionnées selon
429 // les offsets kernel de `params` ; elles vivent dans l'`IoUring` (RAII) au
430 // moins aussi longtemps que les holders qui en dérivent les pointeurs.
431 let sq = unsafe {
432 SubmissionRing::new(
433 sq_base,
434 sqes_base,
435 sizes.sqe_size,
436 ¶ms.sq_off,
437 sq_mask,
438 params.sq_entries,
439 )
440 };
441 // SAFETY: idem pour l'anneau CQ (zone propre ou partagée si SINGLE_MMAP).
442 let cq = unsafe { CompletionRing::new(cq_base, ¶ms.cq_off, cq_mask) };
443
444 Ok(RingMappings {
445 sq_ring,
446 sqes,
447 cq_ring,
448 sq,
449 cq,
450 })
451}
452
453/// `u32` → `usize` (toujours valide sur cible LP64).
454fn usize_of(v: u32) -> usize {
455 usize::try_from(v).expect("usize ≥ u32 sur cible LP64")
456}
457
458/// Encode les [`Restriction`] en `struct io_uring_restriction` et les applique
459/// via `IORING_REGISTER_RESTRICTIONS` (S3, Temps 3f, sous-module [`sandbox`]).
460/// À n'appeler **que** sur un ring `R_DISABLED` (avant [`IoUring::enable`]) — le
461/// kernel refuse autrement. **Default-deny** : dès qu'un opcode est mis en liste
462/// blanche, le kernel refuse tout le reste (`-EACCES`).
463fn apply_restrictions(fd: &OwnedFd, restrictions: &[Restriction]) -> Result<(), Errno> {
464 let encoded: Vec<raw::IoUringRestriction> = restrictions
465 .iter()
466 .copied()
467 .map(encode_restriction)
468 .collect();
469 let nr_args = u32::try_from(encoded.len()).map_err(|_| Errno::EINVAL)?;
470 let arg = encoded.as_ptr() as u64;
471 // SAFETY: `arg` pointe un tableau de `nr_args` `io_uring_restriction` valides,
472 // vivants pour la durée de l'appel ; `fd` est un ring fd `R_DISABLED` possédé.
473 // `REGISTER_RESTRICTIONS` lit l'argument sans le réécrire.
474 let ret = unsafe {
475 syscall::register(
476 fd.as_raw_fd(),
477 raw::IORING_REGISTER_RESTRICTIONS,
478 arg,
479 nr_args,
480 )
481 };
482 if ret < 0 {
483 return Err(raw::errno_from_negative_syscall_ret(ret));
484 }
485 Ok(())
486}
487
488/// Traduit une [`Restriction`] en son `struct io_uring_restriction` : un opcode
489/// de **type** de restriction + un `u8` selon ce type (numéro d'opcode de
490/// soumission via [`opcode_number`], numéro de register-op, ou octet `IOSQE_*`).
491fn encode_restriction(restriction: Restriction) -> raw::IoUringRestriction {
492 let mut out = raw::IoUringRestriction::default();
493 match restriction {
494 Restriction::AllowOp(op) => {
495 out.opcode = raw::IORING_RESTRICTION_SQE_OP;
496 out.op_or_flags = opcode_number(op);
497 }
498 Restriction::AllowRegister(register_op) => {
499 out.opcode = raw::IORING_RESTRICTION_REGISTER_OP;
500 out.op_or_flags = register_op;
501 }
502 Restriction::SqeFlagsAllowed(options) => {
503 out.opcode = raw::IORING_RESTRICTION_SQE_FLAGS_ALLOWED;
504 out.op_or_flags = options.iosqe_flags();
505 }
506 Restriction::SqeFlagsRequired(options) => {
507 out.opcode = raw::IORING_RESTRICTION_SQE_FLAGS_REQUIRED;
508 out.op_or_flags = options.iosqe_flags();
509 }
510 }
511 out
512}
513
514impl IoUring {
515 /// Raccourci : `IoUringBuilder::new(entries).build()`.
516 ///
517 /// # Errors
518 ///
519 /// Voir [`IoUringBuilder::build`].
520 pub fn new(entries: NonZeroU32) -> Result<Self, Errno> {
521 IoUringBuilder::new(entries).build()
522 }
523
524 /// Active un ring créé avec `R_DISABLED` (`REGISTER_ENABLE_RINGS`).
525 ///
526 /// Inutile sur un ring déjà actif : retourne alors l'`EINVAL` du kernel.
527 ///
528 /// # Errors
529 ///
530 /// - [`Errno::EINVAL`] : ring déjà actif (remonté tel quel).
531 /// - [`Errno::EBADF`] : FD de ring invalide.
532 pub fn enable(&mut self) -> Result<(), Errno> {
533 // SAFETY: `fd` est un ring fd valide possédé ; `ENABLE_RINGS` ne lit
534 // aucune mémoire utilisateur (arg/nr_args nuls).
535 let ret =
536 unsafe { syscall::register(self.fd_raw(), raw::IORING_REGISTER_ENABLE_RINGS, 0, 0) };
537 if ret < 0 {
538 return Err(raw::errno_from_negative_syscall_ret(ret));
539 }
540 Ok(())
541 }
542
543 /// Vrai si la CQ a débordé (`IORING_SQ_CQ_OVERFLOW`). Avec `FEAT_NODROP`
544 /// (présent en 6.12) le kernel retient les complétions et les re-livre.
545 #[must_use]
546 pub fn completion_queue_overflowed(&self) -> bool {
547 self.sq.flags() & raw::IORING_SQ_CQ_OVERFLOW != 0
548 }
549
550 /// Nombre de places libres dans la SQ (calculé par `tail - head` masqué).
551 #[must_use]
552 pub fn submission_queue_space_left(&self) -> u32 {
553 self.sq.space_left()
554 }
555
556 /// Nombre d'opérations actuellement en vol (slots occupés du slab S1).
557 #[must_use]
558 pub fn in_flight(&self) -> u32 {
559 self.slab.in_flight()
560 }
561
562 /// Nombre de CQE bruts prêts dans la CQ (introspection de test — sert à
563 /// observer un CQE injecté par `msg_ring` côté cible, que `harvest_ready`
564 /// filtrerait comme périmé faute de slot correspondant).
565 #[cfg(test)]
566 pub(crate) fn cq_ready(&self) -> u32 {
567 self.cq.ready()
568 }
569
570 /// FD numérique du ring (toujours présent avant le téardown). `pub(crate)` :
571 /// le sous-module `registration` (Temps 3a) appelle `io_uring_register(2)`
572 /// directement sur ce FD.
573 pub(crate) fn fd_raw(&self) -> i32 {
574 self.fd.as_ref().map_or(-1, AsRawFd::as_raw_fd)
575 }
576
577 /// Cible d'un `io_uring_enter` : `(fd, flags additionnels)`. Avec un ring fd
578 /// enregistré (Temps 3a §4), l'`fd` est l'**index** enregistré et le flag
579 /// `IORING_ENTER_REGISTERED_RING` est OR-é ; sinon le FD ordinaire, sans flag.
580 fn enter_target(&self) -> (i32, u32) {
581 match self.enter_ring_index {
582 // L'index est petit (table de rings enregistrés) ⇒ tient dans un i32.
583 Some(index) => (index.cast_signed(), raw::IORING_ENTER_REGISTERED_RING),
584 None => (self.fd_raw(), 0),
585 }
586 }
587
588 /// Téardown propre (S2) : annule les ops en vol ([`IoUring::sync_cancel`]
589 /// `Any`), draine la CQ jusqu'à `in_flight() == 0`, `munmap` les anneaux et
590 /// ferme le FD. À préférer au `drop` implicite sur chemin chaud.
591 ///
592 /// # Errors
593 ///
594 /// Propage les erreurs de drainage/annulation (l'appelant décide de la
595 /// suite) ; voir [`IoUring::sync_cancel`].
596 pub fn shutdown(mut self) -> Result<(), Errno> {
597 let result = self.do_teardown();
598 // `do_teardown` a posé `disposed = true` et libéré/fuité les ressources.
599 // À la sortie, `self` est dropé → `Drop` voit `disposed` et ne refait
600 // **rien** (pas de double munmap/close/free — reminder S2 #3).
601 result
602 }
603
604 /// Quiescence + libération **ou fuite contrôlée** des ressources (idempotent
605 /// via `disposed`). Cœur de la décision S2 : on ne libère la mémoire que si
606 /// le kernel ne peut plus y écrire (`in_flight == 0`).
607 fn do_teardown(&mut self) -> Result<(), Errno> {
608 if self.disposed {
609 return Ok(());
610 }
611 self.disposed = true;
612 if self.quiesce() {
613 // RELEASE (sound : in_flight == 0, le kernel n'écrit plus) : munmap
614 // des 3 zones, close du fd, par RAII. Ordre : mmaps puis fd.
615 drop(self.sq_ring_map.take());
616 drop(self.sqes_map.take());
617 drop(self.cq_ring_map.take());
618 drop(self.fd.take());
619 Ok(())
620 } else {
621 // FUITE CONTRÔLÉE (Principe 5, reminder S2 #1) : la quiescence a
622 // échoué (op non annulable / timeout) — le kernel peut encore écrire
623 // dans les anneaux mmap et les buffers en vol. On ne libère RIEN
624 // (forget) : une fuite bornée à ce ring est sound, un UAF jamais.
625 leak_forget(self.sq_ring_map.take());
626 leak_forget(self.sqes_map.take());
627 leak_forget(self.cq_ring_map.take());
628 leak_forget(self.fd.take());
629 self.slab.leak_inflight_buffers();
630 Err(Errno::EBUSY)
631 }
632 }
633
634 /// Amène le ring à `in_flight == 0` : annule (best-effort) puis draine les
635 /// complétions (y compris `-ECANCELED`, déroulement **nominal**, reminder
636 /// S2 #2). Borné en temps. Retourne `true` si quiescent.
637 fn quiesce(&mut self) -> bool {
638 if self.slab.in_flight() == 0 {
639 return true;
640 }
641 // (a) Annulation globale (best-effort : ENOENT/ETIME ignorés).
642 let _ = self.sync_cancel(CancelTarget::Any);
643 // (b) Drainage borné jusqu'à in_flight == 0.
644 const MAX_ROUNDS: u32 = 64;
645 for _ in 0..MAX_ROUNDS {
646 // Consomme tout ce qui est prêt (les CQE `-ECANCELED` libèrent leur
647 // slot via `harvest_ready`, sans propager d'erreur).
648 while self.harvest_ready().is_some() {}
649 if self.slab.in_flight() == 0 {
650 return true;
651 }
652 // Bloque (timeout borné) pour au moins une complétion supplémentaire.
653 if self.wait_bounded().is_err() {
654 // EINTR/ETIME : on retente quelques rounds avant d'abandonner.
655 }
656 }
657 self.slab.in_flight() == 0
658 }
659
660 /// `io_uring_enter(GETEVENTS, min_complete = 1)` avec un timeout court borné
661 /// (100 ms via `EXT_ARG`), pour le drainage de quiescence.
662 fn wait_bounded(&self) -> Result<(), Errno> {
663 let ts = raw::KernelTimespec {
664 tv_sec: 0,
665 tv_nsec: 100_000_000,
666 };
667 let arg = raw::IoUringGeteventsArg {
668 sigmask: 0,
669 sigmask_sz: 0,
670 min_wait_usec: 0,
671 ts: core::ptr::from_ref(&ts) as u64,
672 };
673 let arg_sz = u64::try_from(core::mem::size_of::<raw::IoUringGeteventsArg>())
674 .expect("taille getevents_arg ≤ u64");
675 let (efd, eflag) = self.enter_target();
676 // SAFETY: `fd`/index valide ; `arg`/`ts` vivants pour la durée de l'appel.
677 let ret = unsafe {
678 syscall::enter(
679 efd,
680 0,
681 1,
682 raw::IORING_ENTER_GETEVENTS | raw::IORING_ENTER_EXT_ARG | eflag,
683 core::ptr::from_ref(&arg) as u64,
684 arg_sz,
685 )
686 };
687 if ret < 0 {
688 return Err(raw::errno_from_negative_syscall_ret(ret));
689 }
690 Ok(())
691 }
692}
693
694/// Interroge `IORING_REGISTER_PROBE` et retourne, par numéro d'opcode, le
695/// support du kernel courant (mis en cache par [`IoUringBuilder::build`], §9).
696///
697/// Best-effort : si le probe échoue (kernel ancien, sandbox/`R_DISABLED`), tous
698/// les opcodes sont marqués **non supportés** — conservateur, force le fallback
699/// synchrone (ADR-022 D8).
700fn probe_supported_ops(fd: i32) -> [bool; 256] {
701 let mut probe = Box::new(raw::IoUringProbe {
702 last_op: 0,
703 ops_len: 0,
704 resv: 0,
705 resv2: [0; 3],
706 ops: [raw::IoUringProbeOp::default(); 256],
707 });
708 let probe_ptr = core::ptr::from_mut(&mut *probe) as u64;
709 // SAFETY: `fd` ring valide ; `probe` pointe une `io_uring_probe` vivante,
710 // dimensionnée pour `IO_URING_PROBE_OPS` entrées, écrite par le kernel.
711 let ret = unsafe {
712 syscall::register(
713 fd,
714 raw::IORING_REGISTER_PROBE,
715 probe_ptr,
716 raw::IO_URING_PROBE_OPS,
717 )
718 };
719 let mut supported = [false; 256];
720 if ret < 0 {
721 return supported;
722 }
723 for (op, entry) in probe.ops.iter().enumerate() {
724 supported[op] = entry.flags & raw::IO_URING_OP_SUPPORTED != 0;
725 }
726 supported
727}
728
729/// Fuite contrôlée d'une ressource RAII optionnelle (`forget` si présente).
730fn leak_forget<T>(resource: Option<T>) {
731 if let Some(value) = resource {
732 core::mem::forget(value);
733 }
734}
735
736impl Drop for IoUring {
737 /// Filet de sécurité (S2) : si des ops sont en vol, quiesce (annule +
738 /// draine, **bloquant et best-effort**, boucle bornée) avant de libérer la
739 /// mémoire. Coût (blocage potentiel) **assumé et documenté** : sur chemin
740 /// chaud, préférer [`IoUring::shutdown`]. Un `Drop` qui laisserait le kernel
741 /// écrire dans de la mémoire libérée serait un défaut de soundness
742 /// inacceptable en couche 0 (Principe 5 : sur-sécuriser puis dégraisser).
743 fn drop(&mut self) {
744 // Filet de sécurité S2 : quiesce (annule + draine, **bloquant**,
745 // best-effort borné) puis libère — ou **fuite** de façon contrôlée si la
746 // quiescence échoue (jamais d'UAF). No-op si `shutdown` a déjà disposé.
747 let _ = self.do_teardown();
748 }
749}
750
751// ---------------------------------------------------------------------------
752// Soumission
753// ---------------------------------------------------------------------------
754
755impl IoUring {
756 /// Soumet les SQE en attente (`io_uring_enter`, sans attente). Retourne le
757 /// nombre de SQE consommés par le kernel.
758 ///
759 /// Publie la queue SQ par un **store release** (§3.2) avant l'`enter`. En
760 /// mode `SQPOLL` chaud, peut n'avoir **aucun** syscall à faire (juste la
761 /// publication), ne réveillant le thread que si `SQ_NEED_WAKEUP`.
762 ///
763 /// # Errors
764 ///
765 /// - [`Errno::EINTR`] : remonté tel quel (ADR-021 conv. 2 ; l'appelant écrit
766 /// sa propre boucle de retry).
767 /// - [`Errno::EAGAIN`] : ressources momentanément indisponibles.
768 /// - [`Errno::EBUSY`] : CQ pleine non drainée (selon contexte).
769 /// - [`Errno::EINVAL`], [`Errno::EFAULT`], [`Errno::EBADF`].
770 pub fn submit(&mut self) -> Result<u32, Errno> {
771 self.debug_assert_raw_tags();
772 let to_submit = self.sq.publish_and_pending();
773 if to_submit == 0 {
774 return Ok(0);
775 }
776 let (efd, eflag) = self.enter_target();
777 // SAFETY: `fd`/index est valide ; pas d'argument étendu.
778 let ret = unsafe { syscall::enter(efd, to_submit, 0, eflag, 0, 0) };
779 if ret < 0 {
780 // EINTR/EAGAIN remontés tels quels (ADR-021 conv. 2 — aucun retry).
781 return Err(raw::errno_from_negative_syscall_ret(ret));
782 }
783 Ok(u32::try_from(ret).unwrap_or(0))
784 }
785
786 /// Soumet puis attend au moins `want` complétions
787 /// (`IORING_ENTER_GETEVENTS`, `min_complete = want`).
788 ///
789 /// # Errors
790 ///
791 /// Mêmes erreurs que [`IoUring::submit`] ; [`Errno::EINTR`] remonté tel quel.
792 pub fn submit_and_wait(&mut self, want: u32) -> Result<u32, Errno> {
793 self.debug_assert_raw_tags();
794 let to_submit = self.sq.publish_and_pending();
795 let (efd, eflag) = self.enter_target();
796 // SAFETY: `fd`/index est valide ; pas d'argument étendu.
797 let ret = unsafe {
798 syscall::enter(
799 efd,
800 to_submit,
801 want,
802 raw::IORING_ENTER_GETEVENTS | eflag,
803 0,
804 0,
805 )
806 };
807 if ret < 0 {
808 // EINTR remonté tel quel (ADR-021 conv. 2).
809 return Err(raw::errno_from_negative_syscall_ret(ret));
810 }
811 Ok(u32::try_from(ret).unwrap_or(0))
812 }
813
814 /// Applique des options par-opération ([`SubmitOptions`]) à la **prochaine**
815 /// soumission `submit_*`. Style builder (chaînable).
816 pub fn with(&mut self, opts: SubmitOptions) -> &mut Self {
817 self.pending_options = opts;
818 self
819 }
820
821 /// Exécute la **prochaine** soumission `submit_*` avec les credentials d'une
822 /// [`Personality`] enregistrée (`sqe.personality`, Temps 3a §6). Consommée
823 /// pour cette seule opération. Style builder (chaînable).
824 pub fn with_personality(&mut self, personality: Personality) -> &mut Self {
825 self.pending_personality = personality.raw();
826 self
827 }
828
829 /// Prépare et met en file un `IORING_OP_NOP` — la première opération de
830 /// validation du cœur. Réserve un slot S1, écrit le SQE, et retourne le
831 /// jeton ; la soumission effective se fait par [`IoUring::submit`] /
832 /// [`IoUring::submit_and_wait`].
833 ///
834 /// # Errors
835 ///
836 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins (back-pressure
837 /// structurelle, **aucun** syscall — §4.3).
838 pub fn submit_nop(&mut self) -> Result<SubmissionToken, Errno> {
839 if self.sq.space_left() == 0 {
840 return Err(Errno::EBUSY);
841 }
842 let token = self.slab.reserve(None).map_err(|_| Errno::EBUSY)?;
843 // SAFETY: place SQ vérifiée > 0 ; le SQE est rempli intégralement
844 // ci-dessous avant toute publication de la `tail`.
845 let sqe = unsafe { self.sq.prepare() }.expect("place SQ vérifiée");
846 let opts = self.pending_options;
847 // SAFETY: `sqe` pointe un slot SQE valide fraîchement zéro-initialisé,
848 // exclusivement détenu jusqu'à la publication.
849 unsafe {
850 (*sqe).opcode = raw::IORING_OP_NOP;
851 (*sqe).flags = opts.iosqe_flags();
852 (*sqe).user_data = token.to_user_data();
853 }
854 self.pending_options = SubmitOptions::default();
855 if opts.skips_cqe_on_success() {
856 // Aucun CQE attendu en cas de succès → libère le slot S1 dès la
857 // soumission (§6.2). Un échec produirait un CQE périmé, filtré.
858 self.slab.release(token);
859 }
860 Ok(token)
861 }
862
863 /// Moissonne une complétion **non périmée** déjà disponible (sans syscall),
864 /// en filtrant les complétions périmées (§4.2). `None` si la CQ est vide.
865 fn harvest_ready(&mut self) -> Option<Completion> {
866 loop {
867 let cqe = self.cq.peek()?;
868 if cqe.user_data & raw::RAW_USER_DATA_TAG != 0 {
869 // Complétion **brute** (Temps 4 §5) : ne PAS toucher au slab ni
870 // consommer le CQE — laissé à `raw_peek_completion_queue_entry` /
871 // `raw_advance_completion_queue`. La moisson gérée s'arrête ici
872 // (FIFO : aucune complétion gérée disponible tant que cette brute,
873 // en tête, n'est pas consommée par l'appelant).
874 return None;
875 }
876 self.cq.advance();
877 let flags = CompletionFlags::from_bits_truncate(cqe.flags);
878 // Marquage multishot lu **avant** `complete` (qui peut libérer le slot
879 // à la terminale) : permet à `Completion::multishot_token` de
880 // distinguer un flux multishot d'une op mono-coup (Temps 3d).
881 let multishot = self.slab.is_multishot(cqe.user_data);
882 match self
883 .slab
884 .complete(cqe.user_data, flags.contains(CompletionFlags::MORE))
885 {
886 slab::SlotOutcome::Stale => {} // périmée : passer à la suivante
887 slab::SlotOutcome::More => {
888 return Some(Completion {
889 token: SubmissionToken::from_user_data(cqe.user_data),
890 res: cqe.res,
891 raw_flags: cqe.flags,
892 payload: None,
893 multishot,
894 });
895 }
896 slab::SlotOutcome::Final { payload } => {
897 return Some(Completion {
898 token: SubmissionToken::from_user_data(cqe.user_data),
899 res: cqe.res,
900 raw_flags: cqe.flags,
901 payload,
902 multishot,
903 });
904 }
905 }
906 }
907 }
908}
909
910// ───────────────────────────────────────────────────────────────────────────
911// Temps 4 — accès brut niveau 1 (soupape ADR-022 Décision 1)
912// ───────────────────────────────────────────────────────────────────────────
913impl IoUring {
914 /// Capacité totale de la SQ (entrées effectives, arrondi kernel). **Sûr.**
915 #[must_use]
916 pub fn submission_queue_capacity(&self) -> u32 {
917 self.sq.capacity()
918 }
919
920 /// Emplacements SQ actuellement disponibles. **Sûr.**
921 #[must_use]
922 pub fn submission_queue_available(&self) -> u32 {
923 self.sq.space_left()
924 }
925
926 /// Capacité totale de la CQ. **Sûr.**
927 #[must_use]
928 pub fn completion_queue_capacity(&self) -> u32 {
929 self.cq.capacity()
930 }
931
932 /// Réserve un emplacement SQE libre et le rend pour **remplissage manuel**
933 /// (accès brut niveau 1, Temps 4). `None` si la SQ est pleine. La
934 /// **publication reste `submit()`/`submit_and_wait()`** (sûrs) : la façade
935 /// fait le store release et garde la main sur l'ordering d'anneau ; l'`unsafe`
936 /// ne porte donc QUE sur le **contenu** du SQE et la **validité des buffers**.
937 ///
938 /// # Safety
939 ///
940 /// L'appelant garantit (contrat §6 de la spec) :
941 /// 1. **opcode valide** et supporté par le kernel courant ([`IoUring::supports_op`]/probe) ;
942 /// 2. **cohérence des champs** du SQE (`fd`/`addr`/`len`/flags) pour cet opcode ;
943 /// 3. **validité mémoire** : tout buffer pointé par `addr`/`addr2`/`addr3` reste
944 /// valide et **non déplacé jusqu'à la complétion** (le slab S1 ne le protège
945 /// **pas** en accès brut) ;
946 /// 4. **tag `user_data`** : poser [`RAW_USER_DATA_TAG`] (§5) pour coexister avec
947 /// le niveau 2 sans collision (vérifié en build debug à la soumission) ;
948 /// 5. **pas de double consommation** de la complétion correspondante
949 /// ([`IoUring::raw_advance_completion_queue`] cohérent avec les peeks).
950 pub unsafe fn raw_get_submission_queue_entry(
951 &mut self,
952 ) -> Option<&mut RawSubmissionQueueEntry> {
953 let tail = self.sq.local_tail();
954 // SAFETY: préconditions déléguées à l'appelant (contrat ci-dessus) ;
955 // `prepare` rend un slot fraîchement zéro-initialisé, exclusivement détenu
956 // jusqu'à la publication par `submit`.
957 let sqe = unsafe { self.sq.prepare() }?;
958 self.raw_pending.push(tail);
959 // SAFETY: `RawSubmissionQueueEntry` est `#[repr(transparent)]` sur
960 // `IoUringSqe` (layout identique, asserté statiquement) ; `sqe` désigne un
961 // slot SQE valide de la mmap, emprunté `&mut` exclusivement le temps du
962 // remplissage (avant publication).
963 Some(unsafe { &mut *sqe.cast::<RawSubmissionQueueEntry>() })
964 }
965
966 /// Inspecte la prochaine complétion **sans la consommer** (`None` si la CQ est
967 /// vide). **Sûr** : lecture seule via le load acquire interne. L'interprétation
968 /// de `res`/`flags` et la lecture du tag `user_data` (§5) sont à la charge de
969 /// l'appelant (selon l'opcode soumis).
970 #[must_use]
971 pub fn raw_peek_completion_queue_entry(&self) -> Option<&RawCompletionQueueEntry> {
972 let cqe = self.cq.peek_entry()?;
973 // SAFETY: `cqe` pointe un slot CQE vivant de la mmap (stable tant qu'aucune
974 // avance ; l'emprunt `&self` interdit `advance`/`&mut` concurrent) ;
975 // `RawCompletionQueueEntry` est un miroir `#[repr(C)]` d'`IoUringCqe`
976 // (layout identique, asserté).
977 Some(unsafe { &*cqe.cast::<RawCompletionQueueEntry>() })
978 }
979
980 /// Consomme `n` complétions de la CQ (avance la tête + store release par la
981 /// façade). **Sûr** : borné au nombre réellement prêt (Principe 4) ; l'appelant
982 /// garantit la cohérence avec ses [`IoUring::raw_peek_completion_queue_entry`]
983 /// (pas de double consommation, §6 pt 5).
984 pub fn raw_advance_completion_queue(&mut self, n: u32) {
985 let n = n.min(self.cq.ready());
986 for _ in 0..n {
987 self.cq.advance();
988 }
989 }
990
991 /// Vérifie (build **debug**) que tout SQE préparé via
992 /// [`IoUring::raw_get_submission_queue_entry`] porte [`RAW_USER_DATA_TAG`]
993 /// (§5) — sinon une op brute collisionnerait avec l'encodage du slab S1. En
994 /// release, `debug_assert!` est inerte ; la purge de `raw_pending` a lieu dans
995 /// tous les cas (idempotente, sans allocation sur liste vide).
996 fn debug_assert_raw_tags(&mut self) {
997 // COUVERTURE (ADR-035, STRUCTURAL — cf. docs/COVERAGE-EXCEPTIONS.md,
998 // section `io_uring`). Le `}` fermant ce bloc `if cfg!(debug_assertions)`
999 // porte une région LLVM dégénérée à `count == 0` alors que le **corps**
1000 // de la boucle est prouvé exécuté (mesure : 308 itérations via les tests
1001 // `raw_nop_*`). Artefact d'instrumentation du `cfg!`-constant analogue aux
1002 // ombres `[True:0, False:0]` documentées en tête de registre — non
1003 // couvrable par aucun test.
1004 if cfg!(debug_assertions) {
1005 for &tail in &self.raw_pending {
1006 // SAFETY: `tail` désigne un SQE préparé pendant ce staging, non
1007 // encore publié ; lecture seule de son `user_data`.
1008 let user_data = unsafe { self.sq.sqe_user_data_at(tail) };
1009 debug_assert!(
1010 user_data & raw::RAW_USER_DATA_TAG != 0,
1011 "opération brute (Temps 4) soumise sans RAW_USER_DATA_TAG : \
1012 collision possible avec le slab S1 (§5)"
1013 );
1014 }
1015 }
1016 self.raw_pending.clear();
1017 }
1018}
1019
1020/// Jeton opaque reliant une soumission à sa complétion (décision S1).
1021///
1022/// Encapsule un index de slot du slab d'opérations en vol **et** une génération
1023/// (compteur anti-réutilisation, protège contre l'ABA et les complétions
1024/// périmées). Le `user_data` kernel est dérivé en interne — l'API ne laisse
1025/// **jamais** l'appelant manipuler un `user_data` brut.
1026#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1027pub struct SubmissionToken {
1028 slot: u32,
1029 generation: u32,
1030}
1031
1032impl SubmissionToken {
1033 /// Construit un jeton à partir d'un index de slot et d'une génération.
1034 pub(crate) const fn new(slot: u32, generation: u32) -> Self {
1035 Self { slot, generation }
1036 }
1037
1038 /// Index de slot dans le slab d'opérations en vol.
1039 pub(crate) const fn slot(self) -> u32 {
1040 self.slot
1041 }
1042
1043 /// Génération du slot au moment de la réservation (anti-réutilisation/ABA).
1044 pub(crate) const fn generation(self) -> u32 {
1045 self.generation
1046 }
1047
1048 /// Encode le jeton en `user_data` kernel :
1049 /// `((generation as u64) << 32) | (slot as u64)` (§4.2).
1050 pub(crate) fn to_user_data(self) -> u64 {
1051 // `wrapping_shl(32)` : la valeur (u32 élargie) ne déborde jamais sur 64
1052 // bits ; choix wrapping explicite pour rester lint-clean (Principe 2).
1053 u64::from(self.generation).wrapping_shl(32) | u64::from(self.slot)
1054 }
1055
1056 /// Décode un `user_data` kernel en `(generation, slot)`.
1057 ///
1058 /// # Panics
1059 ///
1060 /// Jamais : les masques 32 bits garantissent que chaque moitié tient dans
1061 /// un `u32` (les `expect` sont structurellement inatteignables).
1062 pub(crate) fn from_user_data(user_data: u64) -> Self {
1063 let slot = u32::try_from(user_data & 0xFFFF_FFFF).expect("masque 32 bits bas");
1064 let generation = u32::try_from(user_data >> 32).expect("décalage 32 bits haut");
1065 Self { slot, generation }
1066 }
1067}
1068
1069/// Jeton d'une opération multishot (un slot, plusieurs complétions). Voir
1070/// [`multishot`] (Temps 3d).
1071#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1072pub struct MultishotToken {
1073 slot: u32,
1074 generation: u32,
1075}
1076
1077impl MultishotToken {
1078 /// Construit un jeton multishot à partir d'un index de slot et d'une
1079 /// génération (sémantique Temps 3d).
1080 pub(crate) const fn new(slot: u32, generation: u32) -> Self {
1081 Self { slot, generation }
1082 }
1083
1084 /// `SubmissionToken` équivalent (même `(slot, generation)`) — pour
1085 /// l'annulation par jeton et la corrélation aux complétions.
1086 pub(crate) const fn as_submission(self) -> SubmissionToken {
1087 SubmissionToken::new(self.slot, self.generation)
1088 }
1089}
1090
1091/// Options de soumission par-opération (flags `IOSQE_*` exposés sûrement).
1092///
1093/// Construit par combinateurs ; appliqué via [`IoUring::with`]. Les liens
1094/// (`IO_LINK`/`IO_HARDLINK`) passent par le `LinkedChainBuilder` (Temps 3c) ; la
1095/// sélection de buffer (`BUFFER_SELECT`) par `ProvidedBufferRing` (Temps 3b).
1096#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1097pub struct SubmitOptions {
1098 // Représentation interne (Pass B) : bitmask IOSQE_*.
1099 flags: u32,
1100}
1101
1102impl SubmitOptions {
1103 /// Active `IOSQE_IO_DRAIN` : draine les opérations déjà soumises avant
1104 /// celle-ci.
1105 #[must_use]
1106 pub fn drain(mut self) -> Self {
1107 self.flags |= u32::from(raw::IOSQE_IO_DRAIN);
1108 self
1109 }
1110
1111 /// Active `IOSQE_ASYNC` : force l'exécution sur le pool io-wq.
1112 #[must_use]
1113 pub fn force_async(mut self) -> Self {
1114 self.flags |= u32::from(raw::IOSQE_ASYNC);
1115 self
1116 }
1117
1118 /// Active `IOSQE_CQE_SKIP_SUCCESS` : pas de CQE en cas de succès (libère le
1119 /// slot S1 **à la soumission**). Nécessite la feature `FEAT_CQE_SKIP`.
1120 #[must_use]
1121 pub fn skip_cqe_on_success(mut self) -> Self {
1122 self.flags |= u32::from(raw::IOSQE_CQE_SKIP_SUCCESS);
1123 self
1124 }
1125
1126 /// Active `IOSQE_FIXED_FILE` : le `fd` du SQE est un **index de slot fixe**
1127 /// (Temps 3a — `FixedSlot`/`fixed_fd_install`). Interne : posé par les
1128 /// façades fixed-file, jamais par l'appelant.
1129 pub(crate) fn fixed_file(mut self) -> Self {
1130 self.flags |= u32::from(raw::IOSQE_FIXED_FILE);
1131 self
1132 }
1133
1134 /// Active `IOSQE_BUFFER_SELECT` : l'op sélectionne automatiquement un buffer
1135 /// dans un groupe fourni (Temps 3b). Interne : posé par les façades
1136 /// `submit_*_provided`, jamais par l'appelant.
1137 pub(crate) fn buffer_select(mut self) -> Self {
1138 self.flags |= u32::from(raw::IOSQE_BUFFER_SELECT);
1139 self
1140 }
1141
1142 /// Octet de flags `IOSQE_*` à poser dans le SQE (tous < 256).
1143 fn iosqe_flags(self) -> u8 {
1144 u8::try_from(self.flags).unwrap_or(0)
1145 }
1146
1147 /// Vrai si `skip_cqe_on_success` est demandé (libération anticipée du slot).
1148 fn skips_cqe_on_success(self) -> bool {
1149 self.flags & u32::from(raw::IOSQE_CQE_SKIP_SUCCESS) != 0
1150 }
1151}
1152
1153// ---------------------------------------------------------------------------
1154// Complétion
1155// ---------------------------------------------------------------------------
1156
1157impl IoUring {
1158 /// Attend (bloquant) la prochaine complétion et la consomme
1159 /// (`io_uring_enter(.., GETEVENTS, min_complete = 1)` ; lecture de la CQ par
1160 /// **load acquire**, §3.2). Les complétions **périmées** (génération de slot
1161 /// non concordante, §4.2) sont filtrées en silence.
1162 ///
1163 /// # Errors
1164 ///
1165 /// - [`Errno::EINTR`] : remonté tel quel.
1166 /// - [`Errno::EBADF`].
1167 pub fn wait_completion(&mut self) -> Result<Completion, Errno> {
1168 loop {
1169 if let Some(completion) = self.harvest_ready() {
1170 return Ok(completion);
1171 }
1172 // Aucune complétion non périmée disponible : bloque jusqu'à au moins
1173 // une via `io_uring_enter(GETEVENTS, min_complete = 1)`.
1174 let (efd, eflag) = self.enter_target();
1175 // SAFETY: `fd`/index est valide ; pas d'argument étendu.
1176 let ret =
1177 unsafe { syscall::enter(efd, 0, 1, raw::IORING_ENTER_GETEVENTS | eflag, 0, 0) };
1178 if ret < 0 {
1179 // EINTR remonté tel quel (ADR-021 conv. 2). La boucle ne
1180 // re-`enter` que sur succès renvoyant des CQE périmés à filtrer.
1181 return Err(raw::errno_from_negative_syscall_ret(ret));
1182 }
1183 }
1184 }
1185
1186 /// Attend une complétion avec délai maximal (`IORING_ENTER_EXT_ARG` +
1187 /// `io_uring_getevents_arg`, ou `ABS_TIMER` si `FEAT_MIN_TIMEOUT`).
1188 /// Retourne `Ok(None)` à l'expiration.
1189 ///
1190 /// # Errors
1191 ///
1192 /// - [`Errno::EINTR`] : remonté tel quel.
1193 /// - [`Errno::EBADF`], [`Errno::EINVAL`].
1194 pub fn wait_completion_timeout(
1195 &mut self,
1196 timeout: Duration,
1197 ) -> Result<Option<Completion>, Errno> {
1198 if let Some(completion) = self.harvest_ready() {
1199 return Ok(Some(completion));
1200 }
1201 let ts = raw::KernelTimespec {
1202 tv_sec: i64::try_from(timeout.as_secs()).unwrap_or(i64::MAX),
1203 tv_nsec: i64::from(timeout.subsec_nanos()),
1204 };
1205 let arg = raw::IoUringGeteventsArg {
1206 sigmask: 0,
1207 sigmask_sz: 0,
1208 min_wait_usec: 0,
1209 ts: core::ptr::from_ref(&ts) as u64,
1210 };
1211 let arg_sz = u64::try_from(core::mem::size_of::<raw::IoUringGeteventsArg>())
1212 .expect("taille getevents_arg ≤ u64");
1213 let (efd, eflag) = self.enter_target();
1214 // SAFETY: `fd`/index valide ; `arg` pointe une `io_uring_getevents_arg`
1215 // vivante de taille `arg_sz` cohérente avec le flag `EXT_ARG`, et `ts`
1216 // reste en vie pendant l'appel (variables locales).
1217 let ret = unsafe {
1218 syscall::enter(
1219 efd,
1220 0,
1221 1,
1222 raw::IORING_ENTER_GETEVENTS | raw::IORING_ENTER_EXT_ARG | eflag,
1223 core::ptr::from_ref(&arg) as u64,
1224 arg_sz,
1225 )
1226 };
1227 if ret < 0 {
1228 let err = raw::errno_from_negative_syscall_ret(ret);
1229 if err == Errno::ETIME {
1230 // Expiration : aucune complétion dans le délai imparti.
1231 return Ok(None);
1232 }
1233 // EINTR et autres errno remontés tels quels (ADR-021 conv. 2).
1234 return Err(err);
1235 }
1236 Ok(self.harvest_ready())
1237 }
1238
1239 /// Récupère une complétion si disponible, **sans bloquer** ni syscall
1240 /// (load acquire de la CQ). Filtre les complétions périmées.
1241 pub fn try_completion(&mut self) -> Option<Completion> {
1242 self.harvest_ready()
1243 }
1244
1245 /// Itère les complétions actuellement disponibles dans la CQ, avançant
1246 /// `head` (store release) au fil de l'itération.
1247 pub fn completions(&mut self) -> CompletionIter<'_> {
1248 CompletionIter { ring: self }
1249 }
1250
1251 /// Vrai si le kernel courant supporte `op` (`IORING_REGISTER_PROBE`, mis en
1252 /// cache à la construction ; ADR-022 Décision 8). Permet le fallback vers le
1253 /// syscall synchrone correspondant.
1254 #[must_use]
1255 pub fn supports_op(&self, op: IoUringOpcode) -> bool {
1256 self.supported_ops
1257 .get(usize::from(opcode_number(op)))
1258 .copied()
1259 .unwrap_or(false)
1260 }
1261
1262 /// Features io_uring négociées au setup (axe G), exposées par prédicats
1263 /// stables.
1264 #[must_use]
1265 pub fn capabilities(&self) -> IoUringCapabilities {
1266 self.capabilities
1267 }
1268
1269 /// Annulation synchrone globale ou ciblée (`IORING_REGISTER_SYNC_CANCEL`,
1270 /// avec timeout). Brique de [`IoUring::shutdown`] (S2). Retourne le nombre
1271 /// d'opérations annulées.
1272 ///
1273 /// # Errors
1274 ///
1275 /// - [`Errno::EINTR`] : remonté tel quel.
1276 /// - [`Errno::EALREADY`] : annulation déjà en cours pour la cible.
1277 /// - [`Errno::ENOENT`] : aucune opération ne correspond à la cible.
1278 /// - [`Errno::EINVAL`].
1279 pub fn sync_cancel(&mut self, target: CancelTarget<'_>) -> Result<u32, Errno> {
1280 let mut reg = raw::IoUringSyncCancelReg {
1281 // Timeout interne borné : le kernel attend jusqu'à 2 s que les
1282 // annulations postent leurs CQE, puis rend la main.
1283 timeout_sec: 2,
1284 ..raw::IoUringSyncCancelReg::default()
1285 };
1286 match target {
1287 CancelTarget::Token(token) => reg.addr = token.to_user_data(),
1288 CancelTarget::Fd(fd) => {
1289 reg.fd = fd.as_raw_fd();
1290 reg.flags = raw::IORING_ASYNC_CANCEL_FD;
1291 }
1292 CancelTarget::Op(op) => {
1293 reg.opcode = opcode_number(op);
1294 reg.flags = raw::IORING_ASYNC_CANCEL_OP;
1295 }
1296 CancelTarget::Any => reg.flags = raw::IORING_ASYNC_CANCEL_ANY,
1297 }
1298 let reg_ptr = core::ptr::from_ref(®) as u64;
1299 // SAFETY: `fd` ring valide ; `reg` pointe une `io_uring_sync_cancel_reg`
1300 // vivante (variable locale) pour la durée de l'appel (`nr_args = 1`).
1301 let ret = unsafe {
1302 syscall::register(self.fd_raw(), raw::IORING_REGISTER_SYNC_CANCEL, reg_ptr, 1)
1303 };
1304 if ret < 0 {
1305 return Err(raw::errno_from_negative_syscall_ret(ret));
1306 }
1307 Ok(u32::try_from(ret).unwrap_or(0))
1308 }
1309}
1310
1311/// Numéro d'opcode kernel `IORING_OP_*` correspondant à un [`IoUringOpcode`].
1312///
1313/// Les 3 opcodes obsolètes (18 `OPENAT`, 31 `PROVIDE_BUFFERS`, 32
1314/// `REMOVE_BUFFERS`) ne figurent pas dans l'énumération : leurs numéros sont
1315/// donc absents (trous 18/31/32).
1316fn opcode_number(op: IoUringOpcode) -> u8 {
1317 match op {
1318 IoUringOpcode::Nop => 0,
1319 IoUringOpcode::Readv => 1,
1320 IoUringOpcode::Writev => 2,
1321 IoUringOpcode::Fsync => 3,
1322 IoUringOpcode::ReadFixed => 4,
1323 IoUringOpcode::WriteFixed => 5,
1324 IoUringOpcode::PollAdd => 6,
1325 IoUringOpcode::PollRemove => 7,
1326 IoUringOpcode::SyncFileRange => 8,
1327 IoUringOpcode::Sendmsg => 9,
1328 IoUringOpcode::Recvmsg => 10,
1329 IoUringOpcode::Timeout => 11,
1330 IoUringOpcode::TimeoutRemove => 12,
1331 IoUringOpcode::Accept => 13,
1332 IoUringOpcode::AsyncCancel => 14,
1333 IoUringOpcode::LinkTimeout => 15,
1334 IoUringOpcode::Connect => 16,
1335 IoUringOpcode::Fallocate => 17,
1336 IoUringOpcode::Close => 19,
1337 IoUringOpcode::FilesUpdate => 20,
1338 IoUringOpcode::Statx => 21,
1339 IoUringOpcode::Read => 22,
1340 IoUringOpcode::Write => 23,
1341 IoUringOpcode::Fadvise => 24,
1342 IoUringOpcode::Madvise => 25,
1343 IoUringOpcode::Send => 26,
1344 IoUringOpcode::Recv => 27,
1345 IoUringOpcode::Openat2 => 28,
1346 IoUringOpcode::EpollCtl => 29,
1347 IoUringOpcode::Splice => 30,
1348 IoUringOpcode::Tee => 33,
1349 IoUringOpcode::Shutdown => 34,
1350 IoUringOpcode::Renameat => 35,
1351 IoUringOpcode::Unlinkat => 36,
1352 IoUringOpcode::Mkdirat => 37,
1353 IoUringOpcode::Symlinkat => 38,
1354 IoUringOpcode::Linkat => 39,
1355 IoUringOpcode::MsgRing => 40,
1356 IoUringOpcode::Fsetxattr => 41,
1357 IoUringOpcode::Setxattr => 42,
1358 IoUringOpcode::Fgetxattr => 43,
1359 IoUringOpcode::Getxattr => 44,
1360 IoUringOpcode::Socket => 45,
1361 IoUringOpcode::UringCmd => 46,
1362 IoUringOpcode::SendZc => 47,
1363 IoUringOpcode::SendmsgZc => 48,
1364 IoUringOpcode::ReadMultishot => 49,
1365 IoUringOpcode::Waitid => 50,
1366 IoUringOpcode::FutexWait => 51,
1367 IoUringOpcode::FutexWake => 52,
1368 IoUringOpcode::FutexWaitv => 53,
1369 IoUringOpcode::FixedFdInstall => 54,
1370 IoUringOpcode::Ftruncate => 55,
1371 IoUringOpcode::Bind => 56,
1372 IoUringOpcode::Listen => 57,
1373 // `IoUringOpcode` est `#[non_exhaustive]` : un opcode d'un kernel futur
1374 // non encore mappé ⇒ numéro invalide (`255`), qui ne matchera aucune
1375 // opération réelle côté `sync_cancel`/restriction (plutôt qu'un faux
1376 // match sur l'opcode 0).
1377 _ => u8::MAX,
1378 }
1379}
1380
1381/// Une complétion (CQE décodé). Au Temps 1, les interprétations sont
1382/// **génériques** ; les variantes typées riches (fd accepté/ouvert, etc.)
1383/// arrivent aux Temps 2a–2d (ADR-022 Décision 9). Convention de résultat : un
1384/// `res` négatif est un `-errno`, converti en `Err(Errno)` par les méthodes
1385/// `into_*` ; un `res ≥ 0` est la valeur utile.
1386pub struct Completion {
1387 /// Jeton décodé depuis `user_data`.
1388 token: SubmissionToken,
1389 /// Résultat brut du kernel (`res` du CQE).
1390 res: i32,
1391 /// Flags bruts du CQE (`IORING_CQE_F_*` + id de buffer dans les bits hauts).
1392 raw_flags: u32,
1393 /// Payload possédé récupéré du slot S1 (`None` si l'op n'en portait pas).
1394 /// Extrait par les accesseurs typés (`into_buffer_result`, `into_statx`…).
1395 payload: Option<OwnedOp>,
1396 /// `true` si cette complétion provient d'une opération **multishot** (Temps
1397 /// 3d) : le slot a été réservé multishot (lu avant `complete`). Sert à
1398 /// [`Completion::multishot_token`].
1399 multishot: bool,
1400}
1401
1402impl Completion {
1403 /// Jeton de l'opération qui a produit cette complétion.
1404 #[must_use]
1405 pub fn token(&self) -> SubmissionToken {
1406 self.token
1407 }
1408
1409 /// Jeton **multishot** si cette complétion provient d'une opération multishot
1410 /// (Temps 3d), `None` sinon. Toutes les complétions d'un même multishot
1411 /// (intermédiaires `CQE_F_MORE` **et** terminale) portent le **même** jeton ;
1412 /// [`Completion::has_more`] distingue les intermédiaires de la terminale.
1413 #[must_use]
1414 pub fn multishot_token(&self) -> Option<MultishotToken> {
1415 if self.multishot {
1416 Some(MultishotToken::new(
1417 self.token.slot(),
1418 self.token.generation(),
1419 ))
1420 } else {
1421 None
1422 }
1423 }
1424
1425 /// Résultat brut du kernel (sémantique dépendante de l'op soumise).
1426 #[must_use]
1427 pub fn raw_result(&self) -> i32 {
1428 self.res
1429 }
1430
1431 /// Flags de complétion (cf. [`CompletionFlags`]).
1432 #[must_use]
1433 pub fn flags(&self) -> CompletionFlags {
1434 CompletionFlags::from_bits_truncate(self.raw_flags)
1435 }
1436
1437 /// `IORING_CQE_F_MORE` : d'autres complétions suivront (multishot).
1438 #[must_use]
1439 pub fn has_more(&self) -> bool {
1440 self.flags().contains(CompletionFlags::MORE)
1441 }
1442
1443 /// `IORING_CQE_F_NOTIF` : complétion de notification zero-copy.
1444 #[must_use]
1445 pub fn is_notif(&self) -> bool {
1446 self.flags().contains(CompletionFlags::NOTIF)
1447 }
1448
1449 /// ID du buffer fourni consommé (`IORING_CQE_F_BUFFER`), le cas échéant.
1450 /// L'identifiant occupe les 16 bits de poids fort des flags du CQE.
1451 #[must_use]
1452 pub fn buffer_id(&self) -> Option<u16> {
1453 if self.flags().contains(CompletionFlags::BUFFER) {
1454 Some(u16::try_from(self.raw_flags >> 16).unwrap_or(0))
1455 } else {
1456 None
1457 }
1458 }
1459
1460 /// `IORING_CQE_F_SOCK_NONEMPTY` : données restantes lisibles après un `recv`.
1461 #[must_use]
1462 pub fn socket_has_pending_data(&self) -> bool {
1463 self.flags().contains(CompletionFlags::SOCK_NONEMPTY)
1464 }
1465
1466 /// Interprétation générique : `res ≥ 0` → valeur utile, `res < 0` →
1467 /// `Err(Errno)` (= `-res`).
1468 ///
1469 /// # Errors
1470 ///
1471 /// [`Errno`] correspondant à `-res` si le kernel a signalé un échec.
1472 pub fn into_result(self) -> Result<i32, Errno> {
1473 res_to_result(self.res)
1474 }
1475
1476 /// Récupère le buffer **déplacé** hors du slot (S1, zéro copie) avec le
1477 /// nombre d'octets traités (`submit_read`/`submit_write`).
1478 ///
1479 /// # Errors
1480 ///
1481 /// [`Errno`] correspondant à `-res` si l'opération a échoué (le buffer est
1482 /// alors consommé sans être restitué).
1483 pub fn into_buffer_result(self) -> Result<(Vec<u8>, usize), Errno> {
1484 let bytes = res_to_result(self.res)?;
1485 let length = usize::try_from(bytes).unwrap_or(0);
1486 let buffer = match self.payload {
1487 Some(OwnedOp::Bytes(buffer)) => buffer,
1488 // Op sans buffer `Bytes` (NOP, ou mauvais accesseur) : buffer vide.
1489 _ => Vec::new(),
1490 };
1491 Ok((buffer, length))
1492 }
1493
1494 /// Récupère les buffers vectorisés **déplacés** hors du slot avec le nombre
1495 /// total d'octets traités (`submit_readv`/`submit_writev`).
1496 ///
1497 /// # Errors
1498 ///
1499 /// [`Errno`] correspondant à `-res` si l'opération a échoué (les buffers
1500 /// sont alors consommés sans être restitués).
1501 pub fn into_vectored_result(self) -> Result<(Vec<Vec<u8>>, usize), Errno> {
1502 let bytes = res_to_result(self.res)?;
1503 let length = usize::try_from(bytes).unwrap_or(0);
1504 let buffers = match self.payload {
1505 Some(OwnedOp::Vectored { buffers, .. }) => buffers,
1506 _ => Vec::new(),
1507 };
1508 Ok((buffers, length))
1509 }
1510
1511 /// Récupère le tampon `statx` **déplacé** hors du slot, rempli par le kernel
1512 /// (`submit_statx`).
1513 ///
1514 /// # Errors
1515 ///
1516 /// [`Errno`] correspondant à `-res` si l'opération a échoué (le tampon est
1517 /// alors consommé sans être restitué).
1518 pub fn into_statx(self) -> Result<Box<air_sys_types::fs::Statx>, Errno> {
1519 res_to_result(self.res)?;
1520 match self.payload {
1521 Some(OwnedOp::Statx { out, .. }) => Ok(out),
1522 // Accesseur appelé sur une complétion sans tampon statx : tampon
1523 // zéro-initialisé (défensif ; jamais sur le chemin nominal `statx`).
1524 _ => Ok(Box::new(air_sys_types::fs::Statx::default())),
1525 }
1526 }
1527
1528 /// Récupère le buffer de valeur xattr **déplacé** hors du slot avec la
1529 /// taille rendue par le kernel (`submit_getxattr`/`submit_fgetxattr`). La
1530 /// taille permet de retailler (un buffer trop court ⇒ `Err(ERANGE)`).
1531 ///
1532 /// # Errors
1533 ///
1534 /// [`Errno`] correspondant à `-res` si l'opération a échoué (le buffer est
1535 /// alors consommé sans être restitué).
1536 pub fn into_xattr_result(self) -> Result<(Vec<u8>, usize), Errno> {
1537 let bytes = res_to_result(self.res)?;
1538 let length = usize::try_from(bytes).unwrap_or(0);
1539 let value = match self.payload {
1540 Some(OwnedOp::Xattr { value, .. }) => value,
1541 _ => Vec::new(),
1542 };
1543 Ok((value, length))
1544 }
1545
1546 /// Récupère le descripteur ouvert par `submit_openat2`.
1547 ///
1548 /// `res` (≥ 0) est le numéro de FD ; il devient un [`OwnedFd`] possédé
1549 /// (CLOEXEC selon `how`). Le chemin source, gardé en vie dans le slot, est
1550 /// libéré ici.
1551 ///
1552 /// # Errors
1553 ///
1554 /// [`Errno`] correspondant à `-res` si l'ouverture a échoué.
1555 pub fn opened_fd(self) -> Result<OwnedFd, Errno> {
1556 let fd = res_to_result(self.res)?;
1557 // SAFETY: `res ≥ 0` est un FD frais possédé, retourné par `openat2` via
1558 // io_uring ; `from_raw_fd` en prend l'ownership exclusif (pas de double
1559 // close : le chemin parké est libéré, aucun autre détenteur).
1560 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1561 }
1562
1563 /// Récupère le socket créé par `submit_socket` (CLOEXEC).
1564 ///
1565 /// # Errors
1566 ///
1567 /// [`Errno`] correspondant à `-res` si la création a échoué.
1568 pub fn into_socket_fd(self) -> Result<OwnedFd, Errno> {
1569 let fd = res_to_result(self.res)?;
1570 // SAFETY: `res ≥ 0` est un FD socket frais possédé (`IORING_OP_SOCKET`) ;
1571 // `from_raw_fd` en prend l'ownership exclusif.
1572 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1573 }
1574
1575 /// Récupère le socket accepté par `submit_accept` (CLOEXEC), sans adresse
1576 /// pair.
1577 ///
1578 /// # Errors
1579 ///
1580 /// [`Errno`] correspondant à `-res` si l'`accept` a échoué.
1581 pub fn accepted_fd(self) -> Result<OwnedFd, Errno> {
1582 let fd = res_to_result(self.res)?;
1583 // SAFETY: `res ≥ 0` est un FD socket connecté frais possédé
1584 // (`IORING_OP_ACCEPT`) ; ownership exclusif via `from_raw_fd`.
1585 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1586 }
1587
1588 /// Récupère le socket accepté **et** l'adresse du pair
1589 /// (`submit_accept_with_peer`). L'adresse est décodée du stockage `sockaddr`
1590 /// possédé écrit par le kernel.
1591 ///
1592 /// # Errors
1593 ///
1594 /// [`Errno`] correspondant à `-res` si l'`accept` a échoué.
1595 pub fn into_accept_result(self) -> Result<(OwnedFd, air_sys_types::net::SocketAddr), Errno> {
1596 let fd = res_to_result(self.res)?;
1597 let address = match self.payload {
1598 Some(OwnedOp::Accept { addr, addrlen }) => {
1599 crate::net::raw_to_socket_addr(&addr.bytes, *addrlen)
1600 // Décodage défensif : un kernel renvoyant une famille inconnue ou une
1601 // longueur nulle ⇒ adresse « non nommée » plutôt que panique.
1602 .unwrap_or(air_sys_types::net::SocketAddr::Unix(
1603 air_sys_types::net::UnixSocketAddr::Unnamed,
1604 ))
1605 }
1606 _ => air_sys_types::net::SocketAddr::Unix(air_sys_types::net::UnixSocketAddr::Unnamed),
1607 };
1608 // SAFETY: `res ≥ 0` est un FD socket connecté frais possédé ; ownership
1609 // exclusif via `from_raw_fd`.
1610 Ok((unsafe { OwnedFd::from_raw_fd(fd) }, address))
1611 }
1612
1613 /// Décode le résultat d'un `submit_receive_message` : buffers reçus +
1614 /// métadonnées (`ReceiveMessageMeta`, adossé à `io_uring_recvmsg_out`). Les
1615 /// **FD reçus** via `SCM_RIGHTS` sont matérialisés en [`OwnedFd`] **CLOEXEC**
1616 /// (drapeau `MSG_CMSG_CLOEXEC` posé par la façade). Un cmsg tronqué
1617 /// (`MSG_CTRUNC`) est signalé via `meta.control_truncated()` ; les FD
1618 /// incomplets ne sont **jamais** matérialisés (le kernel les ferme) ⇒ aucune
1619 /// fuite de FD.
1620 ///
1621 /// # Errors
1622 ///
1623 /// [`Errno`] correspondant à `-res` si le `recvmsg` a échoué.
1624 pub fn into_receive_message_result(
1625 self,
1626 ) -> Result<
1627 (
1628 air_sys_types::net::OwnedReceiveMessage,
1629 air_sys_types::net::ReceiveMessageMeta,
1630 ),
1631 Errno,
1632 > {
1633 use air_sys_types::net::{MessageFlags, OwnedReceiveMessage, ReceiveMessageMeta};
1634 let bytes = res_to_result(self.res)?;
1635 let payloadlen = u32::try_from(bytes).unwrap_or(0);
1636 let Some(OwnedOp::RecvMsg(state)) = self.payload else {
1637 // Accesseur appelé sur une complétion sans état recvmsg : résultat
1638 // vide (défensif ; jamais sur le chemin nominal `recvmsg`).
1639 return Ok((
1640 OwnedReceiveMessage::default(),
1641 ReceiveMessageMeta {
1642 namelen: 0,
1643 controllen: 0,
1644 payloadlen,
1645 flags: MessageFlags::empty(),
1646 address: None,
1647 fds: Vec::new(),
1648 },
1649 ));
1650 };
1651 let owned::RecvMsgState {
1652 msghdr,
1653 buffers,
1654 name,
1655 control,
1656 ..
1657 } = *state;
1658 let namelen = msghdr.msg_namelen;
1659 let controllen_usize = usize::try_from(msghdr.msg_controllen).unwrap_or(0);
1660 let controllen = u32::try_from(msghdr.msg_controllen).unwrap_or(0);
1661 let flags = MessageFlags::from_bits_truncate(msghdr.msg_flags);
1662 // Extrait les FD complets du buffer de contrôle (jamais de FD partiel).
1663 let fds = crate::net::parse_scm_rights(&control, controllen_usize.min(control.len()));
1664 let address = if namelen > 0 {
1665 crate::net::raw_to_socket_addr(&name.bytes, namelen)
1666 } else {
1667 None
1668 };
1669 let control_capacity = control.len();
1670 let meta = ReceiveMessageMeta {
1671 namelen,
1672 controllen,
1673 payloadlen,
1674 flags,
1675 address,
1676 fds,
1677 };
1678 let message = OwnedReceiveMessage {
1679 buffers,
1680 control_capacity,
1681 flags,
1682 };
1683 Ok((message, meta))
1684 }
1685
1686 /// Restitue le buffer zero-copy de `submit_send_zero_copy` à la complétion
1687 /// **NOTIF** (`is_notif`) — le kernel ne référence plus la mémoire. En cas
1688 /// d'**échec précoce** (CQE de résultat `res < 0` **sans** `F_MORE`, donc
1689 /// aucune NOTIF à venir), renvoie l'erreur ; le buffer est alors libéré (sûr,
1690 /// aucune fuite).
1691 ///
1692 /// Pour un `submit_send_message_zero_copy` **multi-buffers**, utiliser
1693 /// [`Completion::into_zero_copy_buffers`] (restitution intégrale, ADR-032) ;
1694 /// appelé ici, ses buffers sont **concaténés** sans perte d'octet (les
1695 /// frontières sont perdues, mais aucune donnée n'est *discardée*).
1696 ///
1697 /// # Errors
1698 ///
1699 /// [`Errno`] correspondant à `-res` sur le chemin d'échec précoce.
1700 pub fn into_zero_copy_buffer(self) -> Result<Vec<u8>, Errno> {
1701 let is_notif = self.flags().contains(CompletionFlags::NOTIF);
1702 let res = self.res;
1703 let buffer = match self.payload {
1704 Some(OwnedOp::Bytes(buffer)) => buffer,
1705 // ADR-032 : ne *discarde* jamais — concatène tous les buffers (aucun
1706 // octet perdu) plutôt que de ne rendre que le premier.
1707 Some(OwnedOp::SendMsg(state)) => state.buffers.into_iter().flatten().collect(),
1708 _ => Vec::new(),
1709 };
1710 if is_notif {
1711 // NOTIF : succès ; le `res` peut porter le bit « copié » (REPORT_USAGE),
1712 // ce n'est pas une erreur — la restitution est inconditionnelle.
1713 Ok(buffer)
1714 } else {
1715 // Échec précoce (résultat sans F_MORE) : propage l'erreur.
1716 res_to_result(res).map(|_| buffer)
1717 }
1718 }
1719
1720 /// Restitue **l'intégralité** des buffers zero-copy d'un
1721 /// `submit_send_message_zero_copy` à la complétion **NOTIF** — **tous** les
1722 /// buffers du message, **intacts et dans l'ordre** (ADR-032, zéro perte de
1723 /// donnée). Le slot S1 les a tous retenus jusqu'au NOTIF. Pour un
1724 /// `submit_send_zero_copy` (mono-buffer), renvoie un `Vec` à un seul élément.
1725 ///
1726 /// Échec précoce (CQE résultat `res < 0` sans `F_MORE`) : renvoie l'erreur ;
1727 /// les buffers sont alors libérés (sûr, aucune fuite).
1728 ///
1729 /// # Errors
1730 ///
1731 /// [`Errno`] correspondant à `-res` sur le chemin d'échec précoce.
1732 pub fn into_zero_copy_buffers(self) -> Result<Vec<Vec<u8>>, Errno> {
1733 let is_notif = self.flags().contains(CompletionFlags::NOTIF);
1734 let res = self.res;
1735 let buffers = match self.payload {
1736 // `sendmsg_zc` : tous les buffers, dans l'ordre — jamais un sous-ensemble.
1737 Some(OwnedOp::SendMsg(state)) => state.buffers,
1738 // `send_zc` mono-buffer : enveloppé dans un `Vec` à un élément.
1739 Some(OwnedOp::Bytes(buffer)) => vec![buffer],
1740 _ => Vec::new(),
1741 };
1742 if is_notif {
1743 Ok(buffers)
1744 } else {
1745 res_to_result(res).map(|_| buffers)
1746 }
1747 }
1748
1749 /// `IORING_NOTIF_USAGE_ZC_COPIED` : sur une complétion NOTIF avec
1750 /// `ZeroCopyFlags::REPORT_USAGE`, indique que le kernel a dû **copier** (pas
1751 /// de vrai zero-copy). `false` hors NOTIF ou sans `REPORT_USAGE`.
1752 #[must_use]
1753 pub fn zero_copy_copied(&self) -> bool {
1754 self.flags().contains(CompletionFlags::NOTIF)
1755 && (self.res & raw::IORING_NOTIF_USAGE_ZC_COPIED) != 0
1756 }
1757
1758 /// Décode les événements prêts d'un `submit_poll_add` (`res ≥ 0` = bitset
1759 /// `PollEvents`).
1760 ///
1761 /// # Errors
1762 ///
1763 /// [`Errno`] correspondant à `-res` si le poll a échoué.
1764 pub fn into_poll_result(self) -> Result<air_sys_types::io_uring::PollEvents, Errno> {
1765 let bits = res_to_result(self.res)?;
1766 // `res` ≥ 0 est un bitset d'événements (`u32`) — jamais négatif ici.
1767 let bits = u32::try_from(bits).unwrap_or(0);
1768 Ok(air_sys_types::io_uring::PollEvents::from_bits_truncate(
1769 bits,
1770 ))
1771 }
1772
1773 /// Récupère le tampon `siginfo_t` **déplacé** hors du slot, rempli par le
1774 /// kernel (`submit_waitid`). À lire via
1775 /// [`SignalInfo::as_bytes`](air_sys_types::signal::SignalInfo::as_bytes).
1776 ///
1777 /// # Errors
1778 ///
1779 /// [`Errno`] correspondant à `-res` si le `waitid` a échoué (le tampon est
1780 /// alors consommé sans être restitué).
1781 pub fn into_waitid_result(self) -> Result<Box<air_sys_types::signal::SignalInfo>, Errno> {
1782 res_to_result(self.res)?;
1783 match self.payload {
1784 Some(OwnedOp::Waitid(info)) => Ok(info),
1785 // Accesseur appelé hors chemin `waitid` : `siginfo` zéro-initialisé.
1786 _ => Ok(Box::new(air_sys_types::signal::SignalInfo::zeroed())),
1787 }
1788 }
1789
1790 /// Construit une [`Completion`] de test (sans kernel) pour exercer les
1791 /// accesseurs typés sur des `(res, flags, payload)` arbitraires.
1792 #[cfg(test)]
1793 pub(crate) fn for_test(
1794 token: SubmissionToken,
1795 res: i32,
1796 raw_flags: u32,
1797 payload: Option<OwnedOp>,
1798 ) -> Self {
1799 Self {
1800 token,
1801 res,
1802 raw_flags,
1803 payload,
1804 multishot: false,
1805 }
1806 }
1807
1808 /// Succès sans valeur de retour (close, fsync, nop…).
1809 ///
1810 /// # Errors
1811 ///
1812 /// [`Errno`] correspondant à `-res` si l'opération a échoué.
1813 pub fn completed(&self) -> Result<(), Errno> {
1814 res_to_result(self.res).map(|_| ())
1815 }
1816}
1817
1818/// Convertit le `res` d'un CQE en `Result` : `res < 0` ⇒ `Err(-res)`, sinon
1819/// `Ok(res)`. Convention io_uring (ADR-022 Décision 9).
1820fn res_to_result(res: i32) -> Result<i32, Errno> {
1821 if res < 0 {
1822 // `res` négatif ⇒ `-res` est un errno kernel (1..=4095) non nul.
1823 // `checked_neg` ne renvoie `None` que pour `i32::MIN` (défensif, jamais
1824 // en pratique pour un CQE kernel).
1825 match res.checked_neg().and_then(core::num::NonZeroI32::new) {
1826 Some(nz) => Err(Errno::from_nonzero(nz)),
1827 None => Err(Errno::EINVAL),
1828 }
1829 } else {
1830 Ok(res)
1831 }
1832}
1833
1834/// Surface de fuzzing (frontière de décode des données écrites par le kernel,
1835/// Principe 3). N'existe QUE sous `--cfg fuzzing` (posé par `cargo-fuzz`) :
1836/// zéro surface en build normal. Le harnais est **pur** (aucun syscall, aucun
1837/// ring réel) — on modélise un kernel buggé/hostile remplissant la mémoire
1838/// partagée. **Invariant : TOTALITÉ** — toute entrée donne une valeur typée ou
1839/// une erreur typée, jamais de panic/UB/accès hors-bornes.
1840#[cfg(fuzzing)]
1841pub mod fuzz_api {
1842 use super::owned::OwnedOp;
1843 use super::{Completion, SetupFlags, SubmissionToken, raw, ring_sizes};
1844 use crate::io_uring::slab::InflightSlab;
1845 use air_sys_types::fs::{Statx, StatxMask};
1846 // Crate `#![no_std]` (sauf `cfg(test)`) : sous `--cfg fuzzing` on n'est pas en
1847 // test, donc les types possédés viennent d'`alloc`, pas du prélude `std`. La
1848 // macro `vec!` reste fournie par `#[macro_use] extern crate alloc` (crate root).
1849 use alloc::boxed::Box;
1850 use alloc::ffi::CString;
1851 use alloc::vec::Vec;
1852 use core::num::NonZeroU32;
1853
1854 /// Décode les **résultats kernel externes** des opérations 2a (`statx`,
1855 /// `getxattr`, `readv`) via les accesseurs typés de [`Completion`], pour
1856 /// tout `(res, raw_flags)` et tout contenu de tampon. Invariant de
1857 /// **totalité** : aucune entrée ne produit panic/UB — la valeur typée ou
1858 /// l'erreur typée, jamais autre chose. (Le kernel écrit `Statx`/la valeur
1859 /// xattr ; ce sont des données externes au sens du Principe 3.)
1860 pub fn decode_2a_results(res: i32, raw_flags: u32, mask: u32, value: Vec<u8>) {
1861 let token = SubmissionToken::new(0, 0);
1862
1863 // statx : tampon écrit par le kernel, masque arbitraire.
1864 let mut out = Box::new(Statx::default());
1865 out.mask = mask;
1866 out.size = u64::from(raw_flags);
1867 let statx_completion = Completion {
1868 token,
1869 res,
1870 raw_flags,
1871 payload: Some(OwnedOp::Statx {
1872 out,
1873 path: CString::new("p").expect("pas de NUL"),
1874 }),
1875 multishot: false,
1876 };
1877 if let Ok(decoded) = statx_completion.into_statx() {
1878 let _ = decoded.has(StatxMask::from_bits_truncate(mask));
1879 let _ = decoded.size;
1880 let _ = decoded.mtime;
1881 }
1882
1883 // getxattr : valeur de sortie de taille arbitraire.
1884 let xattr_completion = Completion {
1885 token,
1886 res,
1887 raw_flags,
1888 payload: Some(OwnedOp::Xattr {
1889 value: value.clone(),
1890 name: CString::new("n").expect("pas de NUL"),
1891 path: None,
1892 }),
1893 multishot: false,
1894 };
1895 let _ = xattr_completion.into_xattr_result();
1896
1897 // readv : restitution vectorisée (un buffer + son iovec dérivé).
1898 let mut buffers = vec![value];
1899 let iovecs: Box<[raw::Iovec]> = buffers
1900 .iter_mut()
1901 .map(|buffer| raw::Iovec {
1902 iov_base: buffer.as_mut_ptr(),
1903 iov_len: buffer.len(),
1904 })
1905 .collect();
1906 let vectored_completion = Completion {
1907 token,
1908 res,
1909 raw_flags,
1910 payload: Some(OwnedOp::Vectored { buffers, iovecs }),
1911 multishot: false,
1912 };
1913 let _ = vectored_completion.into_vectored_result();
1914 }
1915
1916 /// Décode les **complétions `URING_CMD` socket** (Temps 2d) : `getsockopt`
1917 /// restitue son **buffer de sortie** (rempli par le kernel) via
1918 /// `into_buffer_result` (avec `res` = longueur effective annoncée par le
1919 /// kernel — donnée externe, Principe 3) ; `inq`/`outq` via `into_result`.
1920 /// Invariant de **totalité** : pour tout `(res, value)`, ni panic ni UB —
1921 /// la longueur retournée peut dépasser `value.len()` si le kernel ment, mais
1922 /// l'accesseur ne déréférence pas au-delà du buffer (il rend `(Vec, usize)`).
1923 pub fn decode_2d_results(res: i32, value: Vec<u8>) {
1924 let token = SubmissionToken::new(0, 0);
1925 // getsockopt : buffer de sortie + longueur effective (`res`).
1926 let getsockopt_completion = Completion {
1927 token,
1928 res,
1929 raw_flags: 0,
1930 payload: Some(OwnedOp::Bytes(value)),
1931 multishot: false,
1932 };
1933 let _ = getsockopt_completion.into_buffer_result();
1934 // inq/outq : pas de buffer, `res` = compteur d'octets.
1935 let count_completion = Completion {
1936 token,
1937 res,
1938 raw_flags: 0,
1939 payload: None,
1940 multishot: false,
1941 };
1942 let _ = count_completion.completed();
1943 let _ = count_completion.into_result();
1944 }
1945
1946 /// Décode les **données réseau externes** d'un `recvmsg` io_uring (Temps 2b)
1947 /// via `into_receive_message_result` : le **buffer de contrôle** (cmsg
1948 /// `SCM_RIGHTS`) et le **stockage d'adresse** (`sockaddr`) sont remplis par
1949 /// le kernel ⇒ entrées externes (Principe 3). Invariant de **totalité** :
1950 /// pour tout `(res, controllen, namelen, flags, control, name)`, le parsing
1951 /// (`parse_scm_rights` + `raw_to_socket_addr`) ne panique ni ne déborde. Les
1952 /// `i32` extraits du cmsg ne sont **pas** matérialisés en FD ici (on ne
1953 /// fournit pas de vrai descripteur), évitant toute fermeture parasite.
1954 pub fn decode_2b_recvmsg(
1955 res: i32,
1956 controllen: u32,
1957 namelen: u32,
1958 msg_flags: i32,
1959 control: Vec<u8>,
1960 name: Vec<u8>,
1961 ) {
1962 let mut name_storage = Box::new(crate::net::RawSockaddrStorage {
1963 bytes: [0u8; crate::net::MAX_SOCKADDR_LEN],
1964 });
1965 let take = name.len().min(crate::net::MAX_SOCKADDR_LEN);
1966 name_storage.bytes[..take].copy_from_slice(&name[..take]);
1967 let state = crate::io_uring::owned::RecvMsgState {
1968 msghdr: Box::new(raw::Msghdr {
1969 msg_namelen: namelen,
1970 msg_controllen: u64::from(controllen),
1971 msg_flags,
1972 ..raw::Msghdr::default()
1973 }),
1974 iovecs: Vec::new().into_boxed_slice(),
1975 buffers: Vec::new(),
1976 name: name_storage,
1977 control: control.into_boxed_slice(),
1978 };
1979 let completion = Completion {
1980 token: SubmissionToken::new(0, 0),
1981 res,
1982 raw_flags: 0,
1983 payload: Some(OwnedOp::RecvMsg(Box::new(state))),
1984 multishot: false,
1985 };
1986 if let Ok((_msg, meta)) = completion.into_receive_message_result() {
1987 let _ = meta.payload_truncated();
1988 let _ = meta.control_truncated();
1989 // Ne pas `drop` les FD ici : ce sont des `i32` arbitraires enveloppés
1990 // en `OwnedFd` ; on les **oublie** pour éviter de fermer des FD réels
1991 // du processus de fuzz (totalité sans effet de bord).
1992 for fd in meta.fds {
1993 core::mem::forget(fd);
1994 }
1995 }
1996 }
1997
1998 /// Décode les **résultats kernel externes** des accesseurs 2c (`poll`,
1999 /// `waitid`) pour tout `res`. Invariant de **totalité** : `into_poll_result`
2000 /// (bitset d'événements écrit par le kernel) et `into_waitid_result` (tampon
2001 /// `siginfo` rempli par le kernel) ne paniquent ni ne débordent. Le tampon
2002 /// `siginfo` est de la **mémoire pure** (aucun FD) ⇒ droppé normalement.
2003 pub fn decode_2c_results(res: i32) {
2004 let token = SubmissionToken::new(0, 0);
2005 let poll = Completion {
2006 token,
2007 res,
2008 raw_flags: 0,
2009 payload: None,
2010 multishot: false,
2011 };
2012 let _ = poll.into_poll_result();
2013 let waitid = Completion {
2014 token,
2015 res,
2016 raw_flags: 0,
2017 payload: Some(OwnedOp::Waitid(Box::new(
2018 air_sys_types::signal::SignalInfo::zeroed(),
2019 ))),
2020 multishot: false,
2021 };
2022 if let Ok(info) = waitid.into_waitid_result() {
2023 let _ = info.as_bytes();
2024 }
2025 }
2026
2027 /// Décode un CQE (champs bruts kernel) et exerce toutes les interprétations
2028 /// de [`Completion`] : aucune ne doit paniquer pour quelque entrée que ce
2029 /// soit (`res` incluant `i32::MIN`, flags arbitraires).
2030 pub fn decode_cqe(user_data: u64, res: i32, raw_flags: u32) {
2031 let completion = Completion {
2032 token: SubmissionToken::from_user_data(user_data),
2033 res,
2034 raw_flags,
2035 payload: None,
2036 multishot: false,
2037 };
2038 let _ = completion.token();
2039 let _ = completion.raw_result();
2040 let _ = completion.flags();
2041 let _ = completion.has_more();
2042 let _ = completion.is_notif();
2043 let _ = completion.buffer_id();
2044 let _ = completion.socket_has_pending_data();
2045 let _ = completion.completed();
2046 let _ = completion.into_result();
2047 }
2048
2049 /// Décode une **complétion brute** (Temps 4) à partir de **données kernel
2050 /// externes** arbitraires (`user_data`/`res`/`flags`, Principe 3). Invariant de
2051 /// **totalité** : aucune entrée ne panique ; le routage du tag (§5) est total
2052 /// (brut ⇔ bit 63 posé) ; un `user_data` **non tagué** hostile passé au
2053 /// décodage géré (`SubmissionToken::from_user_data`) reste borné (jamais d'OOB,
2054 /// jamais de panic). Couvre la frontière de décode brut/géré coexistant.
2055 pub fn decode_raw_cqe(user_data: u64, res: i32, flags: u32) {
2056 let raw_cqe = raw::RawCompletionQueueEntry {
2057 user_data,
2058 res,
2059 flags,
2060 };
2061 // Lecture totale des champs (POD) — jamais de panic.
2062 let _ = raw_cqe.user_data;
2063 let _ = raw_cqe.res;
2064 let _ = raw_cqe.flags;
2065 // Routage du tag : exactement deux classes, exhaustives.
2066 if raw_cqe.user_data & raw::RAW_USER_DATA_TAG == 0 {
2067 // Classe « gérée » : le décodage slab doit rester borné même sur un
2068 // `user_data` hostile non tagué (S1).
2069 let _ = SubmissionToken::from_user_data(raw_cqe.user_data);
2070 }
2071 }
2072
2073 /// Invariant de soundness S1 : un `user_data` **hostile** (slot arbitraire,
2074 /// potentiellement ≥ capacité) passé à `slab.complete` doit être **borné**
2075 /// (rejeté en périmé via `get`), jamais indexer le slab hors-bornes.
2076 pub fn slab_complete_is_bounded(capacity: u8, hostile_user_data: &[u64]) {
2077 let cap = NonZeroU32::new(u32::from(capacity).max(1)).expect("≥ 1");
2078 let mut slab = InflightSlab::with_capacity(cap);
2079 // Quelques réservations réelles pour avoir des slots en vol.
2080 let mut tokens = Vec::new();
2081 for _ in 0..u32::from(capacity).min(4) {
2082 if let Ok(t) = slab.reserve(None) {
2083 tokens.push(t);
2084 }
2085 }
2086 // Complétions hostiles entrelacées : aucune ne doit paniquer/OOB.
2087 for &user_data in hostile_user_data {
2088 let _ = slab.complete(user_data, user_data & 1 == 0);
2089 }
2090 for token in tokens {
2091 let _ = slab.complete(token.to_user_data(), false);
2092 }
2093 }
2094
2095 /// Décode les tailles d'anneau depuis des `io_uring_params` bruts : toute
2096 /// combinaison d'entrées/offsets/features ⇒ `Ok`/`Err(EINVAL)`, jamais
2097 /// d'overflow ni de panic (arithmétique checked).
2098 pub fn ring_sizes_decode(
2099 sq_entries: u32,
2100 cq_entries: u32,
2101 sq_array_off: u32,
2102 cq_cqes_off: u32,
2103 features: u32,
2104 setup_bits: u32,
2105 ) {
2106 let mut params = raw::IoUringParams {
2107 sq_entries,
2108 cq_entries,
2109 features,
2110 ..raw::IoUringParams::default()
2111 };
2112 params.sq_off.array = sq_array_off;
2113 params.cq_off.cqes = cq_cqes_off;
2114 let _ = ring_sizes(¶ms, SetupFlags::from_bits_truncate(setup_bits));
2115 }
2116}
2117
2118/// Itérateur sur les complétions disponibles d'un [`IoUring`] (emprunte le ring).
2119pub struct CompletionIter<'ring> {
2120 ring: &'ring mut IoUring,
2121}
2122
2123impl Iterator for CompletionIter<'_> {
2124 type Item = Completion;
2125
2126 fn next(&mut self) -> Option<Completion> {
2127 self.ring.harvest_ready()
2128 }
2129}
2130
2131// ---------------------------------------------------------------------------
2132// Capacités, annulation, sandbox (types couplés au wrapper)
2133// ---------------------------------------------------------------------------
2134
2135/// Miroir typé de `struct io_uring_params` (entrées/sorties de
2136/// `io_uring_setup(2)`).
2137///
2138/// Porte les setup flags demandés, les profondeurs effectives
2139/// (`sq_entries`/`cq_entries`, arrondies par le kernel), les `features`
2140/// négociées et les offsets d'anneau. Le layout `#[repr(C)]` réel est posé en
2141/// Pass B (les champs ABI conservent leurs noms kernel, ADR-029 zone
2142/// d'interface). Opaque au Temps 1.
2143#[derive(Debug, Clone, Copy)]
2144pub struct IoUringParams {
2145 // Layout #[repr(C)] complet posé en Pass B.
2146 _opaque: [u8; 0],
2147}
2148
2149/// Features io_uring du kernel courant (axe G, 16 features en 6.12). Lues dans
2150/// `io_uring_params.features` après setup, exposées par **prédicats stables**.
2151#[derive(Debug, Clone, Copy)]
2152pub struct IoUringCapabilities {
2153 // Champ interne (Pass B) : bitmask IORING_FEAT_*.
2154 features: u32,
2155}
2156
2157impl IoUringCapabilities {
2158 /// Vrai si le bit de feature `bit` (un `IORING_FEAT_*`) est négocié.
2159 fn has(&self, bit: u32) -> bool {
2160 self.features & bit != 0
2161 }
2162
2163 /// `IORING_FEAT_SINGLE_MMAP` : SQ et CQ partagent une seule mmap.
2164 #[must_use]
2165 pub fn single_mmap(&self) -> bool {
2166 self.has(raw::IORING_FEAT_SINGLE_MMAP)
2167 }
2168 /// `IORING_FEAT_NODROP` : la CQ ne perd pas de complétions (overflow retenu).
2169 #[must_use]
2170 pub fn nodrop(&self) -> bool {
2171 self.has(raw::IORING_FEAT_NODROP)
2172 }
2173 /// `IORING_FEAT_SUBMIT_STABLE` : données de soumission stables après `enter`.
2174 #[must_use]
2175 pub fn submit_stable(&self) -> bool {
2176 self.has(raw::IORING_FEAT_SUBMIT_STABLE)
2177 }
2178 /// `IORING_FEAT_RW_CUR_POS` : offset `-1` = position courante du fichier.
2179 #[must_use]
2180 pub fn rw_cur_pos(&self) -> bool {
2181 self.has(raw::IORING_FEAT_RW_CUR_POS)
2182 }
2183 /// `IORING_FEAT_CUR_PERSONALITY` : applique la personnalité courante.
2184 #[must_use]
2185 pub fn cur_personality(&self) -> bool {
2186 self.has(raw::IORING_FEAT_CUR_PERSONALITY)
2187 }
2188 /// `IORING_FEAT_FAST_POLL` : poll interne rapide.
2189 #[must_use]
2190 pub fn fast_poll(&self) -> bool {
2191 self.has(raw::IORING_FEAT_FAST_POLL)
2192 }
2193 /// `IORING_FEAT_POLL_32BITS` : masques de poll 32 bits.
2194 #[must_use]
2195 pub fn poll_32bits(&self) -> bool {
2196 self.has(raw::IORING_FEAT_POLL_32BITS)
2197 }
2198 /// `IORING_FEAT_SQPOLL_NONFIXED` : SQPOLL sans FD fixes obligatoires.
2199 #[must_use]
2200 pub fn sqpoll_nonfixed(&self) -> bool {
2201 self.has(raw::IORING_FEAT_SQPOLL_NONFIXED)
2202 }
2203 /// `IORING_FEAT_EXT_ARG` : argument étendu d'`enter` (timeout d'attente).
2204 #[must_use]
2205 pub fn ext_arg(&self) -> bool {
2206 self.has(raw::IORING_FEAT_EXT_ARG)
2207 }
2208 /// `IORING_FEAT_NATIVE_WORKERS` : workers io-wq natifs.
2209 #[must_use]
2210 pub fn native_workers(&self) -> bool {
2211 self.has(raw::IORING_FEAT_NATIVE_WORKERS)
2212 }
2213 /// `IORING_FEAT_RSRC_TAGS` : étiquetage des ressources enregistrées.
2214 #[must_use]
2215 pub fn rsrc_tags(&self) -> bool {
2216 self.has(raw::IORING_FEAT_RSRC_TAGS)
2217 }
2218 /// `IORING_FEAT_CQE_SKIP` : support de `IOSQE_CQE_SKIP_SUCCESS`.
2219 #[must_use]
2220 pub fn cqe_skip(&self) -> bool {
2221 self.has(raw::IORING_FEAT_CQE_SKIP)
2222 }
2223 /// `IORING_FEAT_LINKED_FILE` : résolution de FD dans les chaînes liées.
2224 #[must_use]
2225 pub fn linked_file(&self) -> bool {
2226 self.has(raw::IORING_FEAT_LINKED_FILE)
2227 }
2228 /// `IORING_FEAT_REG_REG_RING` : enregistrement du ring fd via ring enregistré.
2229 #[must_use]
2230 pub fn reg_reg_ring(&self) -> bool {
2231 self.has(raw::IORING_FEAT_REG_REG_RING)
2232 }
2233 /// `IORING_FEAT_RECVSEND_BUNDLE` : recv/send groupés (bundle).
2234 #[must_use]
2235 pub fn recvsend_bundle(&self) -> bool {
2236 self.has(raw::IORING_FEAT_RECVSEND_BUNDLE)
2237 }
2238 /// `IORING_FEAT_MIN_TIMEOUT` : timeout minimal d'attente (`ABS_TIMER`).
2239 #[must_use]
2240 pub fn min_timeout(&self) -> bool {
2241 self.has(raw::IORING_FEAT_MIN_TIMEOUT)
2242 }
2243}
2244
2245/// Cible d'une annulation synchrone ([`IoUring::sync_cancel`]).
2246///
2247/// Mappée sur les flags `IORING_ASYNC_CANCEL_*`. Le paramètre de durée de vie
2248/// `'fd` borne l'emprunt du descripteur ciblé par [`CancelTarget::Fd`].
2249#[derive(Debug, Clone, Copy)]
2250pub enum CancelTarget<'fd> {
2251 /// Annule l'opération identifiée par ce jeton (flag `USERDATA`).
2252 Token(SubmissionToken),
2253 /// Annule toutes les opérations portant sur ce descripteur (flag `FD`).
2254 Fd(BorrowedFd<'fd>),
2255 /// Annule toutes les opérations de cet opcode (flag `OP`).
2256 Op(IoUringOpcode),
2257 /// Annule **toutes** les opérations en vol (flag `ANY`).
2258 Any,
2259}
2260
2261/// Restriction appliquée à un ring **avant** son activation (décision S3 ;
2262/// `struct io_uring_restriction`).
2263///
2264/// Posée via [`IoUringBuilder::restrict`], elle réduit la surface d'attaque du
2265/// ring (capability, Temps 3f / [`sandbox`]). Le ring est créé désactivé
2266/// (`R_DISABLED`) puis activé par [`IoUring::enable`].
2267#[derive(Debug, Clone, Copy)]
2268pub enum Restriction {
2269 /// N'autoriser que cet opcode de soumission.
2270 AllowOp(IoUringOpcode),
2271 /// N'autoriser que ce register opcode (numéro `IORING_REGISTER_*`).
2272 AllowRegister(u8),
2273 /// Flags SQE (`IOSQE_*`) autorisés.
2274 SqeFlagsAllowed(SubmitOptions),
2275 /// Flags SQE requis sur chaque soumission.
2276 SqeFlagsRequired(SubmitOptions),
2277}
2278
2279#[cfg(test)]
2280mod token_tests {
2281 use super::SubmissionToken;
2282
2283 #[test]
2284 fn user_data_round_trips_slot_and_generation() {
2285 for &(slot, generation) in &[
2286 (0u32, 0u32),
2287 (1, 0),
2288 (0, 1),
2289 (0xABCD, 0x1234),
2290 (u32::MAX, u32::MAX),
2291 (u32::MAX, 0),
2292 (0, u32::MAX),
2293 ] {
2294 let token = SubmissionToken::new(slot, generation);
2295 let decoded = SubmissionToken::from_user_data(token.to_user_data());
2296 assert_eq!(decoded.slot(), slot);
2297 assert_eq!(decoded.generation(), generation);
2298 assert_eq!(decoded, token);
2299 }
2300 }
2301
2302 #[test]
2303 fn user_data_layout_is_generation_high_slot_low() {
2304 let token = SubmissionToken::new(0x0000_0007, 0x0000_0003);
2305 // generation dans les 32 bits hauts, slot dans les 32 bits bas.
2306 assert_eq!(token.to_user_data(), 0x0000_0003_0000_0007);
2307 }
2308
2309 #[test]
2310 fn from_user_data_splits_halves() {
2311 let token = SubmissionToken::from_user_data(0xDEAD_BEEF_CAFE_F00D);
2312 assert_eq!(token.slot(), 0xCAFE_F00D);
2313 assert_eq!(token.generation(), 0xDEAD_BEEF);
2314 }
2315
2316 proptest::proptest! {
2317 /// `user_data = (generation << 32) | slot` est bijectif sur tout
2318 /// `(slot, generation)` ∈ (u32, u32) : encodage/décodage sans perte.
2319 #[test]
2320 fn user_data_is_bijective(slot: u32, generation: u32) {
2321 let token = SubmissionToken::new(slot, generation);
2322 let decoded = SubmissionToken::from_user_data(token.to_user_data());
2323 proptest::prop_assert_eq!(decoded.slot(), slot);
2324 proptest::prop_assert_eq!(decoded.generation(), generation);
2325 proptest::prop_assert_eq!(decoded, token);
2326 }
2327 }
2328}
2329
2330#[cfg(test)]
2331mod construction_tests {
2332 // Tests d'intégration kernel (cible 6.12) : exécutés sur les exécuteurs
2333 // Linux (speedy/raspi). Couvrent la construction du ring, la lecture des
2334 // features et le `Drop` propre (ring vide).
2335 use super::{IoUring, IoUringBuilder, SetupFlags};
2336 use core::num::NonZeroU32;
2337
2338 fn entries(n: u32) -> NonZeroU32 {
2339 NonZeroU32::new(n).expect("entries non nul")
2340 }
2341
2342 #[test]
2343 fn new_creates_a_ring_and_reports_capabilities() {
2344 let ring = IoUring::new(entries(8)).expect("io_uring_setup doit réussir sur 6.12");
2345 // En 6.12, SINGLE_MMAP et NODROP sont toujours présents.
2346 assert!(
2347 ring.capabilities().single_mmap(),
2348 "SINGLE_MMAP attendu en 6.12"
2349 );
2350 assert!(ring.capabilities().nodrop(), "NODROP attendu en 6.12");
2351 assert_eq!(ring.in_flight(), 0);
2352 // `entries` est arrondi à la puissance de 2 ≥ : au moins 8 places SQ.
2353 assert!(ring.submission_queue_space_left() >= 8);
2354 assert!(!ring.completion_queue_overflowed());
2355 // `Drop` (ring vide) ne doit pas paniquer.
2356 }
2357
2358 #[test]
2359 fn builder_with_recommended_flags_builds() {
2360 let ring = IoUringBuilder::new(entries(16))
2361 .with_flags(SetupFlags::SINGLE_ISSUER | SetupFlags::DEFER_TASKRUN)
2362 .build()
2363 .expect("ring avec SINGLE_ISSUER|DEFER_TASKRUN");
2364 assert_eq!(ring.in_flight(), 0);
2365 assert!(ring.submission_queue_space_left() >= 16);
2366 }
2367
2368 #[test]
2369 fn explicit_cq_size_is_honored() {
2370 let ring = IoUringBuilder::new(entries(8))
2371 .with_completion_queue_entries(entries(64))
2372 .build()
2373 .expect("ring avec CQSIZE explicite");
2374 // La CQ demandée (64) ≥ SQ (8) ; le slab par défaut suit cq_entries.
2375 assert_eq!(ring.in_flight(), 0);
2376 }
2377
2378 #[test]
2379 fn distinct_rings_are_independent() {
2380 let a = IoUring::new(entries(8)).expect("ring a");
2381 let b = IoUring::new(entries(8)).expect("ring b");
2382 assert_eq!(a.in_flight(), 0);
2383 assert_eq!(b.in_flight(), 0);
2384 }
2385
2386 #[test]
2387 fn probe_reports_core_ops_supported() {
2388 use super::IoUringOpcode;
2389 let ring = IoUring::new(entries(8)).expect("ring");
2390 // En 6.12, ces opcodes sont tous supportés (probe via REGISTER_PROBE).
2391 assert!(ring.supports_op(IoUringOpcode::Nop), "NOP supporté");
2392 assert!(ring.supports_op(IoUringOpcode::Read));
2393 assert!(ring.supports_op(IoUringOpcode::Write));
2394 assert!(ring.supports_op(IoUringOpcode::Accept));
2395 assert!(ring.supports_op(IoUringOpcode::Openat2));
2396 }
2397}
2398
2399#[cfg(test)]
2400mod submission_tests {
2401 // Round-trip NOP end-to-end sur kernel 6.12 (soumission §6 + complétion §7).
2402 use super::{IoUring, IoUringBuilder, SubmitOptions};
2403 use air_sys_types::Errno;
2404 use core::num::NonZeroU32;
2405 use core::time::Duration;
2406
2407 fn entries(n: u32) -> NonZeroU32 {
2408 NonZeroU32::new(n).expect("entries non nul")
2409 }
2410
2411 #[test]
2412 fn nop_round_trips_with_submit_and_wait() {
2413 let mut ring = IoUring::new(entries(8)).expect("ring");
2414 let token = ring.submit_nop().expect("submit_nop");
2415 assert_eq!(ring.in_flight(), 1);
2416 let submitted = ring.submit_and_wait(1).expect("submit_and_wait");
2417 assert_eq!(submitted, 1);
2418 let completion = ring.wait_completion().expect("wait_completion");
2419 assert_eq!(completion.token(), token, "user_data round-trip");
2420 assert_eq!(completion.raw_result(), 0, "NOP res == 0");
2421 assert!(completion.completed().is_ok());
2422 assert!(!completion.has_more());
2423 assert_eq!(ring.in_flight(), 0, "slot S1 libéré à la complétion");
2424 }
2425
2426 #[test]
2427 fn submit_returns_count_and_completions_iterates() {
2428 let mut ring = IoUring::new(entries(16)).expect("ring");
2429 for _ in 0..3 {
2430 ring.submit_nop().expect("submit_nop");
2431 }
2432 assert_eq!(ring.in_flight(), 3);
2433 assert_eq!(ring.submit_and_wait(3).expect("submit_and_wait"), 3);
2434 let mut count = 0;
2435 while count < 3 {
2436 // Draine au fur et à mesure (les 3 CQE peuvent arriver groupés).
2437 for completion in ring.completions() {
2438 assert_eq!(completion.raw_result(), 0);
2439 count += 1;
2440 }
2441 }
2442 assert_eq!(count, 3);
2443 assert_eq!(ring.in_flight(), 0);
2444 }
2445
2446 #[test]
2447 fn try_completion_is_none_before_any_submission() {
2448 let mut ring = IoUring::new(entries(8)).expect("ring");
2449 assert!(ring.try_completion().is_none());
2450 }
2451
2452 #[test]
2453 fn timeout_expires_on_empty_ring() {
2454 let mut ring = IoUring::new(entries(8)).expect("ring");
2455 let res = ring
2456 .wait_completion_timeout(Duration::from_millis(20))
2457 .expect("pas d'erreur");
2458 assert!(res.is_none(), "aucune complétion ⇒ None à l'expiration");
2459 }
2460
2461 #[test]
2462 fn timeout_returns_completion_when_available() {
2463 let mut ring = IoUring::new(entries(8)).expect("ring");
2464 ring.submit_nop().expect("submit_nop");
2465 ring.submit().expect("submit");
2466 let res = ring
2467 .wait_completion_timeout(Duration::from_secs(1))
2468 .expect("pas d'erreur");
2469 let completion = res.expect("une complétion avant expiration");
2470 assert_eq!(completion.raw_result(), 0);
2471 }
2472
2473 #[test]
2474 fn slab_back_pressure_returns_ebusy() {
2475 // Slab limité à 1 : la 2ᵉ op en vol est refusée AVANT tout syscall.
2476 let mut ring = IoUringBuilder::new(entries(8))
2477 .max_inflight(entries(1))
2478 .build()
2479 .expect("ring");
2480 let _t = ring.submit_nop().expect("1ʳᵉ op");
2481 assert_eq!(ring.submit_nop(), Err(Errno::EBUSY), "slab plein ⇒ EBUSY");
2482 // Après complétion, on peut re-soumettre.
2483 ring.submit_and_wait(1).expect("submit_and_wait");
2484 let _c = ring.wait_completion().expect("complétion");
2485 assert_eq!(ring.in_flight(), 0);
2486 // Ré-soumission possible après libération ; drainée pour laisser le ring
2487 // quiescent au Drop (la quiescence automatique arrive à l'incrément 5).
2488 ring.submit_nop().expect("ré-soumission après libération");
2489 ring.submit_and_wait(1).expect("submit_and_wait 2");
2490 let _c2 = ring.wait_completion().expect("complétion 2");
2491 assert_eq!(ring.in_flight(), 0);
2492 }
2493
2494 #[test]
2495 fn skip_cqe_on_success_frees_slot_at_submission() {
2496 let mut ring = IoUring::new(entries(8)).expect("ring");
2497 ring.with(SubmitOptions::default().skip_cqe_on_success());
2498 ring.submit_nop().expect("submit_nop skip");
2499 // Slot libéré dès la soumission : aucune op en vol.
2500 assert_eq!(ring.in_flight(), 0);
2501 assert_eq!(ring.submit().expect("submit"), 1);
2502 }
2503}
2504
2505#[cfg(test)]
2506mod coverage_tests {
2507 // Complète la couverture du cœur (prédicats de capacités, mapping d'opcodes,
2508 // méthodes builder/Completion, chemin de fuite S2) jusqu'à 100 % hors
2509 // exceptions documentées (docs/COVERAGE-EXCEPTIONS.md).
2510 use super::owned::OwnedOp;
2511 use super::syscall::sim::{self, Syscall};
2512 use super::{
2513 CancelTarget, Completion, CompletionFlags, IoUring, IoUringBuilder, IoUringOpcode,
2514 SetupFlags, SubmissionToken, SubmitOptions, opcode_number,
2515 };
2516 use air_sys_types::Errno;
2517 use air_sys_types::fd::BorrowedFd;
2518 use core::num::NonZeroU32;
2519 use core::time::Duration;
2520
2521 fn entries(n: u32) -> NonZeroU32 {
2522 NonZeroU32::new(n).expect("entries non nul")
2523 }
2524
2525 /// Appelle les 16 prédicats de `IoUringCapabilities` (axe G).
2526 #[test]
2527 fn all_capability_predicates_are_callable() {
2528 let ring = IoUring::new(entries(8)).expect("ring");
2529 let caps = ring.capabilities();
2530 // En 6.12, ces deux-là sont vrais ; les autres : on exerce juste l'appel.
2531 assert!(caps.single_mmap() && caps.nodrop());
2532 let _ = caps.submit_stable();
2533 let _ = caps.rw_cur_pos();
2534 let _ = caps.cur_personality();
2535 let _ = caps.fast_poll();
2536 let _ = caps.poll_32bits();
2537 let _ = caps.sqpoll_nonfixed();
2538 let _ = caps.ext_arg();
2539 let _ = caps.native_workers();
2540 let _ = caps.rsrc_tags();
2541 let _ = caps.cqe_skip();
2542 let _ = caps.linked_file();
2543 let _ = caps.reg_reg_ring();
2544 let _ = caps.recvsend_bundle();
2545 let _ = caps.min_timeout();
2546 }
2547
2548 /// Couvre les 55 bras de `opcode_number` (le bras `_` = futur opcode
2549 /// `#[non_exhaustive]`, inatteignable, est consigné en exception).
2550 #[test]
2551 fn opcode_number_maps_all_retained_opcodes() {
2552 use IoUringOpcode::{
2553 Accept, AsyncCancel, Bind, Close, Connect, EpollCtl, Fadvise, Fallocate, Fgetxattr,
2554 FilesUpdate, FixedFdInstall, Fsetxattr, Fsync, Ftruncate, FutexWait, FutexWaitv,
2555 FutexWake, Getxattr, LinkTimeout, Linkat, Listen, Madvise, Mkdirat, MsgRing, Nop,
2556 Openat2, PollAdd, PollRemove, Read, ReadFixed, ReadMultishot, Readv, Recv, Recvmsg,
2557 Renameat, Send, SendZc, Sendmsg, SendmsgZc, Setxattr, Shutdown, Socket, Splice, Statx,
2558 Symlinkat, SyncFileRange, Tee, Timeout, TimeoutRemove, Unlinkat, UringCmd, Waitid,
2559 Write, WriteFixed, Writev,
2560 };
2561 let all = [
2562 Nop,
2563 Readv,
2564 Writev,
2565 Fsync,
2566 ReadFixed,
2567 WriteFixed,
2568 PollAdd,
2569 PollRemove,
2570 SyncFileRange,
2571 Sendmsg,
2572 Recvmsg,
2573 Timeout,
2574 TimeoutRemove,
2575 Accept,
2576 AsyncCancel,
2577 LinkTimeout,
2578 Connect,
2579 Fallocate,
2580 Close,
2581 FilesUpdate,
2582 Statx,
2583 Read,
2584 Write,
2585 Fadvise,
2586 Madvise,
2587 Send,
2588 Recv,
2589 Openat2,
2590 EpollCtl,
2591 Splice,
2592 Tee,
2593 Shutdown,
2594 Renameat,
2595 Unlinkat,
2596 Mkdirat,
2597 Symlinkat,
2598 Linkat,
2599 MsgRing,
2600 Fsetxattr,
2601 Setxattr,
2602 Fgetxattr,
2603 Getxattr,
2604 Socket,
2605 UringCmd,
2606 SendZc,
2607 SendmsgZc,
2608 ReadMultishot,
2609 Waitid,
2610 FutexWait,
2611 FutexWake,
2612 FutexWaitv,
2613 FixedFdInstall,
2614 Ftruncate,
2615 Bind,
2616 Listen,
2617 ];
2618 assert_eq!(all.len(), 55);
2619 assert_eq!(opcode_number(Nop), 0);
2620 assert_eq!(opcode_number(Listen), 57);
2621 for op in all {
2622 assert!(opcode_number(op) < 58);
2623 }
2624 }
2625
2626 /// Exerce les méthodes builder non couvertes par le chemin nominal.
2627 #[test]
2628 fn builder_configurators_are_exercised() {
2629 let base = IoUring::new(entries(8)).expect("ring de base");
2630 let ring = IoUringBuilder::new(entries(8))
2631 .with_completion_queue_entries(entries(32))
2632 .max_inflight(entries(4))
2633 .with_sqpoll_idle(Duration::from_millis(10))
2634 .with_sqpoll_cpu(0)
2635 .attach_work_queue(&base)
2636 .with_flags(SetupFlags::SINGLE_ISSUER)
2637 .build()
2638 .expect("ring configuré (ATTACH_WQ partagé)");
2639 assert_eq!(ring.in_flight(), 0);
2640 }
2641
2642 /// Exerce tous les accesseurs de `Completion` (round-trip NOP).
2643 #[test]
2644 fn completion_accessors_are_exercised() {
2645 let mut ring = IoUring::new(entries(8)).expect("ring");
2646 let token = ring.submit_nop().expect("nop");
2647 ring.submit_and_wait(1).expect("submit");
2648 let completion = ring.wait_completion().expect("complétion");
2649 assert_eq!(completion.token(), token);
2650 assert_eq!(completion.raw_result(), 0);
2651 let _ = completion.flags();
2652 assert!(!completion.has_more());
2653 assert!(!completion.is_notif());
2654 assert_eq!(completion.buffer_id(), None);
2655 assert!(!completion.socket_has_pending_data());
2656 assert!(completion.completed().is_ok());
2657 // NOP : pas de buffer transféré ⇒ buffer vide, 0 octet.
2658 let (buffer, length) = completion.into_buffer_result().expect("succès");
2659 assert!(buffer.is_empty());
2660 assert_eq!(length, 0);
2661 }
2662
2663 /// `with(SubmitOptions::drain().force_async())` : exerce les combinateurs.
2664 #[test]
2665 fn submit_options_combinators_are_exercised() {
2666 let mut ring = IoUring::new(entries(8)).expect("ring");
2667 ring.with(SubmitOptions::default().drain().force_async());
2668 ring.submit_nop().expect("nop avec options");
2669 ring.submit_and_wait(1).expect("submit");
2670 let _ = ring.wait_completion().expect("complétion");
2671 }
2672
2673 /// Chemin de **fuite contrôlée** S2 : si la quiescence n'aboutit pas
2674 /// (drainage en échec), `shutdown` fuite les ressources (sound) et rend
2675 /// `Err`. On force l'échec en injectant des `enter` en erreur pendant le
2676 /// drainage (l'op réservée n'est jamais complétée).
2677 #[test]
2678 fn teardown_leak_path_when_quiescence_fails() {
2679 sim::clear();
2680 let mut ring = IoUring::new(entries(8)).expect("ring");
2681 ring.submit_nop().expect("op réservée, jamais complétée");
2682 // `sync_cancel` (register) réel ; chaque `enter` de drainage échoue
2683 // immédiatement (assez d'injections pour épuiser la boucle bornée).
2684 for _ in 0..80 {
2685 sim::inject(Syscall::Enter, -4); // EINTR
2686 }
2687 // Quiescence impossible ⇒ fuite contrôlée ⇒ Err (jamais d'UAF/panic).
2688 assert_eq!(ring.shutdown(), Err(Errno::EBUSY));
2689 sim::clear();
2690 }
2691
2692 /// `submit` sans rien en attente : aucun syscall, `Ok(0)`.
2693 #[test]
2694 fn submit_with_nothing_pending_returns_zero() {
2695 let mut ring = IoUring::new(entries(8)).expect("ring");
2696 assert_eq!(ring.submit(), Ok(0));
2697 }
2698
2699 /// SQ pleine ⇒ `submit_nop` rend `EBUSY` (back-pressure SQ, avant le slab).
2700 #[test]
2701 fn submit_nop_on_full_sq_returns_ebusy() {
2702 // SQ = 4 entrées, slab = 8 : la SQ se remplit en premier.
2703 let mut ring = IoUringBuilder::new(entries(4))
2704 .max_inflight(entries(8))
2705 .build()
2706 .expect("ring");
2707 for _ in 0..4 {
2708 ring.submit_nop().expect("remplissage SQ");
2709 }
2710 assert_eq!(ring.submit_nop(), Err(Errno::EBUSY), "SQ pleine ⇒ EBUSY");
2711 // Nettoyage : draine les 4.
2712 ring.submit_and_wait(4).expect("submit");
2713 let mut n = 0;
2714 while n < 4 {
2715 for _ in ring.completions() {
2716 n += 1;
2717 }
2718 }
2719 }
2720
2721 /// Couvre les variantes `Token`/`Fd`/`Op` de `sync_cancel` (register injecté).
2722 #[test]
2723 fn sync_cancel_target_variants_are_exercised() {
2724 sim::clear();
2725 let mut ring = IoUring::new(entries(8)).expect("ring");
2726 let token = SubmissionToken::new(0, 0);
2727 sim::inject(Syscall::Register, 0);
2728 assert_eq!(ring.sync_cancel(CancelTarget::Token(token)), Ok(0));
2729 sim::inject(Syscall::Register, 0);
2730 // SAFETY: fd 0 (stdin) emprunté brièvement pour construire la cible ;
2731 // l'appel register est intercepté (aucune annulation réelle).
2732 let borrowed = unsafe { BorrowedFd::borrow_raw(0) };
2733 assert_eq!(ring.sync_cancel(CancelTarget::Fd(borrowed)), Ok(0));
2734 sim::inject(Syscall::Register, 0);
2735 assert_eq!(
2736 ring.sync_cancel(CancelTarget::Op(IoUringOpcode::Read)),
2737 Ok(0)
2738 );
2739 sim::clear();
2740 }
2741
2742 /// `enable` réussit quand `REGISTER_ENABLE_RINGS` rend `0` (ring désactivé).
2743 #[test]
2744 fn enable_succeeds_when_register_ok() {
2745 sim::clear();
2746 let mut ring = IoUring::new(entries(8)).expect("ring");
2747 sim::inject(Syscall::Register, 0);
2748 assert_eq!(ring.enable(), Ok(()));
2749 sim::clear();
2750 }
2751
2752 /// Construit un ring avec `SQE128`/`CQE32` (couvre les tailles 128/32).
2753 #[test]
2754 fn build_with_sqe128_and_cqe32() {
2755 let ring = IoUringBuilder::new(entries(8))
2756 .with_flags(SetupFlags::SQE128 | SetupFlags::CQE32)
2757 .build()
2758 .expect("ring SQE128|CQE32 (supporté en 6.12)");
2759 assert_eq!(ring.in_flight(), 0);
2760 }
2761
2762 // NB : le flux `restrict → build → enable`, l'application effective des
2763 // restrictions (`REGISTER_RESTRICTIONS`) et leur sémantique default-deny /
2764 // immuabilité sont couverts par les tests d'intégration kernel du sous-module
2765 // `sandbox` (Temps 3f).
2766
2767 /// `Completion::buffer_id` quand le flag `BUFFER` est posé (id en bits hauts).
2768 #[test]
2769 fn completion_buffer_id_decodes_high_bits() {
2770 let raw_flags = CompletionFlags::BUFFER.bits() | (7u32 << 16);
2771 let completion = Completion {
2772 token: SubmissionToken::new(0, 0),
2773 res: 0,
2774 raw_flags,
2775 payload: None,
2776 multishot: false,
2777 };
2778 assert_eq!(completion.buffer_id(), Some(7));
2779 }
2780
2781 /// `Completion` en échec : `res < 0` ⇒ `Err(errno)` ; `i32::MIN` ⇒ `EINVAL`.
2782 #[test]
2783 fn completion_error_results_are_decoded() {
2784 let failed = Completion {
2785 token: SubmissionToken::new(0, 0),
2786 res: -22, // EINVAL
2787 raw_flags: 0,
2788 payload: Some(OwnedOp::Bytes(vec![1, 2, 3])),
2789 multishot: false,
2790 };
2791 assert_eq!(failed.completed(), Err(Errno::EINVAL));
2792 assert_eq!(failed.into_result(), Err(Errno::EINVAL));
2793
2794 let pathological = Completion {
2795 token: SubmissionToken::new(0, 0),
2796 res: i32::MIN, // `checked_neg` ⇒ None ⇒ EINVAL (défensif)
2797 raw_flags: 0,
2798 payload: None,
2799 multishot: false,
2800 };
2801 assert_eq!(pathological.into_result(), Err(Errno::EINVAL));
2802 }
2803}
2804
2805#[cfg(test)]
2806mod simulator_tests {
2807 // Simulateur de syscalls : injecte des `-errno` aux frontières
2808 // setup/enter/register (sans kernel pour l'appel intercepté) afin de PROUVER
2809 // les contrats de comportement couche 0 (ADR-021) et de rendre atteignables
2810 // les branches d'erreur (levier du 100 % branches, incrément 8). L'errno
2811 // injecté transite par le MÊME décode (`errno_from_negative_syscall_ret`).
2812 use super::syscall::sim::{self, Syscall};
2813 use super::{CancelTarget, IoUring, IoUringOpcode};
2814 use air_sys_types::Errno;
2815 use core::num::NonZeroU32;
2816 use core::time::Duration;
2817
2818 fn entries(n: u32) -> NonZeroU32 {
2819 NonZeroU32::new(n).expect("entries non nul")
2820 }
2821
2822 /// `ENOSYS` au setup (kernel sans io_uring) ⇒ erreur PROPRE, pas de panic.
2823 #[test]
2824 fn setup_enosys_degrades_gracefully() {
2825 sim::clear();
2826 sim::inject(Syscall::Setup, -38);
2827 assert_eq!(IoUring::new(entries(8)).err(), Some(Errno::ENOSYS));
2828 sim::clear();
2829 }
2830
2831 /// Autres erreurs de setup remontées telles quelles (sans masquage).
2832 #[test]
2833 fn setup_errors_are_surfaced_verbatim() {
2834 for (raw, expected) in [
2835 (-12i64, Errno::ENOMEM),
2836 (-22, Errno::EINVAL),
2837 (-1, Errno::EPERM),
2838 ] {
2839 sim::clear();
2840 sim::inject(Syscall::Setup, raw);
2841 assert_eq!(IoUring::new(entries(8)).err(), Some(expected));
2842 }
2843 sim::clear();
2844 }
2845
2846 /// `EINTR` sur `submit_and_wait` ⇒ REMONTÉ, JAMAIS retried (ADR-021 #2).
2847 /// Une seule injection : si l'appel rebouclait, il ferait un vrai syscall —
2848 /// l'assert `Err(EINTR)` prouve l'absence de retry.
2849 #[test]
2850 fn submit_and_wait_raises_eintr_without_retry() {
2851 sim::clear();
2852 let mut ring = IoUring::new(entries(8)).expect("ring réel");
2853 sim::inject(Syscall::Enter, -4); // EINTR, une seule fois
2854 assert_eq!(ring.submit_and_wait(1), Err(Errno::EINTR));
2855 sim::clear();
2856 }
2857
2858 /// `EAGAIN` sur `submit` (ring avec op en attente) ⇒ back-pressure propre.
2859 #[test]
2860 fn submit_raises_eagain_cleanly() {
2861 sim::clear();
2862 let mut ring = IoUring::new(entries(8)).expect("ring");
2863 ring.submit_nop().expect("op en attente");
2864 sim::inject(Syscall::Enter, -11); // EAGAIN
2865 assert_eq!(ring.submit(), Err(Errno::EAGAIN));
2866 // Nettoyage : soumission/complétion réelles pour quiescer le ring.
2867 ring.submit_and_wait(1).expect("submit réel");
2868 let _ = ring.wait_completion().expect("complétion");
2869 sim::clear();
2870 }
2871
2872 /// Compte de soumission partiel cohérent : `enter` rend `2` sur `3` soumis.
2873 #[test]
2874 fn submit_returns_partial_count_consistently() {
2875 sim::clear();
2876 let mut ring = IoUring::new(entries(8)).expect("ring");
2877 for _ in 0..3 {
2878 ring.submit_nop().expect("nop");
2879 }
2880 sim::inject(Syscall::Enter, 2); // le kernel n'en a consommé que 2
2881 assert_eq!(ring.submit(), Ok(2));
2882 // Nettoyage : draine réellement les 3 (la tail est publiée).
2883 ring.submit_and_wait(3).expect("submit réel");
2884 let mut harvested = 0;
2885 while harvested < 3 {
2886 for _ in ring.completions() {
2887 harvested += 1;
2888 }
2889 }
2890 sim::clear();
2891 }
2892
2893 /// `EINTR` sur `wait_completion` ⇒ remonté tel quel.
2894 #[test]
2895 fn wait_completion_raises_eintr() {
2896 sim::clear();
2897 let mut ring = IoUring::new(entries(8)).expect("ring");
2898 sim::inject(Syscall::Enter, -4);
2899 assert_eq!(ring.wait_completion().err(), Some(Errno::EINTR));
2900 sim::clear();
2901 }
2902
2903 /// `wait_completion_timeout` : `ETIME` ⇒ `Ok(None)` ; `EINTR` ⇒ `Err`.
2904 #[test]
2905 fn wait_completion_timeout_maps_etime_and_eintr() {
2906 sim::clear();
2907 let mut ring = IoUring::new(entries(8)).expect("ring");
2908 sim::inject(Syscall::Enter, -62); // ETIME
2909 let timed_out = ring.wait_completion_timeout(Duration::from_millis(1));
2910 assert!(matches!(timed_out, Ok(None)), "ETIME ⇒ Ok(None)");
2911 sim::inject(Syscall::Enter, -4); // EINTR
2912 assert_eq!(
2913 ring.wait_completion_timeout(Duration::from_millis(1)).err(),
2914 Some(Errno::EINTR)
2915 );
2916 sim::clear();
2917 }
2918
2919 /// `sync_cancel` : `ENOENT` (aucune op à annuler) remonté.
2920 #[test]
2921 fn sync_cancel_surfaces_enoent() {
2922 sim::clear();
2923 let mut ring = IoUring::new(entries(8)).expect("ring");
2924 sim::inject(Syscall::Register, -2); // ENOENT
2925 assert_eq!(
2926 ring.sync_cancel(CancelTarget::Any).err(),
2927 Some(Errno::ENOENT)
2928 );
2929 sim::clear();
2930 }
2931
2932 /// `enable` : `EINVAL` remonté tel quel.
2933 #[test]
2934 fn enable_surfaces_einval() {
2935 sim::clear();
2936 let mut ring = IoUring::new(entries(8)).expect("ring");
2937 sim::inject(Syscall::Register, -22); // EINVAL
2938 assert_eq!(ring.enable().err(), Some(Errno::EINVAL));
2939 sim::clear();
2940 }
2941
2942 /// Probe en échec ⇒ tous les opcodes marqués non supportés (conservateur :
2943 /// force le fallback synchrone, pas de faux positif).
2944 #[test]
2945 fn probe_failure_reports_all_ops_unsupported() {
2946 sim::clear();
2947 sim::inject(Syscall::Register, -1); // le probe (register) échoue
2948 let ring = IoUring::new(entries(8)).expect("ring construit malgré probe KO");
2949 assert!(!ring.supports_op(IoUringOpcode::Nop));
2950 assert!(!ring.supports_op(IoUringOpcode::Read));
2951 sim::clear();
2952 }
2953}
2954
2955#[cfg(test)]
2956mod teardown_tests {
2957 // Téardown S2 sur kernel 6.12 : shutdown propre, quiescence, Drop sûr.
2958 use super::IoUring;
2959 use core::num::NonZeroU32;
2960
2961 fn entries(n: u32) -> NonZeroU32 {
2962 NonZeroU32::new(n).expect("entries non nul")
2963 }
2964
2965 #[test]
2966 fn shutdown_on_empty_ring_succeeds() {
2967 let ring = IoUring::new(entries(8)).expect("ring");
2968 ring.shutdown().expect("shutdown propre d'un ring vide");
2969 }
2970
2971 #[test]
2972 fn shutdown_quiesces_a_pending_op() {
2973 let mut ring = IoUring::new(entries(8)).expect("ring");
2974 ring.submit_nop().expect("submit_nop");
2975 ring.submit().expect("submit");
2976 // L'op est en vol et non moissonnée : `shutdown` doit quiescer (drainer)
2977 // avant de libérer, puis réussir.
2978 ring.shutdown().expect("shutdown quiesce l'op en vol");
2979 }
2980
2981 #[test]
2982 fn drop_with_pending_op_quiesces_without_panic() {
2983 let mut ring = IoUring::new(entries(8)).expect("ring");
2984 ring.submit_nop().expect("submit_nop");
2985 ring.submit().expect("submit");
2986 // `Drop` implicite en fin de scope : quiesce + libère sans paniquer ni
2987 // laisser le kernel écrire dans de la mémoire libérée.
2988 drop(ring);
2989 }
2990
2991 #[test]
2992 fn shutdown_is_not_double_freed_by_drop() {
2993 // `shutdown` consomme `self` ; son `Drop` interne ne doit pas re-dérouler
2994 // le téardown (drapeau `disposed`). Si double munmap/close, le kernel ou
2995 // le runtime paniquerait/abort — l'absence de panique vaut assertion.
2996 for _ in 0..16 {
2997 let ring = IoUring::new(entries(8)).expect("ring");
2998 ring.shutdown().expect("shutdown");
2999 }
3000 }
3001}