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 {
863    use super::*;
864    use crate::io_uring::{Completion, IoUring, SubmissionToken};
865    use crate::test_support::sfd;
866    use air_sys_types::fd::AsFd;
867    use air_sys_types::fs::OpenHow;
868    use core::num::NonZeroU32;
869    use std::ffi::CString;
870    use std::io::{Read, Write};
871    use std::os::unix::ffi::OsStrExt;
872    use std::path::{Path, PathBuf};
873    use std::sync::atomic::{AtomicU32, Ordering};
874
875    static COUNTER: AtomicU32 = AtomicU32::new(0);
876
877    fn ring8() -> IoUring {
878        IoUring::new(NonZeroU32::new(8).expect("8 ≠ 0")).expect("io_uring_setup 6.12")
879    }
880
881    fn temp_path(tag: &str) -> PathBuf {
882        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
883        std::env::temp_dir().join(format!("air-iou2a-{}-{n}-{tag}", std::process::id()))
884    }
885
886    fn cpath(p: &Path) -> CString {
887        CString::new(p.as_os_str().as_bytes()).expect("chemin sans NUL")
888    }
889
890    /// Crée (tronque) un fichier de travail et le rend en lecture/écriture.
891    fn rw_file(path: &Path) -> std::fs::File {
892        std::fs::OpenOptions::new()
893            .read(true)
894            .write(true)
895            .create(true)
896            .truncate(true)
897            .open(path)
898            .expect("ouverture fichier de test")
899    }
900
901    /// Soumet, attend une complétion, et vérifie le jeton retourné.
902    fn complete_one(ring: &mut IoUring, expected: SubmissionToken) -> Completion {
903        assert_eq!(ring.submit_and_wait(1).expect("submit_and_wait"), 1);
904        let completion = ring.wait_completion().expect("wait_completion");
905        assert_eq!(completion.token(), expected, "user_data round-trip");
906        completion
907    }
908
909    // ── Intégration : lecture / écriture + offsets ────────────────────────
910
911    #[test]
912    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
913    fn write_then_read_round_trips_with_some_and_none_offset() {
914        let path = temp_path("rw");
915        let file = rw_file(&path);
916        let mut ring = ring8();
917
918        // write "hello" @ Some(0).
919        let tok = ring
920            .submit_write(sfd(&file), b"hello".to_vec(), Some(0))
921            .expect("submit_write");
922        let (buf, n) = complete_one(&mut ring, tok)
923            .into_buffer_result()
924            .expect("write ok");
925        assert_eq!(n, 5);
926        assert_eq!(buf, b"hello");
927
928        // read 5 @ Some(0) → "hello" restitué.
929        let tok = ring
930            .submit_read(sfd(&file), vec![0u8; 5], Some(0))
931            .expect("submit_read");
932        let (buf, n) = complete_one(&mut ring, tok)
933            .into_buffer_result()
934            .expect("read ok");
935        assert_eq!(n, 5);
936        assert_eq!(&buf, b"hello");
937
938        // write @ None (position courante = 0) puis read @ None lit l'EOF (0).
939        let tok = ring
940            .submit_write(sfd(&file), b"WORLD".to_vec(), None)
941            .expect("submit_write None");
942        let (_b, n) = complete_one(&mut ring, tok)
943            .into_buffer_result()
944            .expect("write None ok");
945        assert_eq!(n, 5);
946        let tok = ring
947            .submit_read(sfd(&file), vec![0u8; 5], None)
948            .expect("submit_read None");
949        let (_b, n) = complete_one(&mut ring, tok)
950            .into_buffer_result()
951            .expect("read None ok");
952        assert_eq!(n, 0, "position avancée par l'écriture None ⇒ EOF");
953
954        let _ = std::fs::remove_file(&path);
955    }
956
957    #[test]
958    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
959    fn readv_writev_round_trip_multi_buffers() {
960        let path = temp_path("rwv");
961        let file = rw_file(&path);
962        let mut ring = ring8();
963
964        let tok = ring
965            .submit_writev(sfd(&file), vec![b"foo".to_vec(), b"bar".to_vec()], Some(0))
966            .expect("submit_writev");
967        let (_bufs, n) = complete_one(&mut ring, tok)
968            .into_vectored_result()
969            .expect("writev ok");
970        assert_eq!(n, 6);
971
972        let tok = ring
973            .submit_readv(sfd(&file), vec![vec![0u8; 3], vec![0u8; 3]], Some(0))
974            .expect("submit_readv");
975        let (bufs, n) = complete_one(&mut ring, tok)
976            .into_vectored_result()
977            .expect("readv ok");
978        assert_eq!(n, 6);
979        assert_eq!(bufs, vec![b"foo".to_vec(), b"bar".to_vec()]);
980
981        let _ = std::fs::remove_file(&path);
982    }
983
984    #[test]
985    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
986    fn openat2_then_read_then_close_chain() {
987        let path = temp_path("open");
988        std::fs::write(&path, b"hello world").expect("fixture");
989        let mut ring = ring8();
990
991        let tok = ring
992            .submit_openat2(DirFd::Cwd, cpath(&path), OpenHow::default())
993            .expect("submit_openat2");
994        let opened = complete_one(&mut ring, tok)
995            .opened_fd()
996            .expect("openat2 ok");
997
998        let tok = ring
999            .submit_read(opened.as_fd(), vec![0u8; 11], Some(0))
1000            .expect("submit_read");
1001        let (buf, n) = complete_one(&mut ring, tok)
1002            .into_buffer_result()
1003            .expect("read ok");
1004        assert_eq!(&buf[..n], b"hello world");
1005
1006        let tok = ring.submit_close(opened).expect("submit_close");
1007        complete_one(&mut ring, tok).completed().expect("close ok");
1008
1009        let _ = std::fs::remove_file(&path);
1010    }
1011
1012    #[test]
1013    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1014    fn statx_reports_size() {
1015        let path = temp_path("statx");
1016        std::fs::write(&path, b"0123456789").expect("fixture (10 octets)");
1017        let mut ring = ring8();
1018
1019        let tok = ring
1020            .submit_statx(
1021                DirFd::Cwd,
1022                cpath(&path),
1023                StatxFlags::empty(),
1024                StatxMask::SIZE,
1025                Box::new(Statx::default()),
1026            )
1027            .expect("submit_statx");
1028        let statx = complete_one(&mut ring, tok).into_statx().expect("statx ok");
1029        assert!(statx.has(StatxMask::SIZE));
1030        assert_eq!(statx.size, 10);
1031
1032        let _ = std::fs::remove_file(&path);
1033    }
1034
1035    #[test]
1036    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1037    fn directory_and_link_ops() {
1038        let mut ring = ring8();
1039
1040        // mkdirat + unlinkat(REMOVEDIR).
1041        let dir = temp_path("dir");
1042        let tok = ring
1043            .submit_mkdirat(DirFd::Cwd, cpath(&dir), 0o755)
1044            .expect("submit_mkdirat");
1045        complete_one(&mut ring, tok).completed().expect("mkdir ok");
1046        assert!(dir.is_dir());
1047        let tok = ring
1048            .submit_unlinkat(DirFd::Cwd, cpath(&dir), UnlinkFlags::REMOVEDIR)
1049            .expect("submit_unlinkat");
1050        complete_one(&mut ring, tok).completed().expect("rmdir ok");
1051        assert!(!dir.exists());
1052
1053        // renameat.
1054        let src = temp_path("ren-a");
1055        let dst = temp_path("ren-b");
1056        std::fs::write(&src, b"x").expect("fixture");
1057        let tok = ring
1058            .submit_renameat(
1059                DirFd::Cwd,
1060                cpath(&src),
1061                DirFd::Cwd,
1062                cpath(&dst),
1063                RenameFlags::empty(),
1064            )
1065            .expect("submit_renameat");
1066        complete_one(&mut ring, tok).completed().expect("rename ok");
1067        assert!(!src.exists() && dst.exists());
1068
1069        // linkat (hardlink) puis unlinkat (fichier).
1070        let link = temp_path("link");
1071        let tok = ring
1072            .submit_linkat(
1073                DirFd::Cwd,
1074                cpath(&dst),
1075                DirFd::Cwd,
1076                cpath(&link),
1077                LinkFlags::empty(),
1078            )
1079            .expect("submit_linkat");
1080        complete_one(&mut ring, tok).completed().expect("link ok");
1081        assert!(link.exists());
1082
1083        // symlinkat.
1084        let sym = temp_path("sym");
1085        let tok = ring
1086            .submit_symlinkat(cpath(&dst), DirFd::Cwd, cpath(&sym))
1087            .expect("submit_symlinkat");
1088        complete_one(&mut ring, tok)
1089            .completed()
1090            .expect("symlink ok");
1091        assert!(
1092            std::fs::symlink_metadata(&sym)
1093                .expect("lstat")
1094                .file_type()
1095                .is_symlink()
1096        );
1097
1098        for p in [&dst, &link, &sym] {
1099            let _ = std::fs::remove_file(p);
1100        }
1101    }
1102
1103    #[test]
1104    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1105    fn xattr_set_get_round_trip() {
1106        let path = temp_path("xattr");
1107        std::fs::write(&path, b"data").expect("fixture");
1108        let mut ring = ring8();
1109        let name = CString::new("user.air_test").expect("nom sans NUL");
1110
1111        // setxattr (chemin).
1112        let tok = ring
1113            .submit_setxattr(
1114                cpath(&path),
1115                name.clone(),
1116                b"value42".to_vec(),
1117                XattrFlags::empty(),
1118            )
1119            .expect("submit_setxattr");
1120        complete_one(&mut ring, tok)
1121            .completed()
1122            .expect("setxattr ok (ext4 supporte user.*)");
1123
1124        // getxattr (chemin) → restitue la valeur.
1125        let tok = ring
1126            .submit_getxattr(cpath(&path), name.clone(), vec![0u8; 64])
1127            .expect("submit_getxattr");
1128        let (value, n) = complete_one(&mut ring, tok)
1129            .into_xattr_result()
1130            .expect("getxattr ok");
1131        assert_eq!(&value[..n], b"value42");
1132
1133        // fsetxattr / fgetxattr (par FD) sur le même fichier.
1134        let file = std::fs::OpenOptions::new()
1135            .read(true)
1136            .write(true)
1137            .open(&path)
1138            .expect("réouverture");
1139        let fname = CString::new("user.air_fd").expect("nom sans NUL");
1140        let tok = ring
1141            .submit_fsetxattr(
1142                sfd(&file),
1143                fname.clone(),
1144                b"fdval".to_vec(),
1145                XattrFlags::empty(),
1146            )
1147            .expect("submit_fsetxattr");
1148        complete_one(&mut ring, tok)
1149            .completed()
1150            .expect("fsetxattr ok");
1151        let tok = ring
1152            .submit_fgetxattr(sfd(&file), fname, vec![0u8; 64])
1153            .expect("submit_fgetxattr");
1154        let (value, n) = complete_one(&mut ring, tok)
1155            .into_xattr_result()
1156            .expect("fgetxattr ok");
1157        assert_eq!(&value[..n], b"fdval");
1158
1159        let _ = std::fs::remove_file(&path);
1160    }
1161
1162    #[test]
1163    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1164    fn sync_alloc_trunc_advise_ops() {
1165        let path = temp_path("sync");
1166        let file = rw_file(&path);
1167        let mut ring = ring8();
1168
1169        // Écrit un contenu pour donner du grain aux ops.
1170        let tok = ring
1171            .submit_write(sfd(&file), vec![7u8; 256], Some(0))
1172            .expect("submit_write");
1173        let _ = complete_one(&mut ring, tok)
1174            .into_buffer_result()
1175            .expect("ok");
1176
1177        // fsync (DATASYNC) puis fsync (complet).
1178        let tok = ring
1179            .submit_fsync(sfd(&file), FsyncFlags::DATASYNC)
1180            .expect("submit_fsync");
1181        complete_one(&mut ring, tok)
1182            .completed()
1183            .expect("fdatasync ok");
1184        let tok = ring
1185            .submit_fsync(sfd(&file), FsyncFlags::empty())
1186            .expect("submit_fsync");
1187        complete_one(&mut ring, tok).completed().expect("fsync ok");
1188
1189        // sync_file_range.
1190        let tok = ring
1191            .submit_sync_file_range(sfd(&file), 0, 256, SyncFileRangeFlags::WRITE)
1192            .expect("submit_sync_file_range");
1193        complete_one(&mut ring, tok)
1194            .completed()
1195            .expect("sync_file_range ok");
1196
1197        // fallocate (alloue 4 Kio).
1198        let tok = ring
1199            .submit_fallocate(sfd(&file), FallocateMode::empty(), 0, 4096)
1200            .expect("submit_fallocate");
1201        complete_one(&mut ring, tok)
1202            .completed()
1203            .expect("fallocate ok");
1204        assert!(std::fs::metadata(&path).expect("stat").len() >= 4096);
1205
1206        // ftruncate (réduit à 100).
1207        let tok = ring
1208            .submit_ftruncate(sfd(&file), 100)
1209            .expect("submit_ftruncate");
1210        complete_one(&mut ring, tok)
1211            .completed()
1212            .expect("ftruncate ok");
1213        assert_eq!(std::fs::metadata(&path).expect("stat").len(), 100);
1214
1215        // fadvise.
1216        let tok = ring
1217            .submit_fadvise(sfd(&file), 0, 0, FadviseAdvice::WillNeed)
1218            .expect("submit_fadvise");
1219        complete_one(&mut ring, tok)
1220            .completed()
1221            .expect("fadvise ok");
1222
1223        let _ = std::fs::remove_file(&path);
1224    }
1225
1226    #[test]
1227    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1228    fn splice_file_to_pipe_and_tee_pipe_to_pipe() {
1229        let path = temp_path("splice");
1230        std::fs::write(&path, b"splice me").expect("fixture (9 octets)");
1231        let file = std::fs::File::open(&path).expect("open ro");
1232        let mut ring = ring8();
1233
1234        // splice fichier → pipe.
1235        let (mut reader, writer) = std::io::pipe().expect("pipe");
1236        let tok = ring
1237            .submit_splice(
1238                sfd(&file),
1239                Some(0),
1240                sfd(&writer),
1241                None,
1242                9,
1243                SpliceFlags::empty(),
1244            )
1245            .expect("submit_splice");
1246        let n = complete_one(&mut ring, tok)
1247            .into_result()
1248            .expect("splice ok");
1249        assert_eq!(n, 9);
1250        let mut got = [0u8; 9];
1251        reader.read_exact(&mut got).expect("lecture pipe");
1252        assert_eq!(&got, b"splice me");
1253
1254        // tee pipe → pipe.
1255        let (reader1, mut writer1) = std::io::pipe().expect("pipe1");
1256        let (mut reader2, writer2) = std::io::pipe().expect("pipe2");
1257        writer1.write_all(b"tee").expect("alim pipe1");
1258        let tok = ring
1259            .submit_tee(sfd(&reader1), sfd(&writer2), 3, SpliceFlags::empty())
1260            .expect("submit_tee");
1261        let n = complete_one(&mut ring, tok).into_result().expect("tee ok");
1262        assert_eq!(n, 3);
1263        let mut got = [0u8; 3];
1264        reader2.read_exact(&mut got).expect("lecture pipe2");
1265        assert_eq!(&got, b"tee");
1266
1267        let _ = std::fs::remove_file(&path);
1268    }
1269
1270    #[test]
1271    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1272    fn relative_ops_resolve_against_a_directory_fd() {
1273        // Couvre le bras `DirFd::Fd` de `dirfd_to_raw` (les autres tests usent
1274        // `DirFd::Cwd`) : un chemin relatif est résolu contre un FD de
1275        // répertoire ouvert.
1276        let dir = temp_path("dirfd");
1277        std::fs::create_dir(&dir).expect("mkdir fixture");
1278        std::fs::write(dir.join("inside.txt"), b"abcd").expect("fichier interne");
1279        let dir_fd = std::fs::File::open(&dir).expect("open dir fd");
1280        let mut ring = ring8();
1281
1282        let rel = CString::new("inside.txt").expect("nom sans NUL");
1283        let tok = ring
1284            .submit_statx(
1285                DirFd::Fd(sfd(&dir_fd)),
1286                rel,
1287                StatxFlags::empty(),
1288                StatxMask::SIZE,
1289                Box::new(Statx::default()),
1290            )
1291            .expect("submit_statx relatif");
1292        let statx = complete_one(&mut ring, tok).into_statx().expect("statx ok");
1293        assert_eq!(statx.size, 4, "taille du fichier résolu via le dir fd");
1294
1295        let _ = std::fs::remove_file(dir.join("inside.txt"));
1296        let _ = std::fs::remove_dir(&dir);
1297    }
1298
1299    // ── Validation amont (Principe 4) — sans kernel ───────────────────────
1300
1301    #[test]
1302    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1303    fn fallocate_rejects_invalid_geometry_before_submission() {
1304        let path = temp_path("falloc-inval");
1305        let file = rw_file(&path);
1306        let mut ring = ring8();
1307        // length ≤ 0.
1308        assert_eq!(
1309            ring.submit_fallocate(sfd(&file), FallocateMode::empty(), 0, 0),
1310            Err(Errno::EINVAL)
1311        );
1312        // offset < 0.
1313        assert_eq!(
1314            ring.submit_fallocate(sfd(&file), FallocateMode::empty(), -1, 10),
1315            Err(Errno::EINVAL)
1316        );
1317        // offset + length déborde un i64.
1318        assert_eq!(
1319            ring.submit_fallocate(sfd(&file), FallocateMode::empty(), i64::MAX, 1),
1320            Err(Errno::EINVAL)
1321        );
1322        assert_eq!(ring.in_flight(), 0, "aucun slot consommé sur rejet amont");
1323        let _ = std::fs::remove_file(&path);
1324    }
1325
1326    #[test]
1327    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1328    fn length_overflow_validations_reject_before_submission() {
1329        let path = temp_path("len-inval");
1330        let file = rw_file(&path);
1331        let mut ring = ring8();
1332        let over_u32 = u64::from(u32::MAX) + 1;
1333        assert_eq!(
1334            ring.submit_sync_file_range(sfd(&file), 0, over_u32, SyncFileRangeFlags::WRITE),
1335            Err(Errno::EINVAL)
1336        );
1337        assert_eq!(
1338            ring.submit_fadvise(sfd(&file), 0, over_u32, FadviseAdvice::Normal),
1339            Err(Errno::EINVAL)
1340        );
1341        // readv au-delà d'IOV_MAX.
1342        let too_many = vec![Vec::<u8>::new(); raw::IOV_MAX + 1];
1343        assert_eq!(
1344            ring.submit_readv(sfd(&file), too_many, Some(0)),
1345            Err(Errno::EINVAL)
1346        );
1347        assert_eq!(ring.in_flight(), 0);
1348        let _ = std::fs::remove_file(&path);
1349    }
1350
1351    // ── Back-pressure (EBUSY) : les deux branches de `submit_op` ───────────
1352
1353    #[test]
1354    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1355    fn slab_full_returns_ebusy() {
1356        use crate::io_uring::IoUringBuilder;
1357        let path = temp_path("ebusy-slab");
1358        let file = rw_file(&path);
1359        // Slab limité à 1 via le builder.
1360        let mut ring = IoUringBuilder::new(NonZeroU32::new(8).expect("8"))
1361            .max_inflight(NonZeroU32::new(1).expect("1"))
1362            .build()
1363            .expect("ring slab=1");
1364        let _t = ring
1365            .submit_read(sfd(&file), vec![0u8; 4], Some(0))
1366            .expect("1ʳᵉ op");
1367        assert_eq!(
1368            ring.submit_read(sfd(&file), vec![0u8; 4], Some(0)),
1369            Err(Errno::EBUSY),
1370            "slab plein ⇒ EBUSY (reserve)"
1371        );
1372        // Quiesce.
1373        ring.submit_and_wait(1).expect("submit");
1374        let _ = ring.wait_completion().expect("complétion");
1375        let _ = std::fs::remove_file(&path);
1376    }
1377
1378    #[test]
1379    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1380    fn full_submission_queue_returns_ebusy() {
1381        use crate::io_uring::IoUringBuilder;
1382        let path = temp_path("ebusy-sq");
1383        let file = rw_file(&path);
1384        // SQ = 4, slab = 8 : la SQ se remplit d'abord.
1385        let mut ring = IoUringBuilder::new(NonZeroU32::new(4).expect("4"))
1386            .max_inflight(NonZeroU32::new(8).expect("8"))
1387            .build()
1388            .expect("ring SQ=4");
1389        for _ in 0..4 {
1390            ring.submit_fsync(sfd(&file), FsyncFlags::empty())
1391                .expect("remplissage SQ");
1392        }
1393        assert_eq!(
1394            ring.submit_fsync(sfd(&file), FsyncFlags::empty()),
1395            Err(Errno::EBUSY),
1396            "SQ pleine ⇒ EBUSY (space_left == 0)"
1397        );
1398        // Draine les 4.
1399        ring.submit_and_wait(4).expect("submit");
1400        let mut n = 0;
1401        while n < 4 {
1402            for _ in ring.completions() {
1403                n += 1;
1404            }
1405        }
1406        let _ = std::fs::remove_file(&path);
1407    }
1408
1409    // ── Options de soumission (skip_cqe_on_success) ───────────────────────
1410
1411    #[test]
1412    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1413    fn skip_cqe_applies_to_no_payload_ops_only() {
1414        use crate::io_uring::SubmitOptions;
1415        let path = temp_path("skip");
1416        let file = rw_file(&path);
1417        let mut ring = ring8();
1418
1419        // Op SANS payload (fsync) + skip : slot libéré à la soumission.
1420        ring.with(SubmitOptions::default().skip_cqe_on_success());
1421        ring.submit_fsync(sfd(&file), FsyncFlags::empty())
1422            .expect("fsync skip");
1423        assert_eq!(ring.in_flight(), 0, "no-payload + skip ⇒ slot libéré");
1424        assert_eq!(ring.submit().expect("submit"), 1);
1425
1426        // Op AVEC payload (read) + skip : le bit est retiré (sûreté), slot retenu.
1427        ring.with(SubmitOptions::default().skip_cqe_on_success());
1428        let tok = ring
1429            .submit_read(sfd(&file), vec![0u8; 4], Some(0))
1430            .expect("read skip");
1431        assert_eq!(ring.in_flight(), 1, "payload + skip ⇒ slot conservé");
1432        let (_b, _n) = complete_one(&mut ring, tok)
1433            .into_buffer_result()
1434            .expect("read complète bien (CQE non sauté)");
1435        assert_eq!(ring.in_flight(), 0);
1436        let _ = std::fs::remove_file(&path);
1437    }
1438
1439    // ── Robustesse aux chemins non-UTF-8 (property-based) ──────────────────
1440
1441    proptest::proptest! {
1442        /// Un chemin fait d'octets arbitraires non nuls (souvent non-UTF-8) ne
1443        /// fait jamais paniquer la soumission ni le décodage : la complétion
1444        /// renvoie une valeur typée ou une erreur typée (ici `ENOENT`/`Ok`).
1445        #[test]
1446        #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1447        fn non_utf8_paths_never_panic(bytes in proptest::collection::vec(1u8..=255, 1..40)) {
1448            let cpath = CString::new(bytes).expect("aucun NUL (octets 1..=255)");
1449            let mut ring = ring8();
1450            let tok = ring
1451                .submit_statx(
1452                    DirFd::Cwd,
1453                    cpath,
1454                    StatxFlags::empty(),
1455                    StatxMask::SIZE,
1456                    Box::new(Statx::default()),
1457                )
1458                .expect("submission jamais en panique");
1459            assert_eq!(ring.submit_and_wait(1).expect("submit"), 1);
1460            let completion = ring.wait_completion().expect("complétion");
1461            proptest::prop_assert_eq!(completion.token(), tok);
1462            // `into_statx` rend Ok(Box<Statx>) ou Err(errno) — jamais de panique.
1463            let _ = completion.into_statx();
1464        }
1465
1466        /// read/write : octets transférés ≤ len, et restitution **fidèle** du
1467        /// buffer (le contenu écrit est relu à l'identique).
1468        #[test]
1469        #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1470        fn write_read_transfers_bounded_and_faithful(
1471            data in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..512)
1472        ) {
1473            let path = temp_path("prop-rw");
1474            let file = rw_file(&path);
1475            let mut ring = ring8();
1476            let len = data.len();
1477
1478            let tok = ring
1479                .submit_write(sfd(&file), data.clone(), Some(0))
1480                .expect("submit_write");
1481            assert_eq!(ring.submit_and_wait(1).expect("submit"), 1);
1482            let (_b, written) = ring
1483                .wait_completion()
1484                .expect("complétion")
1485                .into_buffer_result()
1486                .expect("write ok");
1487            let _ = tok;
1488            proptest::prop_assert!(written <= len);
1489            proptest::prop_assert_eq!(written, len);
1490
1491            let tok = ring
1492                .submit_read(sfd(&file), vec![0u8; len], Some(0))
1493                .expect("submit_read");
1494            assert_eq!(ring.submit_and_wait(1).expect("submit"), 1);
1495            let (buf, read) = ring
1496                .wait_completion()
1497                .expect("complétion")
1498                .into_buffer_result()
1499                .expect("read ok");
1500            let _ = tok;
1501            proptest::prop_assert!(read <= len);
1502            proptest::prop_assert_eq!(&buf[..read], &data[..read]);
1503            let _ = std::fs::remove_file(&path);
1504        }
1505    }
1506
1507    // ── Accesseurs de complétion : bras défensifs + erreurs (purs) ─────────
1508    //
1509    // Construits directement (sans kernel) ⇒ tournent sous Miri. Couvrent les
1510    // bras `_` (mauvais payload) et les bras d'erreur (`res < 0`) des
1511    // accesseurs typés, que le chemin nominal (intégration) ne traverse pas.
1512
1513    fn cqe(res: i32, payload: Option<OwnedOp>) -> Completion {
1514        Completion {
1515            token: SubmissionToken::new(0, 0),
1516            res,
1517            raw_flags: 0,
1518            payload,
1519            multishot: false,
1520        }
1521    }
1522
1523    #[test]
1524    fn accessors_on_wrong_payload_yield_empty_defaults() {
1525        // into_buffer_result sur payload non-Bytes ⇒ buffer vide.
1526        let (b, n) = cqe(0, Some(OwnedOp::Path(CString::new("x").unwrap())))
1527            .into_buffer_result()
1528            .expect("ok");
1529        assert!(b.is_empty() && n == 0);
1530        // into_vectored_result sur Bytes ⇒ vecteur vide.
1531        let (v, n) = cqe(0, Some(OwnedOp::Bytes(vec![1, 2])))
1532            .into_vectored_result()
1533            .expect("ok");
1534        assert!(v.is_empty() && n == 0);
1535        // into_statx sur None ⇒ Statx zéro-initialisé.
1536        let st = cqe(0, None).into_statx().expect("ok");
1537        assert_eq!(st.mask, 0);
1538        // into_xattr_result sur None ⇒ valeur vide.
1539        let (val, n) = cqe(0, None).into_xattr_result().expect("ok");
1540        assert!(val.is_empty() && n == 0);
1541    }
1542
1543    #[test]
1544    fn accessors_propagate_kernel_errors() {
1545        // res < 0 ⇒ Err(errno) sur chaque accesseur.
1546        assert_eq!(
1547            cqe(-2, Some(OwnedOp::Bytes(vec![1]))).into_buffer_result(),
1548            Err(Errno::ENOENT)
1549        );
1550        assert_eq!(cqe(-2, None).into_vectored_result(), Err(Errno::ENOENT));
1551        assert_eq!(cqe(-13, None).into_statx().err(), Some(Errno::EACCES));
1552        assert_eq!(cqe(-9, None).into_xattr_result(), Err(Errno::EBADF));
1553        // opened_fd : res < 0 ⇒ Err (pas de FD créé).
1554        assert_eq!(cqe(-2, None).opened_fd().err(), Some(Errno::ENOENT));
1555    }
1556
1557    // ── Invariants d'ownership (Miri) : transfert + restitution sound ──────
1558
1559    #[test]
1560    fn ownership_bytes_round_trips_without_uaf() {
1561        let data = vec![3u8; 32];
1562        let (buf, n) = cqe(32, Some(OwnedOp::Bytes(data.clone())))
1563            .into_buffer_result()
1564            .expect("ok");
1565        assert_eq!(n, 32);
1566        assert_eq!(buf, data);
1567    }
1568
1569    #[test]
1570    fn ownership_vectored_round_trips_without_uaf() {
1571        let original = vec![vec![1u8, 2, 3], vec![4, 5]];
1572        let mut buffers = original.clone();
1573        let iovecs: Box<[Iovec]> = buffers
1574            .iter_mut()
1575            .map(|b| Iovec {
1576                iov_base: b.as_mut_ptr(),
1577                iov_len: b.len(),
1578            })
1579            .collect();
1580        let payload = OwnedOp::Vectored { buffers, iovecs };
1581        let (restituted, n) = cqe(5, Some(payload)).into_vectored_result().expect("ok");
1582        assert_eq!(n, 5);
1583        assert_eq!(restituted, original);
1584    }
1585
1586    #[test]
1587    fn ownership_statx_and_xattr_round_trip_without_uaf() {
1588        let mut out = Box::new(Statx::default());
1589        out.size = 4242;
1590        let payload = OwnedOp::Statx {
1591            out,
1592            path: CString::new("/some/path").unwrap(),
1593        };
1594        let st = cqe(0, Some(payload)).into_statx().expect("ok");
1595        assert_eq!(st.size, 4242);
1596
1597        let payload = OwnedOp::Xattr {
1598            value: b"abc".to_vec(),
1599            name: CString::new("user.k").unwrap(),
1600            path: Some(CString::new("/p").unwrap()),
1601        };
1602        let (value, n) = cqe(3, Some(payload)).into_xattr_result().expect("ok");
1603        assert_eq!(&value[..n], b"abc");
1604    }
1605
1606    #[test]
1607    fn ownership_payloads_drop_without_extraction() {
1608        // Un payload jamais extrait (op annulée, complétion ignorée) se drop
1609        // proprement : aucun double-free / fuite (vérifié sous Miri).
1610        let mut buffers = vec![vec![9u8; 8]];
1611        let iovecs: Box<[Iovec]> = buffers
1612            .iter_mut()
1613            .map(|b| Iovec {
1614                iov_base: b.as_mut_ptr(),
1615                iov_len: b.len(),
1616            })
1617            .collect();
1618        let payloads = [
1619            OwnedOp::Bytes(vec![1, 2, 3]),
1620            OwnedOp::Vectored { buffers, iovecs },
1621            OwnedOp::Path(CString::new("p").unwrap()),
1622            OwnedOp::TwoPaths(CString::new("a").unwrap(), CString::new("b").unwrap()),
1623            OwnedOp::Xattr {
1624                value: vec![0u8; 4],
1625                name: CString::new("n").unwrap(),
1626                path: None,
1627            },
1628        ];
1629        for p in payloads {
1630            drop(cqe(0, Some(p)));
1631        }
1632    }
1633
1634    // ── Madvise sur MmapRegion ────────────────────────────────────────────
1635
1636    /// `submit_madvise(WILLNEED)` sur une `MmapRegion` complète ; la validation
1637    /// amont du `range` rend `EINVAL` (hors bornes / mal formé) avant soumission.
1638    #[test]
1639    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1640    fn madvise_on_mmap_region_completes_and_validates() {
1641        use air_sys_types::mem::{MapFlags, ProtectionFlags};
1642        let region = MmapRegion::new_anonymous(
1643            4096,
1644            ProtectionFlags::READ | ProtectionFlags::WRITE,
1645            MapFlags::PRIVATE,
1646        )
1647        .expect("new_anonymous");
1648        let mut ring = ring8();
1649
1650        let tok = ring
1651            .submit_madvise(&region, 0..4096, MadviseAdvice::WillNeed)
1652            .expect("submit_madvise");
1653        complete_one(&mut ring, tok)
1654            .completed()
1655            .expect("madvise WILLNEED ok");
1656
1657        // Sous-plage valide (DONTNEED sur une moitié).
1658        let tok = ring
1659            .submit_madvise(&region, 0..2048, MadviseAdvice::DontNeed)
1660            .expect("submit_madvise demi");
1661        complete_one(&mut ring, tok)
1662            .completed()
1663            .expect("madvise DONTNEED ok");
1664
1665        // Validation amont : range.end > len ⇒ EINVAL avant soumission.
1666        assert_eq!(
1667            ring.submit_madvise(&region, 0..8192, MadviseAdvice::DontNeed)
1668                .unwrap_err(),
1669            Errno::EINVAL
1670        );
1671        // range mal formé (start > end) ⇒ EINVAL. Construit explicitement pour
1672        // ne pas déclencher `clippy::reversed_empty_ranges` sur un littéral.
1673        let malformed = Range {
1674            start: 100,
1675            end: 50,
1676        };
1677        assert_eq!(
1678            ring.submit_madvise(&region, malformed, MadviseAdvice::Normal)
1679                .unwrap_err(),
1680            Errno::EINVAL
1681        );
1682    }
1683
1684    /// Dropper la `MmapRegion` côté utilisateur **pendant** qu'une op `madvise`
1685    /// est en vol ne corrompt rien : la garde du slot tient la région mappée
1686    /// jusqu'à la complétion (ni UAF, ni fuite).
1687    #[test]
1688    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1689    fn madvise_keeps_region_alive_while_in_flight() {
1690        use air_sys_types::mem::{MapFlags, ProtectionFlags};
1691        let region = MmapRegion::new_anonymous(
1692            4096,
1693            ProtectionFlags::READ | ProtectionFlags::WRITE,
1694            MapFlags::PRIVATE,
1695        )
1696        .expect("new_anonymous");
1697        let mut ring = ring8();
1698        let tok = ring
1699            .submit_madvise(&region, 0..4096, MadviseAdvice::WillNeed)
1700            .expect("submit_madvise");
1701        // L'utilisateur lâche sa référence : la garde garée en S1 reste.
1702        drop(region);
1703        // L'op complète proprement ; la région est démappée au drop du slot.
1704        complete_one(&mut ring, tok)
1705            .completed()
1706            .expect("madvise en vol ok malgré drop côté utilisateur");
1707    }
1708}