air_sys_syscall/io_uring/fs_ops.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//! Opérations **filesystem** d'io_uring (Temps 2a), par-dessus le cœur du
6//! Temps 1. Chaque façade encode un opcode `IORING_OP_*` (uapi 6.12) dans un
7//! SQE, **gare l'état possédé** (buffers, chemins) dans le slot S1 à la
8//! soumission, et restitue à la complétion via les accesseurs typés de
9//! [`Completion`](super::Completion).
10//!
11//! Référence normative : `docs/specs/layer-0/io-uring-2a-filesystem.md`.
12//!
13//! **Hors périmètre (Temps 3a) :** `read_fixed`/`write_fixed` (reposent sur
14//! `RegisteredBufferSlice`), variantes « direct descriptor », `close` fixed.
15//! **`madvise` (OP 25)** est désormais **implémenté** (PR coordonnée
16//! `family-mem`) : il prend une [`MmapRegion`] dont une garde de vivacité est
17//! garée dans le slot S1 — cf. `family-mem-mmap-region.md`.
18
19use super::owned::OwnedOp;
20use super::raw::{self, IoUringSqe, Iovec, OpenHowRaw};
21use super::{IoUring, SubmissionToken, SubmitOptions};
22use crate::mem::MmapRegion;
23use air_sys_types::Errno;
24use air_sys_types::fd::{AsRawFd, BorrowedFd, IntoRawFd, OwnedFd};
25use air_sys_types::fs::{
26 DirFd, FadviseAdvice, FallocateMode, FsyncFlags, LinkFlags, Mode, OpenHow, RenameFlags, Statx,
27 StatxFlags, StatxMask, SyncFileRangeFlags, UnlinkFlags, XattrFlags,
28};
29use air_sys_types::ipc::SpliceFlags;
30use air_sys_types::mem::MadviseAdvice;
31use alloc::boxed::Box;
32use alloc::ffi::CString;
33use alloc::vec::Vec;
34use core::ops::Range;
35
36/// Descripteur kernel d'un [`DirFd`] : `AT_FDCWD` pour `Cwd`, le FD brut sinon.
37/// Le `None` typé (`DirFd::Cwd`) remplace la sentinelle `-100` (ADR-021 conv. 1).
38fn dirfd_to_raw(dirfd: DirFd<'_>) -> i32 {
39 match dirfd {
40 DirFd::Cwd => raw::AT_FDCWD,
41 DirFd::Fd(fd) => fd.as_raw_fd(),
42 }
43}
44
45/// Encode un `offset: Option<u64>` : `Some(n)` ⇒ `n` ; `None` ⇒ position
46/// courante (transmis comme `-1` au kernel, `IORING_FEAT_RW_CUR_POS`). On
47/// n'expose **jamais** le `-1` magique (ADR-021 conv. 1).
48fn offset_to_raw(offset: Option<u64>) -> u64 {
49 // `-1i64 as u64` = `u64::MAX` : le motif binaire attendu par le kernel.
50 offset.unwrap_or(u64::MAX)
51}
52
53/// Longueur `usize` → `u32` (champ `len` du SQE), `EINVAL` si débordement
54/// (validation amont, Principe 4).
55fn len_u32(n: usize) -> Result<u32, Errno> {
56 u32::try_from(n).map_err(|_| Errno::EINVAL)
57}
58
59impl IoUring {
60 /// Réserve un slot S1 (y déplaçant `payload`), prépare un SQE zéro-initialisé
61 /// rempli par `fill`, pose `user_data` et les flags `IOSQE_*` en attente, et
62 /// retourne le jeton. La soumission effective se fait par
63 /// [`IoUring::submit`] / [`IoUring::submit_and_wait`].
64 ///
65 /// Les pointeurs vers le payload utilisés dans `fill` doivent être capturés
66 /// **avant** l'appel (les tas restent stables lors du déplacement du payload
67 /// dans le slot — c'est l'invariant de sûreté S1).
68 pub(crate) fn submit_op(
69 &mut self,
70 payload: Option<OwnedOp>,
71 fill: impl FnOnce(&mut IoUringSqe),
72 ) -> Result<SubmissionToken, Errno> {
73 self.submit_filled(payload, |sqe_ptr| {
74 // SAFETY: `sqe_ptr` pointe un slot SQE valide fraîchement
75 // zéro-initialisé, exclusivement détenu jusqu'à la publication ; le
76 // `&mut` ne vit que le temps de `fill`.
77 let sqe = unsafe { &mut *sqe_ptr };
78 fill(sqe);
79 })
80 }
81
82 /// Variante bas niveau de [`IoUring::submit_op`] : `fill` reçoit le **pointeur
83 /// brut** du slot SQE (et non un `&mut IoUringSqe`), ce qui autorise l'écriture
84 /// de la **zone `cmd[]`** d'`URING_CMD` au-delà des 64 octets nommés (jusqu'à
85 /// `sqe_size`, cf. `SETUP_SQE128`) — la provenance du pointeur couvre tout le
86 /// slot. Toute la mécanique S1 (réservation slab, `user_data`, flags `IOSQE_*`)
87 /// est partagée. Réservé au sous-module `cmd` (Temps 2d).
88 pub(crate) fn submit_filled(
89 &mut self,
90 payload: Option<OwnedOp>,
91 fill: impl FnOnce(*mut IoUringSqe),
92 ) -> Result<SubmissionToken, Errno> {
93 if self.sq.space_left() == 0 {
94 return Err(Errno::EBUSY);
95 }
96 let has_payload = payload.is_some();
97 let token = self.slab.reserve(payload).map_err(|_| Errno::EBUSY)?;
98 let opts = self.pending_options;
99 self.pending_options = SubmitOptions::default();
100 // Personality en attente (Temps 3a §6) : `0` = aucune (credentials du
101 // process). Consommée pour cette seule soumission.
102 let personality = self.pending_personality;
103 self.pending_personality = 0;
104 // SAFETY: place SQ vérifiée > 0 juste au-dessus ; le slot (entier,
105 // `sqe_size` octets) est zéro-initialisé par `prepare` et rempli
106 // intégralement ci-dessous avant toute publication de la `tail`.
107 let sqe_ptr = unsafe { self.sq.prepare() }.expect("place SQ vérifiée");
108 fill(sqe_ptr);
109 // SAFETY: `sqe_ptr` pointe le slot SQE, exclusivement détenu jusqu'à la
110 // publication ; après `fill`, on pose `user_data`/`flags` via les champs
111 // nommés (le `&mut` est créé *après* d'éventuelles écritures brutes de
112 // `cmd[]` — pas d'aliasing simultané).
113 let sqe = unsafe { &mut *sqe_ptr };
114 sqe.user_data = token.to_user_data();
115 sqe.personality = personality;
116 let mut flags = opts.iosqe_flags();
117 if has_payload {
118 // Une op à payload ne doit jamais sauter son CQE : le slot doit être
119 // libéré à la complétion pour restituer/dropper le buffer (sinon le
120 // kernel écrirait dans de la mémoire qu'on aurait pu libérer). On
121 // retire donc `CQE_SKIP_SUCCESS` pour ces ops (sûreté > micro-optim).
122 flags &= !raw::IOSQE_CQE_SKIP_SUCCESS;
123 }
124 sqe.flags = flags;
125 if !has_payload && opts.skips_cqe_on_success() {
126 // Op sans payload + skip demandé : aucun CQE en cas de succès ⇒
127 // libère le slot S1 dès la soumission (cohérent avec `submit_nop`).
128 self.slab.release(token);
129 }
130 Ok(token)
131 }
132
133 // ── Lecture / écriture ────────────────────────────────────────────────
134
135 /// Soumet un `IORING_OP_READ` (22). Le `buffer` est déplacé dans le slot ;
136 /// sa longueur logique borne le nombre d'octets lus. À la complétion,
137 /// [`Completion::into_buffer_result`](super::Completion::into_buffer_result)
138 /// le restitue avec le compte d'octets.
139 ///
140 /// # Errors
141 ///
142 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins (avant tout syscall) ;
143 /// [`Errno::EINVAL`] si `buffer.len()` déborde un `u32`.
144 pub fn submit_read(
145 &mut self,
146 fd: BorrowedFd<'_>,
147 mut buffer: Vec<u8>,
148 offset: Option<u64>,
149 ) -> Result<SubmissionToken, Errno> {
150 let fd = fd.as_raw_fd();
151 let len = len_u32(buffer.len())?;
152 let addr = buffer.as_mut_ptr() as u64;
153 let off = offset_to_raw(offset);
154 self.submit_op(Some(OwnedOp::Bytes(buffer)), |sqe| {
155 sqe.opcode = raw::IORING_OP_READ;
156 sqe.fd = fd;
157 sqe.addr_or_splice_off_in = addr;
158 sqe.len = len;
159 sqe.off_or_addr2 = off;
160 })
161 }
162
163 /// Soumet un `IORING_OP_WRITE` (23) : écrit `buffer.len()` octets du buffer
164 /// (déplacé dans le slot). Complétion via `into_buffer_result`.
165 ///
166 /// # Errors
167 ///
168 /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si `buffer.len()`
169 /// déborde un `u32`.
170 pub fn submit_write(
171 &mut self,
172 fd: BorrowedFd<'_>,
173 mut buffer: Vec<u8>,
174 offset: Option<u64>,
175 ) -> Result<SubmissionToken, Errno> {
176 let fd = fd.as_raw_fd();
177 let len = len_u32(buffer.len())?;
178 let addr = buffer.as_mut_ptr() as u64;
179 let off = offset_to_raw(offset);
180 self.submit_op(Some(OwnedOp::Bytes(buffer)), |sqe| {
181 sqe.opcode = raw::IORING_OP_WRITE;
182 sqe.fd = fd;
183 sqe.addr_or_splice_off_in = addr;
184 sqe.len = len;
185 sqe.off_or_addr2 = off;
186 })
187 }
188
189 /// Soumet un `IORING_OP_READV` (1). Le tableau d'`iovec` est construit en
190 /// interne et garé dans le slot avec les `buffers` possédés. Complétion via
191 /// [`Completion::into_vectored_result`](super::Completion::into_vectored_result).
192 ///
193 /// # Errors
194 ///
195 /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si le nombre de
196 /// buffers dépasse `IOV_MAX` (1024).
197 pub fn submit_readv(
198 &mut self,
199 fd: BorrowedFd<'_>,
200 buffers: Vec<Vec<u8>>,
201 offset: Option<u64>,
202 ) -> Result<SubmissionToken, Errno> {
203 self.submit_vectored(raw::IORING_OP_READV, fd, buffers, offset)
204 }
205
206 /// Soumet un `IORING_OP_WRITEV` (2). Voir [`IoUring::submit_readv`].
207 ///
208 /// # Errors
209 ///
210 /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si le nombre de
211 /// buffers dépasse `IOV_MAX`.
212 pub fn submit_writev(
213 &mut self,
214 fd: BorrowedFd<'_>,
215 buffers: Vec<Vec<u8>>,
216 offset: Option<u64>,
217 ) -> Result<SubmissionToken, Errno> {
218 self.submit_vectored(raw::IORING_OP_WRITEV, fd, buffers, offset)
219 }
220
221 /// Cœur commun de `readv`/`writev` : valide `IOV_MAX`, construit le tableau
222 /// d'`iovec` à partir des buffers possédés, et gare les deux dans le slot.
223 fn submit_vectored(
224 &mut self,
225 opcode: u8,
226 fd: BorrowedFd<'_>,
227 mut buffers: Vec<Vec<u8>>,
228 offset: Option<u64>,
229 ) -> Result<SubmissionToken, Errno> {
230 if buffers.len() > raw::IOV_MAX {
231 return Err(Errno::EINVAL);
232 }
233 let fd = fd.as_raw_fd();
234 let off = offset_to_raw(offset);
235 // Construit les `iovec` à partir des tas (stables) des buffers possédés.
236 let mut iovecs: Vec<Iovec> = Vec::with_capacity(buffers.len());
237 for buf in &mut buffers {
238 iovecs.push(Iovec {
239 iov_base: buf.as_mut_ptr(),
240 iov_len: buf.len(),
241 });
242 }
243 let iovecs = iovecs.into_boxed_slice();
244 // `iovecs.len() == buffers.len() ≤ IOV_MAX (1024)` (validé ci-dessus),
245 // tient toujours dans un `u32` : `unwrap_or` n'est jamais déclenché (pas
246 // de branche d'erreur morte ici).
247 let count = u32::try_from(iovecs.len()).unwrap_or(u32::MAX);
248 let addr = iovecs.as_ptr() as u64;
249 self.submit_op(Some(OwnedOp::Vectored { buffers, iovecs }), |sqe| {
250 sqe.opcode = opcode;
251 sqe.fd = fd;
252 sqe.addr_or_splice_off_in = addr;
253 sqe.len = count;
254 sqe.off_or_addr2 = off;
255 })
256 }
257
258 // ── Synchronisation / allocation / troncature ─────────────────────────
259
260 /// Soumet un `IORING_OP_FSYNC` (3). `FsyncFlags::DATASYNC` ⇒ sémantique
261 /// `fdatasync`. Complétion via [`Completion::completed`](super::Completion::completed).
262 ///
263 /// # Errors
264 ///
265 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
266 pub fn submit_fsync(
267 &mut self,
268 fd: BorrowedFd<'_>,
269 flags: FsyncFlags,
270 ) -> Result<SubmissionToken, Errno> {
271 let fd = fd.as_raw_fd();
272 let fsync_flags = if flags.contains(FsyncFlags::DATASYNC) {
273 raw::IORING_FSYNC_DATASYNC
274 } else {
275 0
276 };
277 self.submit_op(None, |sqe| {
278 sqe.opcode = raw::IORING_OP_FSYNC;
279 sqe.fd = fd;
280 sqe.op_flags = fsync_flags;
281 })
282 }
283
284 /// Soumet un `IORING_OP_SYNC_FILE_RANGE` (8). Complétion via `completed`.
285 ///
286 /// # Errors
287 ///
288 /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si `nbytes` déborde
289 /// un `u32` (champ `len` du SQE).
290 pub fn submit_sync_file_range(
291 &mut self,
292 fd: BorrowedFd<'_>,
293 offset: u64,
294 nbytes: u64,
295 flags: SyncFileRangeFlags,
296 ) -> Result<SubmissionToken, Errno> {
297 let fd = fd.as_raw_fd();
298 let len = u32::try_from(nbytes).map_err(|_| Errno::EINVAL)?;
299 let op_flags = flags.bits();
300 self.submit_op(None, |sqe| {
301 sqe.opcode = raw::IORING_OP_SYNC_FILE_RANGE;
302 sqe.fd = fd;
303 sqe.off_or_addr2 = offset;
304 sqe.len = len;
305 sqe.op_flags = op_flags;
306 })
307 }
308
309 /// Soumet un `IORING_OP_FALLOCATE` (17). Valide en amont `length > 0` et
310 /// l'absence de débordement `offset + length` (Principe 4). Complétion via
311 /// `completed`.
312 ///
313 /// # Errors
314 ///
315 /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si `length ≤ 0`,
316 /// `offset < 0`, ou `offset + length` déborde un `i64`.
317 pub fn submit_fallocate(
318 &mut self,
319 fd: BorrowedFd<'_>,
320 mode: FallocateMode,
321 offset: i64,
322 length: i64,
323 ) -> Result<SubmissionToken, Errno> {
324 if length <= 0 || offset < 0 || offset.checked_add(length).is_none() {
325 return Err(Errno::EINVAL);
326 }
327 let fd = fd.as_raw_fd();
328 // `off = offset`, `addr = length`, `len = mode` (mapping io_uring).
329 let off = offset.cast_unsigned();
330 let addr = length.cast_unsigned();
331 let mode_bits = mode.bits().cast_unsigned();
332 self.submit_op(None, |sqe| {
333 sqe.opcode = raw::IORING_OP_FALLOCATE;
334 sqe.fd = fd;
335 sqe.off_or_addr2 = off;
336 sqe.addr_or_splice_off_in = addr;
337 sqe.len = mode_bits;
338 })
339 }
340
341 /// Soumet un `IORING_OP_FTRUNCATE` (55) : redimensionne à `length` octets.
342 /// Complétion via `completed`.
343 ///
344 /// # Errors
345 ///
346 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
347 pub fn submit_ftruncate(
348 &mut self,
349 fd: BorrowedFd<'_>,
350 length: u64,
351 ) -> Result<SubmissionToken, Errno> {
352 let fd = fd.as_raw_fd();
353 self.submit_op(None, |sqe| {
354 sqe.opcode = raw::IORING_OP_FTRUNCATE;
355 sqe.fd = fd;
356 sqe.off_or_addr2 = length;
357 })
358 }
359
360 // ── Ouverture / fermeture ─────────────────────────────────────────────
361
362 /// Soumet un `IORING_OP_OPENAT2` (28). Le chemin et la `struct open_how`
363 /// (lus de façon asynchrone) sont gardés en vie dans le slot. `O_CLOEXEC`
364 /// est ajouté d'office (cohérent avec le wrapper synchrone). Complétion via
365 /// [`Completion::opened_fd`](super::Completion::opened_fd).
366 ///
367 /// # Errors
368 ///
369 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
370 pub fn submit_openat2(
371 &mut self,
372 dirfd: DirFd<'_>,
373 path: CString,
374 how: OpenHow,
375 ) -> Result<SubmissionToken, Errno> {
376 let dirfd = dirfd_to_raw(dirfd);
377 let how = Box::new(OpenHowRaw {
378 flags: how.flags.bits() | raw::O_CLOEXEC,
379 mode: u64::from(how.mode),
380 resolve: how.resolve.bits(),
381 });
382 let how_ptr = core::ptr::from_ref(&*how) as u64;
383 let path_ptr = path.as_ptr() as u64;
384 // `len` = sizeof(open_how) = 24, figé par l'assert de layout de `raw.rs`.
385 let how_len: u32 = 24;
386 self.submit_op(Some(OwnedOp::Open { path, how }), |sqe| {
387 sqe.opcode = raw::IORING_OP_OPENAT2;
388 sqe.fd = dirfd;
389 sqe.addr_or_splice_off_in = path_ptr;
390 sqe.len = how_len;
391 sqe.off_or_addr2 = how_ptr;
392 })
393 }
394
395 /// Soumet un `IORING_OP_CLOSE` (19). **Consomme** l'`OwnedFd` (déplacé dans
396 /// le slot) : il ne peut plus être réutilisé ⇒ pas de double close. Le FD
397 /// n'est effectivement fermé qu'à l'exécution kernel. Complétion via
398 /// `completed`.
399 ///
400 /// # Errors
401 ///
402 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
403 pub fn submit_close(&mut self, fd: OwnedFd) -> Result<SubmissionToken, Errno> {
404 // `into_raw_fd` **cède** l'ownership au kernel sans fermer : c'est l'op
405 // CLOSE qui fermera le FD. On ne gare donc aucun payload (sinon le `Drop`
406 // de l'`OwnedFd` à la complétion fermerait une seconde fois — double
407 // close). Le numéro reste valide jusqu'à l'exécution kernel (personne
408 // d'autre ne le possède), donc aucune réutilisation prématurée.
409 let raw_fd = fd.into_raw_fd();
410 self.submit_op(None, |sqe| {
411 sqe.opcode = raw::IORING_OP_CLOSE;
412 sqe.fd = raw_fd;
413 })
414 }
415
416 // ── Métadonnées ───────────────────────────────────────────────────────
417
418 /// Soumet un `IORING_OP_STATX` (21). Le kernel écrit la structure dans `out`
419 /// (gardé en vie avec le chemin). Complétion via
420 /// [`Completion::into_statx`](super::Completion::into_statx).
421 ///
422 /// # Errors
423 ///
424 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
425 pub fn submit_statx(
426 &mut self,
427 dirfd: DirFd<'_>,
428 path: CString,
429 flags: StatxFlags,
430 mask: StatxMask,
431 mut out: Box<Statx>,
432 ) -> Result<SubmissionToken, Errno> {
433 let dirfd = dirfd_to_raw(dirfd);
434 let path_ptr = path.as_ptr() as u64;
435 let out_ptr = core::ptr::from_mut(&mut *out) as u64;
436 let mask_bits = mask.bits();
437 let flags_bits = flags.bits();
438 self.submit_op(Some(OwnedOp::Statx { out, path }), |sqe| {
439 sqe.opcode = raw::IORING_OP_STATX;
440 sqe.fd = dirfd;
441 sqe.addr_or_splice_off_in = path_ptr;
442 sqe.len = mask_bits;
443 sqe.off_or_addr2 = out_ptr;
444 sqe.op_flags = flags_bits;
445 })
446 }
447
448 // ── Conseils d'accès ──────────────────────────────────────────────────
449
450 /// Soumet un `IORING_OP_FADVISE` (24) : conseil d'accès `posix_fadvise`.
451 /// Complétion via `completed`.
452 ///
453 /// # Errors
454 ///
455 /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si `length` déborde
456 /// un `u32` (champ `len` du SQE en 6.12).
457 pub fn submit_fadvise(
458 &mut self,
459 fd: BorrowedFd<'_>,
460 offset: u64,
461 length: u64,
462 advice: FadviseAdvice,
463 ) -> Result<SubmissionToken, Errno> {
464 let fd = fd.as_raw_fd();
465 let len = u32::try_from(length).map_err(|_| Errno::EINVAL)?;
466 let advice = advice.as_raw().cast_unsigned();
467 self.submit_op(None, |sqe| {
468 sqe.opcode = raw::IORING_OP_FADVISE;
469 sqe.fd = fd;
470 sqe.off_or_addr2 = offset;
471 sqe.len = len;
472 sqe.op_flags = advice;
473 })
474 }
475
476 /// Soumet un `IORING_OP_MADVISE` (25) : conseil kernel sur le pattern
477 /// d'accès à un sous-`range` d'une [`MmapRegion`] **partageable**. La région
478 /// **doit rester mappée jusqu'à la complétion** : le slot S1 retient une
479 /// garde de vivacité ([`MmapRegion::liveness_handle`]) — `munmap` ne peut
480 /// donc pas survenir tant que l'op est en vol (sûreté par construction,
481 /// cf. `family-mem-mmap-region.md`). Complétion via
482 /// [`Completion::completed`](super::Completion::completed).
483 ///
484 /// `range` est validé **en amont** (Principe 4) contre les bornes de la
485 /// région : pas de plage brute, pas d'API `unsafe`.
486 ///
487 /// # Errors
488 ///
489 /// [`Errno::EINVAL`] si `range` est mal formé (`start > end`) ou déborde la
490 /// région (`range.end > region.len()`), ou si la longueur déborde un `u32` ;
491 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
492 pub fn submit_madvise(
493 &mut self,
494 region: &MmapRegion,
495 range: Range<usize>,
496 advice: MadviseAdvice,
497 ) -> Result<SubmissionToken, Errno> {
498 // Validation amont : sous-plage bien formée et dans les bornes.
499 if range.start > range.end || range.end > region.len() {
500 return Err(Errno::EINVAL);
501 }
502 let span = range.end.checked_sub(range.start).ok_or(Errno::EINVAL)?;
503 let len = len_u32(span)?;
504 let start = u64::try_from(range.start).map_err(|_| Errno::EINVAL)?;
505 let addr = (region.as_ptr() as u64)
506 .checked_add(start)
507 .ok_or(Errno::EINVAL)?;
508 // MadviseAdvice est `#[repr(i32)]` : `advice as i32` est exact et sans
509 // perte (même idiome que le `madvise` synchrone de la famille `mem`).
510 #[allow(clippy::as_conversions)]
511 let advice_flags = (advice as i32).cast_unsigned();
512 let guard = region.liveness_handle();
513 self.submit_op(Some(OwnedOp::MemLiveness(guard)), |sqe| {
514 sqe.opcode = raw::IORING_OP_MADVISE;
515 sqe.fd = -1;
516 sqe.addr_or_splice_off_in = addr;
517 sqe.len = len;
518 sqe.op_flags = advice_flags;
519 })
520 }
521
522 // ── Zero-copy FD↔FD ───────────────────────────────────────────────────
523
524 /// Soumet un `IORING_OP_SPLICE` (30). Au moins un des deux FDs est un pipe.
525 /// Les offsets `None` valent « position courante / FD non seekable ».
526 /// Complétion via [`Completion::into_result`](super::Completion::into_result)
527 /// (octets transférés).
528 ///
529 /// # Errors
530 ///
531 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
532 pub fn submit_splice(
533 &mut self,
534 fd_in: BorrowedFd<'_>,
535 offset_in: Option<u64>,
536 fd_out: BorrowedFd<'_>,
537 offset_out: Option<u64>,
538 length: u32,
539 flags: SpliceFlags,
540 ) -> Result<SubmissionToken, Errno> {
541 let fd_in = fd_in.as_raw_fd();
542 let fd_out = fd_out.as_raw_fd();
543 let off_in = offset_to_raw(offset_in);
544 let off_out = offset_to_raw(offset_out);
545 let op_flags = flags.bits();
546 self.submit_op(None, |sqe| {
547 sqe.opcode = raw::IORING_OP_SPLICE;
548 sqe.fd = fd_out;
549 sqe.off_or_addr2 = off_out;
550 sqe.addr_or_splice_off_in = off_in;
551 sqe.len = length;
552 sqe.op_flags = op_flags;
553 sqe.splice_fd_in_or_file_index = fd_in.cast_unsigned();
554 })
555 }
556
557 /// Soumet un `IORING_OP_TEE` (33) : duplique entre deux pipes sans consommer
558 /// la source. Complétion via `into_result` (octets transférés).
559 ///
560 /// # Errors
561 ///
562 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
563 pub fn submit_tee(
564 &mut self,
565 fd_in: BorrowedFd<'_>,
566 fd_out: BorrowedFd<'_>,
567 length: u32,
568 flags: SpliceFlags,
569 ) -> Result<SubmissionToken, Errno> {
570 let fd_in = fd_in.as_raw_fd();
571 let fd_out = fd_out.as_raw_fd();
572 let op_flags = flags.bits();
573 self.submit_op(None, |sqe| {
574 sqe.opcode = raw::IORING_OP_TEE;
575 sqe.fd = fd_out;
576 sqe.len = length;
577 sqe.op_flags = op_flags;
578 sqe.splice_fd_in_or_file_index = fd_in.cast_unsigned();
579 })
580 }
581
582 // ── Répertoires et liens ──────────────────────────────────────────────
583
584 /// Soumet un `IORING_OP_MKDIRAT` (37). Complétion via `completed`.
585 ///
586 /// # Errors
587 ///
588 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
589 pub fn submit_mkdirat(
590 &mut self,
591 dirfd: DirFd<'_>,
592 path: CString,
593 mode: Mode,
594 ) -> Result<SubmissionToken, Errno> {
595 let dirfd = dirfd_to_raw(dirfd);
596 let path_ptr = path.as_ptr() as u64;
597 self.submit_op(Some(OwnedOp::Path(path)), |sqe| {
598 sqe.opcode = raw::IORING_OP_MKDIRAT;
599 sqe.fd = dirfd;
600 sqe.addr_or_splice_off_in = path_ptr;
601 sqe.len = mode;
602 })
603 }
604
605 /// Soumet un `IORING_OP_UNLINKAT` (36). `UnlinkFlags::REMOVEDIR` supprime un
606 /// répertoire. Complétion via `completed`.
607 ///
608 /// # Errors
609 ///
610 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
611 pub fn submit_unlinkat(
612 &mut self,
613 dirfd: DirFd<'_>,
614 path: CString,
615 flags: UnlinkFlags,
616 ) -> Result<SubmissionToken, Errno> {
617 let dirfd = dirfd_to_raw(dirfd);
618 let path_ptr = path.as_ptr() as u64;
619 let op_flags = flags.bits().cast_unsigned();
620 self.submit_op(Some(OwnedOp::Path(path)), |sqe| {
621 sqe.opcode = raw::IORING_OP_UNLINKAT;
622 sqe.fd = dirfd;
623 sqe.addr_or_splice_off_in = path_ptr;
624 sqe.op_flags = op_flags;
625 })
626 }
627
628 /// Soumet un `IORING_OP_RENAMEAT` (35, sémantique `renameat2`). Les deux
629 /// chemins sont gardés en vie dans le slot. Complétion via `completed`.
630 ///
631 /// # Errors
632 ///
633 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
634 pub fn submit_renameat(
635 &mut self,
636 old_dirfd: DirFd<'_>,
637 old_path: CString,
638 new_dirfd: DirFd<'_>,
639 new_path: CString,
640 flags: RenameFlags,
641 ) -> Result<SubmissionToken, Errno> {
642 let old_dirfd = dirfd_to_raw(old_dirfd);
643 let new_dirfd = dirfd_to_raw(new_dirfd);
644 let old_ptr = old_path.as_ptr() as u64;
645 let new_ptr = new_path.as_ptr() as u64;
646 let op_flags = flags.bits();
647 self.submit_op(Some(OwnedOp::TwoPaths(old_path, new_path)), |sqe| {
648 sqe.opcode = raw::IORING_OP_RENAMEAT;
649 sqe.fd = old_dirfd;
650 sqe.addr_or_splice_off_in = old_ptr;
651 sqe.len = new_dirfd.cast_unsigned();
652 sqe.off_or_addr2 = new_ptr;
653 sqe.op_flags = op_flags;
654 })
655 }
656
657 /// Soumet un `IORING_OP_LINKAT` (39). Complétion via `completed`.
658 ///
659 /// # Errors
660 ///
661 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
662 pub fn submit_linkat(
663 &mut self,
664 old_dirfd: DirFd<'_>,
665 old_path: CString,
666 new_dirfd: DirFd<'_>,
667 new_path: CString,
668 flags: LinkFlags,
669 ) -> Result<SubmissionToken, Errno> {
670 let old_dirfd = dirfd_to_raw(old_dirfd);
671 let new_dirfd = dirfd_to_raw(new_dirfd);
672 let old_ptr = old_path.as_ptr() as u64;
673 let new_ptr = new_path.as_ptr() as u64;
674 let op_flags = flags.bits().cast_unsigned();
675 self.submit_op(Some(OwnedOp::TwoPaths(old_path, new_path)), |sqe| {
676 sqe.opcode = raw::IORING_OP_LINKAT;
677 sqe.fd = old_dirfd;
678 sqe.addr_or_splice_off_in = old_ptr;
679 sqe.len = new_dirfd.cast_unsigned();
680 sqe.off_or_addr2 = new_ptr;
681 sqe.op_flags = op_flags;
682 })
683 }
684
685 /// Soumet un `IORING_OP_SYMLINKAT` (38) : crée `link_path` pointant vers
686 /// `target`. Complétion via `completed`.
687 ///
688 /// # Errors
689 ///
690 /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
691 pub fn submit_symlinkat(
692 &mut self,
693 target: CString,
694 new_dirfd: DirFd<'_>,
695 link_path: CString,
696 ) -> Result<SubmissionToken, Errno> {
697 let new_dirfd = dirfd_to_raw(new_dirfd);
698 let target_ptr = target.as_ptr() as u64;
699 let link_ptr = link_path.as_ptr() as u64;
700 self.submit_op(Some(OwnedOp::TwoPaths(target, link_path)), |sqe| {
701 sqe.opcode = raw::IORING_OP_SYMLINKAT;
702 sqe.fd = new_dirfd;
703 sqe.addr_or_splice_off_in = target_ptr;
704 sqe.off_or_addr2 = link_ptr;
705 })
706 }
707
708 // ── Attributs étendus (xattr) ─────────────────────────────────────────
709
710 /// Soumet un `IORING_OP_SETXATTR` (42). `name`, `value` et `path` sont gardés
711 /// en vie dans le slot. Complétion via `completed`.
712 ///
713 /// # Errors
714 ///
715 /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si `value.len()`
716 /// déborde un `u32`.
717 pub fn submit_setxattr(
718 &mut self,
719 path: CString,
720 name: CString,
721 value: Vec<u8>,
722 flags: XattrFlags,
723 ) -> Result<SubmissionToken, Errno> {
724 let name_ptr = name.as_ptr() as u64;
725 let path_ptr = path.as_ptr() as u64;
726 let value_ptr = value.as_ptr() as u64;
727 let len = len_u32(value.len())?;
728 let op_flags = flags.bits().cast_unsigned();
729 self.submit_op(
730 Some(OwnedOp::Xattr {
731 value,
732 name,
733 path: Some(path),
734 }),
735 |sqe| {
736 sqe.opcode = raw::IORING_OP_SETXATTR;
737 sqe.addr_or_splice_off_in = name_ptr;
738 sqe.off_or_addr2 = value_ptr;
739 sqe.len = len;
740 sqe.addr3 = path_ptr;
741 sqe.op_flags = op_flags;
742 },
743 )
744 }
745
746 /// Soumet un `IORING_OP_GETXATTR` (44). `value` sert de buffer de sortie.
747 /// Complétion via
748 /// [`Completion::into_xattr_result`](super::Completion::into_xattr_result).
749 ///
750 /// # Errors
751 ///
752 /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si `value.len()`
753 /// déborde un `u32`.
754 pub fn submit_getxattr(
755 &mut self,
756 path: CString,
757 name: CString,
758 mut value: Vec<u8>,
759 ) -> Result<SubmissionToken, Errno> {
760 let name_ptr = name.as_ptr() as u64;
761 let path_ptr = path.as_ptr() as u64;
762 let value_ptr = value.as_mut_ptr() as u64;
763 let len = len_u32(value.len())?;
764 self.submit_op(
765 Some(OwnedOp::Xattr {
766 value,
767 name,
768 path: Some(path),
769 }),
770 |sqe| {
771 sqe.opcode = raw::IORING_OP_GETXATTR;
772 sqe.addr_or_splice_off_in = name_ptr;
773 sqe.off_or_addr2 = value_ptr;
774 sqe.len = len;
775 sqe.addr3 = path_ptr;
776 },
777 )
778 }
779
780 /// Soumet un `IORING_OP_FSETXATTR` (41) : variante par FD. Complétion via
781 /// `completed`.
782 ///
783 /// # Errors
784 ///
785 /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si `value.len()`
786 /// déborde un `u32`.
787 pub fn submit_fsetxattr(
788 &mut self,
789 fd: BorrowedFd<'_>,
790 name: CString,
791 value: Vec<u8>,
792 flags: XattrFlags,
793 ) -> Result<SubmissionToken, Errno> {
794 let fd = fd.as_raw_fd();
795 let name_ptr = name.as_ptr() as u64;
796 let value_ptr = value.as_ptr() as u64;
797 let len = len_u32(value.len())?;
798 let op_flags = flags.bits().cast_unsigned();
799 self.submit_op(
800 Some(OwnedOp::Xattr {
801 value,
802 name,
803 path: None,
804 }),
805 |sqe| {
806 sqe.opcode = raw::IORING_OP_FSETXATTR;
807 sqe.fd = fd;
808 sqe.addr_or_splice_off_in = name_ptr;
809 sqe.off_or_addr2 = value_ptr;
810 sqe.len = len;
811 sqe.op_flags = op_flags;
812 },
813 )
814 }
815
816 /// Soumet un `IORING_OP_FGETXATTR` (43) : variante par FD. Complétion via
817 /// `into_xattr_result`.
818 ///
819 /// # Errors
820 ///
821 /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si `value.len()`
822 /// déborde un `u32`.
823 pub fn submit_fgetxattr(
824 &mut self,
825 fd: BorrowedFd<'_>,
826 name: CString,
827 mut value: Vec<u8>,
828 ) -> Result<SubmissionToken, Errno> {
829 let fd = fd.as_raw_fd();
830 let name_ptr = name.as_ptr() as u64;
831 let value_ptr = value.as_mut_ptr() as u64;
832 let len = len_u32(value.len())?;
833 self.submit_op(
834 Some(OwnedOp::Xattr {
835 value,
836 name,
837 path: None,
838 }),
839 |sqe| {
840 sqe.opcode = raw::IORING_OP_FGETXATTR;
841 sqe.fd = fd;
842 sqe.addr_or_splice_off_in = name_ptr;
843 sqe.off_or_addr2 = value_ptr;
844 sqe.len = len;
845 },
846 )
847 }
848}
849
850// ───────────────────────────────────────────────────────────────────────────
851// Tests
852// ───────────────────────────────────────────────────────────────────────────
853//
854// Les tests d'**intégration** (kernel 6.12 réel : tmpfs/ext4, pipes) portent
855// `#[cfg_attr(miri, ignore)]` car Miri ne modélise pas `io_uring_enter` ni
856// l'`asm!`. Les tests **purs** (accesseurs de `Completion`, invariants
857// d'ownership) tournent SOUS Miri : `cargo +nightly miri test -p
858// air-sys-syscall io_uring::fs_ops::ownership` prouve qu'un buffer en vol ne
859// peut être ni libéré ni réutilisé (transfert/restitution sound).
860
861#[cfg(test)]
862mod tests;