Skip to content
Draft
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.

3 changes: 3 additions & 0 deletions crates/fspy_shm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ rust-version.workspace = true
[dependencies]
memmap2 = { workspace = true }

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

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

Expand Down Expand Up @@ -82,9 +82,7 @@ pub fn create(path: &OsStr, 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(path: &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(path)?;
let file = open_file(path)?;
// 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.
Expand All @@ -96,6 +94,32 @@ pub fn open(path: &OsStr) -> io::Result<ShmHandle> {
Ok(ShmHandle { file, size })
}

fn open_file(path: &OsStr) -> io::Result<File> {
let path_bytes = path.as_bytes();
let len_with_nul = path_bytes.len().checked_add(1).ok_or(io::ErrorKind::InvalidInput)?;
let mut path_buf = [0_u8; sigsafe::fs::PATH_MAX];
let path = path_buf
.get_mut(..len_with_nul)
.ok_or_else(|| io::Error::from_raw_os_error(sigsafe::Errno::NAMETOOLONG.raw_os_error()))?;
path[..path_bytes.len()].copy_from_slice(path_bytes);
let path = sigsafe::CStr::from_bytes_with_nul(path).map_err(|_| io::ErrorKind::InvalidInput)?;
let fd = sigsafe::fs::openat(
sigsafe::CWD,
path,
sigsafe::fs::OFlags::RDWR | sigsafe::fs::OFlags::CLOEXEC,
sigsafe::fs::Mode::empty(),
)
.map_err(errno_to_io)?;
let fd = sigsafe::IntoRawFd::into_raw_fd(fd);
// SAFETY: ownership of the descriptor returned by `openat` transfers to
// this `File` without closing or duplicating it.
Ok(unsafe { File::from_raw_fd(fd) })
}

fn errno_to_io(errno: sigsafe::Errno) -> io::Error {
io::Error::from_raw_os_error(errno.raw_os_error())
}

impl Drop for ShmKeeper {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
Expand Down
19 changes: 19 additions & 0 deletions crates/sigsafe/src/c_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ impl<'a> CStr<'a, Thin> {
}

impl<'a> CStr<'a, Fat> {
/// Creates a length-retaining C string after validating its bytes.
///
/// # Errors
///
/// Returns an error unless `bytes` contains exactly one NUL byte at the
/// end.
pub fn from_bytes_with_nul(bytes: &'a [u8]) -> Result<Self, core::ffi::FromBytesWithNulError> {
core::ffi::CStr::from_bytes_with_nul(bytes)?;
// SAFETY: validation above established exactly one trailing NUL.
Ok(unsafe { Self::from_bytes_with_nul_unchecked(bytes) })
}

/// Creates a length-retaining C string from bytes without validation.
///
/// # Safety
Expand Down Expand Up @@ -191,6 +203,13 @@ mod tests {
assert_eq!(counted.as_bytes_with_nul(), fat.as_bytes_with_nul());
}

#[test]
fn checked_fat_constructor_validates_the_terminator() {
assert_eq!(CStr::<Fat>::from_bytes_with_nul(b"abc\0").unwrap().as_bytes(), b"abc");
assert!(CStr::<Fat>::from_bytes_with_nul(b"abc").is_err());
assert!(CStr::<Fat>::from_bytes_with_nul(b"a\0bc\0").is_err());
}

#[test]
fn thin_view_accepts_a_checked_non_null_pointer() {
let ptr = NonNull::new(c"abc".as_ptr().cast_mut()).unwrap();
Expand Down
40 changes: 38 additions & 2 deletions crates/sigsafe/src/fs/linux.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,46 @@
use core::{mem::MaybeUninit, slice};

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

use crate::{
AsRawFd as _, BorrowedFd, CStr, Errno, 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(|_| Errno::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 reads no variadic arguments; all four syscall arguments are
// passed explicitly.
let fd = unsafe {
syscalls::syscall4(
syscalls::Sysno::openat,
syscall_fd(dirfd)?,
path.as_ptr().addr(),
usize::try_from(flags.bits()).map_err(|_| Errno::INVAL)?,
usize::try_from(mode.bits()).map_err(|_| Errno::INVAL)?,
)
}
.map_err(|errno| Errno::from_raw_os_error(errno.into_raw()))?;
let fd = i32::try_from(fd).map_err(|_| Errno::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 +63,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
7 changes: 5 additions & 2 deletions crates/sigsafe/src/fs/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ use rustix::{

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

pub(super) const PATH_MAX: usize = libc::PATH_MAX as usize;
// Darwin UAPI `MAXPATHLEN`. Keep the Rust array length checked against libc's
// declaration without using an unchecked integer cast.
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/sigsafe/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};

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
3 changes: 2 additions & 1 deletion crates/sigsafe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// preload library.
#![cfg(unix)]
#![cfg_attr(not(test), no_std)]
#![deny(clippy::as_conversions)]

mod c_str;
pub mod env;
Expand All @@ -22,7 +23,7 @@ pub mod param;

pub use c_str::{Bytes, CStr, Fat, Thin};
pub use rustix::{
fd::{AsRawFd, BorrowedFd},
fd::{AsRawFd, BorrowedFd, IntoRawFd, OwnedFd},
fs::CWD,
io::{Errno, Errno as Error, Result},
};
Expand Down
Loading