f2fs: fix to zero post-EOF data when extending file size

generic/794  4s ... - output mismatch (see /share/git/fstests/results//generic/794.out.bad)
    --- tests/generic/794.out   2026-06-12 08:46:32.766426241 +0800
    +++ /share/git/fstests/results//generic/794.out.bad 2026-07-05 18:32:55.000000000 +0800
    @@ -1,4 +1,16 @@
     QA output created by 794
     append_write
    +FAIL: non-zero data in gap [4080,4096) after shutdown+remount
    +000000 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a  >ZZZZZZZZZZZZZZZZ<
    +*
    +001000
     truncate_up
    ...
    (Run 'diff -u /share/git/fstests/tests/generic/794.out /share/git/fstests/results//generic/794.out.bad'  to see the entire diff)
Ran: generic/794
Failures: generic/794
Failed 1 of 1 tests

Steps of generic/794:
1. write 4096 bytes to file w/ 0x5a
2. use fiemap to get PBA of first block in file
3. truncate file to 4080
4. umount; write 4096 bytes to file w/ 0x5a directly via PBA; mount
5. extend filesize via
   a) append 4096 from offset 4096, or
   b) truncate 8192, or
   c) fallocate 4096 from offset 4096
6. verify the gap is zeroed in memory [4080,4096)
7. sync range 4096 from offset 4096; shutdown -f (flush meta before shutdown)
8. umount; mount; verify [4080,4096) is zeroed or not.

When extending file size (e.g. via truncate, fallocate, or write) across an
unaligned EOF boundary, we need to ensure that post-EOF data in the partial
page is zeroed out in pagecache and marked dirty, then writeback the cache to
persist zeroed data before committing inode w/ updated i_size.

This help to prevent stale disk data beyond the previous EOF from being exposed
after remounting or crash recovery.

Since f2fs is a LFS filesystem, we only support direct write via PBA in pinfile,
and pinfile has section-aligned filesize, so in Android, there should no problem,
but for other usage in different environment, let's fix this w/ fsync_mode=strict
mount option.

Cc: stable@kernel.org
Signed-off-by: Chao Yu <chao@kernel.org>
Signed-off-by: Jaegeuk Kim <jaegeuk@kernel.org>
This commit is contained in:
Chao Yu 2026-08-11 15:16:37 +08:00 committed by Jaegeuk Kim
parent b2205d3cfd
commit 5eced87b7d

View File

