diff --git a/pkg/os/os.go b/pkg/os/os.go index 3c0f53d51a..62ad1a7ae4 100644 --- a/pkg/os/os.go +++ b/pkg/os/os.go @@ -98,9 +98,12 @@ func CopyFile(src, dst string) (err error) { if info.IsDir() { return errors.New("cannot read from directories") } + if dstInfo, statErr := os.Stat(dst); statErr == nil && os.SameFile(info, dstInfo) { + return errors.New("source and destination are the same file") + } - // create new file, truncate if exists and apply same permissions as the original one - dstfile, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode().Perm()) //nolint:gosec + // Open without truncating so we can check the opened file's identity first. + dstfile, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, info.Mode().Perm()) //nolint:gosec if err != nil { return err } @@ -114,6 +117,17 @@ func CopyFile(src, dst string) (err error) { } }() + dstInfo, err := dstfile.Stat() + if err != nil { + return err + } + if os.SameFile(info, dstInfo) { + return errors.New("source and destination are the same file") + } + if err := dstfile.Truncate(0); err != nil { + return err + } + _, err = io.Copy(dstfile, srcfile) return err } diff --git a/pkg/os/os_test.go b/pkg/os/os_test.go index 1c1a88b41b..9c926af63f 100644 --- a/pkg/os/os_test.go +++ b/pkg/os/os_test.go @@ -167,6 +167,39 @@ func TestMustReadFile(t *testing.T) { } } +func TestCopyFileRejectsSameFile(t *testing.T) { + for _, name := range []string{"same path", "hard link", "symbolic link"} { + t.Run(name, func(t *testing.T) { + src := filepath.Join(t.TempDir(), "source") + content := []byte("must survive a failed copy") + if err := os.WriteFile(src, content, 0600); err != nil { + t.Fatal(err) + } + dst := src + if name != "same path" { + dst = src + ".link" + link := os.Link + if name == "symbolic link" { + link = os.Symlink + } + if err := link(src, dst); err != nil { + t.Skipf("link is unavailable: %v", err) + } + } + if err := CopyFile(src, dst); err == nil { + t.Fatal("expected copying a file onto itself to fail") + } + got, err := os.ReadFile(src) + if err != nil { + t.Fatal(err) + } + if string(got) != string(content) { + t.Fatalf("source content changed: got %q, want %q", got, content) + } + }) + } +} + func TestMustWriteFile(t *testing.T) { tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.txt")