Skip to content

fix: graceful error on permission denied during swamp update#871

Merged
stack72 merged 1 commit intomainfrom
fix-graceful-update
Mar 26, 2026
Merged

fix: graceful error on permission denied during swamp update#871
stack72 merged 1 commit intomainfrom
fix-graceful-update

Conversation

@stack72
Copy link
Contributor

@stack72 stack72 commented Mar 26, 2026

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

  • deno check passes
  • deno lint and deno fmt clean
  • All 3557 tests pass
  • New tests: permission denied throws UserError with sudo suggestion
  • New tests: writable path proceeds normally
  • New tests: nonexistent binary path (fresh install) proceeds normally

## 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
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Blocking Issues

None.

Suggestions

  1. 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 the UpdateService domain service. The safety-net catch in replaceBinary() (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 }) without truncate correctly 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.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Review

Critical / High

None.

Medium

  1. TOCTOU gap between pre-flight check and actual writeupdate_service.ts:154-168: checkWritePermission() opens the file at the start of update(), 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 net PermissionDenied catch in replaceBinary() (line 73-76 of http_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.

  2. Pre-flight check doesn't cover parent directory for fresh installsupdate_service.ts:166: When the binary doesn't exist yet, Deno.open throws NotFound, which is silently caught, and checkWritePermission() returns successfully. But if the parent directory isn't writable, the download will complete and then replaceBinary will fail at Deno.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

  1. Test may be vacuous when run as rootupdate_service_test.ts:283-307: The "binary path is not writable" test sets chmod 0o444, but root can write to read-only files. If CI runs tests as root, Deno.open({ write: true }) succeeds, the test's assertRejects fails, 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.

@stack72 stack72 merged commit ee02542 into main Mar 26, 2026
10 checks passed
@stack72 stack72 deleted the fix-graceful-update branch March 26, 2026 09:35
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.

swamp update fails with PermissionDenied (EACCES) removing /usr/local/bin/swamp

1 participant