rightkit_child_process/unix_child.rs
1//! The Unix child shared by `OwnedChild` and every `ProcessHandle` clone, and the one place a
2//! signal to a child or its process group is sent.
3//!
4//! # Identity rule
5//!
6//! A pid, and with it the process-group id the child leads, cannot be recycled while the
7//! child is a zombie: the kernel releases the number only when the parent reaps it, and only
8//! this process can reap its own child. So a group signal is safe exactly while this process
9//! still holds the leader as its own **unreaped** child.
10//!
11//! * Every reap goes through [`UnixChild`], under the lock that guards it. The std `Child` is
12//! private, so no path can reap behind this type's back.
13//! * Every group signal is sent under that same lock, after the leader is confirmed unreaped,
14//! and therefore before any reap that could follow it.
15//! * After any reap, by any path (a wait, a `try_wait`, a handle or a clone, the detached
16//! reaper, the public [`OwnedChild::child`](crate::OwnedChild::child) accessor), the
17//! state is `Reaped` for good. Group signalling is over for this child and all its clones:
18//! no signal, and no signal-0 membership probe, is ever sent again.
19//!
20//! * **Unreaped by this type is not unreaped by the kernel.** Other code in the process (a
21//! `waitpid(-1)` loop) can collect our child behind our back, and a replacement child can then
22//! take the same number. So every action that names the pid (a signal, a `waitpid`, a
23//! `waitid` probe, a membership probe) is preceded by [`UnixChild::prove`], which compares the
24//! kernel's description of the pid (parent and start time, read at spawn) with the process
25//! we spawned. A different or missing description marks the leader `Lost` and nothing is
26//! sent or waited. The proof and the call that follows it are two system calls: foreign code
27//! that reaps the child in exactly that gap, and a replacement that takes the number there,
28//! defeat it. This is a known limit; see `identity`.
29//!
30//! # Waiting
31//!
32//! * **A wait on a shared child is a loop of non-blocking looks.** [`wait_shared`] takes the
33//! lock, makes one non-blocking `try_wait` (a `waitpid(WNOHANG)`), releases the lock, sleeps,
34//! and looks again until the leader is reaped or the caller's deadline passes.
35//! [`observe_shared`] does the same with the non-reaping `waitid(WNOHANG | WNOWAIT)` probe.
36//! `ProcessHandle::wait`, `wait_timeout` and `wait_exited*`, `OwnedChild::wait*`,
37//! `wait_or_kill`, `terminate_tree*`, `Drop`, the detached reaper thread
38//! (`rightkit-detached-reap`) and, up to its timeout, the failed-spawn cleanup all wait this
39//! way.
40//! None of them reaps with a blocking call or sleeps while holding the lock, so `kill`,
41//! `terminate_tree` and every other waiter run while one of them is in progress, and a
42//! deadline is honoured whatever the child does.
43//! * The sleep is [`ExitWatch::pause`](crate::exit_watch::ExitWatch::pause): a hint. With an
44//! event source it ends early at the child's exit; without one it is a 5 ms interval. The
45//! event never decides anything, so it cannot make a wait return early or hang.
46//! * **Where the lock is held across a wait or a long action (the complete list).** None of
47//! these is a bounded wait that entered a blocking reap; the first is the only one a
48//! deadline-bearing caller on another thread can be held behind.
49//! 1. [`UnixChild::wait`] through the `OwnedChild::child()` guard. The caller holds the lock
50//! by holding the guard, so the wait cannot release it: a bounded wait on a clone, a
51//! `kill`, `terminate_tree` or `Drop` on another thread waits behind it until the child
52//! has exited, and can exceed its own deadline. That is the caller's choice, not the
53//! crate's: `std::process::Child::wait` through the same guard did exactly this in
54//! 0.3.3 (documented: "release this guard before another accessor or exit reader").
55//! The wait still reaps only with non-blocking looks. A program that needs a deadline
56//! or a cancel to stay live must wait through `OwnedChild`/`ProcessHandle` instead.
57//! 2. `cleanup_direct` with no timeout (`partial_spawn_cleanup_timeout` not set), and
58//! 3. the `rightkit-partial-reap` thread it hands a timed-out child to. Both call
59//! `UnixChild::wait` under the lock, as 0.3.3 called `std::process::Child::wait`. They
60//! run only for a child whose spawn failed after the process existed, and that `Arc`
61//! is held by nothing but the spawning thread (the partial `OwnedChild`, which no
62//! caller has seen and from which no `ProcessHandle` was cloned) and then by the reaper
63//! thread: no other thread can reach the lock, so nothing can be held behind it.
64//! 4. `OwnedChild::while_unreaped` holds the lock while the caller's closure runs, which
65//! is its purpose (no reap can fall between the proof and the action). A bounded wait on
66//! another thread can be delayed by as long as the closure takes.
67//! `rightkit-sandbox` passes a descendant sweep with a 200 ms grace. New since 0.3.3.
68//!
69//! Unidentified spawn rejection uses `cleanup_unidentified`: runner callers supply a finite
70//! cleanup budget; ordinary commands retain their existing unlimited cleanup default.
71//! * No wait-class system call names the pid while the lock is released. A blocked
72//! `waitpid(pid)`/`waitid(P_PID, pid)` outside the lock could be entered after another reader
73//! reaped the leader and the kernel recycled the number, and would then wait on an unrelated
74//! child.
75//! * Seeing that the leader exited does not require reaping it (`UnixChild::leader_state`), so
76//! an exit monitor can observe the exit and leave the reap, and with it the group's identity,
77//! to the owner.
78//!
79//! The state is one lock-guarded enum, never separate atomics, so no thread can observe a
80//! half-published transition.
81
82use crate::exit_watch::ExitWatch;
83use crate::identity::{Origin, Proof};
84use crate::process_handle::lock_child;
85use std::{
86 io,
87 process::{Child, ChildStderr, ChildStdin, ChildStdout, ExitStatus},
88 sync::{Arc, Mutex},
89 time::{Duration, Instant},
90};
91
92/// Whether this host can see that the leader exited without reaping it (`probe_exit` is real).
93/// Where it cannot, `ProcessHandle::wait_exited` falls back to a reaping wait.
94pub(crate) const CAN_OBSERVE_WITHOUT_REAPING: bool = cfg!(any(
95 target_os = "linux",
96 target_os = "android",
97 target_os = "macos",
98 target_os = "ios"
99));
100
101/// What this process knows about the leader. Only [`UnixChild`] changes it, under the lock.
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103enum Leader {
104 /// Not reaped by this process: the zombie, or the running process, still pins the id.
105 Unreaped,
106 /// Reaped through this type; the cached status is returned to every later reader.
107 Reaped(ExitStatus),
108 /// The kernel says the pid is no longer our child although we never reaped it (something
109 /// else in the process reaped it). Treated as reaped; the status is lost.
110 Lost,
111}
112
113/// Where the leader is, as far as a non-reaping look can tell.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub(crate) enum LeaderState {
116 Running,
117 /// Exited but not reaped: a zombie that still pins the id. The last moment a group
118 /// signal is possible.
119 Exited,
120 Reaped,
121}
122
123/// Who is in the leader's process group, besides the leader.
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub(crate) enum Members {
126 Pids(Vec<u32>),
127 /// The host cannot enumerate the group (or it is larger than one listing).
128 Unknown,
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub(crate) enum SignalTarget {
133 /// `kill(-pgid, ..)`: the leader's whole process group.
134 Group,
135 /// `kill(pid, ..)`: the leader alone (detached children, which own only their pid).
136 Leader,
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub(crate) enum SignalOutcome {
141 Sent,
142 /// The leader was already reaped, so nothing was sent.
143 Retired,
144 /// The target was already gone (`ESRCH`, or only zombies were left).
145 Gone,
146 /// A group signal answered `EPERM` while the leader still reads as running (macOS does
147 /// this when the last members are mid-exit). Nothing was sent. The caller may look again
148 /// after the lock is released: the group is gone as soon as the exit completes.
149 MidExit,
150}
151
152/// A spawned Unix child. The same API as Windows' `WindowsChild`: public stdio fields, `id`,
153/// `try_wait`, `wait` and `kill`. It does not expose the std `Child`, because a std reap the
154/// crate cannot see would make later group signals unsafe (see the module documentation).
155///
156/// Obtained through [`OwnedChild::child`](crate::OwnedChild::child), which holds the shared
157/// lock while the guard lives. `wait` through the guard therefore holds that lock until the
158/// child has exited, and every other thread's access to the same child (a bounded wait on a
159/// clone, `kill`, `terminate_tree`, `Drop`) waits behind it, as `std::process::Child::wait`
160/// through the guard did in 0.3.3. Prefer `OwnedChild::wait`, `wait_timeout` or
161/// `ProcessHandle::wait`, which release the lock between looks.
162#[derive(Debug)]
163pub struct UnixChild {
164 pub stdin: Option<ChildStdin>,
165 pub stdout: Option<ChildStdout>,
166 pub stderr: Option<ChildStderr>,
167 child: Child,
168 pid: u32,
169 leader: Leader,
170 /// Who the leader was at spawn. Checked by [`UnixChild::prove`] before any action by pid.
171 origin: Origin,
172 /// The wake-up hint for waiters (never a verdict), opened at spawn and shared with every
173 /// waiter.
174 watch: Arc<ExitWatch>,
175 /// Test only: the id used as the signal target, to stand in for a recycled id.
176 #[cfg(test)]
177 recycled_id: Option<u32>,
178}
179
180impl UnixChild {
181 pub(crate) fn new(mut child: Child) -> Self {
182 // Both are taken now, while the child is certainly still ours: the hint is bound to the
183 // process, and the identity names it. (A child collected by foreign code before this
184 // point would hand its replacement's description over as the baseline; that is part of
185 // the known limit described in `identity`.)
186 let watch = Arc::new(ExitWatch::open(child.id()));
187 let origin = Origin::capture(child.id());
188 Self {
189 stdin: child.stdin.take(),
190 stdout: child.stdout.take(),
191 stderr: child.stderr.take(),
192 pid: child.id(),
193 origin,
194 watch,
195 child,
196 leader: Leader::Unreaped,
197 #[cfg(test)]
198 recycled_id: None,
199 }
200 }
201
202 /// The wake-up hint, for waiters that sleep without holding the lock.
203 pub(crate) fn exit_watch(&self) -> Arc<ExitWatch> {
204 Arc::clone(&self.watch)
205 }
206
207 pub fn id(&self) -> u32 {
208 self.pid
209 }
210
211 /// Spawn-time check: a host that has an identity source but would not describe the child
212 /// cannot prove later that its pid is still the child's, so the child is not handed out.
213 /// Caller must stop & reap a rejected child through `cleanup_unidentified` before returning.
214 pub(crate) fn require_identity(&mut self) -> io::Result<()> {
215 if !matches!(self.origin, Origin::Missing) {
216 return Ok(());
217 }
218 Err(io::Error::other(
219 "the spawned child could not be identified; it must be stopped because a later \
220 signal could not be proven to reach it",
221 ))
222 }
223
224 /// Used only immediately after creation when identity capture failed. std Child owns
225 /// this just-created child, so kill/try_wait are directed at it before any wrapper escapes.
226 /// A finite caller budget transfers an unreaped child to an exclusive background reaper.
227 pub(crate) fn cleanup_unidentified(mut self, timeout: Option<Duration>) -> bool {
228 let _ = self.child.kill();
229 let Some(timeout) = timeout else {
230 return self.child.wait().is_ok();
231 };
232 let deadline = std::time::Instant::now().checked_add(timeout);
233 #[cfg(test)]
234 let refused = crate::owned::cleanup_test_fault::refuse(timeout);
235 #[cfg(not(test))]
236 let refused = false;
237 let reaped = if refused {
238 Ok(None)
239 } else {
240 self.watch.wait_for(deadline, || self.child.try_wait())
241 };
242 if matches!(reaped, Ok(Some(_))) {
243 return true;
244 }
245 let mut child = self.child;
246 let _ = std::thread::Builder::new()
247 .name("rightkit-partial-reap".into())
248 .spawn(move || {
249 let _ = child.wait();
250 });
251 false
252 }
253
254 /// The one proof every action by pid starts with: `Ok(true)` when the pid still names the
255 /// process this type spawned and has not reaped. `Ok(false)` (and the leader becomes
256 /// `Lost`, never touched again) when the number changed hands or the process is gone.
257 /// `Err` when the kernel cannot be asked: the caller must then act on nothing.
258 fn prove(&mut self) -> io::Result<bool> {
259 if !matches!(self.leader, Leader::Unreaped) {
260 return Ok(false);
261 }
262 #[cfg(test)]
263 trace::record(trace::Event::Proof { leader: self.pid });
264 if self.origin.check(self.pid)? == Proof::Same {
265 Ok(true)
266 } else {
267 self.leader = Leader::Lost;
268 Ok(false)
269 }
270 }
271
272 /// `prove`, as the guard of a wait: an unproven pid is reported as `ECHILD`, as the kernel
273 /// would for a child that is not ours.
274 fn require_ours(&mut self) -> io::Result<()> {
275 if self.prove()? {
276 Ok(())
277 } else {
278 Err(io::Error::from_raw_os_error(libc::ECHILD))
279 }
280 }
281
282 /// Test only: pretend the pid was described differently at spawn, as if our child had been
283 /// collected elsewhere and the number given to the process now holding it.
284 #[cfg(test)]
285 pub(crate) fn simulate_replaced(&mut self) {
286 self.origin = Origin::Known(crate::identity::Identity {
287 parent: 0,
288 started_sec: 1,
289 started_usec: 1,
290 });
291 }
292
293 /// Non-blocking. A `Some` result means the leader has been reaped, and ends group
294 /// signalling for this child.
295 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
296 match self.leader {
297 Leader::Reaped(status) => return Ok(Some(status)),
298 Leader::Lost => return Err(io::Error::from_raw_os_error(libc::ECHILD)),
299 Leader::Unreaped => {}
300 }
301 if let Err(error) = self.require_ours() {
302 return Err(self.note_error(error));
303 }
304 #[cfg(test)]
305 trace::record(trace::Event::Wait { leader: self.pid });
306 match self.child.try_wait() {
307 Ok(Some(status)) => {
308 self.note_reaped(status);
309 Ok(Some(status))
310 }
311 Ok(None) => Ok(None),
312 Err(error) => Err(self.note_error(error)),
313 }
314 }
315
316 /// Closes stdin as std does, then waits until the leader is reaped. The reap itself is only
317 /// ever a non-blocking `waitpid(WNOHANG)` between sleeps, but this method cannot release
318 /// the lock: when it is reached through the `OwnedChild::child()` guard the caller holds
319 /// it for the whole wait, so other threads' waits, kills and drops on the same child
320 /// queue behind it (as with `std::process::Child::wait` in 0.3.3). Use `OwnedChild::wait`
321 /// or `ProcessHandle::wait` where that matters.
322 pub fn wait(&mut self) -> io::Result<ExitStatus> {
323 drop(self.stdin.take());
324 self.wait_reaping()
325 }
326
327 /// `SIGKILL` to the leader alone, only while it is unreaped. Nothing is sent, and `Ok` is
328 /// returned, once it has been reaped.
329 pub fn kill(&mut self) -> io::Result<()> {
330 self.signal(SignalTarget::Leader, libc::SIGKILL).map(|_| ())
331 }
332
333 /// Reaps without touching stdin, for the guard's `wait` and for cleanup of a child that
334 /// has just been killed. Each look is proven and non-blocking; between looks the thread
335 /// sleeps on the hint. A blocking `waitpid(pid)` from the start would name the number for
336 /// as long as it sleeps, and if other code collected the leader meanwhile it would wait on
337 /// whatever child the kernel gave the number to, and take its status.
338 pub(crate) fn wait_reaping(&mut self) -> io::Result<ExitStatus> {
339 let watch = Arc::clone(&self.watch);
340 watch
341 .wait_for(None, || self.try_wait())?
342 .ok_or_else(|| io::Error::other("an unbounded wait ended without a status"))
343 }
344
345 /// Looks at the leader without reaping it. `Lost` is recorded here when the kernel no
346 /// longer lists the pid as our child.
347 pub(crate) fn leader_state(&mut self) -> io::Result<LeaderState> {
348 if !self.prove()? {
349 return Ok(LeaderState::Reaped);
350 }
351 match probe_exit(self.pid) {
352 // The probe names the number once more; a replacement that took it in between and
353 // has itself exited would read as our zombie. A zombie is describable, so ask again.
354 Ok(true) => Ok(if self.prove()? {
355 LeaderState::Exited
356 } else {
357 LeaderState::Reaped
358 }),
359 Ok(false) => Ok(LeaderState::Running),
360 Err(error) if error.raw_os_error() == Some(libc::ECHILD) => {
361 self.leader = Leader::Lost;
362 Ok(LeaderState::Reaped)
363 }
364 Err(error) => Err(error),
365 }
366 }
367
368 /// Sends `signal` only if the leader is still unreaped, and does so under the caller's
369 /// lock, so no reap can fall between the check and the signal.
370 pub(crate) fn signal(
371 &mut self,
372 target: SignalTarget,
373 signal: i32,
374 ) -> io::Result<SignalOutcome> {
375 if self.leader_state()? == LeaderState::Reaped {
376 return Ok(SignalOutcome::Retired);
377 }
378 // The state above looked at the pid; prove it once more immediately before it is used
379 // as a signal target.
380 if !self.prove()? {
381 return Ok(SignalOutcome::Retired);
382 }
383 let id = self.signal_id() as i32;
384 let target_id = match target {
385 SignalTarget::Group => -id,
386 SignalTarget::Leader => id,
387 };
388 if raw_kill(self.pid, target_id, signal) == 0 {
389 return Ok(SignalOutcome::Sent);
390 }
391 let error = io::Error::last_os_error();
392 match error.raw_os_error() {
393 Some(libc::ESRCH) => Ok(SignalOutcome::Gone),
394 // macOS answers a group signal with EPERM, not ESRCH, when no member can take it:
395 // only zombies are left, or the last members are mid-exit. Our own children are
396 // always signalable, so this is never a real denial. Once the leader is no longer
397 // running (exited, or reaped by a handle clone) the group is gone. A leader that
398 // still reads as running is mid-exit: say so, and let the caller look again once it
399 // has released the lock (`OwnedChild::signal`).
400 Some(libc::EPERM) if target == SignalTarget::Group => {
401 Ok(if self.leader_state()? == LeaderState::Running {
402 SignalOutcome::MidExit
403 } else {
404 SignalOutcome::Gone
405 })
406 }
407 _ => Err(error),
408 }
409 }
410
411 /// The processes other than the leader still in the group.
412 /// Valid only while the leader is unreaped; a reaped leader has no group to speak of. An
413 /// unknown answer is [`Members::Unknown`]: the caller then waits out its grace instead of
414 /// cutting it short.
415 pub(crate) fn group_other_members(&mut self) -> io::Result<Members> {
416 if self.leader_state()? == LeaderState::Reaped {
417 return Ok(Members::Pids(Vec::new()));
418 }
419 group_members_besides(self.pid, self.signal_id())
420 }
421
422 /// True when a process other than the leader is still in the group (unknown counts as yes).
423 #[cfg(test)]
424 pub(crate) fn group_has_other_members(&mut self) -> io::Result<bool> {
425 Ok(match self.group_other_members()? {
426 Members::Pids(pids) => !pids.is_empty(),
427 Members::Unknown => true,
428 })
429 }
430
431 fn signal_id(&self) -> u32 {
432 #[cfg(test)]
433 if let Some(id) = self.recycled_id {
434 return id;
435 }
436 self.pid
437 }
438
439 /// Test only: aim every later signal at `id`, as if the kernel had recycled the leader's
440 /// pid and group id for an unrelated group.
441 #[cfg(test)]
442 pub(crate) fn simulate_recycled_id(&mut self, id: u32) {
443 self.recycled_id = Some(id);
444 }
445
446 fn note_reaped(&mut self, status: ExitStatus) {
447 self.leader = Leader::Reaped(status);
448 #[cfg(test)]
449 trace::record(trace::Event::Reaped { leader: self.pid });
450 }
451
452 fn note_error(&mut self, error: io::Error) -> io::Error {
453 if error.raw_os_error() == Some(libc::ECHILD) {
454 self.leader = Leader::Lost;
455 }
456 error
457 }
458}
459
460/// `waitid(P_PID, pid, WEXITED | WNOHANG | WNOWAIT)`: has the child exited, without reaping
461/// it? `ECHILD` means the pid is not (or no longer) our child.
462#[cfg(any(
463 target_os = "linux",
464 target_os = "android",
465 target_os = "macos",
466 target_os = "ios"
467))]
468fn probe_exit(pid: u32) -> io::Result<bool> {
469 loop {
470 #[cfg(test)]
471 trace::record(trace::Event::Wait { leader: pid });
472 // SAFETY: an all-zero siginfo_t is valid, and waitid fills it.
473 let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
474 // SAFETY: valid out pointer; WNOWAIT leaves the zombie in place.
475 let result = unsafe {
476 libc::waitid(
477 libc::P_PID,
478 pid as libc::id_t,
479 &mut info,
480 libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
481 )
482 };
483 if result == 0 {
484 #[cfg(any(target_os = "linux", target_os = "android"))]
485 // SAFETY: si_pid is valid after a successful waitid.
486 let reported = unsafe { info.si_pid() };
487 #[cfg(not(any(target_os = "linux", target_os = "android")))]
488 let reported = info.si_pid;
489 return Ok(reported as u32 == pid);
490 }
491 let error = io::Error::last_os_error();
492 if error.raw_os_error() != Some(libc::EINTR) {
493 return Err(error);
494 }
495 }
496}
497
498/// Other Unix hosts have no verified non-reaping probe here. The reaped state still guards
499/// every signal; only the early "leader exited" observation is unavailable.
500#[cfg(not(any(
501 target_os = "linux",
502 target_os = "android",
503 target_os = "macos",
504 target_os = "ios"
505)))]
506fn probe_exit(_pid: u32) -> io::Result<bool> {
507 Ok(false)
508}
509
510/// The single place a signal reaches a child or its group, so tests can observe every one
511/// (signal 0 included).
512fn raw_kill(leader: u32, target: i32, signal: i32) -> i32 {
513 #[cfg(test)]
514 trace::record(trace::Event::Signal {
515 leader,
516 target,
517 signal,
518 });
519 #[cfg(not(test))]
520 let _ = leader;
521 // SAFETY: plain kill(2). The callers hold the lock and have just confirmed the leader is
522 // unreaped, which pins the target id.
523 unsafe { libc::kill(target, signal) }
524}
525
526#[cfg(target_os = "macos")]
527fn group_members_besides(leader: u32, group: u32) -> io::Result<Members> {
528 /// `PROC_PGRP_ONLY` from `<libproc.h>`: list the members of one process group.
529 const PROC_PGRP_ONLY: u32 = 2;
530 const CAPACITY: usize = 256;
531 let mut pids = [0 as libc::pid_t; CAPACITY];
532 // SAFETY: the buffer is CAPACITY pids and the size passed says so.
533 let bytes = unsafe {
534 libc::proc_listpids(
535 PROC_PGRP_ONLY,
536 group,
537 pids.as_mut_ptr().cast(),
538 std::mem::size_of_val(&pids) as libc::c_int,
539 )
540 };
541 if bytes <= 0 {
542 // Fall back to signal 0: ESRCH, or EPERM when only zombies remain, means empty.
543 if raw_kill(leader, -(group as i32), 0) == 0 {
544 return Ok(Members::Unknown);
545 }
546 return Ok(match io::Error::last_os_error().raw_os_error() {
547 Some(libc::ESRCH) | Some(libc::EPERM) => Members::Pids(Vec::new()),
548 _ => Members::Unknown,
549 });
550 }
551 let count = (bytes as usize / std::mem::size_of::<libc::pid_t>()).min(CAPACITY);
552 if count >= CAPACITY {
553 return Ok(Members::Unknown);
554 }
555 Ok(Members::Pids(
556 pids[..count]
557 .iter()
558 .filter(|&&pid| pid > 0 && pid as u32 != leader)
559 .map(|&pid| pid as u32)
560 .collect(),
561 ))
562}
563
564#[cfg(any(target_os = "linux", target_os = "android"))]
565fn group_members_besides(leader: u32, group: u32) -> io::Result<Members> {
566 // kill(-pgid, 0) cannot tell an exited, unreaped leader from a live member, so read
567 // /proc: a member is any non-zombie process whose group is this one.
568 let Ok(entries) = std::fs::read_dir("/proc") else {
569 return Ok(Members::Unknown);
570 };
571 let mut members = Vec::new();
572 for entry in entries.flatten() {
573 let Some(pid) = entry
574 .file_name()
575 .to_str()
576 .and_then(|name| name.parse::<u32>().ok())
577 else {
578 continue;
579 };
580 if pid == leader {
581 continue;
582 }
583 let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
584 continue; // exited while we looked
585 };
586 let Some(rest) = stat.rsplit_once(')').map(|(_, rest)| rest) else {
587 return Ok(Members::Unknown);
588 };
589 let mut fields = rest.split_whitespace();
590 let (Some(state), Some(_parent), Some(pgrp)) =
591 (fields.next(), fields.next(), fields.next())
592 else {
593 return Ok(Members::Unknown);
594 };
595 if pgrp.parse::<u32>().ok() == Some(group) && state != "Z" && state != "X" {
596 members.push(pid);
597 }
598 }
599 Ok(Members::Pids(members))
600}
601
602#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
603fn group_members_besides(_leader: u32, _group: u32) -> io::Result<Members> {
604 Ok(Members::Unknown)
605}
606
607/// Waits for the leader of `shared` to be reaped, or for `deadline` to pass (`Ok(None)`); `None`
608/// waits without limit.
609///
610/// Every look is one non-blocking `try_wait` under the lock, released before the thread sleeps
611/// (see the module documentation), so a deadline is honoured whatever the child does, a
612/// `kill` or `terminate_tree` from another thread runs at once, and no wait-class system call
613/// names the pid while the lock is free. `ProcessHandle` waits this way, and so does the
614/// failed-spawn cleanup that has a timeout; the cleanup without one waits through
615/// `UnixChild::wait` instead (see the module documentation, item 2).
616pub(crate) fn wait_shared(
617 shared: &Mutex<UnixChild>,
618 deadline: Option<Instant>,
619) -> io::Result<Option<ExitStatus>> {
620 let (watch, pid) = {
621 let child = lock_child(shared);
622 (child.exit_watch(), child.id())
623 };
624 let mut first = true;
625 watch.wait_for(deadline, || {
626 let looked = lock_child(shared).try_wait();
627 pause_for_test(&mut first, pid, matches!(looked, Ok(None)));
628 looked
629 })
630}
631
632/// Like [`wait_shared`] but does not reap: `Ok(true)` once the leader has exited (or was reaped
633/// by someone), `Ok(false)` when `deadline` passed first. The zombie keeps pinning the group id.
634pub(crate) fn observe_shared(
635 shared: &Mutex<UnixChild>,
636 deadline: Option<Instant>,
637) -> io::Result<bool> {
638 let (watch, pid) = {
639 let child = lock_child(shared);
640 (child.exit_watch(), child.id())
641 };
642 let mut first = true;
643 let seen = watch.wait_for(deadline, || {
644 let state = lock_child(shared).leader_state();
645 pause_for_test(&mut first, pid, matches!(state, Ok(LeaderState::Running)));
646 Ok(match state? {
647 LeaderState::Running => None,
648 LeaderState::Exited | LeaderState::Reaped => Some(()),
649 })
650 })?;
651 Ok(seen.is_some())
652}
653
654/// Test seam: after the first look that found the child still running, with the lock already
655/// released and before the thread sleeps, run the hook installed for `pid` (see [`gate`]).
656#[cfg(test)]
657fn pause_for_test(first: &mut bool, pid: u32, running: bool) {
658 if *first && running {
659 *first = false;
660 gate::pause(pid);
661 }
662}
663
664#[cfg(not(test))]
665fn pause_for_test(_first: &mut bool, _pid: u32, _running: bool) {}
666
667/// Test instrumentation: every signal, every reap and every wait-class system call that names
668/// the leader's pid, in the order they happened. All are recorded under the child's lock, so the
669/// order is the real order.
670#[cfg(test)]
671pub(crate) mod trace {
672 use std::sync::Mutex;
673
674 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
675 pub(crate) enum Event {
676 Signal {
677 leader: u32,
678 target: i32,
679 signal: i32,
680 },
681 Reaped {
682 leader: u32,
683 },
684 /// The pid was proven to still name the spawned process (see `UnixChild::prove`).
685 Proof {
686 leader: u32,
687 },
688 /// A `waitpid`/`waitid` (probe or reap) was issued naming this pid. After `Reaped` the
689 /// number may belong to someone else, so none may follow.
690 Wait {
691 leader: u32,
692 },
693 }
694
695 static EVENTS: Mutex<Vec<Event>> = Mutex::new(Vec::new());
696
697 pub(crate) fn record(event: Event) {
698 EVENTS.lock().unwrap_or_else(|p| p.into_inner()).push(event);
699 }
700
701 pub(crate) fn mark() -> usize {
702 EVENTS.lock().unwrap_or_else(|p| p.into_inner()).len()
703 }
704
705 /// Signals and reaps about `leader` recorded since `mark` (wait calls are left out; see
706 /// [`since_with_waits`]).
707 pub(crate) fn since(mark: usize, leader: u32) -> Vec<Event> {
708 since_with_waits(mark, leader)
709 .into_iter()
710 .filter(|event| !matches!(event, Event::Wait { .. } | Event::Proof { .. }))
711 .collect()
712 }
713
714 /// Every event about `leader` recorded since `mark`, wait calls included.
715 pub(crate) fn since_with_waits(mark: usize, leader: u32) -> Vec<Event> {
716 EVENTS.lock().unwrap_or_else(|p| p.into_inner())[mark..]
717 .iter()
718 .copied()
719 .filter(|event| match event {
720 Event::Signal { leader: l, .. }
721 | Event::Reaped { leader: l }
722 | Event::Proof { leader: l }
723 | Event::Wait { leader: l } => *l == leader,
724 })
725 .collect()
726 }
727}
728
729/// Test seam for waiters: a pause point a test can use to hold a waiter at the exact moment
730/// the old blocking wait had checked the shared state and was about to name the pid.
731#[cfg(test)]
732pub(crate) mod gate {
733 use std::sync::{Arc, Mutex};
734
735 type Hook = Arc<dyn Fn() + Send + Sync>;
736
737 static HOOKS: Mutex<Vec<(u32, Hook)>> = Mutex::new(Vec::new());
738
739 /// Runs `hook` each time a waiter on `pid` reaches its pause point.
740 pub(crate) fn install(pid: u32, hook: impl Fn() + Send + Sync + 'static) {
741 let mut hooks = HOOKS.lock().unwrap_or_else(|p| p.into_inner());
742 hooks.retain(|(id, _)| *id != pid);
743 hooks.push((pid, Arc::new(hook)));
744 }
745
746 pub(crate) fn remove(pid: u32) {
747 HOOKS
748 .lock()
749 .unwrap_or_else(|p| p.into_inner())
750 .retain(|(id, _)| *id != pid);
751 }
752
753 /// Called by a waiter after it released the child lock and before it sleeps.
754 pub(crate) fn pause(pid: u32) {
755 let hook = HOOKS
756 .lock()
757 .unwrap_or_else(|p| p.into_inner())
758 .iter()
759 .find(|(id, _)| *id == pid)
760 .map(|(_, hook)| Arc::clone(hook));
761 if let Some(hook) = hook {
762 hook();
763 }
764 }
765}