Skip to content

Fix: pid-less lock silently aborts the run (v0.4.0 release blocker) - #13

Merged
AdamXweb merged 1 commit into
AdamXweb:mainfrom
adamXbot:fix/hardening-and-improvements
Aug 6, 2026
Merged

Fix: pid-less lock silently aborts the run (v0.4.0 release blocker)#13
AdamXweb merged 1 commit into
AdamXweb:mainfrom
adamXbot:fix/hardening-and-improvements

Conversation

@adamXbot

@adamXbot adamXbot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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_lock read 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_lock is 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:

$ sh manage.sh stop alpha
$ echo $?
1

Nothing on stdout. Nothing on stderr. doctor was 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.lock by 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 announced Clearing 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 ps is asked first (it can see other users' processes), falling back to kill -0 where ps -p isn't supported. The unguarded rm -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.sha256 for 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.sh alone (keeping the new tests) fails 4 assertions — they detect exactly what they guard:

FAIL [lock: a pid-less lock is reported, never silently fatal] output should contain: no owner recorded
FAIL [lock: a pid-less lock is reported, never silently fatal] expected exit 0, got 1
FAIL [lock: a pid-less lock is reported, never silently fatal] output should contain: settings:

91 assertions pass on macOS sh, Debian dash and Alpine busybox ash. shellcheck clean, sh -n and dash -n pass.

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

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>
@AdamXweb
AdamXweb merged commit fc05678 into AdamXweb:main Aug 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants