storage: fsync staging directory before atomic rename#922
Conversation
mtrmac
left a comment
There was a problem hiding this comment.
https://github.com/podman-container-tools/container-libs/blob/main/CONTRIBUTING.md#sign-your-prs please, we can’t really even look at contributions with unclear copyright status.
We now have sync=filesystem via #622 , isn’t that sufficient? What is missing?
e411848 to
986fac7
Compare
Thank you @mtrmac for your feedback. sync=filesystem helps, but doesn't fully close this gap: it's opt-in ( |
*shrug* so opt in.
That shouldn’t matter; the store is locked in the meantime, so there should be no concurrent accesses . And in case of power loss or the like, what governs is the layer database in |
Signed-off-by: Aditi Sahay <asahay@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
986fac7 to
1e62330
Compare
| // inode metadata, which is already covered once the parent | ||
| // directory is fsync'd below. Treating them like regular |
There was a problem hiding this comment.
What documents that fsync on a directory also syncs all nodes of its children?
|
Thanks for the detailed explanation, @mtrmac, that makes sense. If the store lock genuinely prevents any concurrent access to the layer while it's still flagged incomplete, and layers.json is what governs recovery after a crash (not the on-disk layer content itself), then sync=filesystem should indeed close this gap without needing a pre-rename fsync. I am going to validate that end-to-end (kill -9 mid-pull with sync=filesystem enabled, confirm check --repair cleans up correctly) before I invest further here. If that holds up, I will likely move to adopting/backporting sync=filesystem instead of this approach, and I'll close this PR out. Will report back either way. appreciate you pushing on this. |
PR Summary
Add
fsyncof overlay staging directory contents before the atomic rename thatcommits a new layer during image pull/unpack.
Replaces auto-closed containers/storage#2397
after the storage library moved into this monorepo.
Problem
This gap was reported against real customer deployments. Edge and
disconnected SNO customers (vehicle-mounted, tactical, and air-gapped
deployments with unstable power) hit
readlink: invalid argumenterrors andCRI-O crashes after hard resets, and requested a fix, since these nodes cannot
simply re-pull images from a registry that isn't reachable.
Root cause: when CRI-O (via
go.podman.io/storage) pulls an image, layer datais written into a staging directory and then committed with
os.Rename().Nothing forces that data to physical disk before the rename makes it visible
at its final path.
If power is lost in that window, the layer isn't just missing — it's present
but corrupted: binaries can end up truncated, and overlay
l/symlinks can beleft invalid, producing exactly the
readlink: invalid argumenterror reportedabove. On restart, CRI-O detects the unclean shutdown and
internal_repaircan't tell which layer is actually bad, so it falls back to a broad,
destructive repair — on a disconnected node, that means unrecoverable image
loss.
Solution
Guarantee durability before the rename ever exposes the new layer, so that
window can't exist regardless of
sync=filesystemconfiguration:fdatasync()every file under the staging directoryfsync()every directory under the staging tree, bottom-upos.Rename()This mirrors the pattern already used by
atomicFileWriter(
storage/pkg/ioutils/fswriters_linux.go) for single files, extended to awhole staging tree. It's scoped to just the layer being committed — much
cheaper than
Syncfs()'s whole-filesystem flushChanges
storage/pkg/ioutils/sync_directory_linux.goSyncDirectoryContents()helper (Linux)storage/pkg/ioutils/sync_directory_linux_test.gostorage/drivers/overlay/overlay.goApplyDiffFromStagingDirectory()Testing