Skip to content

Commit 82b5572

Browse files
wan9chicodex
andcommitted
feat(sigsafe): resolve macOS descriptor paths
Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent 5fbbf64 commit 82b5572

8 files changed

Lines changed: 100 additions & 18 deletions

File tree

crates/fspy_preload_unix/src/client/convert.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#[cfg(target_os = "macos")]
2-
use std::os::unix::ffi::OsStrExt as _;
31
use std::{ffi::CStr, os::fd::RawFd};
42

53
use allocator_api2::{alloc::Allocator, vec::Vec};
@@ -62,15 +60,17 @@ fn get_fd_path<A: Allocator>(fd: RawFd, allocator: A) -> nix::Result<Option<Abso
6260
if fd == libc::AT_FDCWD {
6361
return getcwd(allocator).map(Some);
6462
}
65-
let mut path = std::path::PathBuf::new();
66-
match nix::fcntl::fcntl(
67-
// SAFETY: fd is a valid file descriptor provided by the caller, and the borrow does not outlive this function call
68-
unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) },
69-
nix::fcntl::FcntlArg::F_GETPATH(&mut path),
70-
) {
71-
Ok(_) => Ok(Some(AbsolutePath::new_in(path.as_os_str().as_bytes(), allocator))),
72-
Err(nix::Error::EBADF | nix::Error::ENOENT) => Ok(None), // invalid fd or no such file (Most likely a stdio fd)
73-
Err(e) => Err(e),
63+
64+
// SAFETY: the descriptor remains borrowed for this call.
65+
let fd = unsafe { sigsafe::BorrowedFd::borrow_raw(fd) };
66+
match sigsafe_alloc::fs::fcntl_getpath(allocator, fd) {
67+
Ok(path) => {
68+
// `F_GETPATH` does not return a length. Count at this caller before
69+
// converting its allocation into the returned path.
70+
Ok(Some(AbsolutePath::from_bytes(path.count().into_bytes())))
71+
}
72+
Err(sigsafe::Errno::BADF | sigsafe::Errno::NOENT) => Ok(None),
73+
Err(errno) => Err(nix::errno::Errno::from_raw(errno.raw_os_error())),
7474
}
7575
}
7676

crates/sigsafe/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ rustix can be built with a libc backend instead of raw syscalls, and anything in
2929
Functions whose rustix implementation already meets the rules are re-exposed as-is; being listed in a module here is what marks a call as allowed, and the backend check above is what keeps that true.
3030

3131
- `mm` — anonymous memory mappings: `mmap_anonymous`, `munmap`.
32-
- `fs` — caller-buffer filesystem operations: `getcwd`.
32+
- `fs` — caller-buffer filesystem operations: `getcwd`, plus macOS `fcntl_getpath`.
3333
- `param``page_size`.
3434

3535
Allocation without malloc lives in [`sigsafe_alloc`](../sigsafe_alloc).

crates/sigsafe/src/fs/mac.rs

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

33
use rustix::{
4-
fd::{AsFd as _, AsRawFd as _, BorrowedFd, FromRawFd as _, OwnedFd},
4+
fd::{AsFd as _, AsRawFd as _, FromRawFd as _, OwnedFd},
55
fs::{Mode, OFlags, fstat, stat},
66
};
77

8-
use crate::{CStr, Errno, Fat, Result, Thin};
8+
use crate::{BorrowedFd, CStr, Errno, Fat, Result, Thin};
99

1010
pub(super) const PATH_MAX: usize = libc::PATH_MAX as usize;
1111

@@ -25,7 +25,16 @@ fn open<R>(path: CStr<'_, R>, flags: OFlags, mode: Mode) -> Result<OwnedFd> {
2525
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
2626
}
2727

28-
fn fcntl_getpath<'buf>(
28+
/// Gets the path associated with `fd`.
29+
///
30+
/// `F_GETPATH` writes a NUL-terminated path into its `MAXPATHLEN` buffer but
31+
/// does not report the length, so this function returns [`CStr<Thin>`] without
32+
/// scanning for the terminator.
33+
///
34+
/// # Errors
35+
///
36+
/// Returns the error reported by `fcntl`.
37+
pub fn fcntl_getpath<'buf>(
2938
fd: BorrowedFd<'_>,
3039
buf: &'buf mut [MaybeUninit<u8>; PATH_MAX],
3140
) -> Result<CStr<'buf, Thin>> {

crates/sigsafe/src/fs/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use linux as imp;
1515
pub use linux::readlink;
1616
#[cfg(target_os = "macos")]
1717
use mac as imp;
18+
#[cfg(target_os = "macos")]
19+
pub use mac::fcntl_getpath;
1820

1921
/// The platform's maximum pathname size, including the terminating NUL.
2022
pub const PATH_MAX: usize = imp::PATH_MAX;

crates/sigsafe/src/fs/tests.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ fn getcwd_rejects_an_empty_buffer() {
2626
assert!(matches!(getcwd(&mut []), Err(Errno::RANGE)));
2727
}
2828

