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(®) 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}