diff --git a/Cargo.lock b/Cargo.lock index 10f4e533..549e2cb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1390,9 +1390,10 @@ version = "0.0.0" dependencies = [ "base64", "ctor", + "memfd", "memmap2", + "nix 0.31.2", "passfd", - "rustix", "subprocess_test", "tokio", "tokio-util", @@ -2000,6 +2001,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + [[package]] name = "memmap2" version = "0.9.11" diff --git a/Cargo.toml b/Cargo.toml index 8c242060..ab21d022 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,7 @@ jsonc-parser = { version = "0.32.0", features = ["serde"] } libc = "0.2.185" libtest-mimic = "0.8.2" memmap2 = "0.9.11" +memfd = "0.6.5" monostate = "1.0.2" napi = "3" napi-build = "2" @@ -116,7 +117,6 @@ ref-cast = "1.0.24" regex = "1.11.3" rusqlite = "0.39.0" rustc-hash = "2.1.1" -rustix = "1.1" # SeccompAction::UserNotif (SECCOMP_RET_USER_NOTIF) was added after the latest published release (v0.5.0) seccompiler = { git = "https://github.com/rust-vmm/seccompiler", rev = "08587106340b8e3cb361c7561411510039436857" } serde = "1.0.219" diff --git a/crates/fspy_shm/Cargo.toml b/crates/fspy_shm/Cargo.toml index 5ed62d81..82f571b8 100644 --- a/crates/fspy_shm/Cargo.toml +++ b/crates/fspy_shm/Cargo.toml @@ -9,8 +9,9 @@ rust-version.workspace = true [target.'cfg(target_os = "linux")'.dependencies] memmap2 = { workspace = true } +memfd = { workspace = true } passfd = { workspace = true, features = ["async"] } -rustix = { workspace = true, features = ["fs", "net", "process"] } +nix = { workspace = true, features = ["fs", "socket", "user"] } tokio = { workspace = true, features = ["macros", "net", "rt", "time"] } tokio-util = { workspace = true } tracing = { workspace = true } @@ -19,7 +20,7 @@ uuid = { workspace = true, features = ["v4"] } [target.'cfg(target_os = "macos")'.dependencies] base64 = { workspace = true } memmap2 = { workspace = true } -rustix = { workspace = true, features = ["fs", "shm"] } +nix = { workspace = true, features = ["fs", "mman"] } uuid = { workspace = true, features = ["v4"] } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/fspy_shm/src/linux/broker.rs b/crates/fspy_shm/src/linux/broker.rs index fd539b47..484f3e23 100644 --- a/crates/fspy_shm/src/linux/broker.rs +++ b/crates/fspy_shm/src/linux/broker.rs @@ -9,15 +9,14 @@ use std::{ sync::Arc, }; -use passfd::{FdPassingExt as SyncFdPassingExt, tokio::FdPassingExt as AsyncFdPassingExt}; -use rustix::{ - io::Errno, - net::{ - AddressFamily, SocketAddrUnix, SocketFlags, SocketType, connect, socket_with, - sockopt::socket_peercred, +use nix::{ + sys::socket::{ + AddressFamily, SockFlag, SockType, UnixAddr, connect, getsockopt, socket, + sockopt::PeerCredentials, }, - process::{Uid, geteuid}, + unistd::{Uid, geteuid}, }; +use passfd::{FdPassingExt as SyncFdPassingExt, tokio::FdPassingExt as AsyncFdPassingExt}; use tokio::{net::UnixListener, task::JoinSet}; use tokio_util::sync::{CancellationToken, DropGuard}; use tracing::{debug, warn}; @@ -64,14 +63,14 @@ async fn run_broker( // https://man7.org/linux/man-pages/man7/unix.7.html // D-Bus prefers the same mechanism because it requires no peer cooperation: // https://gitlab.freedesktop.org/dbus/dbus/-/blob/958bf9db2100553bcd2fe2a854e1ebb42e886054/dbus/dbus-sysdeps-unix.c#L2296-2303 - let credentials = match socket_peercred(&client) { + let credentials = match getsockopt(&client, PeerCredentials) { Ok(credentials) => credentials, Err(error) => { debug!("shared-memory broker failed to read peer credentials: {error}"); continue; } }; - if credentials.uid != owner_uid { + if credentials.uid() != owner_uid.as_raw() { debug!("shared-memory broker rejected a client owned by another user"); continue; } @@ -93,15 +92,14 @@ async fn run_broker( } } -pub(super) fn request_memfd(id: &str) -> rustix::io::Result { +pub(super) fn request_memfd(id: &str) -> io::Result { // Prevent the broker connection from leaking into later execs. - let socket = socket_with(AddressFamily::UNIX, SocketType::STREAM, SocketFlags::CLOEXEC, None)?; - let address = SocketAddrUnix::new_abstract_name(id.as_bytes())?; - connect(&socket, &address)?; + let socket = socket(AddressFamily::Unix, SockType::Stream, SockFlag::SOCK_CLOEXEC, None)?; + let address = UnixAddr::new_abstract(id.as_bytes())?; + connect(socket.as_raw_fd(), &address)?; // `SCM_RIGHTS` does not preserve descriptor flags; `passfd` sets // `FD_CLOEXEC` on the received descriptor before returning it. - let descriptor = SyncFdPassingExt::recv_fd(&socket.as_raw_fd()) - .map_err(|error| Errno::from_io_error(&error).unwrap_or(Errno::IO))?; + let descriptor = SyncFdPassingExt::recv_fd(&socket.as_raw_fd())?; // SAFETY: passfd returns a newly received descriptor owned by the caller. Ok(unsafe { OwnedFd::from_raw_fd(descriptor) }) } @@ -110,10 +108,8 @@ pub(super) fn request_memfd(id: &str) -> rustix::io::Result { mod tests { use std::{io, process::Command}; - use rustix::{ - fs::{SealFlags, fcntl_get_seals}, - io::{FdFlags, fcntl_getfd}, - }; + use memfd::MemfdOptions; + use nix::fcntl::{FcntlArg, FdFlag, SealFlag, fcntl}; use subprocess_test::command_for_fn; use super::*; @@ -152,9 +148,12 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn broker_stops_when_guard_drops() { - let memfd = rustix::fs::memfd_create("shared-memory-test", rustix::fs::MemfdFlags::CLOEXEC) - .map_err(io::Error::from) - .unwrap(); + let memfd: OwnedFd = MemfdOptions::new() + .close_on_exec(true) + .create("shared-memory-test") + .unwrap() + .into_file() + .into(); let (_id, service, guard) = new(memfd).unwrap(); let broker = tokio::spawn(service); tokio::task::yield_now().await; @@ -172,10 +171,13 @@ mod tests { let owner = crate::create(4096).unwrap(); let id = owner.id().to_owned(); let descriptor = request_memfd_blocking(id).await.unwrap(); - assert!(fcntl_getfd(&descriptor).unwrap().contains(FdFlags::CLOEXEC)); + assert!( + FdFlag::from_bits_retain(fcntl(&descriptor, FcntlArg::F_GETFD).unwrap()) + .contains(FdFlag::FD_CLOEXEC) + ); assert_eq!( - fcntl_get_seals(&descriptor).unwrap(), - SealFlags::GROW | SealFlags::SHRINK | SealFlags::SEAL + SealFlag::from_bits_retain(fcntl(&descriptor, FcntlArg::F_GET_SEALS).unwrap()), + SealFlag::F_SEAL_GROW | SealFlag::F_SEAL_SHRINK | SealFlag::F_SEAL_SEAL ); } @@ -223,7 +225,7 @@ mod tests { assert!(open_blocking(id).await.is_ok()); } - async fn request_memfd_blocking(id: String) -> rustix::io::Result { + async fn request_memfd_blocking(id: String) -> io::Result { tokio::task::spawn_blocking(move || request_memfd(&id)).await.unwrap() } diff --git a/crates/fspy_shm/src/linux/mod.rs b/crates/fspy_shm/src/linux/mod.rs index 298ee0f5..dc840141 100644 --- a/crates/fspy_shm/src/linux/mod.rs +++ b/crates/fspy_shm/src/linux/mod.rs @@ -2,10 +2,11 @@ mod broker; -use std::{io, slice}; +use std::{io, os::fd::OwnedFd, slice}; +use memfd::{FileSeal, MemfdOptions}; use memmap2::{MmapOptions, MmapRaw}; -use rustix::fs::{MemfdFlags, SealFlags, fcntl_add_seals, fstat, ftruncate, memfd_create}; +use nix::sys::stat::fstat; use tokio_util::sync::DropGuard; /// An owned Linux shared-memory mapping. @@ -33,13 +34,19 @@ pub fn create(size: usize) -> io::Result { let size_u64 = valid_size(size)?; // Prevent the descriptor from leaking across exec while permitting the // size and seal set to be locked after initialization. - let memfd = - memfd_create("vite-task-shared-memory", MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING)?; - ftruncate(&memfd, size_u64)?; + let memfd = MemfdOptions::new() + .allow_sealing(true) + .close_on_exec(true) + .create("vite-task-shared-memory") + .map_err(memfd_error)?; + memfd.as_file().set_len(size_u64)?; // Keep the initialized size fixed and prevent removal of these seals; // writes through the shared mapping remain allowed. - fcntl_add_seals(&memfd, SealFlags::GROW | SealFlags::SHRINK | SealFlags::SEAL)?; - let mapping = MmapOptions::new().len(size).map_raw(&memfd)?; + memfd + .add_seals(&[FileSeal::SealGrow, FileSeal::SealShrink, FileSeal::SealSeal]) + .map_err(memfd_error)?; + let mapping = MmapOptions::new().len(size).map_raw(memfd.as_file())?; + let memfd: OwnedFd = memfd.into_file().into(); let (id, service, guard) = broker::new(memfd)?; runtime.spawn(service); @@ -79,6 +86,14 @@ fn valid_size(size: usize) -> io::Result { .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size exceeds u64")) } +fn memfd_error(error: memfd::Error) -> io::Error { + match error { + memfd::Error::Create(error) + | memfd::Error::AddSeals(error) + | memfd::Error::GetSeals(error) => error, + } +} + #[expect(clippy::len_without_is_empty, reason = "shared-memory mappings are always non-empty")] impl Shm { /// Returns this mapping's opaque broker identifier. diff --git a/crates/fspy_shm/src/macos/mod.rs b/crates/fspy_shm/src/macos/mod.rs index 0e5f20b7..f95cf63b 100644 --- a/crates/fspy_shm/src/macos/mod.rs +++ b/crates/fspy_shm/src/macos/mod.rs @@ -1,13 +1,17 @@ #![doc = include_str!("README.md")] -use std::{io, slice}; +use std::{io, os::fd::AsFd, slice}; use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use memmap2::{MmapOptions, MmapRaw}; -use rustix::{ - fs::{Mode, fstat, ftruncate}, - io::Errno, - shm::{self, OFlags}, +use nix::{ + errno::Errno, + fcntl::{FcntlArg, FdFlag, OFlag, fcntl}, + sys::{ + mman::{shm_open, shm_unlink}, + stat::{Mode, fstat}, + }, + unistd::ftruncate, }; use uuid::Uuid; @@ -33,31 +37,34 @@ pub fn create(size: usize) -> io::Result { "shared-memory size must be nonzero", )); } - let size_u64 = u64::try_from(size).map_err(|_| { - io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size exceeds u64") + let size_i64 = i64::try_from(size).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size exceeds supported range") })?; loop { let id = new_id(); - // `rustix::shm::open` sets `FD_CLOEXEC` on the returned descriptor. - let fd = match shm::open( + let fd = match shm_open( id.as_str(), - OFlags::CREATE | OFlags::EXCL | OFlags::RDWR, - Mode::RUSR | Mode::WUSR, + OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_RDWR, + Mode::S_IRUSR | Mode::S_IWUSR, ) { Ok(fd) => fd, - Err(Errno::EXIST) => continue, + Err(Errno::EEXIST) => continue, Err(error) => return Err(error.into()), }; - if let Err(error) = ftruncate(&fd, size_u64) { - let _ = shm::unlink(id.as_str()); + if let Err(error) = ensure_cloexec(&fd) { + let _ = shm_unlink(id.as_str()); + return Err(error.into()); + } + if let Err(error) = ftruncate(&fd, size_i64) { + let _ = shm_unlink(id.as_str()); return Err(error.into()); } let mapping = match MmapOptions::new().len(size).map_raw(&fd) { Ok(mapping) => mapping, Err(error) => { - let _ = shm::unlink(id.as_str()); + let _ = shm_unlink(id.as_str()); return Err(error); } }; @@ -72,8 +79,8 @@ pub fn create(size: usize) -> io::Result { /// /// Returns an error if the mapping is unavailable. pub fn open(id: &str) -> io::Result { - // `rustix::shm::open` sets `FD_CLOEXEC` on the returned descriptor. - let fd = shm::open(id, OFlags::RDWR, Mode::empty()).map_err(io::Error::from)?; + let fd = shm_open(id, OFlag::O_RDWR, Mode::empty())?; + ensure_cloexec(&fd)?; // If another process shrinks the object before `mmap`, `mmap` returns an // error. If it resizes the object after `mmap`, `open` does not access the // mapped pages. A concurrent resize cannot make `open` access invalid memory. @@ -92,10 +99,19 @@ fn new_id() -> String { format!("{NAME_PREFIX}{}", URL_SAFE_NO_PAD.encode(Uuid::new_v4().as_bytes())) } +// macOS rejects O_CLOEXEC for shm_open, so preserve the descriptor guarantee via fcntl. +fn ensure_cloexec(fd: &Fd) -> nix::Result<()> { + let flags = FdFlag::from_bits_retain(fcntl(fd, FcntlArg::F_GETFD)?); + if !flags.contains(FdFlag::FD_CLOEXEC) { + fcntl(fd, FcntlArg::F_SETFD(flags | FdFlag::FD_CLOEXEC))?; + } + Ok(()) +} + impl Drop for Shm { fn drop(&mut self) { if self.owner { - let _ = shm::unlink(self.id.as_str()); + let _ = shm_unlink(self.id.as_str()); } } }