[PATCH 6/7] f2fs: fix to serialize truncation of COW blocks w/ GC

From: Chao Yu

Date: Thu Oct 22 2026 - 08:57:12 EST


From: Chao Yu <chao@xxxxxxxxxx>

GC migrates blocks of COW inode w/ the COW inode and its i_gc_rwsem,
however f2fs_ioc_start_atomic_write() truncates blocks of the reused
COW inode w/o holding COW inode's i_gc_rwsem, it only holds the
original inode's locks, so the truncation can race w/ GC.

Case #1: GC caches stale data of a freed block in meta cache

- GC (phase 3, cow inode C) - ioctl(F2FS_IOC_START_ATOMIC_WRITE)
- f2fs_down_write_trylock(C's i_gc_rwsem[WRITE])
- ra_data_block(C, j)
- f2fs_grab_cache_folio(orig mapping, j)
- f2fs_get_dnode_of_data(C, j)
: get P
- f2fs_put_dnode()
- f2fs_is_valid_blkaddr(P)
- f2fs_folio_wait_writeback()
- f2fs_wait_on_block_writeback()
- f2fs_ioc_start_atomic_write()
- f2fs_down_write(orig's i_gc_rwsem)
- f2fs_do_truncate_blocks(C, 0, true)
- f2fs_truncate_data_blocks_range()
- f2fs_set_data_blkaddr(NULL_ADDR)
- f2fs_invalidate_blocks(P)
- f2fs_invalidate_internal_cache(P)
: nothing is cached
- f2fs_grab_cache(META, P)
: cache P in META[P]
- f2fs_submit_cache_read(P)
: SIT of P is still valid
- update_sit_entry(P, -1)
: P was freed
- end_io: f2fs_cache_set_uptodate(META[P])
: META[P] is stale for a freed block

On superblock w/o encrypt, verity and compression features, checkpoint
does not truncate meta cache, so META[P] survives after checkpoint, then:

- checkpoint, P can be reused
- writeback of victim's atomic write
- do_write_block()
: allocate P, write victim's data to P, META[P] is not invalidated
- GC phase 3: ra_data_block(victim's cow inode, k)
: META[P] is uptodate, skip reading
- GC phase 4: move_data_block(victim's cow inode, k)
: META[P] is uptodate, skip reading
- memcpy(tentry, sentry)
- f2fs_update_data_blkaddr(newaddr)
: victim's block was overwritten w/ stale data

Case #2: GC reports a false inconsistency error

- GC (phase 3, cow inode C) - ioctl(F2FS_IOC_START_ATOMIC_WRITE)
- ra_data_block(C, j)
- f2fs_get_dnode_of_data(C, j)
: get P
- f2fs_put_dnode()
- f2fs_do_truncate_blocks(C, 0, true)
- f2fs_invalidate_blocks(P)
- update_sit_entry(P, -1)
- f2fs_is_valid_blkaddr(P, DATA_GENERIC_ENHANCE)
: or in f2fs_submit_cache_read()
- __is_bitmap_valid()
- f2fs_err("Inconsistent error blkaddr:...")
- set_sbi_flag(SBI_NEED_FSCK)
- dump_stack()
- f2fs_handle_error(ERROR_INVALID_BLKADDR)

All other truncation paths hold i_gc_rwsem[WRITE] of the truncated
inode, let's hold COW inode's i_gc_rwsem[WRITE] during truncation of
COW blocks as well, then GC will skip COW blocks due to failure of
f2fs_down_write_trylock().

Cc: stable@xxxxxxxxxx
Fixes: f18d00769336 ("f2fs: use meta inode for GC of COW file")
Signed-off-by: Chao Yu <chao@xxxxxxxxxx>
---
fs/f2fs/file.c | 9 +++++++++
1 file changed, 9 insertions(+)

diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c
index 363039da3ab2..3a7093a7c12e 100644
--- a/fs/f2fs/file.c
+++ b/fs/f2fs/file.c
@@ -2445,11 +2445,20 @@ static int f2fs_ioc_start_atomic_write(struct file *filp, bool truncate)
F2FS_I(fi->cow_inode)->atomic_inode = inode;
} else {
/* Reuse the already created COW inode */
+ struct f2fs_inode_info *cfi = F2FS_I(fi->cow_inode);
+
f2fs_bug_on(sbi, get_dirty_pages(fi->cow_inode));

invalidate_mapping_pages(fi->cow_inode->i_mapping, 0, -1);

+ /*
+ * GC migrates COW blocks w/ cow inode and its i_gc_rwsem, so
+ * hold it to serialize truncation of COW blocks w/ GC.
+ */
+ f2fs_down_write_nested(&cfi->i_gc_rwsem[WRITE],
+ SINGLE_DEPTH_NESTING);
ret = f2fs_do_truncate_blocks(fi->cow_inode, 0, true);
+ f2fs_up_write(&cfi->i_gc_rwsem[WRITE]);
if (ret)
goto out_unlock;
}
--
2.49.0