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;