Skip to content

Commit

Permalink
wait: implement waitid()
Browse files Browse the repository at this point in the history
waitid() has a number of additional features that waitpid() is missing:

- WNOWAIT is only accepted for waitid() on Linux (and possibly other
  platforms)
- Support for waiting on PID file descriptors on Linux

For now support is added for all platforms with waitid() that have proper
siginfo_t support in libc. NetBSD support is currently a work in progress
[1].

Tests for the signal/exit code are currently skipped on MIPS platforms due
to multiple bugs in qemu-user in the translation of siginfo_t (one fixed
in January [2], one currently under review [3]).

[1] rust-lang/libc#2476
[2] https://lists.nongnu.org/archive/html/qemu-devel/2021-01/msg04810.html
[3] https://lists.nongnu.org/archive/html/qemu-devel/2021-10/msg05433.html
  • Loading branch information
neocturne committed Nov 19, 2021
1 parent 7c3121a commit 2dcab9c
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased] - ReleaseDate

### Added

- Added `waitid`.
(#[1584](https://github.com/nix-rust/nix/pull/1584))

### Changed
### Fixed

Expand Down
108 changes: 108 additions & 0 deletions src/sys/wait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use crate::Result;
use cfg_if::cfg_if;
use libc::{self, c_int};
use std::convert::TryFrom;
#[cfg(any(target_os = "android", target_os = "linux"))]
use std::os::unix::io::RawFd;

libc_bitflags!(
/// Controls the behavior of [`waitpid`].
Expand Down Expand Up @@ -225,6 +227,61 @@ impl WaitStatus {
WaitStatus::Continued(pid)
})
}

/// Convert a `siginfo_t` as returned by `waitid` to a `WaitStatus`
///
/// # Errors
///
/// Returns an `Error` corresponding to `EINVAL` for invalid values.
///
/// # Safety
///
/// siginfo_t is actually a union, not all fields may be initialized.
/// The functions si_pid() and si_status() must be valid to call on
/// the passed siginfo_t.
#[cfg(any(
target_os = "android",
target_os = "freebsd",
target_os = "haiku",
target_os = "linux",
))]
unsafe fn from_siginfo(siginfo: &libc::siginfo_t) -> Result<WaitStatus> {
let si_pid = siginfo.si_pid();
if si_pid == 0 {
return Ok(WaitStatus::StillAlive);
}

assert_eq!(siginfo.si_signo, libc::SIGCHLD);

let pid = Pid::from_raw(si_pid);
let si_status = siginfo.si_status();

let status = match siginfo.si_code {
libc::CLD_EXITED => WaitStatus::Exited(pid, si_status),
libc::CLD_KILLED | libc::CLD_DUMPED => WaitStatus::Signaled(
pid,
Signal::try_from(si_status)?,
siginfo.si_code == libc::CLD_DUMPED,
),
libc::CLD_STOPPED => WaitStatus::Stopped(pid, Signal::try_from(si_status)?),
libc::CLD_CONTINUED => WaitStatus::Continued(pid),
#[cfg(any(target_os = "android", target_os = "linux"))]
libc::CLD_TRAPPED => {
if si_status == libc::SIGTRAP | 0x80 {
WaitStatus::PtraceSyscall(pid)
} else {
WaitStatus::PtraceEvent(
pid,
Signal::try_from(si_status & 0xff)?,
(si_status >> 8) as c_int,
)
}
}
_ => return Err(Errno::EINVAL),
};

Ok(status)
}
}

/// Wait for a process to change status
Expand Down Expand Up @@ -260,3 +317,54 @@ pub fn waitpid<P: Into<Option<Pid>>>(pid: P, options: Option<WaitPidFlag>) -> Re
pub fn wait() -> Result<WaitStatus> {
waitpid(None, None)
}

/// The ID argument for `waitid`
#[cfg(any(
target_os = "android",
target_os = "freebsd",
target_os = "haiku",
target_os = "linux",
))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Id {
/// Wait for any child
All,
/// Wait for the child whose process ID matches the given PID
Pid(Pid),
/// Wait for the child whose process group ID matches the given PID
///
/// If the PID is zero, the caller's process group is used since Linux 5.4.
PGid(Pid),
/// Wait for the child referred to by the given PID file descriptor
#[cfg(any(target_os = "android", target_os = "linux"))]
PIDFd(RawFd),
}

/// Wait for a process to change status
///
/// See also [waitid(2)](https://pubs.opengroup.org/onlinepubs/9699919799/functions/waitid.html)
#[cfg(any(
target_os = "android",
target_os = "freebsd",
target_os = "haiku",
target_os = "linux",
))]
pub fn waitid(id: Id, flags: WaitPidFlag) -> Result<WaitStatus> {
let (idtype, idval) = match id {
Id::All => (libc::P_ALL, 0),
Id::Pid(pid) => (libc::P_PID, pid.as_raw() as libc::id_t),
Id::PGid(pid) => (libc::P_PGID, pid.as_raw() as libc::id_t),
#[cfg(any(target_os = "android", target_os = "linux"))]
Id::PIDFd(fd) => (libc::P_PIDFD, fd as libc::id_t),
};

let siginfo = unsafe {
// Memory is zeroed rather than uninitialized, as not all platforms
// initialize the memory in the StillAlive case
let mut siginfo: libc::siginfo_t = std::mem::zeroed();
Errno::result(libc::waitid(idtype, idval, &mut siginfo, flags.bits()))?;
siginfo
};

unsafe { WaitStatus::from_siginfo(&siginfo) }
}
116 changes: 114 additions & 2 deletions test/sys/test_wait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,33 @@ fn test_wait_signal() {
}
}

