summaryrefslogtreecommitdiff
path: root/mm/truncate.c
diff options
context:
space:
mode:
authorBrian Foster <bfoster@redhat.com>2024-09-19 12:07:41 -0400
committerTheodore Ts'o <tytso@mit.edu>2024-11-12 23:54:14 -0500
commit52aecaee1c26409ebafe080293e0841884f6e9fb (patch)
treea96be6e5c3c2e4848a9c7e5840d00d66b78cd052 /mm/truncate.c
parentc7fc0366c65628fd69bfc310affec4918199aae2 (diff)
mm: zero range of eof folio exposed by inode size extension
On some filesystems, it is currently possible to create a transient data inconsistency between pagecache and on-disk state. For example, on a 1k block size ext4 filesystem: $ xfs_io -fc "pwrite 0 2k" -c "mmap 0 4k" -c "mwrite 2k 2k" \ -c "truncate 8k" -c "fiemap -v" -c "pread -v 2k 16" <file> ... EXT: FILE-OFFSET BLOCK-RANGE TOTAL FLAGS 0: [0..3]: 17410..17413 4 0x1 1: [4..15]: hole 12 00000800: 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 XXXXXXXXXXXXXXXX $ umount <mnt>; mount <dev> <mnt> $ xfs_io -c "pread -v 2k 16" <file> 00000800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ This allocates and writes two 1k blocks, map writes to the post-eof portion of the (4k) eof folio, extends the file, and then shows that the post-eof data is not cleared before the file size is extended. The result is pagecache with a clean and uptodate folio over a hole that returns non-zero data. Once reclaimed, pagecache begins to return valid data. Some filesystems avoid this problem by flushing the EOF folio before inode size extension. This triggers writeback time partial post-eof zeroing. XFS explicitly zeroes newly exposed file ranges via iomap_zero_range(), but this includes a hack to flush dirty but hole-backed folios, which means writeback actually does the zeroing in this particular case as well. bcachefs explicitly flushes the eof folio on truncate extension to the same effect, but doesn't handle the analogous write extension case (i.e., replace "truncate 8k" with "pwrite 4k 4k" in the above example command to reproduce the same problem on bcachefs). btrfs doesn't seem to support subpage block sizes. The two main options to avoid this behavior are to either flush or do the appropriate zeroing during size extending operations. Zeroing is only required when the size change exposes ranges of the file that haven't been directly written, such as a write or truncate that starts beyond the current eof. The pagecache_isize_extended() helper is already used for this particular scenario. It currently cleans any pte's for the eof folio to ensure preexisting mappings fault and allow the filesystem to take action based on the updated inode size. This is required to ensure the folio is fully backed by allocated blocks, for example, but this also happens to be the same scenario zeroing is required. Update pagecache_isize_extended() to zero the post-eof range of the eof folio if it is dirty at the time of the size change, since writeback now won't have the chance. If non-dirty, the folio has either not been written or the post-eof portion was zeroed by writeback. Signed-off-by: Brian Foster <bfoster@redhat.com> Link: https://patch.msgid.link/20240919160741.208162-3-bfoster@redhat.com Signed-off-by: Theodore Ts'o <tytso@mit.edu>
Diffstat (limited to 'mm/truncate.c')
-rw-r--r--mm/truncate.c15
1 files changed, 15 insertions, 0 deletions
diff --git a/mm/truncate.c b/mm/truncate.c
index 0668cd340a46..6e7f3cfb982d 100644
--- a/mm/truncate.c
+++ b/mm/truncate.c
@@ -797,6 +797,21 @@ void pagecache_isize_extended(struct inode *inode, loff_t from, loff_t to)
*/
if (folio_mkclean(folio))
folio_mark_dirty(folio);
+
+ /*
+ * The post-eof range of the folio must be zeroed before it is exposed
+ * to the file. Writeback normally does this, but since i_size has been
+ * increased we handle it here.
+ */
+ if (folio_test_dirty(folio)) {
+ unsigned int offset, end;
+
+ offset = from - folio_pos(folio);
+ end = min_t(unsigned int, to - folio_pos(folio),
+ folio_size(folio));
+ folio_zero_segment(folio, offset, end);
+ }
+
folio_unlock(folio);
folio_put(folio);
}