Repository navigation
fix(native): await EPERM storage command groups, tag the finalizer owner veto and widen the recorded probe-budget case - #507
Merged
Conversation
…ner veto and widen the recorded probe-budget case HACK-1463 follow-ups after #497. - groupAbsent no longer refuses a process group probe that answers EPERM. macOS can return EPERM just after a group's last member exits, so a loaded host refused healthy identity reads (`ps`, `sysctl`) inside recorded storage commands as `unclassified`. Only ESRCH still proves absence; EPERM keeps the wait to the same deadline and refuses there as `probe-budget`. This was the cause of the carrier-state call-site case seeing 9 inspects. - The carrier-state cases assert the reported refusal before the inspection count, so a refusal before the fault names its own stage and reason. - The deferred finish's owner-confirmed check reports `private-state` at `storage-helper-cleanup` instead of an untagged refusal that read `storage-budget` past the deadline. - The recorded exec/up slow-start case gets a named 120 s Bun budget: its 20 s start is virtual, the wall time is host work. Refs HACK-1463 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… wait (HACK-1463) EPERM is now retried until the 15 s settlement budget runs out, so the case's fixed 30 s budget left little headroom on a loaded host. Name a 60 s case budget and say why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
Fix unit G111: the three HACK-1463 follow-ups from the #497 verifier. One of them was not a test problem. The carrier-state case failed under load because the storage transport refused healthy commands, so this PR includes one small product fix in
groupAbsent.Root causes and fixes
(1) Carrier-state call-site case "saw 9 inspects": a real refusal under load
The count was a symptom. I ran the three carrier-state cases in a loop at load average 100-240 with diagnostics on every refusal path. About 1 run in 6 failed. Each time, the carrier had refused before the fault fired, at whichever recorded command was running (
storage-helper-createorstorage-helper-start), asunclassified. One failure stopped after inspection 5, another before inspection 1. The inspect count was simply wherever that refusal landed.The diagnostics traced the refusal to
groupAbsentinsrc/lib/native-compose-storage-witness-docker.ts. A recorded command makes host identity reads (/bin/ps,sysctl kern.bootsessionuuid) through the unrecorded runner. After each read exits, the runner proves its process group is gone withkill(-pgid, 0). OnlyESRCHwas accepted, and any other error refused at once, with no cause attached. On macOS,kill(-pgid, 0)can returnEPERMjust after the group's last member exits. A standalone probe (spawn a detachedps, await its exit, probe the group) sawEPERMonce in 400 exits on this host. So a healthy read was refused, and the refusal surfaced as anunclassifiedcarrier refusal at the enclosing stage.Fix:
EPERMnow means "not yet absent" (the group still has a member this process cannot signal). The wait continues to the same monotonic deadline: 3 s for an unrecorded command, or the 15 s settlement budget. OnlyESRCHproves absence. IfEPERMpersists, the command still refuses at its deadline asprobe-budget. Any other error still refuses at once. This follows the existing group-absence polls innative-compose-file-command.tsandnative-host-hook-runner.ts. The carrier reference doc said "EPERM/EIO ... refuse". It now says that EPERM is not absence and is waited out to the same budget.Test change: the carrier-state cases now assert the reported refusal (stage and reason) before the inspection count. A refusal that happens before the fault then fails with its own stage and reason instead of a bare count. Only the fault reports
helper-policy-stability, and it refuses at once. So once that refusal is confirmed, the count still pins which inspection reached the tagged call site, and the property the case proves is kept.New regression tests in
tests/native-compose-storage-witness-docker-io.test.tsdrive the real unrecorded runner with aprocess.killspy that answersEPERMto the group probe:EPERMthree times and thenESRCHcompletes (exitCode 0, exact stdout).EPERMis never treated as absent and refuses asprobe-budgetat its 3 s budget.(2) Probe-budget recorded slow start: wall time is host work, not the 20 s delay
The issue assumed the simulated 20 s start used 20 s of the 30 s Bun budget. It does not. The engine stubs
Date.nowand advances it on a virtual clock (engine.elapsed), soSLOW_START_MScosts no wall time. The wall time is the recorded branch's host work: three published commands, each with stopped-childps/sysctlidentity reads, private-state publications and settlement, plusfinish()retirement. At load 120-140 that took more than 30 s. Shortening the simulated delay would not change it.Fix: the recorded case gets a named budget,
RECORDED_PROOF_CASE_BUDGET_MS = 120_000, documented as a hang guard rather than a speed assertion.SLOW_START_MSstays at 20 s, so the "slower than the former 15 s cap" property and its mutation proof are unchanged. Biome re-wraps that one test body because its last argument is now an identifier.(3) Finalizer owner-confirmed refusal tagged
The deferred finish's
if (!nativeComposeStorageCommandOwnerConfirmed(owner)) return refuse();now callsrefuseObserved("storage-helper-cleanup", "private-state"). The finalizer is registered only after all three commands settled. Only a running command can invalidate the owner, so no deadline can veto it at this point. A vetoed owner is the invocation's saved private state, reported at the carrier's own cleanup stage, as the binding check right after it is.private-stateis outsidedeadlineReasons, so it never readsstorage-budgetpast the deadline. Both reference docs say so.The new carrier case drives the real carrier against the synthetic daemon. It captures the real owner from
createNativeComposeStorageCommandOwnerand invalidates it withinvalidateNativeComposeStorageCommandOwnerafter the invoke returns and beforefinish(). It asserts{stage: storage-helper-cleanup, reason: private-state}, a late reason ofprivate-state, and refusal before the finish's binding read (48 reads, not 49) and before evidence retirement.Mutation proof
Each mutant was applied to the source, then its focused test was run with
--timeout 60000. The source was restored byte for byte afterwards (checked by comparing file contents).return refuse()undefined, latestorage-budgetgroupAbsentrefusesEPERMat once (the old code)unclassifiedinstead ofprobe-budgetgroupAbsenttreatsEPERMas absentcommandprobesif (!same(...)) return refuse()storage-helper-cleanup/unclassified, latestorage-budgetEvidence
mise x bun@1.4.2),bun install --frozen-lockfile; nobun.lockchange.bun run check: privacy-check ok, 34 warnings, all of them complexity warnings that were already there (invokeis still 57).bun run --cwd packages/cli typecheckand rootbun run typecheck: 0 errors.ultracite checkon the three changed test files: clean.gitleaks dirover thegit log -p origin/next..HEADexport): no leaks.--timeout 60000at load average about 100: docker IO 15 pass, probe budget 6 pass, command record 34 pass, docker witness 90 pass, carrier call sites 18 pass (767 s).unclassifiedrefusal described above.yesCPU generator that a shell trap stopped on exit, and every run was a freshbun test --timeout 60000process:bun testprocess took 50-71 s, more than the former 30 s case budget.testshards 1-4 and aggregate, docker-e2e shards 1-4 and aggregate, Candidate core (Linux, macOS), Config compiler (ubuntu-latest, ubuntu-24.04-arm, macos-latest, macos-15-intel), Runtime state models, linux-process-lifetime, runtime-images and secret-scan.Gaps
EPERMroot cause is shown by a standalone probe and by diagnostics on the refusal path. The diagnostics were temporary and are not committed. I did not establish exactly which macOS kernel state producesEPERM. The doc comment says only what was observed.process.killspy, so they prove the transport's handling ofEPERM, not that the kernel produces it.ESRCHresult (native-compose-file-command.ts,native-host-hook-runner.ts).native-compose-vm-file-client.tsandnative-compose-adoption-routing-execution.tstreat a singleEPERMas "present", which is safe but was not reviewed here for load behavior.Round 2 (ce5d368): EPERM case budget
The verifier found that
"EPERM is never group absence or settlement"intests/native-compose-storage-command-record.test.tsnow waits the full 15 s settlement budget before refusing (15.7 s locally, 15050 ms in hosted CI) inside a fixed 30 s case budget. Its case budget is now the namedGROUP_PROBE_REFUSAL_CASE_BUDGET_MS = 60_000, with the reason in its comment. Both rows pass. Only the test budget changed.Follow-ups from the verifiers are ticketed: two remaining untagged finalizer refusals, and EPERM handling in the vm-file-client and adoption-routing group probes plus the 30 s sibling budgets in the probe-budget file.
Refs HACK-1463
🤖 Generated with Claude Code