#[test]
#[cfg(any(
target_os = "android",
target_os = "freebsd",
target_os = "haiku",
target_os = "linux",
))]
#[cfg(not(any(target_arch = "mips", target_arch = "mips64")))]
fn test_waitid_signal() {
let _m = crate::FORK_MTX.lock().expect("Mutex got poisoned by another test");

// Safe: The child only calls `pause` and/or `_exit`, which are async-signal-safe.
match unsafe{fork()}.expect("Error: Fork Failed") {
Child => {
pause();
unsafe { _exit(123) }
},
Parent { child } => {
kill(child, Some(SIGKILL)).expect("Error: Kill Failed");
assert_eq!(
waitid(Id::Pid(child), WaitPidFlag::WEXITED),
Ok(WaitStatus::Signaled(child, SIGKILL, false)),
);
},
}
}

#[test]
fn test_wait_exit() {
let _m = crate::FORK_MTX.lock().expect("Mutex got poisoned by another test");
Expand All @@ -36,6 +63,29 @@ fn test_wait_exit() {
}
}

#[test]
#[cfg(any(
target_os = "android",
target_os = "freebsd",
target_os = "haiku",
target_os = "linux",
))]
#[cfg(not(any(target_arch = "mips", target_arch = "mips64")))]
fn test_waitid_exit() {
let _m = crate::FORK_MTX.lock().expect("Mutex got poisoned by another test");

// Safe: Child only calls `_exit`, which is async-signal-safe.
match unsafe{fork()}.expect("Error: Fork Failed") {
Child => unsafe { _exit(12); },
Parent { child } => {
assert_eq!(
waitid(Id::Pid(child), WaitPidFlag::WEXITED),
Ok(WaitStatus::Exited(child, 12)),
);
}
}
}

#[test]
fn test_waitstatus_from_raw() {
let pid = Pid::from_raw(1);
Expand All @@ -57,6 +107,25 @@ fn test_waitstatus_pid() {
}
}

#[test]
#[cfg(any(
target_os = "android",
target_os = "freebsd",
target_os = "haiku",
target_os = "linux",
))]
fn test_waitid_pid() {
let _m = crate::FORK_MTX.lock().expect("Mutex got poisoned by another test");

match unsafe { fork() }.unwrap() {
Child => unsafe { _exit(0) },
Parent { child } => {
let status = waitid(Id::Pid(child), WaitPidFlag::WEXITED).unwrap();
assert_eq!(status.pid(), Some(child));
}
}
}

#[cfg(any(target_os = "linux", target_os = "android"))]
// FIXME: qemu-user doesn't implement ptrace on most arches
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Expand All @@ -77,7 +146,7 @@ mod ptrace {
unsafe { _exit(0) }
}

fn ptrace_parent(child: Pid) {
fn ptrace_wait_parent(child: Pid) {
// Wait for the raised SIGTRAP
assert_eq!(waitpid(child, None), Ok(WaitStatus::Stopped(child, SIGTRAP)));
// We want to test a syscall stop and a PTRACE_EVENT stop
Expand All @@ -94,14 +163,57 @@ mod ptrace {
assert_eq!(waitpid(child, None), Ok(WaitStatus::Exited(child, 0)));
}

fn ptrace_waitid_parent(child: Pid) {
// Wait for the raised SIGTRAP
//
// Unlike waitpid(), waitid() can distinguish trap events from regular
// stop events, so unlike ptrace_wait_parent(), we get a PtraceEvent here
assert_eq!(
waitid(Id::Pid(child), WaitPidFlag::WEXITED),
Ok(WaitStatus::PtraceEvent(child, SIGTRAP, 0)),
);
// We want to test a syscall stop and a PTRACE_EVENT stop
assert!(ptrace::setoptions(child, Options::PTRACE_O_TRACESYSGOOD | Options::PTRACE_O_TRACEEXIT).is_ok());

// First, stop on the next system call, which will be exit()
assert!(ptrace::syscall(child, None).is_ok());
assert_eq!(
waitid(Id::Pid(child), WaitPidFlag::WEXITED),
Ok(WaitStatus::PtraceSyscall(child)),
);
// Then get the ptrace event for the process exiting
assert!(ptrace::cont(child, None).is_ok());
assert_eq!(
waitid(Id::Pid(child), WaitPidFlag::WEXITED),
Ok(WaitStatus::PtraceEvent(child, SIGTRAP, Event::PTRACE_EVENT_EXIT as i32)),
);
// Finally get the normal wait() result, now that the process has exited
assert!(ptrace::cont(child, None).is_ok());
assert_eq!(
waitid(Id::Pid(child), WaitPidFlag::WEXITED),
Ok(WaitStatus::Exited(child, 0)),
);
}

#[test]
fn test_wait_ptrace() {
require_capability!("test_wait_ptrace", CAP_SYS_PTRACE);
let _m = crate::FORK_MTX.lock().expect("Mutex got poisoned by another test");

match unsafe{fork()}.expect("Error: Fork Failed") {
Child => ptrace_child(),
Parent { child } => ptrace_parent(child),
Parent { child } => ptrace_wait_parent(child),
}
}

#[test]
fn test_waitid_ptrace() {
require_capability!("test_waitid_ptrace", CAP_SYS_PTRACE);
let _m = crate::FORK_MTX.lock().expect("Mutex got poisoned by another test");

match unsafe{fork()}.expect("Error: Fork Failed") {
Child => ptrace_child(),
Parent { child } => ptrace_waitid_parent(child),
}
}
}

0 comments on commit 2dcab9c

Please sign in to comment.