Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions tokio/src/fs/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
/// # }
/// ```
///
Comment thread
martin-g marked this conversation as resolved.
/// # Errors
///
/// Returns [`std::io::ErrorKind::Unsupported`] if `io_uring` or
/// `IORING_OP_URING_CMD` is not available at runtime.
Comment thread
martin-g marked this conversation as resolved.
#[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<u16>,
) -> io::Result<u32> {
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.
Expand Down
45 changes: 45 additions & 0 deletions tokio/src/io/uring/cmd.rs
Original file line number Diff line number Diff line change
@@ -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<u32>, 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<UringCmd> {
pub(crate) fn uring_cmd16(
fd: OwnedFd,
cmd_op: u32,
cmd: [u8; 16],
buf_index: Option<u16>,
) -> 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 }) }
}
}
1 change: 1 addition & 0 deletions tokio/src/io/uring/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) mod cmd;
pub(crate) mod open;
pub(crate) mod read;
pub(crate) mod utils;
Expand Down
2 changes: 2 additions & 0 deletions tokio/src/runtime/driver/op.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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),
Expand Down
25 changes: 24 additions & 1 deletion tokio/tests/fs_uring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<NamedTempFile>, Vec<PathBuf>) = 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<NamedTempFile>, Vec<PathBuf>) {
let mut files = Vec::with_capacity(num_files);
for _ in 0..num_files {
Expand Down
Loading