Fix: pid-less lock silently aborts the run (v0.4.0 release blocker) - #13
Merged
Merged
Conversation
Release verification of v0.4.0 caught two defects in the lock hardening that shipped in c045cc0, both mine. acquire_lock read the recorded owner with an unguarded al_pid=$(cat "$LOCK_DIR/pid" 2>/dev/null) An assignment from a failing command substitution takes that failure as its own status, and acquire_lock is invoked as the last command of an AND-OR list, where errexit is not suppressed - so a lock directory with no readable pid killed the script instantly. Reproduced: `stop` exited 1 having printed nothing at all on stdout or stderr, and `doctor` stopped mid-report after the disk-space check. That state is reachable by a run killed between mkdir and the pid write, or by a disk full at that moment, and it made the folder look inert until someone found the lock by hand. The read is now guarded and a lock with no owner is reported rather than either aborting or being stolen - with no pid there is nothing to judge. Liveness also used `kill -0`, which returns "not permitted" rather than "no such process" for a process owned by another user. On a shared group-writable folder that made someone else's live run look dead: the second run announced it was clearing a stale lock and proceeded concurrently, where v0.3.0 correctly refused. It now asks `ps` first, which can see other users' processes, and falls back to `kill -0` where ps has no -p. The unguarded `rm -rf "$LOCK_DIR"` that aborted before its own error message when the lock belonged to root is guarded too. Also corrects text that would have been published as the release notes: 0.4.0 is the first release to carry a checksum, not 0.4.1. Both defects have regression tests; reverting manage.sh alone fails 4 of them. Co-Authored-By: Claude Opus 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.
Two defects in the lock hardening from #12, found while verifying that v0.4.0 was safe to tag. Both are mine, both are in code that shipped days ago, and both are release-blocking.
1. A pid-less lock silently killed every run
acquire_lockread the recorded owner with an unguarded command substitution:al_pid=$(cat "$LOCK_DIR/pid" 2>/dev/null)An assignment from a failing command substitution takes that failure as its own exit status, and
acquire_lockis called as the last command of an AND-OR list ([ -z "$DRY_RUN" ] && acquire_lock), which is exactly the position where POSIX errexit is not suppressed. So a lock directory with no readable pid inside killed the script on the spot:Nothing on stdout. Nothing on stderr.
doctorwas worse — it stopped mid-report, right after the disk-space check, so the very command you'd run to diagnose it went quiet too.That state is reachable: a run killed between
mkdir "$LOCK_DIR"and the pid write, or a disk that fills at that instant. The folder then looks inert until someone finds.dockerdance.lockby hand.Fixed by guarding the read. A lock with no owner recorded is now reported and left alone rather than aborting or being stolen — with no pid there is nothing to judge, so taking it would be a guess.
2. Cross-user lock stealing
Liveness used
kill -0, which returns not permitted rather than no such process for a process owned by another user — indistinguishable from dead. On a shared group-writable stacks folder, a second user's run announcedClearing a stale lock left by exited run (pid …)for a very much alive process and proceeded concurrently. v0.3.0 correctly refused. So this was a safety regression, not just a wart.Now
psis asked first (it can see other users' processes), falling back tokill -0whereps -pisn't supported. The unguardedrm -rf "$LOCK_DIR"— which aborted before printing its own error when the lock belonged to root — is guarded too.3. Release-notes correction
The CHANGELOG told users checksums start at 0.4.1. The release workflow publishes
manage.sh.sha256for whatever tag is pushed, so v0.4.0 is the first release to carry one. That text is published verbatim as the release notes, so it would have shipped wrong.Verification
Regression tests added for both defects. Reverting
manage.shalone (keeping the new tests) fails 4 assertions — they detect exactly what they guard:91 assertions pass on macOS
sh, Debiandashand Alpine busyboxash.shellcheckclean,sh -nanddash -npass.These were found by replaying the release workflow and running v0.3.0 and v0.4.0 side by side against a stub docker, rather than by reading the diff — the failure is invisible in review because the aborting line looks completely ordinary.
🤖 Generated with Claude Code