[PATCH] f2fs: read atomic replacements through the COW inode
From: Hungyu Lin
Date: Wed Nov 04 2026 - 16:09:42 EST
Starting an atomic replacement drops the page cache and resets i_size,
but keeps the original block mappings so the operation can be aborted.
After a sparse write extends the replacement, buffered reads still use
those original mappings for the skipped range and cache the old data.
Commit removes the old gap mappings, but the cached data remains visible
even after reopening the file. A subsequent partial write to the gap can
write those stale bytes back to disk.
Resolve buffered reads through the COW inode during atomic replacement,
so unwritten ranges are read as holes. Keep using the original inode for
the read bio and its encryption context. Once FI_ATOMIC_COMMITTED is set,
use the newly committed original mappings again.
Hold the mapping invalidate lock while committing or aborting the atomic
write to serialize these mapping transitions with buffered-read faults
and readahead.
Fixes: 41e8f85a75fc ("f2fs: introduce F2FS_IOC_START_ATOMIC_REPLACE")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Hungyu Lin <dennylin0707@xxxxxxxxx>
---
fs/f2fs/data.c | 8 +++++++-
fs/f2fs/segment.c | 6 ++++++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c
index 28fd8afff5d..89c4de3640d 100644
--- a/fs/f2fs/data.c
+++ b/fs/f2fs/data.c
@@ -2489,7 +2489,13 @@ static int f2fs_read_single_page(struct inode *inode, struct fsverity_info *vi,
map->m_lblk = block_in_file;
map->m_len = last_block - block_in_file;
- ret = f2fs_map_blocks(inode, map, F2FS_GET_BLOCK_DEFAULT);
+ /* Original blocks are kept for rollback, not for replacement reads. */
+ if (is_inode_flag_set(inode, FI_ATOMIC_REPLACE) &&
+ !is_inode_flag_set(inode, FI_ATOMIC_COMMITTED))
+ ret = f2fs_map_blocks(F2FS_I(inode)->cow_inode, map,
+ F2FS_GET_BLOCK_DEFAULT);
+ else
+ ret = f2fs_map_blocks(inode, map, F2FS_GET_BLOCK_DEFAULT);
if (ret)
goto out;
got_it:
diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c
index 43f40071576..f45b5ad78fc 100644
--- a/fs/f2fs/segment.c
+++ b/fs/f2fs/segment.c
@@ -193,6 +193,8 @@ void f2fs_abort_atomic_write(struct inode *inode, bool clean)
if (!f2fs_is_atomic_file(inode))
return;
+ filemap_invalidate_lock(inode->i_mapping);
+
if (clean)
truncate_inode_pages(inode->i_mapping, 0);
@@ -218,6 +220,8 @@ void f2fs_abort_atomic_write(struct inode *inode, bool clean)
f2fs_i_size_write(inode, fi->original_i_size);
fi->original_i_size = 0;
}
+ filemap_invalidate_unlock(inode->i_mapping);
+
/* avoid stale dirty inode during eviction */
sync_inode_metadata(inode, 0);
}
@@ -409,11 +413,13 @@ int f2fs_commit_atomic_write(struct inode *inode)
return err;
f2fs_down_write(&fi->i_gc_rwsem[WRITE]);
+ filemap_invalidate_lock(inode->i_mapping);
f2fs_lock_op(sbi, &lc);
err = __f2fs_commit_atomic_write(inode);
f2fs_unlock_op(sbi, &lc);
+ filemap_invalidate_unlock(inode->i_mapping);
f2fs_up_write(&fi->i_gc_rwsem[WRITE]);
return err;
base-commit: 05441d697f7f9a927264f4058cbbc6a03dfcac9d
--
2.50.1 (Apple Git-155)