GeezerVison is showing github.com — fix(update): retry sanity-exec on ETXTBSY — deflake coverage and harden self-update against the fork/exec fd race by mikolalysenko · Pull Request #140 · SocketDev/socket-patch
Conversation
Summary
Fixes the coverage-job flake first seen on PR #139 (failing run): update::download::tests::sanity_exec_rejects_wrong_program_and_honors_strictness died with
VerifyFailed("downloaded binary failed to execute (wrong architecture?): Text file busy (os error 26)")
Root cause — the classic Unix fork/exec fd-inheritance race, not a broken test assertion: between a sibling thread's fork() and its exec(), the child briefly inherits every open fd, including the write fd on a script/binary written moments earlier. Exec'ing that file during the window fails with ETXTBSY. The parallel test binary forks constantly, and llvm-cov instrumentation widens the window — which is why it bit coverage while the identical test passed in test/test-release of the same run. The race is equally real for production self-update in any multi-threaded process.
Changes (two commits, RED then GREEN)
test(update) — RED: adds sanity_exec_retries_when_binary_briefly_text_busy (linux-gated; other platforms don't reliably enforce ETXTBSY), which reproduces the race deterministically by holding a write fd on the staged script for 150 ms from another thread while sanity_exec runs. Also tightens the existing strictness test to assert each rejection's reason instead of bare is_err() — an ETXTBSY spawn failure previously masqueraded as the expected rejection on the assertion one line above where the flake surfaced. This commit is pushed alone so CI demonstrates the failure in the exact environment that flaked; see the first (red) run on this branch.
fix(update) — GREEN: sanity_exec retries the spawn on ErrorKind::ExecutableFileBusy (up to 10 attempts, 25 ms linear backoff, ≤ ~1.4 s worst case) instead of failing a fully SHA-verified download. Same dance Go's os/exec and cargo do. All other spawn errors still fail immediately with the existing message; the 10 s hang timeout still applies per attempt.
Verification
- macOS: full
update::download suite passes with the fix (regression test correctly linux-skipped).
- Linux (CI): the branch's first push (test-only) is expected RED on the new test in
test/test-release/coverage; the fix commit turns it GREEN — commit history preserves the evidence.
🤖 Generated with Claude Code
Note Low Risk
Changes are confined to unit tests; no production logic appears in this diff.
Overview
Test-only changes in download.rs around post-download sanity_exec (--version self-check).
The existing strictness test now checks specific error text for each failure case (wrong program name, non-zero exit, version mismatch, exec failure) instead of only is_err(), so unrelated spawn errors—especially ETXTBSY from the fork/exec fd race—cannot satisfy the wrong assertion.
A new Linux-only test holds a write handle on a staged script for 150ms on another thread while sanity_exec runs, reproducing “text file busy” and expecting a successful pass once the handle is released (regression for the coverage-job flake described in the PR).
Reviewed by Cursor Bugbot for commit 3fca03a. Configure here.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Reproduces the coverage-job flake deterministically: a write fd held open
on the staged binary while sanity_exec runs — the shape a sibling
thread's fork() leaves behind via fd inheritance — makes the exec fail
with "Text file busy" (Linux enforces ETXTBSY; the test is linux-gated).
Also tightens the existing strictness test to assert each rejection's
REASON instead of bare is_err(), which previously let an ETXTBSY spawn
failure masquerade as the expected rejection one line before the flake
surfaced.
This commit is intentionally pushed without the fix so CI demonstrates
the failure; the follow-up commit makes it pass.
Co-Authored-By: Claude Fable 5 <[email protected]>
…ace)
Between a sibling thread's fork() and its exec(), the child briefly
inherits every open fd — including a write fd on the binary staged
moments ago — and exec'ing the file during that window fails with
"Text file busy". Retry the spawn on ErrorKind::ExecutableFileBusy
(10 attempts, 25 ms linear backoff, <=1.4 s worst case) instead of
failing a fully SHA-verified download; all other spawn errors still
fail immediately and the 10 s hang timeout applies per attempt. Same
dance Go's os/exec and cargo do.
Turns the previous commit's RED regression test green and deflakes the
coverage job (first bitten on PR #139: llvm-cov widens the race window,
which is why `test`/`test-release` never caught it).
Co-Authored-By: Claude Fable 5 <[email protected]>
|
RED confirmed on real CI (commit 3fca03a, test-only): test (ubuntu-latest) failed on exactly the new regression test:
thread 'update::download::tests::sanity_exec_retries_when_binary_briefly_text_busy' panicked:
called `Result::unwrap()` on an `Err` value: VerifyFailed("downloaded binary failed to execute (wrong architecture?): Text file busy (os error 26)")
The fix commit (fe03f2c) is now pushed; CI should go green.
Separate observation from the RED run — test (windows-latest) failed on update_upgrade_branch_swaps (self_update_e2e.rs:133), not related to this diff (everything here is unix/linux-gated test code + a unix-race retry):
Error: could not install the update: self-replace failed: The process cannot access the file because it is being used by another process. (os error 32)
That's the Windows sibling of the same disease — a transient handle (typically Defender's real-time scan) on the freshly written binary during the swap rename-dance → ERROR_SHARING_VIOLATION. The analogous hardening (retry sharing violations around the swap with backoff) is deliberately not in this PR to keep it single-purpose; flagging it as a candidate follow-up.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
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.
|