Stale page cache reads after file truncation on local filesystems
0 reputation · 28 Nov 2024, 14:23 UTC
0 reputation · 28 Nov 2024, 14:23 UTC
When a file is truncated to a smaller size, the Linux kernel may leave existing page‑cache pages that cover the truncated region intact until they are reclaimed or explicitly invalidated. This can cause subsequent reads—either via regular read() or through a shared memory mapping—to return data that was written before the truncate, i.e., stale content. The invalidation is immediate for private mappings on most local filesystems, but it can be delayed when the file is mapped shared, when the underlying filesystem uses delayed allocation or writeback, or when the client is an NFS mount relying on attribute caching. The precise point at which the kernel discards the stale pages depends on factors such as the vm.dirty_ratio setting, page‑reclaim pressure, and kernel version (changes appeared between 4.14 and 5.4).
Under what circumstances does the kernel defer invalidation of page‑cache pages after truncate? How do shared memory mappings affect the visibility of stale data after truncate? Which kernel version ranges exhibit the deferred invalidation behavior?
On local filesystems, ftruncate() normally removes page‑cache pages beyond the new size. Deferred invalidation is not the default for MAP_PRIVATE mappings; it is primarily observed with MAP_SHARED mappings on filesystems that delay block allocation or writeback, and when dirty pages have not yet been flushed. Kernel version influences how promptly the truncate path zaps shared pages, but the practical trigger is the combination of a shared mapping, unwritten dirty data, and low writeback or reclaim pressure.
Established on standard local filesystems:
MAP_PRIVATE mappings do not expose pre‑truncate bytes after ftruncate(); the kernel unmaps or copy‑on‑writes the affected ranges.MAP_SHARED mappings alias the page cache directly, so any page that survives truncation remains readable through the mapping.Conditional and configuration‑dependent:
delalloc, XFS, btrfs) can keep truncated regions backed by dirty pages that have not yet been flushed.vm.dirty_ratio and vm.dirty_background_ratio control how long those pages persist; higher values widen the window.With MAP_SHARED, the mapping is a direct window into the file's page cache. If ftruncate() shrinks the file but the kernel leaves the tail pages resident—because they are dirty, under writeback, or not yet reclaimed—a read through the mapping still sees the old bytes. The data is not restored from disk; it is the same in‑memory page that was present before the truncate. MAP_PRIVATE avoids this because the kernel severs the connection to those pages during truncation.
MAP_SHARED, the risk exists.delalloc or XFS), dirty pages can outlive the truncate.sysctl vm.dirty_ratio vm.dirty_background_ratio.mmap it MAP_SHARED, ftruncate to a smaller size, and read the tail through the mapping.MAP_PRIVATE; stale data should not appear.msync(addr, len, MS_SYNC) before truncating, or invalidate after truncation with msync(addr, len, MS_INVALIDATE) where supported. Dropping caches or inducing reclaim pressure can confirm the diagnosis but are not production fixes.The single most useful missing detail is the filesystem type and mount options. On ext4 with nodelalloc or on a filesystem that allocates blocks eagerly, the stale‑page window is typically negligible. On ext4 with delalloc, XFS, or btrfs, the recommendation shifts toward explicit msync discipline or avoiding MAP_SHARED for files that are truncated concurrently.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.