Skip to main content

air_sys_syscall/io_uring/
async_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 **async-spécifiques** d'io_uring (Temps 2c) : contrôle du ring
6//! (`nop`), temporisation (`timeout`), annulation asynchrone (`cancel`),
7//! surveillance d'événements (`poll`, `epoll_ctl`), attente de processus
8//! (`waitid`), notification inter-ring (`msg_ring` data). Par-dessus le cœur
9//! Temps 1, conventions Temps 2a/2b (ownership S1, accesseurs typés).
10//!
11//! Référence normative : `docs/specs/layer-0/io-uring-2c-async.md`.
12//!
13//! **`futex_wait`/`wake`/`waitv` (51/52/53)** sont désormais **implémentés**
14//! (PR coordonnée `family-mem`) : le mot futex vit dans une [`MmapRegion`]
15//! partageable et le slot S1 retient sa garde de vivacité — la signature
16//! historique `&AtomicU32` (insoundable en async) est remplacée par
17//! `&MmapRegion` + offset (cf. `family-mem-mmap-region.md §3`).
18//!
19//! **Différé (signalé)** :
20//! - `link_timeout` (15) → **Temps 3c** (n'a de sens qu'attaché à une chaîne
21//!   liée via le `LinkedChainBuilder` ; jamais en op isolée).
22//! - `files_update` (20), `fixed_fd_install` (54), `msg_ring_fd` (40
23//!   `MSG_SEND_FD`) → **Temps 3a** (table de FD enregistrée / descripteurs
24//!   directs).
25
26use super::owned::OwnedOp;
27use super::raw::{self, FutexWaitvRaw, KernelTimespec};
28use super::{CancelTarget, IoUring, SubmissionToken, opcode_number};
29use crate::mem::MmapRegion;
30use air_sys_types::Errno;
31use air_sys_types::fd::{AsRawFd, BorrowedFd};
32use air_sys_types::io_uring::{
33    CancelFlags, EpollEvent, EpollOp, FutexFlags, MessageRingFlags, PollEvents, TimeoutFlags,
34    TimeoutSpec,
35};
36use air_sys_types::process::{WaitOptions, WaitTarget};
37use air_sys_types::signal::SignalInfo;
38use alloc::boxed::Box;
39use alloc::vec::Vec;
40use core::time::Duration;
41
42/// Construit un `__kernel_timespec` depuis la durée d'un [`TimeoutSpec`]
43/// (`None` ⇒ zéro, le timeout est alors purement par comptage).
44pub(crate) fn timespec_of(spec: TimeoutSpec) -> KernelTimespec {
45    let d = spec.duration.unwrap_or(Duration::ZERO);
46    KernelTimespec {
47        tv_sec: i64::try_from(d.as_secs()).unwrap_or(i64::MAX),
48        tv_nsec: i64::from(d.subsec_nanos()),
49    }
50}
51
52/// `(idtype, id)` kernel d'une [`WaitTarget`] (sentinelles typées, ADR-021).
53fn waitid_target(target: WaitTarget<'_>) -> (i32, i32) {
54    match target {
55        WaitTarget::AnyChild => (raw::P_ALL, 0),
56        WaitTarget::Pid(p) => (raw::P_PID, p.as_raw()),
57        WaitTarget::ProcessGroup(p) => (raw::P_PGID, p.as_raw()),
58        WaitTarget::AnyProcessGroup => (raw::P_PGID, 0),
59        WaitTarget::PidFd(fd) => (raw::P_PIDFD, fd.as_raw_fd()),
60    }
61}
62
63impl IoUring {
64    // ── Contrôle : nop ────────────────────────────────────────────────────
65
66    /// Soumet un `IORING_OP_NOP` (0) **injectant un résultat** (`res = injected`)
67    /// via `IORING_NOP_INJECT_RESULT`. Précieux pour exercer les chemins
68    /// d'erreur des accesseurs **sans** provoquer de vraie erreur kernel.
69    /// Complétion via [`Completion::into_result`](super::Completion::into_result).
70    ///
71    /// # Errors
72    ///
73    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
74    pub fn submit_nop_with_result(&mut self, injected: i32) -> Result<SubmissionToken, Errno> {
75        let len = injected.cast_unsigned();
76        self.submit_op(None, |sqe| {
77            sqe.opcode = raw::IORING_OP_NOP;
78            sqe.len = len;
79            sqe.op_flags = raw::IORING_NOP_INJECT_RESULT;
80        })
81    }
82
83    // ── Temporisation ─────────────────────────────────────────────────────
84
85    /// Soumet un `IORING_OP_TIMEOUT` (11) : échéance temporelle et/ou seuil de
86    /// complétions. `res == -ETIME` à l'expiration (succès si
87    /// `TimeoutFlags::ETIME_SUCCESS`). Complétion via
88    /// [`Completion::completed`](super::Completion::completed).
89    ///
90    /// # Errors
91    ///
92    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
93    pub fn submit_timeout(
94        &mut self,
95        spec: TimeoutSpec,
96        flags: TimeoutFlags,
97    ) -> Result<SubmissionToken, Errno> {
98        let ts = Box::new(timespec_of(spec));
99        let ts_ptr = core::ptr::from_ref(&*ts) as u64;
100        let count = u64::from(spec.count);
101        let op_flags = flags.bits();
102        self.submit_op(Some(OwnedOp::Timeout(ts)), |sqe| {
103            sqe.opcode = raw::IORING_OP_TIMEOUT;
104            sqe.fd = -1;
105            sqe.addr_or_splice_off_in = ts_ptr;
106            sqe.len = 1;
107            sqe.off_or_addr2 = count;
108            sqe.op_flags = op_flags;
109        })
110    }
111
112    /// Soumet un `IORING_OP_TIMEOUT_REMOVE` (12) : annule un timeout en vol
113    /// désigné par son jeton. Complétion via `completed` (`ENOENT` si la cible
114    /// est inconnue/déjà déclenchée).
115    ///
116    /// # Errors
117    ///
118    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
119    pub fn submit_timeout_remove(
120        &mut self,
121        target: SubmissionToken,
122    ) -> Result<SubmissionToken, Errno> {
123        let addr = target.to_user_data();
124        self.submit_op(None, |sqe| {
125            sqe.opcode = raw::IORING_OP_TIMEOUT_REMOVE;
126            sqe.fd = -1;
127            sqe.addr_or_splice_off_in = addr;
128        })
129    }
130
131    /// Soumet un `IORING_OP_TIMEOUT_REMOVE` + `IORING_TIMEOUT_UPDATE` (12) :
132    /// re-arme un timeout en vol avec un nouveau `spec`. Complétion via
133    /// `completed`.
134    ///
135    /// # Errors
136    ///
137    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
138    pub fn submit_timeout_update(
139        &mut self,
140        target: SubmissionToken,
141        spec: TimeoutSpec,
142        flags: TimeoutFlags,
143    ) -> Result<SubmissionToken, Errno> {
144        let ts = Box::new(timespec_of(spec));
145        let ts_ptr = core::ptr::from_ref(&*ts) as u64;
146        let addr = target.to_user_data();
147        let op_flags = flags.bits() | raw::IORING_TIMEOUT_UPDATE;
148        self.submit_op(Some(OwnedOp::Timeout(ts)), |sqe| {
149            sqe.opcode = raw::IORING_OP_TIMEOUT_REMOVE;
150            sqe.fd = -1;
151            sqe.addr_or_splice_off_in = addr;
152            sqe.off_or_addr2 = ts_ptr;
153            sqe.op_flags = op_flags;
154        })
155    }
156
157    // ── Annulation asynchrone ─────────────────────────────────────────────
158
159    /// Soumet un `IORING_OP_ASYNC_CANCEL` (14) : variante **asynchrone** (dans le
160    /// flux) du `sync_cancel` du Temps 1. La complétion de l'op annulée arrive en
161    /// `-ECANCELED`, son slot S1 (et son buffer) restitué normalement.
162    /// Complétion via `into_result` (= nombre d'opérations annulées ; `-ENOENT`
163    /// si rien ne correspond, `-EALREADY` si déjà en cours).
164    ///
165    /// # Errors
166    ///
167    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
168    pub fn submit_cancel(
169        &mut self,
170        target: CancelTarget<'_>,
171        flags: CancelFlags,
172    ) -> Result<SubmissionToken, Errno> {
173        let mut fd = -1;
174        let mut addr = 0u64;
175        let mut len = 0u32;
176        let mut cancel_flags = flags.bits();
177        match target {
178            CancelTarget::Token(token) => addr = token.to_user_data(),
179            CancelTarget::Fd(target_fd) => {
180                fd = target_fd.as_raw_fd();
181                cancel_flags |= raw::IORING_ASYNC_CANCEL_FD;
182            }
183            CancelTarget::Op(op) => {
184                len = u32::from(opcode_number(op));
185                cancel_flags |= raw::IORING_ASYNC_CANCEL_OP;
186            }
187            CancelTarget::Any => cancel_flags |= raw::IORING_ASYNC_CANCEL_ANY,
188        }
189        self.submit_op(None, |sqe| {
190            sqe.opcode = raw::IORING_OP_ASYNC_CANCEL;
191            sqe.fd = fd;
192            sqe.addr_or_splice_off_in = addr;
193            sqe.len = len;
194            sqe.op_flags = cancel_flags;
195        })
196    }
197
198    // ── Surveillance d'événements ─────────────────────────────────────────
199
200    /// Soumet un `IORING_OP_POLL_ADD` (6) : surveille `fd` pour `events` (mono-
201    /// coup ; multishot/level-triggered au Temps 3d). Complétion via
202    /// [`Completion::into_poll_result`](super::Completion::into_poll_result).
203    ///
204    /// # Errors
205    ///
206    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
207    pub fn submit_poll_add(
208        &mut self,
209        fd: BorrowedFd<'_>,
210        events: PollEvents,
211    ) -> Result<SubmissionToken, Errno> {
212        let fd = fd.as_raw_fd();
213        let events = events.bits();
214        self.submit_op(None, |sqe| {
215            sqe.opcode = raw::IORING_OP_POLL_ADD;
216            sqe.fd = fd;
217            sqe.op_flags = events;
218        })
219    }
220
221    /// Soumet un `IORING_OP_POLL_REMOVE` (7) : retire un poll en vol désigné par
222    /// son jeton. Complétion via `completed`.
223    ///
224    /// # Errors
225    ///
226    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
227    pub fn submit_poll_remove(
228        &mut self,
229        target: SubmissionToken,
230    ) -> Result<SubmissionToken, Errno> {
231        let addr = target.to_user_data();
232        self.submit_op(None, |sqe| {
233            sqe.opcode = raw::IORING_OP_POLL_REMOVE;
234            sqe.fd = -1;
235            sqe.addr_or_splice_off_in = addr;
236        })
237    }
238
239    /// Soumet un `IORING_OP_POLL_REMOVE` + `IORING_POLL_UPDATE_EVENTS` (7) :
240    /// modifie les événements surveillés d'un poll en vol. Complétion via
241    /// `completed`.
242    ///
243    /// # Errors
244    ///
245    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
246    pub fn submit_poll_update(
247        &mut self,
248        target: SubmissionToken,
249        events: PollEvents,
250    ) -> Result<SubmissionToken, Errno> {
251        let ud = target.to_user_data();
252        let events = events.bits();
253        self.submit_op(None, |sqe| {
254            sqe.opcode = raw::IORING_OP_POLL_REMOVE;
255            sqe.fd = -1;
256            sqe.addr_or_splice_off_in = ud;
257            sqe.off_or_addr2 = ud;
258            sqe.len = raw::IORING_POLL_UPDATE_EVENTS;
259            sqe.op_flags = events;
260        })
261    }
262
263    /// Soumet un `IORING_OP_EPOLL_CTL` (29) : add/mod/del sur un set epoll, en
264    /// asynchrone. L'`epoll_event` est gardé en vie dans le slot (lu par le
265    /// kernel). Complétion via `completed`.
266    ///
267    /// # Errors
268    ///
269    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
270    pub fn submit_epoll_ctl(
271        &mut self,
272        epfd: BorrowedFd<'_>,
273        op: EpollOp,
274        fd: BorrowedFd<'_>,
275        event: EpollEvent,
276    ) -> Result<SubmissionToken, Errno> {
277        let epfd = epfd.as_raw_fd();
278        let target_fd = u64::from(fd.as_raw_fd().cast_unsigned());
279        let op = op.as_raw().cast_unsigned();
280        let event = Box::new(event);
281        let event_ptr = core::ptr::from_ref(&*event) as u64;
282        self.submit_op(Some(OwnedOp::Epoll(event)), |sqe| {
283            sqe.opcode = raw::IORING_OP_EPOLL_CTL;
284            sqe.fd = epfd;
285            sqe.addr_or_splice_off_in = event_ptr;
286            sqe.len = op;
287            sqe.off_or_addr2 = target_fd;
288        })
289    }
290
291    // ── Attente de processus ──────────────────────────────────────────────
292
293    /// Soumet un `IORING_OP_WAITID` (50) : récolte la fin d'un enfant **sans
294    /// bloquer** le reactor. Le kernel remplit `info` (`siginfo_t`, déplacé dans
295    /// le slot). Complétion via
296    /// [`Completion::into_waitid_result`](super::Completion::into_waitid_result).
297    ///
298    /// # Errors
299    ///
300    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
301    pub fn submit_waitid(
302        &mut self,
303        target: WaitTarget<'_>,
304        options: WaitOptions,
305        info: Box<SignalInfo>,
306    ) -> Result<SubmissionToken, Errno> {
307        let (idtype, id) = waitid_target(target);
308        let info_ptr = info.as_bytes().as_ptr() as u64;
309        let options = options.bits().cast_unsigned();
310        self.submit_op(Some(OwnedOp::Waitid(info)), |sqe| {
311            sqe.opcode = raw::IORING_OP_WAITID;
312            sqe.fd = id;
313            sqe.len = idtype.cast_unsigned();
314            sqe.off_or_addr2 = info_ptr;
315            sqe.splice_fd_in_or_file_index = options;
316        })
317    }
318
319    // ── Inter-ring (notification) ─────────────────────────────────────────
320
321    /// Soumet un `IORING_OP_MSG_RING` (40, `MSG_DATA`) : poste un CQE (`res` +
322    /// `user_data` arbitraires) dans le **ring cible** — notification inter-
323    /// reactor très légère. Complétion (côté émetteur) via `completed` ; côté
324    /// cible, un CQE apparaît dans sa CQ.
325    ///
326    /// # Errors
327    ///
328    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
329    pub fn submit_message_ring_data(
330        &mut self,
331        target: &IoUring,
332        res: i32,
333        user_data: u64,
334        flags: MessageRingFlags,
335    ) -> Result<SubmissionToken, Errno> {
336        let target_fd = target.fd_raw();
337        let len = res.cast_unsigned();
338        let op_flags = flags.bits();
339        self.submit_op(None, |sqe| {
340            sqe.opcode = raw::IORING_OP_MSG_RING;
341            sqe.fd = target_fd;
342            sqe.addr_or_splice_off_in = raw::IORING_MSG_DATA;
343            sqe.len = len;
344            sqe.off_or_addr2 = user_data;
345            sqe.op_flags = op_flags;
346        })
347    }
348
349    // ── Futex (mémoire partagée) ──────────────────────────────────────────
350    //
351    // Le mot futex vit dans une `MmapRegion` partageable : le kernel le lit
352    // **de façon asynchrone** jusqu'à la complétion. Le slot S1 retient la
353    // garde de vivacité de la région (`OwnedOp::MemLiveness` /
354    // `OwnedOp::FutexWaitv`) ⇒ `munmap` impossible tant que l'op est en vol :
355    // ni use-after-unmap, ni fuite (cf. `family-mem-mmap-region.md`).
356
357    /// Encode les drapeaux `FUTEX2_*` posés dans `sqe->fd` (`WAIT`/`WAKE`). La
358    /// taille `FUTEX2_SIZE_U32` est **imposée** (le mot d'une `MmapRegion` est un
359    /// `AtomicU32`) ; seul `PRIVATE` provient de `flags`.
360    fn futex2_fd(flags: FutexFlags) -> Result<i32, Errno> {
361        i32::try_from(raw::FUTEX2_SIZE_U32 | flags.bits()).map_err(|_| Errno::EINVAL)
362    }
363
364    /// Soumet un `IORING_OP_FUTEX_WAIT` (51) : attend que le mot futex à `offset`
365    /// dans `region` soit réveillé (ou `-EAGAIN` si sa valeur ≠ `expected` à
366    /// l'armement). `mask` est le bitset futex (bits surveillés). La région est
367    /// gardée vivante par le slot S1. Complétion via
368    /// [`Completion::completed`](super::Completion::completed).
369    ///
370    /// # Errors
371    ///
372    /// [`Errno::EINVAL`] si `offset` est hors bornes, non aligné sur 4, ou si la
373    /// région n'est pas lisible (cf. [`MmapRegion::futex_word`]) ;
374    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
375    pub fn submit_futex_wait(
376        &mut self,
377        region: &MmapRegion,
378        offset: usize,
379        expected: u64,
380        mask: u64,
381        flags: FutexFlags,
382    ) -> Result<SubmissionToken, Errno> {
383        let uaddr = core::ptr::from_ref(region.futex_word(offset)?) as u64;
384        let futex2 = Self::futex2_fd(flags)?;
385        let guard = region.liveness_handle();
386        self.submit_op(Some(OwnedOp::MemLiveness(guard)), |sqe| {
387            sqe.opcode = raw::IORING_OP_FUTEX_WAIT;
388            sqe.fd = futex2;
389            sqe.addr_or_splice_off_in = uaddr;
390            sqe.off_or_addr2 = expected;
391            sqe.addr3 = mask;
392        })
393    }
394
395    /// Soumet un `IORING_OP_FUTEX_WAKE` (52) : réveille jusqu'à `nr` waiters sur
396    /// le mot futex à `offset` dans `region` (filtrés par `mask`). Complétion via
397    /// [`Completion::into_result`](super::Completion::into_result) (nombre de
398    /// waiters réveillés).
399    ///
400    /// # Errors
401    ///
402    /// [`Errno::EINVAL`] si `offset` est invalide (cf. [`MmapRegion::futex_word`]) ;
403    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
404    pub fn submit_futex_wake(
405        &mut self,
406        region: &MmapRegion,
407        offset: usize,
408        nr: u64,
409        mask: u64,
410        flags: FutexFlags,
411    ) -> Result<SubmissionToken, Errno> {
412        let uaddr = core::ptr::from_ref(region.futex_word(offset)?) as u64;
413        let futex2 = Self::futex2_fd(flags)?;
414        let guard = region.liveness_handle();
415        self.submit_op(Some(OwnedOp::MemLiveness(guard)), |sqe| {
416            sqe.opcode = raw::IORING_OP_FUTEX_WAKE;
417            sqe.fd = futex2;
418            sqe.addr_or_splice_off_in = uaddr;
419            sqe.off_or_addr2 = nr;
420            sqe.addr3 = mask;
421        })
422    }
423
424    /// Soumet un `IORING_OP_FUTEX_WAITV` (53) : attend sur **plusieurs** mots
425    /// futex à la fois (réveil dès qu'**un** se déclenche). Chaque [`FutexWaiter`]
426    /// désigne une région + offset, sa valeur attendue. Le tableau de
427    /// `futex_waitv` et toutes les gardes de vivacité sont garés dans le slot S1.
428    /// Complétion via [`Completion::completed`](super::Completion::completed).
429    ///
430    /// **Note ABI.** `struct futex_waitv` du kernel ne porte **pas** de masque
431    /// par-attendu (champs `{ val, uaddr, flags, __reserved }`) : [`FutexWaiter`]
432    /// n'expose donc **pas** de masque (un champ silencieusement ignoré serait un
433    /// footgun, cf. ADR-032). Pour une attente **masquée**, utiliser
434    /// [`IoUring::submit_futex_wait`] (mono-attente, qui garde `mask`).
435    ///
436    /// # Errors
437    ///
438    /// [`Errno::EINVAL`] si `waiters` est vide, dépasse `FUTEX_WAITV_MAX` (128),
439    /// ou si un offset est invalide ; [`Errno::EBUSY`] si la SQ ou le slab sont
440    /// pleins.
441    pub fn submit_futex_waitv(
442        &mut self,
443        waiters: Vec<FutexWaiter>,
444        flags: FutexFlags,
445    ) -> Result<SubmissionToken, Errno> {
446        // Validation amont : au moins un attendu, pas plus que le maximum kernel.
447        if waiters.is_empty() || waiters.len() > raw::FUTEX_WAITV_MAX {
448            return Err(Errno::EINVAL);
449        }
450        let futex2 = raw::FUTEX2_SIZE_U32 | flags.bits();
451        let mut raws: Vec<FutexWaitvRaw> = Vec::with_capacity(waiters.len());
452        let mut guards = Vec::with_capacity(waiters.len());
453        for waiter in &waiters {
454            // Valide chaque offset (bornes + alignement) AVANT toute soumission.
455            let uaddr = core::ptr::from_ref(waiter.region.futex_word(waiter.offset)?) as u64;
456            raws.push(FutexWaitvRaw {
457                val: waiter.expected,
458                uaddr,
459                flags: futex2,
460                reserved: 0,
461            });
462            guards.push(waiter.region.liveness_handle());
463        }
464        let raws = raws.into_boxed_slice();
465        let arr_ptr = raws.as_ptr() as u64;
466        // `len() ≤ FUTEX_WAITV_MAX = 128`, donc le `u32` ne déborde jamais.
467        let nr = u32::try_from(raws.len()).map_err(|_| Errno::EINVAL)?;
468        self.submit_op(
469            Some(OwnedOp::FutexWaitv {
470                waiters: raws,
471                guards,
472            }),
473            |sqe| {
474                sqe.opcode = raw::IORING_OP_FUTEX_WAITV;
475                sqe.fd = 0;
476                sqe.addr_or_splice_off_in = arr_ptr;
477                sqe.len = nr;
478            },
479        )
480    }
481}
482
483/// Un attendu de [`IoUring::submit_futex_waitv`] : la région partageable + le
484/// décalage du mot futex, et la valeur attendue.
485///
486/// La `region` est **possédée** (un `clone` de la `MmapRegion`) : elle *est* la
487/// garde de vivacité naturelle — tant que le `FutexWaiter` (et donc la garde
488/// garée dans le slot) vit, la région reste mappée.
489#[derive(Debug, Clone)]
490pub struct FutexWaiter {
491    /// Région partageable contenant le mot futex (gardée vivante par le slot).
492    pub region: MmapRegion,
493    /// Décalage du mot futex dans la région (borné, aligné sur 4).
494    pub offset: usize,
495    /// Valeur attendue au mot à l'armement.
496    pub expected: u64,
497}
498
499// ───────────────────────────────────────────────────────────────────────────
500// Tests
501// ───────────────────────────────────────────────────────────────────────────
502//
503// Intégration (kernel 6.12 réel) : `#[cfg_attr(miri, ignore)]`. Tests purs
504// (accesseurs, ownership des payloads possédés) sous Miri.
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509    use crate::io_uring::{Completion, IoUring, IoUringBuilder, SubmissionToken};
510    use crate::test_support::sfd;
511    use air_sys_types::fd::{AsFd, FromRawFd, OwnedFd};
512    use air_sys_types::process::Pid;
513    use core::num::NonZeroU32;
514    use std::io::Write;
515    use std::process::Command;
516
517    fn ring8() -> IoUring {
518        IoUring::new(NonZeroU32::new(16).expect("16 ≠ 0")).expect("io_uring_setup 6.12")
519    }
520
521    fn complete_one(ring: &mut IoUring, expected: SubmissionToken) -> Completion {
522        ring.submit_and_wait(1).expect("submit_and_wait");
523        let c = ring.wait_completion().expect("wait_completion");
524        assert_eq!(c.token(), expected, "user_data round-trip");
525        c
526    }
527
528    /// `epoll_create1(EPOLL_CLOEXEC)` brut (aucun wrapper epoll en couche 0) —
529    /// strictement pour fournir un epfd aux tests de `submit_epoll_ctl`.
530    fn epoll_create1_cloexec() -> OwnedFd {
531        const EPOLL_CLOEXEC: i32 = 0x0008_0000;
532        let ret: i64;
533        #[cfg(target_arch = "x86_64")]
534        // SAFETY: `epoll_create1` ne lit/écrit aucune mémoire utilisateur (un seul
535        // scalaire `flags`) ; retourne un FD frais possédé ou `-errno`.
536        unsafe {
537            core::arch::asm!(
538                "syscall",
539                in("rax") 291i64,
540                in("rdi") EPOLL_CLOEXEC,
541                lateout("rax") ret,
542                lateout("rcx") _,
543                lateout("r11") _,
544                options(nostack, preserves_flags),
545            );
546        }
547        #[cfg(target_arch = "aarch64")]
548        // SAFETY: idem x86_64 (`svc #0`, NR 20 = epoll_create1 sur aarch64).
549        unsafe {
550            core::arch::asm!(
551                "svc #0",
552                in("x8") 20i64,
553                in("x0") EPOLL_CLOEXEC,
554                lateout("x0") ret,
555                options(nostack, preserves_flags),
556            );
557        }
558        let raw = i32::try_from(ret).expect("fd epoll dans i32");
559        assert!(raw >= 0, "epoll_create1 a échoué : {ret}");
560        // SAFETY: `raw ≥ 0` est un FD epoll frais possédé.
561        unsafe { OwnedFd::from_raw_fd(raw) }
562    }
563
564    // ── nop + injection de résultat ───────────────────────────────────────
565
566    #[test]
567    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
568    fn nop_with_result_injects_res() {
569        let mut ring = ring8();
570        // Injection d'un succès (42).
571        let tok = ring.submit_nop_with_result(42).expect("nop inject");
572        assert_eq!(complete_one(&mut ring, tok).into_result().expect("ok"), 42);
573        // Injection d'une erreur (-EINVAL) sans vraie erreur kernel.
574        let tok = ring.submit_nop_with_result(-22).expect("nop inject err");
575        assert_eq!(
576            complete_one(&mut ring, tok).into_result().err(),
577            Some(Errno::EINVAL)
578        );
579    }
580
581    // ── timeout ───────────────────────────────────────────────────────────
582
583    #[test]
584    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
585    fn timeout_expires_etime() {
586        let mut ring = ring8();
587        // Expiration ⇒ `res == -ETIME`.
588        let tok = ring
589            .submit_timeout(
590                TimeoutSpec::after(Duration::from_millis(5)),
591                TimeoutFlags::empty(),
592            )
593            .expect("timeout");
594        assert_eq!(
595            complete_one(&mut ring, tok).completed().err(),
596            Some(Errno::ETIME)
597        );
598        // Avec `ETIME_SUCCESS`, le `res` du CQE reste **-ETIME** (le flag ne
599        // change que le marquage « échec » utile aux **chaînes liées** du Temps
600        // 3c — il n'altère pas le résultat hors chaîne). On exerce le flag.
601        let tok = ring
602            .submit_timeout(
603                TimeoutSpec::after(Duration::from_millis(5)),
604                TimeoutFlags::ETIME_SUCCESS,
605            )
606            .expect("timeout etime_success");
607        assert_eq!(
608            complete_one(&mut ring, tok).completed().err(),
609            Some(Errno::ETIME)
610        );
611    }
612
613    #[test]
614    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
615    fn timeout_after_completions_count() {
616        let mut ring = ring8();
617        // Timeout qui se déclenche après 1 complétion (comptage), sans durée.
618        let timeout_tok = ring
619            .submit_timeout(TimeoutSpec::after_completions(1), TimeoutFlags::empty())
620            .expect("timeout count");
621        // Un nop fournit la complétion qui déclenche le timeout.
622        let nop_tok = ring.submit_nop_with_result(7).expect("nop");
623        ring.submit_and_wait(2).expect("submit");
624        let mut seen_nop = false;
625        let mut seen_timeout = false;
626        while ring.in_flight() > 0 {
627            let c = ring.wait_completion().expect("wait");
628            if c.token() == nop_tok {
629                seen_nop = true;
630            } else if c.token() == timeout_tok {
631                seen_timeout = true;
632            }
633        }
634        assert!(seen_nop && seen_timeout);
635    }
636
637    #[test]
638    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
639    fn timeout_remove_and_update() {
640        let mut ring = ring8();
641        // Arme un timeout long, puis le supprime ⇒ il complète en -ECANCELED.
642        let t = ring
643            .submit_timeout(
644                TimeoutSpec::after(Duration::from_secs(60)),
645                TimeoutFlags::empty(),
646            )
647            .expect("timeout long");
648        let rm = ring.submit_timeout_remove(t).expect("timeout_remove");
649        ring.submit_and_wait(2).expect("submit");
650        while ring.in_flight() > 0 {
651            let _ = ring.wait_completion().expect("wait");
652        }
653        let _ = (t, rm);
654
655        // Arme un timeout long, puis le re-arme court via update ⇒ expire vite.
656        let t2 = ring
657            .submit_timeout(
658                TimeoutSpec::after(Duration::from_secs(60)),
659                TimeoutFlags::empty(),
660            )
661            .expect("timeout long 2");
662        let up = ring
663            .submit_timeout_update(
664                t2,
665                TimeoutSpec::after(Duration::from_millis(5)),
666                TimeoutFlags::empty(),
667            )
668            .expect("timeout_update");
669        ring.submit_and_wait(2).expect("submit");
670        while ring.in_flight() > 0 {
671            let _ = ring.wait_completion().expect("wait");
672        }
673        let _ = (t2, up);
674        assert_eq!(ring.in_flight(), 0);
675    }
676
677    // ── cancel async ──────────────────────────────────────────────────────
678
679    #[test]
680    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
681    fn cancel_inflight_poll_yields_ecanceled_and_restitutes_slot() {
682        let (reader, _writer) = std::io::pipe().expect("pipe");
683        let mut ring = ring8();
684        // poll sur le read d'un pipe sans données ⇒ ne fire pas.
685        let poll = ring
686            .submit_poll_add(sfd(&reader), PollEvents::IN)
687            .expect("poll_add");
688        // **Armer** le poll (soumission séparée) avant de l'annuler : sans cela,
689        // poll et cancel d'un même batch s'exécutent sans ordre garanti et le
690        // cancel ne trouverait pas encore la cible.
691        ring.submit().expect("arm poll");
692        // `CancelFlags::ALL` ⇒ `into_result` rend le **nombre** d'annulées (sans
693        // `ALL`, l'annulation réussie renvoie 0, pas le compte).
694        let cancel = ring
695            .submit_cancel(CancelTarget::Token(poll), CancelFlags::ALL)
696            .expect("cancel");
697        ring.submit_and_wait(2).expect("submit");
698        let mut cancel_count = None;
699        let mut poll_err = None;
700        while ring.in_flight() > 0 {
701            let c = ring.wait_completion().expect("wait");
702            if c.token() == cancel {
703                cancel_count = Some(c.into_result().expect("cancel ok"));
704            } else if c.token() == poll {
705                poll_err = Some(c.into_poll_result().err());
706            }
707        }
708        assert_eq!(cancel_count, Some(1), "une op annulée");
709        assert_eq!(poll_err, Some(Some(Errno::ECANCELED)), "poll en -ECANCELED");
710        assert_eq!(ring.in_flight(), 0, "slot restitué");
711    }
712
713    // ── poll ──────────────────────────────────────────────────────────────
714
715    #[test]
716    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
717    fn poll_add_fires_on_write() {
718        let (reader, mut writer) = std::io::pipe().expect("pipe");
719        let mut ring = ring8();
720        let tok = ring
721            .submit_poll_add(sfd(&reader), PollEvents::IN)
722            .expect("poll_add");
723        ring.submit().expect("submit");
724        writer.write_all(b"x").expect("write");
725        let events = complete_one(&mut ring, tok)
726            .into_poll_result()
727            .expect("poll ok");
728        assert!(events.contains(PollEvents::IN), "POLLIN signalé");
729    }
730
731    #[test]
732    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
733    fn poll_update_then_remove() {
734        let (reader, _writer) = std::io::pipe().expect("pipe");
735        let mut ring = ring8();
736        let poll = ring
737            .submit_poll_add(sfd(&reader), PollEvents::IN)
738            .expect("poll_add");
739        let up = ring
740            .submit_poll_update(poll, PollEvents::IN | PollEvents::PRI)
741            .expect("poll_update");
742        complete_one(&mut ring, up).completed().expect("update ok");
743        let rm = ring.submit_poll_remove(poll).expect("poll_remove");
744        ring.submit_and_wait(2).expect("submit");
745        while ring.in_flight() > 0 {
746            let _ = ring.wait_completion().expect("wait");
747        }
748        let _ = rm;
749        assert_eq!(ring.in_flight(), 0);
750    }
751
752    // ── epoll_ctl ─────────────────────────────────────────────────────────
753
754    #[test]
755    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
756    fn epoll_ctl_add_mod_del() {
757        let epfd = epoll_create1_cloexec();
758        let (reader, _writer) = std::io::pipe().expect("pipe");
759        let mut ring = ring8();
760
761        let tok = ring
762            .submit_epoll_ctl(
763                epfd.as_fd(),
764                EpollOp::Add,
765                sfd(&reader),
766                EpollEvent::new(PollEvents::IN, 0xA),
767            )
768            .expect("epoll add");
769        complete_one(&mut ring, tok).completed().expect("add ok");
770
771        let tok = ring
772            .submit_epoll_ctl(
773                epfd.as_fd(),
774                EpollOp::Modify,
775                sfd(&reader),
776                EpollEvent::new(PollEvents::IN | PollEvents::OUT, 0xB),
777            )
778            .expect("epoll mod");
779        complete_one(&mut ring, tok).completed().expect("mod ok");
780
781        let tok = ring
782            .submit_epoll_ctl(
783                epfd.as_fd(),
784                EpollOp::Delete,
785                sfd(&reader),
786                EpollEvent::new(PollEvents::empty(), 0),
787            )
788            .expect("epoll del");
789        complete_one(&mut ring, tok).completed().expect("del ok");
790    }
791
792    // ── waitid ────────────────────────────────────────────────────────────
793
794    #[test]
795    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
796    fn waitid_reaps_child() {
797        // Enfant réel qui se termine immédiatement (`true`).
798        let child = Command::new("true").spawn().expect("spawn true");
799        let pid_raw = i32::try_from(child.id()).expect("pid i32");
800        // io_uring `waitid` récolte le zombie ; on empêche std de le faire.
801        std::mem::forget(child);
802        let pid = Pid::try_from_raw(pid_raw).expect("pid > 0");
803
804        let mut ring = ring8();
805        let tok = ring
806            .submit_waitid(
807                WaitTarget::Pid(pid),
808                WaitOptions::EXITED,
809                Box::new(SignalInfo::zeroed()),
810            )
811            .expect("submit_waitid");
812        let info = complete_one(&mut ring, tok)
813            .into_waitid_result()
814            .expect("waitid ok");
815        let bytes = info.as_bytes();
816        let si_signo = i32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
817        let si_pid = i32::from_ne_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
818        assert_eq!(si_signo, 17, "SIGCHLD");
819        assert_eq!(si_pid, pid_raw, "PID de l'enfant récolté");
820    }
821
822    // ── message_ring (notification inter-ring) ────────────────────────────
823
824    #[test]
825    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
826    fn message_ring_data_posts_cqe_to_target() {
827        let mut sender = ring8();
828        let mut target = ring8();
829        assert_eq!(target.cq_ready(), 0);
830        let tok = sender
831            .submit_message_ring_data(&target, 99, 0xABCD, MessageRingFlags::empty())
832            .expect("msg_ring data");
833        complete_one(&mut sender, tok)
834            .completed()
835            .expect("émetteur ok");
836        // Le CQE injecté apparaît dans la CQ de la cible (filtré comme périmé par
837        // `harvest_ready` faute de slot, mais bien présent dans la CQ brute).
838        assert_eq!(target.cq_ready(), 1, "CQE posté côté cible");
839        // Le consomme (avance la head) pour quiescer la cible au Drop.
840        assert!(target.try_completion().is_none(), "CQE périmé filtré");
841        assert_eq!(target.cq_ready(), 0);
842    }
843
844    // ── Back-pressure EBUSY (simulateur structurel : slab plein) ───────────
845
846    #[test]
847    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
848    fn slab_full_returns_ebusy() {
849        let mut ring = IoUringBuilder::new(NonZeroU32::new(8).expect("8"))
850            .max_inflight(NonZeroU32::new(1).expect("1"))
851            .build()
852            .expect("ring slab=1");
853        let _t = ring.submit_nop_with_result(0).expect("1ʳᵉ op");
854        assert_eq!(ring.submit_nop_with_result(0), Err(Errno::EBUSY));
855        ring.submit_and_wait(1).expect("submit");
856        let _ = ring.wait_completion().expect("complétion");
857    }
858
859    // ── Accesseurs : bras défensifs + directions d'erreur (purs, Miri) ────
860
861    fn cqe(res: i32, payload: Option<OwnedOp>) -> Completion {
862        Completion::for_test(SubmissionToken::new(0, 0), res, 0, payload)
863    }
864
865    #[test]
866    fn poll_result_accessor_paths() {
867        // Succès : `res ≥ 0` est un bitset d'événements (IN|OUT = 0x005).
868        let ok = cqe(0x005, None).into_poll_result().expect("ok");
869        assert_eq!(ok, PollEvents::IN | PollEvents::OUT);
870        // Erreur : res < 0.
871        assert_eq!(
872            cqe(-125, None).into_poll_result().err(),
873            Some(Errno::ECANCELED)
874        );
875    }
876
877    #[test]
878    fn waitid_result_accessor_paths() {
879        // Erreur : res < 0 ⇒ Err.
880        assert_eq!(
881            cqe(-10, None).into_waitid_result().err(),
882            Some(Errno::ECHILD)
883        );
884        // Payload présent ⇒ restitution du tampon.
885        let mut info = Box::new(SignalInfo::zeroed());
886        // (les octets sont à 0 ; on vérifie juste la restitution.)
887        let _ = &mut info;
888        let restituted = cqe(0, Some(OwnedOp::Waitid(info)))
889            .into_waitid_result()
890            .expect("ok");
891        assert_eq!(restituted.as_bytes()[0], 0);
892        // Payload absent ⇒ tampon zéro-initialisé (bras défensif `_`).
893        let default = cqe(0, None).into_waitid_result().expect("ok");
894        assert_eq!(default.as_bytes(), &[0u8; 128]);
895    }
896
897    #[test]
898    fn waitid_target_maps_all_variants() {
899        // Helper pur (sans kernel) : couvre les 5 bras de `waitid_target`.
900        assert_eq!(super::waitid_target(WaitTarget::AnyChild), (0, 0));
901        let pid = Pid::try_from_raw(123).expect("pid");
902        assert_eq!(super::waitid_target(WaitTarget::Pid(pid)), (1, 123));
903        assert_eq!(
904            super::waitid_target(WaitTarget::ProcessGroup(pid)),
905            (2, 123)
906        );
907        assert_eq!(super::waitid_target(WaitTarget::AnyProcessGroup), (2, 0));
908        // SAFETY: fd 0 (stdin) emprunté brièvement pour exercer le mapping
909        // `PidFd` ; aucun syscall n'est émis (helper pur).
910        let borrowed = unsafe { BorrowedFd::borrow_raw(0) };
911        assert_eq!(super::waitid_target(WaitTarget::PidFd(borrowed)), (3, 0));
912    }
913
914    #[test]
915    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
916    fn cancel_target_variants_are_submittable() {
917        // Couvre les bras `Fd`/`Op`/`Any` de `submit_cancel` (le bras `Token` est
918        // couvert par le test d'annulation effective). Sur un ring sans cible
919        // correspondante, l'annulation complète (`ENOENT`/0) sans paniquer.
920        let mut ring = ring8();
921        // SAFETY: fd 0 (stdin) emprunté pour cibler une annulation par FD ;
922        // aucune op réelle ne le référence (résultat ENOENT attendu).
923        let stdin = unsafe { BorrowedFd::borrow_raw(0) };
924        let tok = ring
925            .submit_cancel(CancelTarget::Fd(stdin), CancelFlags::ALL)
926            .expect("cancel fd");
927        let _ = complete_one(&mut ring, tok).into_result();
928        let tok = ring
929            .submit_cancel(
930                CancelTarget::Op(crate::io_uring::IoUringOpcode::Read),
931                CancelFlags::ALL,
932            )
933            .expect("cancel op");
934        let _ = complete_one(&mut ring, tok).into_result();
935        let tok = ring
936            .submit_cancel(CancelTarget::Any, CancelFlags::ALL)
937            .expect("cancel any");
938        let _ = complete_one(&mut ring, tok).into_result();
939        assert_eq!(ring.in_flight(), 0);
940    }
941
942    // ── Ownership des payloads possédés (Miri, pur) ───────────────────────
943
944    #[test]
945    fn ownership_2c_payloads_round_trip_via_slab() {
946        use super::super::slab::{InflightSlab, SlotOutcome};
947        let mut slab = InflightSlab::with_capacity(NonZeroU32::new(4).expect("4"));
948        // Waitid : le tampon siginfo est restitué à la complétion.
949        let info = Box::new(SignalInfo::zeroed());
950        let token = slab.reserve(Some(OwnedOp::Waitid(info))).expect("réserve");
951        let restituted = matches!(
952            slab.complete(token.to_user_data(), false),
953            SlotOutcome::Final {
954                payload: Some(OwnedOp::Waitid(_))
955            }
956        );
957        assert!(restituted);
958        // Timeout / Epoll : gardes de vivacité droppées proprement (pas d'UAF).
959        let ts = slab
960            .reserve(Some(OwnedOp::Timeout(Box::new(KernelTimespec {
961                tv_sec: 1,
962                tv_nsec: 2,
963            }))))
964            .expect("réserve ts");
965        assert!(matches!(
966            slab.complete(ts.to_user_data(), false),
967            SlotOutcome::Final { .. }
968        ));
969        let ev = slab
970            .reserve(Some(OwnedOp::Epoll(Box::new(EpollEvent::new(
971                PollEvents::IN,
972                7,
973            )))))
974            .expect("réserve ev");
975        assert!(matches!(
976            slab.complete(ev.to_user_data(), false),
977            SlotOutcome::Final { .. }
978        ));
979        assert_eq!(slab.in_flight(), 0);
980    }
981
982    // ── Futex (mémoire partagée via MmapRegion) ───────────────────────────
983
984    use core::sync::atomic::Ordering;
985
986    fn rw_region(len: usize) -> MmapRegion {
987        MmapRegion::new_anonymous(
988            len,
989            air_sys_types::mem::ProtectionFlags::READ | air_sys_types::mem::ProtectionFlags::WRITE,
990            air_sys_types::mem::MapFlags::PRIVATE,
991        )
992        .expect("new_anonymous RW")
993    }
994
995    /// Les variantes de payload `MemLiveness`/`FutexWaitv` se construisent, se
996    /// `Debug`-ent et se droppent ; la garde garée ne munmap pas tant que la
997    /// région vit (Miri-safe : région fictive, aucun syscall). Hors loom :
998    /// utilise l'`Arc`/atomique `std` (le compteur de `new_for_test` est l'`Arc`
999    /// du module `mem`, loom sous `--cfg loom`).
1000    #[cfg(not(loom))]
1001    #[test]
1002    fn mem_liveness_payloads_debug_and_drop() {
1003        use std::sync::Arc;
1004        use std::sync::atomic::AtomicUsize;
1005        let counter = Arc::new(AtomicUsize::new(0));
1006        let region = MmapRegion::new_for_test(64, Arc::clone(&counter));
1007        let p1 = OwnedOp::MemLiveness(region.liveness_handle());
1008        let raws = Vec::<FutexWaitvRaw>::new().into_boxed_slice();
1009        let p2 = OwnedOp::FutexWaitv {
1010            waiters: raws,
1011            guards: vec![region.liveness_handle()],
1012        };
1013        let _ = format!("{p1:?} {p2:?}");
1014        drop(p1);
1015        drop(p2);
1016        assert_eq!(counter.load(Ordering::SeqCst), 0, "region vit encore");
1017        drop(region);
1018        assert_eq!(counter.load(Ordering::SeqCst), 1, "munmap au dernier drop");
1019    }
1020
1021    /// `futex_wake` sans waiter : complète, 0 réveillé.
1022    #[test]
1023    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1024    fn futex_wake_without_waiters_returns_zero() {
1025        let region = rw_region(4096);
1026        region
1027            .futex_word(0)
1028            .expect("word")
1029            .store(0, Ordering::SeqCst);
1030        let mut ring = ring8();
1031        let tok = ring
1032            .submit_futex_wake(&region, 0, 1, u64::from(u32::MAX), FutexFlags::PRIVATE)
1033            .expect("submit_futex_wake");
1034        let woken = complete_one(&mut ring, tok).into_result().expect("wake ok");
1035        assert_eq!(woken, 0);
1036    }
1037
1038    /// `futex_wait` armé sur une valeur ≠ courante complète immédiatement
1039    /// `-EAGAIN` (le kernel lit le mot à l'armement).
1040    #[test]
1041    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1042    fn futex_wait_value_mismatch_is_eagain() {
1043        let region = rw_region(4096);
1044        region
1045            .futex_word(0)
1046            .expect("word")
1047            .store(1, Ordering::SeqCst);
1048        let mut ring = ring8();
1049        let tok = ring
1050            .submit_futex_wait(&region, 0, 999, u64::from(u32::MAX), FutexFlags::PRIVATE)
1051            .expect("submit_futex_wait");
1052        let err = complete_one(&mut ring, tok).completed().unwrap_err();
1053        assert_eq!(err, Errno::EAGAIN);
1054    }
1055
1056    /// Rendez-vous réel : un `futex_wait` armé est **réveillé** par un
1057    /// `futex_wake` sur le même mot d'une `MmapRegion` partagée.
1058    #[test]
1059    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1060    fn futex_wait_woken_by_futex_wake() {
1061        let region = rw_region(4096);
1062        region
1063            .futex_word(0)
1064            .expect("word")
1065            .store(0, Ordering::SeqCst);
1066        let mut ring = ring8();
1067        let mask = u64::from(u32::MAX);
1068        // Arme le wait (valeur 0 == expected 0 ⇒ bloque) via un submit séparé,
1069        // pour garantir qu'il est en vol avant le wake (pas de réveil manqué).
1070        let wait_tok = ring
1071            .submit_futex_wait(&region, 0, 0, mask, FutexFlags::PRIVATE)
1072            .expect("submit_futex_wait");
1073        assert_eq!(ring.submit().expect("submit (arm)"), 1);
1074        let wake_tok = ring
1075            .submit_futex_wake(&region, 0, 1, mask, FutexFlags::PRIVATE)
1076            .expect("submit_futex_wake");
1077        ring.submit_and_wait(2).expect("submit_and_wait(2)");
1078        let (mut seen_wait, mut seen_wake) = (false, false);
1079        for _ in 0..2 {
1080            let c = ring.wait_completion().expect("wait_completion");
1081            // Exactement deux jetons possibles (wait/wake) ⇒ branche binaire,
1082            // les deux bras sont exercés (pas d'arme « impossible » morte).
1083            if c.token() == wait_tok {
1084                c.completed().expect("wait réveillé");
1085                seen_wait = true;
1086            } else {
1087                assert_eq!(c.token(), wake_tok, "jeton wake attendu");
1088                assert_eq!(c.into_result().expect("wake ok"), 1, "un waiter réveillé");
1089                seen_wake = true;
1090            }
1091        }
1092        assert!(seen_wait && seen_wake, "les deux ops complètent");
1093    }
1094
1095    /// `futex_waitv` multi-attente : réveillé par un wake sur l'un des deux mots.
1096    #[test]
1097    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1098    fn futex_waitv_woken_by_wake_on_one_word() {
1099        let region = rw_region(4096);
1100        region.futex_word(0).expect("w0").store(0, Ordering::SeqCst);
1101        region
1102            .futex_word(64)
1103            .expect("w64")
1104            .store(0, Ordering::SeqCst);
1105        let mut ring = ring8();
1106        let waiters = vec![
1107            FutexWaiter {
1108                region: region.clone(),
1109                offset: 0,
1110                expected: 0,
1111            },
1112            FutexWaiter {
1113                region: region.clone(),
1114                offset: 64,
1115                expected: 0,
1116            },
1117        ];
1118        let wait_tok = ring
1119            .submit_futex_waitv(waiters, FutexFlags::PRIVATE)
1120            .expect("submit_futex_waitv");
1121        assert_eq!(ring.submit().expect("submit (arm)"), 1);
1122        let wake_tok = ring
1123            .submit_futex_wake(&region, 64, 1, u64::from(u32::MAX), FutexFlags::PRIVATE)
1124            .expect("submit_futex_wake");
1125        ring.submit_and_wait(2).expect("submit_and_wait(2)");
1126        let mut seen = 0;
1127        for _ in 0..2 {
1128            let c = ring.wait_completion().expect("wait_completion");
1129            if c.token() == wait_tok {
1130                c.completed().expect("waitv réveillé");
1131                seen += 1;
1132            } else if c.token() == wake_tok {
1133                let _ = c.into_result();
1134                seen += 1;
1135            }
1136        }
1137        assert_eq!(seen, 2);
1138    }
1139
1140    /// Validation amont de `futex_waitv` : vide, trop d'attendus, offset invalide
1141    /// ⇒ `EINVAL` **avant** soumission.
1142    #[test]
1143    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1144    fn futex_waitv_validation_bounds() {
1145        let region = rw_region(4096);
1146        let mut ring = ring8();
1147        assert_eq!(
1148            ring.submit_futex_waitv(vec![], FutexFlags::empty())
1149                .unwrap_err(),
1150            Errno::EINVAL
1151        );
1152        let too_many: Vec<FutexWaiter> = (0..=raw::FUTEX_WAITV_MAX)
1153            .map(|_| FutexWaiter {
1154                region: region.clone(),
1155                offset: 0,
1156                expected: 0,
1157            })
1158            .collect();
1159        assert_eq!(
1160            ring.submit_futex_waitv(too_many, FutexFlags::empty())
1161                .unwrap_err(),
1162            Errno::EINVAL
1163        );
1164        let bad_offset = vec![FutexWaiter {
1165            region: region.clone(),
1166            offset: 1,
1167            expected: 0,
1168        }];
1169        assert_eq!(
1170            ring.submit_futex_waitv(bad_offset, FutexFlags::empty())
1171                .unwrap_err(),
1172            Errno::EINVAL
1173        );
1174    }
1175
1176    /// Validation amont de `futex_wait`/`wake` : offset non aligné ou hors bornes
1177    /// ⇒ `EINVAL` (propagé depuis `MmapRegion::futex_word`).
1178    #[test]
1179    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1180    fn futex_wait_wake_offset_validation() {
1181        let region = rw_region(4096);
1182        let mut ring = ring8();
1183        // Non aligné sur 4.
1184        assert_eq!(
1185            ring.submit_futex_wait(&region, 1, 0, 0, FutexFlags::PRIVATE)
1186                .unwrap_err(),
1187            Errno::EINVAL
1188        );
1189        // Hors bornes (offset + 4 > len).
1190        assert_eq!(
1191            ring.submit_futex_wake(&region, 4096, 1, 0, FutexFlags::PRIVATE)
1192                .unwrap_err(),
1193            Errno::EINVAL
1194        );
1195    }
1196
1197    proptest::proptest! {
1198        /// `cancel` d'un jeton inexistant est idempotent : toujours `-ENOENT`
1199        /// (rien à annuler), sans panique, slot restitué.
1200        #[test]
1201        #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
1202        fn cancel_unknown_token_is_enoent(slot in 100u32..1000, generation in 0u32..1000) {
1203            let mut ring = ring8();
1204            let bogus = SubmissionToken::new(slot, generation);
1205            let tok = ring
1206                .submit_cancel(CancelTarget::Token(bogus), CancelFlags::empty())
1207                .expect("cancel");
1208            let err = complete_one(&mut ring, tok).into_result().err();
1209            proptest::prop_assert_eq!(err, Some(Errno::ENOENT));
1210            proptest::prop_assert_eq!(ring.in_flight(), 0);
1211        }
1212    }
1213}