1use crate::{
5 async_fd,
6 mutex::{self, lock},
7};
8use anyhow::{Context, Result, bail};
9use nix::{
10 errno::Errno,
11 sys::{
12 prctl,
13 signal::{Signal, kill},
14 wait::{Id, WaitPidFlag},
15 },
16 unistd::{Pid, getpgid, getpid},
17};
18use std::{
19 os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd},
20 path::Path,
21 sync::{Arc, LazyLock, Mutex, RwLock, RwLockWriteGuard},
22 thread,
23 time::Duration,
24};
25use tokio::{io::Interest, sync::oneshot, task, time};
26use tracing::{debug, error};
27
28pub(crate) fn read_pid_file(path: &Path) -> Result<Pid> {
31 let content = std::fs::read_to_string(path)
32 .with_context(|| format!("read PID file {}", path.display()))?;
33 parse_pid(&content).with_context(|| format!("parse PID file {}", path.display()))
34}
35
36fn parse_pid(pid: &str) -> Result<Pid> {
38 positive_pid(pid.trim().parse()?)
39}
40
41pub(crate) fn child_pid(id: impl Into<Option<u32>>) -> Result<Pid> {
44 let id = id.into().context("child got reaped already")?;
45 positive_pid(i32::try_from(id).with_context(|| format!("invalid PID {id}"))?)
46}
47
48fn positive_pid(pid: i32) -> Result<Pid> {
51 if pid <= 0 {
52 bail!("invalid PID {pid}")
53 }
54 Ok(Pid::from_raw(pid))
55}
56
57static REAP_LOCK: RwLock<()> = RwLock::new(());
64
65pub(crate) fn reap_guard() -> RwLockWriteGuard<'static, ()> {
67 mutex::write(&REAP_LOCK)
68}
69
70#[derive(Debug, PartialEq)]
73struct Stat {
74 state: char,
75 ppid: i32,
76 pgrp: i32,
77}
78
79impl Stat {
80 fn read(pid: Pid) -> Option<Self> {
81 Self::parse(&std::fs::read(format!("/proc/{pid}/stat")).ok()?)
82 }
83
84 fn parse(stat: &[u8]) -> Option<Self> {
87 let end = stat.windows(2).rposition(|w| w == b") ")?;
88 let rest = std::str::from_utf8(&stat[end + 2..]).ok()?;
89 let mut fields = rest.split_whitespace();
90 Some(Self {
91 state: fields.next()?.chars().next()?,
92 ppid: fields.next()?.parse().ok()?,
93 pgrp: fields.next()?.parse().ok()?,
94 })
95 }
96}
97
98fn pids() -> impl Iterator<Item = Pid> {
100 std::fs::read_dir("/proc")
101 .into_iter()
102 .flatten()
103 .flatten()
104 .filter_map(|entry| parse_pid(entry.file_name().to_str()?).ok())
105}
106
107pub(crate) fn is_zombie(pid: Pid) -> bool {
109 Stat::read(pid).is_some_and(|stat| stat.state == 'Z')
110}
111
112pub(crate) fn children_by_scan() -> Vec<Pid> {
114 let own = getpid().as_raw();
115 pids()
116 .filter(|pid| Stat::read(*pid).is_some_and(|stat| stat.ppid == own))
117 .collect()
118}
119
120static PROC_CHILDREN: LazyLock<bool> =
122 LazyLock::new(|| Path::new("/proc/thread-self/children").exists());
123
124fn group_has_child(pgid: Pid) -> bool {
130 let own = getpid().as_raw();
131 let member = |pids: Vec<Pid>| {
132 pids.into_iter()
133 .filter_map(Stat::read)
134 .any(|stat| stat.ppid == own && stat.pgrp == pgid.as_raw())
135 };
136 member(children().0) || *PROC_CHILDREN && member(children_by_scan())
138}
139
140fn signals_group(pgid: Pid, own_pgid: Option<Pid>) -> bool {
146 pgid.as_raw() > 1 && Some(pgid) != own_pgid
147}
148
149#[expect(unsafe_code, reason = "a system call nix does not wrap")]
152pub(crate) fn pidfd_open(pid: Pid) -> nix::Result<OwnedFd> {
153 let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid.as_raw(), 0) };
156 let fd = RawFd::try_from(Errno::result(fd)?).map_err(|_| Errno::EBADF)?;
157 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
160}
161
162#[expect(unsafe_code, reason = "a system call nix does not wrap")]
165fn pidfd_send_signal(pidfd: impl AsFd, signal: Option<Signal>) -> nix::Result<()> {
166 let signal = signal.map_or(0, |signal| signal as libc::c_int);
167 let res = unsafe {
170 libc::syscall(
171 libc::SYS_pidfd_send_signal,
172 pidfd.as_fd().as_raw_fd(),
173 signal,
174 std::ptr::null::<libc::siginfo_t>(),
175 0,
176 )
177 };
178 Errno::result(res).map(drop)
179}
180
181#[derive(Debug)]
184pub(crate) struct Process {
185 pid: Pid,
186
187 pidfd: Option<OwnedFd>,
190}
191
192impl Process {
193 pub(crate) fn open(pid: Pid) -> Self {
196 let pidfd = match pidfd_open(pid) {
197 Ok(pidfd) => Some(pidfd),
198 Err(e) => {
199 debug!("No pidfd for PID {pid}, checking the PID instead: {e}");
200 None
201 }
202 };
203 Self { pid, pidfd }
204 }
205
206 pub(crate) fn child(pid: Pid) -> Self {
209 Self { pid, pidfd: None }
210 }
211
212 pub(crate) fn pid(&self) -> Pid {
214 self.pid
215 }
216
217 pub(crate) fn signal(&self, signal: impl Into<Option<Signal>>) -> nix::Result<()> {
222 let _guard = mutex::read(&REAP_LOCK);
223 self.signal_unlocked(signal.into())
224 }
225
226 #[cfg(test)]
229 pub(crate) fn is_running(&self) -> bool {
230 let _guard = mutex::read(&REAP_LOCK);
231 self.signal_unlocked(None).is_ok() && matches!(peek_exit(Id::Pid(self.pid)), Ok(None))
232 }
233
234 pub(crate) fn signal_group(&self, signal: Signal) -> nix::Result<()> {
244 let _guard = mutex::read(&REAP_LOCK);
245 let own_pgid = getpgid(None).ok();
246 if let Err(e) = self.signal_unlocked(None) {
247 return self.signal_remaining_group(signal, own_pgid, e);
248 }
249 if let Ok(pgid) = getpgid(Some(self.pid))
250 && signals_group(pgid, own_pgid)
251 && let Err(e) = kill(Pid::from_raw(-pgid.as_raw()), signal)
252 {
253 debug!("Unable to signal process group {pgid}: {e}");
254 }
255 self.signal_unlocked(Some(signal))
256 }
257
258 fn signal_remaining_group(
265 &self,
266 signal: Signal,
267 own_pgid: Option<Pid>,
268 e: Errno,
269 ) -> nix::Result<()> {
270 if signals_group(self.pid, own_pgid)
271 && kill(self.pid, None) == Err(Errno::ESRCH)
272 && group_has_child(self.pid)
273 {
274 debug!("Signalling the remaining group of PID {}", self.pid);
275 return kill(Pid::from_raw(-self.pid.as_raw()), signal);
276 }
277 Err(e)
278 }
279
280 fn signal_unlocked(&self, signal: Option<Signal>) -> nix::Result<()> {
281 if let Some(pidfd) = &self.pidfd {
282 return pidfd_send_signal(pidfd, signal);
283 }
284
285 peek_exit(Id::Pid(self.pid))?;
289 kill(self.pid, signal)
290 }
291}
292
293fn peek_exit(id: Id<'_>) -> nix::Result<Option<i32>> {
297 wait_exit(id, WaitPidFlag::WNOHANG | WaitPidFlag::WNOWAIT)
298}
299
300pub(crate) fn is_child(pid: Pid) -> bool {
304 peek_exit(Id::Pid(pid)) != Err(Errno::ECHILD)
305}
306
307pub(crate) fn is_subreaper() -> bool {
312 prctl::get_child_subreaper().unwrap_or(true)
313}
314
315pub(crate) fn has_zombie() -> bool {
318 !matches!(peek_exit(Id::All), Ok(None) | Err(Errno::ECHILD))
319}
320
321pub(crate) fn zombie_children() -> (Vec<Pid>, bool) {
324 let (children, complete) = children();
325 let zombies = children.into_iter().filter(|pid| is_zombie(*pid)).collect();
326 (zombies, complete)
327}
328
329fn children() -> (Vec<Pid>, bool) {
334 if !*PROC_CHILDREN {
335 return (children_by_scan(), true);
336 }
337 let Ok(tasks) = std::fs::read_dir("/proc/self/task") else {
340 return (vec![], true);
341 };
342 let mut children = vec![];
343 let mut complete = true;
344 for task in tasks.flatten() {
345 let list = match std::fs::read_to_string(task.path().join("children")) {
348 Ok(list) => list,
349 Err(e) => {
350 if matches!(e.raw_os_error(), Some(libc::ENOENT | libc::ESRCH)) {
351 complete = false;
352 }
353 continue;
354 }
355 };
356 children.extend(
357 list.split_whitespace()
358 .filter_map(|pid| pid.parse().ok())
359 .map(Pid::from_raw),
360 );
361 }
362 (children, complete)
363}
364
365const EXIT_CODE_UNKNOWN: i32 = -1;
369
370pub(crate) const EXIT_CODE_WAIT_FAILED: i32 = -3;
376
377pub(crate) fn wait_exit(id: Id<'_>, flags: WaitPidFlag) -> nix::Result<Option<i32>> {
383 Ok(wait_child(id, flags)?.map(|(_, exit)| exit.code()))
384}
385
386pub(crate) fn signal_name(signal: i32) -> String {
389 Signal::try_from(signal).map_or_else(|_| signal.to_string(), |s| s.as_str().into())
390}
391
392#[derive(Clone, Copy)]
394enum ChildExit {
395 Exited(i32),
396 Signaled(i32),
397 Unknown,
398}
399
400impl ChildExit {
401 pub(crate) fn code(self) -> i32 {
403 match self {
404 Self::Exited(code) => code,
405 Self::Signaled(sig) => sig + 128,
406 Self::Unknown => EXIT_CODE_UNKNOWN,
407 }
408 }
409
410 fn log(self) {
413 match self {
414 Self::Exited(code) => debug!("Exited {code}"),
415 Self::Signaled(sig) => debug!("Signaled: {}", signal_name(sig)),
416 Self::Unknown => {}
417 }
418 }
419}
420
421#[expect(unsafe_code, reason = "nix does not convert real-time signals")]
424fn wait_child(id: Id<'_>, flags: WaitPidFlag) -> nix::Result<Option<(Pid, ChildExit)>> {
425 let (idtype, id) = match id {
426 Id::All => (libc::P_ALL, 0),
427 Id::Pid(pid) => (libc::P_PID, pid.as_raw().cast_unsigned()),
428 Id::PGid(pgid) => (libc::P_PGID, pgid.as_raw().cast_unsigned()),
429 Id::PIDFd(fd) => (libc::P_PIDFD, fd.as_raw_fd().cast_unsigned()),
430 _ => return Err(Errno::EINVAL),
432 };
433 let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
436 let flags = (WaitPidFlag::WEXITED | flags).bits();
437 Errno::result(unsafe { libc::waitid(idtype, id, &raw mut info, flags) })?;
439 let (pid, status) = unsafe { (info.si_pid(), info.si_status()) };
441 if pid == 0 {
442 return Ok(None);
443 }
444 let exit = match info.si_code {
445 libc::CLD_EXITED => ChildExit::Exited(status),
446 libc::CLD_KILLED | libc::CLD_DUMPED => ChildExit::Signaled(status),
447 code => {
450 debug!("Unexpected wait code {code} of PID {pid}");
451 ChildExit::Unknown
452 }
453 };
454 Ok(Some((Pid::from_raw(pid), exit)))
455}
456
457pub(crate) async fn wait_for_exit_code(pid: Pid) -> i32 {
460 debug!("Waiting for exit code of PID {pid}");
461
462 let pidfd = match pidfd_open(pid) {
464 Ok(pidfd) => Some(pidfd),
465 Err(Errno::ESRCH) => {
466 debug!("PID {pid} got reaped already");
467 return EXIT_CODE_UNKNOWN;
468 }
469 Err(_) => None,
470 };
471 wait_for_exit_with(pid, pidfd.as_ref().map(AsFd::as_fd)).await
472}
473
474pub(crate) async fn wait_for_exit(process: &Process) -> i32 {
479 match &process.pidfd {
480 Some(pidfd) => {
481 debug!("Waiting for exit code of PID {}", process.pid);
482 wait_for_exit_with(process.pid, Some(pidfd.as_fd())).await
483 }
484 None => wait_for_exit_code(process.pid).await,
485 }
486}
487
488async fn wait_for_exit_with(pid: Pid, pidfd: Option<BorrowedFd<'_>>) -> i32 {
491 if let Some(pidfd) = pidfd
492 && let Some(code) = try_pidfd_wait(pid, pidfd).await
493 {
494 return code;
495 }
496 debug!("Pidfd not available, falling back to blocking waitpid");
497 wait_for_exit_code_on_thread(pid).await
498}
499
500async fn wait_for_exit_code_on_thread(pid: Pid) -> i32 {
503 let (tx, rx) = oneshot::channel();
504 spawn_waitpid_thread(move || {
505 let _ = tx.send(wait_for_exit_code_blocking(pid));
506 });
507 rx.await.unwrap_or_else(|_| {
508 error!("Waitpid thread exited without a result");
509 EXIT_CODE_WAIT_FAILED
510 })
511}
512
513const WAITPID_STACK_SIZE: usize = 256 * 1024;
515
516fn spawn_waitpid_thread<F: FnOnce() + Send + 'static>(f: F) {
522 let slot = Arc::new(Mutex::new(Some(f)));
524 fn run<F: FnOnce()>(slot: &Mutex<Option<F>>) {
525 let f = lock(slot).take();
527 if let Some(f) = f {
528 f();
529 }
530 }
531
532 let thread_slot = slot.clone();
533 if let Err(e) = thread::Builder::new()
534 .name("waitpid".into())
535 .stack_size(WAITPID_STACK_SIZE)
536 .spawn(move || run(&thread_slot))
537 {
538 error!("Unable to spawn waitpid thread, using blocking pool: {e:#}");
539 task::spawn_blocking(move || run(&slot));
540 }
541}
542
543const PIDFD_RETRY_MIN: Duration = Duration::from_millis(10);
548const PIDFD_RETRY_MAX: Duration = Duration::from_secs(1);
549
550async fn try_pidfd_wait(pid: Pid, pidfd: BorrowedFd<'_>) -> Option<i32> {
553 let async_fd = match async_fd::register(pidfd, Interest::READABLE) {
556 Ok(fd) => fd,
557 Err(e) => {
558 error!("Unable to create AsyncFd from pidfd: {e:#}");
559 return None;
560 }
561 };
562
563 let mut ready = match async_fd.readable().await {
564 Ok(ready) => Some(ready),
565 Err(_) => return None,
566 };
567
568 let mut backoff = PIDFD_RETRY_MIN;
573 let exit = loop {
574 let reaping = reap_guard();
575 let pidfd = Id::PIDFd(async_fd.get_ref().as_fd());
576 match wait_child(pidfd, WaitPidFlag::WNOHANG) {
577 Err(Errno::EINVAL) => break wait_child(Id::Pid(pid), WaitPidFlag::WNOHANG),
578 Ok(None) => {}
581 exit => break exit,
582 }
583 drop(reaping);
584 if backoff == PIDFD_RETRY_MIN {
585 debug!("PID {pid} is not waitable yet, retrying until its tracer waited");
586 }
587 if let Some(mut ready) = ready.take() {
593 ready.clear_ready();
594 }
595 ready = time::timeout(backoff, async_fd.readable())
596 .await
597 .ok()
598 .and_then(Result::ok);
599 backoff = (backoff * 2).min(PIDFD_RETRY_MAX);
600 };
601 match exit {
602 Ok(Some((_, exit))) => {
603 exit.log();
604 Some(exit.code())
605 }
606 Ok(None) => {
607 debug!("PID {pid} is not waitable yet, not retrying by its PID");
608 Some(EXIT_CODE_UNKNOWN)
609 }
610 Err(Errno::ECHILD) => {
612 debug!("PID {pid} got reaped already");
613 Some(EXIT_CODE_UNKNOWN)
614 }
615 Err(e) => {
616 error!("Waitid after pidfd readable failed: {e:#}");
617 Some(EXIT_CODE_UNKNOWN)
618 }
619 }
620}
621
622fn wait_for_exit_code_blocking(pid: Pid) -> i32 {
625 loop {
626 match wait_exit(Id::Pid(pid), WaitPidFlag::WNOWAIT) {
627 Ok(_) => {}
628 Err(Errno::EINTR) => continue,
629 Err(Errno::ECHILD) => {
630 debug!("PID {pid} got reaped already");
631 return EXIT_CODE_WAIT_FAILED;
632 }
633 Err(err) => {
634 error!("Unable to wait for PID {pid}: {err:#}");
635 return EXIT_CODE_WAIT_FAILED;
636 }
637 }
638 let reaping = reap_guard();
639 let exit = wait_child(Id::Pid(pid), WaitPidFlag::WNOHANG);
640 drop(reaping);
641 match exit {
642 Ok(Some((_, exit))) => {
643 exit.log();
644 return exit.code();
645 }
646 Ok(None) => {}
647 Err(Errno::EINTR) => debug!("Failed to wait for PID {pid} on EINTR, retrying"),
648 Err(err) => {
649 error!("Unable to wait for PID {pid}: {err:#}");
650 return EXIT_CODE_WAIT_FAILED;
651 }
652 }
653 }
654}
655
656#[cfg(any(test, feature = "fuzzing"))]
658pub(crate) mod fuzzing;
659
660#[cfg(test)]
661mod tests;