Repository navigation
End a Scenario's Runs and their descendants when its harness dies (#1912) - #1931
Merged
Merged
Conversation
A second Run could otherwise start while the first's process id was still a zombie and so looked alive.
Take a Run's pipes before waiting on its launcher, which closes stdin. Close inherited close-on-exec descriptors before the guardian forks, so it never holds another test's executable open for writing. Bound the Scenario drop's wait for its guardians, name the report values, record which terminal Runs follow the setup PTY contract, and only kill unreaped adopted processes in the lifetime test's emergency cleanup.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
Scenario's Runs now start under a guardian that outlives a killed harness and ends everything the Run started.tests/support/child_lifetime.rs: newOwnedRuns/OwnedRun, alongside the existingFixtureChild. The guardian is forked rather than exec'd, so it allocates nothing and runs only async-signal-safe calls. It reports the Run's pid and raw wait status over a socket. It keeps the Run unreaped until the test has read its status, as aChilddoes, soHeldRun::signal/killnever hit a reused pid.tests/support/mod.rs:run,run_with_input,run_from,run_with_env,run_until/run_from_until(andrun_and_signal*),read_to_fileandrun_copy_replaced_midwayspawn throughOwnedRuns.tests/scenario_lifetime.rs: lifecycle tests with a subreaping supervisor.docs/testing.md: the new "Scenario Run lifetime" contract, beside the setup PTY and release ones.Confirmed cause:
Scenariostarted thirdshift as a plain child of the harness, and thirdshift starts each child in a process group of its own. A SIGKILLed harness never unwinds, so nothing ended those children. They were reparented to pid 1 and kept polling.Evidence
Execution evidence
Lifecycle test:
cargo test --test scenario_lifetimemain'stests/support/{mod,child_lifetime}.rs):test result: FAILED. 0 passed; 6 failed, naming survivors, e.g.test result: ok. 6 passed; 0 failed; 2 ignoredIt covers:
git.The issue's two reproductions: a throwaway script runs the test binary in its own process group under
setsid, with a privateTMPDIR. Once the marker exists, it SIGKILLs the group (or only the harness), waits, then lists processes whose cwd is inside thatTMPDIR.main):process_tree a_command_started_during_the_stop_grace_period_is_killed_too, group kill:FAIL: 3 fixture processes survived(fakecodexwith ppid 1,bash -e claude-script.sh, …).FAIL: 4 fixture processes survived(thirdshift harness codex …with ppid 1, fakecodex, its script,sleep 120).PASS: no fixture process survived, for both that test andpickup_run two_machines_racing_for_two_ready_issues_each_take_one, in both kill modes.Full suite wall time: precompiled
cargo test --no-fail-faston a 4-core Ubuntu 24.04 host. Compilation is excluded.main's support code)processfindings::*flakes, see below)scripts/affected-tests mainselects the full Rust suite, andcargo nextestis unavailable here, so the full suite ran ascargo test.Regressions fixed along the way:
processfindings::merged_fix_is_rechecked_after_a_later_fix_fails: a first version letwaitreturn before the guardian reaped the Run. A second Run then saw the first's zombie pid as alive. The release is now synchronous: the guardian reaps the Run, then closes the socket.triage_run:run_with_inputlost stdin becauseChild::waiton the launcher closes it. The pipes are now taken before that wait.Pre-existing flakes, seen on
maintoo:processfindings::*:main's support code failedmerged_fix_is_rechecked_after_a_later_fix_failsin 3 of 3 whole-binary runs.updateconsole::*ETXTBSYattests/update/console.rs:43: 1 of 6 runs onmain. These tests write an executable in-process withfs::copyand exec it while a sibling test forks.Merge Danger
Door: two-way
The change is test-only and touches no production code.
Blast Radius: test-suite
ScenarioRun now goes through the guardian, andScenariodrop waits up toWAIT_BOUNDfor cleanup. A descendant stuck in uninterruptible sleep now fails the test after 120 s instead of leaking.Fixtures with the same gap (out of scope)
These commands don't go through these
Scenariomethods, so their children can still outlive a killed harness:Console::start_command(scenario.command(..))(tests/console/*.rs).tests/update/console.rs:130.machine.command()intests/console/fleet.rsandtests/console/other_accounts.rs.tests/eval_arms.rs:130.Terminal Runs keep the setup PTY contract: immediate child only.
Unaddressed findings
Standards
All six Standards findings were addressed:
Reportenum and aGuardianFdsstruct.Spec
run/output()path. Declined. The acceptance criteria ask only for held-Run cases. EveryScenariomethod sharesOwnedRuns::spawn, and the full suite's 3966 passing tests exerciserun,run_with_inputandread_to_filestdio through it. Thetriage_runstdin regression above was caught that way.Childsemantics (see theprocessregression above), and the drop is now bounded byWAIT_BOUND./proc/thread-self/children. No change.FixtureChildalready depends on the same interface (/proc/self/task/*/children).Security
None. The change is test-support code only: the guardian's lease, report socket and done pipe are anonymous, close-on-exec descriptors, and it signals only its own unreaped children, so no lower-trust principal or trust boundary is involved.
Review coverage: both reviewers read all four changed files; none left unread.
Closes #1912
Built with claude 2.1.314 · opus · default effort
Security review outcome
No unaddressed introduced findings.