Skip to main content

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;