Skip to main content

air_sys_syscall/io_uring/
cmd.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ération **`URING_CMD`** (opcode 46, **Temps 2d**) : passthrough par lequel
6//! io_uring transmet une **commande opaque** au gestionnaire `uring_cmd` du
7//! sous-système propriétaire du fichier visé (socket, NVMe, ublk…). C'est
8//! l'opération la plus « brute » des Temps 2x : sa sémantique dépend du
9//! sous-système cible, pas d'io_uring.
10//!
11//! Référence normative : `docs/specs/layer-0/io-uring-2d-cmd.md`.
12//!
13//! Deux niveaux d'exposition :
14//! - **Commandes socket typées sûres** (`submit_socket_inq`/`outq`,
15//!   `submit_getsockopt`/`setsockopt`) — les quatre `io_uring_socket_op` de 6.12 ;
16//! - **Générique** : [`UringCommand`] (`unsafe trait`) + [`IoUring::submit_uring_cmd`]
17//!   pour les sous-systèmes typés par leur consommateur (les structures NVMe/ublk
18//!   vivent **là où elles sont utilisées**, pas en couche 0), et la forme **brute
19//!   `unsafe`** [`IoUring::submit_uring_cmd_raw`] en dernier recours.
20//!
21//! **Disposition kernel du SQE pour `URING_CMD`** (uapi 6.12) : `cmd_op` en
22//! `off` (offset 8) ; pour `getsockopt`/`setsockopt`, `level`/`optname` en `addr`
23//! (offset 16, deux `u32`), `optlen` en `splice_fd_in` (offset 44), `optval` en
24//! `addr3` (offset 48) ; la **zone `cmd[]`** générique commence à l'offset 48
25//! (`addr3`/`__pad2` = 16 octets, étendue à 80 octets avec `SETUP_SQE128`).
26
27use super::owned::OwnedOp;
28use super::raw;
29use super::{Completion, IoUring, SubmissionToken};
30use air_sys_types::Errno;
31use air_sys_types::fd::{AsRawFd, BorrowedFd};
32use air_sys_types::io_uring::UringCmdFlags;
33use air_sys_types::net::SocketOptionLevel;
34use alloc::vec::Vec;
35
36/// Une **commande passthrough** d'un sous-système, sérialisable dans la zone
37/// `cmd[]` d'`URING_CMD`.
38///
39/// # Safety
40///
41/// L'implémenteur garantit que [`encode`](UringCommand::encode) produit une
42/// commande **valide** pour le gestionnaire `uring_cmd` du `fd` ciblé, que
43/// [`cmd_op`](UringCommand::cmd_op) est l'opération correcte du sous-système, et
44/// que tout buffer externe référencé par la commande reste valide jusqu'à la
45/// complétion. io_uring ne peut pas vérifier ces invariants (la zone `cmd[]` lui
46/// est **opaque**) — d'où le `unsafe trait`.
47pub unsafe trait UringCommand {
48    /// Type de la valeur interprétée à la complétion.
49    type Output;
50
51    /// Opération propre au sous-système (`cmd_op`, posée en `off`).
52    fn cmd_op(&self) -> u32;
53
54    /// Taille requise de la zone de commande, en octets (`≤ 16`, ou `≤ 80` si le
55    /// ring a `SETUP_SQE128`).
56    fn cmd_len(&self) -> usize;
57
58    /// Sérialise la commande dans `out` (de longueur exacte `cmd_len()`).
59    fn encode(&self, out: &mut [u8]);
60
61    /// Interprétation typée de la complétion (appelée par le consommateur, qui
62    /// conserve une instance de la commande).
63    ///
64    /// # Errors
65    ///
66    /// L'[`Errno`] décodé de la complétion, ou toute erreur d'interprétation
67    /// propre au sous-système.
68    fn interpret(&self, completion: &Completion) -> Result<Self::Output, Errno>;
69}
70
71impl IoUring {
72    /// Capacité de la zone `cmd[]` en octets : `16` (SQE standard) ou `80` si le
73    /// ring a été créé avec `SetupFlags::SQE128`.
74    fn cmd_zone_capacity(&self) -> usize {
75        self.sq.sqe_size().saturating_sub(raw::CMD_ZONE_OFFSET)
76    }
77
78    /// Cœur commun `inq`/`outq` : `URING_CMD` sans buffer, `cmd_op` en `off`.
79    /// Complétion via [`Completion::into_result`] (nombre d'octets en file).
80    fn submit_socket_queue(
81        &mut self,
82        sock: BorrowedFd<'_>,
83        cmd_op: u32,
84    ) -> Result<SubmissionToken, Errno> {
85        let fd = sock.as_raw_fd();
86        self.submit_op(None, |sqe| {
87            sqe.opcode = raw::IORING_OP_URING_CMD;
88            sqe.fd = fd;
89            sqe.off_or_addr2 = u64::from(cmd_op);
90        })
91    }
92
93    /// Soumet `SIOCINQ` (octets disponibles en **lecture**) via `URING_CMD` sur
94    /// un socket — l'équivalent `ioctl(SIOCINQ)` **dans le ring**.
95    /// `cmd_op = SOCKET_URING_OP_SIOCINQ` (0). Complétion via
96    /// [`Completion::into_result`] (nombre d'octets).
97    ///
98    /// # Errors
99    ///
100    /// [`Errno::EBUSY`] si la SQ ou le slab sont pleins ; à la complétion,
101    /// `ENOTSOCK`/`EINVAL` selon le FD.
102    pub fn submit_socket_inq(&mut self, sock: BorrowedFd<'_>) -> Result<SubmissionToken, Errno> {
103        self.submit_socket_queue(sock, raw::SOCKET_URING_OP_SIOCINQ)
104    }
105
106    /// Soumet `SIOCOUTQ` (octets **en attente d'émission**) via `URING_CMD`.
107    /// `cmd_op = SOCKET_URING_OP_SIOCOUTQ` (1). Complétion via
108    /// [`Completion::into_result`].
109    ///
110    /// # Errors
111    ///
112    /// Voir [`IoUring::submit_socket_inq`].
113    pub fn submit_socket_outq(&mut self, sock: BorrowedFd<'_>) -> Result<SubmissionToken, Errno> {
114        self.submit_socket_queue(sock, raw::SOCKET_URING_OP_SIOCOUTQ)
115    }
116
117    /// Encode `level`/`optname` dans le champ `addr` (offset 16) : `level` en bits
118    /// bas, `optname` en bits hauts (deux `u32`, ABI `getsockopt`/`setsockopt`).
119    fn level_optname(level: SocketOptionLevel, optname: i32) -> u64 {
120        u64::from(level.as_raw().cast_unsigned()) | (u64::from(optname.cast_unsigned()) << 32)
121    }
122
123    /// Soumet un **`getsockopt`** via `URING_CMD` (`cmd_op = GETSOCKOPT`, 2). Le
124    /// buffer `value` est le **buffer de sortie** (ownership transféré, S1) : le
125    /// kernel y écrit l'option. Complétion via
126    /// [`Completion::into_buffer_result`] → `(value, longueur effective)`.
127    ///
128    /// # Errors
129    ///
130    /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si `value.len()`
131    /// déborde un `u32` ; à la complétion, `ENOPROTOOPT`/`EINVAL`/`ENOTSOCK`/`EFAULT`.
132    pub fn submit_getsockopt(
133        &mut self,
134        sock: BorrowedFd<'_>,
135        level: SocketOptionLevel,
136        optname: i32,
137        mut value: Vec<u8>,
138    ) -> Result<SubmissionToken, Errno> {
139        let fd = sock.as_raw_fd();
140        let optlen = u32::try_from(value.len()).map_err(|_| Errno::EINVAL)?;
141        let optval = value.as_mut_ptr() as u64;
142        let level_optname = Self::level_optname(level, optname);
143        self.submit_op(Some(OwnedOp::Bytes(value)), |sqe| {
144            sqe.opcode = raw::IORING_OP_URING_CMD;
145            sqe.fd = fd;
146            sqe.off_or_addr2 = u64::from(raw::SOCKET_URING_OP_GETSOCKOPT);
147            sqe.addr_or_splice_off_in = level_optname;
148            sqe.splice_fd_in_or_file_index = optlen;
149            sqe.addr3 = optval;
150        })
151    }
152
153    /// Soumet un **`setsockopt`** via `URING_CMD` (`cmd_op = SETSOCKOPT`, 3). Le
154    /// buffer `value` (octets, Principe 3) est **déplacé dans le slot** (le kernel
155    /// le lit jusqu'à la complétion). Complétion via [`Completion::completed`].
156    ///
157    /// # Errors
158    ///
159    /// [`Errno::EBUSY`] (SQ/slab plein) ; [`Errno::EINVAL`] si `value.len()`
160    /// déborde un `u32` ; à la complétion, `ENOPROTOOPT`/`EINVAL`/`ENOTSOCK`/`EFAULT`.
161    pub fn submit_setsockopt(
162        &mut self,
163        sock: BorrowedFd<'_>,
164        level: SocketOptionLevel,
165        optname: i32,
166        value: Vec<u8>,
167    ) -> Result<SubmissionToken, Errno> {
168        let fd = sock.as_raw_fd();
169        let optlen = u32::try_from(value.len()).map_err(|_| Errno::EINVAL)?;
170        let optval = value.as_ptr() as u64;
171        let level_optname = Self::level_optname(level, optname);
172        self.submit_op(Some(OwnedOp::Bytes(value)), |sqe| {
173            sqe.opcode = raw::IORING_OP_URING_CMD;
174            sqe.fd = fd;
175            sqe.off_or_addr2 = u64::from(raw::SOCKET_URING_OP_SETSOCKOPT);
176            sqe.addr_or_splice_off_in = level_optname;
177            sqe.splice_fd_in_or_file_index = optlen;
178            sqe.addr3 = optval;
179        })
180    }
181
182    /// Écrit les champs nommés (`opcode`/`fd`/`cmd_op`/`op_flags`) puis sérialise
183    /// `cmd_bytes` dans la **zone `cmd[]`** (offset 48), via le pointeur brut du
184    /// slot (dont la provenance couvre tout le SQE, y compris l'extension
185    /// `SQE128`). Cœur commun du générique et du brut.
186    fn submit_cmd_zone(
187        &mut self,
188        fd: i32,
189        cmd_op: u32,
190        op_flags: u32,
191        cmd_bytes: &[u8],
192    ) -> Result<SubmissionToken, Errno> {
193        self.submit_filled(None, |sqe_ptr| {
194            {
195                // SAFETY: `sqe_ptr` pointe un slot SQE valide et exclusivement
196                // détenu ; le `&mut` ne vit que cette portée (avant l'écriture
197                // brute de `cmd[]` ci-dessous — pas d'aliasing simultané).
198                let sqe = unsafe { &mut *sqe_ptr };
199                sqe.opcode = raw::IORING_OP_URING_CMD;
200                sqe.fd = fd;
201                sqe.off_or_addr2 = u64::from(cmd_op);
202                sqe.op_flags = op_flags;
203            }
204            // SAFETY: `cmd_bytes.len() ≤ cmd_zone_capacity()` (validé par les
205            // appelants) ⇒ `[48, 48 + len)` est **dans** le slot (`sqe_size`
206            // octets). La provenance de `sqe_ptr` couvre tout le slot (cf.
207            // `SubmissionRing::prepare`). `cmd_bytes` (source) et la zone (dest)
208            // ne se chevauchent pas (buffer local vs mmap).
209            unsafe {
210                let zone = sqe_ptr.cast::<u8>().add(raw::CMD_ZONE_OFFSET);
211                core::ptr::copy_nonoverlapping(cmd_bytes.as_ptr(), zone, cmd_bytes.len());
212            }
213        })
214    }
215
216    /// Soumet une commande passthrough **typée** ([`UringCommand`]) sur `fd`. La
217    /// commande est **sérialisée** dans la zone `cmd[]` (la complétion sera
218    /// interprétée par le consommateur via [`UringCommand::interpret`]).
219    ///
220    /// Vérifie `cmd.cmd_len() ≤ 16` (ou `≤ 80` si le ring a `SQE128`) **en amont**
221    /// (Principe 4) — **pas de troncature silencieuse**.
222    ///
223    /// # Errors
224    ///
225    /// [`Errno::EINVAL`] si `cmd.cmd_len()` dépasse la capacité de la zone `cmd[]`
226    /// du ring ; [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
227    pub fn submit_uring_cmd<C: UringCommand>(
228        &mut self,
229        fd: BorrowedFd<'_>,
230        cmd: C,
231    ) -> Result<SubmissionToken, Errno> {
232        let len = cmd.cmd_len();
233        if len > self.cmd_zone_capacity() {
234            return Err(Errno::EINVAL);
235        }
236        // Zone maximale 6.12 (80 octets avec SQE128) ; sérialisation sur la pile.
237        let mut buffer = [0u8; 80];
238        // `len ≤ cmd_zone_capacity() ≤ 80` ⇒ tranche valide.
239        let scratch = buffer.get_mut(..len).ok_or(Errno::EINVAL)?;
240        cmd.encode(scratch);
241        let fd = fd.as_raw_fd();
242        let cmd_op = cmd.cmd_op();
243        self.submit_cmd_zone(fd, cmd_op, 0, &buffer[..len])
244    }
245
246    /// Soumet une commande passthrough **brute** — échappatoire de dernier recours
247    /// pour un sous-système non encore typé.
248    ///
249    /// # Safety
250    ///
251    /// L'appelant garantit que :
252    /// 1. `cmd_data` est une commande **valide** pour le gestionnaire `uring_cmd`
253    ///    du `fd` ciblé (contenu opaque pour io_uring) ;
254    /// 2. tout buffer **référencé** par `cmd_data` reste valide **jusqu'à la
255    ///    complétion** (la façade ne le maintient pas en vie) ;
256    /// 3. `cmd_data.len() ≤ 16` (ou `≤ 80` si le ring a `SetupFlags::SQE128`) —
257    ///    sinon `Err(EINVAL)` est rendu sans rien soumettre.
258    ///
259    /// # Errors
260    ///
261    /// [`Errno::EINVAL`] si `cmd_data.len()` dépasse la capacité de la zone
262    /// `cmd[]` ; [`Errno::EBUSY`] si la SQ ou le slab sont pleins.
263    pub unsafe fn submit_uring_cmd_raw(
264        &mut self,
265        fd: BorrowedFd<'_>,
266        cmd_op: u32,
267        cmd_data: &[u8],
268        flags: UringCmdFlags,
269    ) -> Result<SubmissionToken, Errno> {
270        if cmd_data.len() > self.cmd_zone_capacity() {
271            return Err(Errno::EINVAL);
272        }
273        let fd = fd.as_raw_fd();
274        let op_flags = if flags.contains(UringCmdFlags::FIXED) {
275            raw::IORING_URING_CMD_FIXED
276        } else {
277            0
278        };
279        self.submit_cmd_zone(fd, cmd_op, op_flags, cmd_data)
280    }
281}
282
283// ───────────────────────────────────────────────────────────────────────────
284// Tests
285// ───────────────────────────────────────────────────────────────────────────
286//
287// Intégration (kernel 6.12 réel : socket loopback) en `#[cfg_attr(miri, ignore)]`
288// (Miri ne modélise pas `io_uring_enter`). La sérialisation `encode` (bornée
289// dans `cmd[]`) est testée **sous Miri** (logique pure, sans ring).
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::io_uring::{IoUringBuilder, SetupFlags};
295    use crate::test_support::sfd;
296    use air_sys_types::fd::AsFd;
297    use core::num::NonZeroU32;
298    use std::io::Write;
299
300    // SOL_SOCKET / SO_* (valeurs uapi, cf. `family-net`).
301    const SO_REUSEADDR: i32 = 2;
302    const SO_RCVBUF: i32 = 8;
303
304    fn ring8() -> IoUring {
305        IoUring::new(NonZeroU32::new(16).expect("16 ≠ 0")).expect("io_uring_setup 6.12")
306    }
307
308    fn ring_sqe128() -> IoUring {
309        IoUringBuilder::new(NonZeroU32::new(16).expect("16 ≠ 0"))
310            .with_flags(SetupFlags::SQE128)
311            .build()
312            .expect("io_uring_setup SQE128 6.12")
313    }
314
315    fn tcp_pair() -> (std::net::TcpStream, std::net::TcpStream) {
316        let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind loopback");
317        let addr = listener.local_addr().expect("local_addr");
318        let client = std::net::TcpStream::connect(addr).expect("connect loopback");
319        let (server, _) = listener.accept().expect("accept loopback");
320        (client, server)
321    }
322
323    fn complete_one(ring: &mut IoUring, expected: SubmissionToken) -> Completion {
324        ring.submit_and_wait(1).expect("submit_and_wait");
325        let c = ring.wait_completion().expect("wait_completion");
326        assert_eq!(c.token(), expected, "user_data round-trip");
327        c
328    }
329
330    /// Commande passthrough de test, sérialisée comme un motif `[op, i, i, …]`.
331    #[derive(Clone, Copy)]
332    struct TestCmd {
333        op: u32,
334        len: usize,
335    }
336
337    // SAFETY (test): `encode` écrit exactement `len` octets ; cette commande
338    // n'est utilisée qu'avec un `cmd_op` **inconnu** du gestionnaire socket
339    // (rejeté proprement) ou sous Miri (pas de soumission réelle).
340    unsafe impl UringCommand for TestCmd {
341        type Output = i32;
342        fn cmd_op(&self) -> u32 {
343            self.op
344        }
345        fn cmd_len(&self) -> usize {
346            self.len
347        }
348        fn encode(&self, out: &mut [u8]) {
349            for (i, byte) in out.iter_mut().enumerate() {
350                // Motif déterministe borné (troncature volontaire sur 8 bits).
351                *byte = u8::try_from(i & 0xFF).unwrap_or(0);
352            }
353        }
354        fn interpret(&self, completion: &Completion) -> Result<i32, Errno> {
355            // `completed()` décode le signe de `res` ; on rend la valeur brute.
356            completion.completed().map(|()| completion.raw_result())
357        }
358    }
359
360    // ── Sérialisation `encode` bornée (pure ; tourne sous Miri) ───────────────
361
362    #[test]
363    fn uring_command_encode_is_bounded() {
364        // Reproduit la sérialisation de `submit_uring_cmd` sans ring (Miri-safe).
365        let cmd = TestCmd { op: 7, len: 32 };
366        let mut buffer = [0xFFu8; 80];
367        let scratch = buffer.get_mut(..cmd.cmd_len()).expect("len ≤ 80");
368        cmd.encode(scratch);
369        // Les `len` premiers octets suivent le motif ; le reste reste intact.
370        assert_eq!(buffer[0], 0);
371        assert_eq!(buffer[31], 31);
372        assert_eq!(buffer[32], 0xFF, "au-delà de cmd_len : non écrit");
373        assert_eq!(cmd.cmd_op(), 7);
374    }
375
376    // ── Validation amont SQE128 (pure pour le refus ; ring requis) ────────────
377
378    #[test]
379    #[cfg_attr(miri, ignore = "io_uring_setup non supporté par Miri")]
380    fn uring_cmd_over_16_refused_without_sqe128() {
381        let (client, _server) = tcp_pair();
382        let mut ring = ring8(); // SQE standard (16 o de cmd[])
383        let cmd = TestCmd { op: 99, len: 32 };
384        // Générique typé : > 16 o sans SQE128 ⇒ EINVAL en amont.
385        assert_eq!(
386            ring.submit_uring_cmd(sfd(&client), cmd).unwrap_err(),
387            Errno::EINVAL
388        );
389        // Brut : idem.
390        let data = [0u8; 32];
391        // SAFETY (test): cmd_op inconnu, aucun buffer référencé ; on attend EINVAL
392        // (refus en amont, rien n'est soumis).
393        let err =
394            unsafe { ring.submit_uring_cmd_raw(sfd(&client), 99, &data, UringCmdFlags::empty()) }
395                .unwrap_err();
396        assert_eq!(err, Errno::EINVAL);
397    }
398
399    #[test]
400    #[cfg_attr(miri, ignore = "io_uring_setup non supporté par Miri")]
401    fn uring_cmd_over_16_accepted_with_sqe128() {
402        let (client, _server) = tcp_pair();
403        let mut ring = ring_sqe128(); // 80 o de cmd[]
404        let cmd = TestCmd { op: 99, len: 32 }; // `Copy` : reste utilisable après submit
405        // Accepté par la façade (pas d'EINVAL amont) ⇒ jeton.
406        let tok = ring
407            .submit_uring_cmd(sfd(&client), cmd)
408            .expect("submit_uring_cmd accepté avec SQE128");
409        // La soumission/encodage SQE128 aboutit ; le gestionnaire socket rejette
410        // un cmd_op inconnu (EOPNOTSUPP/EINVAL) — c'est une **complétion**, pas
411        // un crash : preuve que le SQE 128 o est bien placé et lu.
412        let completion = complete_one(&mut ring, tok);
413        // L'interprétation typée du consommateur (trait `UringCommand`) décode la
414        // même complétion : ici un rejet (cmd_op inconnu).
415        let via_interpret = cmd.interpret(&completion);
416        let via_result = completion.into_result();
417        assert!(via_interpret.is_err(), "interpret : cmd_op inconnu rejeté");
418        assert!(via_result.is_err(), "into_result : {via_result:?}");
419    }
420
421    #[test]
422    #[cfg_attr(miri, ignore = "io_uring_setup non supporté par Miri")]
423    fn uring_cmd_len_exceeding_capacity_refused() {
424        let (client, _server) = tcp_pair();
425        let mut ring = ring_sqe128();
426        // 81 o > 80 (capacité SQE128) ⇒ EINVAL même avec SQE128.
427        let cmd = TestCmd { op: 99, len: 81 };
428        assert_eq!(
429            ring.submit_uring_cmd(sfd(&client), cmd).unwrap_err(),
430            Errno::EINVAL
431        );
432    }
433
434    // ── Intégration socket : inq / outq / getsockopt / setsockopt ─────────────
435
436    #[test]
437    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
438    fn socket_inq_reports_queued_bytes() {
439        let (mut client, server) = tcp_pair();
440        let payload = b"abcd1234";
441        client.write_all(payload).expect("write");
442        client.flush().expect("flush");
443        // Laisse le loopback livrer les octets dans la file de réception (≫ µs).
444        std::thread::sleep(std::time::Duration::from_millis(20));
445        let mut ring = ring8();
446        let tok = ring.submit_socket_inq(sfd(&server)).expect("submit inq");
447        let observed = complete_one(&mut ring, tok).into_result().expect("inq ok");
448        assert_eq!(observed, i32::try_from(payload.len()).unwrap());
449    }
450
451    #[test]
452    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
453    fn uring_cmd_raw_submits_with_and_without_fixed() {
454        let (client, _server) = tcp_pair();
455        let mut ring = ring8();
456        let data = [0u8; 8]; // ≤ 16 o : tient dans un SQE standard
457        // Sans FIXED (op_flags = 0) : soumission acceptée, cmd_op inconnu rejeté.
458        // SAFETY (test): cmd_op inconnu, aucun buffer externe référencé.
459        let tok =
460            unsafe { ring.submit_uring_cmd_raw(sfd(&client), 99, &data, UringCmdFlags::empty()) }
461                .expect("submit_uring_cmd_raw");
462        assert!(complete_one(&mut ring, tok).into_result().is_err());
463        // Avec FIXED (op_flags = IORING_URING_CMD_FIXED) : couvre la branche du
464        // drapeau ; le kernel rejette (pas de buffer enregistré) → complétion.
465        // SAFETY (test): idem ; FIXED sans buffer enregistré ⇒ erreur de complétion.
466        let tok =
467            unsafe { ring.submit_uring_cmd_raw(sfd(&client), 99, &data, UringCmdFlags::FIXED) }
468                .expect("submit_uring_cmd_raw FIXED");
469        assert!(complete_one(&mut ring, tok).into_result().is_err());
470    }
471
472    #[test]
473    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
474    fn socket_outq_completes_nonnegative() {
475        let (client, _server) = tcp_pair();
476        let mut ring = ring8();
477        let tok = ring.submit_socket_outq(sfd(&client)).expect("submit outq");
478        let bytes = complete_one(&mut ring, tok).into_result().expect("outq ok");
479        assert!(bytes >= 0, "octets en file d'émission ≥ 0");
480    }
481
482    #[test]
483    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
484    fn getsockopt_so_rcvbuf_matches_sync() {
485        let (_client, server) = tcp_pair();
486        let mut ring = ring8();
487        let tok = ring
488            .submit_getsockopt(
489                sfd(&server),
490                SocketOptionLevel::Socket,
491                SO_RCVBUF,
492                vec![0u8; 4],
493            )
494            .expect("submit getsockopt");
495        let (value, len) = complete_one(&mut ring, tok)
496            .into_buffer_result()
497            .expect("getsockopt ok");
498        assert_eq!(len, 4, "SO_RCVBUF rend un i32");
499        let via_uring = i32::from_ne_bytes(value[..4].try_into().expect("4 octets"));
500        // Même option lue par le getter synchrone de `family-net` : valeurs égales.
501        let via_sync = crate::net::get_so_rcvbuf(sfd(&server)).expect("get sync");
502        assert_eq!(
503            u32::try_from(via_uring).unwrap_or(0),
504            via_sync,
505            "getsockopt cohérent"
506        );
507        assert!(via_uring > 0);
508    }
509
510    #[test]
511    #[cfg_attr(miri, ignore = "syscall io_uring non supporté par Miri")]
512    fn setsockopt_so_reuseaddr_round_trip() {
513        // Socket frais non lié (REUSEADDR se règle avant bind).
514        let sock = crate::net::socket(
515            air_sys_types::net::SocketDomain::Ipv4,
516            air_sys_types::net::SocketType::Stream,
517            0,
518        )
519        .expect("socket");
520        let mut ring = ring8();
521        let tok = ring
522            .submit_setsockopt(
523                sock.as_fd(),
524                SocketOptionLevel::Socket,
525                SO_REUSEADDR,
526                1_i32.to_ne_bytes().to_vec(),
527            )
528            .expect("submit setsockopt");
529        complete_one(&mut ring, tok)
530            .completed()
531            .expect("setsockopt ok");
532        // Vérification croisée via le getter synchrone de `family-net`.
533        assert!(
534            crate::net::get_so_reuseaddr(sock.as_fd()).expect("get_so_reuseaddr"),
535            "REUSEADDR activé via io_uring"
536        );
537    }
538}