diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 9b2ac14712..6ef756aa03 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -205,7 +205,17 @@ func writeTemporaryKeyFile(file string, content []byte) (string, error) { os.Remove(f.Name()) return "", err } - f.Close() + // Sync before rename so a crash between the write and the rename + // cannot leave a zero-length keyfile at the final path. + if err := f.Sync(); err != nil { + f.Close() + os.Remove(f.Name()) + return "", err + } + if err := f.Close(); err != nil { + os.Remove(f.Name()) + return "", err + } return f.Name(), nil } diff --git a/accounts/keystore/key_test.go b/accounts/keystore/key_test.go new file mode 100644 index 0000000000..da8e264724 --- /dev/null +++ b/accounts/keystore/key_test.go @@ -0,0 +1,54 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "bytes" + "os" + "path/filepath" + "testing" +) + +func TestWriteKeyFile(t *testing.T) { + t.Parallel() + dir := t.TempDir() + path := filepath.Join(dir, "keyfile") + content := []byte(`{"address":"deadbeef"}`) + + if err := writeKeyFile(path, content); err != nil { + t.Fatalf("writeKeyFile: %v", err) + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read back: %v", err) + } + if !bytes.Equal(got, content) { + t.Fatalf("content mismatch: got %q want %q", got, content) + } + // Verify no temp file leaked into the directory. + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("readdir: %v", err) + } + if len(entries) != 1 || entries[0].Name() != "keyfile" { + var names []string + for _, e := range entries { + names = append(names, e.Name()) + } + t.Fatalf("unexpected directory contents: %v", names) + } +}