fix(nesinit): a pid stops being the workload's the moment it is reaped

A pid is only a name for a process until that process is reaped; after that
the kernel may hand the same number to something else. The handle kept the
number, so a stop or a kill issued during shutdown — which every session
outcome reaches — could land on a process nobody meant, and the one aimed at
the workload would have been a SIGKILL.

The registry now clears the flag as it delivers the exit, in the same call, and
nothing signals a pid whose flag is down. Waiting for the workload on the way
out takes the same answer: already reaped is already gone.

There is a window left, between the reap and the delivery, and closing it
entirely needs a handle the kernel keeps rather than a number. Recorded rather
than papered over.
This commit is contained in:
KAAL1
2026-09-05 10:03:28 +03:00
parent cb8f37a0e4
commit 071241f944
3 changed files with 129 additions and 26 deletions

View File

@@ -115,9 +115,10 @@ fn an_exit_reaches_whoever_asked_for_it() {
let _alone = alone();
let waiters = Waiters::new();
let mut exited = waiters
let mut watched = waiters
.watch(|| Ok(fork_child(|| unsafe { libc::_exit(9) })))
.expect("the child never started");
let mut exited = watched.take_exit().unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
@@ -154,7 +155,7 @@ fn an_exit_that_happens_before_the_caller_is_registered_is_not_lost() {
}
});
let mut exited = waiters
let mut watched = waiters
.watch(|| {
let pid = fork_child(|| unsafe { libc::_exit(5) });
// Long enough for the child to exit and the reaper to reach it.
@@ -162,6 +163,7 @@ fn an_exit_that_happens_before_the_caller_is_registered_is_not_lost() {
Ok(pid)
})
.expect("the child never started");
let mut exited = watched.take_exit().unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
let exit = loop {
@@ -179,3 +181,37 @@ fn an_exit_that_happens_before_the_caller_is_registered_is_not_lost() {
stop.store(true, Ordering::Relaxed);
reaper.join().unwrap();
}
#[test]
fn a_pid_stops_being_the_workload_the_moment_it_is_reaped() {
let _alone = alone();
let waiters = Waiters::new();
let mut watched = waiters
.watch(|| Ok(fork_child(|| unsafe { libc::_exit(0) })))
.expect("the child never started");
let mut exited = watched.take_exit().unwrap();
assert!(
watched.running(),
"a child that has not been reaped is still itself"
);
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
for (pid, exit) in reap_exited() {
waiters.deliver(pid, exit);
}
if exited.try_recv().is_ok() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
// The pid was freed by the reap that produced that exit, and the kernel is
// entitled to hand the number to something else. Signalling it after this
// point is signalling a stranger.
assert!(
!watched.running(),
"a reaped pid is still being treated as the workload's",
);
}