aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-02 09:39:04 +0900
committernsfisis <nsfisis@gmail.com>2026-08-02 09:39:04 +0900
commitc241931303448f9449051ea2080c1b65561066f2 (patch)
tree2bca8a0a4d009ed18d68ce427a0c7be64af42edd
parent2c3373a7466c31f5cce7c2867c00a91afaa8e128 (diff)
downloadphp-shirabe-c241931303448f9449051ea2080c1b65561066f2.tar.gz
php-shirabe-c241931303448f9449051ea2080c1b65561066f2.tar.zst
php-shirabe-c241931303448f9449051ea2080c1b65561066f2.zip
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) <noreply@anthropic.com>
-rw-r--r--Cargo.lock13
-rw-r--r--Cargo.toml1
-rw-r--r--crates/shirabe-php-shim/Cargo.toml1
-rw-r--r--crates/shirabe-php-shim/src/fs.rs189
-rw-r--r--crates/shirabe-php-shim/src/lib.rs8
-rw-r--r--crates/shirabe-php-shim/src/process.rs303
-rw-r--r--crates/shirabe-php-shim/src/runtime.rs44
-rw-r--r--crates/shirabe-php-shim/src/stream.rs122
8 files changed, 402 insertions, 279 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 6e800afd..480c4f2d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1293,6 +1293,18 @@ dependencies = [
]
[[package]]
+name = "nix"
+version = "0.31.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2150,6 +2162,7 @@ dependencies = [
"flate2",
"indexmap",
"md5",
+ "nix",
"regex",
"regex-macro",
"reqwest",
diff --git a/Cargo.toml b/Cargo.toml
index 3fc7c926..31a8c998 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,6 +28,7 @@ indexmap = { version = "2.14.0", features = ["serde"] }
jsonschema = { version = "0.46.6", default-features = false }
md5 = "0.7.0"
mockall = "0.14.0"
+nix = { version = "0.31.3", features = ["feature", "fs", "poll", "process", "signal", "user"] }
regex = "1.12.3"
regex-macro = "0.3.0"
reqwest = { version = "0.13.4", features = ["blocking"] }
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(), &times) == 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);