fix: graceful error on permission denied during swamp update#871
fix: graceful error on permission denied during swamp update#871
Conversation
## Summary - Add pre-flight write permission check before downloading the update, so users get a clear error immediately instead of after a wasted download - Catch `PermissionDenied` in `replaceBinary()` as a safety net with the same actionable message - Error message directs users to re-run with `sudo swamp update` Fixes #870 ## Test Plan - [x] `deno check` passes - [x] `deno lint` and `deno fmt` clean - [x] All 3557 tests pass - [x] New tests: permission denied throws UserError with sudo suggestion - [x] New tests: writable path proceeds normally - [x] New tests: nonexistent binary path (fresh install) proceeds normally
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
- Root in CI: The "binary path is not writable" test (
chmod 0o444) will pass silently as a no-op when the test suite runs as root, since root bypasses POSIX file permission checks. Not a real concern for correctness, but worth noting if you ever see unexpected green in a root-privileged CI runner.
Notes
- DDD:
checkWritePermission()is well-placed as a pre-condition on theUpdateServicedomain service. The safety-net catch inreplaceBinary()(infrastructure) provides defense-in-depth. Clean separation of concerns. - Import boundaries: No violations — domain imports stay within domain, infrastructure imports from domain as expected.
- Security:
Deno.open({ write: true })withouttruncatecorrectly probes write access without modifying the file. Error messages are actionable without leaking sensitive info. - Test coverage: Three new tests cover permission-denied, writable, and nonexistent-binary paths. Assertions check both error type (
UserError) and message content. - Code style: License headers, named exports,
@std/assert, test naming — all conform to CLAUDE.md conventions.
Clean, well-scoped fix. LGTM.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None.
Medium
-
TOCTOU gap between pre-flight check and actual write —
update_service.ts:154-168:checkWritePermission()opens the file at the start ofupdate(), but the actual binary replacement happens much later (after download, checksum verification, and extraction). Permissions could change between the check and the install. This is mitigated by the safety netPermissionDeniedcatch inreplaceBinary()(line 73-76 ofhttp_update_checker.ts), so the user still gets a clear error — they just waste the download first. Since the stated goal of the PR is to avoid that wasted download, this is worth noting but not blocking since it would require an adversarial or unlikely permission change mid-operation. -
Pre-flight check doesn't cover parent directory for fresh installs —
update_service.ts:166: When the binary doesn't exist yet,Deno.openthrowsNotFound, which is silently caught, andcheckWritePermission()returns successfully. But if the parent directory isn't writable, the download will complete and thenreplaceBinarywill fail atDeno.rename/Deno.copyFile. For fresh installs to a non-writable directory, the user still gets a post-download failure. Edge case, not a regression from current behavior.
Low
- Test may be vacuous when run as root —
update_service_test.ts:283-307: The "binary path is not writable" test setschmod 0o444, but root can write to read-only files. If CI runs tests as root,Deno.open({ write: true })succeeds, the test'sassertRejectsfails, and you'd catch it immediately. So this isn't a silent correctness issue — it would be a noisy test failure, not a false pass. Still worth knowing if you ever see this test behave differently in CI vs local.
Verdict
PASS — Clean, well-scoped change. The pre-flight check is a genuine UX improvement, the safety net in replaceBinary is correctly wired (the UserError propagates through the outer EXDEV catch without being swallowed), and the tests cover the three important cases (denied, writable, nonexistent). The TOCTOU gap is inherent to pre-flight checks and is properly backstopped.
Summary
PermissionDeniedinreplaceBinary()as a safety net with the same actionable messagesudo swamp updateFixes #870
Test Plan
deno checkpassesdeno lintanddeno fmtclean