From 69d3c3e5cbf5623b95d1f469c284b09cdac92374 Mon Sep 17 00:00:00 2001 From: Grok Bot slot driver Date: Fri, 30 Oct 2026 21:08:18 +0700 Subject: [PATCH 1/2] ci: finite census waits out the exit window before refusing a new child (flaky source-home-linux) source-home-linux failed intermittently with 'finite journey live child identity is unavailable' (base run 41052807996 at cf24ae37, #1256 run 41059901772). It was green at 68c01165, at 995618dd and on #1238's rerun. observe_once saw a new pid whose /proc//exe was already gone (after exit_mm) while its flags=0 pidfd was not yet readable (before whole-group exit), and refused it. It now waits up to 250 ms on that pidfd, once; a pidfd that stays quiet still refuses exactly as before. Test: test_finite_census_waits_out_exit_race_and_still_refuses_a_quiet_pidfd (falsifier: revert the wait -> red). ci-release-policy-test.py 121 OK. --- scripts/ci-installed-journeys.py | 11 ++++++-- scripts/ci-release-policy-test.py | 47 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/scripts/ci-installed-journeys.py b/scripts/ci-installed-journeys.py index 1e554e40a..d6cd28507 100644 --- a/scripts/ci-installed-journeys.py +++ b/scripts/ci-installed-journeys.py @@ -250,10 +250,12 @@ def same_process(left, right): ("pid", "uid", "start_ticks", "executable", "device", "inode")) -def pidfd_exited(fd): +def pidfd_exited(fd, timeout=0): poll = select.poll() poll.register(fd, select.POLLIN) - events = poll.poll(0) + if type(timeout) is not int or not 0 <= timeout <= 250: + raise RuntimeError("finite journey process wait is unavailable") + events = poll.poll(timeout) # Reaped processes can also report HUP. Readability still proves the # flags=0 pidfd's whole thread group exited; HUP alone proves no exit. if events not in ([], [(fd, select.POLLIN)], [(fd, select.POLLIN | select.POLLHUP)]): @@ -528,7 +530,10 @@ def observe_once(self, require_runtime=True): except ProcessLookupError: continue try: - if pidfd_exited(fd): + # An exiting child loses /proc//exe before its + # flags=0 pidfd reports whole-group exit. Wait out that + # short window once; a quiet pidfd still refuses. + if pidfd_exited(fd, timeout=250): continue finally: os.close(fd) diff --git a/scripts/ci-release-policy-test.py b/scripts/ci-release-policy-test.py index 30d660abc..9b2dc17ea 100644 --- a/scripts/ci-release-policy-test.py +++ b/scripts/ci-release-policy-test.py @@ -2914,6 +2914,16 @@ def test_finite_z_leader_requires_retained_birth_and_whole_process_pidfd_exit(se self.assertEqual(self.journey["pidfd_exited"](45), exited) poll.register.assert_called_once_with(45, selector.POLLIN) poll.poll.assert_called_once_with(0) + poll = mock.Mock() + poll.poll.return_value = [(45, selector.POLLIN)] + selector.poll.return_value = poll + with mock.patch.dict(self.globals, {"select": selector}): + self.assertTrue(self.journey["pidfd_exited"](45, timeout=250)) + poll.poll.assert_called_once_with(250) + for timeout in (251, -1, True): + with mock.patch.dict(self.globals, {"select": selector}), \ + self.assertRaisesRegex(RuntimeError, "process wait is unavailable"): + self.journey["pidfd_exited"](45, timeout=timeout) for events in ([(45, selector.POLLHUP)], [(45, selector.POLLERR)], [(45, selector.POLLNVAL)], [(45, 2)], [(45, selector.POLLIN | selector.POLLERR)], [(45, selector.POLLIN | selector.POLLHUP | selector.POLLERR)], @@ -3039,6 +3049,43 @@ def test_finite_census_accepts_same_birth_exec_and_never_rebinds_an_exited_pidfd self.assertNotIn(103, owner.observe_once()) self.assertEqual(opened.call_count, len(rows)) + def test_finite_census_waits_out_exit_race_and_still_refuses_a_quiet_pidfd(self): + for readable in (True, False): + with self.subTest(readable=readable), tempfile.TemporaryDirectory() as temp: + owner, _ = self.finite_owner(Path(temp)) + row, reads = self.process_rows(owner)[100], [] + outer = self.globals["bounded_text"] + child = owner.binding["ControlGroup"] + + def census(path, *rest): + # The cgroup still lists the new pid after its exe vanished. + return ("100" if Path(path).name == "cgroup.procs" else + "0::" + (child if path.parent.name == "100" else "/outside")) + + def exited(fd, timeout=0): + # Its pidfd stays quiet until the bounded whole-group wait. + reads.append((fd, timeout)) + return readable and timeout == 250 + + def identity(pid, **_): + if pid == 100: + raise FileNotFoundError("fixture child exited") + return dict(row) + + with mock.patch.dict(self.globals, {"bounded_text": census, "pidfd_exited": exited, + "linux_process_identity": identity}), \ + mock.patch.object(os, "getuid", return_value=1001), \ + mock.patch.object(os, "pidfd_open", return_value=1100, create=True), \ + mock.patch.object(os, "close") as closed: + if readable: + self.assertEqual(owner.observe_once(require_runtime=False), {}) + else: + with self.assertRaisesRegex(RuntimeError, "live child identity is unavailable"): + owner.observe_once(require_runtime=False) + self.assertEqual(reads, [(1100, 250)]) + closed.assert_called_once_with(1100) + self.assertEqual((owner.known, owner.fds), ({}, {})) + def test_finite_pidfd_binding_closes_on_identity_race_or_unreadable_child(self): for fault in ("reused", "disappeared", "denied"): with self.subTest(fault=fault), tempfile.TemporaryDirectory() as temp: From c508af5f921998348dc0924c67b3dbe547e89fac Mon Sep 17 00:00:00 2001 From: Grok Bot slot driver Date: Fri, 30 Oct 2026 21:48:07 +0700 Subject: [PATCH 2/2] CI census: keep 'finite journey process wait is unavailable' readable (FINITE_OBSERVER_MESSAGES) --- scripts/ci-installed-journeys.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci-installed-journeys.py b/scripts/ci-installed-journeys.py index d6cd28507..a01a440cf 100644 --- a/scripts/ci-installed-journeys.py +++ b/scripts/ci-installed-journeys.py @@ -187,6 +187,7 @@ def observer_failure(phase, error, roots=(), limit=480): 'finite journey outside process identity is unavailable', 'finite journey process census differs', 'finite journey process changed during pidfd binding', + 'finite journey process wait is unavailable', 'finite journey process exit is unavailable', 'finite journey process identity changed', 'finite journey process identity is unavailable',