Skip to content

Commit 8676c07

Browse files
wan9chicodex
andcommitted
feat(fspy-nostd): add Unix owned-file access
Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent 367f651 commit 8676c07

7 files changed

Lines changed: 122 additions & 24 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/fspy_nostd/src/fs/linux.rs‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,45 @@
11
use core::{mem::MaybeUninit, slice};
22

3-
use crate::{AsRawFd as _, BorrowedFd, CStr, Error, Fat, Result, Thin};
3+
use rustix::fd::FromRawFd as _;
4+
5+
use crate::{
6+
AsRawFd as _, BorrowedFd, CStr, Error, Fat, OwnedFd, Result, Thin,
7+
fs::{Mode, OFlags},
8+
};
49

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

13+
fn syscall_fd(fd: BorrowedFd<'_>) -> Result<usize> {
14+
let fd = isize::try_from(fd.as_raw_fd()).map_err(|_| Error::OVERFLOW)?;
15+
Ok(fd.cast_unsigned())
16+
}
17+
18+
#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")]
19+
pub(super) fn openat<R>(
20+
dirfd: BorrowedFd<'_>,
21+
path: CStr<'_, R>,
22+
flags: OFlags,
23+
mode: Mode,
24+
) -> Result<OwnedFd> {
25+
// SAFETY: `dirfd` remains borrowed and `path` is NUL-terminated. The
26+
// kernel receives all four syscall arguments explicitly.
27+
let fd = unsafe {
28+
syscalls::syscall4(
29+
syscalls::Sysno::openat,
30+
syscall_fd(dirfd)?,
31+
path.as_ptr().addr(),
32+
usize::try_from(flags.bits()).map_err(|_| Error::INVAL)?,
33+
usize::try_from(mode.bits()).map_err(|_| Error::INVAL)?,
34+
)
35+
}
36+
.map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?;
37+
let fd = i32::try_from(fd).map_err(|_| Error::OVERFLOW)?;
38+
39+
// SAFETY: a successful `openat` returns a new owned descriptor.
40+
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
41+
}
42+
843
/// Reads the target of `path` relative to `dirfd` into `buf`.
944
///
1045
/// The returned bytes borrow the initialized prefix of `buf`. As with the
@@ -27,7 +62,7 @@ pub fn readlinkat<'buf>(
2762
let initialized = unsafe {
2863
syscalls::syscall4(
2964
syscalls::Sysno::readlinkat,
30-
(dirfd.as_raw_fd() as isize).cast_unsigned(),
65+
syscall_fd(dirfd)?,
3166
path.as_ptr().addr(),
3267
buf.as_mut_ptr().addr(),
3368
buf.len(),

‎crates/fspy_nostd/src/fs/mac.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ use rustix::{
77

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

10-
pub(super) const PATH_MAX: usize = libc::PATH_MAX as usize;
10+
// Darwin UAPI `MAXPATHLEN`.
11+
pub(super) const PATH_MAX: usize = 1024;
12+
const _: () = assert!(libc::PATH_MAX == 1024);
1113

1214
#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")]
13-
fn openat<R>(
15+
pub(super) fn openat<R>(
1416
dirfd: BorrowedFd<'_>,
1517
path: CStr<'_, R>,
1618
flags: OFlags,

‎crates/fspy_nostd/src/fs/mod.rs‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
33
use core::mem::MaybeUninit;
44

5-
use crate::{CStr, Fat, Result};
5+
pub use rustix::fs::{Mode, OFlags, fstat};
6+
7+
use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result};
68

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

26+
/// Opens `path` relative to `dirfd` and returns its owned descriptor.
27+
///
28+
/// # Errors
29+
///
30+
/// Returns the error reported by `openat`.
31+
pub fn openat<R>(
32+
dirfd: BorrowedFd<'_>,
33+
path: CStr<'_, R>,
34+
flags: OFlags,
35+
mode: Mode,
36+
) -> Result<OwnedFd> {
37+
imp::openat(dirfd, path, flags, mode)
38+
}
39+
2440
/// Writes the absolute pathname of the current working directory into `buf`.
2541
///
2642
/// The returned C string borrows `buf`, starts at the same address as `buf`,

‎crates/fspy_nostd/src/lib.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ macro_rules! wide_cstr {
6565

6666
#[cfg(unix)]
6767
pub use rustix::{
68-
fd::{AsRawFd, BorrowedFd},
68+
fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd},
6969
fs::CWD,
7070
io::Errno as Error,
7171
};

‎crates/fspy_shm/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ rust-version.workspace = true
1111
memmap2 = { workspace = true }
1212
uuid = { workspace = true, features = ["v4"] }
1313

14+
[target.'cfg(unix)'.dependencies]
15+
fspy_nostd = { workspace = true }
16+
1417
[target.'cfg(target_os = "windows")'.dependencies]
1518
windows-sys = { workspace = true, features = [
1619
"Win32_Foundation",

‎crates/fspy_shm/src/unix.rs‎

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use std::{
66
ffi::OsStr,
77
fs::{self, File, OpenOptions},
88
io,
9-
os::unix::fs::OpenOptionsExt as _,
9+
num::NonZeroUsize,
10+
os::unix::{ffi::OsStrExt as _, fs::OpenOptionsExt as _, io::IntoRawFd as _},
1011
path::PathBuf,
1112
};
1213

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

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

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

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

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

108+
fn open_file(path: &OsStr) -> io::Result<fspy_nostd::OwnedFd> {
109+
let mut buf = [0_u8; fspy_nostd::fs::PATH_MAX];
110+
let path = copy_path(path, &mut buf)?;
111+
fspy_nostd::fs::openat(
112+
fspy_nostd::CWD,
113+
path,
114+
fspy_nostd::fs::OFlags::RDWR | fspy_nostd::fs::OFlags::CLOEXEC,
115+
fspy_nostd::fs::Mode::empty(),
116+
)
117+
.map_err(error_to_io)
118+
}
119+
120+
fn copy_path<'buf>(
121+
path: &OsStr,
122+
buf: &'buf mut [u8; fspy_nostd::fs::PATH_MAX],
123+
) -> io::Result<fspy_nostd::CStr<'buf, fspy_nostd::Fat>> {
124+
let bytes = path.as_bytes();
125+
if bytes.contains(&0) {
126+
return Err(io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL"));
127+
}
128+
129+
let len_with_nul = bytes.len().checked_add(1).ok_or(io::ErrorKind::InvalidInput)?;
130+
let initialized = buf.get_mut(..len_with_nul).ok_or_else(|| {
131+
io::Error::from_raw_os_error(fspy_nostd::Error::NAMETOOLONG.raw_os_error())
132+
})?;
133+
initialized[..bytes.len()].copy_from_slice(bytes);
134+
initialized[bytes.len()] = 0;
135+
136+
// SAFETY: the copied path contains no NUL, followed by the terminator set
137+
// above, and the returned view borrows the initialized buffer prefix.
138+
Ok(unsafe { fspy_nostd::CStr::from_units_with_nul_unchecked(initialized) })
139+
}
140+
141+
fn error_to_io(error: fspy_nostd::Error) -> io::Error {
142+
io::Error::from_raw_os_error(error.raw_os_error())
143+
}
144+
145+
fn into_nostd_fd(file: File) -> fspy_nostd::OwnedFd {
146+
let fd = file.into_raw_fd();
147+
// SAFETY: ownership of `file`'s descriptor transfers without closing or
148+
// duplicating it.
149+
unsafe { fspy_nostd::FromRawFd::from_raw_fd(fd) }
150+
}
151+
112152
impl Drop for ShmKeeper {
113153
fn drop(&mut self) {
114154
let _ = fs::remove_file(&self.path);
@@ -131,7 +171,8 @@ impl ShmHandle {
131171
///
132172
/// Returns an error if the mapping cannot be established.
133173
pub fn map(&self) -> io::Result<Mapping> {
134-
Ok(Mapping { raw: MmapOptions::new().len(self.size).map_raw(&self.file)? })
174+
let file = fspy_nostd::AsRawFd::as_raw_fd(&self.file);
175+
Ok(Mapping { raw: MmapOptions::new().len(self.size.get()).map_raw(file)? })
135176
}
136177
}
137178

0 commit comments

Comments
 (0)