@ -36,16 +36,52 @@
#include <trace/events/f2fs.h>
#include <uapi/linux/f2fs.h>
static void f2fs_zero_post_eof_page(struct inode *inode,
loff_t new_size, bool lock)
static int fill_zero(struct inode *inode, pgoff_t index,
loff_t start, loff_t len);
static int do_zero_post_eof_page(struct inode *inode, loff_t new_size)
{
loff_t old_size = i_size_read(inode);
unsigned int offset, len;
pgoff_t index;
int err;
offset = old_size & (PAGE_SIZE - 1);
if (!offset)
return 0;
len = min_t(loff_t, PAGE_SIZE - offset, new_size - old_size);
index = old_size >> PAGE_SHIFT;
if (f2fs_has_inline_data(inode)) {
/* data post eof should be always zero */
if (new_size <= MAX_INLINE_DATA(inode))
return 0;
err = f2fs_convert_inline_inode(inode);
if (err)
return err;
}
err = fill_zero(inode, index, offset, len);
if (err)
return err;
return filemap_write_and_wait_range(inode->i_mapping,
old_size, old_size + len - 1);
}
static int f2fs_zero_post_eof_page(struct inode *inode,
loff_t new_size, bool lock, bool writeback)
{
loff_t old_size = i_size_read(inode);
bool strict =
F2FS_OPTION(F2FS_I_SB(inode)).fsync_mode == FSYNC_MODE_STRICT;
if (old_size >= new_size)
return;
return 0;
if (mapping_empty(inode->i_mapping))
return;
if (!strict && mapping_empty(inode->i_mapping))
return 0;
if (lock)
filemap_invalidate_lock(inode->i_mapping);
@ -53,6 +89,16 @@ static void f2fs_zero_post_eof_page(struct inode *inode,
truncate_inode_pages_range(inode->i_mapping, old_size, new_size - 1);
if (lock)
filemap_invalidate_unlock(inode->i_mapping);
if (!writeback || !strict)
return 0;
/*
* In fsync_mode=strict, when we expand an unaligned EOF size, we
* should zero post EOF data and writeback the data immediately,
* so that it can avoid exposing stale data after metadata flush
* and POR.
*/
return do_zero_post_eof_page(inode, new_size);
}
static vm_fault_t f2fs_filemap_fault(struct vm_fault *vmf)
@ -132,7 +178,10 @@ static vm_fault_t f2fs_vm_page_mkwrite(struct vm_fault *vmf)
f2fs_bug_on(sbi, f2fs_has_inline_data(inode));
f2fs_zero_post_eof_page(inode, (folio->index + 1) << PAGE_SHIFT, true);
err = f2fs_zero_post_eof_page(inode,
(folio->index + 1) << PAGE_SHIFT, true, false);
if (err)
goto out_pagefault;
file_update_time(vmf->vma->vm_file);
filemap_invalidate_lock_shared(inode->i_mapping);
@ -189,7 +238,7 @@ static vm_fault_t f2fs_vm_page_mkwrite(struct vm_fault *vmf)
out_sem:
filemap_invalidate_unlock_shared(inode->i_mapping);
out_pagefault:
sb_end_pagefault(inode->i_sb);
out:
ret = vmf_fs_error(err);
@ -1182,8 +1231,12 @@ int f2fs_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
f2fs_down_write(&fi->i_gc_rwsem[WRITE]);
filemap_invalidate_lock(inode->i_mapping);
if (attr->ia_size > old_size)
f2fs_zero_post_eof_page(inode, attr->ia_size, false);
if (attr->ia_size > old_size) {
err = f2fs_zero_post_eof_page(inode,
attr->ia_size, false, true);
if (err)
goto err_out;
}
truncate_setsize(inode, attr->ia_size);
if (attr->ia_size <= old_size)
@ -1192,6 +1245,7 @@ int f2fs_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
* do not trim all blocks after i_size if target size is
* larger than i_size.
*/
err_out:
filemap_invalidate_unlock(inode->i_mapping);
f2fs_up_write(&fi->i_gc_rwsem[WRITE]);
if (err)
@ -1303,7 +1357,9 @@ static int f2fs_punch_hole(struct inode *inode, loff_t offset, loff_t len)
if (ret)
return ret;
f2fs_zero_post_eof_page(inode, offset + len, true);
ret = f2fs_zero_post_eof_page(inode, offset + len, true, false);
if (ret)
return ret;
pg_start = ((unsigned long long) offset) >> PAGE_SHIFT;
pg_end = ((unsigned long long) offset + len) >> PAGE_SHIFT;
@ -1590,7 +1646,9 @@ static int f2fs_do_collapse(struct inode *inode, loff_t offset, loff_t len)
f2fs_down_write(&F2FS_I(inode)->i_gc_rwsem[WRITE]);
filemap_invalidate_lock(inode->i_mapping);
f2fs_zero_post_eof_page(inode, offset + len, false);
ret = f2fs_zero_post_eof_page(inode, offset + len, false, false);
if (ret)
goto out_unlock;
f2fs_lock_op(sbi, &lc);
f2fs_drop_extent_tree(inode);
@ -1598,6 +1656,7 @@ static int f2fs_do_collapse(struct inode *inode, loff_t offset, loff_t len)
ret = __exchange_data_block(inode, inode, end, start, nrpages - end, true);
f2fs_unlock_op(sbi, &lc);
out_unlock:
filemap_invalidate_unlock(inode->i_mapping);
f2fs_up_write(&F2FS_I(inode)->i_gc_rwsem[WRITE]);
return ret;
@ -1719,7 +1778,9 @@ static int f2fs_zero_range(struct inode *inode, loff_t offset, loff_t len,
if (ret)
return ret;
f2fs_zero_post_eof_page(inode, offset + len, true);
ret = f2fs_zero_post_eof_page(inode, offset + len, true, false);
if (ret)
return ret;
pg_start = ((unsigned long long) offset) >> PAGE_SHIFT;
pg_end = ((unsigned long long) offset + len) >> PAGE_SHIFT;
@ -1854,7 +1915,9 @@ static int f2fs_insert_range(struct inode *inode, loff_t offset, loff_t len)
f2fs_down_write(&F2FS_I(inode)->i_gc_rwsem[WRITE]);
filemap_invalidate_lock(mapping);
f2fs_zero_post_eof_page(inode, offset + len, false);
ret = f2fs_zero_post_eof_page(inode, offset + len, false, false);
if (ret)
goto out_unlock;
truncate_pagecache(inode, offset);
while (!ret && idx > pg_start) {
@ -1872,6 +1935,7 @@ static int f2fs_insert_range(struct inode *inode, loff_t offset, loff_t len)
idx + delta, nr, false);
f2fs_unlock_op(sbi, &lc);
}
out_unlock:
filemap_invalidate_unlock(mapping);
f2fs_up_write(&F2FS_I(inode)->i_gc_rwsem[WRITE]);
if (ret)
@ -1914,7 +1978,9 @@ static int f2fs_expand_inode_data(struct inode *inode, loff_t offset,
if (err)
return err;
f2fs_zero_post_eof_page(inode, offset + len, true);
err = f2fs_zero_post_eof_page(inode, offset + len, true, true);
if (err)
return err;
f2fs_balance_fs(sbi, true);
@ -5285,8 +5351,10 @@ static ssize_t f2fs_write_checks(struct kiocb *iocb, struct iov_iter *from)
if (err)
return err;
f2fs_zero_post_eof_page(inode,
iocb->ki_pos + iov_iter_count(from), true);
err = f2fs_zero_post_eof_page(inode,
iocb->ki_pos + iov_iter_count(from), true, true);
if (err)
return err;
return count;
}