aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim/src/process.rs
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 /crates/shirabe-php-shim/src/process.rs
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>
Diffstat (limited to 'crates/shirabe-php-shim/src/process.rs')
-rw-r--r--crates/shirabe-php-shim/src/process.rs303
1 files changed, 238 insertions, 65 deletions
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()));