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;