diff --git a/tokio/src/fs/file.rs b/tokio/src/fs/file.rs index 3579f11ba15..4e5436a0ebd 100644 --- a/tokio/src/fs/file.rs +++ b/tokio/src/fs/file.rs @@ -446,6 +446,104 @@ impl File { asyncify(move || std.metadata()).await } + /// Executes an `IORING_OP_URING_CMD` operation on this file descriptor. + /// + /// This submits a 16-byte device/file-specific command payload that is + /// handled by the kernel subsystem backing this file descriptor. + /// Since the commands and their payloads are device specific, there is no + /// central list of possible commands. See the relevant kernel subsystem + /// documentation for the device you are interacting with. + /// + /// # io_uring support + /// + /// To use this API, enable `--cfg tokio_unstable`, the `io-uring` feature, + /// `Builder::enable_io`, and `Builder::enable_io_uring` on Linux. + /// + /// # Safety + /// + /// This function is unsafe because one could send privileged commands + /// or commands with payloads "telling" the kernel to read/write random + /// memory addresses specific to the command, which could fail due to + /// use-after-free or cause data corruption. + /// + /// # Examples + /// + /// ```no_run + /// # #[tokio::main] + /// # async fn main() -> std::io::Result<()> { + /// use tokio::fs::File; + /// + /// // Suppose you have a character device that supports a specific `uring_cmd`. + /// let file = File::open("/dev/my_custom_device").await?; + /// + /// // The `cmd_op` is defined by the device driver (similar to an ioctl number). + /// const MY_DRIVER_CMD_OP: u32 = 0x1234; + /// + /// // The 16-byte payload is also defined by the device driver. + /// // Often, this is used to pass small serialized C structs or pointers. + /// #[repr(C)] + /// struct MyCmdData { + /// device_id: u32, + /// size_param: u32, + /// _pad: [u8; 8], // Pad to exactly 16 bytes + /// } + /// + /// let data = MyCmdData { + /// device_id: 42, + /// size_param: 100, + /// _pad: [0; 8], + /// }; + /// + /// // Safely transmute the 16-byte struct into a byte array + /// let cmd_payload: [u8; 16] = unsafe { std::mem::transmute(data) }; + /// + /// // SAFETY: The command and payload must be exactly what the specific + /// // device driver expects to prevent undefined behavior in the kernel. + /// let _ = unsafe { file.uring_cmd(MY_DRIVER_CMD_OP, cmd_payload, None).await }; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns [`std::io::ErrorKind::Unsupported`] if `io_uring` or + /// `IORING_OP_URING_CMD` is not available at runtime. + #[cfg(all( + tokio_unstable, + feature = "io-uring", + feature = "rt", + feature = "fs", + target_os = "linux" + ))] + pub async unsafe fn uring_cmd( + &self, + cmd_op: u32, + cmd: [u8; 16], + buf_index: Option, + ) -> io::Result { + use crate::runtime::driver::op::Op; + use std::os::fd::OwnedFd; + + self.inner.lock().await.complete_inflight().await; + + let handle = crate::runtime::Handle::current(); + let driver_handle = handle.inner.driver().io(); + + if !driver_handle + .check_and_init(io_uring::opcode::UringCmd16::CODE) + .await? + { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "io_uring uring_cmd is not supported", + )); + } + + let fd: OwnedFd = self.std.try_clone()?.into(); + let (res, _fd) = Op::uring_cmd16(fd, cmd_op, cmd, buf_index).await; + res + } + /// Creates a new `File` instance that shares the same underlying file handle /// as the existing `File` instance. Reads, writes, and seeks will affect both /// File instances simultaneously. diff --git a/tokio/src/io/uring/cmd.rs b/tokio/src/io/uring/cmd.rs new file mode 100644 index 00000000000..b4cc7eb2764 --- /dev/null +++ b/tokio/src/io/uring/cmd.rs @@ -0,0 +1,45 @@ +use crate::runtime::driver::op::{CancelData, Cancellable, Completable, CqeResult, Op}; + +use io_uring::{opcode, types}; +use std::io::{self, Error}; +use std::os::fd::{AsRawFd, OwnedFd}; + +#[derive(Debug)] +pub(crate) struct UringCmd { + fd: OwnedFd, +} + +impl Completable for UringCmd { + type Output = (io::Result, OwnedFd); + + fn complete(self, cqe: CqeResult) -> Self::Output { + (cqe.result, self.fd) + } + + fn complete_with_error(self, err: Error) -> Self::Output { + (Err(err), self.fd) + } +} + +impl Cancellable for UringCmd { + fn cancel(self) -> CancelData { + CancelData::UringCmd(self) + } +} + +impl Op { + pub(crate) fn uring_cmd16( + fd: OwnedFd, + cmd_op: u32, + cmd: [u8; 16], + buf_index: Option, + ) -> Self { + let uring_cmd = opcode::UringCmd16::new(types::Fd(fd.as_raw_fd()), cmd_op) + .cmd(cmd) + .buf_index(buf_index); + let sqe = uring_cmd.build(); + + // SAFETY: Parameters are valid for the entire duration of the operation + unsafe { Op::new(sqe, UringCmd { fd }) } + } +} diff --git a/tokio/src/io/uring/mod.rs b/tokio/src/io/uring/mod.rs index facad596f63..d10df5109ef 100644 --- a/tokio/src/io/uring/mod.rs +++ b/tokio/src/io/uring/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod cmd; pub(crate) mod open; pub(crate) mod read; pub(crate) mod utils; diff --git a/tokio/src/runtime/driver/op.rs b/tokio/src/runtime/driver/op.rs index d2b9289ceee..f23a0738f9f 100644 --- a/tokio/src/runtime/driver/op.rs +++ b/tokio/src/runtime/driver/op.rs @@ -1,3 +1,4 @@ +use crate::io::uring::cmd::UringCmd; use crate::io::uring::open::Open; use crate::io::uring::read::Read; use crate::io::uring::write::Write; @@ -16,6 +17,7 @@ use std::task::{Context, Poll, Waker}; #[allow(dead_code)] #[derive(Debug)] pub(crate) enum CancelData { + UringCmd(UringCmd), Open(Open), Write(Write), Read(Read), diff --git a/tokio/tests/fs_uring.rs b/tokio/tests/fs_uring.rs index cd0d207d278..269b5c117ce 100644 --- a/tokio/tests/fs_uring.rs +++ b/tokio/tests/fs_uring.rs @@ -15,7 +15,7 @@ use std::time::Duration; use std::{future::poll_fn, path::PathBuf}; use tempfile::NamedTempFile; use tokio::{ - fs::OpenOptions, + fs::{File, OpenOptions}, runtime::{Builder, Runtime}, }; use tokio_util::task::TaskTracker; @@ -145,6 +145,29 @@ async fn cancel_op_future() { assert!(res.is_cancelled()); } +#[tokio::test] +async fn uring_cmd_is_available_via_file() { + let (tmp_file, _path): (Vec, Vec) = create_tmp_files(1); + let file = File::open(tmp_file[0].path()).await.unwrap(); + + let cmd_op = 0; // A dummy command + let cmd = [0; 16]; // A dummy payload + + // SAFETY: The command and payload are dummies for testing availability. + // Standard file systems will safely reject unknown/dummy `cmd_op`s + // without causing kernel-level memory issues. + let result = unsafe { file.uring_cmd(cmd_op, cmd, None).await }; + + // We only care that the function doesn't panic and returns a Result. + // The specific error depends on the kernel and whether io_uring is available. + if let Err(e) = result { + // Because `tmp_file` is a standard file descriptor without an underlying + // `file_operations.uring_cmd` driver implementation, the Linux kernel + // should return EOPNOTSUPP (Operation Not Supported). + assert_eq!(e.kind(), std::io::ErrorKind::Unsupported); + } +} + fn create_tmp_files(num_files: usize) -> (Vec, Vec) { let mut files = Vec::with_capacity(num_files); for _ in 0..num_files {