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}