Skip to main content

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, &params, 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            &params.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, &params.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(&reg) 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(&params, 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}