From 5678125f6f019a788106fedf15bd883ed990bf7c Mon Sep 17 00:00:00 2001 From: Aditi Sahay Date: Wed, 17 Jun 2026 17:30:16 +0530 Subject: [PATCH] overlay: fsync staging directory before atomic rename --- drivers/overlay/overlay.go | 5 ++ pkg/ioutils/sync_directory_linux.go | 63 ++++++++++++++++++++++++ pkg/ioutils/sync_directory_linux_test.go | 32 ++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 pkg/ioutils/sync_directory_linux.go create mode 100644 pkg/ioutils/sync_directory_linux_test.go diff --git a/drivers/overlay/overlay.go b/drivers/overlay/overlay.go index 2bc115afd2..69263b7cb5 100644 --- a/drivers/overlay/overlay.go +++ b/drivers/overlay/overlay.go @@ -30,6 +30,7 @@ import ( "github.com/containers/storage/pkg/directory" "github.com/containers/storage/pkg/fileutils" "github.com/containers/storage/pkg/fsutils" + "github.com/containers/storage/pkg/ioutils" "github.com/containers/storage/pkg/idmap" "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/mount" @@ -2361,6 +2362,10 @@ func (d *Driver) ApplyDiffFromStagingDirectory(id, parent string, diffOutput *gr return err } + if err := ioutils.SyncDirectoryContents(stagingDirectory); err != nil { + return fmt.Errorf("sync staging directory before rename: %w", err) + } + return os.Rename(stagingDirectory, diffPath) } diff --git a/pkg/ioutils/sync_directory_linux.go b/pkg/ioutils/sync_directory_linux.go new file mode 100644 index 0000000000..b8f206a392 --- /dev/null +++ b/pkg/ioutils/sync_directory_linux.go @@ -0,0 +1,63 @@ +//go:build linux + +package ioutils + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + + "golang.org/x/sys/unix" +) + +// SyncDirectoryContents flushes file data and directory metadata under dir to +// physical storage. Call this before atomically renaming a fully populated +// staging directory to its final location. +func SyncDirectoryContents(dir string) error { + var dirs []string + + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if d.IsDir() { + dirs = append(dirs, path) + return nil + } + + f, err := os.Open(path) + if err != nil { + return err + } + + syncErr := unix.Fdatasync(int(f.Fd())) + closeErr := f.Close() + if syncErr != nil { + return syncErr + } + + return closeErr + }) + if err != nil { + return fmt.Errorf("sync directory contents in %q: %w", dir, err) + } + + for i := len(dirs) - 1; i >= 0; i-- { + dfd, err := os.Open(dirs[i]) + if err != nil { + return fmt.Errorf("open directory %q for sync: %w", dirs[i], err) + } + + syncErr := unix.Fsync(int(dfd.Fd())) + closeErr := dfd.Close() + if syncErr != nil { + return fmt.Errorf("sync directory %q: %w", dirs[i], syncErr) + } + if closeErr != nil { + return fmt.Errorf("close directory %q after sync: %w", dirs[i], closeErr) + } + } + + return nil +} diff --git a/pkg/ioutils/sync_directory_linux_test.go b/pkg/ioutils/sync_directory_linux_test.go new file mode 100644 index 0000000000..0ba8779088 --- /dev/null +++ b/pkg/ioutils/sync_directory_linux_test.go @@ -0,0 +1,32 @@ +//go:build linux + +package ioutils + +import ( + "os" + "path/filepath" + "testing" +) + +func TestSyncDirectoryContents(t *testing.T) { + dir := t.TempDir() + + nested := filepath.Join(dir, "nested") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatalf("mkdir nested: %v", err) + } + + files := []string{ + filepath.Join(dir, "file1"), + filepath.Join(nested, "file2"), + } + for _, file := range files { + if err := os.WriteFile(file, []byte("storage-resilience"), 0o644); err != nil { + t.Fatalf("write file %q: %v", file, err) + } + } + + if err := SyncDirectoryContents(dir); err != nil { + t.Fatalf("SyncDirectoryContents: %v", err) + } +}