Repository navigation
fix(tri): per-pid temp scratch cannot outlive its run -- Drop guard plus dead-pid sweep - #5990
Merged
Merged
Conversation
…lus dead-pid sweep The m2l standalone lake test built a ~7.6 GB package in temp_dir()/tri_m2l_standalone_pkg_<pid> and removed it on the line after assert!(status.success()). A failed `lake build` panicked past the cleanup and a killed run never reached it; on 2026-10-04 the leftovers filled the owner's disk twice (about 300 MB free, other runs died with ENOSPC). Side files of 8 dead pids are still in $TMPDIR on that machine. - cli/tri/src/piddir.rs: PidPath, a guard that removes its file or dir on drop (return and unwind alike), and sweep_dead(parent, prefix), which removes `<prefix><pid>[.ext]` entries whose pid is not running. Liveness is `ps -p`; anything short of a clear "no such process" counts as running, so a live run's scratch is never removed. No new dependency, no unsafe. - fpga.rs: the lake test body is m2l_standalone_lake_check(parent, build). Every path is guarded before anything can fail; dead runs' leftovers are swept first. `build` is a parameter so the failure path is driven with `false` instead of a 7.6 GB download. - fpga.rs: test_measured_to_lean_standalone_outputs_consumable_lean created tri_standalone_lake_pkg_<pid> and never removed it; now guarded and swept. - census.rs: Scratch (a whole working-tree copy, a few hundred MB) already had a Drop; a killed run's copy is now swept by the next Scratch::new. Census: `fetches` moved, files read 46 -> 47. That count is the number of cli/tri/src/*.rs files it reads, and piddir.rs is the new one; no fetch site moved. Re-blessed in this commit; quiet and shell did not move. Tests: m2l_scratch_is_gone_after_a_failed_build (asserts the panic came from the build step, then that nothing survived), ..._after_a_passing_build, m2l_scratch_sweeps_a_dead_runs_package_and_keeps_a_live_one, and four piddir unit tests (name parsing, liveness of self / pid 1 / a reaped child, sweep keeps live and unparseable names, guard on unwind). Closes #5982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes #5982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 4, 2026
Contributor
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
This was referenced Oct 4, 2026
The review of #5990 refused it: pid_is_running treated any `ps` that exits 1 with empty stdout as "no such process", including a `ps` that exits 1 with an error on stderr. A fake `ps` first on PATH (`echo err >&2; exit 1`) made pid 1 dead, and sweep_dead removed probe_pkg_1. A `ps` that rejects `-p` or `-o` answers the same way, so on such a machine every pid reads dead and runs delete each other's live 7.6 GB lake packages. The module doc promised the opposite. - Dead now needs exit status 1 AND empty stdout AND empty stderr. A probe that cannot start, a signal, any other status, and exit 1 with any byte on either stream all read as running. Checked on this Mac: `ps -p 99999 -o pid=` exits 1 with 0 bytes on both streams, while `ps -p 4194303 -o pid=` exits 1 with "process id too large" on stderr, which the old rule read as dead. - The probe is a parameter (pid_is_running_by, sweep_dead_by), as `build` is for the lake check. Tests drive a fake per case: silent exit 1 (dead), exit 1 + stderr, exit 1 + stdout, exit 2, a signal, a missing program, no program (all running), and the reviewer's sweep: with every unclear probe, nothing planted is removed. - Pid 0 and this process never ask the probe; a test pins both against a probe that says dead, plus pid_is_running(0) on the real ps. - pid_of tests pin `x_pkg_123_x` (suffix) and `zx_pkg_123` (prefix not at the start) as rejected, and the sweep test keeps both shapes on disk. - The tests' own parents (tri_piddir_<tag>_<pid>, tri_m2l_scratch_<tag>_<pid>) are swept by the same rule before each test makes its own, so a killed test run is cleaned by the next one. Two tests pin it, one per helper. Refs #5982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 4, 2026
Contributor
PR DashboardGenerated at: 2026-10-04 11:20:22 UTC
Summary
Seal Status
|
This was referenced Oct 4, 2026
Merged
This was referenced Oct 4, 2026
Merged
Merged
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.
Pull Request Checklist
Closes #5982docs/now/2026-10-04-tri-per-pid-temp-scratch-cannot-outlive-its-run.mdadded (tri now add ... --closes 5982)./scripts/tri testwas NOT run, because of the disk limit below)Round 2: what the review of 7bc4843 found, and what changed
The review refused 7bc4843 on deletion safety.
pid_is_runningread anypsthat exits 1 with empty stdout as "no such process", including apsthat exits 1 with an error on stderr. A fakepsfirst on PATH (echo err >&2; exit 1) made pid 1 read as dead, andsweep_deadremovedprobe_pkg_1. Apsthat rejects-por-oanswers the same way. On such a machine every pid reads as dead, so runs delete each other's live 7.6 GB packages. The old module doc promised the opposite of what the code did.New commits on the same branch, no force-push:
82a1e8ae5fix(tri): a pid reads dead only on a silent exit 1 from psbuildalready is for the lake check:pid_is_running_by(probe, pid);sweep_dead_by(parent, prefix, running);pid_is_runningandsweep_deadare those with the realps.tri_piddir_<tag>_<pid>andtri_m2l_scratch_<tag>_<pid>are swept by the same rule before each test makes its own. Two tests pin it.9e9d4f094merge origin/master: picks up fix(ci): the orphan-ceiling ledger names cli/t27b #5989 (e84927b60), the fix for the inherited "Every source file is reachable" failure. A clean merge, no conflicts. After the merge,tri census pin --gateprintedPASS: no pinned census moved.What
psanswers on this Mac (macOS,/bin/ps, 2026-10-04)ps -p 99999 -o pid=(no such pid)ps -p 0 -o pid=ps -p 4194303 -o pid=ps: process id too large: 4194303ps -p 1 -o bogus=(a keywordpsrejects)ps: bogus: keyword not foundpshappens to print on stdout too. One that prints its error only on stderr is the review's caseps -p 1 -o pid=1)Linux procps was NOT verified. No Linux container was available on this machine: the Docker daemon was not running and colima was not started, and no image was pulled. If procps answers a missing pid in any way other than a silent exit 1, the sweep keeps everything there. That costs disk, never a live run.
Description
test_measured_to_lean_standalone_builds_in_temp_lake_package(incli/tri/src/fpga.rs) builds a lake package intemp_dir()/tri_m2l_standalone_pkg_<pid>. Itsrequire trinitypulls mathlib into the package's own.lake/packages, about 7.6 GB. The dir was removed only on the line afterassert!(status.success()):lake buildpanicked past that cleanup;On 2026-10-04 the leftovers filled the owner's disk twice. Free space fell to about 300 MB and other runs died with ENOSPC. Side files from 8 dead pids (24 files) are still in
$TMPDIRon that machine.The fix has two parts, because each covers what the other cannot:
PidPathin the newcli/tri/src/piddir.rs).sweep_dead(parent, prefix)).kill -9runs no destructor, so the next run removes<prefix><pid>[.ext]entries whose pid is not running.ps -p <pid> -o pid=. That needs nounsafeand no new dependency; this crate has neitherlibcnortempfile..ext.The test body is now
m2l_standalone_lake_check(parent, build). Taking the build command as a parameter lets the failure path be driven withfalseinstead of a 7.6 GB download.Item 4: other per-pid
temp_dir()paths in the crate, cleaned only on successFixed in this PR (the big or never-cleaned ones):
tri_m2l_standalone_pkg_<pid>+ 3 side filestest_measured_to_lean_standalone_outputs_consumable_leantri_standalone_lake_pkg_<pid>(+_in_,_generated_)Scratch(tri census explain)tri-census-explain-<pid>Scratch::newtri_piddir_<tag>_<pid>,tri_m2l_scratch_<tag>_<pid>Left as is. All are KB-sized, and a leak would need a panic or a kill:
?orbailskips the trailing cleanup:vsim.rs:141tri-vsim-<pid>oneaway.rs:187tri-oneaway-<pid>misread.rs:272tri-misread-<pid>types_dup.rs:889tri-redef-probe-<pid>modreach.rs:184tri-mods-selfcheck-<pid>gates.rs:tri-sha-(4408),tri_t119_(4836),tri_t112_(4882),tri_t111_(4975, a git init),tri_gates_empty_(6105),tri_copy_from_/tri_copy_to_(6348)hooks.rs:449now_gate_issues.rs:1173nownote.rs:524seals.rs:810w719-modreach.rs:532tri-modreach-bin-fpga.rs:tri_sweep_report_json_,tri_cold_por_mock_,tri_cold_por_synthetic_,tri_sweep_report_synthetic_,tri_smoke_gate_validate_standalone(_snapshot)_(JSON logs)unparsed.rs:tri-unparsed-probes,tri-unparsed-counters,tri-locate-probe.t27prose.rs:tri-prose-<spec>.t27Any of these can adopt
PidPathwith one line when someone touches it.Item 3: shared, locked lake package cache. Deferred.
Free space was 9.0 GiB at the start of round 1, and 5.7 GiB at its lowest during that build. In round 2 it read between 8.2 and 11 GiB. The rule for a real
lake buildis at least 20 GiB, so no before/after measurement was possible. Without one, the cache cannot be shown to be safe:cargo testprocesses would need a cross-process lock aroundlake build.std::fs::File::lockis available in this toolchain. Whether lake tolerates onepackagesDirshared across different root packages is unverified.Measurement plan, for when there is room (>= 20 GiB free):
lean-toolchaininto the temp package first, copied fromproofs/lean4/lean-toolchain./usr/bin/time -l cargo test -p tri -- --exact fpga::tests::test_measured_to_lean_standalone_builds_in_temp_lake_package, takingdu -shof the package dir before the guard drops it.tri_m2l_standalone_cache/behind aFile::locked.lockand a.completemarker, with lake pointed at it through an absolutepackagesDir.packagesDir.kill -9in the middle of a cold clone. Confirm that the next run wipes the cache and rebuilds, rather than trusting it.Finding (not changed here): why the 8 runs probably failed
The temp package has no
lean-toolchain, so elan uses its default toolchain there. On this machine that is v4.34.1 (lake 5.0.0). Meanwhile:proofs/lean4/lean-toolchainpinsleanprover/lean4:v4.31.0;proofs/lean4/lake-manifest.json(800238935) is built for v4.31.0.That mismatch is the likely reason
lake buildfailed and the dirs leaked. Copying the toolchain file into the temp package is a one-line fix. It is unmeasured for the same disk reason, so it belongs with item 3.Testing
The same 13 passed again after the merge of master.
m2l_scratch_is_gone_after_a_failed_buildfalse. It asserts that the panic message is the build step's own, so the test cannot pass by failing earlier, before the package existed. It then asserts that nothing survived.m2l_scratch_is_gone_after_a_passing_buildtrue: the success path leaves nothing eitherm2l_scratch_sweeps_a_dead_runs_package_and_keeps_a_live_one..._pkg_1(pid 1, always running) is keptm2l_scratch_parent_sweeps_a_killed_runs_parenttri_m2l_scratch_killed_<pid>is gone once the next run makes its own parentpiddir_pid_of_reads_only_prefix_digits_and_an_extension<prefix><pid>[.ext]only. Rejected: overflow, empty, signed and foreign names,x_pkg_123_x(suffix) andzx_pkg_123(prefix not at the start)piddir_probe_dead_only_on_a_silent_exit_1piddir_pid_zero_and_self_are_running_whatever_the_probe_sayspid_is_running(0)with the realps; pid 0 and self with a probe that says deadpiddir_liveness_running_and_reapedps: self and pid 1 are running, a reaped child is notpiddir_sweep_removes_dead_pids_and_keeps_live_onesprobe_pkg_<dead>_xandzprobe_pkg_<dead>piddir_sweep_removes_nothing_when_the_probe_is_unclearprobe_pkg_1,probe_pkg_<dead>andprobe_pkg_<dead>.jsonall survive and nothing is reportedpiddir_test_parent_sweeps_a_killed_runs_parenttri_piddir_killed_<pid>is gone once the next run makes its own parentpiddir_guard_removes_its_path_on_unwindMutants. The fix was committed first (
82a1e8ae5). Then each mutant was applied to the file by a script that checked the diff was non-empty, rebuilt, and ran the 13 tests. Each was reverted withgit checkout --, andgit status --porcelainwas checked to be empty after every one (it was, 16 of 16).All runs used a private
TMPDIR, so the mutants that break liveness could not reach anyone else's temp files. 16 of 16 went red:pid_ofsplits at_as well as.(accepts a suffix)pid_of_reads_only...,sweep_removes_dead_pids...the sweep must report exactly what it removedprobe_dead_only...,sweep_removes_nothing...a probe that cannot start read as deadprobe_dead_only...,sweep_removes_nothing...exit 1 with stdout read as deadcode == Some(1)->!status.success()(any non-zero)probe_dead_only...,sweep_removes_nothing...exit 2 read as deadname.find(prefix))pid_of_reads_only...,sweep_removes_dead_pids...the sweep must report exactly what it removedpid_zero_and_self...pid 0, real ps. Killed, not argued: realps -p 0exits 1 silently on macOS; the fake-probe assert covers other systemsprobe_dead_only...,sweep_removes_nothing...probe ["sh", "-c", "echo err >&2; exit 1", "fake-ps"] swept [.../probe_pkg_1, ...]pid_zero_and_self...this process, a probe saying deadcode().unwrap_or(1) == 1(a signal reads as exit 1)probe_dead_only...,sweep_removes_nothing...a probe killed by a signal read as deadprobe_dead_only...no probe at all read as deadsweep_removes_nothing...,sweep_removes_dead_pids...,m2l_scratch_sweeps...the sweep must report exactly what it removedPidPath::dropskips whilestd::thread::panicking()guard_removes_its_path_on_unwind,m2l_scratch_is_gone_after_a_failed_buildthe guarded dir outlived a panicManuallyDrop+remove_dir_allafter theassert!, the old shape)m2l_scratch_is_gone_after_a_failed_builda failed build left ["tri_m2l_standalone_pkg_88482"] behindm2l_scratch_sweeps...a dead run's package survived the sweepparent()helper sweep removedpiddir_test_parent_sweeps...a killed run's test parent survivedm2l_scratch_parent()helper sweep removedm2l_scratch_parent_sweeps...a killed run's probe parent survivedNot run: the real
test_measured_to_lean_standalone_builds_in_temp_lake_package. It needs 7.6 GB, and free space never reached the 20 GiB rule (at most 11 GiB). It still compiles and is listed.In the wild (round 1): running
test_measured_to_lean_standalone_outputs_consumable_leanswept two real dead-pid leftovers,tri_standalone_lake_pkg_34565andtri_standalone_lake_pkg_53435.Census:
fetchesmoved in round 1, files read 46 -> 47. That count is the number ofcli/tri/src/*.rsfiles the census reads, and the newpiddir.rsis the extra one. It was re-blessed in the same commit, and no fetch site moved. Round 2 added no file; the hook printedPASS: no pinned census moved.Checks that were red, and why
cli-tri: red at 7bc4843, inherited from master (the modreachcli/t27bceiling,6765d6379). Passes at 9e9d4f0 after the merge.Every source file is reachable: red at 7bc4843, inherited from master and fixed there by fix(ci): the orphan-ceiling ledger names cli/t27b #5989. Passes at 9e9d4f0 after the merge.fpga-conformance: the elaboration ratchet failed at 7bc4843 withelaboration errors: 178 (baseline 176), and the log notes the baseline came from iverilog 13.0 while the run used 12.0. The same job was red on all 8 of the most recent finishedFPGA E2E Buildruns listed at the time: 7 other branches plus this one. This PR touches onlytritest scaffolding and temp-dir cleanup.Review Notes
Scratch::new) is a three-line sweep using the testedsweep_dead. The call site itself is not exercised by a test, becausetri census explainonly builds aScratchwhen a pinned census has moved.psis looked up on PATH. A hostilepsthat exits 1 silently for every pid would still make every pid read as dead; no output-only rule can tell that apart from the real answer. A merely broken or unfamiliarps(one that errors, prints, or exits with another code) now reads as running.Closes #5982
🤖 Generated with Claude Code