Skip to content
Closed
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 37 additions & 2 deletions crates/fspy_nostd/src/fs/linux.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,45 @@
use core::{mem::MaybeUninit, slice};

use crate::{AsRawFd as _, BorrowedFd, CStr, Error, Fat, Result, Thin};
use rustix::fd::FromRawFd as _;

use crate::{
AsRawFd as _, BorrowedFd, CStr, Error, Fat, OwnedFd, Result, Thin,
fs::{Mode, OFlags},
};

// Linux UAPI `PATH_MAX`.
pub(super) const PATH_MAX: usize = 4096;

fn syscall_fd(fd: BorrowedFd<'_>) -> Result<usize> {
let fd = isize::try_from(fd.as_raw_fd()).map_err(|_| Error::OVERFLOW)?;
Ok(fd.cast_unsigned())
}

#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")]
pub(super) fn openat<R>(
dirfd: BorrowedFd<'_>,
path: CStr<'_, R>,
flags: OFlags,
mode: Mode,
) -> Result<OwnedFd> {
// SAFETY: `dirfd` remains borrowed and `path` is NUL-terminated. The
// kernel receives all four syscall arguments explicitly.
let fd = unsafe {
syscalls::syscall4(
syscalls::Sysno::openat,
syscall_fd(dirfd)?,
path.as_ptr().addr(),
usize::try_from(flags.bits()).map_err(|_| Error::INVAL)?,
usize::try_from(mode.bits()).map_err(|_| Error::INVAL)?,
)
}
.map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?;
let fd = i32::try_from(fd).map_err(|_| Error::OVERFLOW)?;

// SAFETY: a successful `openat` returns a new owned descriptor.
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}

