Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wait: implement waitid() #1584

Merged
merged 1 commit into from
Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
(#[1664](https://github.com/nix-rust/nix/pull/1664))
- Added `MSG_NOSIGNAL` for Android, Dragonfly, FreeBSD, Fuchsia, Haiku, Illumos, Linux, NetBSD, OpenBSD and Solaris.
(#[1670](https://github.com/nix-rust/nix/pull/1670))
- Added `waitid`.
(#[1584](https://github.com/nix-rust/nix/pull/1584))

### Changed

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

libc_bitflags!(
/// Controls the behavior of [`waitpid`].
Expand Down Expand Up @@ -233,6 +238,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",
all(target_os = "linux", not(target_env = "uclibc")),
))]
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 @@ -268,3 +328,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",
all(target_os = "linux", not(target_env = "uclibc")),
))]
#[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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libc defines this function for almost all OSes. I think it might be everything except Redox. Any reason not to enable it for all of them in Nix?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started with the list of platforms for which WEXITED is defined in WaitPidFlag and then removed all that had some obvious issue:

  • NetBSD has broken siginfo_t in libc (see issue linked in PR description)
  • Apple/IOS are missing CLD_EXITED/KILLED/DUMPED in libc (and I don't really want to open another libc PR for a system that I do not use and that I'm not familiar with...)

target_os = "android",
target_os = "freebsd",
target_os = "haiku",
all(target_os = "linux", not(target_env = "uclibc")),
))]
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) }
}
118 changes: 116 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",
all(target_os = "linux", not(target_env = "uclibc")),
))]
#[cfg(not(any(target_arch = "mips", target_arch = "mips64")))]
fn test_waitid_signal() {
let _m = crate::FORK_MTX.lock();

// 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();
Expand All @@ -36,6 +63,29 @@ fn test_wait_exit() {
}
}

#[test]
#[cfg(any(
target_os = "android",
target_os = "freebsd",
target_os = "haiku",
all(target_os = "linux", not(target_env = "uclibc")),
))]
#[cfg(not(any(target_arch = "mips", target_arch = "mips64")))]
fn test_waitid_exit() {
let _m = crate::FORK_MTX.lock();

// 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",
all(target_os = "linux", not(target_env = "uclibc")),
))]
fn test_waitid_pid() {
let _m = crate::FORK_MTX.lock();

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,59 @@ mod ptrace {
assert_eq!(waitpid(child, None), Ok(WaitStatus::Exited(child, 0)));
}

#[cfg(not(target_env = "uclibc"))]
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();

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

#[test]
#[cfg(not(target_env = "uclibc"))]
fn test_waitid_ptrace() {
require_capability!("test_waitid_ptrace", CAP_SYS_PTRACE);
let _m = crate::FORK_MTX.lock();

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