use std::{
io::{self, Read, Write},
net::Shutdown,
os::{fd::AsRawFd, unix::net::UnixStream},
process::ExitStatus,
sync::{Arc, OnceLock, mpsc},
};
use process_wrap::tokio::{ChildWrapper, CommandWrap, ProcessGroup};
use tokio::{
io::unix::AsyncFd,
process::{ChildStderr, ChildStdin, ChildStdout, Command},
task::JoinHandle,
};
pub(crate) struct Child {
child: Box<dyn ChildWrapper>,
group: Option<crate::process::ChildIdentity>,
}
impl Child {
pub fn stdin(&mut self) -> &mut Option<ChildStdin> {
self.child.stdin()
}
pub fn stdout(&mut self) -> &mut Option<ChildStdout> {
self.child.stdout()
}
pub fn stderr(&mut self) -> &mut Option<ChildStderr> {
self.child.stderr()
}
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
self.child.wait().await
}
pub async fn wait_leader(&mut self) -> io::Result<ExitStatus> {
self.child.inner_mut().wait().await
}
pub fn try_wait_leader(&mut self) -> io::Result<Option<ExitStatus>> {
self.child.inner_mut().try_wait()
}
pub fn interrupt(&self) -> io::Result<()> {
if let Some(group) = self.group {
signal_saved_group(group, libc::SIGTERM)
} else {
Ok(())
}
}
pub fn kill(&mut self) -> io::Result<()> {
if let Some(group) = self.group {
kill_saved_group(group)?;
self.group = None;
}
Ok(())
}
pub async fn cleanup(&mut self) -> io::Result<ExitStatus> {
self.kill()?;
self.wait().await
}
}
#[cfg(target_os = "macos")]
const PROC_FLAG_INEXIT: u32 = 4;
#[cfg(target_os = "macos")]
fn process_group_exited(pgid: u32) -> io::Result<bool> {
let pgid = i32::try_from(pgid).map_err(|_| io::ErrorKind::InvalidInput)?;
let capacity = unsafe { libc::proc_listpgrppids(pgid, std::ptr::null_mut(), 0) };
if capacity <= 0 {
return Err(io::Error::last_os_error());
}
let bytes = capacity
.checked_mul(std::mem::size_of::<i32>() as i32)
.ok_or_else(|| io::Error::other("process group information size overflow"))?;
const MAX_GROUP_INFO_BYTES: i32 = 16 * 1024 * 1024;
if bytes > MAX_GROUP_INFO_BYTES {
return Err(io::Error::other("process group information exceeds 16 MiB"));
}
let mut pids = vec![0_i32; capacity as usize];
let (read, error) = unsafe {
*libc::__error() = 0;
let read = libc::proc_listpgrppids(pgid, pids.as_mut_ptr().cast(), bytes);
(read, io::Error::last_os_error())
};
if read < 0 || (read == 0 && error.raw_os_error() != Some(0)) {
return Err(error);
}
if read >= capacity {
return Err(io::Error::other("incomplete process group information"));
}
for pid in pids.into_iter().take(read as usize) {
match process_info(pid, 1) {
Ok(info)
if info.pbi_pgid == pgid as u32
&& info.pbi_status != libc::SZOMB
&& info.pbi_flags & PROC_FLAG_INEXIT == 0 =>
{
return Ok(false);
}
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
}
Ok(true)
}
impl Drop for Child {
fn drop(&mut self) {
let _ = self.kill();
}
}
pub(crate) fn spawn_gated(command: Command) -> io::Result<(Gate, JoinHandle<io::Result<Child>>)> {
let mut command = CommandWrap::from(command);
command.wrap(ProcessGroup::leader());
spawn(command)
}
pub(crate) fn spawn_detached(mut command: Command) -> io::Result<tokio::process::Child> {
unsafe {
command.pre_exec(|| {
if libc::setsid() == -1 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
command.spawn()
}
pub(crate) fn spawn_run(command: Command) -> io::Result<tokio::process::Child> {
spawn_detached(command)
}
pub(crate) fn spawn_watched(mut command: Command) -> io::Result<tokio::process::Child> {
unsafe {
command.pre_exec(|| {
if libc::setsid() == -1 {
return Err(io::Error::last_os_error());
}
if libc::signal(libc::SIGPIPE, libc::SIG_IGN) == libc::SIG_ERR {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
command.spawn()
}
#[cfg(not(target_os = "macos"))]
const START_TIME_AFTER_NAME: usize = 22 - 3;
#[cfg(not(target_os = "macos"))]
pub(crate) fn process_start_time(pid: u32) -> io::Result<u64> {
process_stat(pid).map(|(_, start)| start)
}
#[cfg(not(target_os = "macos"))]
fn process_stat(pid: u32) -> io::Result<(char, u64)> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat"))?;
let mut fields = stat
.rsplit_once(')')
.ok_or_else(|| io::Error::other("invalid process stat"))?
.1
.split_whitespace();
let state = fields
.next()
.and_then(|field| field.chars().next())
.ok_or_else(|| io::Error::other("invalid process state"))?;
let start = fields
.nth(START_TIME_AFTER_NAME - 1)
.and_then(|field| field.parse().ok())
.ok_or_else(|| io::Error::other("invalid child process start time"))?;
Ok((state, start))
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn running_process_start_time(pid: u32) -> io::Result<u64> {
let (state, start) = process_stat(pid)?;
if matches!(state, 'Z' | 'X' | 'x') {
Err(io::ErrorKind::NotFound.into())
} else {
Ok(start)
}
}
#[cfg(target_os = "macos")]
pub(crate) fn running_process_start_time(pid: u32) -> io::Result<u64> {
let pid = i32::try_from(pid).map_err(|_| io::ErrorKind::InvalidInput)?;
let info = process_info(pid, 1)?;
if info.pbi_status == libc::SZOMB || info.pbi_flags & PROC_FLAG_INEXIT != 0 {
Err(io::ErrorKind::NotFound.into())
} else {
process_birth_time(info)
}
}
#[cfg(target_os = "macos")]
pub(crate) fn process_start_time(pid: u32) -> io::Result<u64> {
let pid = i32::try_from(pid).map_err(|_| io::ErrorKind::InvalidInput)?;
if pid <= 0 {
return Err(io::ErrorKind::InvalidInput.into());
}
process_birth_time(process_info(pid, 1)?)
}
#[cfg(target_os = "macos")]
fn process_birth_time(info: libc::proc_bsdinfo) -> io::Result<u64> {
const MICROSECONDS_PER_SECOND: u64 = 1_000_000;
info.pbi_start_tvsec
.checked_mul(MICROSECONDS_PER_SECOND)
.and_then(|seconds| seconds.checked_add(info.pbi_start_tvusec))
.ok_or_else(|| io::Error::other("invalid child process start time"))
}
#[cfg(target_os = "macos")]
fn process_info(pid: i32, include_zombies: u64) -> io::Result<libc::proc_bsdinfo> {
let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::uninit();
let size = std::mem::size_of::<libc::proc_bsdinfo>() as i32;
let read = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDTBSDINFO,
include_zombies,
info.as_mut_ptr().cast(),
size,
)
};
if read <= 0 {
let error = io::Error::last_os_error();
return Err(if error.raw_os_error() == Some(libc::ESRCH) {
io::ErrorKind::NotFound.into()
} else {
error
});
}
if read != size {
return Err(io::Error::other("incomplete child process information"));
}
let info = unsafe { info.assume_init() };
if info.pbi_pid != pid as u32 {
return Err(io::Error::other("invalid child process identity"));
}
Ok(info)
}
const GATE_ADMIT: u8 = 1;
pub(crate) struct Gate {
socket: Arc<AsyncFd<UnixStream>>,
identity: OnceLock<crate::process::ChildIdentity>,
admitted: mpsc::Sender<crate::process::ChildIdentity>,
}
impl Gate {
pub async fn pid(&self) -> io::Result<u32> {
let mut bytes = [0; 4];
let mut offset = 0;
while offset < bytes.len() {
let mut ready = self.socket.readable().await?;
match ready.try_io(|socket| socket.get_ref().read(&mut bytes[offset..])) {
Ok(Ok(0)) => return Err(io::ErrorKind::UnexpectedEof.into()),
Ok(Ok(count)) => offset += count,
Ok(Err(error)) => return Err(error),
Err(_) => continue,
}
}
let pid = u32::from_ne_bytes(bytes);
let identity = crate::process::child_identity(pid)?;
let _ = self.identity.set(identity);
Ok(pid)
}
pub fn admit(&self) -> io::Result<()> {
let identity = self.identity.get().ok_or(io::ErrorKind::InvalidInput)?;
self.socket.get_ref().write_all(&[GATE_ADMIT])?;
self.admitted
.send(*identity)
.map_err(|_| io::ErrorKind::BrokenPipe.into())
}
}
impl Drop for Gate {
fn drop(&mut self) {
let _ = self.socket.get_ref().shutdown(Shutdown::Both);
}
}
fn spawn(mut command: CommandWrap) -> io::Result<(Gate, JoinHandle<io::Result<Child>>)> {
let (parent, child) = UnixStream::pair()?;
parent.set_nonblocking(true)?;
let (admitted, admission) = mpsc::channel();
let gate = Gate {
socket: Arc::new(AsyncFd::new(parent)?),
identity: OnceLock::new(),
admitted,
};
let parent = gate.socket.clone();
unsafe {
command.command_mut().pre_exec(move || {
libc::close(parent.get_ref().as_raw_fd());
let pid = (libc::getpid() as u32).to_ne_bytes();
let fd = child.as_raw_fd();
let mut offset = 0;
while offset < pid.len() {
let written = libc::write(fd, pid[offset..].as_ptr().cast(), pid.len() - offset);
if written < 0 {
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(error);
}
if written == 0 {
return Err(io::Error::from_raw_os_error(libc::EPIPE));
}
offset += written as usize;
}
let mut admission = 0_u8;
loop {
match libc::read(fd, (&mut admission as *mut u8).cast(), 1) {
1 if admission == GATE_ADMIT => return Ok(()),
-1 => {
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(libc::EINTR) {
return Err(error);
}
}
_ => return Err(io::Error::from_raw_os_error(libc::ECANCELED)),
}
}
});
}
let spawning = tokio::task::spawn_blocking(move || {
let spawned = command.spawn();
drop(command);
let mut child = spawned?;
match admission.recv() {
Ok(group) => Ok(Child {
child,
group: Some(group),
}),
Err(_) => {
tokio::runtime::Handle::current().block_on(child.inner_mut().wait())?;
Err(io::Error::from_raw_os_error(libc::ECANCELED))
}
}
});
Ok((gate, spawning))
}
pub(crate) fn kill_saved_session(leader: crate::process::ChildIdentity) -> io::Result<()> {
match process_start_time(leader.pid.get()) {
Ok(start) if start != leader.start_time => return Ok(()),
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
let session = i32::try_from(leader.pid.get()).map_err(|_| io::ErrorKind::InvalidInput)?;
let mut groups = std::collections::BTreeMap::new();
for pid in process_ids()? {
if pid <= 0 {
continue;
}
let Ok(member_start) = process_start_time(pid as u32) else {
continue;
};
let (sid, group) = unsafe { (libc::getsid(pid), libc::getpgid(pid)) };
if sid != session || group <= 0 {
continue;
}
let start = match process_start_time(group as u32) {
Ok(start) => start,
Err(error) if error.kind() == io::ErrorKind::NotFound => 0,
Err(error) => return Err(error),
};
if process_start_time(pid as u32).ok() == Some(member_start) {
groups.insert(group, start);
}
}
for (group, start_time) in groups {
if process_start_time(leader.pid.get()).is_ok_and(|start| start != leader.start_time) {
return Ok(());
}
kill_saved_group(crate::process::ChildIdentity {
pid: crate::process::ProcessId::new(group as u32),
start_time,
})?;
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn process_ids() -> io::Result<Vec<i32>> {
Ok(std::fs::read_dir("/proc")?
.filter_map(|entry| entry.ok()?.file_name().to_str()?.parse().ok())
.collect())
}
#[cfg(target_os = "macos")]
fn process_ids() -> io::Result<Vec<i32>> {
const MAX_PID_BYTES: i32 = 16 * 1024 * 1024;
let count = unsafe { libc::proc_listallpids(std::ptr::null_mut(), 0) };
if count <= 0 {
return Err(io::Error::last_os_error());
}
let bytes = count
.checked_mul(std::mem::size_of::<i32>() as i32)
.ok_or_else(|| io::Error::other("process enumeration size overflow"))?;
if bytes > MAX_PID_BYTES {
return Err(io::Error::other("process enumeration exceeds 16 MiB"));
}
let mut pids = vec![0; count as usize];
let read = unsafe { libc::proc_listallpids(pids.as_mut_ptr().cast(), bytes) };
if read < 0 {
return Err(io::Error::last_os_error());
}
if read >= count {
return Err(io::Error::other("incomplete process enumeration"));
}
pids.truncate(read as usize);
Ok(pids)
}
pub(crate) fn kill_saved_group(leader: crate::process::ChildIdentity) -> io::Result<()> {
signal_saved_group(leader, libc::SIGKILL)
}
fn signal_saved_group(leader: crate::process::ChildIdentity, signal: i32) -> io::Result<()> {
match process_start_time(leader.pid.get()) {
Ok(start) if start != leader.start_time => return Ok(()),
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
let pgid = i32::try_from(leader.pid.get()).map_err(|_| io::ErrorKind::InvalidInput)?;
if signal_group(pgid, signal) == 0 {
return Ok(());
}
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(libc::ESRCH) {
return Ok(());
}
#[cfg(target_os = "macos")]
if error.raw_os_error() == Some(libc::EPERM) && process_group_exited(leader.pid.get())? {
return Ok(());
}
Err(error)
}
#[cfg(test)]
thread_local! { static GROUP_SIGNALS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) }; }
fn signal_group(pgid: i32, signal: i32) -> i32 {
#[cfg(test)]
GROUP_SIGNALS.with(|count| count.set(count.get() + 1));
unsafe { libc::killpg(pgid, signal) }
}
#[cfg(test)]
mod tests {
use std::{process::Stdio, time::Duration};
use process_wrap::tokio::ProcessGroup;
use super::*;
use crate::test_os::BeforeIdentity;
#[cfg(target_os = "macos")]
#[tokio::test]
async fn killing_a_zombie_only_group_succeeds_without_reaping_it_first() {
let mut command = Command::new(crate::test_os::true_program());
command.env_clear().stdin(Stdio::null());
let (gate, spawning) = spawn_gated(command).unwrap();
let pid = gate.pid().await.unwrap();
assert!(!process_group_exited(pid).unwrap());
gate.admit().unwrap();
drop(gate);
let mut child = spawning.await.unwrap().unwrap();
tokio::task::spawn_blocking(move || crate::test_os::wait_for_zombie(pid))
.await
.unwrap();
assert!(process_group_exited(pid).unwrap());
assert_eq!(
child.child.start_kill().unwrap_err().raw_os_error(),
Some(libc::EPERM)
);
child.kill().unwrap();
assert!(child.wait().await.unwrap().success());
assert!(process_group_exited(pid).unwrap());
child.kill().unwrap();
}
#[tokio::test]
async fn a_cleaned_group_no_longer_owns_a_signal_target() {
let mut command = Command::new(crate::test_os::true_program());
command.env_clear().stdin(Stdio::null());
let (gate, spawning) = spawn_gated(command).unwrap();
gate.pid().await.unwrap();
gate.admit().unwrap();
drop(gate);
let mut child = spawning.await.unwrap().unwrap();
assert!(child.wait_leader().await.unwrap().success());
GROUP_SIGNALS.with(|count| count.set(0));
child.cleanup().await.unwrap();
assert_eq!(GROUP_SIGNALS.with(std::cell::Cell::get), 1);
child.interrupt().unwrap();
child.cleanup().await.unwrap();
drop(child);
assert_eq!(
GROUP_SIGNALS.with(std::cell::Cell::get),
1,
"a cleaned group must never be signalled again"
);
}
#[tokio::test]
async fn disconnect_before_identity_publication_cancels_and_reaps_native_child() {
let mut command = Command::new(crate::test_os::true_program());
command.env_clear().stdin(Stdio::null());
let mut checkpoint = BeforeIdentity::hold(&mut command);
let (gate, spawning) = spawn_gated(command).unwrap();
let pid = checkpoint.pid().await;
drop(gate);
checkpoint.release().await;
let error = spawning
.await
.expect("pre-admission cancellation must not panic")
.err()
.expect("an unadmitted native child must not be returned");
assert_eq!(error.raw_os_error(), Some(libc::ECANCELED));
assert!(crate::test_os::gone(pid), "the native child must be reaped");
}
#[tokio::test]
async fn native_death_before_identity_publication_closes_pid_observation_and_reaps() {
let mut command = Command::new(crate::test_os::true_program());
command.env_clear().stdin(Stdio::null());
let mut checkpoint = BeforeIdentity::hold(&mut command);
let (gate, spawning) = spawn_gated(command).unwrap();
let pid = checkpoint.pid().await;
crate::test_os::kill(pid);
let error = gate.pid().await.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);
drop(gate);
let error = spawning
.await
.expect("pre-admission death must not panic")
.err()
.expect("an unadmitted native child must not be returned");
assert_eq!(error.raw_os_error(), Some(libc::ECANCELED));
assert!(crate::test_os::gone(pid), "the native child must be reaped");
}
#[tokio::test]
async fn parent_disconnect_before_admission_prevents_exec_and_reaps() {
let mut command = CommandWrap::with_new(crate::test_os::true_program(), |command| {
command.env_clear().stdin(Stdio::null());
});
command.wrap(ProcessGroup::leader());
let (gate, spawning) = spawn(command).unwrap();
let pid = gate.pid().await.unwrap();
assert!(process_start_time(pid).is_ok());
drop(gate);
let error = tokio::time::timeout(Duration::from_secs(2), spawning)
.await
.unwrap()
.unwrap()
.err()
.expect("the child must not exec");
assert_eq!(error.raw_os_error(), Some(libc::ECANCELED));
assert!(process_start_time(pid).is_err());
}
}