29+
#[cfg(target_os = "macos")]
30+
#[test]
31+
fn fcntl_getpath_returns_descriptor_path() {
32+
use rustix::{
33+
fd::AsFd as _,
34+
fs::{Mode, OFlags, open},
35+
};
36+
37+
let root = open(c"/", OFlags::RDONLY, Mode::empty()).unwrap();
38+
let mut buf = [MaybeUninit::uninit(); super::PATH_MAX];
39+
40+
let path = super::fcntl_getpath(root.as_fd(), &mut buf).unwrap().count();
41+
42+
assert_eq!(path.as_bytes_with_nul(), b"/\0");
43+
}
44+
2945
#[cfg(target_os = "linux")]
3046
#[test]
3147
fn readlink_returns_the_initialized_target() {

crates/sigsafe/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ pub mod mm;
2020
pub mod param;
2121

2222
pub use c_str::{CStr, Fat, Thin};
23-
pub use rustix::io::{Errno, Result};
23+
pub use rustix::{
24+
fd::BorrowedFd,
25+
io::{Errno, Result},
26+
};
2427

2528
// Compile-time proof that rustix uses its raw-syscall backend (`linux_raw`)
2629
// on Linux — and with it, that no call in this crate goes through libc there.

crates/sigsafe_alloc/src/c_string.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use core::mem::MaybeUninit;
22

33
use allocator_api2::{alloc::Allocator, boxed::Box, vec::Vec};
4-
use sigsafe::Fat;
4+
use sigsafe::{CStr, Fat, Thin};
55

66
/// An allocator-backed owned C string.
77
pub struct CString<R, A: Allocator> {
@@ -27,6 +27,23 @@ impl<R, A: Allocator> CString<R, A> {
2727
}
2828
}
2929

30+
impl<A: Allocator> CString<Thin, A> {
31+
/// Returns a thin borrowed view of this C string.
32+
#[must_use]
33+
pub fn as_c_str(&self) -> CStr<'_, Thin> {
34+
// SAFETY: upheld by the constructor; `self` owns the storage.
35+
unsafe { CStr::from_ptr(self.bytes.as_ptr().cast()) }
36+
}
37+
38+
/// Counts through the terminating NUL and returns a length-retaining C
39+
/// string using the same allocation.
40+
#[must_use]
41+
pub fn count(self) -> CString<Fat, A> {
42+
let repr = self.as_c_str().count().into_repr();
43+
CString { bytes: self.bytes, repr }
44+
}
45+
}
46+
3047
impl<A: Allocator> CString<Fat, A> {
3148
/// Consumes this C string and returns its bytes without the terminating NUL.
3249
///

crates/sigsafe_alloc/src/fs.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
use allocator_api2::vec::Vec;
55
use allocator_api2::{alloc::Allocator, boxed::Box};
66
#[cfg(target_os = "linux")]
7-
use sigsafe::{CStr, Thin};
7+
use sigsafe::CStr;
8+
#[cfg(any(target_os = "linux", target_os = "macos"))]
9+
use sigsafe::Thin;
810
use sigsafe::{Errno, Fat, Result};
911

1012
use crate::CString;
@@ -57,6 +59,26 @@ pub fn readlink<A: Allocator>(allocator: A, path: CStr<'_, Thin>) -> Result<Vec<
5759
}
5860
}
5961

62+
/// Returns the path associated with `fd`, allocated with `allocator`.
63+
///
64+
/// The returned C string remains thin because `F_GETPATH` reports no length.
65+
///
66+
/// # Errors
67+
///
68+
/// Returns [`Errno::NOMEM`] if storage cannot be allocated, or the error from
69+
/// [`sigsafe::fs::fcntl_getpath`].
70+
#[cfg(target_os = "macos")]
71+
pub fn fcntl_getpath<A: Allocator>(
72+
allocator: A,
73+
fd: sigsafe::BorrowedFd<'_>,
74+
) -> Result<CString<Thin, A>> {
75+
let mut bytes = path_buffer(allocator)?;
76+
let repr = sigsafe::fs::fcntl_getpath(fd, &mut bytes)?.into_repr();
77+
78+
// SAFETY: `fcntl_getpath` initialized the C string described by `repr`.
79+
Ok(unsafe { CString::from_buffer_unchecked(bytes, repr) })
80+
}
81+
6082
fn path_buffer<A: Allocator>(
6183
allocator: A,
6284
) -> Result<Box<[core::mem::MaybeUninit<u8>; sigsafe::fs::PATH_MAX], A>> {
@@ -81,6 +103,19 @@ mod tests {
81103
assert_eq!(path.as_slice(), &expected.as_bytes_with_nul()[..expected.len_with_nul() - 1]);
82104
}
83105

106+
#[cfg(target_os = "macos")]
107+
#[test]
108+
fn fcntl_getpath_allocates_a_thin_c_string() {
109+
use std::{fs::File, os::fd::AsRawFd as _};
110+
111+
let root = File::open("/").unwrap();
112+
// SAFETY: `root` remains open for the call.
113+
let root = unsafe { sigsafe::BorrowedFd::borrow_raw(root.as_raw_fd()) };
114+
let path = super::fcntl_getpath(Global, root).unwrap();
115+
116+
assert_eq!(path.as_c_str().count().as_bytes_with_nul(), b"/\0");
117+
}
118+
84119
#[cfg(target_os = "linux")]
85120
#[test]
86121
fn readlink_allocates_the_complete_target() {

0 commit comments

Comments
 (0)