diff --git a/docs/updates.md b/docs/updates.md index 74e89c6b..e597a2b6 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -47,6 +47,11 @@ Dry-run downloads bounded release metadata and the checksum sidecar to validate asset selection and checksum availability. It does not download the executable archive, replace the binary, or update the automatic-check timestamp. +The update destination must not be writable by its group or by other users. If +`--update` reports that the replacement directory is writable, remove those +permissions with `chmod go-w ` (use `sudo` if needed), or install +`fetch` in a private directory such as `~/.local/bin`. + ## Automatic checks Set an interval in the configuration file: diff --git a/internal/update/update_unix.go b/internal/update/update_unix.go index f78774d3..9b1b5923 100644 --- a/internal/update/update_unix.go +++ b/internal/update/update_unix.go @@ -231,7 +231,8 @@ func validateReplacementDirectory(target string) error { return errors.New("replacement directory is not a real directory") } if info.Mode().Perm()&022 != 0 { - return fmt.Errorf("replacement directory is writable by group or others (mode %04o)", info.Mode().Perm()) + dir := filepath.Dir(target) + return fmt.Errorf("replacement directory %q is writable by group or others (mode %04o); remove group/other write permission by running `chmod go-w` on this directory (use `sudo` if needed), or install fetch in a private directory such as `~/.local/bin`", dir, info.Mode().Perm()) } return nil } diff --git a/internal/update/update_unix_test.go b/internal/update/update_unix_test.go index 7ece23ee..27d2564a 100644 --- a/internal/update/update_unix_test.go +++ b/internal/update/update_unix_test.go @@ -8,6 +8,7 @@ import ( "errors" "os" "path/filepath" + "strings" "testing" "github.com/klauspost/compress/gzip" @@ -170,6 +171,24 @@ func TestSelfReplaceIsAtomicAndRejectsSymlink(t *testing.T) { } } +func TestValidateReplacementDirectory_ActionableError(t *testing.T) { + dir := t.TempDir() + if err := os.Chmod(dir, 0775); err != nil { + t.Fatal(err) + } + + err := validateReplacementDirectory(filepath.Join(dir, "fetch")) + if err == nil { + t.Fatal("validateReplacementDirectory accepted a group-writable directory") + } + message := err.Error() + for _, want := range []string{"writable by group or others", "0775", dir, "chmod go-w", "private directory"} { + if !strings.Contains(message, want) { + t.Errorf("error = %q, want it to contain %q", message, want) + } + } +} + func TestCanReplaceFile_ReadOnlyFileWritableDirectory(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "fetch")