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;