From c241931303448f9449051ea2080c1b65561066f2 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 2 Aug 2026 09:39:04 +0900 Subject: feat(php-shim): implement syscall-backed functions via the nix crate Several shim functions were left as todo!() because the standard library exposes no equivalent and no syscall crate was available. Adding nix unblocks them: - proc_open now wires descriptors beyond stderr, creating the pipe itself and installing the child end with dup2(2) from pre_exec - proc_terminate and posix_kill deliver arbitrary signals via kill(2) - get_current_user reports the owner of the running executable - php_uname answers every mode from uname(2) instead of only "s" and "r" It also closes gaps that were previously approximated: - fstat stats the stdio streams and pipes rather than reporting failure - touch stamps mtime/atime on an existing path, including directories - is_writable/is_executable use access(2) instead of permission bits - umask falls back to the read-modify-write umask(2) off Linux The hand-written repr(C) structs and extern "C" declarations for getpwuid, utime, statvfs, fcntl and select are replaced by their nix wrappers, which in turn lets disk_free_space drop its Linux-only cfg. cli_set_process_title, setproctitle and the pcntl_signal pair stay as todo!(): the first two need access to the process's own argv block, and the latter two depend on the signal-handling subsystem rather than on sigaction(2) itself. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe-php-shim/src/stream.rs | 122 +++++++++++----------------------- 1 file changed, 38 insertions(+), 84 deletions(-) (limited to 'crates/shirabe-php-shim/src/stream.rs') diff --git a/crates/shirabe-php-shim/src/stream.rs b/crates/shirabe-php-shim/src/stream.rs index ce6048c4..b977ce4b 100644 --- a/crates/shirabe-php-shim/src/stream.rs +++ b/crates/shirabe-php-shim/src/stream.rs @@ -258,52 +258,13 @@ fn build_meta_data( map } -// libc is already linked into every binary, so these can be declared directly without an extra -// crate (mirrors the `getuid`/`geteuid` declarations in process.rs). -const F_GETFL: i32 = 3; -const F_SETFL: i32 = 4; -const O_NONBLOCK: i32 = 0o4000; -const FD_SETSIZE: usize = 1024; - -unsafe extern "C" { - fn fcntl(fd: i32, cmd: i32, ...) -> i32; - fn select( - nfds: i32, - readfds: *mut FdSet, - writefds: *mut FdSet, - exceptfds: *mut FdSet, - timeout: *mut TimeVal, - ) -> i32; -} - -#[repr(C)] -struct TimeVal { - tv_sec: i64, - tv_usec: i64, -} - -// `fd_set` is a bitmap of `FD_SETSIZE` bits laid out as an array of `long` words. -#[repr(C)] -struct FdSet { - fds_bits: [i64; FD_SETSIZE / (8 * std::mem::size_of::())], -} - -impl FdSet { - fn zero() -> Self { - FdSet { - fds_bits: [0; FD_SETSIZE / (8 * std::mem::size_of::())], - } - } - - fn set(&mut self, fd: i32) { - let bits = 8 * std::mem::size_of::(); - self.fds_bits[fd as usize / bits] |= 1i64 << (fd as usize % bits); - } - - fn is_set(&self, fd: i32) -> bool { - let bits = 8 * std::mem::size_of::(); - (self.fds_bits[fd as usize / bits] & (1i64 << (fd as usize % bits))) != 0 - } +/// Wraps a live descriptor obtained from `PhpResource::raw_fd` for the duration of one syscall. +/// +/// # Safety +/// +/// The caller must keep the owning resource alive and unclosed for the lifetime of the result. +unsafe fn borrow_fd<'a>(fd: std::os::fd::RawFd) -> std::os::fd::BorrowedFd<'a> { + unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) } } /// PHP `stream_set_blocking()`: toggle `O_NONBLOCK` on the resource's underlying fd via `fcntl(2)`. @@ -312,17 +273,19 @@ pub fn stream_set_blocking(resource: &PhpResource, enable: bool) -> bool { let Some(fd) = resource.raw_fd() else { return false; }; - let flags = unsafe { fcntl(fd, F_GETFL) }; - if flags < 0 { + // SAFETY: `resource` owns the descriptor and outlives both calls below. + let fd = unsafe { borrow_fd(fd) }; + let Ok(flags) = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_GETFL) else { return false; - } + }; + let flags = nix::fcntl::OFlag::from_bits_truncate(flags); // `enable` means blocking, i.e. clear O_NONBLOCK. let new_flags = if enable { - flags & !O_NONBLOCK + flags & !nix::fcntl::OFlag::O_NONBLOCK } else { - flags | O_NONBLOCK + flags | nix::fcntl::OFlag::O_NONBLOCK }; - unsafe { fcntl(fd, F_SETFL, new_flags) >= 0 } + nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_SETFL(new_flags)).is_ok() } /// PHP `stream_select`. Returns the number of changed streams, or `None` for the PHP `false` @@ -336,22 +299,17 @@ pub fn stream_select( seconds: i64, microseconds: Option, ) -> Option { - let mut readfds = FdSet::zero(); - let mut writefds = FdSet::zero(); - let mut exceptfds = FdSet::zero(); - let mut nfds = 0i32; + let mut readfds = nix::sys::select::FdSet::new(); + let mut writefds = nix::sys::select::FdSet::new(); + let mut exceptfds = nix::sys::select::FdSet::new(); // Resources without an fd (in-memory streams, process handles) cannot be waited on; PHP would // emit a warning for them. We skip them here, leaving them out of the ready set. - let mut prepare = |set: &mut FdSet, resources: &[PhpResource]| { + let prepare = |set: &mut nix::sys::select::FdSet<'static>, resources: &[PhpResource]| { for resource in resources { - if let Some(fd) = resource.raw_fd() - && (fd as usize) < FD_SETSIZE - { - set.set(fd); - if fd + 1 > nfds { - nfds = fd + 1; - } + if let Some(fd) = resource.raw_fd() { + // SAFETY: the resources stay alive and unclosed for the whole select(2) call. + set.insert(unsafe { borrow_fd(fd) }); } } }; @@ -359,31 +317,27 @@ pub fn stream_select( prepare(&mut writefds, write); prepare(&mut exceptfds, except); - let mut timeout = TimeVal { - tv_sec: seconds, - tv_usec: microseconds.unwrap_or(0), - }; + let mut timeout = nix::sys::time::TimeVal::new(seconds, microseconds.unwrap_or(0)); - let ret = unsafe { - select( - nfds, - &mut readfds, - &mut writefds, - &mut exceptfds, - &mut timeout, - ) - }; - - if ret < 0 { + let ret = nix::sys::select::select( + None, + &mut readfds, + &mut writefds, + &mut exceptfds, + &mut timeout, + ); + let ret = match ret { + Ok(ret) => ret, // select(2) failed (e.g. EINTR). PHP returns false. - return None; - } + Err(_) => return None, + }; // Narrow each array in place to the resources whose fd is still set in the result bitmap. - let narrow = |set: &FdSet, resources: &mut Vec| { + let narrow = |set: &nix::sys::select::FdSet<'static>, resources: &mut Vec| { resources.retain(|resource| match resource.raw_fd() { - Some(fd) if (fd as usize) < FD_SETSIZE => set.is_set(fd), - _ => false, + // SAFETY: as above, the resource still owns the descriptor. + Some(fd) => set.contains(unsafe { borrow_fd(fd) }), + None => false, }); }; narrow(&readfds, read); -- cgit v1.3.1