fix(updater): probe swap path before enabling self-updates

The 2.0.0 update helper renames staged files from os.TempDir() onto the
running executable's directory. On the common Arch/Fedora layout where
/tmp is tmpfs and $HOME is persistent, that rename crosses filesystems
and fails with EXDEV ("invalid cross-device link"). The helper's
recovery path then deletes the working binary while failing to restore
it: twenty observed attempts, an orphaned .bak, and no running app.

Guard against this at startup instead of mid-swap:

- swapPathProven() exercises the exact move the helper will perform
  (os.Rename from os.TempDir() into the executable's directory) with a
  throwaway probe file, then cleans up after itself.
- maybeEnableUpdater() skips the updater entirely when the probe fails
  or the build version is empty, logging the reason. A loud skip beats
  a destructive attempt that can strand the user with no app.
- renameProbeOK() is the testable core, taking explicit tmp/target dirs.
- updater_probe_test.go covers same-filesystem success with no leftover
  files, and fail-closed behavior for missing tmp dir, missing target
  dir, and unwritable target dir.
- V3_MIGRATION.md documents the probe and the incident that motivated
  it.
This commit is contained in:
John O'Keefe
2026-09-22 13:02:00 -04:00
parent 2ab5a080a9
commit 4f36457563
3 changed files with 115 additions and 1 deletions
+6 -1
View File
@@ -49,7 +49,12 @@ headless; the builtin window opens only when an update is found.
Only the binary self-swaps; icons/`.desktop` still come from the tarball's
`install_linux.sh`. Signing: public half `updater.pub` is embedded; the
private key lives in the password manager + the `UPDATER_SIGNING_KEY` CI
secret, never in the repo (see `.gitignore`).
secret, never in the repo (see `.gitignore`). Startup also guards the swap
itself: it proves `os.TempDir()` can be renamed into the executable directory
before offering an update, because the 2.0.0 helper showed twenty EXDEV
`invalid cross-device link` attempts on tmpfs `/tmp` versus persistent `$HOME`,
then orphaned `.bak` with nothing running. A failed probe disables updates
with a log line instead of risking the installed binary.
## Future plans on v3