1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
//! Take the test processes down with the runner.
//!
//! The owned Rust runner starts every libtest case in a process of its own and
//! waits for it. A signal that reaches the runner alone -- a supervisor's
//! SIGTERM, a `kill <pid>` from a script -- ended the runner and left the test
//! process running: on 2026-09-07 an orphaned tokio `test_tuning` spun on two
//! cores for an hour and starved every run that followed. Ctrl-C in a terminal
//! never showed this, because the shell signals the whole foreground process
//! group and the child dies with its parent.
//!
//! While a guard is installed, SIGHUP, SIGINT and SIGTERM first send SIGTERM
//! to every registered child, then let the signal take its default action, so
//! the runner exits exactly as it did before. The handler touches nothing but
//! a fixed table of atomics and `kill(2)`, both of which are safe inside a
//! signal handler.
//!
//! Windows delivers no signal a process can catch, but the kernel closes
//! every handle a dying process held: while a guard is installed, each
//! registered child is placed in a Job Object that kills its members when its
//! handle closes, so the test processes die with the runner however it ends.
use std::io;
use std::process::{Child, Command, ExitStatus, Output, Stdio};
use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
/// How many children can be registered at once. The runner runs at most one
/// child per worker thread, and machines with more cores than this are rare;
/// a child that finds no free slot is simply not tracked.
const SLOTS: usize = 512;
static CHILDREN: [AtomicI32; SLOTS] = [const { AtomicI32::new(0) }; SLOTS];
static GUARDS: AtomicUsize = AtomicUsize::new(0);
/// Send `signal` to every registered child. Safe to call from a signal
/// handler: it only loads atomics and calls `kill(2)`.
pub fn signal_registered_children(signal: i32) {
for slot in &CHILDREN {
let pid = slot.load(Ordering::SeqCst);
if pid > 0 {
#[cfg(unix)]
// SAFETY: `kill` with a positive pid signals that process only.
unsafe {
libc::kill(pid, signal);
}
#[cfg(not(unix))]
let _ = signal;
}
}
}
/// A child's registration; dropping it removes the child from the table.
pub struct RegisteredChild {
slot: Option<usize>,
}
impl Drop for RegisteredChild {
fn drop(&mut self) {
if let Some(slot) = self.slot {
CHILDREN[slot].store(0, Ordering::SeqCst);
}
}
}
/// Register a running child so a signal to the runner reaches it.
pub fn register(child: &Child) -> RegisteredChild {
let Ok(pid) = i32::try_from(child.id()) else {
return RegisteredChild { slot: None };
};
#[cfg(windows)]
windows_job::assign(child);
for (index, slot) in CHILDREN.iter().enumerate() {
if slot
.compare_exchange(0, pid, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
return RegisteredChild { slot: Some(index) };
}
}
RegisteredChild { slot: None }
}
#[cfg(windows)]
mod windows_job {
use std::process::Child;
use std::sync::Mutex;
use crate::process_supervision::JobHandle;
/// One job for the guard's lifetime; children assigned to it die when the
/// runner's handle closes, which the kernel does however the runner ends.
static JOB: Mutex<Option<JobHandle>> = Mutex::new(None);
fn job() -> std::sync::MutexGuard<'static, Option<JobHandle>> {
JOB.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn create() {
// Best effort: a runner that cannot create a job runs as it did.
if let Ok(handle) = JobHandle::new() {
*job() = Some(handle);
}
}
pub fn release() {
// Closing the handle kills whatever is still assigned; the runner has
// waited for every child by the time the outermost guard drops.
*job() = None;
}
pub fn assign(child: &Child) {
// A child already inside a job that forbids nesting stays outside
// ours; the runner behaves as it did before the guard existed.
if let Some(handle) = job().as_ref() {
let _ = handle.assign(child);
}
}
}
/// `Command::output()`, with the child registered while it runs: stdin
/// closed, stdout and stderr captured, exactly as `output()` does.
pub fn output(command: &mut Command) -> io::Result<Output> {
let child = command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let _registered = register(&child);
child.wait_with_output()
}
/// `Command::status()`, with the child registered while it runs.
pub fn status(command: &mut Command) -> io::Result<ExitStatus> {
let mut child = command.spawn()?;
let _registered = register(&child);
child.wait()
}
#[cfg(unix)]
extern "C" fn on_signal(signal: libc::c_int) {
signal_registered_children(libc::SIGTERM);
// The default action, delivered once this handler returns: the runner
// dies of the signal exactly as it did before the guard existed.
// SAFETY: both calls are async-signal-safe; `signal` came from the kernel.
unsafe {
libc::signal(signal, libc::SIG_DFL);
libc::raise(signal);
}
}
/// Installs the handlers for the guard's lifetime and restores the previous
/// dispositions when dropped. Guards nest: only the outermost installs.
pub struct ChildSignalGuard {
#[cfg(unix)]
previous: Vec<(libc::c_int, libc::sighandler_t)>,
}
impl ChildSignalGuard {
pub fn install() -> io::Result<Self> {
#[cfg(unix)]
{
let mut previous = Vec::new();
if GUARDS.fetch_add(1, Ordering::SeqCst) == 0 {
for signal in [libc::SIGHUP, libc::SIGINT, libc::SIGTERM] {
// SAFETY: installs a handler that is async-signal-safe
// (see `on_signal`); the old disposition is kept to restore.
let old = unsafe {
libc::signal(
signal,
on_signal as extern "C" fn(libc::c_int) as libc::sighandler_t,
)
};
if old == libc::SIG_ERR {
let error = io::Error::last_os_error();
for (installed, disposition) in previous.drain(..).rev() {
// SAFETY: restores a disposition `signal` returned.
unsafe {
libc::signal(installed, disposition);
}
}
GUARDS.fetch_sub(1, Ordering::SeqCst);
return Err(error);
}
previous.push((signal, old));
}
}
Ok(Self { previous })
}
#[cfg(not(unix))]
{
if GUARDS.fetch_add(1, Ordering::SeqCst) == 0 {
#[cfg(windows)]
windows_job::create();
}
Ok(Self {})
}
}
}
impl Drop for ChildSignalGuard {
fn drop(&mut self) {
#[cfg(unix)]
for (signal, disposition) in self.previous.drain(..).rev() {
// SAFETY: restores a disposition `signal` returned when installing.
unsafe {
libc::signal(signal, disposition);
}
}
let outermost = GUARDS.fetch_sub(1, Ordering::SeqCst) == 1;
#[cfg(windows)]
if outermost {
windows_job::release();
}
#[cfg(not(windows))]
let _ = outermost;
}
}
#[cfg(all(test, unix))]
mod tests {
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use super::*;
fn sleeper() -> Child {
Command::new("sleep")
.arg("30")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap()
}
fn exits_within(child: &mut Child, limit: Duration) -> bool {
let started = Instant::now();
while started.elapsed() < limit {
if child.try_wait().unwrap().is_some() {
return true;
}
std::thread::sleep(Duration::from_millis(20));
}
false
}
#[test]
fn a_registered_child_receives_the_signal_and_a_released_one_does_not() {
let _guard = ChildSignalGuard::install().unwrap();
let mut tracked = sleeper();
let mut released = sleeper();
let registration = register(&tracked);
drop(register(&released));
// What the handler does, without dying of the signal ourselves.
signal_registered_children(libc::SIGTERM);
assert!(
exits_within(&mut tracked, Duration::from_secs(5)),
"the registered child was not signalled"
);
assert!(
released.try_wait().unwrap().is_none(),
"a child whose registration was dropped must not be signalled"
);
drop(registration);
released.kill().unwrap();
released.wait().unwrap();
}
}