Skip to main content

air_sys_syscall/io_uring/
registration.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//! **Registration** (Temps 3a) : enregistrement de ressources fixes auprès du
6//! kernel via `io_uring_register(2)` (n° 427) — tables de descripteurs fixes
7//! ([`FixedFdTable`]), buffers enregistrés ([`RegisteredBuffers`]), ring fd
8//! enregistré, eventfd, personality, réglage du pool io-wq, NAPI, horloge. Ce
9//! Temps **débloque** les variantes « direct »/« fixed » référencées depuis les
10//! Temps 2a–2c.
11//!
12//! Référence normative : `docs/specs/layer-0/io-uring-3a-registration.md`.
13//!
14//! **Modèle d'ownership (ADR-028, ADR-032).** Les ressources enregistrées
15//! restent valides **tant qu'elles sont enregistrées** : [`FixedFdTable`]
16//! **possède** les [`OwnedFd`] des slots remplis ; [`RegisteredBuffers`]
17//! possède les buffers épinglés (`Vec<u8>` ou [`MmapRegion`]). Les références
18//! d'usage ([`FixedSlot`], [`RegisteredBufferSlice`]) y sont liées par
19//! *lifetime* → inutilisables après désenregistrement (sûreté par construction).
20//! Le désenregistrement **restitue l'intégralité** des ressources reprises
21//! (zéro discard) ; toute consommation explicite (FD remplacé par `set`, buffer
22//! remplacé par `update`) est documentée par contrat.
23//!
24//! **`io_uring_register` est synchrone** (pas une op de SQE) : ces façades
25//! appellent directement la couture `syscall::register` sur le FD du ring
26//! (emprunté `&mut` le temps de l'appel), et **n'enregistrent jamais
27//! automatiquement** (ADR-022 D4) — l'application le demande.
28//!
29//! **Legacy évacués** : seules les variantes `*2` taguées sont exposées (pas de
30//! `REGISTER_BUFFERS`/`FILES`/`FILES_UPDATE` v1).
31
32use super::owned::OwnedOp;
33use super::{Completion, IoUring, SubmissionToken, raw, syscall};
34use crate::mem::MmapRegion;
35use air_sys_types::Errno;
36use air_sys_types::fd::{AsRawFd, BorrowedFd, OwnedFd};
37use air_sys_types::fs::{DirFd, OpenHow};
38use air_sys_types::net::{AcceptFlags, SocketDomain, SocketType};
39use air_sys_types::system::CpuSet;
40use alloc::boxed::Box;
41use alloc::vec::Vec;
42use core::marker::PhantomData;
43use core::num::NonZeroU32;
44use core::ops::Range;
45
46// ───────────────────────────────────────────────────────────────────────────
47// Couture register : appel synchrone + décodage errno
48// ───────────────────────────────────────────────────────────────────────────
49
50/// Appelle `io_uring_register(2)` sur le FD du ring et décode le retour.
51///
52/// Rend la valeur non-négative renvoyée par le kernel (`0` en général, l'**id**
53/// pour `REGISTER_PERSONALITY`, un compte pour certains opcodes).
54///
55/// # Safety
56///
57/// `(opcode, arg, nr_args)` doivent être **cohérents** : `arg` pointe une
58/// structure valide et accessible du type attendu par `opcode`, dimensionnée
59/// selon la convention du register opcode (`nr_args`). L'appelant emprunte le
60/// ring `&mut` le temps de l'appel (pas de soumission concurrente).
61unsafe fn register_raw(ring: &IoUring, opcode: u32, arg: u64, nr_args: u32) -> Result<i32, Errno> {
62    // SAFETY: déléguée à l'appelant (cf. doc) ; `fd_raw` est un ring fd valide.
63    let ret = unsafe { syscall::register(ring.fd_raw(), opcode, arg, nr_args) };
64    if ret < 0 {
65        return Err(raw::errno_from_negative_syscall_ret(ret));
66    }
67    // Les register opcodes rendent un petit entier non-négatif (toujours ≤ i32).
68    Ok(i32::try_from(ret).unwrap_or(0))
69}
70
71/// Convertit un indice `u32` (slot/buffer) en `usize` (toujours valide sur la
72/// cible LP64). La borne effective est vérifiée par le `get`/`get_mut` qui suit
73/// (l'`expect` est structurellement inatteignable, comme `ring::usz`).
74fn usz(index: u32) -> usize {
75    usize::try_from(index).expect("u32 ⊆ usize sur cible LP64")
76}
77
78/// `nr_args` d'un register opcode dont l'argument est une **structure unique** :
79/// la **taille en octets** de cette structure (convention kernel de
80/// `FILES2`/`BUFFERS2` et `*_UPDATE2`). Les tailles (32 o) sont figées par les
81/// `assert` de layout de `raw.rs` ⇒ l'`unwrap_or` est structurellement inerte.
82fn struct_nr_args<T>() -> u32 {
83    u32::try_from(core::mem::size_of::<T>()).unwrap_or(0)
84}
85
86// ───────────────────────────────────────────────────────────────────────────
87// Types de la surface 3a
88// ───────────────────────────────────────────────────────────────────────────
89
90/// Cible d'un slot pour une variante « direct » : indice **précis**, ou
91/// **auto-allocation** par le kernel.
92///
93/// [`FixedSlotTarget::Alloc`] est un `enum` typé, **jamais** la sentinelle
94/// kernel `IORING_FILE_INDEX_ALLOC` (`~0U`) exposée (ADR-021 conv. 1).
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum FixedSlotTarget {
97    /// Slot précis (l'indice exact dans la table de FD fixes).
98    Index(u32),
99    /// Le kernel choisit un slot libre (dans la plage d'`set_alloc_range`) et le
100    /// rend dans `cqe->res` ([`Completion::allocated_slot`]) ; `-ENFILE` si plein.
101    Alloc,
102}
103
104impl FixedSlotTarget {
105    /// Encode le `file_index` du SQE : `Alloc` ⇒ sentinelle kernel `~0U` ;
106    /// `Index(n)` ⇒ `n + 1` (le kernel décrémente ; `0` = « pas de slot »).
107    fn to_file_index(self) -> Result<u32, Errno> {
108        match self {
109            Self::Alloc => Ok(raw::IORING_FILE_INDEX_ALLOC),
110            // `n + 1` : `u32::MAX` (qui collisionnerait avec `~0U`/Alloc) ⇒ EINVAL.
111            Self::Index(n) => n.checked_add(1).ok_or(Errno::EINVAL),
112        }
113    }
114}
115
116/// Identité (credentials) enregistrée dans le ring ([`IoUring::register_personality`]).
117///
118/// L'**id** opaque est placé dans `sqe.personality` (via
119/// [`IoUring::with_personality`]) pour exécuter une op avec ces credentials.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct Personality(u16);
122
123impl Personality {
124    /// Id kernel de la personality (placé dans `sqe.personality`).
125    #[must_use]
126    pub fn id(self) -> u16 {
127        self.0
128    }
129
130    /// Id brut, usage interne (façade `with_personality`).
131    pub(crate) fn raw(self) -> u16 {
132        self.0
133    }
134}
135
136/// Plafonds de workers io-wq rendus par [`IoUring::set_work_queue_max_workers`]
137/// (les valeurs **précédentes**, réécrites par le kernel).
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
139pub struct WorkQueueWorkerLimits {
140    /// Plafond de la catégorie **bornée** (`IO_WQ_BOUND` : I/O disque).
141    pub bounded: u32,
142    /// Plafond de la catégorie **non-bornée** (`IO_WQ_UNBOUND` : réseau).
143    pub unbounded: u32,
144}
145
146/// Configuration du busy-poll NAPI ([`IoUring::register_napi`]).
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
148pub struct NapiConfig {
149    /// Délai de busy-poll en **microsecondes** (`busy_poll_to`). `0` désactive.
150    pub busy_poll_us: u32,
151    /// Préfère le busy-poll (`prefer_busy_poll`).
152    pub prefer_busy_poll: bool,
153}
154
155/// Source d'horloge des timeouts du ring ([`IoUring::register_clock`]).
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum ClockSource {
158    /// `CLOCK_MONOTONIC` : monotone, hors veille.
159    Monotonic,
160    /// `CLOCK_BOOTTIME` : monotone incluant le temps de veille.
161    Boottime,
162    /// `CLOCK_REALTIME` : temps réel (peut sauter).
163    Realtime,
164}
165
166impl ClockSource {
167    /// Valeur kernel (`CLOCK_*`).
168    fn clockid(self) -> u32 {
169        match self {
170            Self::Monotonic => raw::CLOCK_MONOTONIC,
171            Self::Boottime => raw::CLOCK_BOOTTIME,
172            Self::Realtime => raw::CLOCK_REALTIME,
173        }
174    }
175}
176
177// ───────────────────────────────────────────────────────────────────────────
178// Table de descripteurs fixes — FixedFdTable
179// ───────────────────────────────────────────────────────────────────────────
180
181/// Table de descripteurs de fichiers **fixes** (`REGISTER_FILES2`, 13).
182///
183/// Possède les [`OwnedFd`] des slots remplis ; sparse par défaut (slots vides à
184/// remplir plus tard). Les opérations sur FD fixe ([`FixedSlot`]) évitent au
185/// kernel la résolution du FD à chaque op (`IOSQE_FIXED_FILE`).
186#[derive(Debug)]
187pub struct FixedFdTable {
188    /// Un slot par capacité : `Some(fd)` rempli, `None` vide (sparse). Possède
189    /// les FD tant qu'ils sont enregistrés (restitués au désenregistrement).
190    slots: Vec<Option<OwnedFd>>,
191}
192
193impl FixedFdTable {
194    /// `REGISTER_FILES2` (13) : table **sparse** de `capacity` slots vides.
195    ///
196    /// # Errors
197    ///
198    /// Erreurs de `io_uring_register` : [`Errno::EINVAL`] (capacité trop grande,
199    /// déjà enregistré), [`Errno::EMFILE`]/[`Errno::ENOMEM`].
200    pub fn register(ring: &mut IoUring, capacity: NonZeroU32) -> Result<Self, Errno> {
201        let nr = capacity.get();
202        let rr = raw::IoUringRsrcRegister {
203            nr,
204            flags: raw::IORING_RSRC_REGISTER_SPARSE,
205            resv2: 0,
206            data: 0,
207            tags: 0,
208        };
209        // SAFETY: FILES2 attend une `io_uring_rsrc_register` (nr_args == 1) ;
210        // sparse ⇒ `data` nul (aucun FD initial lu). `rr` vit pendant l'appel.
211        unsafe {
212            register_raw(
213                ring,
214                raw::IORING_REGISTER_FILES2,
215                core::ptr::from_ref(&rr) as u64,
216                struct_nr_args::<raw::IoUringRsrcRegister>(),
217            )?;
218        }
219        Ok(Self {
220            slots: (0..nr).map(|_| None).collect(),
221        })
222    }
223
224    /// `REGISTER_FILES2` (13) avec un jeu **initial** de FD (slots remplis dans
225    /// l'ordre, à partir de l'indice 0). Les FD sont possédés par la table.
226    ///
227    /// # Errors
228    ///
229    /// [`Errno::EINVAL`] si `fds` est vide ou déborde un `u32` ; erreurs de
230    /// `io_uring_register`.
231    pub fn register_with(ring: &mut IoUring, fds: Vec<OwnedFd>) -> Result<Self, Errno> {
232        let nr = u32::try_from(fds.len()).map_err(|_| Errno::EINVAL)?;
233        if nr == 0 {
234            return Err(Errno::EINVAL);
235        }
236        // Tableau de FD bruts lu par le kernel pendant l'enregistrement synchrone.
237        let raw_fds: Vec<i32> = fds.iter().map(AsRawFd::as_raw_fd).collect();
238        let rr = raw::IoUringRsrcRegister {
239            nr,
240            flags: 0,
241            resv2: 0,
242            data: raw_fds.as_ptr() as u64,
243            tags: 0,
244        };
245        // SAFETY: FILES2 (nr_args == 1) ; `data` pointe `nr` `i32` (`raw_fds`)
246        // vivants pour la durée de l'appel synchrone ; `rr` vit aussi.
247        unsafe {
248            register_raw(
249                ring,
250                raw::IORING_REGISTER_FILES2,
251                core::ptr::from_ref(&rr) as u64,
252                struct_nr_args::<raw::IoUringRsrcRegister>(),
253            )?;
254        }
255        Ok(Self {
256            slots: fds.into_iter().map(Some).collect(),
257        })
258    }
259
260    /// `REGISTER_FILES_UPDATE2` (14) : place/remplace le FD du slot `slot`.
261    ///
262    /// **Consomme** tout FD précédemment présent dans ce slot (fermé) — transfert
263    /// d'ownership voulu, gravé dans la signature (ADR-032 : consommation
264    /// explicite ≠ discard).
265    ///
266    /// # Errors
267    ///
268    /// [`Errno::EINVAL`] si `slot` est hors borne ; erreurs de `io_uring_register`.
269    pub fn set(&mut self, ring: &mut IoUring, slot: u32, fd: OwnedFd) -> Result<(), Errno> {
270        let slot_ref = self.slots.get_mut(usz(slot)).ok_or(Errno::EINVAL)?;
271        let raw_fd = fd.as_raw_fd();
272        let up = raw::IoUringRsrcUpdate2 {
273            offset: slot,
274            resv: 0,
275            data: core::ptr::from_ref(&raw_fd) as u64,
276            tags: 0,
277            nr: 1,
278            resv2: 0,
279        };
280        // SAFETY: FILES_UPDATE2 (nr_args == 1) ; `data` pointe un `i32` (`raw_fd`)
281        // vivant ; `up` vit pendant l'appel. Le slot n'est mis à jour côté Air
282        // qu'**après** le succès kernel (sinon `fd` est rendu via le `?`).
283        unsafe {
284            register_raw(
285                ring,
286                raw::IORING_REGISTER_FILES_UPDATE2,
287                core::ptr::from_ref(&up) as u64,
288                struct_nr_args::<raw::IoUringRsrcUpdate2>(),
289            )?;
290        }
291        // L'ancien FD (le cas échéant) est fermé par ce remplacement.
292        *slot_ref = Some(fd);
293        Ok(())
294    }
295
296    /// `REGISTER_FILES_UPDATE2` (14) avec `fd = -1` : **vide** le slot. Rend le
297    /// FD qu'il contenait (zéro discard), ou `None` s'il était déjà vide.
298    ///
299    /// # Errors
300    ///
301    /// [`Errno::EINVAL`] si `slot` est hors borne ; erreurs de `io_uring_register`.
302    pub fn clear(&mut self, ring: &mut IoUring, slot: u32) -> Result<Option<OwnedFd>, Errno> {
303        let slot_ref = self.slots.get_mut(usz(slot)).ok_or(Errno::EINVAL)?;
304        let clear_fd: i32 = -1;
305        let up = raw::IoUringRsrcUpdate2 {
306            offset: slot,
307            resv: 0,
308            data: core::ptr::from_ref(&clear_fd) as u64,
309            tags: 0,
310            nr: 1,
311            resv2: 0,
312        };
313        // SAFETY: FILES_UPDATE2 (nr_args == 1) ; `data` pointe un `i32` (`-1`)
314        // vivant ; `up` vit pendant l'appel.
315        unsafe {
316            register_raw(
317                ring,
318                raw::IORING_REGISTER_FILES_UPDATE2,
319                core::ptr::from_ref(&up) as u64,
320                struct_nr_args::<raw::IoUringRsrcUpdate2>(),
321            )?;
322        }
323        Ok(slot_ref.take())
324    }
325
326    /// `REGISTER_FILE_ALLOC_RANGE` (25) : borne la plage `[start, end)` des slots
327    /// éligibles à l'auto-allocation ([`FixedSlotTarget::Alloc`]).
328    ///
329    /// # Errors
330    ///
331    /// [`Errno::EINVAL`] si `range` est inversé ou déborde la capacité ; erreurs
332    /// de `io_uring_register`.
333    pub fn set_alloc_range(&mut self, ring: &mut IoUring, range: Range<u32>) -> Result<(), Errno> {
334        // `end - start` : `None` (donc EINVAL) si `start > end`.
335        let len = range.end.checked_sub(range.start).ok_or(Errno::EINVAL)?;
336        let capacity = u32::try_from(self.slots.len()).unwrap_or(u32::MAX);
337        if range.end > capacity {
338            return Err(Errno::EINVAL);
339        }
340        let index_range = raw::IoUringFileIndexRange {
341            off: range.start,
342            len,
343            resv: 0,
344        };
345        // SAFETY: FILE_ALLOC_RANGE attend une `io_uring_file_index_range`
346        // (nr_args == 1) ; `index_range` vit pendant l'appel.
347        unsafe {
348            register_raw(
349                ring,
350                raw::IORING_REGISTER_FILE_ALLOC_RANGE,
351                core::ptr::from_ref(&index_range) as u64,
352                0,
353            )?;
354        }
355        Ok(())
356    }
357
358    /// `UNREGISTER_FILES` (3) : désenregistre toute la table et **rend** les FD
359    /// restants (restitution intégrale, ADR-032).
360    ///
361    /// **Sûreté par construction** : un [`FixedSlot`] emprunté à la table ne peut
362    /// pas survivre à ce désenregistrement (qui consomme `self`) — le code
363    /// suivant **ne compile pas** :
364    ///
365    /// ```compile_fail
366    /// # use air_sys_syscall::io_uring::{FixedFdTable, IoUring};
367    /// # use core::num::NonZeroU32;
368    /// # fn demo(ring: &mut IoUring) {
369    /// let table = FixedFdTable::register(ring, NonZeroU32::new(4).unwrap()).unwrap();
370    /// let slot = table.slot(0).unwrap();         // emprunte `table`
371    /// let _fds = table.unregister(ring).unwrap(); // déplace `table`…
372    /// let _ = slot.index();                       // …emprunt encore vivant ⇒ ERREUR
373    /// # }
374    /// ```
375    ///
376    /// # Errors
377    ///
378    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`] si rien n'est
379    /// enregistré, [`Errno::EBUSY`]). En cas d'erreur, les FD possédés sont
380    /// fermés (la table est consommée).
381    pub fn unregister(self, ring: &mut IoUring) -> Result<Vec<OwnedFd>, Errno> {
382        // SAFETY: UNREGISTER_FILES ne lit aucune mémoire (arg/nr_args nuls).
383        unsafe {
384            register_raw(ring, raw::IORING_UNREGISTER_FILES, 0, 0)?;
385        }
386        Ok(self.slots.into_iter().flatten().collect())
387    }
388
389    /// Référence empruntée au slot `slot` **pour usage en opération**
390    /// (`IOSQE_FIXED_FILE`), si l'indice est **dans la capacité** de la table.
391    /// `None` si hors borne.
392    ///
393    /// La référence est bornée par l'indice, pas par le suivi d'ownership : un
394    /// slot peut avoir été rempli par une variante « direct » (côté kernel, sans
395    /// `OwnedFd` côté Air). Référencer un slot **vide** est sûr — l'op échoue
396    /// alors proprement à la complétion (`EBADF`), jamais d'UB.
397    #[must_use]
398    pub fn slot(&self, slot: u32) -> Option<FixedSlot<'_>> {
399        if usz(slot) < self.slots.len() {
400            Some(FixedSlot {
401                index: slot,
402                _marker: PhantomData,
403            })
404        } else {
405            None
406        }
407    }
408}
409
410/// Référence empruntée à un slot d'une [`FixedFdTable`], liée à la table par
411/// *lifetime* — inutilisable après son désenregistrement (sûreté par
412/// construction).
413#[derive(Debug, Clone, Copy)]
414pub struct FixedSlot<'t> {
415    /// Indice du slot dans la table.
416    index: u32,
417    /// Lie la référence à la table (empêche l'usage après `unregister`).
418    _marker: PhantomData<&'t FixedFdTable>,
419}
420
421impl FixedSlot<'_> {
422    /// Indice du slot dans la table de FD fixes.
423    #[must_use]
424    pub fn index(&self) -> u32 {
425        self.index
426    }
427}
428
429// ───────────────────────────────────────────────────────────────────────────
430// Buffers enregistrés — RegisteredBuffers
431// ───────────────────────────────────────────────────────────────────────────
432
433/// Origine des buffers d'une [`RegisteredBuffers`] (mémoire possédée).
434#[derive(Debug)]
435enum BufferBacking {
436    /// Buffers `Vec<u8>` possédés (épinglés par le kernel à l'enregistrement).
437    Owned(Vec<Vec<u8>>),
438    /// Régions mmap possédées (data plane : `memfd` partagé AirCom).
439    Mapped(Vec<MmapRegion>),
440    /// Buffers **clonés** d'un autre ring ([`RegisteredBuffers::clone_from`]) :
441    /// la mémoire est possédée par le ring **source** (qui doit rester vivant).
442    Cloned,
443}
444
445/// Buffers enregistrés (`REGISTER_BUFFERS2`, 15) : l'épinglage et la traduction
446/// d'adresses sont faits **une fois** ⇒ `read_fixed`/`write_fixed` évitent ce
447/// coût par op (cas chaud du data plane AirCom).
448///
449/// Possède les buffers épinglés ; les références d'usage
450/// ([`RegisteredBufferSlice`]) y sont liées par *lifetime*.
451#[derive(Debug)]
452pub struct RegisteredBuffers {
453    backing: BufferBacking,
454}
455
456impl RegisteredBuffers {
457    /// Construit les `iovec` désignant `bufs` puis enregistre via `BUFFERS2`.
458    /// Cœur commun [`Self::register`] / [`Self::register_mmap`].
459    fn register_iovecs(
460        ring: &mut IoUring,
461        iovecs: &[raw::Iovec],
462        backing: BufferBacking,
463    ) -> Result<Self, Errno> {
464        let nr = u32::try_from(iovecs.len()).map_err(|_| Errno::EINVAL)?;
465        if nr == 0 {
466            return Err(Errno::EINVAL);
467        }
468        let rr = raw::IoUringRsrcRegister {
469            nr,
470            flags: 0,
471            resv2: 0,
472            data: iovecs.as_ptr() as u64,
473            tags: 0,
474        };
475        // SAFETY: BUFFERS2 attend une `io_uring_rsrc_register` (nr_args == 1) ;
476        // `data` pointe `nr` `iovec` vivants pour la durée de l'appel ; le kernel
477        // épingle les pages référencées (synchrone). La mémoire des buffers est
478        // possédée par `backing` (vivante tant que la table existe).
479        unsafe {
480            register_raw(
481                ring,
482                raw::IORING_REGISTER_BUFFERS2,
483                core::ptr::from_ref(&rr) as u64,
484                struct_nr_args::<raw::IoUringRsrcRegister>(),
485            )?;
486        }
487        Ok(Self { backing })
488    }
489
490    /// `REGISTER_BUFFERS2` (15) : épingle des buffers `Vec<u8>` possédés.
491    ///
492    /// # Errors
493    ///
494    /// [`Errno::EINVAL`] si `buffers` est vide ou déborde un `u32` ; erreurs de
495    /// `io_uring_register` ([`Errno::ENOMEM`], [`Errno::EFAULT`]).
496    pub fn register(ring: &mut IoUring, buffers: Vec<Vec<u8>>) -> Result<Self, Errno> {
497        let iovecs: Vec<raw::Iovec> = buffers
498            .iter()
499            .map(|b| raw::Iovec {
500                iov_base: b.as_ptr() as *mut u8,
501                iov_len: b.len(),
502            })
503            .collect();
504        Self::register_iovecs(ring, &iovecs, BufferBacking::Owned(buffers))
505    }
506
507    /// `REGISTER_BUFFERS2` (15) adossé à des [`MmapRegion`] **possédées** (data
508    /// plane : `memfd` partagé). Réutilise le handle de vivacité de `family-mem`.
509    ///
510    /// # Errors
511    ///
512    /// Voir [`RegisteredBuffers::register`].
513    pub fn register_mmap(ring: &mut IoUring, regions: Vec<MmapRegion>) -> Result<Self, Errno> {
514        let iovecs: Vec<raw::Iovec> = regions
515            .iter()
516            .map(|r| raw::Iovec {
517                iov_base: r.as_ptr() as *mut u8,
518                iov_len: r.len(),
519            })
520            .collect();
521        Self::register_iovecs(ring, &iovecs, BufferBacking::Mapped(regions))
522    }
523
524    /// `REGISTER_BUFFERS_UPDATE` (16) : remplace le buffer d'indice `index`.
525    ///
526    /// **Consomme** le buffer précédent (remplacement explicite). Réservé au
527    /// backing `Vec<u8>` (les variantes mmap/clonées rendent [`Errno::EINVAL`]).
528    ///
529    /// # Errors
530    ///
531    /// [`Errno::EINVAL`] si `index` est hors borne, ou si le backing n'est pas
532    /// `Vec<u8>` ; erreurs de `io_uring_register`.
533    pub fn update(&mut self, ring: &mut IoUring, index: u32, buffer: Vec<u8>) -> Result<(), Errno> {
534        let BufferBacking::Owned(buffers) = &mut self.backing else {
535            return Err(Errno::EINVAL);
536        };
537        let slot = buffers.get_mut(usz(index)).ok_or(Errno::EINVAL)?;
538        let iov = raw::Iovec {
539            iov_base: buffer.as_ptr() as *mut u8,
540            iov_len: buffer.len(),
541        };
542        let up = raw::IoUringRsrcUpdate2 {
543            offset: index,
544            resv: 0,
545            data: core::ptr::from_ref(&iov) as u64,
546            tags: 0,
547            nr: 1,
548            resv2: 0,
549        };
550        // SAFETY: BUFFERS_UPDATE (nr_args == 1) ; `data` pointe un `iovec` vivant
551        // désignant `buffer` (vivant) ; `up` vit pendant l'appel. Le slot n'est
552        // remplacé côté Air qu'**après** le succès kernel.
553        unsafe {
554            register_raw(
555                ring,
556                raw::IORING_REGISTER_BUFFERS_UPDATE,
557                core::ptr::from_ref(&up) as u64,
558                struct_nr_args::<raw::IoUringRsrcUpdate2>(),
559            )?;
560        }
561        *slot = buffer;
562        Ok(())
563    }
564
565    /// `REGISTER_CLONE_BUFFERS` (30) : clone les buffers enregistrés du ring
566    /// `src` (partage thread-per-core sans réépinglage).
567    ///
568    /// La mémoire reste possédée par `src` : la table rendue ne possède rien et
569    /// n'est valide que **tant que `src` garde ses buffers enregistrés et
570    /// vivants** (consommation par contrat). Les [`RegisteredBufferSlice`] à
571    /// utiliser proviennent de la table de `src` (mêmes adresses, même process).
572    ///
573    /// # Errors
574    ///
575    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`], [`Errno::EBUSY`] si la
576    /// table de destination est déjà peuplée).
577    pub fn clone_from(ring: &mut IoUring, src: &IoUring) -> Result<Self, Errno> {
578        let clone = raw::IoUringCloneBuffers {
579            src_fd: src.fd_raw().cast_unsigned(),
580            flags: 0,
581            pad: [0; 6],
582        };
583        // SAFETY: CLONE_BUFFERS attend une `io_uring_clone_buffers` ; `clone` vit
584        // pendant l'appel ; `src_fd` est le FD du ring source vivant.
585        unsafe {
586            register_raw(
587                ring,
588                raw::IORING_REGISTER_CLONE_BUFFERS,
589                core::ptr::from_ref(&clone) as u64,
590                1,
591            )?;
592        }
593        Ok(Self {
594            backing: BufferBacking::Cloned,
595        })
596    }
597
598    /// `UNREGISTER_BUFFERS` (1) : désenregistre et **rend** les buffers `Vec<u8>`
599    /// possédés (restitution intégrale, ADR-032).
600    ///
601    /// Pour un backing mmap, les régions sont **consommées** (munmap au dernier
602    /// drop — la signature `Vec<Vec<u8>>` ne peut les porter ; clonez la
603    /// [`MmapRegion`] avant si besoin). Pour un clone, rien à rendre.
604    ///
605    /// **Sûreté par construction** : un [`RegisteredBufferSlice`] ne peut pas
606    /// survivre à ce désenregistrement — le code suivant **ne compile pas** :
607    ///
608    /// ```compile_fail
609    /// # use air_sys_syscall::io_uring::{IoUring, RegisteredBuffers};
610    /// # fn demo(ring: &mut IoUring) {
611    /// let buffers = RegisteredBuffers::register(ring, vec![vec![0u8; 16]]).unwrap();
612    /// let slice = buffers.slice(0, 0..8).unwrap();  // emprunte `buffers`
613    /// let _ = buffers.unregister(ring).unwrap();     // déplace `buffers`…
614    /// let _ = slice.len();                           // …emprunt encore vivant ⇒ ERREUR
615    /// # }
616    /// ```
617    ///
618    /// # Errors
619    ///
620    /// Erreurs de `io_uring_register`. En cas d'erreur, les buffers possédés sont
621    /// libérés (la table est consommée).
622    pub fn unregister(self, ring: &mut IoUring) -> Result<Vec<Vec<u8>>, Errno> {
623        // SAFETY: UNREGISTER_BUFFERS ne lit aucune mémoire (arg/nr_args nuls).
624        unsafe {
625            register_raw(ring, raw::IORING_UNREGISTER_BUFFERS, 0, 0)?;
626        }
627        match self.backing {
628            BufferBacking::Owned(buffers) => Ok(buffers),
629            // Régions munmappées / clone sans mémoire propre : rien à restituer.
630            BufferBacking::Mapped(_) | BufferBacking::Cloned => Ok(Vec::new()),
631        }
632    }
633
634    /// Tranche `[range)` du buffer enregistré d'indice `index`, pour
635    /// `read_fixed`/`write_fixed`. `None` si l'indice/range est hors borne, ou si
636    /// le backing est cloné (géométrie inconnue côté destination).
637    #[must_use]
638    pub fn slice(&self, index: u32, range: Range<usize>) -> Option<RegisteredBufferSlice<'_>> {
639        let idx = usz(index);
640        let (base, buf_len) = match &self.backing {
641            BufferBacking::Owned(buffers) => {
642                let buf = buffers.get(idx)?;
643                (buf.as_ptr() as u64, buf.len())
644            }
645            BufferBacking::Mapped(regions) => {
646                let region = regions.get(idx)?;
647                (region.as_ptr() as u64, region.len())
648            }
649            BufferBacking::Cloned => return None,
650        };
651        if range.end > buf_len {
652            return None;
653        }
654        // `end - start` : `None` si `start > end`.
655        let span = range.end.checked_sub(range.start)?;
656        let start = u64::try_from(range.start).ok()?;
657        let addr = base.checked_add(start)?;
658        Some(RegisteredBufferSlice {
659            index,
660            addr,
661            len: span,
662            _marker: PhantomData,
663        })
664    }
665}
666
667/// Référence à une tranche d'un buffer enregistré, liée par *lifetime* à la
668/// [`RegisteredBuffers`] — `read_fixed` sur une tranche périmée ne compile pas.
669#[derive(Debug, Clone, Copy)]
670pub struct RegisteredBufferSlice<'b> {
671    /// Indice du buffer enregistré (`buf_index` du SQE).
672    index: u32,
673    /// Adresse de la tranche (dans la mémoire épinglée).
674    addr: u64,
675    /// Longueur de la tranche en octets.
676    len: usize,
677    /// Lie la tranche à la table (empêche l'usage après `unregister`).
678    _marker: PhantomData<&'b RegisteredBuffers>,
679}
680
681impl RegisteredBufferSlice<'_> {
682    /// Indice du buffer enregistré.
683    #[must_use]
684    pub fn index(&self) -> u32 {
685        self.index
686    }
687
688    /// Longueur de la tranche en octets.
689    #[must_use]
690    pub fn len(&self) -> usize {
691        self.len
692    }
693
694    /// `true` si la tranche est vide.
695    #[must_use]
696    pub fn is_empty(&self) -> bool {
697        self.len == 0
698    }
699}
700
701// ───────────────────────────────────────────────────────────────────────────
702// Registrations portées par l'IoUring (ring fd, eventfd, personality, io-wq,
703// napi, horloge) + variantes « direct »/« fixed » et fixed_fd_install.
704// ───────────────────────────────────────────────────────────────────────────
705
706impl IoUring {
707    // ── Ring fd enregistré (§4) ───────────────────────────────────────────
708
709    /// `REGISTER_RING_FDS` (20) : enregistre le FD du ring → les
710    /// `io_uring_enter` suivants utilisent `IORING_ENTER_REGISTERED_RING` (pas
711    /// de résolution de FD). Idempotent côté façade : un second appel
712    /// ré-enregistre (le kernel rend l'index).
713    ///
714    /// # Errors
715    ///
716    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`] si déjà enregistré côté
717    /// kernel, [`Errno::ENXIO`]).
718    pub fn register_ring_fd(&mut self) -> Result<(), Errno> {
719        let mut update = raw::IoUringRsrcUpdate {
720            offset: u32::MAX,
721            resv: 0,
722            data: u64::from(self.fd_raw().cast_unsigned()),
723        };
724        // SAFETY: RING_FDS attend un tableau de `io_uring_rsrc_update`
725        // (nr_args == 1) ; `update` vit pendant l'appel et le kernel y **réécrit**
726        // l'index assigné.
727        let ret = unsafe {
728            syscall::register(
729                self.fd_raw(),
730                raw::IORING_REGISTER_RING_FDS,
731                core::ptr::from_mut(&mut update) as u64,
732                1,
733            )
734        };
735        if ret < 0 {
736            return Err(raw::errno_from_negative_syscall_ret(ret));
737        }
738        self.enter_ring_index = Some(update.offset);
739        Ok(())
740    }
741
742    /// `UNREGISTER_RING_FDS` (21) : désenregistre le FD du ring (retour aux
743    /// `enter` par FD ordinaire).
744    ///
745    /// # Errors
746    ///
747    /// [`Errno::EINVAL`] si le ring fd n'était pas enregistré ; erreurs de
748    /// `io_uring_register`.
749    pub fn unregister_ring_fd(&mut self) -> Result<(), Errno> {
750        let index = self.enter_ring_index.ok_or(Errno::EINVAL)?;
751        let mut update = raw::IoUringRsrcUpdate {
752            offset: index,
753            resv: 0,
754            data: 0,
755        };
756        // SAFETY: UNREGISTER_RING_FDS attend un tableau de `io_uring_rsrc_update`
757        // (nr_args == 1) ; `update` vit pendant l'appel. On désigne le FD
758        // ordinaire (jamais l'index) pour ce register.
759        let ret = unsafe {
760            syscall::register(
761                self.fd_raw(),
762                raw::IORING_UNREGISTER_RING_FDS,
763                core::ptr::from_mut(&mut update) as u64,
764                1,
765            )
766        };
767        if ret < 0 {
768            return Err(raw::errno_from_negative_syscall_ret(ret));
769        }
770        self.enter_ring_index = None;
771        Ok(())
772    }
773
774    // ── eventfd (§5) ──────────────────────────────────────────────────────
775
776    /// Cœur commun `register_eventfd`/`_async` : enregistre `efd` sous `opcode`.
777    fn register_eventfd_op(&mut self, efd: BorrowedFd<'_>, opcode: u32) -> Result<(), Errno> {
778        let fd = efd.as_raw_fd();
779        // SAFETY: REGISTER_EVENTFD[_ASYNC] attend un `*const i32` (nr_args == 1) ;
780        // `fd` (variable locale) vit pendant l'appel.
781        unsafe {
782            register_raw(self, opcode, core::ptr::from_ref(&fd) as u64, 1)?;
783        }
784        Ok(())
785    }
786
787    /// `REGISTER_EVENTFD` (4) : lie un `eventfd` (famille `ipc`) aux complétions
788    /// (le kernel y écrit à chaque CQE posté → réveil d'un reactor via epoll).
789    ///
790    /// # Errors
791    ///
792    /// [`Errno::EINVAL`] si un eventfd est déjà enregistré ; [`Errno::EBADF`] si
793    /// `efd` est invalide.
794    pub fn register_eventfd(&mut self, efd: BorrowedFd<'_>) -> Result<(), Errno> {
795        self.register_eventfd_op(efd, raw::IORING_REGISTER_EVENTFD)
796    }
797
798    /// `REGISTER_EVENTFD_ASYNC` (7) : comme [`IoUring::register_eventfd`] mais ne
799    /// notifie que pour les complétions traitées en **asynchrone** (filtre le
800    /// bruit des complétions inline).
801    ///
802    /// # Errors
803    ///
804    /// Voir [`IoUring::register_eventfd`].
805    pub fn register_eventfd_async(&mut self, efd: BorrowedFd<'_>) -> Result<(), Errno> {
806        self.register_eventfd_op(efd, raw::IORING_REGISTER_EVENTFD_ASYNC)
807    }
808
809    /// `UNREGISTER_EVENTFD` (5) : détache l'eventfd.
810    ///
811    /// # Errors
812    ///
813    /// [`Errno::EINVAL`] si aucun eventfd n'était enregistré.
814    pub fn unregister_eventfd(&mut self) -> Result<(), Errno> {
815        // SAFETY: UNREGISTER_EVENTFD ne lit aucune mémoire (arg/nr_args nuls).
816        unsafe {
817            register_raw(self, raw::IORING_UNREGISTER_EVENTFD, 0, 0)?;
818        }
819        Ok(())
820    }
821
822    // ── Personality (§6) ──────────────────────────────────────────────────
823
824    /// `REGISTER_PERSONALITY` (9) : enregistre les credentials du process et rend
825    /// un id ([`Personality`]) — une op peut ensuite s'exécuter avec ces
826    /// credentials (`sqe.personality`, via [`IoUring::with_personality`]).
827    ///
828    /// # Errors
829    ///
830    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`] si la table de
831    /// personalities est pleine).
832    pub fn register_personality(&mut self) -> Result<Personality, Errno> {
833        // SAFETY: REGISTER_PERSONALITY ne lit aucune mémoire (arg/nr_args nuls) ;
834        // le kernel rend l'id (> 0) en valeur de retour.
835        let id = unsafe { register_raw(self, raw::IORING_REGISTER_PERSONALITY, 0, 0)? };
836        let id = u16::try_from(id).map_err(|_| Errno::EINVAL)?;
837        Ok(Personality(id))
838    }
839
840    /// `UNREGISTER_PERSONALITY` (10) : retire la personality `p` (id passé en
841    /// `nr_args`).
842    ///
843    /// # Errors
844    ///
845    /// [`Errno::EINVAL`] si l'id n'est pas enregistré.
846    pub fn unregister_personality(&mut self, p: Personality) -> Result<(), Errno> {
847        // SAFETY: UNREGISTER_PERSONALITY ne lit aucune mémoire ; l'id voyage dans
848        // `nr_args` (convention kernel).
849        unsafe {
850            register_raw(
851                self,
852                raw::IORING_UNREGISTER_PERSONALITY,
853                0,
854                u32::from(p.raw()),
855            )?;
856        }
857        Ok(())
858    }
859
860    // ── Réglage du pool io-wq (§7) ────────────────────────────────────────
861
862    /// `REGISTER_IOWQ_AFF` (17) : fixe l'affinité CPU des workers io-wq.
863    ///
864    /// # Errors
865    ///
866    /// [`Errno::EINVAL`] (masque invalide) ; erreurs de `io_uring_register`.
867    pub fn set_work_queue_affinity(&mut self, cpus: &CpuSet) -> Result<(), Errno> {
868        let bytes = cpus.as_bytes();
869        let nr = u32::try_from(bytes.len()).map_err(|_| Errno::EINVAL)?;
870        // SAFETY: IOWQ_AFF attend un `cpumask` de `nr` octets ; `bytes` (vue du
871        // `CpuSet` emprunté) vit pendant l'appel.
872        unsafe {
873            register_raw(
874                self,
875                raw::IORING_REGISTER_IOWQ_AFF,
876                bytes.as_ptr() as u64,
877                nr,
878            )?;
879        }
880        Ok(())
881    }
882
883    /// `UNREGISTER_IOWQ_AFF` (18) : rétablit l'affinité par défaut des workers.
884    ///
885    /// # Errors
886    ///
887    /// Erreurs de `io_uring_register`.
888    pub fn clear_work_queue_affinity(&mut self) -> Result<(), Errno> {
889        // SAFETY: UNREGISTER_IOWQ_AFF ne lit aucune mémoire (arg/nr_args nuls).
890        unsafe {
891            register_raw(self, raw::IORING_UNREGISTER_IOWQ_AFF, 0, 0)?;
892        }
893        Ok(())
894    }
895
896    /// `REGISTER_IOWQ_MAX_WORKERS` (19) : plafonne les workers bornés/non-bornés
897    /// (`0` = inchangé). Rend les valeurs **précédentes** (réécrites par le
898    /// kernel) — maîtrise de l'empreinte CPU sur matériel modeste (Charte).
899    ///
900    /// # Errors
901    ///
902    /// Erreurs de `io_uring_register`.
903    pub fn set_work_queue_max_workers(
904        &mut self,
905        bounded: u32,
906        unbounded: u32,
907    ) -> Result<WorkQueueWorkerLimits, Errno> {
908        let mut limits = [bounded, unbounded];
909        // SAFETY: IOWQ_MAX_WORKERS attend un `[u32; 2]` (nr_args == 2) ; le kernel
910        // y réécrit les anciennes valeurs. `limits` vit pendant l'appel.
911        unsafe {
912            register_raw(
913                self,
914                raw::IORING_REGISTER_IOWQ_MAX_WORKERS,
915                limits.as_mut_ptr() as u64,
916                2,
917            )?;
918        }
919        Ok(WorkQueueWorkerLimits {
920            bounded: limits[raw::IO_WQ_BOUND],
921            unbounded: limits[raw::IO_WQ_UNBOUND],
922        })
923    }
924
925    // ── NAPI busy-poll (§8) ───────────────────────────────────────────────
926
927    /// `REGISTER_NAPI` (27) : active le busy-poll NAPI (latence réseau ↓, CPU ↑).
928    /// Rend la configuration **précédente** (réécrite par le kernel).
929    ///
930    /// # Errors
931    ///
932    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`] si NAPI indisponible).
933    pub fn register_napi(&mut self, config: NapiConfig) -> Result<NapiConfig, Errno> {
934        let mut napi = raw::IoUringNapi {
935            busy_poll_to: config.busy_poll_us,
936            prefer_busy_poll: u8::from(config.prefer_busy_poll),
937            pad: [0; 3],
938            resv: 0,
939        };
940        // SAFETY: REGISTER_NAPI attend une `io_uring_napi` ; le kernel y réécrit
941        // la config précédente. `napi` vit pendant l'appel (nr_args == 0, liburing).
942        unsafe {
943            register_raw(
944                self,
945                raw::IORING_REGISTER_NAPI,
946                core::ptr::from_mut(&mut napi) as u64,
947                0,
948            )?;
949        }
950        Ok(NapiConfig {
951            busy_poll_us: napi.busy_poll_to,
952            prefer_busy_poll: napi.prefer_busy_poll != 0,
953        })
954    }
955
956    /// `UNREGISTER_NAPI` (28) : désactive le busy-poll NAPI.
957    ///
958    /// # Errors
959    ///
960    /// Erreurs de `io_uring_register`.
961    pub fn unregister_napi(&mut self) -> Result<(), Errno> {
962        // Le kernel réécrit l'ancienne config dans le tampon passé ; ignorée ici.
963        let mut napi = raw::IoUringNapi::default();
964        // SAFETY: UNREGISTER_NAPI attend une `io_uring_napi` ; `napi` vit pendant
965        // l'appel (nr_args == 0, liburing).
966        unsafe {
967            register_raw(
968                self,
969                raw::IORING_UNREGISTER_NAPI,
970                core::ptr::from_mut(&mut napi) as u64,
971                0,
972            )?;
973        }
974        Ok(())
975    }
976
977    // ── Source d'horloge (§9) ─────────────────────────────────────────────
978
979    /// `REGISTER_CLOCK` (29) : fixe l'horloge des timeouts du ring (cohérent avec
980    /// `TimeoutFlags` du Temps 2c et `family-time`).
981    ///
982    /// # Errors
983    ///
984    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`] si l'horloge n'est pas
985    /// supportée).
986    pub fn register_clock(&mut self, clock: ClockSource) -> Result<(), Errno> {
987        let reg = raw::IoUringClockRegister {
988            clockid: clock.clockid(),
989            resv: [0; 3],
990        };
991        // SAFETY: REGISTER_CLOCK attend une `io_uring_clock_register` ; `reg` vit
992        // pendant l'appel (nr_args == 0, liburing).
993        unsafe {
994            register_raw(
995                self,
996                raw::IORING_REGISTER_CLOCK,
997                core::ptr::from_ref(&reg) as u64,
998                0,
999            )?;
1000        }
1001        Ok(())
1002    }
1003
1004    // ── Variantes « direct descriptor » (§2.1) ────────────────────────────
1005
1006    /// `IORING_OP_OPENAT2` (28) **direct** : range le FD ouvert dans un slot fixe
1007    /// (`slot`) au lieu de le rendre en FD ordinaire. Pour [`FixedSlotTarget::Alloc`],
1008    /// le slot choisi est rendu dans `cqe->res` ([`Completion::allocated_slot`]).
1009    ///
1010    /// # Errors
1011    ///
1012    /// [`Errno::EINVAL`] si `slot` est `Index(u32::MAX)` ; [`Errno::EBUSY`] si la
1013    /// SQ/le slab sont pleins.
1014    pub fn submit_openat2_direct(
1015        &mut self,
1016        dirfd: DirFd<'_>,
1017        path: alloc::ffi::CString,
1018        how: OpenHow,
1019        slot: FixedSlotTarget,
1020    ) -> Result<SubmissionToken, Errno> {
1021        let file_index = slot.to_file_index()?;
1022        let dirfd = match dirfd {
1023            DirFd::Cwd => raw::AT_FDCWD,
1024            DirFd::Fd(fd) => fd.as_raw_fd(),
1025        };
1026        // Pas de `O_CLOEXEC` ici : le kernel le **rejette** (`EINVAL`) sur un
1027        // descripteur direct (kernel-managed, non hérité à l'exec par nature ;
1028        // le `O_CLOEXEC` est posé à la matérialisation `fixed_fd_install`).
1029        let how = Box::new(raw::OpenHowRaw {
1030            flags: how.flags.bits(),
1031            mode: u64::from(how.mode),
1032            resolve: how.resolve.bits(),
1033        });
1034        let how_ptr = core::ptr::from_ref(&*how) as u64;
1035        let path_ptr = path.as_ptr() as u64;
1036        self.submit_op(Some(OwnedOp::Open { path, how }), |sqe| {
1037            sqe.opcode = raw::IORING_OP_OPENAT2;
1038            sqe.fd = dirfd;
1039            sqe.addr_or_splice_off_in = path_ptr;
1040            sqe.len = 24; // sizeof(open_how), figé par l'assert de layout
1041            sqe.off_or_addr2 = how_ptr;
1042            sqe.splice_fd_in_or_file_index = file_index;
1043        })
1044    }
1045
1046    /// `IORING_OP_ACCEPT` (13) **direct** : range le socket accepté dans un slot
1047    /// fixe. Pas de restitution d'adresse pair.
1048    ///
1049    /// Le kernel **rejette** `SOCK_CLOEXEC` sur un descripteur direct
1050    /// (kernel-managed) : ne pas l'ajouter (`flags` transmis tel quel). Le
1051    /// `O_CLOEXEC` est posé à la matérialisation (`fixed_fd_install`).
1052    ///
1053    /// # Errors
1054    ///
1055    /// Voir [`IoUring::submit_openat2_direct`].
1056    pub fn submit_accept_direct(
1057        &mut self,
1058        listener: BorrowedFd<'_>,
1059        flags: AcceptFlags,
1060        slot: FixedSlotTarget,
1061    ) -> Result<SubmissionToken, Errno> {
1062        let file_index = slot.to_file_index()?;
1063        let fd = listener.as_raw_fd();
1064        let accept_flags = flags.bits().cast_unsigned();
1065        self.submit_op(None, |sqe| {
1066            sqe.opcode = raw::IORING_OP_ACCEPT;
1067            sqe.fd = fd;
1068            sqe.op_flags = accept_flags;
1069            sqe.splice_fd_in_or_file_index = file_index;
1070        })
1071    }
1072
1073    /// `IORING_OP_SOCKET` (45) **direct** : crée un socket rangé dans un slot fixe.
1074    ///
1075    /// Le kernel **rejette** `SOCK_CLOEXEC` sur un descripteur direct : ne pas
1076    /// l'ajouter. Le `O_CLOEXEC` est posé à la matérialisation (`fixed_fd_install`).
1077    ///
1078    /// # Errors
1079    ///
1080    /// Voir [`IoUring::submit_openat2_direct`].
1081    pub fn submit_socket_direct(
1082        &mut self,
1083        domain: SocketDomain,
1084        ty: SocketType,
1085        protocol: i32,
1086        slot: FixedSlotTarget,
1087    ) -> Result<SubmissionToken, Errno> {
1088        let file_index = slot.to_file_index()?;
1089        let domain = domain as i32;
1090        let ty = ty as i32;
1091        let protocol = protocol.cast_unsigned();
1092        self.submit_op(None, |sqe| {
1093            sqe.opcode = raw::IORING_OP_SOCKET;
1094            sqe.fd = domain;
1095            sqe.off_or_addr2 = u64::from(ty.cast_unsigned());
1096            sqe.len = protocol;
1097            sqe.splice_fd_in_or_file_index = file_index;
1098        })
1099    }
1100
1101    // ── Opérations sur FD fixe (IOSQE_FIXED_FILE) ─────────────────────────
1102
1103    /// `IORING_OP_READ` (22) via un [`FixedSlot`] (`IOSQE_FIXED_FILE`) : lit dans
1104    /// `buffer` (déplacé dans le slot). Complétion via
1105    /// [`Completion::into_buffer_result`].
1106    ///
1107    /// # Errors
1108    ///
1109    /// [`Errno::EINVAL`] si `buffer.len()` déborde un `u32` ; [`Errno::EBUSY`]
1110    /// (SQ/slab pleins).
1111    pub fn submit_read_fixed_file(
1112        &mut self,
1113        slot: FixedSlot<'_>,
1114        mut buffer: Vec<u8>,
1115        offset: Option<u64>,
1116    ) -> Result<SubmissionToken, Errno> {
1117        let len = u32::try_from(buffer.len()).map_err(|_| Errno::EINVAL)?;
1118        let addr = buffer.as_mut_ptr() as u64;
1119        let off = offset.unwrap_or(u64::MAX);
1120        let index = slot.index().cast_signed();
1121        self.pending_options = self.pending_options.fixed_file();
1122        self.submit_op(Some(OwnedOp::Bytes(buffer)), |sqe| {
1123            sqe.opcode = raw::IORING_OP_READ;
1124            sqe.fd = index;
1125            sqe.addr_or_splice_off_in = addr;
1126            sqe.len = len;
1127            sqe.off_or_addr2 = off;
1128        })
1129    }
1130
1131    /// `IORING_OP_WRITE` (23) via un [`FixedSlot`] : écrit `buffer` (déplacé dans
1132    /// le slot). Complétion via [`Completion::into_buffer_result`].
1133    ///
1134    /// # Errors
1135    ///
1136    /// Voir [`IoUring::submit_read_fixed_file`].
1137    pub fn submit_write_fixed_file(
1138        &mut self,
1139        slot: FixedSlot<'_>,
1140        buffer: Vec<u8>,
1141        offset: Option<u64>,
1142    ) -> Result<SubmissionToken, Errno> {
1143        let len = u32::try_from(buffer.len()).map_err(|_| Errno::EINVAL)?;
1144        let addr = buffer.as_ptr() as u64;
1145        let off = offset.unwrap_or(u64::MAX);
1146        let index = slot.index().cast_signed();
1147        self.pending_options = self.pending_options.fixed_file();
1148        self.submit_op(Some(OwnedOp::Bytes(buffer)), |sqe| {
1149            sqe.opcode = raw::IORING_OP_WRITE;
1150            sqe.fd = index;
1151            sqe.addr_or_splice_off_in = addr;
1152            sqe.len = len;
1153            sqe.off_or_addr2 = off;
1154        })
1155    }
1156
1157    /// `IORING_OP_FIXED_FD_INSTALL` (54) : matérialise un [`FixedSlot`] (FD
1158    /// direct) en FD **ordinaire** (`O_CLOEXEC` posé). Complétion via
1159    /// [`Completion::opened_fd`].
1160    ///
1161    /// # Errors
1162    ///
1163    /// [`Errno::EBUSY`] (SQ/slab pleins) ; à la complétion, `EBADF` si le slot
1164    /// est vide.
1165    pub fn submit_fixed_fd_install(
1166        &mut self,
1167        slot: FixedSlot<'_>,
1168    ) -> Result<SubmissionToken, Errno> {
1169        let index = slot.index().cast_signed();
1170        self.pending_options = self.pending_options.fixed_file();
1171        self.submit_op(None, |sqe| {
1172            sqe.opcode = raw::IORING_OP_FIXED_FD_INSTALL;
1173            sqe.fd = index;
1174            // install_fd_flags = 0 ⇒ O_CLOEXEC posé (défaut Air, cohérent couche 0).
1175            sqe.op_flags = 0;
1176        })
1177    }
1178
1179    // ── read_fixed / write_fixed (buffer enregistré) ──────────────────────
1180
1181    /// `IORING_OP_READ_FIXED` (4) : lit `fd` dans la tranche d'un buffer
1182    /// enregistré (`slice`). Les octets atterrissent dans la mémoire épinglée
1183    /// (relue via la [`RegisteredBuffers`]). Complétion via
1184    /// [`Completion::into_result`] (nombre d'octets).
1185    ///
1186    /// # Errors
1187    ///
1188    /// [`Errno::EINVAL`] si la tranche déborde un `u32`/`u16` ; [`Errno::EBUSY`].
1189    pub fn submit_read_fixed(
1190        &mut self,
1191        fd: BorrowedFd<'_>,
1192        slice: RegisteredBufferSlice<'_>,
1193        offset: Option<u64>,
1194    ) -> Result<SubmissionToken, Errno> {
1195        let fd = fd.as_raw_fd();
1196        let len = u32::try_from(slice.len()).map_err(|_| Errno::EINVAL)?;
1197        let buf_index = u16::try_from(slice.index()).map_err(|_| Errno::EINVAL)?;
1198        let off = offset.unwrap_or(u64::MAX);
1199        let addr = slice.addr;
1200        self.submit_op(None, |sqe| {
1201            sqe.opcode = raw::IORING_OP_READ_FIXED;
1202            sqe.fd = fd;
1203            sqe.addr_or_splice_off_in = addr;
1204            sqe.len = len;
1205            sqe.off_or_addr2 = off;
1206            sqe.buf_index_or_group = buf_index;
1207        })
1208    }
1209
1210    /// `IORING_OP_WRITE_FIXED` (5) : écrit dans `fd` depuis la tranche d'un buffer
1211    /// enregistré. Complétion via [`Completion::into_result`] (nombre d'octets).
1212    ///
1213    /// # Errors
1214    ///
1215    /// Voir [`IoUring::submit_read_fixed`].
1216    pub fn submit_write_fixed(
1217        &mut self,
1218        fd: BorrowedFd<'_>,
1219        slice: RegisteredBufferSlice<'_>,
1220        offset: Option<u64>,
1221    ) -> Result<SubmissionToken, Errno> {
1222        let fd = fd.as_raw_fd();
1223        let len = u32::try_from(slice.len()).map_err(|_| Errno::EINVAL)?;
1224        let buf_index = u16::try_from(slice.index()).map_err(|_| Errno::EINVAL)?;
1225        let off = offset.unwrap_or(u64::MAX);
1226        let addr = slice.addr;
1227        self.submit_op(None, |sqe| {
1228            sqe.opcode = raw::IORING_OP_WRITE_FIXED;
1229            sqe.fd = fd;
1230            sqe.addr_or_splice_off_in = addr;
1231            sqe.len = len;
1232            sqe.off_or_addr2 = off;
1233            sqe.buf_index_or_group = buf_index;
1234        })
1235    }
1236}
1237
1238impl Completion {
1239    /// Slot choisi par le kernel pour une variante « direct » à cible
1240    /// [`FixedSlotTarget::Alloc`] (rendu dans `cqe->res`).
1241    ///
1242    /// # Errors
1243    ///
1244    /// L'[`Errno`] décodé de la complétion (ex. [`Errno::ENFILE`] si la plage
1245    /// d'auto-allocation est pleine), ou [`Errno::EINVAL`] si `res` est négatif
1246    /// hors errno.
1247    pub fn allocated_slot(self) -> Result<u32, Errno> {
1248        let res = self.into_result()?;
1249        u32::try_from(res).map_err(|_| Errno::EINVAL)
1250    }
1251}
1252
1253// ───────────────────────────────────────────────────────────────────────────
1254// Tests
1255// ───────────────────────────────────────────────────────────────────────────
1256//
1257// Les tests **purs** (encodage `FixedSlotTarget`, propagation d'erreur via le
1258// simulateur de syscalls) tournent **sous Miri**. Les tests d'**intégration**
1259// (kernel réel : registration, variantes direct, read/write fixed) portent
1260// `#[cfg_attr(miri, ignore)]` — Miri ne modélise pas `io_uring_register`/`enter`.
1261// La **sûreté de lifetime** (une référence ne survit pas au désenregistrement)
1262// est prouvée par les doctests `compile_fail` sur `FixedFdTable::unregister` et
1263// `RegisteredBuffers::unregister`.
1264
1265#[cfg(test)]
1266mod tests {
1267    use super::syscall::sim::{self, Syscall};
1268    use super::*;
1269    use crate::test_support::{sfd, std_file};
1270    use air_sys_types::fd::AsFd;
1271    use air_sys_types::fs::{OpenFlags, OpenHow};
1272    use air_sys_types::ipc::EventFdFlags;
1273    use air_sys_types::mem::{MapFlags, ProtectionFlags};
1274    use air_sys_types::net::{SocketDomain, SocketType};
1275    use core::sync::atomic::{AtomicU32, Ordering};
1276    use std::ffi::CString;
1277    use std::io::Read;
1278    use std::os::unix::ffi::OsStrExt;
1279    use std::path::{Path, PathBuf};
1280
1281    static COUNTER: AtomicU32 = AtomicU32::new(0);
1282
1283    fn ring8() -> IoUring {
1284        IoUring::new(NonZeroU32::new(8).expect("8 ≠ 0")).expect("io_uring_setup 6.12")
1285    }
1286
1287    fn nz(n: u32) -> NonZeroU32 {
1288        NonZeroU32::new(n).expect("n ≠ 0")
1289    }
1290
1291    fn temp_path(tag: &str) -> PathBuf {
1292        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1293        std::env::temp_dir().join(format!("air-iou3a-{}-{n}-{tag}", std::process::id()))
1294    }
1295
1296    fn cpath(p: &Path) -> CString {
1297        CString::new(p.as_os_str().as_bytes()).expect("chemin sans NUL")
1298    }
1299
1300    fn rw_file(path: &Path) -> std::fs::File {
1301        std::fs::OpenOptions::new()
1302            .read(true)
1303            .write(true)
1304            .create(true)
1305            .truncate(true)
1306            .open(path)
1307            .expect("ouverture fichier de test")
1308    }
1309
1310    fn rw_region(len: usize) -> MmapRegion {
1311        MmapRegion::new_anonymous(
1312            len,
1313            ProtectionFlags::READ | ProtectionFlags::WRITE,
1314            MapFlags::PRIVATE,
1315        )
1316        .expect("new_anonymous RW")
1317    }
1318
1319    fn complete_one(ring: &mut IoUring, expected: SubmissionToken) -> Completion {
1320        assert_eq!(ring.submit_and_wait(1).expect("submit_and_wait"), 1);
1321        let completion = ring.wait_completion().expect("wait_completion");
1322        assert_eq!(completion.token(), expected, "user_data round-trip");
1323        completion
1324    }
1325
1326    // ── Pur : encodage FixedSlotTarget (tourne sous Miri) ─────────────────
1327
1328    #[test]
1329    fn fixed_slot_target_encoding() {
1330        assert_eq!(
1331            FixedSlotTarget::Alloc.to_file_index(),
1332            Ok(raw::IORING_FILE_INDEX_ALLOC)
1333        );
1334        assert_eq!(FixedSlotTarget::Index(0).to_file_index(), Ok(1));
1335        assert_eq!(FixedSlotTarget::Index(41).to_file_index(), Ok(42));
1336        // `u32::MAX` collisionnerait avec la sentinelle Alloc ⇒ refusé en amont.
1337        assert_eq!(
1338            FixedSlotTarget::Index(u32::MAX).to_file_index(),
1339            Err(Errno::EINVAL)
1340        );
1341        // Couvre Debug/Eq/Copy des types de surface (lints `must_use`).
1342        assert_eq!(FixedSlotTarget::Alloc, FixedSlotTarget::Alloc);
1343        assert_ne!(FixedSlotTarget::Index(1), FixedSlotTarget::Alloc);
1344    }
1345
1346    #[test]
1347    fn surface_types_are_debug_and_eq() {
1348        // Couvre les dérivations Debug/Default/Eq des types de configuration.
1349        let limits = WorkQueueWorkerLimits::default();
1350        assert!(format!("{limits:?}").contains("bounded"));
1351        let napi = NapiConfig {
1352            busy_poll_us: 7,
1353            prefer_busy_poll: true,
1354        };
1355        assert_eq!(
1356            napi,
1357            NapiConfig {
1358                busy_poll_us: 7,
1359                prefer_busy_poll: true
1360            }
1361        );
1362        assert_ne!(ClockSource::Monotonic, ClockSource::Realtime);
1363        let p = Personality(3);
1364        assert_eq!(p.id(), 3);
1365        assert!(format!("{p:?}").contains("Personality"));
1366        assert_eq!(ClockSource::Monotonic.clockid(), raw::CLOCK_MONOTONIC);
1367        assert_eq!(ClockSource::Boottime.clockid(), raw::CLOCK_BOOTTIME);
1368        assert_eq!(ClockSource::Realtime.clockid(), raw::CLOCK_REALTIME);
1369    }
1370
1371    // ── Pur : propagation d'erreur via simulateur (tourne sous Miri) ──────
1372
1373    #[test]
1374    fn register_errors_propagate_through_facades() {
1375        sim::clear();
1376        let mut ring = ring8();
1377        // FixedFdTable::register → EINVAL injecté.
1378        sim::inject(Syscall::Register, -22); // -EINVAL
1379        assert_eq!(
1380            FixedFdTable::register(&mut ring, nz(4)).err(),
1381            Some(Errno::EINVAL)
1382        );
1383        // RegisteredBuffers::register → EINVAL injecté.
1384        sim::inject(Syscall::Register, -22);
1385        assert_eq!(
1386            RegisteredBuffers::register(&mut ring, vec![vec![0u8; 8]]).err(),
1387            Some(Errno::EINVAL)
1388        );
1389        // register_personality → EBADF injecté (-9).
1390        sim::inject(Syscall::Register, -9);
1391        assert_eq!(ring.register_personality().err(), Some(Errno::EBADF));
1392        // register_clock → EINVAL injecté.
1393        sim::inject(Syscall::Register, -22);
1394        assert_eq!(
1395            ring.register_clock(ClockSource::Monotonic).err(),
1396            Some(Errno::EINVAL)
1397        );
1398        // register_napi → EINVAL injecté.
1399        sim::inject(Syscall::Register, -22);
1400        assert_eq!(
1401            ring.register_napi(NapiConfig::default()).err(),
1402            Some(Errno::EINVAL)
1403        );
1404        // set_work_queue_max_workers → EINVAL injecté.
1405        sim::inject(Syscall::Register, -22);
1406        assert_eq!(
1407            ring.set_work_queue_max_workers(1, 1).err(),
1408            Some(Errno::EINVAL)
1409        );
1410        // register_eventfd → EBADF injecté.
1411        let efd = crate::ipc::eventfd2(0, EventFdFlags::empty()).expect("eventfd2");
1412        sim::inject(Syscall::Register, -9);
1413        assert_eq!(ring.register_eventfd(efd.as_fd()).err(), Some(Errno::EBADF));
1414        // register_ring_fd → EINVAL injecté (chemin d'erreur du syscall direct).
1415        sim::inject(Syscall::Register, -22);
1416        assert_eq!(ring.register_ring_fd().err(), Some(Errno::EINVAL));
1417        // set_work_queue_affinity → EINVAL injecté.
1418        let mut cpus = CpuSet::new();
1419        cpus.set(0);
1420        sim::inject(Syscall::Register, -22);
1421        assert_eq!(
1422            ring.set_work_queue_affinity(&cpus).err(),
1423            Some(Errno::EINVAL)
1424        );
1425        // clear_work_queue_affinity / unregister_eventfd / unregister_napi
1426        // (chemins d'erreur des opcodes sans argument).
1427        sim::inject(Syscall::Register, -22);
1428        assert_eq!(ring.clear_work_queue_affinity().err(), Some(Errno::EINVAL));
1429        sim::inject(Syscall::Register, -22);
1430        assert_eq!(ring.unregister_eventfd().err(), Some(Errno::EINVAL));
1431        sim::inject(Syscall::Register, -22);
1432        assert_eq!(ring.unregister_napi().err(), Some(Errno::EINVAL));
1433        sim::clear();
1434    }
1435
1436    #[test]
1437    fn register_with_and_buffers_reject_empty() {
1438        sim::clear();
1439        let mut ring = ring8();
1440        // register_with([]) → EINVAL **en amont** (aucun syscall).
1441        assert_eq!(
1442            FixedFdTable::register_with(&mut ring, Vec::new()).err(),
1443            Some(Errno::EINVAL)
1444        );
1445        // RegisteredBuffers::register([]) → EINVAL en amont.
1446        assert_eq!(
1447            RegisteredBuffers::register(&mut ring, Vec::new()).err(),
1448            Some(Errno::EINVAL)
1449        );
1450        assert_eq!(
1451            RegisteredBuffers::register_mmap(&mut ring, Vec::new()).err(),
1452            Some(Errno::EINVAL)
1453        );
1454    }
1455
1456    // ── Intégration : FixedFdTable (sparse, set/clear/unregister) ─────────
1457
1458    #[test]
1459    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1460    fn fixed_fd_table_sparse_bounds_and_unregister() {
1461        let mut ring = ring8();
1462        let table = FixedFdTable::register(&mut ring, nz(4)).expect("register sparse");
1463        // slot in-range → Some ; hors borne → None.
1464        assert!(table.slot(0).is_some());
1465        assert!(table.slot(3).is_some());
1466        assert!(table.slot(4).is_none());
1467        assert_eq!(table.slot(2).expect("slot 2").index(), 2);
1468        // Désenregistrement : aucun FD possédé (sparse) ⇒ Vec vide.
1469        let returned = table.unregister(&mut ring).expect("unregister");
1470        assert!(returned.is_empty());
1471    }
1472
1473    #[test]
1474    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1475    fn fixed_fd_table_register_with_set_clear() {
1476        let mut ring = ring8();
1477        // Deux eventfd comme FD possédés.
1478        let a = crate::ipc::eventfd2(0, EventFdFlags::empty()).expect("eventfd a");
1479        let b = crate::ipc::eventfd2(0, EventFdFlags::empty()).expect("eventfd b");
1480        let mut table = FixedFdTable::register_with(&mut ring, vec![a.into_fd(), b.into_fd()])
1481            .expect("register_with");
1482        // set hors borne → EINVAL (aucun syscall).
1483        let spare = crate::ipc::eventfd2(0, EventFdFlags::empty()).expect("eventfd spare");
1484        assert_eq!(
1485            table.set(&mut ring, 99, spare.into_fd()).err(),
1486            Some(Errno::EINVAL)
1487        );
1488        // clear hors borne → EINVAL.
1489        assert_eq!(table.clear(&mut ring, 99).err(), Some(Errno::EINVAL));
1490        // clear slot 0 → rend le FD possédé (encore valide).
1491        let cleared = table.clear(&mut ring, 0).expect("clear ok");
1492        // FD restitué (possédé, encore valide) ; son drop le ferme — non-fuite.
1493        let _cleared_fd = cleared.expect("slot 0 était rempli");
1494        // set slot 0 avec un nouveau FD.
1495        let c = crate::ipc::eventfd2(0, EventFdFlags::empty()).expect("eventfd c");
1496        table.set(&mut ring, 0, c.into_fd()).expect("set ok");
1497        // unregister rend les FD restants (slots 0 et 1).
1498        let remaining = table.unregister(&mut ring).expect("unregister");
1499        assert_eq!(remaining.len(), 2, "slots 0 (re-set) et 1 restitués");
1500    }
1501
1502    #[test]
1503    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1504    fn fixed_fd_table_alloc_range_validation() {
1505        let mut ring = ring8();
1506        let mut table = FixedFdTable::register(&mut ring, nz(4)).expect("register");
1507        table
1508            .set_alloc_range(&mut ring, 0..4)
1509            .expect("alloc range plein");
1510        table
1511            .set_alloc_range(&mut ring, 1..3)
1512            .expect("alloc range partiel");
1513        // Plage inversée → EINVAL (checked_sub None). `Range` explicite pour
1514        // éviter le lint `reversed_empty_ranges` (intention testée).
1515        let inverted = core::ops::Range {
1516            start: 3u32,
1517            end: 2u32,
1518        };
1519        assert_eq!(
1520            table.set_alloc_range(&mut ring, inverted).err(),
1521            Some(Errno::EINVAL)
1522        );
1523        // Plage débordant la capacité → EINVAL.
1524        assert_eq!(
1525            table.set_alloc_range(&mut ring, 0..9).err(),
1526            Some(Errno::EINVAL)
1527        );
1528    }
1529
1530    // ── Intégration : variantes direct + fixed-file + fixed_fd_install ────
1531
1532    #[test]
1533    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1534    fn direct_openat2_then_fixed_rw_then_install() {
1535        let path = temp_path("direct-rw");
1536        let _f = rw_file(&path); // crée le fichier
1537        let mut ring = ring8();
1538        let table = FixedFdTable::register(&mut ring, nz(4)).expect("register");
1539        // openat2_direct(Alloc) : le FD ouvert va dans un slot choisi par le kernel.
1540        let how = OpenHow {
1541            flags: OpenFlags::RDWR,
1542            ..OpenHow::default()
1543        };
1544        let tok = ring
1545            .submit_openat2_direct(DirFd::Cwd, cpath(&path), how, FixedSlotTarget::Alloc)
1546            .expect("openat2_direct");
1547        let slot_idx = complete_one(&mut ring, tok)
1548            .allocated_slot()
1549            .expect("slot alloué");
1550        assert!(slot_idx < 4);
1551        // write via FixedSlot.
1552        let fs = table.slot(slot_idx).expect("slot rempli");
1553        let tok = ring
1554            .submit_write_fixed_file(fs, b"fixed!".to_vec(), Some(0))
1555            .expect("write_fixed_file");
1556        let (_buf, n) = complete_one(&mut ring, tok)
1557            .into_buffer_result()
1558            .expect("write ok");
1559        assert_eq!(n, 6);
1560        // read via FixedSlot.
1561        let tok = ring
1562            .submit_read_fixed_file(fs, vec![0u8; 6], Some(0))
1563            .expect("read_fixed_file");
1564        let (buf, n) = complete_one(&mut ring, tok)
1565            .into_buffer_result()
1566            .expect("read ok");
1567        assert_eq!(n, 6);
1568        assert_eq!(&buf, b"fixed!");
1569        // fixed_fd_install : le FD direct redevient un FD ordinaire.
1570        let tok = ring.submit_fixed_fd_install(fs).expect("fixed_fd_install");
1571        let ofd = complete_one(&mut ring, tok)
1572            .opened_fd()
1573            .expect("fd ordinaire");
1574        // Le FD ordinaire lit le contenu écrit.
1575        let mut file = std_file(ofd);
1576        let mut content = String::new();
1577        use std::io::Seek;
1578        file.seek(std::io::SeekFrom::Start(0)).expect("seek");
1579        file.read_to_string(&mut content).expect("read");
1580        assert_eq!(content, "fixed!");
1581        table.unregister(&mut ring).expect("unregister");
1582        std::fs::remove_file(&path).ok();
1583    }
1584
1585    #[test]
1586    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1587    fn direct_socket_index_then_install() {
1588        let mut ring = ring8();
1589        let table = FixedFdTable::register(&mut ring, nz(4)).expect("register");
1590        // socket_direct vers un slot **précis** (Index).
1591        let tok = ring
1592            .submit_socket_direct(
1593                SocketDomain::Ipv4,
1594                SocketType::Stream,
1595                0,
1596                FixedSlotTarget::Index(2),
1597            )
1598            .expect("socket_direct");
1599        // Pour Index, cqe->res == 0 (succès).
1600        complete_one(&mut ring, tok).completed().expect("socket ok");
1601        let fs = table.slot(2).expect("slot 2");
1602        let tok = ring.submit_fixed_fd_install(fs).expect("install");
1603        let ofd = complete_one(&mut ring, tok).opened_fd().expect("fd socket");
1604        // FD ordinaire valide : un getsockopt socket réussit (c'est bien un socket).
1605        crate::net::get_so_reuseaddr(ofd.as_fd()).expect("getsockopt sur socket");
1606        table.unregister(&mut ring).expect("unregister");
1607    }
1608
1609    #[test]
1610    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1611    fn direct_accept_into_slot() {
1612        let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1613        let addr = listener.local_addr().expect("addr");
1614        let _client = std::net::TcpStream::connect(addr).expect("connect"); // remplit le backlog
1615        std::thread::sleep(std::time::Duration::from_millis(20));
1616        let mut ring = ring8();
1617        let table = FixedFdTable::register(&mut ring, nz(4)).expect("register");
1618        let tok = ring
1619            .submit_accept_direct(sfd(&listener), AcceptFlags::empty(), FixedSlotTarget::Alloc)
1620            .expect("accept_direct");
1621        let slot = complete_one(&mut ring, tok)
1622            .allocated_slot()
1623            .expect("slot accepté");
1624        assert!(slot < 4);
1625        let fs = table.slot(slot).expect("slot");
1626        let tok = ring.submit_fixed_fd_install(fs).expect("install");
1627        let _ofd = complete_one(&mut ring, tok)
1628            .opened_fd()
1629            .expect("fd accepté");
1630        table.unregister(&mut ring).expect("unregister");
1631    }
1632
1633    // ── Intégration : RegisteredBuffers (mmap round-trip + owned + clone) ──
1634
1635    #[test]
1636    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1637    fn registered_buffers_mmap_read_write_fixed_round_trip() {
1638        let path = temp_path("regbuf");
1639        let file = rw_file(&path);
1640        let region0 = rw_region(4096);
1641        let region1 = rw_region(4096);
1642        // Écrit un motif connu dans region0 avant l'enregistrement.
1643        // SAFETY (test): `region0` est PROT_WRITE, 4096 o ; on écrit 8 o au début.
1644        unsafe {
1645            std::ptr::copy_nonoverlapping(b"regdata!".as_ptr(), region0.as_ptr().cast_mut(), 8);
1646        }
1647        let r1_view = region1.clone(); // partage la mémoire pour relire après read_fixed
1648        let mut ring = ring8();
1649        let buffers = RegisteredBuffers::register_mmap(&mut ring, vec![region0, region1])
1650            .expect("register_mmap");
1651        // write_fixed : region0[0..8] → fichier.
1652        let slice0 = buffers.slice(0, 0..8).expect("slice 0");
1653        assert_eq!(slice0.len(), 8);
1654        assert!(!slice0.is_empty());
1655        assert_eq!(slice0.index(), 0);
1656        let tok = ring
1657            .submit_write_fixed(sfd(&file), slice0, Some(0))
1658            .expect("write_fixed");
1659        let n = complete_one(&mut ring, tok)
1660            .into_result()
1661            .expect("write ok");
1662        assert_eq!(n, 8);
1663        // read_fixed : fichier → region1[0..8].
1664        let slice1 = buffers.slice(1, 0..8).expect("slice 1");
1665        let tok = ring
1666            .submit_read_fixed(sfd(&file), slice1, Some(0))
1667            .expect("read_fixed");
1668        let n = complete_one(&mut ring, tok).into_result().expect("read ok");
1669        assert_eq!(n, 8);
1670        assert_eq!(&r1_view.bytes()[0..8], b"regdata!");
1671        // unregister mmap → Vec vide (régions consommées).
1672        let returned = buffers.unregister(&mut ring).expect("unregister");
1673        assert!(returned.is_empty());
1674        std::fs::remove_file(&path).ok();
1675    }
1676
1677    #[test]
1678    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1679    fn registered_buffers_owned_update_slice_unregister() {
1680        let mut ring = ring8();
1681        let mut buffers =
1682            RegisteredBuffers::register(&mut ring, vec![vec![0u8; 64], vec![0u8; 64]])
1683                .expect("register");
1684        // slice bornée OK ; hors borne → None.
1685        assert!(buffers.slice(0, 0..10).is_some());
1686        assert!(buffers.slice(0, 0..65).is_none()); // range > buffer
1687        assert!(buffers.slice(2, 0..1).is_none()); // index hors borne
1688        // slice inversée → None (checked_sub). `Range` construit explicitement
1689        // pour éviter le lint `reversed_empty_ranges` (intention testée).
1690        let inverted = core::ops::Range {
1691            start: 10usize,
1692            end: 5usize,
1693        };
1694        assert!(buffers.slice(0, inverted).is_none());
1695        // update remplace le buffer 0.
1696        buffers
1697            .update(&mut ring, 0, vec![1u8; 32])
1698            .expect("update ok");
1699        // update hors borne → EINVAL.
1700        assert_eq!(
1701            buffers.update(&mut ring, 9, vec![0u8; 4]).err(),
1702            Some(Errno::EINVAL)
1703        );
1704        // unregister rend les buffers possédés.
1705        let returned = buffers.unregister(&mut ring).expect("unregister");
1706        assert_eq!(returned.len(), 2);
1707    }
1708
1709    #[test]
1710    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1711    fn registered_buffers_update_rejects_mmap_backing() {
1712        let mut ring = ring8();
1713        let mut buffers = RegisteredBuffers::register_mmap(&mut ring, vec![rw_region(4096)])
1714            .expect("register_mmap");
1715        // update est Vec-orienté : backing mmap ⇒ EINVAL.
1716        assert_eq!(
1717            buffers.update(&mut ring, 0, vec![0u8; 4]).err(),
1718            Some(Errno::EINVAL)
1719        );
1720        buffers.unregister(&mut ring).expect("unregister");
1721    }
1722
1723    #[test]
1724    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1725    fn clone_buffers_between_rings() {
1726        let path = temp_path("clone");
1727        let file = rw_file(&path);
1728        let mut src = ring8();
1729        let mut dst = ring8();
1730        let src_bufs = RegisteredBuffers::register(&mut src, vec![b"clonable".to_vec()])
1731            .expect("register src");
1732        let dst_clone = RegisteredBuffers::clone_from(&mut dst, &src).expect("clone_from");
1733        // slice sur un backing cloné → None (géométrie inconnue).
1734        assert!(dst_clone.slice(0, 0..4).is_none());
1735        // Le ring destination écrit depuis le buffer cloné (même VA process).
1736        let slice = src_bufs.slice(0, 0..8).expect("slice src");
1737        let tok = dst
1738            .submit_write_fixed(sfd(&file), slice, Some(0))
1739            .expect("write_fixed dst");
1740        let n = complete_one(&mut dst, tok).into_result().expect("write ok");
1741        assert_eq!(n, 8);
1742        let mut content = String::new();
1743        use std::io::Seek;
1744        let mut f = file;
1745        f.seek(std::io::SeekFrom::Start(0)).expect("seek");
1746        f.read_to_string(&mut content).expect("read");
1747        assert_eq!(content, "clonable");
1748        dst_clone.unregister(&mut dst).expect("unregister dst");
1749        src_bufs.unregister(&mut src).expect("unregister src");
1750        std::fs::remove_file(&path).ok();
1751    }
1752
1753    // ── Intégration : eventfd, personality, io-wq, napi, clock, ring fd ───
1754
1755    #[test]
1756    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1757    fn register_eventfd_notifies_completions() {
1758        let mut ring = ring8();
1759        let efd = crate::ipc::eventfd2(0, EventFdFlags::NONBLOCK).expect("eventfd2");
1760        ring.register_eventfd(efd.as_fd())
1761            .expect("register_eventfd");
1762        // Une complétion (nop) → le kernel écrit dans l'eventfd.
1763        let tok = ring.submit_nop().expect("nop");
1764        complete_one(&mut ring, tok).completed().expect("nop ok");
1765        let count = efd.read().expect("eventfd read");
1766        assert!(count >= 1, "eventfd notifié");
1767        ring.unregister_eventfd().expect("unregister_eventfd");
1768        // Variante async : ré-enregistre puis désenregistre.
1769        ring.register_eventfd_async(efd.as_fd())
1770            .expect("register_eventfd_async");
1771        ring.unregister_eventfd().expect("unregister async");
1772    }
1773
1774    #[test]
1775    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1776    fn register_personality_and_use_in_op() {
1777        let path = temp_path("perso");
1778        let _f = rw_file(&path);
1779        let mut ring = ring8();
1780        let p = ring.register_personality().expect("register_personality");
1781        // Op avec la personality enregistrée.
1782        let how = OpenHow {
1783            flags: OpenFlags::RDONLY,
1784            ..OpenHow::default()
1785        };
1786        let tok = ring
1787            .with_personality(p)
1788            .submit_openat2(DirFd::Cwd, cpath(&path), how)
1789            .expect("openat2 with personality");
1790        let _fd = complete_one(&mut ring, tok).opened_fd().expect("ouvert");
1791        ring.unregister_personality(p)
1792            .expect("unregister_personality");
1793        std::fs::remove_file(&path).ok();
1794    }
1795
1796    #[test]
1797    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1798    fn work_queue_max_workers_reports_values() {
1799        let mut ring = ring8();
1800        // Fixe les plafonds.
1801        ring.set_work_queue_max_workers(2, 3)
1802            .expect("set max workers");
1803        // Relit (0,0 = inchangé) : rend les valeurs courantes (2,3).
1804        let limits = ring
1805            .set_work_queue_max_workers(0, 0)
1806            .expect("read max workers");
1807        assert_eq!(limits.bounded, 2);
1808        assert_eq!(limits.unbounded, 3);
1809    }
1810
1811    #[test]
1812    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1813    fn work_queue_affinity_set_and_clear() {
1814        let mut ring = ring8();
1815        let mut cpus = CpuSet::new();
1816        cpus.set(0);
1817        ring.set_work_queue_affinity(&cpus).expect("set affinity");
1818        ring.clear_work_queue_affinity().expect("clear affinity");
1819    }
1820
1821    #[test]
1822    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1823    fn register_napi_round_trip_when_available() {
1824        let mut ring = ring8();
1825        let cfg = NapiConfig {
1826            busy_poll_us: 50,
1827            prefer_busy_poll: true,
1828        };
1829        // NAPI peut être absent (FEATURE-KERNEL) : on tolère EINVAL.
1830        match ring.register_napi(cfg) {
1831            Ok(_previous) => {
1832                ring.unregister_napi().expect("unregister_napi");
1833            }
1834            Err(e) => assert_eq!(e, Errno::EINVAL, "NAPI indisponible ⇒ EINVAL"),
1835        }
1836    }
1837
1838    #[test]
1839    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1840    fn register_clock_all_sources() {
1841        let mut ring = ring8();
1842        ring.register_clock(ClockSource::Monotonic)
1843            .expect("monotonic");
1844        ring.register_clock(ClockSource::Boottime)
1845            .expect("boottime");
1846        // ÉCART KERNEL : io_uring 6.12 n'accepte que MONOTONIC/BOOTTIME pour
1847        // l'horloge des timeouts ; REALTIME est **rejeté** (`EINVAL`). Le type
1848        // `ClockSource::Realtime` reste exposé (surface validée), mais son
1849        // enregistrement échoue — signalé dans l'`.out` (jamais corrigé en silence).
1850        assert_eq!(
1851            ring.register_clock(ClockSource::Realtime).err(),
1852            Some(Errno::EINVAL)
1853        );
1854    }
1855
1856    #[test]
1857    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1858    fn register_ring_fd_round_trip() {
1859        let mut ring = ring8();
1860        // Désenregistrer sans enregistrer → EINVAL.
1861        assert_eq!(ring.unregister_ring_fd().err(), Some(Errno::EINVAL));
1862        ring.register_ring_fd().expect("register_ring_fd");
1863        // Une soumission passe désormais par le ring fd enregistré (enter_target).
1864        let tok = ring.submit_nop().expect("nop");
1865        complete_one(&mut ring, tok)
1866            .completed()
1867            .expect("nop via registered ring");
1868        ring.unregister_ring_fd().expect("unregister_ring_fd");
1869        // Après désenregistrement, l'enter ordinaire fonctionne toujours.
1870        let tok = ring.submit_nop().expect("nop 2");
1871        complete_one(&mut ring, tok)
1872            .completed()
1873            .expect("nop via fd ordinaire");
1874    }
1875
1876    // ── Chemins d'erreur des façades (registre injecté) ───────────────────
1877    //
1878    // Couvre les bras `?` Err de chaque façade qui appelle `io_uring_register`
1879    // **après** sa validation amont (ces bras ne sont pas atteignables par la
1880    // seule validation, qui retourne avant le syscall). Le simulateur injecte
1881    // un errno sur le **prochain** register.
1882
1883    #[test]
1884    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1885    fn register_facade_error_paths_propagate() {
1886        sim::clear();
1887        let mut ring = ring8();
1888        let mut src = ring8();
1889        // FixedFdTable construite réellement, puis erreurs injectées.
1890        let mut table = FixedFdTable::register(&mut ring, nz(4)).expect("register table");
1891        let fd_a = crate::ipc::eventfd2(0, EventFdFlags::empty()).expect("efd a");
1892        sim::inject(Syscall::Register, -22);
1893        assert_eq!(
1894            table.set(&mut ring, 0, fd_a.into_fd()).err(),
1895            Some(Errno::EINVAL)
1896        );
1897        sim::inject(Syscall::Register, -22);
1898        assert_eq!(table.clear(&mut ring, 0).err(), Some(Errno::EINVAL));
1899        sim::inject(Syscall::Register, -22);
1900        assert_eq!(
1901            table.set_alloc_range(&mut ring, 0..2).err(),
1902            Some(Errno::EINVAL)
1903        );
1904        sim::inject(Syscall::Register, -22);
1905        assert_eq!(table.unregister(&mut ring).err(), Some(Errno::EINVAL));
1906        // register_with (chemin syscall).
1907        let fd_b = crate::ipc::eventfd2(0, EventFdFlags::empty()).expect("efd b");
1908        sim::inject(Syscall::Register, -22);
1909        assert_eq!(
1910            FixedFdTable::register_with(&mut ring, vec![fd_b.into_fd()]).err(),
1911            Some(Errno::EINVAL)
1912        );
1913        // RegisteredBuffers : update + unregister + clone_from.
1914        let mut buffers =
1915            RegisteredBuffers::register(&mut ring, vec![vec![0u8; 8]]).expect("register buffers");
1916        sim::inject(Syscall::Register, -22);
1917        assert_eq!(
1918            buffers.update(&mut ring, 0, vec![1u8; 4]).err(),
1919            Some(Errno::EINVAL)
1920        );
1921        sim::inject(Syscall::Register, -22);
1922        assert_eq!(buffers.unregister(&mut ring).err(), Some(Errno::EINVAL));
1923        sim::inject(Syscall::Register, -22);
1924        assert_eq!(
1925            RegisteredBuffers::clone_from(&mut src, &ring).err(),
1926            Some(Errno::EINVAL)
1927        );
1928        // unregister_personality (id passé en nr_args).
1929        sim::inject(Syscall::Register, -22);
1930        assert_eq!(
1931            ring.unregister_personality(Personality(1)).err(),
1932            Some(Errno::EINVAL)
1933        );
1934        // register_eventfd_async (variante partageant register_eventfd_op).
1935        let efd = crate::ipc::eventfd2(0, EventFdFlags::empty()).expect("efd c");
1936        sim::inject(Syscall::Register, -9);
1937        assert_eq!(
1938            ring.register_eventfd_async(efd.as_fd()).err(),
1939            Some(Errno::EBADF)
1940        );
1941        // unregister_ring_fd : enregistrement réel (pose l'index), puis erreur
1942        // injectée sur le désenregistrement (bras `if ret < 0`).
1943        ring.register_ring_fd().expect("register_ring_fd réel");
1944        sim::inject(Syscall::Register, -22);
1945        assert_eq!(ring.unregister_ring_fd().err(), Some(Errno::EINVAL));
1946        // Remet l'état propre (désenregistre réellement) pour un téardown net.
1947        ring.unregister_ring_fd().expect("unregister réel");
1948        sim::clear();
1949    }
1950
1951    // ── openat2_direct relatif à un dirfd (couvre DirFd::Fd) ───────────────
1952
1953    #[test]
1954    #[cfg_attr(miri, ignore = "io_uring_register non supporté par Miri")]
1955    fn openat2_direct_relative_to_dirfd() {
1956        let dir = temp_path("dir");
1957        std::fs::create_dir_all(&dir).expect("mkdir");
1958        let file_path = dir.join("rel.txt");
1959        let _f = rw_file(&file_path);
1960        let dir_fd = std::fs::File::open(&dir).expect("open dir");
1961        let mut ring = ring8();
1962        let table = FixedFdTable::register(&mut ring, nz(2)).expect("register");
1963        let how = OpenHow {
1964            flags: OpenFlags::RDONLY,
1965            ..OpenHow::default()
1966        };
1967        let tok = ring
1968            .submit_openat2_direct(
1969                DirFd::Fd(sfd(&dir_fd)),
1970                CString::new("rel.txt").expect("nom"),
1971                how,
1972                FixedSlotTarget::Index(1),
1973            )
1974            .expect("openat2_direct relatif");
1975        complete_one(&mut ring, tok)
1976            .completed()
1977            .expect("ouvert relatif au dirfd");
1978        table.unregister(&mut ring).expect("unregister");
1979        std::fs::remove_file(&file_path).ok();
1980        std::fs::remove_dir(&dir).ok();
1981    }
1982}