[PATCH 10/14] f2fs: handle partial truncate of large folio dirty subpages

From: Nanzhe Zhao

Date: Wed Aug 26 2026 - 04:28:40 EST


A large folio can be partial truncated and stays in folio mapping, we
need to clear the subrange dirty bits and uptodate bits that the partial
truncate covers. If this partial truncate happens to clear the last
subrange dirty bits, then cancel the whole folio dirty state.

Also add a guard in f2fs_write_single_data_folio() so a large folio
subpage whose disk block was already truncated (NULL_ADDR) is skipped

Signed-off-by: Nanzhe Zhao <zhaonanzhe@xxxxxxxxxx>
---
fs/f2fs/data.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 73 insertions(+), 1 deletion(-)

diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c
index 59b07b83b216..15774e8e8795 100644
--- a/fs/f2fs/data.c
+++ b/fs/f2fs/data.c
@@ -2693,6 +2693,34 @@ static void f2fs_ffs_mark_subrange_uptodate(struct folio *folio, size_t offset,
folio_mark_uptodate(folio);
}

+static void ffs_clear_subrange_uptodate(struct folio *folio,
+ size_t offset, size_t len)
+{
+ struct f2fs_folio_state *ffs;
+ unsigned int nr_subpages, start, end;
+ unsigned long flags;
+
+ f2fs_bug_on(F2FS_F_SB(folio), offset + len > folio_size(folio));
+
+ if (!f2fs_folio_has_ffs(folio)) {
+ if (folio_test_uptodate(folio))
+ folio_clear_uptodate(folio);
+ return;
+ }
+
+ ffs = (struct f2fs_folio_state *)folio->private;
+ nr_subpages = folio_nr_pages(folio);
+ start = offset >> PAGE_SHIFT;
+ end = (offset + len + PAGE_SIZE - 1) >> PAGE_SHIFT;
+ end = min(end, nr_subpages);
+
+ spin_lock_irqsave(&ffs->state_lock, flags);
+ bitmap_clear(ffs->state, start, end - start);
+ spin_unlock_irqrestore(&ffs->state_lock, flags);
+ if (folio_test_uptodate(folio))
+ folio_clear_uptodate(folio);
+}
+
bool f2fs_ffs_test_blk_dirty(const struct folio *folio, pgoff_t index)
{
struct f2fs_folio_state *ffs;
@@ -3599,6 +3627,21 @@ static int f2fs_write_single_data_folio(struct folio *folio, int *submitted,

fio.old_blkaddr = dn.data_blkaddr;

+ /* This page is already truncated */
+ if (fio.old_blkaddr == NULL_ADDR) {
+ ffs_clear_subrange_uptodate(folio,
+ i << PAGE_SHIFT, PAGE_SIZE);
+ folio_clear_f2fs_gcing(folio);
+ f2fs_put_dnode(&dn);
+ if (f2fs_folio_has_ffs(folio)) {
+ struct f2fs_folio_state *ffs =
+ (struct f2fs_folio_state *)folio->private;
+
+ atomic_dec(&ffs->write_pages_pending);
+ }
+ continue;
+ }
+
got_it:
if (__is_valid_data_blkaddr(fio.old_blkaddr) &&
!f2fs_is_valid_blkaddr(sbi, fio.old_blkaddr,
@@ -5075,8 +5118,37 @@ void f2fs_invalidate_folio(struct folio *folio, size_t offset, size_t length)
struct f2fs_sb_info *sbi = F2FS_I_SB(inode);

if (inode->i_ino >= F2FS_ROOT_INO(sbi) &&
- (offset || length != folio_size(folio)))
+ (offset || length != folio_size(folio))) {
+ size_t clear_start = ALIGN(offset, PAGE_SIZE);
+ size_t clear_end = round_down(offset + length, PAGE_SIZE);
+ size_t clear_length = 0;
+
+ /*
+ * If the truncated range falls within a single subpage, no
+ * subpage state needs to be cleared.
+ */
+ if (clear_start < clear_end && f2fs_folio_has_ffs(folio)) {
+ bool dirty;
+
+ clear_length = clear_end - clear_start;
+ dirty = f2fs_ffs_clear_subrange_dirty(folio,
+ clear_start, clear_length);
+ ffs_clear_subrange_uptodate(folio, clear_start,
+ clear_length);
+
+ /*
+ * If the truncated subrange happens to clear the
+ * remaining dirty bitmap of the whole folio, cancel
+ * the folio-level dirty state.
+ */
+ if (!dirty && folio_test_dirty(folio)) {
+ inode_dec_dirty_pages(inode);
+ f2fs_remove_dirty_inode(inode);
+ folio_cancel_dirty(folio);
+ }
+ }
return;
+ }

if (folio_test_dirty(folio)) {
if (inode->i_ino == F2FS_META_INO(sbi)) {
--
2.43.0