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:
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRenameProbeOKSameFilesystem(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
targetDir := t.TempDir()
|
||||
if err := renameProbeOK(tmpDir, targetDir); err != nil {
|
||||
t.Fatalf("same-filesystem probe: %s", err)
|
||||
}
|
||||
// No litter: probe file removed, nothing else created.
|
||||
entries, err := os.ReadDir(targetDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
t.Errorf("probe left %d entries in target dir", len(entries))
|
||||
}
|
||||
entries, err = os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
t.Errorf("probe left %d entries in tmp dir", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenameProbeFailsClosed(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
t.Run("missing tmp dir", func(t *testing.T) {
|
||||
if err := renameProbeOK(filepath.Join(tmpDir, "nope"), tmpDir); err == nil {
|
||||
t.Error("expected error for missing tmp dir, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unwritable target dir", func(t *testing.T) {
|
||||
targetDir := t.TempDir()
|
||||
if err := os.Chmod(targetDir, 0o500); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chmod(targetDir, 0o700)
|
||||
if err := renameProbeOK(tmpDir, targetDir); err == nil {
|
||||
t.Error("expected error for unwritable target dir, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing target dir", func(t *testing.T) {
|
||||
if err := renameProbeOK(tmpDir, filepath.Join(tmpDir, "nope")); err == nil {
|
||||
t.Error("expected error for missing target dir, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user