diff options
Diffstat (limited to 'crates/shirabe-php-shim')
| -rw-r--r-- | crates/shirabe-php-shim/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/fs.rs | 189 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/lib.rs | 8 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/process.rs | 303 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/runtime.rs | 44 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/stream.rs | 122 |
6 files changed, 388 insertions, 279 deletions
diff --git a/crates/shirabe-php-shim/Cargo.toml b/crates/shirabe-php-shim/Cargo.toml index 2b9b392a..82f3daa9 100644 --- a/crates/shirabe-php-shim/Cargo.toml +++ b/crates/shirabe-php-shim/Cargo.toml @@ -12,6 +12,7 @@ fastrand.workspace = true flate2.workspace = true indexmap.workspace = true md5.workspace = true +nix.workspace = true regex.workspace = true regex-macro.workspace = true reqwest.workspace = true diff --git a/crates/shirabe-php-shim/src/fs.rs b/crates/shirabe-php-shim/src/fs.rs index d15751dd..efc9d2be 100644 --- a/crates/shirabe-php-shim/src/fs.rs +++ b/crates/shirabe-php-shim/src/fs.rs @@ -563,13 +563,13 @@ pub fn rewind(stream: &PhpResource) -> bool { /// PHP `fstat()`: the stat array of an open stream, or `None` for `false`-on-failure. pub fn fstat(stream: &PhpResource) -> Option<IndexMap<String, PhpMixed>> { + use std::os::fd::AsFd as _; match stream { - // TODO(phase-d): the stdio streams expose no fd to stat without a syscall crate; report - // failure rather than fabricate fields. - PhpResource::Stdin - | PhpResource::Stdout - | PhpResource::Stderr - | PhpResource::Process(_) => None, + PhpResource::Stdin => fstat_fd(std::io::stdin().as_fd()), + PhpResource::Stdout => fstat_fd(std::io::stdout().as_fd()), + PhpResource::Stderr => fstat_fd(std::io::stderr().as_fd()), + // A process handle is not a stream and has no descriptor of its own. + PhpResource::Process(_) => None, PhpResource::Stream(state) => { let mut state = state.borrow_mut(); if state.closed { @@ -581,13 +581,38 @@ pub fn fstat(stream: &PhpResource) -> Option<IndexMap<String, PhpMixed>> { (m.len(), Some(m)) } StreamBacking::Memory(c) => (c.get_ref().len() as u64, None), - StreamBacking::Pipe(_) => return None, + StreamBacking::Pipe(p) => { + use std::os::fd::{AsRawFd as _, BorrowedFd}; + // SAFETY: the pipe end is owned by the stream state, which is borrowed here. + return fstat_fd(unsafe { BorrowedFd::borrow_raw(p.as_raw_fd()) }); + } }; Some(build_stat_map(size, file_meta.as_ref())) } } } +/// Stats an open descriptor with `fstat(2)`, used for the stdio streams and for pipes, neither of +/// which is backed by a `std::fs::File`. +fn fstat_fd(fd: std::os::fd::BorrowedFd<'_>) -> Option<IndexMap<String, PhpMixed>> { + let st = nix::sys::stat::fstat(fd).ok()?; + Some(stat_fields_map([ + ("dev", st.st_dev as i64), + ("ino", st.st_ino as i64), + ("mode", st.st_mode as i64), + ("nlink", st.st_nlink as i64), + ("uid", st.st_uid as i64), + ("gid", st.st_gid as i64), + ("rdev", st.st_rdev as i64), + ("size", st.st_size as i64), + ("atime", st.st_atime as i64), + ("mtime", st.st_mtime as i64), + ("ctime", st.st_ctime as i64), + ("blksize", st.st_blksize as i64), + ("blocks", st.st_blocks as i64), + ])) +} + // Builds the 13-field PHP stat array (indexed 0..12 and by name). For in-memory streams only // `size` is meaningful; the rest are reported as 0, matching PHP fstat on php://temp. fn build_stat_map(size: u64, file_meta: Option<&std::fs::Metadata>) -> IndexMap<String, PhpMixed> { @@ -624,6 +649,11 @@ fn build_stat_map(size: u64, file_meta: Option<&std::fs::Metadata>) -> IndexMap< ("blocks", 0), ], }; + stat_fields_map(fields) +} + +// PHP stat/fstat/lstat return the 13 fields both by numeric index (0..12) and by name. +fn stat_fields_map(fields: [(&str, i64); 13]) -> IndexMap<String, PhpMixed> { let mut map = IndexMap::new(); for (i, (_, v)) in fields.iter().enumerate() { map.insert(i.to_string(), PhpMixed::Int(*v)); @@ -654,8 +684,7 @@ pub fn fflush(stream: &PhpResource) -> bool { pub fn lstat(_filename: &str) -> Option<IndexMap<String, PhpMixed>> { use std::os::unix::fs::MetadataExt; let m = std::fs::symlink_metadata(_filename).ok()?; - // PHP stat/lstat return the 13 fields both by numeric index (0..12) and by name. - let fields: [(&str, i64); 13] = [ + Some(stat_fields_map([ ("dev", m.dev() as i64), ("ino", m.ino() as i64), ("mode", m.mode() as i64), @@ -669,26 +698,20 @@ pub fn lstat(_filename: &str) -> Option<IndexMap<String, PhpMixed>> { ("ctime", m.ctime()), ("blksize", m.blksize() as i64), ("blocks", m.blocks() as i64), - ]; - let mut map = IndexMap::new(); - for (i, (_, v)) in fields.iter().enumerate() { - map.insert(i.to_string(), PhpMixed::Int(*v)); - } - for (name, v) in &fields { - map.insert(name.to_string(), PhpMixed::Int(*v)); - } - Some(map) + ])) } -pub fn touch(_path: &str) -> bool { - // TODO(phase-d): for an existing file PHP also bumps its mtime/atime to now; std exposes no - // portable utime, so only the create-if-absent case is handled here. - std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(false) - .open(_path) - .is_ok() +/// PHP `touch($path)`: creates the file when it is missing and stamps mtime/atime with the current +/// time. Returns `false` (PHP failure) on error. +pub fn touch(path: &str) -> bool { + if !touch_create(path) { + return false; + } + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default(); + let now = nix::sys::time::TimeVal::new(now.as_secs() as i64, now.subsec_micros() as i64); + nix::sys::stat::utimes(path, &now, &now).is_ok() } pub fn fflush_resource(resource: &PhpResource) { @@ -699,32 +722,28 @@ pub fn fwrite_resource(resource: &PhpResource, data: &str) { fwrite(resource, data, None); } -// libc is already linked into every binary, so `utime` can be declared directly without an extra -// crate (mirrors the `fcntl`/`statvfs` declarations elsewhere). PHP's `touch($path, $mtime, $atime)` -// passes whole seconds, which matches `struct utimbuf`'s `time_t` fields. -#[repr(C)] -struct Utimbuf { - actime: std::os::raw::c_long, - modtime: std::os::raw::c_long, -} - -unsafe extern "C" { - fn utime(path: *const std::os::raw::c_char, times: *const Utimbuf) -> std::os::raw::c_int; +/// PHP's `touch` creates the file first if it does not exist. An existing path is never opened, so +/// directories — which `utimes` stamps just as well — go through untouched. +fn touch_create(path: &str) -> bool { + if std::path::Path::new(path).exists() { + return true; + } + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(false) + .open(path) + .is_ok() } +// PHP's `touch($path, $mtime, $atime)` passes whole seconds. fn touch_impl(path: &str, mtime: i64, atime: i64) -> bool { - let Ok(c_path) = std::ffi::CString::new(path) else { - return false; - }; - // PHP's `touch` creates the file first if it does not exist. - if !std::path::Path::new(path).exists() && !touch(path) { + if !touch_create(path) { return false; } - let times = Utimbuf { - actime: atime as std::os::raw::c_long, - modtime: mtime as std::os::raw::c_long, - }; - unsafe { utime(c_path.as_ptr(), ×) == 0 } + let atime = nix::sys::time::TimeVal::new(atime, 0); + let mtime = nix::sys::time::TimeVal::new(mtime, 0); + nix::sys::stat::utimes(path, &atime, &mtime).is_ok() } /// PHP `touch($path, $mtime)`: sets the modification time (and access time, per PHP, to the same @@ -760,14 +779,8 @@ pub fn file_exists(path: impl AsRef<std::path::Path>) -> bool { path.as_ref().exists() } -// TODO(phase-c): PHP's is_writable() resolves to access(2) with W_OK, honoring the effective -// user/group and ACLs. This std-only approximation only inspects the permission bits, so it can -// diverge for files the current user does not own. Refine with a syscall (libc/rustix) crate later. pub fn is_writable(_path: &str) -> bool { - match std::fs::metadata(_path) { - Ok(meta) => !meta.permissions().readonly(), - Err(_) => false, - } + nix::unistd::access(_path, nix::unistd::AccessFlags::W_OK).is_ok() } pub fn is_readable(_path: &str) -> bool { @@ -785,13 +798,7 @@ pub fn is_readable(_path: &str) -> bool { } pub fn is_executable(_path: &str) -> bool { - use std::os::unix::fs::PermissionsExt; - // TODO(phase-d): like is_writable, this only inspects the permission bits and ignores the - // effective user/group, so it can diverge from PHP's access(2, X_OK) check. - match std::fs::metadata(_path) { - Ok(m) => (m.permissions().mode() & 0o111) != 0, - Err(_) => false, - } + nix::unistd::access(_path, nix::unistd::AccessFlags::X_OK).is_ok() } pub fn is_file(path: impl AsRef<std::path::Path>) -> bool { @@ -940,18 +947,23 @@ pub fn file(_filename: &str, _flags: i64) -> Option<Vec<String>> { } pub fn umask() -> u32 { - // Linux exposes the current umask via /proc/self/status. - // TODO(phase-d): other platforms have no /proc; reading the umask there needs the - // read-modify-write umask(2), which std does not expose (no libc/syscall crate available). - std::fs::read_to_string("/proc/self/status") + // Linux exposes the current umask via /proc/self/status, which reads it without disturbing it. + let from_proc = std::fs::read_to_string("/proc/self/status") .ok() .and_then(|status| { status.lines().find_map(|line| { line.strip_prefix("Umask:") .and_then(|v| u32::from_str_radix(v.trim(), 8).ok()) }) - }) - .unwrap_or(0o022) + }); + if let Some(mask) = from_proc { + return mask; + } + // Elsewhere umask(2) is the only way to read it, and it is read-modify-write: set a value to + // learn the previous one, then put that one back. + let previous = nix::sys::stat::umask(nix::sys::stat::Mode::from_bits_truncate(0o022)); + nix::sys::stat::umask(previous); + previous.bits() as u32 } pub fn mkdir(_pathname: &str, _mode: u32, _recursive: bool) -> bool { @@ -1133,48 +1145,11 @@ pub fn clearstatcache2(_clear_realpath_cache: bool, _filename: &str) { // cache to invalidate. } -// libc is already linked into every binary, so `statvfs` can be declared directly without an extra -// crate (mirrors the `fcntl`/`select` declarations in stream.rs). The layout below matches Linux's -// `struct statvfs`. -#[cfg(target_os = "linux")] -#[repr(C)] -struct Statvfs { - f_bsize: std::os::raw::c_ulong, - f_frsize: std::os::raw::c_ulong, - f_blocks: u64, - f_bfree: u64, - f_bavail: u64, - f_files: u64, - f_ffree: u64, - f_favail: u64, - f_fsid: std::os::raw::c_ulong, - f_flag: std::os::raw::c_ulong, - f_namemax: std::os::raw::c_ulong, - f_spare: [std::os::raw::c_int; 6], -} - -#[cfg(target_os = "linux")] -unsafe extern "C" { - fn statvfs(path: *const std::os::raw::c_char, buf: *mut Statvfs) -> std::os::raw::c_int; -} - /// PHP `disk_free_space()`: the number of available bytes on the filesystem containing `directory`, /// computed via `statvfs(3)` (`f_bavail * f_frsize`). Returns `None` (PHP `false`) on failure. -#[cfg(target_os = "linux")] pub fn disk_free_space(directory: &str) -> Option<f64> { - let c_path = std::ffi::CString::new(directory).ok()?; - let mut buf = std::mem::MaybeUninit::<Statvfs>::uninit(); - let rc = unsafe { statvfs(c_path.as_ptr(), buf.as_mut_ptr()) }; - if rc != 0 { - return None; - } - let buf = unsafe { buf.assume_init() }; - Some(buf.f_bavail as f64 * buf.f_frsize as f64) -} - -#[cfg(not(target_os = "linux"))] -pub fn disk_free_space(_directory: &str) -> Option<f64> { - None + let stat = nix::sys::statvfs::statvfs(directory).ok()?; + Some(stat.blocks_available() as f64 * stat.fragment_size() as f64) } pub const GLOB_MARK: i64 = 8; diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index 1d9f27e4..93b2e430 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -423,6 +423,10 @@ pub enum ChildPipe { In(std::process::ChildStdin), Out(std::process::ChildStdout), Err(std::process::ChildStderr), + /// The parent end of a pipe wired to a child descriptor beyond stderr. `std::process::Child` + /// exposes no typed handle for those, so the raw end is kept as a file. Its direction is + /// carried by the owning `StreamState`'s `readable`/`writable` flags. + Extra(std::fs::File), } impl std::io::Read for ChildPipe { @@ -430,6 +434,7 @@ impl std::io::Read for ChildPipe { match self { ChildPipe::Out(o) => o.read(buf), ChildPipe::Err(e) => e.read(buf), + ChildPipe::Extra(f) => f.read(buf), ChildPipe::In(_) => Err(std::io::Error::from(std::io::ErrorKind::Unsupported)), } } @@ -439,6 +444,7 @@ impl std::io::Write for ChildPipe { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { match self { ChildPipe::In(i) => i.write(buf), + ChildPipe::Extra(f) => f.write(buf), ChildPipe::Out(_) | ChildPipe::Err(_) => { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) } @@ -448,6 +454,7 @@ impl std::io::Write for ChildPipe { fn flush(&mut self) -> std::io::Result<()> { match self { ChildPipe::In(i) => i.flush(), + ChildPipe::Extra(f) => f.flush(), ChildPipe::Out(_) | ChildPipe::Err(_) => Ok(()), } } @@ -465,6 +472,7 @@ impl std::os::unix::io::AsRawFd for ChildPipe { ChildPipe::In(i) => i.as_raw_fd(), ChildPipe::Out(o) => o.as_raw_fd(), ChildPipe::Err(e) => e.as_raw_fd(), + ChildPipe::Extra(f) => f.as_raw_fd(), } } } diff --git a/crates/shirabe-php-shim/src/process.rs b/crates/shirabe-php-shim/src/process.rs index 57d9b35a..770cea0c 100644 --- a/crates/shirabe-php-shim/src/process.rs +++ b/crates/shirabe-php-shim/src/process.rs @@ -171,8 +171,61 @@ pub fn proc_open( // Remember which fds requested a pipe so their ends can be taken after spawn. let mut pipe_modes: Vec<(i64, String)> = Vec::new(); + // Descriptors beyond stderr, as (target fd, child-side end). `Command` cannot express them, so + // the child installs them with dup2(2) between fork and exec. + let mut extra_fds: Vec<(std::os::fd::RawFd, std::os::fd::OwnedFd)> = Vec::new(); for (index, descriptor) in descriptorspec.iter().enumerate() { let fd = index as i64; + if fd >= 3 { + match descriptor { + Descriptor::Pipe(mode) => { + // O_CLOEXEC keeps the parent end out of the child; dup2 clears it on the + // child end, which is what makes that one survive the exec. + let (read_end, write_end) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC) + .map_err(std::io::Error::from)?; + // The mode is the child's point of view, so "r" means the child reads and the + // parent writes. + let child_reads = mode.starts_with('r'); + let (child_end, parent_end) = if child_reads { + (read_end, write_end) + } else { + (write_end, read_end) + }; + extra_fds.push((fd as std::os::fd::RawFd, child_end)); + pipes.insert( + fd, + PhpResource::Stream(std::rc::Rc::new(std::cell::RefCell::new( + StreamState::new( + StreamBacking::Pipe(ChildPipe::Extra(std::fs::File::from( + parent_end, + ))), + !child_reads, + child_reads, + mode.clone(), + format!("pipe:fd{}", fd), + ), + ))), + ); + } + Descriptor::File(path, mode) => { + extra_fds.push(( + fd as std::os::fd::RawFd, + std::os::fd::OwnedFd::from(resource_to_file(&crate::fs::fopen( + path, mode, + )?)?), + )); + } + Descriptor::Resource(resource) => { + extra_fds.push(( + fd as std::os::fd::RawFd, + std::os::fd::OwnedFd::from(resource_to_file(resource)?), + )); + } + // A gap in a sparse descriptorspec: the child keeps whatever the parent has there. + Descriptor::Inherit => {} + } + continue; + } let stdio = match descriptor { Descriptor::Pipe(mode) => { pipe_modes.push((fd, mode.clone())); @@ -190,15 +243,43 @@ pub fn proc_open( 0 => cmd.stdin(stdio), 1 => cmd.stdout(stdio), 2 => cmd.stderr(stdio), - _ => { - // TODO(phase-d): inheriting fds >= 3 (e.g. the --enable-sigchild pipe 3) requires - // dup2/pre_exec; a syscall crate is intentionally not introduced here. - todo!("proc_open: descriptors with fd >= 3 require fd inheritance (syscall)") + _ => unreachable!(), + }; + } + + if !extra_fds.is_empty() { + use std::os::fd::{AsRawFd as _, FromRawFd as _, IntoRawFd as _}; + use std::os::unix::process::CommandExt as _; + let install_extra_fds = move || { + for (target, child_end) in &extra_fds { + if child_end.as_raw_fd() == *target { + // dup2(fd, fd) is a no-op that leaves FD_CLOEXEC set, which would close the + // descriptor on exec; clear the flag by hand instead. + nix::fcntl::fcntl( + child_end, + nix::fcntl::FcntlArg::F_SETFD(nix::fcntl::FdFlag::empty()), + )?; + continue; + } + // SAFETY: dup2_raw closes `target` if it is open and makes it the duplicate of + // the child end; releasing the returned owner keeps it open across the exec. + let installed = unsafe { + nix::unistd::dup2_raw(child_end, std::os::fd::OwnedFd::from_raw_fd(*target))? + }; + let _ = installed.into_raw_fd(); } + Ok(()) }; + // SAFETY: the closure only calls async-signal-safe syscalls, as required between fork and + // exec. It owns the child-side fds, so they stay alive until the exec happens. + unsafe { + cmd.pre_exec(install_extra_fds); + } } let mut child = cmd.spawn()?; + // Closing the parent's copies of the child-side ends is what lets the child see EOF. + drop(cmd); for (fd, mode) in pipe_modes { // fd 0 is the child's stdin: the parent-side handle is writable. fds 1/2 are stdout/stderr: @@ -289,13 +370,31 @@ pub fn proc_get_status(process: &PhpResource) -> IndexMap<String, PhpMixed> { status } +/// PHP `proc_terminate`. Sends `signal` to the process behind the resource; returns PHP's `false` +/// when the handle is already closed or the signal cannot be delivered. pub fn proc_terminate(process: &PhpResource, signal: i64) -> bool { - let _ = (process, signal); - // TODO(phase-d): sending an arbitrary signal requires kill(2); std's Child::kill only sends - // SIGKILL and a syscall crate is intentionally not introduced here. - todo!( - "proc_terminate: arbitrary signal delivery requires kill(2) (syscall crate not available)" - ) + let PhpResource::Process(state) = process else { + return false; + }; + let state = state.borrow(); + let Some(child) = state.child.as_ref() else { + return false; + }; + send_signal(child.id() as i32, signal) +} + +/// Shared body of `proc_terminate` and `posix_kill`. Signal 0 is PHP's existence probe and is +/// forwarded to `kill(2)` as such. +fn send_signal(pid: i32, signal: i64) -> bool { + let signal = if signal == 0 { + None + } else { + match nix::sys::signal::Signal::try_from(signal as i32) { + Ok(signal) => Some(signal), + Err(_) => return false, + } + }; + nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), signal).is_ok() } pub fn getmypid() -> i64 { @@ -303,13 +402,14 @@ pub fn getmypid() -> i64 { } pub fn cli_set_process_title(_title: &str) -> bool { - // TODO(phase-d): changing the process title visible to ps(1) requires platform-specific calls - // (prctl/setproctitle) not available without a libc/syscall crate. + // TODO(phase-c): PHP rewrites the argv area so the new title shows up in ps(1)'s full command + // line. Rust hands out argv as owned copies, so the original block is not reachable; prctl's + // PR_SET_NAME only replaces the 16-byte comm field and would report a different title. todo!() } pub fn setproctitle(_title: &str) { - // TODO(phase-d): see cli_set_process_title; requires platform-specific process-title support. + // TODO(phase-c): see cli_set_process_title; requires access to the process's own argv block. todo!() } @@ -318,68 +418,52 @@ pub fn setproctitle(_title: &str) { pub fn pcntl_async_signals(_enable: bool) {} pub fn pcntl_signal(_signal: i64, _handler: PhpMixed) -> bool { - // TODO(phase-d): registering a signal handler requires the signal-handling subsystem to be wired - // up (cf. SignalRegistry / the TODO(plugin) notes), plus a syscall crate for sigaction. + // TODO(phase-c): registering a signal handler requires the signal-handling subsystem to be + // wired up (cf. SignalRegistry / the TODO(plugin) notes). sigaction(2) itself is reachable, but + // the handler is a PHP callable whose dispatch depends on the runtime callable mechanism. todo!() } pub fn pcntl_signal_get_handler(_signal: i64) -> PhpMixed { - // TODO(phase-d): see pcntl_signal; needs the signal-handling subsystem. + // TODO(phase-c): see pcntl_signal; needs the signal-handling subsystem. todo!() } -#[repr(C)] -struct Passwd { - pw_name: *const std::os::raw::c_char, - pw_passwd: *const std::os::raw::c_char, - pw_uid: u32, - pw_gid: u32, - pw_gecos: *const std::os::raw::c_char, - pw_dir: *const std::os::raw::c_char, - pw_shell: *const std::os::raw::c_char, -} - -unsafe extern "C" { - fn getuid() -> u32; - fn geteuid() -> u32; - fn getpwuid(uid: u32) -> *const Passwd; -} - pub fn posix_getuid() -> i64 { - // getuid(2) cannot fail; libc is already linked into every binary, so no extra crate is needed. - (unsafe { getuid() }) as i64 + nix::unistd::getuid().as_raw() as i64 } pub fn posix_geteuid() -> i64 { - // geteuid(2) cannot fail; libc is already linked into every binary, so no extra crate is needed. - (unsafe { geteuid() }) as i64 + nix::unistd::geteuid().as_raw() as i64 } +/// Looks up the passwd entry for `uid` and returns it in the shape of PHP's `posix_getpwuid` +/// associative array, or `false` when no entry matches. pub fn posix_getpwuid(uid: i64) -> PhpMixed { - // getpwuid(3) via libc (already linked); mirrors PHP posix_getpwuid returning an associative - // array of the passwd entry, or false when no entry matches the uid. - let pw = unsafe { getpwuid(uid as u32) }; - if pw.is_null() { - return PhpMixed::Bool(false); - } - let cstr = |p: *const std::os::raw::c_char| -> String { - if p.is_null() { - String::new() - } else { - unsafe { std::ffi::CStr::from_ptr(p) } - .to_string_lossy() - .into_owned() - } + let user = match nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(uid as u32)) { + Ok(Some(user)) => user, + _ => return PhpMixed::Bool(false), }; - let pw = unsafe { &*pw }; let mut entry = indexmap::IndexMap::new(); - entry.insert("name".to_string(), PhpMixed::String(cstr(pw.pw_name))); - entry.insert("passwd".to_string(), PhpMixed::String(cstr(pw.pw_passwd))); - entry.insert("uid".to_string(), PhpMixed::Int(pw.pw_uid as i64)); - entry.insert("gid".to_string(), PhpMixed::Int(pw.pw_gid as i64)); - entry.insert("gecos".to_string(), PhpMixed::String(cstr(pw.pw_gecos))); - entry.insert("dir".to_string(), PhpMixed::String(cstr(pw.pw_dir))); - entry.insert("shell".to_string(), PhpMixed::String(cstr(pw.pw_shell))); + entry.insert("name".to_string(), PhpMixed::String(user.name)); + entry.insert( + "passwd".to_string(), + PhpMixed::String(user.passwd.to_string_lossy().into_owned()), + ); + entry.insert("uid".to_string(), PhpMixed::Int(user.uid.as_raw() as i64)); + entry.insert("gid".to_string(), PhpMixed::Int(user.gid.as_raw() as i64)); + entry.insert( + "gecos".to_string(), + PhpMixed::String(user.gecos.to_string_lossy().into_owned()), + ); + entry.insert( + "dir".to_string(), + PhpMixed::String(user.dir.to_string_lossy().into_owned()), + ); + entry.insert( + "shell".to_string(), + PhpMixed::String(user.shell.to_string_lossy().into_owned()), + ); PhpMixed::Array(entry) } @@ -394,15 +478,24 @@ pub fn posix_isatty(stream: PhpResource) -> bool { } } -pub fn posix_kill(_pid: i64, _signal: i64) -> bool { - // TODO(phase-d): kill(2) is not reachable without a libc/syscall crate. - todo!() +pub fn posix_kill(pid: i64, signal: i64) -> bool { + send_signal(pid as i32, signal) } +/// PHP `get_current_user()`: the name of the owner of the running script file. The Shirabe +/// executable takes the place of the script; PHP returns an empty string when the lookup fails. pub fn get_current_user() -> String { - // TODO(phase-d): PHP returns the owner name of the running script file, which needs stat(2) plus - // getpwuid(3); neither is reachable without a libc/syscall crate. - todo!() + use std::os::unix::fs::MetadataExt as _; + let Ok(executable) = std::env::current_exe() else { + return String::new(); + }; + let Ok(metadata) = std::fs::metadata(&executable) else { + return String::new(); + }; + match nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(metadata.uid())) { + Ok(Some(user)) => user.name, + _ => String::new(), + } } #[cfg(test)] @@ -481,6 +574,86 @@ mod tests { } #[test] + fn proc_open_wires_a_pipe_the_child_writes_to_beyond_stderr() { + let mut pipes = IndexMap::new(); + let process = proc_open( + "echo beyond >&3", + &[ + Descriptor::Inherit, + Descriptor::Inherit, + Descriptor::Inherit, + Descriptor::Pipe("w".to_string()), + ], + &mut pipes, + None, + None, + None, + ) + .unwrap(); + + assert_eq!( + stream_get_contents(pipes.get(&3).unwrap()).unwrap(), + "beyond\n" + ); + assert_eq!(proc_close(&process), 0); + } + + #[test] + fn proc_open_wires_a_pipe_the_child_reads_from_beyond_stderr() { + let mut pipes = IndexMap::new(); + let process = proc_open( + "cat <&4", + &[ + Descriptor::Inherit, + Descriptor::Pipe("w".to_string()), + Descriptor::Inherit, + Descriptor::Inherit, + Descriptor::Pipe("r".to_string()), + ], + &mut pipes, + None, + None, + None, + ) + .unwrap(); + + fwrite(pipes.get(&4).unwrap(), "fed\n", None); + fclose(pipes.get(&4).unwrap()); + // Dropping the last handle closes the fd so `cat` sees end-of-input. + pipes.shift_remove(&4); + + assert_eq!( + stream_get_contents(pipes.get(&1).unwrap()).unwrap(), + "fed\n" + ); + assert_eq!(proc_close(&process), 0); + } + + #[test] + fn proc_terminate_signals_the_child() { + let mut pipes = IndexMap::new(); + let process = proc_open( + "sleep 30", + &[ + Descriptor::Inherit, + Descriptor::Inherit, + Descriptor::Inherit, + ], + &mut pipes, + None, + None, + None, + ) + .unwrap(); + + assert!(proc_terminate(&process, SIGTERM)); + // A child killed by a signal has no exit code, which proc_close reports as -1. + assert_eq!(proc_close(&process), -1); + let status = proc_get_status(&process); + assert_eq!(status.get("pid").unwrap().as_int(), Some(-1)); + } + + #[test] fn proc_open_redirects_stdout_to_file() { let path = std::env::temp_dir().join(format!("shirabe_proc_open_{}.txt", std::process::id())); diff --git a/crates/shirabe-php-shim/src/runtime.rs b/crates/shirabe-php-shim/src/runtime.rs index 00104051..71bb4530 100644 --- a/crates/shirabe-php-shim/src/runtime.rs +++ b/crates/shirabe-php-shim/src/runtime.rs @@ -293,31 +293,29 @@ pub fn spl_object_hash_process<T>(_object: &T) -> String { format!("{:032x}", _object as *const T as usize) } +// TODO(phase-c): the Windows branch of php_uname is missing. There PHP reports "Windows NT" as the +// sysname and derives release/version from the OS version APIs rather than uname(2). pub fn php_uname(mode: &str) -> String { + let Ok(utsname) = nix::sys::utsname::uname() else { + return String::new(); + }; + let field = |value: &std::ffi::OsStr| value.to_string_lossy().into_owned(); match mode { - // sysname, as reported by uname(2). On Windows PHP returns "Windows NT", - // which differs from PHP_OS. - "s" => match std::env::consts::OS { - "linux" => "Linux", - "macos" => "Darwin", - "windows" => "Windows NT", - "freebsd" => "FreeBSD", - "netbsd" => "NetBSD", - "openbsd" => "OpenBSD", - "dragonfly" => "DragonFly", - "solaris" => "SunOS", - other => other, - } - .to_string(), - // TODO(phase-c): use libc? - // release, as reported by uname(2). On Linux this matches the contents - // of /proc/sys/kernel/osrelease. - "r" => std::fs::read_to_string("/proc/sys/kernel/osrelease") - .map(|s| s.trim_end().to_string()) - .unwrap_or_default(), - // TODO(phase-d): the remaining php_uname() modes ("n", "v", "m", "a") need uname(2) fields - // (nodename/version/machine) that are not reachable without a libc/syscall crate. - _ => todo!(), + "s" => field(utsname.sysname()), + "n" => field(utsname.nodename()), + "r" => field(utsname.release()), + "v" => field(utsname.version()), + "m" => field(utsname.machine()), + // "a" is the default and any other mode falls back to it: every field in the order + // sysname, nodename, release, version, machine. + _ => format!( + "{} {} {} {} {}", + field(utsname.sysname()), + field(utsname.nodename()), + field(utsname.release()), + field(utsname.version()), + field(utsname.machine()), + ), } } 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::<i64>())], -} - -impl FdSet { - fn zero() -> Self { - FdSet { - fds_bits: [0; FD_SETSIZE / (8 * std::mem::size_of::<i64>())], - } - } - - fn set(&mut self, fd: i32) { - let bits = 8 * std::mem::size_of::<i64>(); - 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::<i64>(); - (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<i64>, ) -> Option<i64> { - 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<PhpResource>| { + let narrow = |set: &nix::sys::select::FdSet<'static>, resources: &mut Vec<PhpResource>| { 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); |
