fix(lock): stop leaking bash's redirection error on a root-owned lock - #38
Merged
Merged
Conversation
Writing the pid into the mutation lock used `>"$pid_file" 2>/dev/null`.
Bash performs redirections left to right and reports a failed one on the
stderr in effect at that moment, so the guard was applied too late: when
the lock directory belongs to root -- the sudo escalation gos takes under
a protected parent such as /usr/local -- `gos latest` printed
gos: line 1342: /usr/local/go.gos-lock/pid: Permission denied
before going on to install normally. Brace-group the write so 2>/dev/null
is in effect before the inner redirection is attempted. The sudo fallback
that records the pid is unchanged, and the lock is still acquired.
`gos completions <shell> --install` had the same ordering against an
unwritable target, printing bash's error next to its own; fix it the same
way.
Both paths get a regression: the lock case emulates a root-created lock
(a mkdir wrapper that leaves .gos-lock read-only plus a sudo stand-in
that never calls the real sudo) and asserts stderr stays clean while the
pid still reaches the lock through sudo. Both fail on the previous code.
The Unreleased notes also cover the three Dependabot action bumps already
on main, which the changelog guard counts.
The new unwritable-target case turned a denied write into an assertion,
but tests/completions.bash runs on Windows too, where chmod 555 does not
stop the owner from writing. `--install` succeeded there and CI failed:
not ok - completions --install should fail when the target is not writable
Replace the root check in both new cases with readonly_bit_enforced(),
which writes into a mode 555 directory and reports whether the write was
actually denied, restoring the mode either way. That covers Windows and
root in one guard, and asserts the platform property rather than guessing
it from the OS name. A file-level skip-os=windows is not an option here:
the rest of the completions suite is exactly what Windows needs to cover.
The helper brace-groups its own probe write for the reason this branch
exists in the first place.
There was a problem hiding this comment.
🟢 Approval recommended
The functional change is minimal and well-covered by targeted regressions, with only a minor readability tweak suggested in the new test helper.
Pull request overview
This PR fixes a Bash redirection-ordering issue that leaked a raw Permission denied message when writing lock/completion files in unwritable directories, while preserving the existing sudo fallback behavior and adding regressions to prevent reintroduction.
Changes:
- Brace-groups the lock PID write and completions
--installwrite so2>/dev/nullapplies before the guarded redirection is attempted. - Adds regression coverage for (1) a root-created (user-unwritable) lock directory and (2) an existing-but-unwritable completions install directory, with filesystem capability probing.
- Updates
CHANGELOG.mdUnreleased notes to cover the fix and existing Dependabot bumps.
File summaries
| File | Description |
|---|---|
| tests/lock-rollback.bash | Adds a regression that simulates a root-created lock dir and asserts stderr stays clean while the sudo fallback still records the PID and the lock is released. |
| tests/lib.bash | Introduces readonly_bit_enforced() helper to probe whether chmod read-only semantics are enforced on the current filesystem. |
| tests/completions.bash | Adds a regression ensuring completions bash --install reports only gos’s own error on an unwritable target (no leaked Bash redirection error). |
| gos.sh | Fixes redirection ordering in _gos_acquire_lock and cmd_completions --install by brace-grouping to silence the redirection failure itself. |
| CHANGELOG.md | Adds Unreleased notes for the redirection leak fix and for existing Dependabot action bumps. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
readonly_bit_enforced() tracked the probe in a variable called `enforced` that was set to 1 exactly when the filesystem did *not* enforce the mode, and worked only because the value was returned as an exit status. The return codes were right, but the name stated the opposite of its content, which is a trap for anyone editing the helper later. Track `probe_wrote` in the repository's usual "true"/"false" spelling and end on the test that answers the function's own question. Behavior is unchanged: 0 when the write was denied, 1 when it went through or the chmod itself failed.
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.
What
gos lateston a host where the Go install slot sits under a root-owned parent (/usr/localon macOS with Homebrew at/opt/homebrew) printed a bare permission error before doing the right thing:The
Password:prompt is expected — creating/usr/local/go.gos-lockneeds root, like the backup rename that follows. The line after it was not.Why it happened
_gos_acquire_lockwrote the pid with:Bash performs redirections left to right and reports a failed one on the stderr in effect at that moment.
>"$pid_file"is attempted while stderr is still the terminal, so the2>/dev/nullthat was meant to silence it never applies. The lock directory was created by root through the sudo escalation, so the user shell cannot write inside it and the redirection fails.A brace group puts the guard in effect first:
Everything else is untouched: the lock is still acquired, and the
sudo teefallback still records the pid.cmd_completionshad the identical ordering when--installtargets a directory that exists but is not writable — bash's error printed alongside gos's owncould not write completion file. Same fix. These were the only two occurrences ingos.sh.The defect dates to
266c8d0(the mutation-lock feature), so every release since carries it, including v1.11.0.Impact
Cosmetic in practice — the update completes and the lock is released. The one edge it protects: if the
sudo teefallback also fails, the lock keeps no pid, and a later run reports "another gos operation appears to be running (the lock has no pid recorded)", which_gos_lock_statealready documents.Tests
Both paths get a regression that fails on the previous code and passes on this one.
tests/lock-rollback.bashemulates a root-created lock without root: amkdirwrapper leaves any.gos-lockdirectory read-only, and asudostand-in lifts the write bit for the command and restores it — the realsudois never invoked, so the suite never prompts for a password. It then runsgos prune --rollbackand asserts stderr stays clean, the pid still reaches the lock through the sudo fallback, and the lock is released.tests/completions.bashrunscompletions bash --installagainst a read-only XDG directory and asserts only gos's own error appears.Both cases skip with an
ok -line when the suite runs as root, since root writes into a read-only directory regardless.Verified on the previous
gos.sh:Local gates:
scripts/validate-local.bashpasses — 27 suites, 2 skipped on macOS, plus ShellCheck andshfmt -d -i 2 -ci -bn ..Note on the changelog
tests/changelog.bashwas already failing onmain: the three Dependabot action bumps are post-tag commits and the guard requires Unreleased notes for them. TheUnreleasedsection here covers both this fix and those bumps, so the gate goes green.No release in this PR.