[PATCH 4/7] f2fs: fix to truncate stale meta cache entry on in-place update

From: Chao Yu

Date: Thu Oct 22 2026 - 08:58:29 EST


From: Chao Yu <chao@xxxxxxxxxx>

GC migrates data blocks of the inode which requires meta inode GC via
meta cache, and it trusts an uptodate entry w/o reading the block from
disk again, in both ra_data_block() and move_data_block().

However, in-place update paths only truncate the entry if the inode
requires meta inode GC at the time of writing:
- f2fs_inplace_write_data() checks fio->meta_gc
- f2fs_wait_on_block_writeback_range() for DIO checks
f2fs_meta_inode_gc_required()

f2fs_meta_inode_gc_required() returns true for atomic and COW files,
which is a temporary state, so an entry cached during atomic write, or
cached before the block was donated by F2FS_IOC_MOVE_RANGE, can survive
in-place update once the owner does not require meta inode GC, then GC
will migrate stale data from the entry once the owner requires meta
inode GC again, resulting in overwriting valid data.

Case #1: atomic write

- App (file F) - GC
- ioctl(F2FS_IOC_START_ATOMIC_WRITE)
: f2fs_meta_inode_gc_required(F) is true
- write F[k], k != j
- gc_data_segment() phase 3
- ra_data_block(F, j)
: cache F[j] in META[P]
- ioctl(F2FS_IOC_COMMIT_ATOMIC_WRITE)
- f2fs_commit_atomic_write()
- f2fs_down_write(i_gc_rwsem[WRITE])
- gc_data_segment() phase 4
- f2fs_down_write_trylock(i_gc_rwsem[WRITE])
: fail, skip F[j]
- __f2fs_commit_atomic_write()
: only F[k] is replaced, F[j] is still mapped to P
- f2fs_up_write(i_gc_rwsem[WRITE])
- f2fs_abort_atomic_write()
- clear_inode_flag(FI_ATOMIC_FILE)
- write F[j] w/ new data, fdatasync()
- f2fs_inplace_write_data()
: fio->meta_gc is false, META[P] becomes stale
- ioctl(F2FS_IOC_START_ATOMIC_WRITE)
- gc_data_segment() phase 3
- ra_data_block(F, j)
: META[P] is uptodate, skip reading
- gc_data_segment() phase 4
- move_data_block(F, j)
: META[P] is uptodate, skip reading
- memcpy(tentry, sentry)
- f2fs_update_data_blkaddr(newaddr)
: F[j] was rolled back to old data

Case #2: F2FS_IOC_MOVE_RANGE

- Thread A (src) - GC - Thread B (dst)
- write src[i], P is not checkpointed
- ioctl(F2FS_IOC_START_ATOMIC_WRITE)
- gc_data_segment() phase 3
- ra_data_block(src, i)
: cache src[i] in META[P]
- gc_data_segment() phase 4
: fail to lock i_gc_rwsem, skip src[i]
- ioctl(F2FS_IOC_ABORT_ATOMIC_WRITE)
- f2fs_abort_atomic_write()
- clear_inode_flag(FI_ATOMIC_FILE)
- ioctl(F2FS_IOC_MOVE_RANGE, src[i] -> dst[j])
- f2fs_move_file_range()
- __exchange_data_block()
- __read_out_blkaddrs()
: P is not checkpointed, do not invalidate it
- __clone_blkaddrs()
- f2fs_replace_block(dst[j], P)
- write dst[j] w/ new data, fdatasync()
- f2fs_inplace_write_data()
: fio->meta_gc is false, META[P] becomes stale
- ioctl(F2FS_IOC_START_ATOMIC_WRITE)
- gc_data_segment()
- ra_data_block(dst, j)
- move_data_block(dst, j)
: migrate stale data in META[P]
: dst[j] was overwritten w/ src[i]'s old data

DIO overwrite has the same issue, since f2fs_map_blocks() w/
F2FS_GET_BLOCK_DIO calls f2fs_wait_on_block_writeback_range() for the
block which will be overwritten in place.

Before commit 570e2ccc7cb3 ("f2fs: avoid reading already updated pages
during GC"), ra_data_block() always read the block from disk, then the
stale entry would be updated before move_data_block() used it.

Let's always truncate the meta cache entry in f2fs_inplace_write_data()
and f2fs_wait_on_block_writeback_range() regardless of current state of
the inode, and only wait for writeback of the cache entry when the inode
requires meta inode GC.

Cc: stable@xxxxxxxxxx
Fixes: b40a2b003709 ("f2fs: use meta inode for GC of atomic file")
Signed-off-by: Chao Yu <chao@xxxxxxxxxx>
---
fs/f2fs/segment.c | 23 ++++++++++++++++-------
1 file changed, 16 insertions(+), 7 deletions(-)

diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c
index daf6d9617d5b..c24910839583 100644
--- a/fs/f2fs/segment.c
+++ b/fs/f2fs/segment.c
@@ -4282,8 +4282,13 @@ int f2fs_inplace_write_data(struct f2fs_io_info *fio)
goto drop_bio;
}

- if (fio->meta_gc)
- f2fs_truncate_meta_caches(sbi, fio->new_blkaddr, 1);
+ /*
+ * GC may have cached this block in meta cache while its owner was in
+ * atomic write or before it was donated by F2FS_IOC_MOVE_RANGE, so
+ * truncate it regardless of fio->meta_gc to avoid that GC migrates
+ * stale data from meta cache later.
+ */
+ f2fs_truncate_meta_caches(sbi, fio->new_blkaddr, 1);

stat_inc_inplace_blocks(fio->sbi);

@@ -4471,12 +4476,16 @@ void f2fs_wait_on_block_writeback_range(struct inode *inode, block_t blkaddr,
struct f2fs_sb_info *sbi = F2FS_I_SB(inode);
block_t i;

- if (!f2fs_meta_inode_gc_required(inode))
- return;
-
- for (i = 0; i < len; i++)
- f2fs_wait_on_block_writeback(inode, blkaddr + i);
+ if (f2fs_meta_inode_gc_required(inode)) {
+ for (i = 0; i < len; i++)
+ f2fs_wait_on_block_writeback(inode, blkaddr + i);
+ }

+ /*
+ * GC may have cached these blocks in meta cache while their owner
+ * required meta inode GC, truncate them regardless of current state
+ * of the inode, since DIO may overwrite them in place.
+ */
f2fs_truncate_meta_caches(sbi, blkaddr, len);
}

--
2.49.0