[PATCH 1/7] f2fs: fix to persist i_size extended inside last block for fsync
From: Chao Yu
Date: Thu Oct 22 2026 - 08:55:06 EST
From: Chao Yu <chao@xxxxxxxxxx>
generic/737 fails on 16k block size f2fs, after writing a 1M file w/
O_SYNC buffered or direct IO and shutdown, the file size is 0xff000
rather than 0x100000, i.e. the last fsynced 4k data is lost.
It can be reproduced as below w/ 16k block size:
xfs_io -fs -c "pwrite -S 0x5a 0 32k" /mnt/f2fs/file
xfs_io -x -c "shutdown" /mnt/f2fs
umount /mnt/f2fs; mount /dev/vdd /mnt/f2fs
stat -c %s /mnt/f2fs/file
28672
xfs_io writes 4k per pwrite() and fsyncs it, the 16k-sized 4k unaligned
direct IO falls back to buffered IO as well:
- pwrite [16k, 20k): allocates the block, last_disk_size = 32k (end of
the page) after writeback, i_size (20k) is unaligned, so fsync writes
the inode and its dnode w/ fsync mark
- pwrite [20k, 24k), [24k, 28k): in-place update of the block, i_size is
unaligned, fsync writes the inode w/ i_size = 28k
- pwrite [28k, 32k): in-place update of the block, f2fs_i_size_write()
sets FI_AUTO_RECOVER as the inode was clean; i_size (32k) is aligned
and equals last_disk_size, so f2fs_skip_inode_update() returns true,
and nothing is written for fsync since the in-place update doesn't
dirty the dnode
After shutdown, roll-forward recovery restores i_size = 28k from the
last fsynced inode, and do_recover_data() only extends i_size to the end
of a recovered block when i_size doesn't exceed the block's start
(28k > 16k), so i_size stays at 28k, losing the fsynced 4k data.
FI_AUTO_RECOVER means that the dirtied inode can be recovered from data
blocks via roll-forward recovery, it's not true when i_size is extended
inside the old last block, let's clear FI_AUTO_RECOVER in that case, so
that fsync will write the inode. It happens w/ 4k block size as well,
e.g. w/ 1k O_SYNC appends.
Cc: stable@xxxxxxxxxx
Fixes: 26de9b117130 ("f2fs: avoid unnecessary updating inode during fsync")
Signed-off-by: Chao Yu <chao@xxxxxxxxxx>
---
fs/f2fs/f2fs.h | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h
index e9f8b31d5dab..d6b3955f9a6f 100644
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -3615,8 +3615,9 @@ static inline void f2fs_i_size_write(struct inode *inode, loff_t i_size)
{
bool clean = !is_inode_flag_set(inode, FI_DIRTY_INODE);
bool recover = is_inode_flag_set(inode, FI_AUTO_RECOVER);
+ loff_t old_size = i_size_read(inode);
- if (i_size_read(inode) == i_size)
+ if (old_size == i_size)
return;
i_size_write(inode, i_size);
@@ -3625,7 +3626,18 @@ static inline void f2fs_i_size_write(struct inode *inode, loff_t i_size)
return;
f2fs_mark_inode_dirty_sync(inode, true);
- if (clean || recover)
+
+ /*
+ * Roll-forward recovery only extends i_size to the end of a recovered
+ * block if i_size doesn't exceed that block's start, so it can't
+ * recover an extension inside the old last block, e.g. an O_SYNC 4k
+ * append filling up a 16k block, which is an in-place update of an
+ * already allocated block. Let fsync write the inode for it.
+ */
+ if (i_size > old_size &&
+ old_size > round_down(i_size - 1, i_blocksize(inode)))
+ clear_inode_flag(inode, FI_AUTO_RECOVER);
+ else if (clean || recover)
set_inode_flag(inode, FI_AUTO_RECOVER);
}
--
2.49.0