Skip to content
Merged
60 changes: 54 additions & 6 deletions packages/runtime-core/src/node/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,21 +579,30 @@ mod tests {
}
impl Job {
fn new(timeout_ms: u64) -> Self {
Self::spawn(timeout_ms, true)
}
/// `leader: false` spawns the child in this test process's group instead of its own.
/// Its identity then records `process_group = pid`, the leadership `verify` requires,
/// so every group signal is authorized but finds no such group (`ESRCH`, tolerated):
/// the child outlives each SIGKILL, as a kill that never takes effect would.
fn spawn(timeout_ms: u64, leader: bool) -> Self {
let root = crate::private_test_root(
&std::env::temp_dir().canonicalize().unwrap(),
"hkl-node-run",
);
let store = Store::open(&root, true).unwrap();
let id = "a".repeat(64);
super::super::test_support::job(&store, &id, "running", timeout_ms);
let mut child = Command::new("/bin/sh")
let mut command = Command::new("/bin/sh");
command
.args(["-c", "trap '' TERM; echo ready; read line; exit 3"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.process_group(0)
.spawn()
.unwrap();
.stderr(Stdio::piped());
if leader {
command.process_group(0);
}
let mut child = command.spawn().unwrap();
let stdin = child.stdin.take();
// Exactly the handshake line: run_child later drains the rest of stdout.
let mut ready = [0_u8; 6];
Expand All @@ -604,7 +613,9 @@ mod tests {
.read_exact(&mut ready)
.unwrap();
assert_eq!(&ready, b"ready\n");
let identity = super::super::ownership::observe(child.id() as i32).unwrap();
let mut identity = super::super::ownership::observe(child.id() as i32).unwrap();
assert_eq!(identity.process_group == identity.pid, leader);
identity.process_group = identity.pid;
Self {
root,
store,
Expand Down Expand Up @@ -800,4 +811,41 @@ mod tests {
drop(job.stdin.take());
assert_eq!(job.child.wait().unwrap().code(), Some(3));
}

/// Hang guard for a stop whose SIGKILL never takes effect. The correct path quarantines
/// about `TERMINATION_GRACE` after the first SIGKILL; only a grace that is re-anchored
/// on every later SIGKILL, and so never expires, should reach this.
const UNSETTLED_KILL_HANG_GUARD: Duration = Duration::from_secs(60);

/// HACK-1457: the grace after this owner's first SIGKILL is anchored once. A child that
/// outlives every SIGKILL is re-authorized and signalled again on each later pass; if
/// each of those re-anchored the grace, the termination deadline would never pass.
#[test]
fn repeated_kills_of_a_child_that_never_exits_keep_the_first_kill_anchor() {
use super::super::ownership::Observed;
let mut job = Job::spawn(50, false);
let pid = job.pid();
let recorded = job.identity.clone();
let begun = Instant::now();
let mut checks = 0;
let failure = job
.run(&mut |observed| {
assert_eq!(observed, pid);
assert!(
begun.elapsed() < UNSETTLED_KILL_HANG_GUARD,
"E_TEST_KILL_GRACE_REANCHORED"
);
checks += 1;
Ok(Observed::Present(recorded.clone()))
})
.unwrap_err();
assert_eq!(failure.message, "Child termination deadline exceeded.");
// The SIGTERM check, the first SIGKILL, and at least the SIGKILL of the pass that
// quarantined: a re-anchoring regression had a later SIGKILL to re-anchor on.
assert!(checks >= 3, "E_TEST_KILL_NOT_REPEATED");
// Every group signal found no group: the child still waits for its input.
assert!(!exit_published(pid));
drop(job.stdin.take());
assert_eq!(job.child.wait().unwrap().code(), Some(3));
}
}
22 changes: 18 additions & 4 deletions packages/runtime-core/src/node/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,13 +338,27 @@ mod tests {
.spawn()
.unwrap();
let pid = child.id() as i32;
// The receipt differs from this live child's identity only in its boot format
// (HACK-1457): same PID, UID, group and start, so nothing but the retired format
// can be why this identity never authorizes a signal.
let live = super::super::ownership::observe(pid).unwrap();
let legacy = crate::node::ProcessIdentity {
pid,
uid: unsafe { libc::geteuid() },
process_group: pid,
start: 1,
boot: "1700000000:250000".into(),
..live.clone()
};
assert_ne!(legacy.boot, live.boot);
// The only signal authority any owner has is `verify`, and it refuses this
// identity for its format alone: against the live child, and against an observer
// that still reports the retired format, which every other field matches.
for observed in [&live, &legacy] {
assert_eq!(
super::super::ownership::verify(&legacy, observed)
.unwrap_err()
.message,
"Owned child identity uses a retired boot format; no signal sent.",
"E_TEST_RETIRED_FORMAT_NOT_REFUSED"
);
}
let id = "b".repeat(64);
super::super::test_support::job(&store, &id, "running", 60_000);
store
Expand Down
Loading
Loading