#![expect(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_ptr_alignment,
reason = "Linux syscall/mmap ABI: register-width arg casts + page-aligned ring field reads"
)]
use alloc::boxed::Box;
use alloc::ffi::CString;
#[cfg(not(feature = "std"))]
use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::sync::atomic::{Ordering, fence};
use spin::Mutex;
use syscalls::{Errno, Sysno, syscall1, syscall2, syscall3, syscall4, syscall5, syscall6};
use crate::fs::{BlockRead, Fs, FsDirEntry, FsFile, FsMetadata, FsOpenOptions};
use crate::io::{Error, ErrorKind, SeekFrom};
use crate::path::Path;
const IORING_OFF_SQ_RING: u64 = 0;
const IORING_OFF_CQ_RING: u64 = 0x0800_0000;
const IORING_OFF_SQES: u64 = 0x1000_0000;
const IORING_ENTER_GETEVENTS: u32 = 1;
const IORING_FEAT_SINGLE_MMAP: u32 = 1;
const IORING_OP_NOP: u8 = 0;
const IORING_OP_FSYNC: u8 = 3;
const IORING_OP_READ: u8 = 22;
const IORING_OP_WRITE: u8 = 23;
const IORING_FSYNC_DATASYNC: u32 = 1;
const AT_FDCWD: i32 = -100;
pub const O_RDONLY: i32 = 0;
pub const O_WRONLY: i32 = 1;
pub const O_RDWR: i32 = 2;
#[cfg(not(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6",
target_arch = "sparc",
target_arch = "sparc64"
)))]
pub const O_CREAT: i32 = 0o100;
#[cfg(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6"
))]
pub const O_CREAT: i32 = 0x100;
#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
pub const O_CREAT: i32 = 0x200;
#[cfg(not(any(target_arch = "sparc", target_arch = "sparc64")))]
pub const O_TRUNC: i32 = 0o1000;
#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
pub const O_TRUNC: i32 = 0x400;
const PROT_READ: usize = 0x1;
const PROT_WRITE: usize = 0x2;
const MAP_SHARED: usize = 0x01;
const MAP_POPULATE: usize = 0x0_8000;
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct SqRingOffsets {
head: u32,
tail: u32,
ring_mask: u32,
ring_entries: u32,
flags: u32,
dropped: u32,
array: u32,
resv1: u32,
resv2: u64,
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct CqRingOffsets {
head: u32,
tail: u32,
ring_mask: u32,
ring_entries: u32,
overflow: u32,
cqes: u32,
flags: u32,
resv1: u32,
resv2: u64,
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct IoUringParams {
sq_entries: u32,
cq_entries: u32,
flags: u32,
sq_thread_cpu: u32,
sq_thread_idle: u32,
features: u32,
wq_fd: u32,
resv: [u32; 3],
sq_off: SqRingOffsets,
cq_off: CqRingOffsets,
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct IoUringSqe {
opcode: u8,
flags: u8,
ioprio: u16,
fd: i32,
off: u64,
addr: u64,
len: u32,
op_flags: u32,
user_data: u64,
buf_index: u16,
personality: u16,
splice_fd_in: i32,
pad2: [u64; 2],
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct IoUringCqe {
user_data: u64,
res: i32,
flags: u32,
}
mod errno {
pub(super) const EPERM: i32 = 1;
pub(super) const ENOENT: i32 = 2;
pub(super) const EINTR: i32 = 4;
#[cfg(target_pointer_width = "64")]
pub(super) const ENXIO: i32 = 6;
pub(super) const EBADF: i32 = 9;
pub(super) const EAGAIN: i32 = 11;
pub(super) const EBUSY: i32 = 16;
pub(super) const ENOMEM: i32 = 12;
pub(super) const EACCES: i32 = 13;
pub(super) const EEXIST: i32 = 17;
pub(super) const EXDEV: i32 = 18;
pub(super) const EINVAL: i32 = 22;
pub(super) const ENFILE: i32 = 23;
pub(super) const EMFILE: i32 = 24;
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
pub(super) const ENOTTY: i32 = 25;
pub(super) const ENOSPC: i32 = 28;
pub(super) const EROFS: i32 = 30;
pub(super) const EPIPE: i32 = 32;
pub(super) use arch::*;
#[cfg(not(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6",
target_arch = "sparc",
target_arch = "sparc64"
)))]
mod arch {
pub(in super::super) const EOPNOTSUPP: i32 = 95;
pub(in super::super) const ENETDOWN: i32 = 100;
pub(in super::super) const ENETUNREACH: i32 = 101;
pub(in super::super) const ECONNABORTED: i32 = 103;
pub(in super::super) const ECONNRESET: i32 = 104;
pub(in super::super) const ENOTCONN: i32 = 107;
pub(in super::super) const ETIMEDOUT: i32 = 110;
pub(in super::super) const ECONNREFUSED: i32 = 111;
pub(in super::super) const EHOSTUNREACH: i32 = 113;
pub(in super::super) const ESTALE: i32 = 116;
pub(in super::super) const EDQUOT: i32 = 122;
}
#[cfg(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6"
))]
mod arch {
pub(in super::super) const EOPNOTSUPP: i32 = 122;
pub(in super::super) const ENETDOWN: i32 = 127;
pub(in super::super) const ENETUNREACH: i32 = 128;
pub(in super::super) const ECONNABORTED: i32 = 130;
pub(in super::super) const ECONNRESET: i32 = 131;
pub(in super::super) const ENOTCONN: i32 = 134;
pub(in super::super) const ETIMEDOUT: i32 = 145;
pub(in super::super) const ECONNREFUSED: i32 = 146;
pub(in super::super) const EHOSTUNREACH: i32 = 148;
pub(in super::super) const ESTALE: i32 = 151;
pub(in super::super) const EDQUOT: i32 = 1133;
}
#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
mod arch {
pub(in super::super) const EOPNOTSUPP: i32 = 45;
pub(in super::super) const ENETDOWN: i32 = 50;
pub(in super::super) const ENETUNREACH: i32 = 51;
pub(in super::super) const ECONNABORTED: i32 = 53;
pub(in super::super) const ECONNRESET: i32 = 54;
pub(in super::super) const ENOTCONN: i32 = 57;
pub(in super::super) const ETIMEDOUT: i32 = 60;
pub(in super::super) const ECONNREFUSED: i32 = 61;
pub(in super::super) const EHOSTUNREACH: i32 = 65;
pub(in super::super) const ESTALE: i32 = 70;
pub(in super::super) const EDQUOT: i32 = 69;
}
}
fn errno_to_kind(code: i32) -> ErrorKind {
match code {
errno::EPERM | errno::EACCES => ErrorKind::PermissionDenied,
errno::ENOENT => ErrorKind::NotFound,
errno::EINTR => ErrorKind::Interrupted,
errno::EBADF | errno::EINVAL => ErrorKind::InvalidInput,
errno::EAGAIN => ErrorKind::WouldBlock,
errno::ENOMEM => ErrorKind::OutOfMemory,
errno::EEXIST => ErrorKind::AlreadyExists,
errno::EXDEV => ErrorKind::CrossesDevices,
errno::ENFILE | errno::EMFILE => ErrorKind::TooManyOpenFiles,
errno::ENOSPC => ErrorKind::StorageFull,
errno::EROFS => ErrorKind::ReadOnlyFilesystem,
errno::EPIPE => ErrorKind::BrokenPipe,
errno::EOPNOTSUPP => ErrorKind::Unsupported,
errno::ENETDOWN => ErrorKind::NetworkDown,
errno::ENETUNREACH => ErrorKind::NetworkUnreachable,
errno::ECONNABORTED => ErrorKind::ConnectionAborted,
errno::ECONNRESET => ErrorKind::ConnectionReset,
errno::ENOTCONN => ErrorKind::NotConnected,
errno::ETIMEDOUT => ErrorKind::TimedOut,
errno::ECONNREFUSED => ErrorKind::ConnectionRefused,
errno::EHOSTUNREACH => ErrorKind::HostUnreachable,
errno::ESTALE => ErrorKind::StaleNetworkFileHandle,
errno::EDQUOT => ErrorKind::QuotaExceeded,
_ => ErrorKind::Other,
}
}
fn err(syscall: &str, e: Errno) -> Error {
let raw = e.into_raw();
Error::new(errno_to_kind(raw), format!("{syscall} failed: errno {raw}"))
}
unsafe fn io_uring_setup(entries: u32, params: *mut IoUringParams) -> Result<i32, Error> {
let fd = unsafe { syscall2(Sysno::io_uring_setup, entries as usize, params as usize) }
.map_err(|e| err("io_uring_setup", e))?;
Ok(fd as i32)
}
unsafe fn io_uring_enter_raw(
fd: i32,
to_submit: u32,
min_complete: u32,
flags: u32,
) -> Result<u32, Errno> {
let n = unsafe {
syscall6(
Sysno::io_uring_enter,
fd as usize,
to_submit as usize,
min_complete as usize,
flags as usize,
0,
0,
)
}?;
Ok(n as u32)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn ring_lost(error: Errno) -> ! {
log::error!("io_uring_enter failed with reads in flight: {error}; aborting");
#[cfg(feature = "std")]
std::process::abort();
#[cfg(not(feature = "std"))]
loop {
let _ = unsafe { syscall1(Sysno::exit_group, 134) };
}
}
#[derive(Clone, Copy)]
struct RawRead {
fd: i32,
buf: *mut u8,
len: u32,
offset: u64,
}
#[cfg_attr(coverage_nightly, coverage(off))]
unsafe fn mmap(
len: usize,
prot: usize,
flags: usize,
fd: i32,
offset: u64,
) -> Result<*mut u8, Error> {
#[cfg(target_pointer_width = "64")]
let (nr, off) = (Sysno::mmap, offset as usize);
#[cfg(target_pointer_width = "32")]
let (nr, off) = {
debug_assert!(
offset.is_multiple_of(4096),
"mmap2 offset is in 4096-byte units"
);
(Sysno::mmap2, (offset / 4096) as usize)
};
let ret = unsafe { syscall6(nr, 0, len, prot, flags, fd as usize, off) };
match ret {
Ok(addr) => Ok(addr as *mut u8),
Err(e) => Err(err("mmap", e)),
}
}
unsafe fn munmap(addr: *mut u8, len: usize) {
let _ = unsafe { syscall2(Sysno::munmap, addr as usize, len) };
}
unsafe fn close(fd: i32) {
let _ = unsafe { syscall1(Sysno::close, fd as usize) };
}
pub fn open_raw(path: &core::ffi::CStr, flags: i32, mode: u32) -> Result<i32, Error> {
let fd = unsafe {
syscall4(
Sysno::openat,
AT_FDCWD as usize,
path.as_ptr() as usize,
(flags | O_LARGEFILE) as usize,
mode as usize,
)
}
.map_err(|e| err("openat", e))?;
Ok(fd as i32)
}
pub fn close_raw(fd: i32) -> Result<(), Error> {
unsafe { syscall1(Sysno::close, fd as usize) }.map_err(|e| err("close", e))?;
Ok(())
}
const SEEK_END: i32 = 2;
const LOCK_EX: i32 = 2;
const LOCK_NB: i32 = 4;
const AT_EMPTY_PATH: i32 = 0x1000;
const STATX_BASIC_STATS: u32 = 0x0000_07ff;
const STATX_INO: u32 = 0x0000_0100;
const S_IFMT: u16 = 0o170_000;
const S_IFDIR: u16 = 0o040_000;
const S_IFREG: u16 = 0o100_000;
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct StatxTimestamp {
tv_sec: i64,
tv_nsec: u32,
_reserved: i32,
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct Statx {
stx_mask: u32,
stx_blksize: u32,
stx_attributes: u64,
stx_nlink: u32,
stx_uid: u32,
stx_gid: u32,
stx_mode: u16,
_spare0: u16,
stx_ino: u64,
stx_size: u64,
stx_blocks: u64,
stx_attributes_mask: u64,
stx_atime: StatxTimestamp,
stx_btime: StatxTimestamp,
stx_ctime: StatxTimestamp,
stx_mtime: StatxTimestamp,
stx_rdev_major: u32,
stx_rdev_minor: u32,
stx_dev_major: u32,
stx_dev_minor: u32,
stx_mnt_id: u64,
stx_dio_mem_align: u32,
stx_dio_offset_align: u32,
_spare3: [u64; 12],
}
pub(crate) struct RawMetadata {
pub(crate) size: u64,
pub(crate) nlink: u64,
pub(crate) is_dir: bool,
pub(crate) is_file: bool,
}
pub(crate) fn fstat_raw(fd: i32) -> Result<RawMetadata, Error> {
let mut buf = Statx::default();
let empty: &core::ffi::CStr = c"";
unsafe {
syscall5(
Sysno::statx,
fd as usize,
empty.as_ptr() as usize,
AT_EMPTY_PATH as usize,
STATX_BASIC_STATS as usize,
&raw mut buf as usize,
)
}
.map_err(|e| err("statx", e))?;
let kind = buf.stx_mode & S_IFMT;
Ok(RawMetadata {
size: buf.stx_size,
nlink: u64::from(buf.stx_nlink),
is_dir: kind == S_IFDIR,
is_file: kind == S_IFREG,
})
}
pub(crate) fn ftruncate_raw(fd: i32, length: u64) -> Result<(), Error> {
#[cfg(target_pointer_width = "64")]
let ret = unsafe { syscall2(Sysno::ftruncate, fd as usize, length as usize) };
#[cfg(target_pointer_width = "32")]
let ret = {
let (a, b) = reg_pair(length);
unsafe {
if ALIGNED_REGISTER_PAIRS {
syscall4(Sysno::ftruncate64, fd as usize, 0, a, b)
} else {
syscall3(Sysno::ftruncate64, fd as usize, a, b)
}
}
};
ret.map_err(|e| err("ftruncate", e))?;
Ok(())
}
#[cfg(target_pointer_width = "32")]
const ALIGNED_REGISTER_PAIRS: bool = cfg!(any(
target_arch = "arm",
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "powerpc"
));
#[cfg(target_pointer_width = "32")]
fn reg_pair(v: u64) -> (usize, usize) {
let (lo, hi) = (v as u32 as usize, (v >> 32) as usize);
if cfg!(target_endian = "little") {
(lo, hi)
} else {
(hi, lo)
}
}
#[cfg(target_pointer_width = "64")]
pub(crate) fn start_writeback_raw(fd: i32, offset: u64, len: u64) -> Result<(), Error> {
const SYNC_FILE_RANGE_WRITE: usize = 2;
unsafe {
syscall4(
Sysno::sync_file_range,
fd as usize,
offset as usize,
len as usize,
SYNC_FILE_RANGE_WRITE,
)
}
.map_err(|e| err("sync_file_range", e))?;
Ok(())
}
pub(crate) fn set_write_lifetime_raw(fd: i32, lifetime: super::WriteLifetime) -> Result<(), Error> {
const F_SET_RW_HINT: usize = 1024 + 12;
let hint: u64 = match lifetime {
super::WriteLifetime::Short => 2,
super::WriteLifetime::Medium => 3,
super::WriteLifetime::Long => 4,
super::WriteLifetime::Extreme => 5,
};
unsafe {
syscall3(
Sysno::fcntl,
fd as usize,
F_SET_RW_HINT,
(&raw const hint) as usize,
)
}
.map_err(|e| err("fcntl(F_SET_RW_HINT)", e))?;
Ok(())
}
pub(crate) fn clear_nonblock_raw(fd: i32) -> Result<(), Error> {
const F_GETFL: usize = 3;
const F_SETFL: usize = 4;
let flags = unsafe { syscall3(Sysno::fcntl, fd as usize, F_GETFL, 0) }
.map_err(|e| err("fcntl(F_GETFL)", e))?;
let cleared = flags & !(O_NONBLOCK.unsigned_abs() as usize);
unsafe { syscall3(Sysno::fcntl, fd as usize, F_SETFL, cleared) }
.map_err(|e| err("fcntl(F_SETFL)", e))?;
Ok(())
}
pub(crate) fn lseek_raw(fd: i32, offset: i64, whence: i32) -> Result<u64, Error> {
lseek_errno(fd, offset, whence).map_err(|e| err("lseek", e))
}
#[cfg(target_pointer_width = "64")]
fn lseek_errno(fd: i32, offset: i64, whence: i32) -> Result<u64, Errno> {
let pos = unsafe { syscall3(Sysno::lseek, fd as usize, offset as usize, whence as usize) }?;
Ok(pos as u64)
}
#[cfg(target_pointer_width = "32")]
fn lseek_errno(fd: i32, offset: i64, whence: i32) -> Result<u64, Errno> {
#[cfg(target_arch = "riscv32")]
let nr = Sysno::llseek;
#[cfg(not(target_arch = "riscv32"))]
let nr = Sysno::_llseek;
let mut pos: u64 = 0;
unsafe {
syscall5(
nr,
fd as usize,
((offset as u64) >> 32) as usize,
offset as u32 as usize,
(&raw mut pos) as usize,
whence as usize,
)
}?;
Ok(pos)
}
pub(crate) fn flock_exclusive_raw(fd: i32, non_blocking: bool) -> Result<bool, Error> {
let op = if non_blocking {
LOCK_EX | LOCK_NB
} else {
LOCK_EX
};
loop {
match unsafe { syscall2(Sysno::flock, fd as usize, op as usize) } {
Ok(_) => return Ok(true),
Err(e) if e.into_raw() == 4 => {}
Err(e) if non_blocking && e.into_raw() == 11 => return Ok(false),
Err(e) => return Err(err("flock", e)),
}
}
}
pub(crate) fn file_size_raw(fd: i32) -> Result<u64, Error> {
lseek_raw(fd, 0, SEEK_END)
}
const O_APPEND: i32 = 0o2000;
#[cfg(not(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6",
target_arch = "sparc",
target_arch = "sparc64"
)))]
const O_NONBLOCK: i32 = 0o4000;
#[cfg(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6"
))]
const O_NONBLOCK: i32 = 0x80;
#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
const O_NONBLOCK: i32 = 0x4000;
const O_EXCL: i32 = 0o200;
#[cfg(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "powerpc",
target_arch = "powerpc64",
target_arch = "m68k"
))]
const O_DIRECTORY: i32 = 0o40_000;
#[cfg(not(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "powerpc",
target_arch = "powerpc64",
target_arch = "m68k"
)))]
const O_DIRECTORY: i32 = 0o200_000;
#[cfg(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "powerpc",
target_arch = "powerpc64",
target_arch = "m68k"
))]
const O_NOFOLLOW: i32 = 0o100_000;
#[cfg(not(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "powerpc",
target_arch = "powerpc64",
target_arch = "m68k"
)))]
const O_NOFOLLOW: i32 = 0o400_000;
#[cfg(all(
target_pointer_width = "32",
not(any(
target_arch = "arm",
target_arch = "powerpc",
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "sparc"
))
))]
const O_LARGEFILE: i32 = 0o100_000;
#[cfg(all(target_pointer_width = "32", target_arch = "arm"))]
const O_LARGEFILE: i32 = 0o400_000;
#[cfg(all(target_pointer_width = "32", target_arch = "powerpc"))]
const O_LARGEFILE: i32 = 0o200_000;
#[cfg(all(
target_pointer_width = "32",
any(target_arch = "mips", target_arch = "mips32r6")
))]
const O_LARGEFILE: i32 = 0x2000;
#[cfg(all(target_pointer_width = "32", target_arch = "sparc"))]
const O_LARGEFILE: i32 = 0x4_0000;
#[cfg(target_pointer_width = "64")]
const O_LARGEFILE: i32 = 0;
#[cfg(all(
target_pointer_width = "32",
any(target_arch = "x86", target_arch = "arm")
))]
const _: () = assert!(O_LARGEFILE == libc::O_LARGEFILE);
#[cfg(not(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6",
target_arch = "sparc",
target_arch = "sparc64"
)))]
const _: () = {
assert!(O_CREAT == libc::O_CREAT);
assert!(O_TRUNC == libc::O_TRUNC);
assert!(O_APPEND == libc::O_APPEND);
assert!(O_EXCL == libc::O_EXCL);
assert!(O_DIRECTORY == libc::O_DIRECTORY);
assert!(O_NOFOLLOW == libc::O_NOFOLLOW);
assert!(O_NONBLOCK == libc::O_NONBLOCK);
};
const AT_REMOVEDIR: i32 = 0x200;
const DT_DIR: u8 = 4;
pub(crate) fn mkdirat_raw(path: &core::ffi::CStr, mode: u32) -> Result<(), Error> {
unsafe {
syscall3(
Sysno::mkdirat,
AT_FDCWD as usize,
path.as_ptr() as usize,
mode as usize,
)
}
.map_err(|e| err("mkdirat", e))?;
Ok(())
}
pub(crate) fn unlinkat_raw(path: &core::ffi::CStr, remove_dir: bool) -> Result<(), Error> {
let flags = if remove_dir { AT_REMOVEDIR } else { 0 };
unsafe {
syscall3(
Sysno::unlinkat,
AT_FDCWD as usize,
path.as_ptr() as usize,
flags as usize,
)
}
.map_err(|e| err("unlinkat", e))?;
Ok(())
}
pub(crate) fn symlinkat_raw(target: &core::ffi::CStr, link: &core::ffi::CStr) -> Result<(), Error> {
unsafe {
syscall3(
Sysno::symlinkat,
target.as_ptr() as usize,
AT_FDCWD as usize,
link.as_ptr() as usize,
)
}
.map_err(|e| err("symlinkat", e))?;
Ok(())
}
pub(crate) fn renameat2_raw(from: &core::ffi::CStr, to: &core::ffi::CStr) -> Result<(), Error> {
unsafe {
syscall5(
Sysno::renameat2,
AT_FDCWD as usize,
from.as_ptr() as usize,
AT_FDCWD as usize,
to.as_ptr() as usize,
0,
)
}
.map_err(|e| err("renameat2", e))?;
Ok(())
}
pub(crate) fn statx_path_raw(path: &core::ffi::CStr) -> Result<Option<RawMetadata>, Error> {
Ok(statx_at(path)?.map(|buf| {
let kind = buf.stx_mode & S_IFMT;
RawMetadata {
size: buf.stx_size,
nlink: u64::from(buf.stx_nlink),
is_dir: kind == S_IFDIR,
is_file: kind == S_IFREG,
}
}))
}
fn statx_at(path: &core::ffi::CStr) -> Result<Option<Statx>, Error> {
let mut buf = Statx::default();
let r = unsafe {
syscall5(
Sysno::statx,
AT_FDCWD as usize,
path.as_ptr() as usize,
0,
STATX_BASIC_STATS as usize,
&raw mut buf as usize,
)
};
match r {
Ok(_) => Ok(Some(buf)),
Err(e) if e.into_raw() == errno::ENOENT => Ok(None),
Err(e) => Err(err("statx", e)),
}
}
pub(crate) fn statx_identity_raw(path: &core::ffi::CStr) -> Result<(u32, u32, u64), Error> {
let mut buf = Statx::default();
unsafe {
syscall5(
Sysno::statx,
AT_FDCWD as usize,
path.as_ptr() as usize,
0,
STATX_BASIC_STATS as usize,
&raw mut buf as usize,
)
}
.map_err(|e| err("statx", e))?;
if buf.stx_mask & STATX_INO == 0 {
return Err(Error::new(
ErrorKind::Unsupported,
"statx: the filesystem reports no inode",
));
}
Ok((buf.stx_dev_major, buf.stx_dev_minor, buf.stx_ino))
}
const PATH_MAX: usize = 4096;
pub(crate) fn readlinkat_raw(path: &core::ffi::CStr) -> Result<Option<Vec<u8>>, Error> {
let mut buf = alloc::vec![0u8; PATH_MAX];
let r = unsafe {
syscall4(
Sysno::readlinkat,
AT_FDCWD as usize,
path.as_ptr() as usize,
buf.as_mut_ptr() as usize,
buf.len(),
)
};
match r {
Ok(n) if n >= buf.len() => Err(Error::new(
ErrorKind::InvalidInput,
"readlinkat: symlink target longer than PATH_MAX",
)),
Ok(n) => {
buf.truncate(n);
Ok(Some(buf))
}
Err(e) if matches!(e.into_raw(), 2 | 22) => Ok(None),
Err(e) => Err(err("readlinkat", e)),
}
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
#[repr(C)]
#[derive(Default, Clone, Copy)]
#[allow(
clippy::struct_field_names,
reason = "field names mirror the kernel `struct statfs` (f_type, f_bsize, …) verbatim"
)]
struct Statfs {
f_type: i64,
f_bsize: i64,
f_blocks: u64,
f_bfree: u64,
f_bavail: u64,
f_files: u64,
f_ffree: u64,
f_fsid: [i32; 2],
f_namelen: i64,
f_frsize: i64,
f_flags: i64,
f_spare: [i64; 4],
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
pub(crate) fn statfs_available_raw(path: &core::ffi::CStr) -> Result<u64, Error> {
let mut buf = Statfs::default();
unsafe { syscall2(Sysno::statfs, path.as_ptr() as usize, &raw mut buf as usize) }
.map_err(|e| err("statfs", e))?;
let frsize = buf.f_frsize as u64;
Ok(buf.f_bavail.saturating_mul(frsize))
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
fn statfs_type_raw(path: &core::ffi::CStr) -> Result<i64, Error> {
let mut buf = Statfs::default();
unsafe { syscall2(Sysno::statfs, path.as_ptr() as usize, &raw mut buf as usize) }
.map_err(|e| err("statfs", e))?;
Ok(buf.f_type)
}
const STATX_BLOCKS: u32 = 0x0000_0400;
#[cfg(target_pointer_width = "64")]
const FALLOC_FL_KEEP_SIZE: i32 = 0x01;
#[cfg(target_pointer_width = "64")]
const FALLOC_FL_PUNCH_HOLE: i32 = 0x02;
#[cfg(target_pointer_width = "64")]
const SEEK_DATA: i32 = 3;
#[cfg(target_pointer_width = "64")]
const SEEK_HOLE: i32 = 4;
const LINKAT_NO_FOLLOW: usize = 0;
#[cfg(target_pointer_width = "64")]
const POSIX_FADV_NORMAL: usize = 0;
#[cfg(target_pointer_width = "64")]
const POSIX_FADV_RANDOM: usize = 1;
#[cfg(target_pointer_width = "64")]
const POSIX_FADV_SEQUENTIAL: usize = 2;
#[cfg(all(target_pointer_width = "64", not(target_arch = "s390x")))]
const POSIX_FADV_DONTNEED: usize = 4;
#[cfg(all(target_pointer_width = "64", target_arch = "s390x"))]
const POSIX_FADV_DONTNEED: usize = 6;
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
const fn ioctl_request(direction: u32, kind: u8, number: u8, size: usize) -> u32 {
(direction << 30) | ((size as u32) << 16) | ((kind as u32) << 8) | number as u32
}
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
const FICLONE: u32 = ioctl_request(1, 0x94, 9, core::mem::size_of::<i32>());
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
const FS_IOC_GETFLAGS: u32 = ioctl_request(2, b'f', 1, core::mem::size_of::<core::ffi::c_long>());
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
const FS_IOC_SETFLAGS: u32 = ioctl_request(1, b'f', 2, core::mem::size_of::<core::ffi::c_long>());
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
const FS_NOCOW_FL: i32 = 0x0080_0000;
#[cfg(target_pointer_width = "64")]
const _: () = {
assert!(FALLOC_FL_KEEP_SIZE == libc::FALLOC_FL_KEEP_SIZE);
assert!(FALLOC_FL_PUNCH_HOLE == libc::FALLOC_FL_PUNCH_HOLE);
assert!(SEEK_DATA == libc::SEEK_DATA);
assert!(SEEK_HOLE == libc::SEEK_HOLE);
assert!(POSIX_FADV_NORMAL == libc::POSIX_FADV_NORMAL as usize);
assert!(POSIX_FADV_RANDOM == libc::POSIX_FADV_RANDOM as usize);
assert!(POSIX_FADV_SEQUENTIAL == libc::POSIX_FADV_SEQUENTIAL as usize);
assert!(POSIX_FADV_DONTNEED == libc::POSIX_FADV_DONTNEED as usize);
};
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
#[cfg_attr(
target_pointer_width = "32",
expect(
clippy::unnecessary_cast,
reason = "libc::Ioctl is u32 on a 32-bit target and u64 on a 64-bit one"
)
)]
const _: () = {
assert!(FICLONE == libc::FICLONE as u32);
assert!(FS_IOC_GETFLAGS == libc::FS_IOC_GETFLAGS as u32);
assert!(FS_IOC_SETFLAGS == libc::FS_IOC_SETFLAGS as u32);
};
#[cfg(any(
target_pointer_width = "64",
target_arch = "x86",
target_arch = "arm",
target_arch = "riscv32"
))]
struct ProbeFd(i32);
#[cfg(any(
target_pointer_width = "64",
target_arch = "x86",
target_arch = "arm",
target_arch = "riscv32"
))]
impl ProbeFd {
fn open(path: &core::ffi::CStr, flags: i32) -> Result<Self, Error> {
open_raw(path, flags, 0).map(Self)
}
}
#[cfg(any(
target_pointer_width = "64",
target_arch = "x86",
target_arch = "arm",
target_arch = "riscv32"
))]
impl Drop for ProbeFd {
fn drop(&mut self) {
let _ = close_raw(self.0);
}
}
fn makedev(major: u32, minor: u32) -> u64 {
let (major, minor) = (u64::from(major), u64::from(minor));
((major & 0xffff_f000) << 32)
| ((major & 0x0000_0fff) << 8)
| ((minor & 0xffff_ff00) << 12)
| (minor & 0x0000_00ff)
}
#[cfg(target_pointer_width = "64")]
fn extent(offset: u64, len: u64) -> Result<(i64, i64, u64), Error> {
let (Ok(off), Ok(length), Some(end)) = (
i64::try_from(offset),
i64::try_from(len),
offset.checked_add(len),
) else {
return Err(Error::new(
ErrorKind::InvalidInput,
"extent past the largest file offset",
));
};
Ok((off, length, end))
}
#[cfg(target_pointer_width = "64")]
fn punch_hole_raw(path: &core::ffi::CStr, offset: u64, len: u64) -> Result<(), Error> {
let (off, length, _) = extent(offset, len)?;
let fd = ProbeFd::open(path, O_WRONLY)?;
unsafe {
syscall4(
Sysno::fallocate,
fd.0 as usize,
(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE) as usize,
off as usize,
length as usize,
)
}
.map_err(|e| err("fallocate", e))?;
Ok(())
}
#[cfg(target_pointer_width = "64")]
fn extent_is_hole_raw(
path: &core::ffi::CStr,
offset: u64,
len: u64,
) -> Result<Option<bool>, Error> {
if len == 0 {
return Ok(Some(false));
}
let (off, _, end) = extent(offset, len)?;
let fd = ProbeFd::open(path, O_RDONLY)?;
match lseek_errno(fd.0, off, SEEK_DATA) {
Ok(next_data) => Ok(Some(next_data >= end)),
Err(e) if e.into_raw() == errno::ENXIO => Ok(Some(true)),
Err(e) if matches!(e.into_raw(), errno::EINVAL | errno::EOPNOTSUPP) => Ok(None),
Err(e) => Err(err("lseek(SEEK_DATA)", e)),
}
}
#[cfg(target_pointer_width = "64")]
fn extent_contains_hole_raw(
path: &core::ffi::CStr,
offset: u64,
len: u64,
) -> Result<Option<bool>, Error> {
if len == 0 {
return Ok(Some(false));
}
let (off, _, end) = extent(offset, len)?;
let fd = ProbeFd::open(path, O_RDONLY)?;
let file_len = fstat_raw(fd.0)?.size;
match lseek_errno(fd.0, off, SEEK_HOLE) {
Ok(next_hole) => Ok(Some(next_hole < end.min(file_len))),
Err(e) if e.into_raw() == errno::ENXIO => Ok(Some(false)),
Err(e) if matches!(e.into_raw(), errno::EINVAL | errno::EOPNOTSUPP) => Ok(None),
Err(e) => Err(err("lseek(SEEK_HOLE)", e)),
}
}
fn linkat_raw(src: &core::ffi::CStr, dst: &core::ffi::CStr) -> Result<(), Error> {
unsafe {
syscall5(
Sysno::linkat,
AT_FDCWD as usize,
src.as_ptr() as usize,
AT_FDCWD as usize,
dst.as_ptr() as usize,
LINKAT_NO_FOLLOW,
)
}
.map_err(|e| err("linkat", e))?;
Ok(())
}
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
fn ficlone_raw(src: &core::ffi::CStr, dst: &core::ffi::CStr) -> Result<Result<(), Errno>, Error> {
let source = ProbeFd::open(src, O_RDONLY)?;
let target = ProbeFd(open_raw(dst, O_WRONLY | O_CREAT | O_EXCL, 0o644)?);
let cloned = unsafe {
syscall3(
Sysno::ioctl,
target.0 as usize,
FICLONE as usize,
source.0 as usize,
)
};
if let Err(e) = cloned {
drop(target);
let _ = unlinkat_raw(dst, false);
return Ok(Err(e));
}
Ok(Ok(()))
}
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
fn disable_cow_raw(path: &core::ffi::CStr) -> Result<(), Error> {
fn not_applicable(e: Errno) -> bool {
matches!(
e.into_raw(),
errno::ENOTTY | errno::EOPNOTSUPP | errno::EINVAL
)
}
let fd = ProbeFd::open(path, O_RDWR)?;
let mut flags: i32 = 0;
let got = unsafe {
syscall3(
Sysno::ioctl,
fd.0 as usize,
FS_IOC_GETFLAGS as usize,
(&raw mut flags) as usize,
)
};
match got {
Ok(_) => {}
Err(e) if not_applicable(e) => return Ok(()),
Err(e) => return Err(err("ioctl(FS_IOC_GETFLAGS)", e)),
}
if flags & FS_NOCOW_FL != 0 {
return Ok(());
}
flags |= FS_NOCOW_FL;
let set = unsafe {
syscall3(
Sysno::ioctl,
fd.0 as usize,
FS_IOC_SETFLAGS as usize,
(&raw const flags) as usize,
)
};
match set {
Ok(_) => Ok(()),
Err(e) if not_applicable(e) => Ok(()),
Err(e) => Err(err("ioctl(FS_IOC_SETFLAGS)", e)),
}
}
#[cfg(target_pointer_width = "64")]
fn fadvise_raw(fd: i32, hint: super::FileHint) -> Result<(), Error> {
let advice = match hint {
super::FileHint::Default => POSIX_FADV_NORMAL,
super::FileHint::Sequential => POSIX_FADV_SEQUENTIAL,
super::FileHint::Random => POSIX_FADV_RANDOM,
super::FileHint::WriteOnce => POSIX_FADV_DONTNEED,
};
match unsafe { syscall4(Sysno::fadvise64, fd as usize, 0, 0, advice) } {
Ok(_) => Ok(()),
Err(e) if e.into_raw() == errno::EINVAL => Ok(()),
Err(e) => Err(err("fadvise64", e)),
}
}
unsafe impl Send for IoUringRaw {}
pub struct IoUringRawFile {
ring: Arc<Mutex<IoUringRaw>>,
fd: i32,
cursor: u64,
is_append: bool,
}
impl IoUringRawFile {
fn read_regions<R: super::read_buf::ReadRegion>(
&self,
regions: &mut [R],
) -> crate::io::Result<()> {
let mut reads = Vec::with_capacity(regions.len());
for region in regions.iter_mut() {
#[expect(unsafe_code, reason = "see safety")]
let (offset, buf) = unsafe { region.target() };
if buf.is_empty() {
continue;
}
reads.push(RawRead {
fd: self.fd,
buf: buf.as_mut_ptr(),
len: batch_read_len(buf.len())?,
offset,
});
}
let mut results = alloc::vec![0i32; reads.len()];
unsafe { self.ring.lock().read_batch(&reads, &mut results) };
reads
.iter()
.zip(results)
.try_for_each(|(read, res)| read_verdict(res, read.len))
}
#[must_use]
pub fn new(ring: Arc<Mutex<IoUringRaw>>, fd: i32, is_append: bool) -> Self {
Self {
ring,
fd,
cursor: 0,
is_append,
}
}
fn offset_add(base: u64, delta: i64) -> Result<u64, Error> {
let r = if delta >= 0 {
base.checked_add(delta as u64)
} else {
base.checked_sub(delta.unsigned_abs())
};
r.ok_or_else(|| Error::new(ErrorKind::InvalidInput, "seek position out of range"))
}
fn read_impl(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
if buf.is_empty() {
return Ok(0);
}
let n = self.ring.lock().read_at(self.fd, buf, self.cursor)?;
self.cursor += n as u64;
Ok(n)
}
fn write_impl(&mut self, buf: &[u8]) -> Result<usize, Error> {
if buf.is_empty() {
return Ok(0);
}
if self.is_append {
return self.ring.lock().write_at(self.fd, buf, u64::MAX);
}
let n = self.ring.lock().write_at(self.fd, buf, self.cursor)?;
self.cursor += n as u64;
Ok(n)
}
fn seek_impl(&mut self, pos: SeekFrom) -> Result<u64, Error> {
let target = match pos {
SeekFrom::Start(o) => o,
SeekFrom::Current(d) => Self::offset_add(self.cursor, d)?,
SeekFrom::End(d) => Self::offset_add(file_size_raw(self.fd)?, d)?,
};
self.cursor = target;
Ok(target)
}
}
impl Drop for IoUringRawFile {
fn drop(&mut self) {
let _ = close_raw(self.fd);
}
}
impl FsFile for IoUringRawFile {
fn sync_all(&self) -> crate::io::Result<()> {
self.ring.lock().fsync(self.fd, false)
}
fn sync_data(&self) -> crate::io::Result<()> {
self.ring.lock().fsync(self.fd, true)
}
fn metadata(&self) -> crate::io::Result<FsMetadata> {
let m = fstat_raw(self.fd)?;
Ok(FsMetadata {
len: m.size,
is_dir: m.is_dir,
is_file: m.is_file,
})
}
fn hard_link_count(&self) -> crate::io::Result<u64> {
Ok(fstat_raw(self.fd)?.nlink)
}
fn set_len(&self, size: u64) -> crate::io::Result<()> {
ftruncate_raw(self.fd, size)
}
fn read_at(&self, buf: &mut [u8], offset: u64) -> crate::io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let mut total = 0usize;
while total < buf.len() {
let remaining = buf
.get_mut(total..)
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, "read_at slice out of range"))?;
let at = offset
.checked_add(total as u64)
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, "read_at offset overflow"))?;
let n = loop {
match self.ring.lock().read_at(self.fd, remaining, at) {
Ok(n) => break n,
Err(e) if e.kind() == ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
};
if n == 0 {
break;
}
total += n;
}
Ok(total)
}
fn read_many(&self, regions: &mut [(u64, &mut [u8])]) -> crate::io::Result<()> {
self.read_regions(regions)
}
fn read_at_buf(&self, buf: &mut super::ReadBuf<'_>, offset: u64) -> crate::io::Result<()> {
#[expect(unsafe_code, reason = "see safety")]
unsafe {
buf.fill_with(|bytes| FsFile::read_at(self, bytes, offset))
}
}
fn read_many_buf(&self, regions: &mut [(u64, super::ReadBuf<'_>)]) -> crate::io::Result<()> {
self.read_regions(regions)?;
#[expect(unsafe_code, reason = "see safety")]
unsafe {
super::read_buf::assume_regions_filled(regions);
}
Ok(())
}
fn backing_fd(&self) -> Option<i32> {
Some(self.fd)
}
fn lock_exclusive(&self) -> crate::io::Result<()> {
flock_exclusive_raw(self.fd, false)?;
Ok(())
}
fn try_lock_exclusive(&self) -> crate::io::Result<bool> {
flock_exclusive_raw(self.fd, true)
}
#[cfg(target_pointer_width = "64")]
fn start_writeback(&self, offset: u64, len: u64) -> crate::io::Result<()> {
start_writeback_raw(self.fd, offset, len)
}
fn set_write_lifetime(&self, lifetime: super::WriteLifetime) -> crate::io::Result<()> {
set_write_lifetime_raw(self.fd, lifetime)
}
#[cfg(target_pointer_width = "64")]
fn hint(&self, hint: super::FileHint) -> crate::io::Result<()> {
fadvise_raw(self.fd, hint)
}
}
#[cfg(feature = "std")]
impl std::io::Read for IoUringRawFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read_impl(buf).map_err(Into::into)
}
}
#[cfg(not(feature = "std"))]
impl crate::io::Read for IoUringRawFile {
fn read(&mut self, buf: &mut [u8]) -> crate::io::Result<usize> {
self.read_impl(buf)
}
}
#[cfg(feature = "std")]
impl std::io::Write for IoUringRawFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.write_impl(buf).map_err(Into::into)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[cfg(not(feature = "std"))]
impl crate::io::Write for IoUringRawFile {
fn write(&mut self, buf: &[u8]) -> crate::io::Result<usize> {
self.write_impl(buf)
}
fn flush(&mut self) -> crate::io::Result<()> {
Ok(())
}
}
#[cfg(feature = "std")]
impl std::io::Seek for IoUringRawFile {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.seek_impl(pos.into()).map_err(Into::into)
}
}
#[cfg(not(feature = "std"))]
impl crate::io::Seek for IoUringRawFile {
fn seek(&mut self, pos: SeekFrom) -> crate::io::Result<u64> {
self.seek_impl(pos)
}
}
fn path_to_cstring(path: &Path) -> Result<CString, Error> {
let s = path
.to_str()
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, "path is not valid UTF-8"))?;
CString::new(s.as_bytes())
.map_err(|_| Error::new(ErrorKind::InvalidInput, "path contains an interior NUL"))
}
fn open_flags(opts: &FsOpenOptions) -> i32 {
let mut flags = if opts.read && (opts.write || opts.append) {
O_RDWR
} else if opts.write || opts.append {
O_WRONLY
} else {
O_RDONLY
};
if opts.create || opts.create_new {
flags |= O_CREAT;
}
if opts.create_new {
flags |= O_EXCL;
}
if opts.truncate {
flags |= O_TRUNC;
}
if opts.append {
flags |= O_APPEND;
}
if opts.no_follow {
flags |= O_NOFOLLOW;
}
if opts.no_wait {
flags |= O_NONBLOCK;
}
flags
}
fn read_dir_entries(fd: i32) -> Result<Vec<(String, bool)>, Error> {
let mut entries = Vec::new();
let mut buf = [0u8; 4096];
loop {
let n = unsafe {
syscall3(
Sysno::getdents64,
fd as usize,
buf.as_mut_ptr() as usize,
buf.len(),
)
}
.map_err(|e| err("getdents64", e))?;
if n == 0 {
break; }
let n = n as usize;
let mut off = 0usize;
while off + 19 <= n {
let reclen = {
let b: [u8; 2] = buf
.get(off + 16..off + 18)
.and_then(|s| s.try_into().ok())
.ok_or_else(|| {
Error::new(ErrorKind::InvalidData, "dirent reclen out of range")
})?;
usize::from(u16::from_ne_bytes(b))
};
if reclen < 19 || off + reclen > n {
return Err(Error::new(
ErrorKind::InvalidData,
"dirent record length invalid",
));
}
let d_type = *buf
.get(off + 18)
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "dirent type out of range"))?;
let name_region = buf
.get(off + 19..off + reclen)
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "dirent name out of range"))?;
let name_len = name_region
.iter()
.position(|&c| c == 0)
.unwrap_or(name_region.len());
let name_bytes = name_region
.get(..name_len)
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "dirent name slice"))?;
let name = core::str::from_utf8(name_bytes)
.map_err(|_| Error::new(ErrorKind::InvalidData, "dirent name is not UTF-8"))?;
if name != "." && name != ".." {
entries.push((name.to_string(), d_type == DT_DIR));
}
off += reclen;
}
}
Ok(entries)
}
pub struct IoUringRawFs {
ring: Arc<Mutex<IoUringRaw>>,
}
impl IoUringRawFs {
pub fn new(ring_entries: u32) -> Result<Self, Error> {
Ok(Self {
ring: Arc::new(Mutex::new(IoUringRaw::new(ring_entries)?)),
})
}
fn read_blocks(
&self,
reqs: &mut [BlockRead<'_>],
on_read: Option<OnRead<'_>>,
) -> crate::io::Result<()> {
self.check_descriptor_batch(reqs)?;
let mut reads = Vec::with_capacity(reqs.len());
let mut ats = Vec::with_capacity(reqs.len());
for (at, req) in reqs.iter_mut().enumerate() {
let Some(fd) = req.file.backing_fd() else {
continue;
};
let want = req.buf.capacity() - req.buf.filled();
if want == 0 {
continue;
}
let offset = req.offset + req.buf.filled() as u64;
reads.push(RawRead {
fd,
buf: req.buf.unfilled_mut().as_mut_ptr(),
len: batch_read_len(want)?,
offset,
});
ats.push(at);
}
let mut results = alloc::vec![0i32; reads.len()];
if !reads.is_empty() {
unsafe { self.ring.lock().read_batch(&reads, &mut results) };
}
let mut failure: Option<(usize, Error)> = None;
for ((&at, read), res) in ats.iter().zip(&reads).zip(results) {
match read_verdict(res, read.len) {
Ok(()) => {
if let Some(req) = reqs.get_mut(at) {
req.buf.advance(read.len as usize);
}
}
Err(error) => {
if failure.as_ref().is_none_or(|(held, _)| at < *held) {
failure = Some((at, error));
}
}
}
}
for (at, req) in reqs.iter_mut().enumerate() {
if req.file.backing_fd().is_some() || req.buf.is_full() {
continue;
}
if failure.as_ref().is_some_and(|(held, _)| *held < at) {
break;
}
let Some(offset) = req.offset.checked_add(req.buf.filled() as u64) else {
failure = Some((
at,
Error::new(
ErrorKind::InvalidInput,
"read_blocks_batched: request offset overflows",
),
));
break;
};
let dst = req.buf.unfilled_mut();
let want = dst.len();
match req.file.read_at(dst, offset) {
Ok(n) => {
req.buf.advance(n);
if n != want {
failure = Some((at, short_block_read()));
break;
}
}
Err(error) => {
failure = Some((at, error));
break;
}
}
}
if let Some(on_read) = on_read {
for (at, req) in reqs.iter().enumerate() {
if req.buf.is_full() {
on_read(at, req);
}
}
}
failure.map_or(Ok(()), |(_, error)| Err(error))
}
}
type OnRead<'f> = &'f mut dyn for<'a, 'b> FnMut(usize, &'a BlockRead<'b>);
fn batch_read_len(len: usize) -> Result<u32, Error> {
i32::try_from(len)
.map(i32::unsigned_abs)
.map_err(|_| Error::new(ErrorKind::InvalidInput, "buffer exceeds i32::MAX"))
}
fn read_verdict(res: i32, len: u32) -> Result<(), Error> {
if res < 0 {
return Err(Error::new(
errno_to_kind(-res),
format!("io_uring read completed with errno {}", -res),
));
}
if res.unsigned_abs() != len {
return Err(short_block_read());
}
Ok(())
}
fn short_block_read() -> Error {
Error::new(
ErrorKind::UnexpectedEof,
"io_uring read_blocks_batched: short read on a fixed-size block",
)
}
impl Fs for IoUringRawFs {
fn read_blocks_batched(&self, reqs: &mut [BlockRead<'_>]) -> crate::io::Result<()> {
self.read_blocks(reqs, None)
}
fn read_blocks_batched_each(
&self,
reqs: &mut [BlockRead<'_>],
on_read: &mut dyn FnMut(usize, &BlockRead<'_>),
) -> crate::io::Result<()> {
self.read_blocks(reqs, Some(on_read))
}
fn reads_by_descriptor(&self) -> bool {
true
}
fn check_descriptor_batch(&self, reqs: &[BlockRead<'_>]) -> crate::io::Result<()> {
for req in reqs {
let want = req.buf.capacity() - req.buf.filled();
if want == 0 || req.file.backing_fd().is_none() {
continue;
}
batch_read_len(want)?;
if req.offset.checked_add(req.buf.filled() as u64).is_none() {
return Err(Error::new(
ErrorKind::InvalidInput,
"read_blocks_batched: request offset overflows",
));
}
}
Ok(())
}
fn open(&self, path: &Path, opts: &FsOpenOptions) -> crate::io::Result<Box<dyn FsFile>> {
let cpath = path_to_cstring(path)?;
let fd = open_raw(&cpath, open_flags(opts), 0o644)?;
let file = IoUringRawFile::new(Arc::clone(&self.ring), fd, opts.append);
if opts.no_wait {
clear_nonblock_raw(fd)?;
}
Ok(Box::new(file))
}
fn create_dir_all(&self, path: &Path) -> crate::io::Result<()> {
if let Some(parent) = path.parent()
&& !parent.to_str().unwrap_or("").is_empty()
{
self.create_dir_all(parent)?;
}
match self.create_dir(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()),
Err(e) => Err(e),
}
}
fn create_dir(&self, path: &Path) -> crate::io::Result<()> {
mkdirat_raw(&path_to_cstring(path)?, 0o755)
}
fn read_dir(&self, path: &Path) -> crate::io::Result<Vec<FsDirEntry>> {
let cpath = path_to_cstring(path)?;
let fd = open_raw(&cpath, O_RDONLY | O_DIRECTORY, 0)?;
let result = read_dir_entries(fd);
let _ = close_raw(fd);
let names = result?;
Ok(names
.into_iter()
.map(|(name, is_dir)| FsDirEntry {
path: path.join(&name),
file_name: name,
is_dir,
})
.collect())
}
fn remove_file(&self, path: &Path) -> crate::io::Result<()> {
unlinkat_raw(&path_to_cstring(path)?, false)
}
fn remove_dir_all(&self, path: &Path) -> crate::io::Result<()> {
for entry in self.read_dir(path)? {
let child = path.join(&entry.file_name);
if entry.is_dir {
self.remove_dir_all(&child)?;
} else {
self.remove_file(&child)?;
}
}
unlinkat_raw(&path_to_cstring(path)?, true)
}
fn remove_dir(&self, path: &Path) -> crate::io::Result<()> {
unlinkat_raw(&path_to_cstring(path)?, true)
}
fn rename(&self, from: &Path, to: &Path) -> crate::io::Result<()> {
renameat2_raw(&path_to_cstring(from)?, &path_to_cstring(to)?)
}
fn same_file(&self, a: &Path, b: &Path) -> crate::io::Result<bool> {
let ia = statx_identity_raw(&path_to_cstring(a)?)?;
let ib = statx_identity_raw(&path_to_cstring(b)?)?;
Ok(ia == ib)
}
fn symlink(&self, target: &Path, link: &Path, _to_dir: bool) -> crate::io::Result<()> {
symlinkat_raw(&path_to_cstring(target)?, &path_to_cstring(link)?)
}
fn read_link(&self, path: &Path) -> crate::io::Result<Option<crate::path::PathBuf>> {
let Some(target) = readlinkat_raw(&path_to_cstring(path)?)? else {
return Ok(None);
};
#[cfg(feature = "std")]
{
use std::os::unix::ffi::OsStringExt as _;
Ok(Some(std::ffi::OsString::from_vec(target).into()))
}
#[cfg(not(feature = "std"))]
{
let target = String::from_utf8(target).map_err(|_| {
Error::new(
ErrorKind::InvalidInput,
"readlinkat: symlink target is not valid UTF-8",
)
})?;
Ok(Some(crate::path::PathBuf::from(target)))
}
}
fn metadata(&self, path: &Path) -> crate::io::Result<FsMetadata> {
match statx_path_raw(&path_to_cstring(path)?)? {
Some(m) => Ok(FsMetadata {
len: m.size,
is_dir: m.is_dir,
is_file: m.is_file,
}),
None => Err(Error::new(ErrorKind::NotFound, "path not found")),
}
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
fn available_space(&self, path: &Path) -> crate::io::Result<u64> {
statfs_available_raw(&path_to_cstring(path)?)
}
fn sync_directory(&self, path: &Path) -> crate::io::Result<()> {
let cpath = path_to_cstring(path)?;
let fd = open_raw(&cpath, O_RDONLY | O_DIRECTORY, 0)?;
let r = self.ring.lock().fsync(fd, false);
let _ = close_raw(fd);
r
}
fn exists(&self, path: &Path) -> crate::io::Result<bool> {
Ok(statx_path_raw(&path_to_cstring(path)?)?.is_some())
}
fn hard_link(&self, src: &Path, dst: &Path) -> crate::io::Result<()> {
linkat_raw(&path_to_cstring(src)?, &path_to_cstring(dst)?)
}
fn backend_id(&self) -> Option<u64> {
Some(super::KERNEL_BACKEND_ID)
}
fn volume_id(&self, path: &Path) -> Option<u64> {
let stat = statx_at(&path_to_cstring(path).ok()?).ok()??;
Some(makedev(stat.stx_dev_major, stat.stx_dev_minor))
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
fn capabilities(&self, path: &Path) -> super::FsCapabilities {
path_to_cstring(path)
.and_then(|path| statfs_type_raw(&path))
.map_or_else(
|_| super::FsCapabilities::default(),
super::linux_fs_type_capabilities,
)
}
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
fn try_disable_cow(&self, path: &Path) -> crate::io::Result<()> {
disable_cow_raw(&path_to_cstring(path)?)
}
#[cfg(target_pointer_width = "64")]
fn punch_hole(&self, path: &Path, offset: u64, len: u64) -> crate::io::Result<()> {
punch_hole_raw(&path_to_cstring(path)?, offset, len)
}
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
fn reflink_file(&self, src: &Path, dst: &Path) -> crate::io::Result<()> {
match ficlone_raw(&path_to_cstring(src)?, &path_to_cstring(dst)?)? {
Ok(()) => Ok(()),
Err(e)
if matches!(
e.into_raw(),
errno::EOPNOTSUPP | errno::EXDEV | errno::EINVAL
) =>
{
super::copy_file_streamed(self, src, dst)
}
Err(e) => Err(err("ioctl(FICLONE)", e)),
}
}
fn hard_link_count(&self, path: &Path) -> crate::io::Result<u64> {
statx_path_raw(&path_to_cstring(path)?)?
.map(|stat| stat.nlink)
.ok_or_else(|| Error::new(ErrorKind::NotFound, "path not found"))
}
fn allocated_size(&self, path: &Path) -> crate::io::Result<Option<u64>> {
let stat = statx_at(&path_to_cstring(path)?)?
.ok_or_else(|| Error::new(ErrorKind::NotFound, "path not found"))?;
if stat.stx_mask & STATX_BLOCKS == 0 {
return Ok(None);
}
Ok(Some(stat.stx_blocks * 512))
}
#[cfg(target_pointer_width = "64")]
fn extent_is_hole(
&self,
path: &Path,
offset: u64,
len: u64,
) -> crate::io::Result<Option<bool>> {
extent_is_hole_raw(&path_to_cstring(path)?, offset, len)
}
#[cfg(target_pointer_width = "64")]
fn extent_contains_hole(
&self,
path: &Path,
offset: u64,
len: u64,
) -> crate::io::Result<Option<bool>> {
extent_contains_hole_raw(&path_to_cstring(path)?, offset, len)
}
}
pub struct IoUringRaw {
ring_fd: i32,
sq_ptr: *mut u8,
sq_len: usize,
cq_ptr: *mut u8,
cq_len: usize,
sqes: *mut IoUringSqe,
sqes_len: usize,
sq_entries: u32,
cq_entries: u32,
#[cfg(test)]
enters: usize,
sq_khead: *const u32,
sq_ktail: *mut u32,
sq_ring_mask: u32,
sq_array: *mut u32,
cq_khead: *mut u32,
cq_ktail: *const u32,
cq_ring_mask: u32,
cqes: *const IoUringCqe,
}
impl IoUringRaw {
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn new(entries: u32) -> Result<Self, Error> {
let mut params = IoUringParams::default();
let ring_fd = unsafe { io_uring_setup(entries, &raw mut params) }?;
let guard = FdGuard(ring_fd);
let sq_entries = params.sq_entries;
let single_mmap = params.features & IORING_FEAT_SINGLE_MMAP != 0;
let sq_ring_sz = params.sq_off.array as usize + sq_entries as usize * size_of::<u32>();
let cq_ring_sz =
params.cq_off.cqes as usize + params.cq_entries as usize * size_of::<IoUringCqe>();
let sq_len = if single_mmap {
sq_ring_sz.max(cq_ring_sz)
} else {
sq_ring_sz
};
let sq_ptr = unsafe {
mmap(
sq_len,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE,
ring_fd,
IORING_OFF_SQ_RING,
)
}?;
let (cq_ptr, cq_len, cq_base) = if single_mmap {
(core::ptr::null_mut(), 0usize, sq_ptr)
} else {
let p = match unsafe {
mmap(
cq_ring_sz,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE,
ring_fd,
IORING_OFF_CQ_RING,
)
} {
Ok(p) => p,
Err(e) => {
unsafe { munmap(sq_ptr, sq_len) };
return Err(e);
}
};
(p, cq_ring_sz, p)
};
let sqes = match unsafe {
mmap(
sq_entries as usize * size_of::<IoUringSqe>(),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE,
ring_fd,
IORING_OFF_SQES,
)
} {
Ok(p) => p.cast::<IoUringSqe>(),
Err(e) => {
unsafe {
munmap(sq_ptr, sq_len);
if !cq_ptr.is_null() {
munmap(cq_ptr, cq_len);
}
}
return Err(e);
}
};
guard.disarm();
let sqes_len = sq_entries as usize * size_of::<IoUringSqe>();
Ok(unsafe {
Self {
ring_fd,
sq_ptr,
sq_len,
cq_ptr,
cq_len,
sqes,
sqes_len,
sq_entries,
cq_entries: params.cq_entries,
#[cfg(test)]
enters: 0,
sq_khead: sq_ptr.add(params.sq_off.head as usize).cast(),
sq_ktail: sq_ptr.add(params.sq_off.tail as usize).cast(),
sq_ring_mask: *(sq_ptr.add(params.sq_off.ring_mask as usize).cast::<u32>()),
sq_array: sq_ptr.add(params.sq_off.array as usize).cast(),
cq_khead: cq_base.add(params.cq_off.head as usize).cast(),
cq_ktail: cq_base.add(params.cq_off.tail as usize).cast(),
cq_ring_mask: *(cq_base.add(params.cq_off.ring_mask as usize).cast::<u32>()),
cqes: cq_base.add(params.cq_off.cqes as usize).cast(),
}
})
}
pub fn nop(&mut self, user_data: u64) -> Result<i32, Error> {
let sqe = IoUringSqe {
opcode: IORING_OP_NOP,
user_data,
..IoUringSqe::default()
};
self.submit_and_reap_one(&sqe)
}
pub fn read_at(&mut self, fd: i32, buf: &mut [u8], offset: u64) -> Result<usize, Error> {
let len = u32::try_from(buf.len()).map_err(|_| {
Error::new(
ErrorKind::InvalidInput,
"read length exceeds the 4 GiB io_uring single-op limit",
)
})?;
let sqe = IoUringSqe {
opcode: IORING_OP_READ,
fd,
addr: buf.as_mut_ptr() as u64,
len,
off: offset,
..IoUringSqe::default()
};
let res = self.submit_and_reap_one(&sqe)?;
Ok(res as usize)
}
pub fn write_at(&mut self, fd: i32, buf: &[u8], offset: u64) -> Result<usize, Error> {
let len = u32::try_from(buf.len()).map_err(|_| {
Error::new(
ErrorKind::InvalidInput,
"write length exceeds the 4 GiB io_uring single-op limit",
)
})?;
let sqe = IoUringSqe {
opcode: IORING_OP_WRITE,
fd,
addr: buf.as_ptr() as u64,
len,
off: offset,
..IoUringSqe::default()
};
let res = self.submit_and_reap_one(&sqe)?;
Ok(res as usize)
}
pub fn fsync(&mut self, fd: i32, datasync: bool) -> Result<(), Error> {
let sqe = IoUringSqe {
opcode: IORING_OP_FSYNC,
fd,
op_flags: if datasync { IORING_FSYNC_DATASYNC } else { 0 },
..IoUringSqe::default()
};
self.submit_and_reap_one(&sqe)?;
Ok(())
}
unsafe fn read_batch(&mut self, reads: &[RawRead], results: &mut [i32]) {
unsafe {
self.submit_all(
&mut |at| {
reads.get(at).map(|read| IoUringSqe {
opcode: IORING_OP_READ,
fd: read.fd,
addr: read.buf as u64,
len: read.len,
off: read.offset,
..IoUringSqe::default()
})
},
results,
);
}
}
fn submit_and_reap_one(&mut self, sqe: &IoUringSqe) -> Result<i32, Error> {
let mut res = [0i32];
unsafe { self.submit_all(&mut |at| (at == 0).then_some(*sqe), &mut res) };
let [res] = res;
if res < 0 {
return Err(Error::new(
errno_to_kind(-res),
format!("io_uring op completed with errno {}", -res),
));
}
Ok(res)
}
#[cfg_attr(coverage_nightly, coverage(off))]
unsafe fn submit_all(
&mut self,
sqe_at: &mut dyn FnMut(usize) -> Option<IoUringSqe>,
results: &mut [i32],
) {
let count = results.len();
let room = self.sq_entries.min(self.cq_entries) as usize;
let mut next = 0usize;
let mut untaken = 0u32;
let mut owed = 0usize;
while next < count || owed > 0 {
unsafe {
let mut tail = core::ptr::read_volatile(self.sq_ktail);
let head = core::ptr::read_volatile(self.sq_khead);
fence(Ordering::Acquire);
while next < count && owed < room && tail.wrapping_sub(head) < self.sq_entries {
let Some(sqe) = sqe_at(next) else {
if let Some(slot) = results.get_mut(next) {
*slot = -errno::EINVAL;
}
next += 1;
continue;
};
let index = tail & self.sq_ring_mask;
core::ptr::write(
self.sqes.add(index as usize),
IoUringSqe {
user_data: next as u64,
..sqe
},
);
core::ptr::write(self.sq_array.add(index as usize), index);
tail = tail.wrapping_add(1);
next += 1;
owed += 1;
untaken += 1;
}
fence(Ordering::Release);
core::ptr::write_volatile(self.sq_ktail, tail);
}
if owed == 0 {
continue;
}
let wait = if next == count {
owed as u32
} else {
1
};
#[cfg(test)]
{
self.enters += 1;
}
match unsafe { io_uring_enter_raw(self.ring_fd, untaken, wait, IORING_ENTER_GETEVENTS) }
{
Ok(taken) => untaken -= taken.min(untaken),
Err(e) if matches!(e.into_raw(), errno::EINTR | errno::EAGAIN | errno::EBUSY) => {}
Err(e) if untaken > 0 => {
let refusal = -e.into_raw();
unsafe {
let tail = core::ptr::read_volatile(self.sq_ktail);
core::ptr::write_volatile(self.sq_ktail, tail.wrapping_sub(untaken));
}
let first = next - untaken as usize;
for slot in results.get_mut(first..).into_iter().flatten() {
*slot = refusal;
}
owed -= untaken as usize;
untaken = 0;
next = count;
}
Err(e) => ring_lost(e),
}
unsafe {
let mut head = core::ptr::read_volatile(self.cq_khead);
let tail = core::ptr::read_volatile(self.cq_ktail);
fence(Ordering::Acquire);
while head != tail {
let cqe = core::ptr::read(self.cqes.add((head & self.cq_ring_mask) as usize));
head = head.wrapping_add(1);
owed -= 1;
if let Some(slot) = usize::try_from(cqe.user_data)
.ok()
.and_then(|at| results.get_mut(at))
{
*slot = cqe.res;
}
}
fence(Ordering::Release);
core::ptr::write_volatile(self.cq_khead, head);
}
}
}
#[must_use]
pub fn sq_entries(&self) -> u32 {
self.sq_entries
}
}
impl Drop for IoUringRaw {
fn drop(&mut self) {
unsafe {
munmap(self.sqes.cast(), self.sqes_len);
if !self.cq_ptr.is_null() {
munmap(self.cq_ptr, self.cq_len);
}
munmap(self.sq_ptr, self.sq_len);
close(self.ring_fd);
}
}
}
struct FdGuard(i32);
impl FdGuard {
fn disarm(self) {
core::mem::forget(self);
}
}
impl Drop for FdGuard {
#[cfg_attr(coverage_nightly, coverage(off))]
fn drop(&mut self) {
unsafe { close(self.0) };
}
}
#[cfg(test)]
#[expect(clippy::expect_used, reason = "test code")]
mod tests;