use alloc::{sync::Arc, vec::Vec};
use bitflags::bitflags;
use linux_raw_sys::general::{
__WALL, __WCLONE, __WNOTHREAD, P_ALL, P_PGID, P_PID, P_PIDFD, WCONTINUED, WEXITED, WNOHANG,
WNOWAIT, WUNTRACED,
};
use starry_signal::{SignalInfo, Signo};
use super::{ptrace::PTRACE_EVENT_STOP, wait_scan::WaitCandidateScan};
use crate::{
Errno, StarryError, StarryResult,
file::{PidFd, get_file_like},
mm::{VmMutPtr, VmPtr},
task::{
JobStatus, PgidNumber, PidIdentity, PidIdentityId, PidNumber, Process, ProcessGroup,
PtraceWaitAction, PtraceWaitStop, ROOT_PID_NS, Tgid, Tid, TidNumber, current_pid_view,
decode_wait_status, future::block_on_user, get_task_by_number, get_zombie_cred,
is_reaped_process, is_zombie_clone_child, is_zombie_process, processes, reap_process,
traced_zombies_for, wait_on_pollset, zombie_wait_parent_tid,
},
};
const PTRACE_O_TRACESYSGOOD: usize = 1;
bitflags! {
#[derive(Debug)]
struct WaitPidOptions: u32 {
const WNOHANG = WNOHANG;
const WUNTRACED = WUNTRACED;
const WCONTINUED = WCONTINUED;
const WNOTHREAD = __WNOTHREAD;
const WALL = __WALL;
const WCLONE = __WCLONE;
}
}
bitflags! {
#[derive(Debug)]
struct WaitIdOptions: u32 {
const WNOHANG = WNOHANG;
const WUNTRACED = WUNTRACED;
const WEXITED = WEXITED;
const WCONTINUED = WCONTINUED;
const WNOWAIT = WNOWAIT;
const WNOTHREAD = __WNOTHREAD;
const WALL = __WALL;
const WCLONE = __WCLONE;
}
}
#[derive(Clone)]
enum WaitTarget {
Any,
Identity(Arc<PidIdentity>),
Group(Arc<ProcessGroup>),
PidFd(Arc<PidIdentity>),
}
enum WaitSelector {
AnyChild,
CurrentProcessGroup,
ProcessOrThread(WaitProcessOrThreadNumber),
ProcessGroup(PgidNumber),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct WaitProcessOrThreadNumber(PidNumber);
impl WaitProcessOrThreadNumber {
fn parse(number: u32) -> StarryResult<Self> {
Ok(Self(PidNumber::try_from(number)?))
}
const fn pid_number(self) -> PidNumber {
self.0
}
}
impl TryFrom<i32> for WaitSelector {
type Error = StarryError;
fn try_from(pid: i32) -> Result<Self, Self::Error> {
match pid {
-1 => Ok(Self::AnyChild),
0 => Ok(Self::CurrentProcessGroup),
1.. => Ok(Self::ProcessOrThread(WaitProcessOrThreadNumber::parse(
pid as u32,
)?)),
..-1 => Ok(Self::ProcessGroup(PgidNumber::try_from(
pid.checked_neg()
.ok_or_else(|| StarryError::from(Errno::ESRCH))? as u32,
)?)),
}
}
}
enum WaitIdSelector {
All,
ProcessOrThread(WaitProcessOrThreadNumber),
CurrentProcessGroup,
ProcessGroup(PgidNumber),
PidFd(i32),
}
impl WaitIdSelector {
fn parse(idtype: u32, id: i32) -> StarryResult<Self> {
match idtype {
P_ALL => Ok(Self::All),
P_PID if id > 0 => Ok(Self::ProcessOrThread(WaitProcessOrThreadNumber::parse(
id as u32,
)?)),
P_PID => Err(StarryError::InvalidInput),
P_PGID if id == 0 => Ok(Self::CurrentProcessGroup),
P_PGID if id > 0 => Ok(Self::ProcessGroup(PgidNumber::try_from(id as u32)?)),
P_PGID => Err(StarryError::InvalidInput),
P_PIDFD => Ok(Self::PidFd(id)),
_ => Err(StarryError::InvalidInput),
}
}
}
impl WaitTarget {
fn identity_matches_thread(identity: &PidIdentity, child: &Process) -> bool {
identity
.live_task()
.is_some_and(|task| core::ptr::eq(Arc::as_ref(&task.as_thread().proc_data.proc), child))
}
fn matches(&self, child: &Process) -> bool {
match self {
WaitTarget::Any => true,
WaitTarget::Identity(identity) => identity.matches_process(child),
WaitTarget::Group(group) => Arc::ptr_eq(group, &child.group()),
WaitTarget::PidFd(identity) => identity.matches_process(child),
}
}
fn matches_process_or_thread(&self, child: &Process) -> bool {
self.matches(child)
|| matches!(self, WaitTarget::Identity(identity) if Self::identity_matches_thread(identity, child))
}
fn ptrace_target_tid(&self, child: &Process) -> Option<TidNumber> {
match self {
WaitTarget::Identity(identity)
if identity.matches_process(child)
|| Self::identity_matches_thread(identity, child) =>
{
Some(TidNumber::from(identity.root_number()))
}
WaitTarget::PidFd(identity) => Some(TidNumber::from(identity.root_number())),
_ => None,
}
}
}
fn visible_identity(identity: &PidIdentity) -> u32 {
current_pid_view()
.visible_number(identity)
.expect("wait target lost visibility before reporting")
.get()
}
fn visible_process(process: &Process) -> u32 {
visible_identity(&process.identity())
}
fn visible_root_tid(tid: TidNumber) -> u32 {
ROOT_PID_NS
.lookup(tid.pid_number())
.map(|identity| visible_identity(&identity))
.unwrap_or_else(|| tid.get())
}
fn waitid_pidfd_target(fd: i32) -> StarryResult<WaitTarget> {
if fd < 0 {
return Err(StarryError::InvalidInput);
}
let pidfd = get_file_like(fd)?
.downcast_arc::<PidFd>()
.map_err(|_| StarryError::BadFileDescriptor)?;
Ok(WaitTarget::PidFd(pidfd.identity()))
}
fn stopped_wait_signo(stop: PtraceWaitStop) -> i32 {
let event = stop.event;
let mut wait_signo = if event != 0 && event != PTRACE_EVENT_STOP {
Signo::SIGTRAP as i32
} else {
stop.signo as i32
};
if event == 0
&& stop.signo == Signo::SIGTRAP
&& stop.syscall
&& stop.options & PTRACE_O_TRACESYSGOOD != 0
{
wait_signo |= 0x80;
}
(event as i32) << 8 | wait_signo
}
fn stopped_wait_status(stop: PtraceWaitStop) -> i32 {
(stopped_wait_signo(stop) << 8) | 0x7f
}
fn child_uid(child: &Process) -> u32 {
get_zombie_cred(child)
.map(|cred| cred.uid)
.or_else(|| {
child.threads().into_iter().find_map(|tid| {
get_task_by_number(tid)
.ok()
.map(|task| task.as_thread().cred().uid)
})
})
.unwrap_or(0)
}
fn zombie_exit_code(child: &Arc<Process>) -> Option<i32> {
is_zombie_process(child).then(|| child.exit_code())
}
#[derive(Debug, Clone, Copy)]
struct WaitChildFilter {
wall: bool,
clone: bool,
no_thread: bool,
}
impl WaitChildFilter {
fn from_waitpid_options(options: &WaitPidOptions) -> Self {
Self {
wall: options.contains(WaitPidOptions::WALL),
clone: options.contains(WaitPidOptions::WCLONE),
no_thread: options.contains(WaitPidOptions::WNOTHREAD),
}
}
fn from_waitid_options(options: &WaitIdOptions) -> Self {
Self {
wall: options.contains(WaitIdOptions::WALL),
clone: options.contains(WaitIdOptions::WCLONE),
no_thread: options.contains(WaitIdOptions::WNOTHREAD),
}
}
fn matches_clone_kind(&self, is_clone_child: bool) -> bool {
self.wall || is_clone_child == self.clone
}
fn matches_process(&self, child: &Process, current_tid: TidNumber) -> bool {
if self.no_thread {
let wait_parent_tid = child
.identity()
.live_data()
.map(|data| data.wait_parent_tid())
.or_else(|| zombie_wait_parent_tid(child));
if wait_parent_tid != Some(current_tid) {
return false;
}
}
let is_clone_child = child
.identity()
.live_data()
.map(|data| data.is_clone_child())
.or_else(|| is_zombie_clone_child(child))
.unwrap_or(false);
self.matches_clone_kind(is_clone_child)
}
}
fn waitable_processes(
proc: &Process,
target: &WaitTarget,
tracer: PidIdentityId,
current_tid: TidNumber,
filter: WaitChildFilter,
) -> Vec<Arc<Process>> {
let mut candidates = match target {
WaitTarget::PidFd(identity) => identity
.public_process()
.ok()
.filter(|child| {
child
.parent()
.is_some_and(|parent| core::ptr::eq(Arc::as_ref(&parent), proc))
&& filter.matches_process(child, current_tid)
})
.into_iter()
.collect::<Vec<_>>(),
_ => proc
.children()
.into_iter()
.filter(|child| target.matches(child) && filter.matches_process(child, current_tid))
.collect::<Vec<_>>(),
};
if proc.identity().may_have_ptrace_tracees() {
for data in processes() {
let traced = data
.ptrace_tracer_identity()
.is_some_and(|identity| identity.id() == tracer);
let proc = data.proc.clone();
if traced
&& target.matches_process_or_thread(&proc)
&& filter.matches_process(&proc, current_tid)
&& !candidates
.iter()
.any(|candidate| candidate.pid() == proc.pid())
{
candidates.push(proc);
}
}
for zombie in traced_zombies_for(tracer) {
if target.matches(&zombie)
&& filter.matches_process(&zombie, current_tid)
&& !candidates
.iter()
.any(|candidate| candidate.pid() == zombie.pid())
{
candidates.push(zombie);
}
}
}
candidates
}
#[cfg(axtest)]
fn untraced_wait_avoids_root_pid_snapshot_for_test() -> bool {
use crate::task::{new_test_pid_namespace, new_test_process_identity};
let namespace = new_test_pid_namespace();
let (identity, tgid) = new_test_process_identity(&namespace);
let process = Process::new_for_axtest(identity.clone());
ROOT_PID_NS.reset_published_members_snapshot_calls_for_test();
let candidates = waitable_processes(
&process,
&WaitTarget::Any,
identity.id(),
TidNumber::from(process.pid().pid_number()),
WaitChildFilter {
wall: true,
clone: false,
no_thread: false,
},
);
let snapshot_calls = ROOT_PID_NS.published_members_snapshot_calls_for_test();
drop(candidates);
drop(process);
identity.mark_task_exited().complete();
tgid.release();
snapshot_calls == 0
}
pub fn sys_waitpid(
current: &crate::task::UserTaskRef,
pid: i32,
exit_code: *mut i32,
options: u32,
) -> StarryResult<isize> {
let options = WaitPidOptions::from_bits(options).ok_or(StarryError::InvalidInput)?;
info!("sys_waitpid <= pid: {pid:?}, options: {options:?}");
let curr = current;
let thr = curr.as_thread();
let proc = &thr.proc_data.proc;
let target = match WaitSelector::try_from(pid)? {
WaitSelector::AnyChild => WaitTarget::Any,
WaitSelector::CurrentProcessGroup => WaitTarget::Group(proc.group()),
WaitSelector::ProcessOrThread(number) => {
let identity = current_pid_view()
.resolve_identity(number.pid_number())
.and_then(|identity| {
(identity.has_role::<Tgid>() || identity.has_role::<Tid>())
.then_some(identity)
.ok_or(StarryError::NoSuchProcess)
})
.map_err(|_| StarryError::from(Errno::ECHILD))?;
WaitTarget::Identity(identity)
}
WaitSelector::ProcessGroup(pgid) => WaitTarget::Group(
current_pid_view()
.resolve_group(pgid)
.map_err(|_| StarryError::from(Errno::ECHILD))?,
),
};
let candidate_scan = WaitCandidateScan::new(|| {
waitable_processes(
proc,
&target,
proc.identity().id(),
thr.tid_number(),
WaitChildFilter::from_waitpid_options(&options),
)
});
if candidate_scan.collect().is_empty() {
return Err(crate::StarryError::from(crate::Errno::ECHILD));
}
let proc_data = curr.as_thread().proc_data.clone();
let check_children = || {
let children = candidate_scan.collect();
if let Some(stop) = children.iter().find_map(|child| {
child.identity().live_data().and_then(|data| {
data.ptrace_wait_stop(target.ptrace_target_tid(child), PtraceWaitAction::Consume)
})
}) {
let wait_pid = visible_root_tid(stop.tid);
let status = stopped_wait_status(stop);
if let Some(exit_code) = exit_code.nullable() {
exit_code.vm_write(current, status)?;
}
return Ok(Some(wait_pid as _));
} else if let Some((child, child_exit_code)) = children
.iter()
.find_map(|child| zombie_exit_code(child).map(|exit_code| (child, exit_code)))
{
if let Some(exit_code) = exit_code.nullable() {
exit_code.vm_write(current, child_exit_code)?;
}
let reported_pid = visible_process(child);
if let Some(cpu_time) = reap_process(child) {
proc_data.add_child_cpu_time(cpu_time.user(), cpu_time.system());
return Ok(Some(reported_pid as _));
}
}
let want_stopped = options.contains(WaitPidOptions::WUNTRACED);
let want_continued = options.contains(WaitPidOptions::WCONTINUED);
if want_stopped || want_continued {
for child in &children {
let Some(cdata) = child.identity().live_data() else {
continue;
};
if let Some(status) = cdata.peek_job_status_if(want_stopped, want_continued) {
let raw = match status {
JobStatus::Stopped(signo) => ((signo as i32) << 8) | 0x7f,
JobStatus::Continued => 0xffff,
};
if let Some(exit_code) = exit_code.nullable() {
exit_code.vm_write(current, raw)?;
}
cdata.take_job_status_if(want_stopped, want_continued);
return Ok(Some(visible_process(child) as _));
}
}
}
if children.iter().all(is_reaped_process) {
Err(StarryError::from(Errno::ECHILD))
} else if options.contains(WaitPidOptions::WNOHANG) {
Ok(Some(0))
} else {
Ok(None)
}
};
let task = current;
block_on_user(
task,
wait_on_pollset(proc_data.child_exit_event(), || {
check_children().transpose()
}),
)
.into_result()?
}
pub fn sys_waitid(
current: &crate::task::UserTaskRef,
idtype: u32,
id: i32,
infop: *mut linux_raw_sys::general::siginfo,
options: u32,
) -> crate::StarryResult<isize> {
let curr = current;
let thr = curr.as_thread();
let proc = &thr.proc_data.proc;
let target = match WaitIdSelector::parse(idtype, id)? {
WaitIdSelector::All => WaitTarget::Any,
WaitIdSelector::ProcessOrThread(number) => {
let identity = current_pid_view()
.resolve_identity(number.pid_number())
.and_then(|identity| {
(identity.has_role::<Tgid>() || identity.has_role::<Tid>())
.then_some(identity)
.ok_or(StarryError::NoSuchProcess)
})
.map_err(|_| StarryError::from(Errno::ECHILD))?;
WaitTarget::Identity(identity)
}
WaitIdSelector::CurrentProcessGroup => WaitTarget::Group(proc.group()),
WaitIdSelector::ProcessGroup(pgid) => WaitTarget::Group(
current_pid_view()
.resolve_group(pgid)
.map_err(|_| StarryError::from(Errno::ECHILD))?,
),
WaitIdSelector::PidFd(fd) => waitid_pidfd_target(fd)?,
};
let options = WaitIdOptions::from_bits(options).ok_or(StarryError::InvalidInput)?;
if !options
.intersects(WaitIdOptions::WEXITED | WaitIdOptions::WUNTRACED | WaitIdOptions::WCONTINUED)
{
return Err(StarryError::InvalidInput);
}
info!("sys_waitid <= idtype: {idtype}, id: {id}, options: {options:?}");
let candidate_scan = WaitCandidateScan::new(|| {
waitable_processes(
proc,
&target,
proc.identity().id(),
thr.tid_number(),
WaitChildFilter::from_waitid_options(&options),
)
});
if candidate_scan.collect().is_empty() {
return Err(crate::StarryError::from(crate::Errno::ECHILD));
}
let proc_data = curr.as_thread().proc_data.clone();
let check_children = || {
let children = candidate_scan.collect();
if options.contains(WaitIdOptions::WUNTRACED)
&& let Some((child, stop)) = children.iter().find_map(|child| {
child.identity().live_data().and_then(|data| {
let action = if options.contains(WaitIdOptions::WNOWAIT) {
PtraceWaitAction::Observe
} else {
PtraceWaitAction::Consume
};
data.ptrace_wait_stop(target.ptrace_target_tid(child), action)
.map(|stop| (child, stop))
})
})
{
let child_pid = visible_root_tid(stop.tid);
let child_uid = child_uid(child);
if let Some(infop) = infop.nullable() {
let siginfo = SignalInfo::new_sigchld(
child_pid,
child_uid,
linux_raw_sys::general::CLD_TRAPPED as i32,
stopped_wait_signo(stop),
);
infop.cast::<SignalInfo>().vm_write(current, siginfo)?;
}
return Ok(Some(0));
}
let want_stopped = options.contains(WaitIdOptions::WUNTRACED);
let want_continued = options.contains(WaitIdOptions::WCONTINUED);
if want_stopped || want_continued {
for child in &children {
let Some(data) = child.identity().live_data() else {
continue;
};
if let Some(status) = data.peek_job_status_if(want_stopped, want_continued) {
let (code, status) = match status {
JobStatus::Stopped(signo) => {
(linux_raw_sys::general::CLD_STOPPED as i32, signo as i32)
}
JobStatus::Continued => (
linux_raw_sys::general::CLD_CONTINUED as i32,
Signo::SIGCONT as i32,
),
};
if let Some(infop) = infop.nullable() {
let siginfo = SignalInfo::new_sigchld(
visible_process(child),
child_uid(child),
code,
status,
);
infop.cast::<SignalInfo>().vm_write(current, siginfo)?;
}
if !options.contains(WaitIdOptions::WNOWAIT) {
data.take_job_status_if(want_stopped, want_continued);
}
return Ok(Some(0));
}
}
}
if options.contains(WaitIdOptions::WEXITED)
&& let Some((child, _child_exit_code)) = children
.iter()
.find_map(|child| zombie_exit_code(child).map(|exit_code| (child, exit_code)))
{
let child_pid = visible_process(child);
let (code, status) = decode_wait_status(child.exit_code());
let child_uid = child_uid(child);
if let Some(infop) = infop.nullable() {
let siginfo = SignalInfo::new_sigchld(child_pid, child_uid, code, status);
infop.cast::<SignalInfo>().vm_write(current, siginfo)?;
}
if options.contains(WaitIdOptions::WNOWAIT) {
return Ok(Some(0));
}
if let Some(cpu_time) = reap_process(child) {
proc_data.add_child_cpu_time(cpu_time.user(), cpu_time.system());
return Ok(Some(0));
}
}
if children.iter().all(is_reaped_process) {
Err(StarryError::from(Errno::ECHILD))
} else if options.contains(WaitIdOptions::WNOHANG) {
if let Some(infop) = infop.nullable() {
let zeroed = SignalInfo::zeroed();
infop.cast::<SignalInfo>().vm_write(current, zeroed)?;
}
Ok(Some(0))
} else {
Ok(None)
}
};
let task = current;
block_on_user(
task,
wait_on_pollset(proc_data.child_exit_event(), || {
check_children().transpose()
}),
)
.into_result()?
}
#[cfg(all(test, not(axtest)))]
mod tests {
use super::*;
#[test]
fn waitpid_selector_preserves_linux_role_semantics() {
assert!(matches!(
WaitSelector::try_from(-1),
Ok(WaitSelector::AnyChild)
));
assert!(matches!(
WaitSelector::try_from(0),
Ok(WaitSelector::CurrentProcessGroup)
));
assert!(matches!(
WaitSelector::try_from(1),
Ok(WaitSelector::ProcessOrThread(number)) if number.pid_number().get() == 1
));
assert!(matches!(
WaitSelector::try_from(-2),
Ok(WaitSelector::ProcessGroup(pgid)) if pgid.get() == 2
));
let error = match WaitSelector::try_from(i32::MIN) {
Ok(_) => panic!("i32::MIN must not identify a process group"),
Err(error) => error,
};
assert_eq!(error.linux_errno(), Errno::ESRCH);
}
#[test]
fn waitid_selector_rejects_invalid_role_values() {
assert!(matches!(
WaitIdSelector::parse(P_PID, 1),
Ok(WaitIdSelector::ProcessOrThread(number)) if number.pid_number().get() == 1
));
assert!(matches!(
WaitIdSelector::parse(P_PID, 0),
Err(StarryError::InvalidInput)
));
assert!(matches!(
WaitIdSelector::parse(P_PGID, 0),
Ok(WaitIdSelector::CurrentProcessGroup)
));
assert!(matches!(
WaitIdSelector::parse(P_PGID, -1),
Err(StarryError::InvalidInput)
));
assert!(matches!(
WaitIdSelector::parse(P_PIDFD, -1),
Ok(WaitIdSelector::PidFd(-1))
));
assert!(matches!(
WaitIdSelector::parse(u32::MAX, 1),
Err(StarryError::InvalidInput)
));
}
}
#[cfg(all(test, axtest))]
mod axtests {
#[axtest::axtest]
fn untraced_wait_avoids_root_pid_snapshot() {
assert!(super::untraced_wait_avoids_root_pid_snapshot_for_test());
}
#[axtest::axtest]
fn ptrace_wait_status_remains_bound_to_the_selected_stop() {
use ax_runtime::hal::cpu::user::UserContext;
use starry_signal::Signo;
use crate::task::{TidNumber, new_test_process_data};
let namespace = crate::task::new_test_pid_namespace();
let (identity, tgid) = crate::task::new_test_process_identity(&namespace);
let parent = TidNumber::try_from(1).unwrap();
let sibling = TidNumber::try_from(2).unwrap();
let data = new_test_process_data(identity, tgid);
let uctx = UserContext::new(0, 0.into(), 0);
data.set_ptrace_pending_event(parent, super::super::ptrace::PTRACE_EVENT_CLONE, 2);
data.set_ptrace_stop(parent, Signo::SIGTRAP, &uctx);
let stop = data
.ptrace_wait_stop(Some(parent), super::PtraceWaitAction::Consume)
.unwrap();
data.set_ptrace_stop(sibling, Signo::SIGSTOP, &uctx);
assert_eq!(super::stopped_wait_status(stop), 0x0003_057f);
assert_eq!(super::stopped_wait_signo(stop), 0x0305);
assert!(
data.ptrace_wait_stop(Some(parent), super::PtraceWaitAction::Consume)
.is_none()
);
data.clear_ptrace_stop();
data.set_ptrace_options(super::PTRACE_O_TRACESYSGOOD);
data.set_ptrace_syscall_stop(parent, Signo::SIGTRAP, &uctx, 39);
let observed = data
.ptrace_wait_stop(Some(parent), super::PtraceWaitAction::Observe)
.unwrap();
data.set_ptrace_stop(sibling, Signo::SIGSTOP, &uctx);
assert_eq!(super::stopped_wait_signo(observed), 0x85);
let consumed = data
.ptrace_wait_stop(Some(parent), super::PtraceWaitAction::Consume)
.unwrap();
assert_eq!(super::stopped_wait_signo(consumed), 0x85);
assert!(
data.ptrace_wait_stop(Some(parent), super::PtraceWaitAction::Observe)
.is_none()
);
}
}