/// Reads the target of `path` relative to `dirfd` into `buf`.
///
/// The returned bytes borrow the initialized prefix of `buf`. As with the
Expand All @@ -27,7 +62,7 @@ pub fn readlinkat<'buf>(
let initialized = unsafe {
syscalls::syscall4(
syscalls::Sysno::readlinkat,
(dirfd.as_raw_fd() as isize).cast_unsigned(),
syscall_fd(dirfd)?,
path.as_ptr().addr(),
buf.as_mut_ptr().addr(),
buf.len(),
Expand Down
6 changes: 4 additions & 2 deletions crates/fspy_nostd/src/fs/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ use rustix::{

use crate::{BorrowedFd, CStr, CWD, Error, Fat, Result, Thin};

pub(super) const PATH_MAX: usize = libc::PATH_MAX as usize;
// Darwin UAPI `MAXPATHLEN`.
pub(super) const PATH_MAX: usize = 1024;
const _: () = assert!(libc::PATH_MAX == 1024);

#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")]
fn openat<R>(
pub(super) fn openat<R>(
dirfd: BorrowedFd<'_>,
path: CStr<'_, R>,
flags: OFlags,
Expand Down
18 changes: 17 additions & 1 deletion crates/fspy_nostd/src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

use core::mem::MaybeUninit;

use crate::{CStr, Fat, Result};
pub use rustix::fs::{Mode, OFlags, fstat};

use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result};

#[cfg(target_os = "linux")]
mod linux;
Expand All @@ -21,6 +23,20 @@ pub use mac::fcntl_getpath;
/// The platform's maximum pathname size, including the terminating NUL.
pub const PATH_MAX: usize = imp::PATH_MAX;

/// Opens `path` relative to `dirfd` and returns its owned descriptor.
///
/// # Errors
///
/// Returns the error reported by `openat`.
pub fn openat<R>(
dirfd: BorrowedFd<'_>,
path: CStr<'_, R>,
flags: OFlags,
mode: Mode,
) -> Result<OwnedFd> {
imp::openat(dirfd, path, flags, mode)
}

/// Writes the absolute pathname of the current working directory into `buf`.
///
/// The returned C string borrows `buf`, starts at the same address as `buf`,
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy_nostd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ macro_rules! wide_cstr {

#[cfg(unix)]
pub use rustix::{
fd::{AsRawFd, BorrowedFd},
fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd},
fs::CWD,
io::Errno as Error,
};
Expand Down
3 changes: 3 additions & 0 deletions crates/fspy_shm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ rust-version.workspace = true
memmap2 = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

[target.'cfg(unix)'.dependencies]
fspy_nostd = { workspace = true }

[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { workspace = true, features = [
"Win32_Foundation",
Expand Down
77 changes: 59 additions & 18 deletions crates/fspy_shm/src/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use std::{
ffi::OsStr,
fs::{self, File, OpenOptions},
io,
os::unix::fs::OpenOptionsExt as _,
num::NonZeroUsize,
os::unix::{ffi::OsStrExt as _, fs::OpenOptionsExt as _, io::IntoRawFd as _},
path::PathBuf,
};

Expand All @@ -29,8 +30,8 @@ pub struct ShmKeeper {
/// [`map`](Self::map) can be called more than once; every call returns another
/// view of the same bytes. Drop the handle once the mappings exist.
pub struct ShmHandle {
file: File,
size: usize,
file: fspy_nostd::OwnedFd,
size: NonZeroUsize,
}

/// The mapped shared bytes.
Expand All @@ -53,13 +54,10 @@ pub struct Mapping {
///
/// Returns an error if the shared memory cannot be created or sized.
pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> {
if size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"shared-memory size must be nonzero",
));
}
let size_u64 = u64::try_from(size).map_err(|_| {
let size = NonZeroUsize::new(size).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size must be nonzero")
})?;
let size_u64 = u64::try_from(size.get()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size exceeds u64")
})?;

Expand All @@ -81,6 +79,7 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> {

// Every byte reads as zero because the file is all holes.
file.set_len(size_u64)?;
let file = into_nostd_fd(file);

Ok((keeper, ShmHandle { file, size }))
}
Expand All @@ -95,20 +94,61 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> {
/// Returns an error if the shared memory is unavailable, which is the common
/// case once its keeper has been dropped.
pub fn open(id: &OsStr) -> io::Result<ShmHandle> {
// Rust opens are `O_CLOEXEC`, so a traced process never leaks this
// descriptor.
let file = OpenOptions::new().read(true).write(true).open(id)?;
let file = open_file(id)?;
// If another process shrinks the file before `map`, mapping fails. If it
// resizes afterwards, nothing here touches the mapped pages. A concurrent
// resize cannot make a mapping access invalid memory.
let size = usize::try_from(file.metadata()?.len())
let size = usize::try_from(fspy_nostd::fs::fstat(&file).map_err(error_to_io)?.st_size)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid shared-memory size"))?;
if size == 0 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "shared-memory size is zero"));
}
let size = NonZeroUsize::new(size)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "shared-memory size is zero"))?;
Ok(ShmHandle { file, size })
}

fn open_file(path: &OsStr) -> io::Result<fspy_nostd::OwnedFd> {
let mut buf = [0_u8; fspy_nostd::fs::PATH_MAX];
let path = copy_path(path, &mut buf)?;
fspy_nostd::fs::openat(
fspy_nostd::CWD,
path,
fspy_nostd::fs::OFlags::RDWR | fspy_nostd::fs::OFlags::CLOEXEC,
fspy_nostd::fs::Mode::empty(),
)
.map_err(error_to_io)
}

fn copy_path<'buf>(
path: &OsStr,
buf: &'buf mut [u8; fspy_nostd::fs::PATH_MAX],
) -> io::Result<fspy_nostd::CStr<'buf, fspy_nostd::Fat>> {
let bytes = path.as_bytes();
if bytes.contains(&0) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL"));
}

let len_with_nul = bytes.len().checked_add(1).ok_or(io::ErrorKind::InvalidInput)?;
let initialized = buf.get_mut(..len_with_nul).ok_or_else(|| {
io::Error::from_raw_os_error(fspy_nostd::Error::NAMETOOLONG.raw_os_error())
})?;
initialized[..bytes.len()].copy_from_slice(bytes);
initialized[bytes.len()] = 0;

// SAFETY: the copied path contains no NUL, followed by the terminator set
// above, and the returned view borrows the initialized buffer prefix.
Ok(unsafe { fspy_nostd::CStr::from_units_with_nul_unchecked(initialized) })
}

fn error_to_io(error: fspy_nostd::Error) -> io::Error {
io::Error::from_raw_os_error(error.raw_os_error())
}

fn into_nostd_fd(file: File) -> fspy_nostd::OwnedFd {
let fd = file.into_raw_fd();
// SAFETY: ownership of `file`'s descriptor transfers without closing or
// duplicating it.
unsafe { fspy_nostd::FromRawFd::from_raw_fd(fd) }
}

impl Drop for ShmKeeper {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
Expand All @@ -131,7 +171,8 @@ impl ShmHandle {
///
/// Returns an error if the mapping cannot be established.
pub fn map(&self) -> io::Result<Mapping> {
Ok(Mapping { raw: MmapOptions::new().len(self.size).map_raw(&self.file)? })
let file = fspy_nostd::AsRawFd::as_raw_fd(&self.file);
Ok(Mapping { raw: MmapOptions::new().len(self.size.get()).map_raw(file)? })
}
}

Expand Down
Loading