From d31088656047465e675d1f8a3c011042e9a4a654 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Tue, 26 May 2026 09:34:51 -0700 Subject: [PATCH] Use platform-specific atomic key replacement on Windows Swap the keystore through a temp and .old file on Windows instead of relying on os.Rename alone, which is not reliably atomic there. Signed-off-by: Bryan Frimin --- pkg/deviceagent/atomicfile_unix.go | 36 ++++++++++++++++++++ pkg/deviceagent/atomicfile_windows.go | 48 +++++++++++++++++++++++++++ pkg/deviceagent/keystore.go | 9 ++--- 3 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 pkg/deviceagent/atomicfile_unix.go create mode 100644 pkg/deviceagent/atomicfile_windows.go diff --git a/pkg/deviceagent/atomicfile_unix.go b/pkg/deviceagent/atomicfile_unix.go new file mode 100644 index 000000000..5b6a4e4d2 --- /dev/null +++ b/pkg/deviceagent/atomicfile_unix.go @@ -0,0 +1,36 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +//go:build !windows + +package deviceagent + +import ( + "fmt" + "os" +) + +func replaceRegularFile(path string, data []byte, perm os.FileMode) error { + tmp := path + ".tmp" + if err := os.WriteFile(tmp, data, perm); err != nil { + return fmt.Errorf("cannot write temp file: %w", err) + } + + if err := os.Rename(tmp, path); err != nil { + _ = os.Remove(tmp) + return fmt.Errorf("cannot atomically replace file: %w", err) + } + + return nil +} diff --git a/pkg/deviceagent/atomicfile_windows.go b/pkg/deviceagent/atomicfile_windows.go new file mode 100644 index 000000000..68e0426f3 --- /dev/null +++ b/pkg/deviceagent/atomicfile_windows.go @@ -0,0 +1,48 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +//go:build windows + +package deviceagent + +import ( + "errors" + "fmt" + "os" +) + +func replaceRegularFile(path string, data []byte, perm os.FileMode) error { + tmp := path + ".tmp" + if err := os.WriteFile(tmp, data, perm); err != nil { + return fmt.Errorf("cannot write temp file: %w", err) + } + + old := path + ".old" + _ = os.Remove(old) + + if err := os.Rename(path, old); err != nil && !errors.Is(err, os.ErrNotExist) { + _ = os.Remove(tmp) + return fmt.Errorf("cannot move existing file aside: %w", err) + } + + if err := os.Rename(tmp, path); err != nil { + _ = os.Rename(old, path) + _ = os.Remove(tmp) + return fmt.Errorf("cannot replace file: %w", err) + } + + _ = os.Remove(old) + + return nil +} diff --git a/pkg/deviceagent/keystore.go b/pkg/deviceagent/keystore.go index bd7d15874..cb7af2b1c 100644 --- a/pkg/deviceagent/keystore.go +++ b/pkg/deviceagent/keystore.go @@ -48,13 +48,8 @@ func SaveAPIKey(dir, key string) error { } path := KeyPath(dir) - tmp := path + ".tmp" - if err := os.WriteFile(tmp, []byte(strings.TrimSpace(key)+"\n"), 0o600); err != nil { - return fmt.Errorf("cannot write key: %w", err) - } - - if err := os.Rename(tmp, path); err != nil { - return fmt.Errorf("cannot atomically replace key: %w", err) + if err := replaceRegularFile(path, []byte(strings.TrimSpace(key)+"\n"), 0o600); err != nil { + return fmt.Errorf("cannot replace key: %w", err) } return nil