diff --git a/config/user_config.go b/config/user_config.go index e88fa61..b7b0308 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net" "os" + "path/filepath" "strconv" "strings" "sync" @@ -61,15 +62,24 @@ func (c *Config) saveConfigLocked() error { if err != nil { return err } - tmp := c.fn + ".tmp" - if err := ioutil.WriteFile(tmp, data, 0600); err != nil { + file, err := os.CreateTemp(filepath.Dir(c.fn), filepath.Base(c.fn)+".tmp-") + if err != nil { return err } - if err := os.Rename(tmp, c.fn); err != nil { - _ = os.Remove(tmp) + tmp := file.Name() + defer os.Remove(tmp) + if err := file.Chmod(0600); err != nil { + file.Close() return err } - return nil + if _, err := file.Write(data); err != nil { + file.Close() + return err + } + if err := file.Close(); err != nil { + return err + } + return os.Rename(tmp, c.fn) } func key(k uiterm.Key) *uiterm.Key { diff --git a/config/user_config_save_test.go b/config/user_config_save_test.go index 4a1b8d5..436ed14 100644 --- a/config/user_config_save_test.go +++ b/config/user_config_save_test.go @@ -1,6 +1,7 @@ package config import ( + "os" "path/filepath" "sync" "testing" @@ -16,6 +17,25 @@ func TestSaveConfigReturnsWriteError(t *testing.T) { } } +func TestSaveConfigDoesNotUsePredictableTemporaryPath(t *testing.T) { + path := filepath.Join(t.TempDir(), "barnard.toml") + legacyTemp := path + ".tmp" + if err := os.WriteFile(legacyTemp, []byte("sentinel"), 0600); err != nil { + t.Fatal(err) + } + cfg := NewConfig(&path) + if err := cfg.SaveConfig(); err != nil { + t.Fatal(err) + } + contents, err := os.ReadFile(legacyTemp) + if err != nil { + t.Fatal(err) + } + if string(contents) != "sentinel" { + t.Fatalf("predictable temporary file was modified: %q", contents) + } +} + func TestConcurrentConfigurationUpdatesAndWrites(t *testing.T) { path := filepath.Join(t.TempDir(), "barnard.toml") cfg := NewConfig(&path)