From e6c8140bd06d7dd8ee1e3c690445d3cfcaf1d892 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Fri, 17 Apr 2026 11:51:26 +0800 Subject: [PATCH 01/42] f2fs: map data writes to FDP streams From: Wenjie Qi F2FS already classifies DATA writes using its existing hot, warm and cold temperature policy, but it only passes that intent down as a write hint. That hint alone is not sufficient for NVMe FDP placement, because the current NVMe command path consumes `bio->bi_write_stream` rather than `bio->bi_write_hint` when selecting a placement ID. When the target block device exposes write streams, map the existing F2FS DATA temperature classes onto stream IDs and set `bio->bi_write_stream` for both buffered and direct writes. If the device exposes no write streams, keep the current behavior by leaving the stream unset. The stream mapping is evaluated against the target block device of each bio, so the existing per-device fallback behavior stays unchanged for multi-device filesystems. Existing blkzoned restrictions also remain in place. The mapping is intentionally small and deterministic: - 1 stream: hot, warm and cold all use stream 1 - 2 streams: hot/warm use 1, cold uses 2 - 3+ streams: hot uses 1, warm uses 2, cold uses 3 Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- Documentation/filesystems/f2fs.rst | 9 +++++++++ fs/f2fs/data.c | 2 ++ fs/f2fs/f2fs.h | 2 ++ fs/f2fs/file.c | 2 ++ fs/f2fs/segment.c | 13 +++++++++++++ 5 files changed, 28 insertions(+) diff --git a/Documentation/filesystems/f2fs.rst b/Documentation/filesystems/f2fs.rst index 7e4031631286..8c4a14ae444f 100644 --- a/Documentation/filesystems/f2fs.rst +++ b/Documentation/filesystems/f2fs.rst @@ -137,6 +137,15 @@ noacl Disable POSIX Access Control List. Note: acl is enabled active_logs=%u Support configuring the number of active logs. In the current design, f2fs supports only 2, 4, and 6 logs. Default number is 6. + When the underlying block device exposes write + streams, the default active_logs=6 configuration + maps hot, warm, and cold DATA writes to streams 1, + 2, and 3, respectively. If only one or two write + streams are available, f2fs falls back to mapping + all DATA writes to stream 1 or mapping hot/warm + to stream 1 and cold to stream 2. If no write + streams are exposed, f2fs leaves the stream + unset. disable_ext_identify Disable the extension list configured by mkfs, so f2fs is not aware of cold files such as media files. inline_xattr Enable the inline xattrs feature. diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index 8d4f1e75dee3..0f92a8805635 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -509,6 +509,8 @@ static struct bio *__bio_alloc(struct f2fs_io_info *fio, int npages) bio->bi_private = sbi; bio->bi_write_hint = f2fs_io_type_to_rw_hint(sbi, fio->type, fio->temp); + bio->bi_write_stream = f2fs_io_type_to_write_stream(bdev, fio->type, + fio->temp); } iostat_alloc_and_bind_ctx(sbi, bio, NULL); diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 91f506e7c9cf..dc8f3b55b560 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -4061,6 +4061,8 @@ void f2fs_destroy_segment_manager_caches(void); int f2fs_rw_hint_to_seg_type(struct f2fs_sb_info *sbi, enum rw_hint hint); enum rw_hint f2fs_io_type_to_rw_hint(struct f2fs_sb_info *sbi, enum page_type type, enum temp_type temp); +u8 f2fs_io_type_to_write_stream(struct block_device *bdev, + enum page_type type, enum temp_type temp); unsigned int f2fs_usable_segs_in_sec(struct f2fs_sb_info *sbi); unsigned int f2fs_usable_blks_in_seg(struct f2fs_sb_info *sbi, unsigned int segno); diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index fb12c5c9affd..2d8b383ecf52 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -5076,6 +5076,8 @@ static void f2fs_dio_write_submit_io(const struct iomap_iter *iter, enum temp_type temp = f2fs_get_segment_temp(sbi, type); bio->bi_write_hint = f2fs_io_type_to_rw_hint(sbi, DATA, temp); + bio->bi_write_stream = + f2fs_io_type_to_write_stream(bio->bi_bdev, DATA, temp); blk_crypto_submit_bio(bio); } diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 788f8b050249..9cce4d94ac82 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -3636,6 +3636,19 @@ enum rw_hint f2fs_io_type_to_rw_hint(struct f2fs_sb_info *sbi, } } +u8 f2fs_io_type_to_write_stream(struct block_device *bdev, + enum page_type type, enum temp_type temp) +{ + unsigned short nr = bdev_max_write_streams(bdev); + + if (type != DATA || !nr) + return 0; + if (nr < NR_TEMP_TYPE) + return temp == COLD ? nr : HOT + 1; + + return temp + 1; +} + static int __get_segment_type_2(struct f2fs_io_info *fio) { if (fio->type == DATA) From ccc6436abb847352b8ed2de7a8ab2df45bb06622 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Sat, 28 Mar 2026 08:36:02 +0000 Subject: [PATCH 02/42] f2fs: support to report fserror This patch supports to report fserror, it provides another way to let userspace to monitor filesystem level error. In addition, it exports /sys/fs/f2fs/features/fserror once f2fs kernel module start to support the new feature, then generic/791 of fstests can notice the feature, and verify validation of fserror report. Cc: Darrick J. Wong Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- Documentation/ABI/testing/sysfs-fs-f2fs | 3 ++- fs/f2fs/compress.c | 2 ++ fs/f2fs/data.c | 5 ++++- fs/f2fs/dir.c | 2 ++ fs/f2fs/inline.c | 3 +++ fs/f2fs/inode.c | 5 +++++ fs/f2fs/node.c | 8 ++++++++ fs/f2fs/recovery.c | 2 ++ fs/f2fs/segment.c | 2 ++ fs/f2fs/super.c | 26 +++++++++++++++++++++++++ fs/f2fs/sysfs.c | 2 ++ fs/f2fs/verity.c | 2 ++ fs/f2fs/xattr.c | 6 ++++++ 13 files changed, 66 insertions(+), 2 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-fs-f2fs b/Documentation/ABI/testing/sysfs-fs-f2fs index 423ec40e2e4e..27d5e88facbe 100644 --- a/Documentation/ABI/testing/sysfs-fs-f2fs +++ b/Documentation/ABI/testing/sysfs-fs-f2fs @@ -270,7 +270,8 @@ Description: Shows all enabled kernel features. inode_checksum, flexible_inline_xattr, quota_ino, inode_crtime, lost_found, verity, sb_checksum, casefold, readonly, compression, test_dummy_encryption_v2, - atomic_write, pin_file, encrypted_casefold, linear_lookup. + atomic_write, pin_file, encrypted_casefold, linear_lookup, + fserror. What: /sys/fs/f2fs//inject_rate Date: May 2016 diff --git a/fs/f2fs/compress.c b/fs/f2fs/compress.c index 881e76158b96..caf522d667d6 100644 --- a/fs/f2fs/compress.c +++ b/fs/f2fs/compress.c @@ -14,6 +14,7 @@ #include #include #include +#include #include "f2fs.h" #include "node.h" @@ -760,6 +761,7 @@ void f2fs_decompress_cluster(struct decompress_io_ctx *dic, bool in_task) /* Avoid f2fs_commit_super in irq context */ f2fs_handle_error(sbi, ERROR_FAIL_DECOMPRESSION); + fserror_report_file_metadata(dic->inode, ret, GFP_NOFS); goto out_release; } diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index 0f92a8805635..b7b8a72d6486 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -20,6 +20,7 @@ #include #include #include +#include #include "f2fs.h" #include "node.h" @@ -377,9 +378,10 @@ static void f2fs_write_end_io(struct bio *bio) if (unlikely(bio->bi_status != BLK_STS_OK)) { mapping_set_error(folio->mapping, -EIO); - if (type == F2FS_WB_CP_DATA) + if (type == F2FS_WB_CP_DATA) { f2fs_stop_checkpoint(sbi, true, STOP_CP_REASON_WRITE_FAIL); + } } if (is_node_folio(folio)) { @@ -1750,6 +1752,7 @@ int f2fs_map_blocks(struct inode *inode, struct f2fs_map_blocks *map, int flag) err = -EFSCORRUPTED; f2fs_handle_error(sbi, ERROR_CORRUPTED_CLUSTER); + fserror_report_file_metadata(inode, err, GFP_NOFS); goto sync_out; } diff --git a/fs/f2fs/dir.c b/fs/f2fs/dir.c index 38802ee2e40d..b1697194c3c4 100644 --- a/fs/f2fs/dir.c +++ b/fs/f2fs/dir.c @@ -11,6 +11,7 @@ #include #include #include +#include #include "f2fs.h" #include "node.h" #include "acl.h" @@ -1020,6 +1021,7 @@ int f2fs_fill_dentries(struct dir_context *ctx, struct f2fs_dentry_ptr *d, set_sbi_flag(sbi, SBI_NEED_FSCK); err = -EFSCORRUPTED; f2fs_handle_error(sbi, ERROR_CORRUPTED_DIRENT); + fserror_report_file_metadata(d->inode, err, GFP_NOFS); goto out; } diff --git a/fs/f2fs/inline.c b/fs/f2fs/inline.c index 7aabfc9b43cb..099f72089701 100644 --- a/fs/f2fs/inline.c +++ b/fs/f2fs/inline.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "f2fs.h" #include "node.h" @@ -179,6 +180,7 @@ int f2fs_convert_inline_folio(struct dnode_of_data *dn, struct folio *folio) f2fs_warn(fio.sbi, "%s: corrupted inline inode ino=%llu, i_addr[0]:0x%x, run fsck to fix.", __func__, dn->inode->i_ino, dn->data_blkaddr); f2fs_handle_error(fio.sbi, ERROR_INVALID_BLKADDR); + fserror_report_file_metadata(dn->inode, -EFSCORRUPTED, GFP_NOFS); return -EFSCORRUPTED; } @@ -435,6 +437,7 @@ static int f2fs_move_inline_dirents(struct inode *dir, struct folio *ifolio, __func__, dir->i_ino, dn.data_blkaddr); f2fs_handle_error(F2FS_F_SB(folio), ERROR_INVALID_BLKADDR); err = -EFSCORRUPTED; + fserror_report_file_metadata(dn.inode, err, GFP_NOFS); goto out; } diff --git a/fs/f2fs/inode.c b/fs/f2fs/inode.c index c6dcda447882..1694726122e6 100644 --- a/fs/f2fs/inode.c +++ b/fs/f2fs/inode.c @@ -11,6 +11,7 @@ #include #include #include +#include #include "f2fs.h" #include "node.h" @@ -480,6 +481,7 @@ static int do_read_inode(struct inode *inode) f2fs_folio_put(node_folio, true); set_sbi_flag(sbi, SBI_NEED_FSCK); f2fs_handle_error(sbi, ERROR_CORRUPTED_INODE); + fserror_report_file_metadata(inode, -EFSCORRUPTED, GFP_NOFS); return -EFSCORRUPTED; } @@ -541,6 +543,7 @@ static int do_read_inode(struct inode *inode) if (!sanity_check_extent_cache(inode, node_folio)) { f2fs_folio_put(node_folio, true); f2fs_handle_error(sbi, ERROR_CORRUPTED_INODE); + fserror_report_file_metadata(inode, -EFSCORRUPTED, GFP_NOFS); return -EFSCORRUPTED; } @@ -583,6 +586,7 @@ struct inode *f2fs_iget(struct super_block *sb, unsigned long ino) trace_f2fs_iget_exit(inode, ret); iput(inode); f2fs_handle_error(sbi, ERROR_CORRUPTED_INODE); + fserror_report_file_metadata(inode, ret, GFP_NOFS); return ERR_PTR(ret); } @@ -787,6 +791,7 @@ void f2fs_update_inode_page(struct inode *inode) if (err == -ENOMEM || ++count <= DEFAULT_RETRY_IO_COUNT) goto retry; stop_checkpoint: + fserror_report_file_metadata(inode, -EFSCORRUPTED, GFP_NOFS); f2fs_stop_checkpoint(sbi, false, STOP_CP_REASON_UPDATE_INODE); return; } diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c index 4e5bd9e4cfc3..b1247de25411 100644 --- a/fs/f2fs/node.c +++ b/fs/f2fs/node.c @@ -12,6 +12,7 @@ #include #include #include +#include #include "f2fs.h" #include "node.h" @@ -1265,6 +1266,8 @@ int f2fs_truncate_inode_blocks(struct inode *inode, pgoff_t from) if (err == -ENOENT) { set_sbi_flag(F2FS_F_SB(folio), SBI_NEED_FSCK); f2fs_handle_error(sbi, ERROR_INVALID_BLKADDR); + fserror_report_file_metadata(dn.inode, -EFSCORRUPTED, + GFP_NOFS); f2fs_err_ratelimited(sbi, "truncate node fail, ino:%llu, nid:%u, " "offset[0]:%d, offset[1]:%d, nofs:%d", @@ -1556,6 +1559,8 @@ int f2fs_sanity_check_node_footer(struct f2fs_sb_info *sbi, next_blkaddr_of_node(folio)); f2fs_handle_error(sbi, ERROR_INCONSISTENT_FOOTER); + fserror_report_file_metadata(folio->mapping->host, + -EFSCORRUPTED, in_irq ? GFP_NOWAIT : GFP_NOFS); return -EFSCORRUPTED; } @@ -1778,6 +1783,7 @@ static bool __write_node_folio(struct folio *folio, bool atomic, bool do_fsync, if (f2fs_sanity_check_node_footer(sbi, folio, nid, NODE_TYPE_REGULAR, false)) { + fserror_report_metadata(sbi->sb, -EFSCORRUPTED, GFP_NOFS); f2fs_stop_checkpoint(sbi, false, STOP_CP_REASON_CORRUPTED_NID); goto redirty_out; } @@ -2703,6 +2709,8 @@ bool f2fs_alloc_nid(struct f2fs_sb_info *sbi, nid_t *nid) spin_unlock(&nm_i->nid_list_lock); f2fs_err(sbi, "Corrupted nid %u in free_nid_list", i->nid); + fserror_report_metadata(sbi->sb, -EFSCORRUPTED, + GFP_NOFS); f2fs_stop_checkpoint(sbi, false, STOP_CP_REASON_CORRUPTED_NID); return false; diff --git a/fs/f2fs/recovery.c b/fs/f2fs/recovery.c index 3d3dacec9482..89af8407b667 100644 --- a/fs/f2fs/recovery.c +++ b/fs/f2fs/recovery.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "f2fs.h" #include "node.h" #include "segment.h" @@ -679,6 +680,7 @@ static int do_recover_data(struct f2fs_sb_info *sbi, struct inode *inode, ofs_of_node(folio)); err = -EFSCORRUPTED; f2fs_handle_error(sbi, ERROR_INCONSISTENT_FOOTER); + fserror_report_file_metadata(dn.inode, err, GFP_NOFS); goto err; } diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 9cce4d94ac82..008432d674dc 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -17,6 +17,7 @@ #include #include #include +#include #include "f2fs.h" #include "segment.h" @@ -2896,6 +2897,7 @@ static int get_new_segment(struct f2fs_sb_info *sbi, /* set it as dirty segment in free segmap */ if (test_bit(segno, free_i->free_segmap)) { ret = -EFSCORRUPTED; + fserror_report_metadata(sbi->sb, -EFSCORRUPTED, GFP_NOFS); f2fs_stop_checkpoint(sbi, false, STOP_CP_REASON_CORRUPTED_FREE_BITMAP); goto out_unlock; } diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index ccf806b676f5..aab4345f3ee7 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -29,6 +29,7 @@ #include #include #include +#include #include "f2fs.h" #include "node.h" @@ -4635,6 +4636,8 @@ static void f2fs_record_stop_reason(struct f2fs_sb_info *sbi) f2fs_err_ratelimited(sbi, "f2fs_commit_super fails to record stop_reason, err:%d", err); + + fserror_report_shutdown(sbi->sb, GFP_NOFS); } void f2fs_save_errors(struct f2fs_sb_info *sbi, unsigned char flag) @@ -4649,6 +4652,27 @@ void f2fs_save_errors(struct f2fs_sb_info *sbi, unsigned char flag) spin_unlock_irqrestore(&sbi->error_lock, flags); } +static void f2fs_report_fserror(struct f2fs_sb_info *sbi, unsigned char error) +{ + switch (error) { + case ERROR_INVALID_BLKADDR: + case ERROR_CORRUPTED_INODE: + case ERROR_INCONSISTENT_SUMMARY: + case ERROR_INCONSISTENT_SUM_TYPE: + case ERROR_CORRUPTED_JOURNAL: + case ERROR_INCONSISTENT_NODE_COUNT: + case ERROR_INCONSISTENT_BLOCK_COUNT: + case ERROR_INVALID_CURSEG: + case ERROR_INCONSISTENT_SIT: + case ERROR_INVALID_NODE_REFERENCE: + case ERROR_INCONSISTENT_NAT: + fserror_report_metadata(sbi->sb, -EFSCORRUPTED, GFP_NOFS); + break; + default: + return; + } +} + void f2fs_handle_error(struct f2fs_sb_info *sbi, unsigned char error) { f2fs_save_errors(sbi, error); @@ -4658,6 +4682,8 @@ void f2fs_handle_error(struct f2fs_sb_info *sbi, unsigned char error) if (!test_bit(error, (unsigned long *)sbi->errors)) return; schedule_work(&sbi->s_error_work); + + f2fs_report_fserror(sbi, error); } static bool system_going_down(void) diff --git a/fs/f2fs/sysfs.c b/fs/f2fs/sysfs.c index 352e96ad5c3a..665687244c93 100644 --- a/fs/f2fs/sysfs.c +++ b/fs/f2fs/sysfs.c @@ -1399,6 +1399,7 @@ F2FS_FEATURE_RO_ATTR(pin_file); F2FS_FEATURE_RO_ATTR(linear_lookup); #endif F2FS_FEATURE_RO_ATTR(packed_ssa); +F2FS_FEATURE_RO_ATTR(fserror); #define ATTR_LIST(name) (&f2fs_attr_##name.attr) static struct attribute *f2fs_attrs[] = { @@ -1566,6 +1567,7 @@ static struct attribute *f2fs_feat_attrs[] = { BASE_ATTR_LIST(linear_lookup), #endif BASE_ATTR_LIST(packed_ssa), + BASE_ATTR_LIST(fserror), NULL, }; ATTRIBUTE_GROUPS(f2fs_feat); diff --git a/fs/f2fs/verity.c b/fs/f2fs/verity.c index 92ebcc19cab0..39f482515445 100644 --- a/fs/f2fs/verity.c +++ b/fs/f2fs/verity.c @@ -25,6 +25,7 @@ */ #include +#include #include "f2fs.h" #include "xattr.h" @@ -243,6 +244,7 @@ static int f2fs_get_verity_descriptor(struct inode *inode, void *buf, f2fs_warn(F2FS_I_SB(inode), "invalid verity xattr"); f2fs_handle_error(F2FS_I_SB(inode), ERROR_CORRUPTED_VERITY_XATTR); + fserror_report_file_metadata(inode, -EFSCORRUPTED, GFP_NOFS); return -EFSCORRUPTED; } if (buf_size) { diff --git a/fs/f2fs/xattr.c b/fs/f2fs/xattr.c index 610d5810074d..24cef7e1f56a 100644 --- a/fs/f2fs/xattr.c +++ b/fs/f2fs/xattr.c @@ -19,6 +19,7 @@ #include #include #include +#include #include "f2fs.h" #include "xattr.h" #include "segment.h" @@ -371,6 +372,7 @@ static int lookup_all_xattrs(struct inode *inode, struct folio *ifolio, err = -ENODATA; f2fs_handle_error(F2FS_I_SB(inode), ERROR_CORRUPTED_XATTR); + fserror_report_file_metadata(inode, err, GFP_NOFS); goto out; } check: @@ -590,6 +592,8 @@ ssize_t f2fs_listxattr(struct dentry *dentry, char *buffer, size_t buffer_size) set_sbi_flag(F2FS_I_SB(inode), SBI_NEED_FSCK); f2fs_handle_error(F2FS_I_SB(inode), ERROR_CORRUPTED_XATTR); + fserror_report_file_metadata(inode, + -EFSCORRUPTED, GFP_NOFS); break; } @@ -677,6 +681,7 @@ static int __f2fs_setxattr(struct inode *inode, int index, error = -EFSCORRUPTED; f2fs_handle_error(F2FS_I_SB(inode), ERROR_CORRUPTED_XATTR); + fserror_report_file_metadata(inode, error, GFP_NOFS); goto exit; } @@ -705,6 +710,7 @@ static int __f2fs_setxattr(struct inode *inode, int index, error = -EFSCORRUPTED; f2fs_handle_error(F2FS_I_SB(inode), ERROR_CORRUPTED_XATTR); + fserror_report_file_metadata(inode, error, GFP_NOFS); goto exit; } last = XATTR_NEXT_ENTRY(last); From 1f70ddb28a3c71df124da5fa4040c808116d6bb9 Mon Sep 17 00:00:00 2001 From: Yongpeng Yang Date: Mon, 27 Apr 2026 21:10:51 +0800 Subject: [PATCH 03/42] f2fs: fix incorrect FI_NO_EXTENT handling in __destroy_extent_node() When __destroy_extent_node() sets the inode flag FI_NO_EXTENT, it does not reset the length of the largest extent to 0 and update the inode folio. Since modifications to the extent tree are disallowed afterward, the cached largest extent may become stale. This can trigger the following error in xfstests generic/388: F2FS-fs (dm-0): sanity_check_extent_cache: inode (ino=1761) extent info [220057, 57, 6] is incorrect, run fsck to fix In the f2fs_drop_inode path, __destroy_extent_node() does not need to guarantee that et->node_cnt is 0, because concurrency with writeback is expected in this path, and writeback may update the extent cache. This patch reverts commit ed78aeebef05 ("f2fs: fix node_cnt race between extent node destroy and writeback"), and remove the unnecessary zero check of et->node_cnt. Fixes: ed78aeebef05 ("f2fs: fix node_cnt race between extent node destroy and writeback") Cc: stable@vger.kernel.org Reported-by: Chao Yu Suggested-by: Chao Yu Signed-off-by: Yongpeng Yang Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/extent_cache.c | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c index d2e006420f04..61f6b9714366 100644 --- a/fs/f2fs/extent_cache.c +++ b/fs/f2fs/extent_cache.c @@ -119,10 +119,9 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type) if (!__init_may_extent_tree(inode, type)) return false; - if (is_inode_flag_set(inode, FI_NO_EXTENT)) - return false; - if (type == EX_READ) { + if (is_inode_flag_set(inode, FI_NO_EXTENT)) + return false; if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) && !f2fs_sb_has_readonly(F2FS_I_SB(inode))) return false; @@ -645,14 +644,10 @@ static unsigned int __destroy_extent_node(struct inode *inode, while (atomic_read(&et->node_cnt)) { write_lock(&et->lock); - if (!is_inode_flag_set(inode, FI_NO_EXTENT)) - set_inode_flag(inode, FI_NO_EXTENT); node_cnt += __free_extent_tree(sbi, et, nr_shrink); write_unlock(&et->lock); } - f2fs_bug_on(sbi, atomic_read(&et->node_cnt)); - return node_cnt; } @@ -691,12 +686,12 @@ static void __update_extent_tree_range(struct inode *inode, write_lock(&et->lock); - if (is_inode_flag_set(inode, FI_NO_EXTENT)) { - write_unlock(&et->lock); - return; - } - if (type == EX_READ) { + if (is_inode_flag_set(inode, FI_NO_EXTENT)) { + write_unlock(&et->lock); + return; + } + prev = et->largest; dei.len = 0; From cac16750e5a8856addb0385b207cff03cd012018 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Wed, 22 Apr 2026 18:48:47 +0800 Subject: [PATCH 04/42] f2fs: doc: fix the wrong description for critical_task_priority The default value should be 120 rather than 100, fix it. Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- Documentation/ABI/testing/sysfs-fs-f2fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/ABI/testing/sysfs-fs-f2fs b/Documentation/ABI/testing/sysfs-fs-f2fs index 27d5e88facbe..1b58c029abd0 100644 --- a/Documentation/ABI/testing/sysfs-fs-f2fs +++ b/Documentation/ABI/testing/sysfs-fs-f2fs @@ -1001,4 +1001,4 @@ Contact: "Chao Yu" Description: It can be used to tune priority of f2fs critical task, e.g. f2fs_ckpt, f2fs_gc threads, limitation as below: - it requires user has CAP_SYS_NICE capability. - - the range is [100, 139], by default the value is 100. + - the range is [100, 139], by default the value is 120. From dd3114870771562036fdcf5abe813956f36d224d Mon Sep 17 00:00:00 2001 From: Ruipeng Qi Date: Sat, 2 May 2026 20:41:57 +0800 Subject: [PATCH 05/42] f2fs: fix potential deadlock in f2fs_balance_fs() When the f2fs filesystem space is nearly exhausted, we encounter deadlock issues as below: INFO: task A:1890 blocked for more than 120 seconds. Tainted: G O 6.12.41-g3fe07ddf05ab #1 "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. task:A state:D stack:0 pid:1890 tgid:1626 ppid:1153 flags:0x00000204 Call trace: __switch_to+0xf4/0x158 __schedule+0x27c/0x908 schedule+0x3c/0x118 io_schedule+0x44/0x68 folio_wait_bit_common+0x174/0x370 folio_wait_bit+0x20/0x38 folio_wait_writeback+0x54/0xc8 truncate_inode_partial_folio+0x70/0x1e0 truncate_inode_pages_range+0x1b0/0x450 truncate_pagecache+0x54/0x88 f2fs_file_write_iter+0x3e8/0xb80 do_iter_readv_writev+0xf0/0x1e0 vfs_writev+0x138/0x2c8 do_writev+0x88/0x130 __arm64_sys_writev+0x28/0x40 invoke_syscall+0x50/0x120 el0_svc_common.constprop.0+0xc8/0xf0 do_el0_svc+0x24/0x38 el0_svc+0x30/0xf8 el0t_64_sync_handler+0x120/0x130 el0t_64_sync+0x190/0x198 INFO: task kworker/u8:11:2680853 blocked for more than 120 seconds. Tainted: G O 6.12.41-g3fe07ddf05ab #1 "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. task:kworker/u8:11 state:D stack:0 pid:2680853 tgid:2680853 ppid:2 flags:0x00000208 Workqueue: writeback wb_workfn (flush-254:0) Call trace: __switch_to+0xf4/0x158 __schedule+0x27c/0x908 schedule+0x3c/0x118 io_schedule+0x44/0x68 folio_wait_bit_common+0x174/0x370 __filemap_get_folio+0x214/0x348 pagecache_get_page+0x20/0x70 f2fs_get_read_data_page+0x150/0x3e8 f2fs_get_lock_data_page+0x2c/0x160 move_data_page+0x50/0x478 do_garbage_collect+0xd38/0x1528 f2fs_gc+0x240/0x7e0 f2fs_balance_fs+0x1a0/0x208 f2fs_write_single_data_page+0x6e4/0x730 f2fs_write_cache_pages+0x378/0x9b0 f2fs_write_data_pages+0x2e4/0x388 do_writepages+0x8c/0x2c8 __writeback_single_inode+0x4c/0x498 writeback_sb_inodes+0x234/0x4a8 __writeback_inodes_wb+0x58/0x118 wb_writeback+0x2f8/0x3c0 wb_workfn+0x2c4/0x508 process_one_work+0x180/0x408 worker_thread+0x258/0x368 kthread+0x118/0x128 ret_from_fork+0x10/0x200 INFO: task kworker/u8:8:2641297 blocked for more than 120 seconds. Tainted: G O 6.12.41-g3fe07ddf05ab #1 "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. task:kworker/u8:8 state:D stack:0 pid:2641297 tgid:2641297 ppid:2 flags:0x00000208 Workqueue: writeback wb_workfn (flush-254:0) Call trace: __switch_to+0xf4/0x158 __schedule+0x27c/0x908 rt_mutex_schedule+0x30/0x60 __rt_mutex_slowlock_locked.constprop.0+0x460/0x8a8 rwbase_write_lock+0x24c/0x378 down_write+0x1c/0x30 f2fs_balance_fs+0x184/0x208 f2fs_write_inode+0xf4/0x328 __writeback_single_inode+0x370/0x498 writeback_sb_inodes+0x234/0x4a8 __writeback_inodes_wb+0x58/0x118 wb_writeback+0x2f8/0x3c0 wb_workfn+0x2c4/0x508 process_one_work+0x180/0x408 worker_thread+0x258/0x368 kthread+0x118/0x128 ret_from_fork+0x10/0x20 INFO: task B:1902 blocked for more than 120 seconds. Tainted: G O 6.12.41-g3fe07ddf05ab #1 "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. task:B state:D stack:0 pid:1902 tgid:1626 ppid:1153 flags:0x0000020c Call trace: __switch_to+0xf4/0x158 __schedule+0x27c/0x908 rt_mutex_schedule+0x30/0x60 __rt_mutex_slowlock_locked.constprop.0+0x460/0x8a8 rwbase_write_lock+0x24c/0x378 down_write+0x1c/0x30 f2fs_balance_fs+0x184/0x208 f2fs_map_blocks+0x94c/0x1110 f2fs_file_write_iter+0x228/0xb80 do_iter_readv_writev+0xf0/0x1e0 vfs_writev+0x138/0x2c8 do_writev+0x88/0x130 __arm64_sys_writev+0x28/0x40 invoke_syscall+0x50/0x120 el0_svc_common.constprop.0+0xc8/0xf0 do_el0_svc+0x24/0x38 el0_svc+0x30/0xf8 el0t_64_sync_handler+0x120/0x130 el0t_64_sync+0x190/0x198 INFO: task sync:2769849 blocked for more than 120 seconds. Tainted: G O 6.12.41-g3fe07ddf05ab #1 "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. task:sync state:D stack:0 pid:2769849 tgid:2769849 ppid:736 flags:0x0000020c Call trace: __switch_to+0xf4/0x158 __schedule+0x27c/0x908 schedule+0x3c/0x118 wb_wait_for_completion+0xb0/0xe8 sync_inodes_sb+0xc8/0x2b0 sync_inodes_one_sb+0x24/0x38 iterate_supers+0xa8/0x138 ksys_sync+0x54/0xc8 __arm64_sys_sync+0x18/0x30 invoke_syscall+0x50/0x120 el0_svc_common.constprop.0+0xc8/0xf0 do_el0_svc+0x24/0x38 el0_svc+0x30/0xf8 el0t_64_sync_handler+0x120/0x130 el0t_64_sync+0x190/0x198 The root cause is a potential deadlock between the following tasks: kworker/u8:11 Thread A - f2fs_write_single_data_page - f2fs_do_write_data_page - folio_start_writeback(X) - f2fs_outplace_write_data - bio_add_folio(X) - folio_unlock(X) - truncate_inode_pages_range - __filemap_get_folio(X, FGP_LOCK) - truncate_inode_partial_folio(X) - folio_wait_writeback(X) - f2fs_balance_fs - f2fs_gc - do_garbage_collect - move_data_page - f2fs_get_lock_data_page - __filemap_get_folio(X, FGP_LOCK) Both threads try to access folio X. Thread A holds the lock but waits for writeback, while kworker waits for the lock. This causes a deadlock. Other threads also enter D state, waiting for locks such as gc_lock and writepages. OPU/IPU DATA folio are all affected by this issue. To avoid such potential deadlocks, always commit these cached folios before triggering f2fs_gc() in f2fs_balance_fs(). Suggested-by: Chao Yu Reviewed-by: Chao Yu Signed-off-by: Ruipeng Qi Signed-off-by: Jaegeuk Kim --- fs/f2fs/data.c | 29 +++++++++++++++++++++++++++++ fs/f2fs/f2fs.h | 1 + fs/f2fs/segment.c | 8 ++++++++ 3 files changed, 38 insertions(+) diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index b7b8a72d6486..5d18119a214a 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -947,6 +947,35 @@ void f2fs_submit_merged_ipu_write(struct f2fs_sb_info *sbi, } } +void f2fs_submit_all_merged_ipu_writes(struct f2fs_sb_info *sbi) +{ + struct bio_entry *be, *tmp; + struct f2fs_bio_info *io; + enum temp_type temp; + + for (temp = HOT; temp < NR_TEMP_TYPE; temp++) { + LIST_HEAD(list); + + io = sbi->write_io[DATA] + temp; + + /* A lockless list_empty() check is safe here: any bios from + * other kworkers that we miss will be submitted by those + * kworkers accordingly. + */ + if (list_empty(&io->bio_list)) + continue; + + f2fs_down_write(&io->bio_list_lock); + list_splice_init(&io->bio_list, &list); + f2fs_up_write(&io->bio_list_lock); + + list_for_each_entry_safe(be, tmp, &list, list) { + f2fs_submit_write_bio(sbi, be->bio, DATA); + del_bio_entry(be); + } + } +} + int f2fs_merge_page_bio(struct f2fs_io_info *fio) { struct bio *bio = *fio->bio; diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index dc8f3b55b560..e555c1d794df 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -4155,6 +4155,7 @@ void f2fs_submit_merged_write_folio(struct f2fs_sb_info *sbi, struct folio *folio, enum page_type type); void f2fs_submit_merged_ipu_write(struct f2fs_sb_info *sbi, struct bio **bio, struct folio *folio); +void f2fs_submit_all_merged_ipu_writes(struct f2fs_sb_info *sbi); void f2fs_flush_merged_writes(struct f2fs_sb_info *sbi); int f2fs_submit_page_bio(struct f2fs_io_info *fio); int f2fs_merge_page_bio(struct f2fs_io_info *fio); diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 008432d674dc..7c8ac62b1b0d 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -463,6 +463,14 @@ void f2fs_balance_fs(struct f2fs_sb_info *sbi, bool need) .should_migrate_blocks = false, .err_gc_skipped = false, .nr_free_secs = 1 }; + + /* + * Submit all cached OPU/IPU DATA bios before triggering + * foreground GC to avoid potential deadlocks. + */ + f2fs_submit_merged_write(sbi, DATA); + f2fs_submit_all_merged_ipu_writes(sbi); + f2fs_down_write_trace(&sbi->gc_lock, &gc_control.lc); stat_inc_gc_call_count(sbi, FOREGROUND); f2fs_gc(sbi, &gc_control); From b952837f734c3a627877bf922408dac04588a643 Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Wed, 6 May 2026 09:07:09 +0800 Subject: [PATCH 06/42] f2fs: annotate lockless last_time[] accesses f2fs stores mount-wide activity timestamps in sbi->last_time[] and samples them from background discard, GC, and balance paths without a dedicated lock. The timestamps are used as best-effort heuristics to decide whether background work should run now or sleep a bit longer. The current helpers use plain loads and stores, so KCSAN can report races between frequent foreground updates and background readers. Exact freshness is not required here, but the intentional lockless accesses should be marked explicitly. Use WRITE_ONCE() in f2fs_update_time() and READ_ONCE() in f2fs_time_over() and f2fs_time_to_wait(). This preserves the existing heuristic behavior and avoids adding locking to hot paths. Signed-off-by: Cen Zhang Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/f2fs.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index e555c1d794df..6d2048cffa66 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -2125,12 +2125,12 @@ static inline void f2fs_update_time(struct f2fs_sb_info *sbi, int type) { unsigned long now = jiffies; - sbi->last_time[type] = now; + WRITE_ONCE(sbi->last_time[type], now); /* DISCARD_TIME and GC_TIME are based on REQ_TIME */ if (type == REQ_TIME) { - sbi->last_time[DISCARD_TIME] = now; - sbi->last_time[GC_TIME] = now; + WRITE_ONCE(sbi->last_time[DISCARD_TIME], now); + WRITE_ONCE(sbi->last_time[GC_TIME], now); } } @@ -2138,7 +2138,7 @@ static inline bool f2fs_time_over(struct f2fs_sb_info *sbi, int type) { unsigned long interval = sbi->interval_time[type] * HZ; - return time_after(jiffies, sbi->last_time[type] + interval); + return time_after(jiffies, READ_ONCE(sbi->last_time[type]) + interval); } static inline unsigned int f2fs_time_to_wait(struct f2fs_sb_info *sbi, @@ -2148,7 +2148,7 @@ static inline unsigned int f2fs_time_to_wait(struct f2fs_sb_info *sbi, unsigned int wait_ms = 0; long delta; - delta = (sbi->last_time[type] + interval) - jiffies; + delta = (READ_ONCE(sbi->last_time[type]) + interval) - jiffies; if (delta > 0) wait_ms = jiffies_to_msecs(delta); From fb645a976f53b175a49bdf52cfcd2c56f4b1456a Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Tue, 5 May 2026 20:55:10 +0800 Subject: [PATCH 07/42] f2fs: annotate lockless NAT counter reads nat_cnt[] is updated while callers hold nat_tree_lock, but F2FS samples the counters locklessly in f2fs_available_free_memory(), excess_dirty_nats(), and excess_cached_nats(). Those helpers only steer cache reclaim and background sync heuristics; they do not control NAT entry lifetime or checkpoint correctness. Document the intent with data_race(READ_ONCE()) and a short comment instead of adding locking to the balance path. Signed-off-by: Cen Zhang Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/node.c | 6 +++++- fs/f2fs/node.h | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c index b1247de25411..a464d5ee1124 100644 --- a/fs/f2fs/node.c +++ b/fs/f2fs/node.c @@ -73,7 +73,11 @@ bool f2fs_available_free_memory(struct f2fs_sb_info *sbi, int type) sizeof(struct free_nid)) >> PAGE_SHIFT; res = mem_size < ((avail_ram * nm_i->ram_thresh / 100) >> 2); } else if (type == NAT_ENTRIES) { - mem_size = (nm_i->nat_cnt[TOTAL_NAT] * + /* + * nat_cnt[] is heuristic accounting. Sample it locklessly here + * to avoid taking nat_tree_lock in the balance path. + */ + mem_size = (data_race(READ_ONCE(nm_i->nat_cnt[TOTAL_NAT])) * sizeof(struct nat_entry)) >> PAGE_SHIFT; res = mem_size < ((avail_ram * nm_i->ram_thresh / 100) >> 2); if (excess_cached_nats(sbi)) diff --git a/fs/f2fs/node.h b/fs/f2fs/node.h index bcf2034e4263..5e114f352099 100644 --- a/fs/f2fs/node.h +++ b/fs/f2fs/node.h @@ -129,13 +129,17 @@ static inline void raw_nat_from_node_info(struct f2fs_nat_entry *raw_ne, static inline bool excess_dirty_nats(struct f2fs_sb_info *sbi) { - return NM_I(sbi)->nat_cnt[DIRTY_NAT] >= NM_I(sbi)->max_nid * + /* nat_cnt[] is heuristic accounting sampled locklessly here. */ + return data_race(READ_ONCE(NM_I(sbi)->nat_cnt[DIRTY_NAT])) >= + NM_I(sbi)->max_nid * NM_I(sbi)->dirty_nats_ratio / 100; } static inline bool excess_cached_nats(struct f2fs_sb_info *sbi) { - return NM_I(sbi)->nat_cnt[TOTAL_NAT] >= DEF_NAT_CACHE_THRESHOLD; + /* nat_cnt[] is heuristic accounting sampled locklessly here. */ + return data_race(READ_ONCE(NM_I(sbi)->nat_cnt[TOTAL_NAT])) >= + DEF_NAT_CACHE_THRESHOLD; } enum mem_type { From f6b24566035835dc22796f24b1e8738581840b17 Mon Sep 17 00:00:00 2001 From: liujinbao1 Date: Wed, 6 May 2026 17:57:31 +0800 Subject: [PATCH 08/42] f2fs: Add trace_f2fs_fault_report Add trace_f2fs_fault_report to trigger reporting upon f2fs_bug_on, need_fsck, stop_checkpoint, and handle_eio. Since f2fs_bug_on and need_fsck can be triggered in hundreds of scenarios, define set_sbi_flag as a macro to help capture the effective fault function and line number. Signed-off-by: shengyong1 Signed-off-by: liujinbao1 Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/f2fs.h | 18 +++++++++++++++++- fs/f2fs/super.c | 9 +++++++++ include/trace/events/f2fs.h | 28 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 6d2048cffa66..b83ff4bd96ec 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -96,6 +96,15 @@ extern const char *f2fs_fault_name[FAULT_MAX]; #define DEFAULT_FAILURE_RETRY_COUNT 1 #endif +enum { + REPORT_FAULT_NEED_FSCK, + REPORT_FAULT_STOP_CP, + REPORT_FAULT_MAX, +}; + +void f2fs_fault_report(struct super_block *sb, unsigned int err_code, + const char *func, unsigned int data); + /* * For mount options */ @@ -2279,11 +2288,18 @@ static inline bool is_sbi_flag_set(struct f2fs_sb_info *sbi, unsigned int type) return test_bit(type, &sbi->s_flag); } -static inline void set_sbi_flag(struct f2fs_sb_info *sbi, unsigned int type) +static inline void __set_sbi_flag(struct f2fs_sb_info *sbi, unsigned int type) { set_bit(type, &sbi->s_flag); } +#define set_sbi_flag(sbi, type) \ +do { \ + __set_sbi_flag(sbi, type); \ + if ((type) == SBI_NEED_FSCK) \ + f2fs_fault_report(sbi->sb, REPORT_FAULT_NEED_FSCK, __func__, __LINE__); \ +} while (0) + static inline void clear_sbi_flag(struct f2fs_sb_info *sbi, unsigned int type) { clear_bit(type, &sbi->s_flag); diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index aab4345f3ee7..f4ab39b24a30 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -4750,9 +4750,18 @@ static void f2fs_handle_critical_error(struct f2fs_sb_info *sbi, */ } +void f2fs_fault_report(struct super_block *sb, unsigned int err_code, + const char *func, unsigned int data) +{ + trace_f2fs_fault_report(sb, err_code, func, data); +} + void f2fs_stop_checkpoint(struct f2fs_sb_info *sbi, bool end_io, unsigned char reason) { + if (reason != STOP_CP_REASON_SHUTDOWN) + f2fs_fault_report(sbi->sb, REPORT_FAULT_STOP_CP, __func__, reason); + f2fs_build_fault_attr(sbi, 0, 0, FAULT_ALL); if (!end_io) f2fs_flush_merged_writes(sbi); diff --git a/include/trace/events/f2fs.h b/include/trace/events/f2fs.h index b5188d2671d7..270c1a2c24c4 100644 --- a/include/trace/events/f2fs.h +++ b/include/trace/events/f2fs.h @@ -2595,6 +2595,34 @@ DEFINE_EVENT(f2fs_priority_update, f2fs_priority_restore, TP_ARGS(sbi, lock_name, is_write, p, orig_prio, new_prio) ); +TRACE_EVENT(f2fs_fault_report, + + TP_PROTO(struct super_block *sb, unsigned int err_code, + const char *func, unsigned int data), + + TP_ARGS(sb, err_code, func, data), + + TP_STRUCT__entry( + __field(dev_t, dev) + __field(unsigned int, err_code) + __string(func, func) + __field(unsigned int, data) + ), + + TP_fast_assign( + __entry->dev = sb->s_dev; + __entry->err_code = err_code; + __assign_str(func); + __entry->data = data; + ), + + TP_printk("dev = (%d,%d), err_code = %u, func = %s, data = %u", + show_dev(__entry->dev), + __entry->err_code, + __get_str(func), + __entry->data) +); + #endif /* _TRACE_F2FS_H */ /* This part must be outside protection */ From 5dfb768326b95c6dd6554f34dec71b44a01a0bc8 Mon Sep 17 00:00:00 2001 From: Daeho Jeong Date: Thu, 14 May 2026 13:55:13 -0700 Subject: [PATCH 09/42] f2fs: optimize representative type determination in GC In large section mode, do_garbage_collect() previously determined the section's representative type by looking only at the first segment of the section. However, if data was fsynced into an area previously used as a node section, and this area is recovered during roll-forward recovery after sudden power off (SPO), GC would incorrectly assume the section's type based on an empty or obsolete first segment. This caused the recovered data segment to be misunderstood as being stuck inside a node section, triggering false inconsistency panics (Inconsistent segment type in SSA and SIT) and subsequent mount failures. This patch optimizes do_garbage_collect() to determine the section's representative type by identifying the first segment that actually contains valid blocks (valid_blocks > 0) during the main GC loop. This eliminates false alarms from empty/obsolete leading segments while maintaining strict section-level type consistency checks for genuine corruption. Signed-off-by: Daeho Jeong Signed-off-by: Jaegeuk Kim --- fs/f2fs/gc.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index ba93010924c0..99bc59889825 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -1754,9 +1754,8 @@ static int do_garbage_collect(struct f2fs_sb_info *sbi, unsigned int end_segno = start_segno + SEGS_PER_SEC(sbi); unsigned int sec_end_segno; int seg_freed = 0, migrated = 0; - unsigned char type = IS_DATASEG(get_seg_entry(sbi, segno)->type) ? - SUM_TYPE_DATA : SUM_TYPE_NODE; - unsigned char data_type = (type == SUM_TYPE_DATA) ? DATA : NODE; + unsigned char type; + unsigned char data_type; int submitted = 0, sum_blk_cnt; if (__is_large_section(sbi)) { @@ -1855,6 +1854,12 @@ static int do_garbage_collect(struct f2fs_sb_info *sbi, migrated >= sbi->migration_granularity) continue; + if (migrated == 0) { + type = IS_DATASEG(get_seg_entry(sbi, cur_segno)->type) ? + SUM_TYPE_DATA : SUM_TYPE_NODE; + data_type = (type == SUM_TYPE_DATA) ? DATA : NODE; + } + sum = SUM_BLK_PAGE_ADDR(sbi, sum_folio, cur_segno); if (type != GET_SUM_TYPE(sum_footer(sbi, sum))) { f2fs_err(sbi, "Inconsistent segment (%u) type " From caac757a3d2dddeb88d79a7524ca0033fc337b3d Mon Sep 17 00:00:00 2001 From: liujinbao1 Date: Wed, 13 May 2026 22:14:36 +0800 Subject: [PATCH 10/42] f2fs: add iostat latency tracking for direct IO F2FS did not collect iostat latency for direct IO reads and writes, hook iomap_dio_ops.submit_io to bind an iostat context and record the submission timestamp. Replace bi_end_io with f2fs_dio_end_bio() to collect IO latency on completion before calling back to the original iomap_dio_bio_end_io(), to add iostat latency tracking support for F2FS DIO. Signed-off-by: shengyong1 Signed-off-by: liujinbao1 Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 2d8b383ecf52..6edf0105dbc8 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -4784,6 +4784,30 @@ static bool f2fs_should_use_dio(struct inode *inode, struct kiocb *iocb, return true; } +#ifdef CONFIG_F2FS_IOSTAT +static void f2fs_dio_end_bio(struct bio *bio) +{ + struct bio_iostat_ctx *iostat_ctx = bio->bi_private; + void *orig_bi_private = iostat_ctx->post_read_ctx; + + iostat_update_and_unbind_ctx(bio); + bio->bi_private = orig_bi_private; + iomap_dio_bio_end_io(bio); +} + +static void f2fs_dio_iostat_start(struct f2fs_sb_info *sbi, struct bio *bio) +{ + void *bi_private = bio->bi_private; + + iostat_alloc_and_bind_ctx(sbi, bio, bi_private); + iostat_update_submit_ctx(bio, DATA); + bio->bi_end_io = f2fs_dio_end_bio; +} +#else +static inline void f2fs_dio_iostat_start(struct f2fs_sb_info *sbi, + struct bio *bio) {} +#endif + static int f2fs_dio_read_end_io(struct kiocb *iocb, ssize_t size, int error, unsigned int flags) { @@ -4796,8 +4820,18 @@ static int f2fs_dio_read_end_io(struct kiocb *iocb, ssize_t size, int error, return 0; } +static void f2fs_dio_read_submit_io(const struct iomap_iter *iter, + struct bio *bio, loff_t file_offset) +{ + struct f2fs_sb_info *sbi = F2FS_I_SB(iter->inode); + + f2fs_dio_iostat_start(sbi, bio); + blk_crypto_submit_bio(bio); +} + static const struct iomap_dio_ops f2fs_iomap_dio_read_ops = { .end_io = f2fs_dio_read_end_io, + .submit_io = f2fs_dio_read_submit_io, }; static ssize_t f2fs_dio_read_iter(struct kiocb *iocb, struct iov_iter *to) @@ -5078,6 +5112,7 @@ static void f2fs_dio_write_submit_io(const struct iomap_iter *iter, bio->bi_write_hint = f2fs_io_type_to_rw_hint(sbi, DATA, temp); bio->bi_write_stream = f2fs_io_type_to_write_stream(bio->bi_bdev, DATA, temp); + f2fs_dio_iostat_start(sbi, bio); blk_crypto_submit_bio(bio); } From 47a60629caca7ea6475deb21d16112b35645b59f Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 11 May 2026 00:30:33 +0800 Subject: [PATCH 11/42] f2fs: add logs in f2fs_disable_checkpoint() In order to troubleshoot in which step we may block on during mount w/ checkpoint_disable mount option. Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/super.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index f4ab39b24a30..629548d78db0 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -2633,6 +2633,9 @@ static int f2fs_disable_checkpoint(struct f2fs_sb_info *sbi) /* check if we need more GC first */ unusable = f2fs_get_unusable_blocks(sbi); + + f2fs_info(sbi, "%s starts, unusable: %u", __func__, unusable); + if (!f2fs_disable_cp_again(sbi, unusable)) goto skip_gc; @@ -2640,6 +2643,8 @@ static int f2fs_disable_checkpoint(struct f2fs_sb_info *sbi) sbi->gc_mode = GC_URGENT_HIGH; + f2fs_info(sbi, "%s: run f2fs_gc() to migrate blocks", __func__); + while (!f2fs_time_over(sbi, DISABLE_TIME)) { struct f2fs_gc_control gc_control = { .victim_segno = NULL_SEGNO, @@ -2660,6 +2665,12 @@ static int f2fs_disable_checkpoint(struct f2fs_sb_info *sbi) break; } + f2fs_info(sbi, "%s: call sync_filesystem() to persist meta: %lld, node: %lld, data: %lld", + __func__, + get_pages(sbi, F2FS_DIRTY_META), + get_pages(sbi, F2FS_DIRTY_NODES), + get_pages(sbi, F2FS_DIRTY_DATA)); + ret = sync_filesystem(sbi->sb); if (ret || err) { err = ret ? ret : err; @@ -2673,6 +2684,12 @@ static int f2fs_disable_checkpoint(struct f2fs_sb_info *sbi) } skip_gc: + f2fs_info(sbi, "%s: call f2fs_write_checkpoint(), meta: %lld, node: %lld, data: %lld", + __func__, + get_pages(sbi, F2FS_DIRTY_META), + get_pages(sbi, F2FS_DIRTY_NODES), + get_pages(sbi, F2FS_DIRTY_DATA)); + f2fs_down_write_trace(&sbi->gc_lock, &lc); cpc.reason = CP_PAUSE; set_sbi_flag(sbi, SBI_CP_DISABLED); @@ -2690,7 +2707,7 @@ static int f2fs_disable_checkpoint(struct f2fs_sb_info *sbi) restore_flag: sbi->gc_mode = gc_mode; sbi->sb->s_flags = s_flags; /* Restore SB_RDONLY status */ - f2fs_info(sbi, "f2fs_disable_checkpoint() finish, err:%d", err); + f2fs_info(sbi, "%s finishes, err:%d", __func__, err); return err; } From 8b4468ec023d0d1b4669dfb867588997cc03a06b Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Tue, 19 May 2026 01:14:38 +0000 Subject: [PATCH 12/42] f2fs: fix potential deadlock in gc_merge path of f2fs_balance_fs() When we mount device w/ gc_merge mount option, we may suffer below potential deadlock: Kworker GC trehad Truncator - f2fs_write_cache_pages - f2fs_write_single_data_page - f2fs_do_write_data_page - folio_start_writeback --- set writeback flag on folio - f2fs_outplace_write_data : cached folio in internal bio cache - f2fs_balance_fs - wake_up(gc_thread) : wake up gc thread to run foreground GC - finish_wait(fggc_wq) : wait on the waitqueue --- wait on GC thread to finish the work - truncate_inode_pages_range - __filemap_get_folio(, FGP_LOCK) --- lock folio - truncate_inode_partial_folio - folio_wait_writeback --- wait on writeback being cleared - do_garbage_collect - move_data_page - f2fs_get_lock_data_folio - lock on folio --- blocked on folio's lock In order to avoid such deadlock, let's call below functions to commit cached bios in GC_MERGE path of f2fs_balance_fs() as the same as we did in NOGC_MERGE path. - f2fs_submit_merged_write(sbi, DATA); - f2fs_submit_all_merged_ipu_writes(sbi); Cc: stable@kernel.org Fixes: 351df4b20115 ("f2fs: add segment operations") Cc: Ruipeng Qi Reported: Sandeep Dhavale Signed-off-by: Chao Yu Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/segment.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 7c8ac62b1b0d..1ef4edb77078 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -445,6 +445,13 @@ void f2fs_balance_fs(struct f2fs_sb_info *sbi, bool need) if (has_enough_free_secs(sbi, 0, 0)) return; + /* + * Submit all cached OPU/IPU DATA bios before triggering + * foreground GC to avoid potential deadlocks. + */ + f2fs_submit_merged_write(sbi, DATA); + f2fs_submit_all_merged_ipu_writes(sbi); + if (test_opt(sbi, GC_MERGE) && sbi->gc_thread && sbi->gc_thread->f2fs_gc_task) { DEFINE_WAIT(wait); @@ -464,13 +471,6 @@ void f2fs_balance_fs(struct f2fs_sb_info *sbi, bool need) .err_gc_skipped = false, .nr_free_secs = 1 }; - /* - * Submit all cached OPU/IPU DATA bios before triggering - * foreground GC to avoid potential deadlocks. - */ - f2fs_submit_merged_write(sbi, DATA); - f2fs_submit_all_merged_ipu_writes(sbi); - f2fs_down_write_trace(&sbi->gc_lock, &gc_control.lc); stat_inc_gc_call_count(sbi, FOREGROUND); f2fs_gc(sbi, &gc_control); From e0288584baa5dc41df4a829a023c4c1b33fe53d7 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Thu, 21 May 2026 10:15:05 +0800 Subject: [PATCH 13/42] f2fs: atomic: fix UAF issue on f2fs_inode_info.atomic_inode - ioctl(F2FS_IOC_GARBAGE_COLLECT_RANGE) - shrink - f2fs_gc - gc_data_segment - ra_data_block(cow_inode) - mapping = F2FS_I(inode)->atomic_inode->i_mapping : f2fs_is_cow_file(cow_inode) is true - f2fs_evict_inode(atomic_inode) - clear_inode_flag(fi->cow_inode, FI_COW_FILE) - F2FS_I(fi->cow_inode)->atomic_inode = NULL ... - truncate_inode_pages_final(atomic_inode) - f2fs_grab_cache_folio(mapping) : create folio in atomic_inode->mapping - clear_inode(atomic_inode) - BUG_ON(atomic_inode->i_data.nrpages) We need to add a reference on fi->atomic_inode before using its mapping field during garbage collection, otherwise, it will cause UAF issue. Cc: stable@kernel.org Cc: Daeho Jeong Cc: Sunmin Jeong Fixes: 3db1de0e582c ("f2fs: change the current atomic write way") Fixes: f18d00769336 ("f2fs: use meta inode for GC of COW file") Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/gc.c | 50 +++++++++++++++++++++++++++++++++++++++++-------- fs/f2fs/inode.c | 11 ++++++++--- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index 99bc59889825..69e0a867219d 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -1220,8 +1220,8 @@ static bool is_alive(struct f2fs_sb_info *sbi, struct f2fs_summary *sum, static int ra_data_block(struct inode *inode, pgoff_t index) { struct f2fs_sb_info *sbi = F2FS_I_SB(inode); - struct address_space *mapping = f2fs_is_cow_file(inode) ? - F2FS_I(inode)->atomic_inode->i_mapping : inode->i_mapping; + struct address_space *mapping = inode->i_mapping; + struct inode *atomic_inode = NULL; struct dnode_of_data dn; struct folio *folio, *efolio; struct f2fs_io_info fio = { @@ -1236,9 +1236,22 @@ static int ra_data_block(struct inode *inode, pgoff_t index) }; int err = 0; + f2fs_down_read(&F2FS_I(inode)->i_sem); + if (f2fs_is_cow_file(inode)) { + atomic_inode = igrab(F2FS_I(inode)->atomic_inode); + if (!atomic_inode) { + f2fs_up_read(&F2FS_I(inode)->i_sem); + return -EBUSY; + } + mapping = atomic_inode->i_mapping; + } + f2fs_up_read(&F2FS_I(inode)->i_sem); + folio = f2fs_grab_cache_folio(mapping, index, true); - if (IS_ERR(folio)) - return PTR_ERR(folio); + if (IS_ERR(folio)) { + err = PTR_ERR(folio); + goto out_iput; + } if (f2fs_lookup_read_extent_cache_block(inode, index, &dn.data_blkaddr)) { @@ -1299,11 +1312,16 @@ static int ra_data_block(struct inode *inode, pgoff_t index) f2fs_update_iostat(sbi, inode, FS_DATA_READ_IO, F2FS_BLKSIZE); f2fs_update_iostat(sbi, NULL, FS_GDATA_READ_IO, F2FS_BLKSIZE); + if (atomic_inode) + iput(atomic_inode); return 0; put_encrypted_page: f2fs_put_page(fio.encrypted_page, true); put_folio: f2fs_folio_put(folio, true); +out_iput: + if (atomic_inode) + iput(atomic_inode); return err; } @@ -1314,8 +1332,8 @@ static int ra_data_block(struct inode *inode, pgoff_t index) static int move_data_block(struct inode *inode, block_t bidx, int gc_type, unsigned int segno, int off) { - struct address_space *mapping = f2fs_is_cow_file(inode) ? - F2FS_I(inode)->atomic_inode->i_mapping : inode->i_mapping; + struct address_space *mapping = inode->i_mapping; + struct inode *atomic_inode = NULL; struct f2fs_io_info fio = { .sbi = F2FS_I_SB(inode), .ino = inode->i_ino, @@ -1337,10 +1355,23 @@ static int move_data_block(struct inode *inode, block_t bidx, (fio.sbi->gc_mode != GC_URGENT_HIGH) ? CURSEG_ALL_DATA_ATGC : CURSEG_COLD_DATA; + f2fs_down_read(&F2FS_I(inode)->i_sem); + if (f2fs_is_cow_file(inode)) { + atomic_inode = igrab(F2FS_I(inode)->atomic_inode); + if (!atomic_inode) { + f2fs_up_read(&F2FS_I(inode)->i_sem); + return -EBUSY; + } + mapping = atomic_inode->i_mapping; + } + f2fs_up_read(&F2FS_I(inode)->i_sem); + /* do not read out */ folio = f2fs_grab_cache_folio(mapping, bidx, false); - if (IS_ERR(folio)) - return PTR_ERR(folio); + if (IS_ERR(folio)) { + err = PTR_ERR(folio); + goto out_iput; + } if (!check_valid_map(F2FS_I_SB(inode), segno, off)) { err = -ENOENT; @@ -1473,6 +1504,9 @@ static int move_data_block(struct inode *inode, block_t bidx, folio_unlock(folio); folio_end_dropbehind(folio); folio_put(folio); +out_iput: + if (atomic_inode) + iput(atomic_inode); return err; } diff --git a/fs/f2fs/inode.c b/fs/f2fs/inode.c index 1694726122e6..939d75663a40 100644 --- a/fs/f2fs/inode.c +++ b/fs/f2fs/inode.c @@ -863,10 +863,15 @@ void f2fs_evict_inode(struct inode *inode) f2fs_abort_atomic_write(inode, true); if (fi->cow_inode && f2fs_is_cow_file(fi->cow_inode)) { - clear_inode_flag(fi->cow_inode, FI_COW_FILE); - F2FS_I(fi->cow_inode)->atomic_inode = NULL; - iput(fi->cow_inode); + struct inode *cow_inode = fi->cow_inode; + + f2fs_down_write(&F2FS_I(cow_inode)->i_sem); + clear_inode_flag(cow_inode, FI_COW_FILE); + F2FS_I(cow_inode)->atomic_inode = NULL; fi->cow_inode = NULL; + f2fs_up_write(&F2FS_I(cow_inode)->i_sem); + + iput(cow_inode); } trace_f2fs_evict_inode(inode); From 74c8d2ec95c59a5651ecd975c466998af1961fd4 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Wed, 20 May 2026 17:52:04 +0800 Subject: [PATCH 14/42] f2fs: fix missing read bio submission on large folio error f2fs_read_data_large_folio() can keep a read bio across multiple readahead folios. If a later folio hits an error before any of its blocks are added to the bio, folio_in_bio is false and the current error path returns immediately after ending that folio. This can leave the bio accumulated for earlier folios unsubmitted. Those folios then never receive read completion, and readers can wait indefinitely on the locked folios. Route errors through the common out path so any pending bio is submitted before returning. Stop consuming more readahead folios once an error is seen, and only wait on and clear the current folio when it was actually added to the bio. Cc: stable@kernel.org Fixes: a5d8b9d94e18 ("f2fs: fix to unlock folio in f2fs_read_data_large_folio()") Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/data.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index 5d18119a214a..d83a21998ec2 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -2529,7 +2529,7 @@ static int f2fs_read_data_large_folio(struct inode *inode, unsigned nrpages; struct f2fs_folio_state *ffs; int ret = 0; - bool folio_in_bio; + bool folio_in_bio = false; if (!IS_IMMUTABLE(inode) || f2fs_compressed_file(inode)) { if (folio) @@ -2645,18 +2645,17 @@ static int f2fs_read_data_large_folio(struct inode *inode, } trace_f2fs_read_folio(folio, DATA); err_out: - if (!folio_in_bio) { + if (!folio_in_bio) folio_end_read(folio, !ret); - if (ret) - return ret; - } + if (ret) + goto out; if (rac) { folio = readahead_folio(rac); goto next_folio; } out: f2fs_submit_read_bio(F2FS_I_SB(inode), bio, DATA); - if (ret) { + if (ret && folio_in_bio) { /* Wait bios and clear uptodate. */ folio_lock(folio); folio_clear_uptodate(folio); From fcb05c26c2a67953b420739b85f49386efc9b6c0 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Wed, 20 May 2026 20:07:05 +0800 Subject: [PATCH 15/42] f2fs: pass correct iostat type for single node writes f2fs_write_single_node_folio() takes an io_type argument, but still passes FS_GC_NODE_IO to __write_node_folio() unconditionally. This was harmless while the helper was only used by f2fs_move_node_folio(), whose caller passes FS_GC_NODE_IO. However, commit fe9b8b30b971 ("f2fs: fix inline data not being written to disk in writeback path") made f2fs_inline_data_fiemap() call the helper with FS_NODE_IO for FIEMAP_FLAG_SYNC. Honor the caller supplied io_type so inline-data FIEMAP sync writeback is accounted as normal node IO instead of GC node IO, while the GC path continues to pass FS_GC_NODE_IO explicitly. Cc: stable@kernel.org Fixes: fe9b8b30b971 ("f2fs: fix inline data not being written to disk in writeback path") Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/node.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c index a464d5ee1124..cd5a394f6111 100644 --- a/fs/f2fs/node.c +++ b/fs/f2fs/node.c @@ -1885,7 +1885,7 @@ int f2fs_write_single_node_folio(struct folio *node_folio, int sync_mode, } if (!__write_node_folio(node_folio, false, false, NULL, - &wbc, false, FS_GC_NODE_IO, NULL)) + &wbc, false, io_type, NULL)) err = -EAGAIN; goto release_folio; out_folio: From 5073c66a96a9c23c0c2533ed4ed06e42f9021208 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Thu, 21 May 2026 11:16:18 +0800 Subject: [PATCH 16/42] f2fs: validate compress cache inode only when enabled F2FS_COMPRESS_INO() uses NM_I(sbi)->max_nid as the synthetic inode number for the compressed page cache inode. That inode only exists when the compress_cache mount option is enabled. When compress_cache is disabled, max_nid is outside the valid inode range. A corrupted directory entry that points to ino == max_nid should therefore be rejected by f2fs_check_nid_range(). However, is_meta_ino() currently treats F2FS_COMPRESS_INO() as a meta inode unconditionally, so f2fs_iget() bypasses do_read_inode() and its nid range check, and instantiates a fake internal inode instead. Gate the compressed cache inode case on COMPRESS_CACHE, matching f2fs_init_compress_inode(). With compress_cache disabled, ino == max_nid now follows the normal inode path and is rejected as an out-of-range nid. Cc: stable@kernel.org Fixes: 6ce19aff0b8c ("f2fs: compress: add compress_inode to cache compressed blocks") Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/inode.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fs/f2fs/inode.c b/fs/f2fs/inode.c index 939d75663a40..25f30b8eadc5 100644 --- a/fs/f2fs/inode.c +++ b/fs/f2fs/inode.c @@ -564,8 +564,13 @@ static int do_read_inode(struct inode *inode) static bool is_meta_ino(struct f2fs_sb_info *sbi, unsigned int ino) { - return ino == F2FS_NODE_INO(sbi) || ino == F2FS_META_INO(sbi) || - ino == F2FS_COMPRESS_INO(sbi); + if (ino == F2FS_NODE_INO(sbi) || ino == F2FS_META_INO(sbi)) + return true; +#ifdef CONFIG_F2FS_FS_COMPRESSION + if (test_opt(sbi, COMPRESS_CACHE) && ino == F2FS_COMPRESS_INO(sbi)) + return true; +#endif + return false; } struct inode *f2fs_iget(struct super_block *sb, unsigned long ino) From 484c84ecc1a497d09239ca3a12dff3cc832830ce Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Thu, 21 May 2026 18:37:48 +0800 Subject: [PATCH 17/42] f2fs: avoid false shutdown fserror reports F2FS records image errors and checkpoint-stop reasons through the same s_error_work worker. The ordinary f2fs_handle_error() path only updates s_errors, but the worker still calls fserror_report_shutdown() unconditionally after committing the superblock. As a result, a metadata corruption report can be followed by a synthetic FAN_FS_ERROR event with ESHUTDOWN and an invalid superblock file handle, even though no stop reason was recorded. Track whether save_stop_reason() actually changed the stop_reason array and only report the shutdown fserror for that case. Pure s_errors updates still commit the superblock, but no longer generate a false shutdown event. Fixes: 50faed607d32 ("f2fs: support to report fserror") Cc: stable@kernel.org Reviewed-by: Chao Yu Signed-off-by: Wenjie Qi Signed-off-by: Jaegeuk Kim --- fs/f2fs/f2fs.h | 1 + fs/f2fs/super.c | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index b83ff4bd96ec..9f24287de4c3 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -1989,6 +1989,7 @@ struct f2fs_sb_info { unsigned char stop_reason[MAX_STOP_REASON]; /* stop reason */ spinlock_t error_lock; /* protect errors/stop_reason array */ bool error_dirty; /* errors of sb is dirty */ + bool stop_reason_dirty; /* stop reason of sb is dirty */ /* For reclaimed segs statistics per each GC mode */ unsigned int gc_segment_mode; /* GC state for reclaimed segments */ diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index 629548d78db0..b277807c8185 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -4626,6 +4626,7 @@ static void save_stop_reason(struct f2fs_sb_info *sbi, unsigned char reason) spin_lock_irqsave(&sbi->error_lock, flags); if (sbi->stop_reason[reason] < GENMASK(BITS_PER_BYTE - 1, 0)) sbi->stop_reason[reason]++; + sbi->stop_reason_dirty = true; spin_unlock_irqrestore(&sbi->error_lock, flags); } @@ -4633,6 +4634,7 @@ static void f2fs_record_stop_reason(struct f2fs_sb_info *sbi) { struct f2fs_super_block *raw_super = F2FS_RAW_SUPER(sbi); unsigned long flags; + bool report_shutdown = false; int err; f2fs_down_write(&sbi->sb_lock); @@ -4644,6 +4646,10 @@ static void f2fs_record_stop_reason(struct f2fs_sb_info *sbi) sbi->error_dirty = false; } memcpy(raw_super->s_stop_reason, sbi->stop_reason, MAX_STOP_REASON); + if (sbi->stop_reason_dirty) { + report_shutdown = true; + sbi->stop_reason_dirty = false; + } spin_unlock_irqrestore(&sbi->error_lock, flags); err = f2fs_commit_super(sbi, false); @@ -4654,7 +4660,8 @@ static void f2fs_record_stop_reason(struct f2fs_sb_info *sbi) "f2fs_commit_super fails to record stop_reason, err:%d", err); - fserror_report_shutdown(sbi->sb, GFP_NOFS); + if (report_shutdown) + fserror_report_shutdown(sbi->sb, GFP_NOFS); } void f2fs_save_errors(struct f2fs_sb_info *sbi, unsigned char flag) From a2b251259bf2bdf550893bac078e9ce0a10e76c9 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Fri, 22 May 2026 14:59:11 +0800 Subject: [PATCH 18/42] Revert: "f2fs: check in-memory block bitmap" Commit 355e78913c0d ("f2fs: check in-memory block bitmap") added a mirror for valid block bitmap, it expects to detect in-memory corruption, however we never got any reports from the check points for almost decade, let's remove the code, it can help to save memories. Cc: wallentx Suggested-by: Jaegeuk Kim Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/segment.c | 38 -------------------------------------- fs/f2fs/segment.h | 6 ------ 2 files changed, 44 deletions(-) diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 1ef4edb77078..9926ba9d77ba 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -2455,9 +2455,6 @@ static int update_sit_entry_for_release(struct f2fs_sb_info *sbi, struct seg_ent unsigned int segno, block_t blkaddr, unsigned int offset, int del) { bool exist; -#ifdef CONFIG_F2FS_CHECK_FS - bool mir_exist; -#endif int i; int del_count = -del; @@ -2465,15 +2462,6 @@ static int update_sit_entry_for_release(struct f2fs_sb_info *sbi, struct seg_ent for (i = 0; i < del_count; i++) { exist = f2fs_test_and_clear_bit(offset + i, se->cur_valid_map); -#ifdef CONFIG_F2FS_CHECK_FS - mir_exist = f2fs_test_and_clear_bit(offset + i, - se->cur_valid_map_mir); - if (unlikely(exist != mir_exist)) { - f2fs_err(sbi, "Inconsistent error when clearing bitmap, blk:%u, old bit:%d", - blkaddr + i, exist); - f2fs_bug_on(sbi, 1); - } -#endif if (unlikely(!exist)) { f2fs_err(sbi, "Bitmap was wrongly cleared, blk:%u", blkaddr + i); f2fs_bug_on(sbi, 1); @@ -2514,20 +2502,8 @@ static int update_sit_entry_for_alloc(struct f2fs_sb_info *sbi, struct seg_entry unsigned int segno, block_t blkaddr, unsigned int offset, int del) { bool exist; -#ifdef CONFIG_F2FS_CHECK_FS - bool mir_exist; -#endif exist = f2fs_test_and_set_bit(offset, se->cur_valid_map); -#ifdef CONFIG_F2FS_CHECK_FS - mir_exist = f2fs_test_and_set_bit(offset, - se->cur_valid_map_mir); - if (unlikely(exist != mir_exist)) { - f2fs_err(sbi, "Inconsistent error when setting bitmap, blk:%u, old bit:%d", - blkaddr, exist); - f2fs_bug_on(sbi, 1); - } -#endif if (unlikely(exist)) { f2fs_err(sbi, "Bitmap was wrongly set, blk:%u", blkaddr); f2fs_bug_on(sbi, 1); @@ -4771,11 +4747,6 @@ void f2fs_flush_sit_entries(struct f2fs_sb_info *sbi, struct cp_control *cpc) int offset, sit_offset; se = get_seg_entry(sbi, segno); -#ifdef CONFIG_F2FS_CHECK_FS - if (memcmp(se->cur_valid_map, se->cur_valid_map_mir, - SIT_VBLOCK_MAP_SIZE)) - f2fs_bug_on(sbi, 1); -#endif /* add discard candidates */ if (!(cpc->reason & CP_DISCARD)) { @@ -4866,11 +4837,7 @@ static int build_sit_info(struct f2fs_sb_info *sbi) if (!sit_i->dirty_sentries_bitmap) return -ENOMEM; -#ifdef CONFIG_F2FS_CHECK_FS - bitmap_size = MAIN_SEGS(sbi) * SIT_VBLOCK_MAP_SIZE * (3 + discard_map); -#else bitmap_size = MAIN_SEGS(sbi) * SIT_VBLOCK_MAP_SIZE * (2 + discard_map); -#endif sit_i->bitmap = f2fs_kvzalloc(sbi, bitmap_size, GFP_KERNEL); if (!sit_i->bitmap) return -ENOMEM; @@ -4884,11 +4851,6 @@ static int build_sit_info(struct f2fs_sb_info *sbi) sit_i->sentries[start].ckpt_valid_map = bitmap; bitmap += SIT_VBLOCK_MAP_SIZE; -#ifdef CONFIG_F2FS_CHECK_FS - sit_i->sentries[start].cur_valid_map_mir = bitmap; - bitmap += SIT_VBLOCK_MAP_SIZE; -#endif - if (discard_map) { sit_i->sentries[start].discard_map = bitmap; bitmap += SIT_VBLOCK_MAP_SIZE; diff --git a/fs/f2fs/segment.h b/fs/f2fs/segment.h index 08735a165433..38a56b8ab2cc 100644 --- a/fs/f2fs/segment.h +++ b/fs/f2fs/segment.h @@ -177,9 +177,6 @@ struct seg_entry { unsigned int ckpt_valid_blocks:10; /* # of valid blocks last cp */ unsigned int padding:6; /* padding */ unsigned char *cur_valid_map; /* validity bitmap of blocks */ -#ifdef CONFIG_F2FS_CHECK_FS - unsigned char *cur_valid_map_mir; /* mirror of current valid bitmap */ -#endif /* * # of valid blocks and the validity bitmap stored in the last * checkpoint pack. This information is used by the SSR mode. @@ -408,9 +405,6 @@ static inline void seg_info_from_raw_sit(struct seg_entry *se, se->ckpt_valid_blocks = GET_SIT_VBLOCKS(rs); memcpy(se->cur_valid_map, rs->valid_map, SIT_VBLOCK_MAP_SIZE); memcpy(se->ckpt_valid_map, rs->valid_map, SIT_VBLOCK_MAP_SIZE); -#ifdef CONFIG_F2FS_CHECK_FS - memcpy(se->cur_valid_map_mir, rs->valid_map, SIT_VBLOCK_MAP_SIZE); -#endif se->type = GET_SIT_TYPE(rs); se->mtime = le64_to_cpu(rs->mtime); } From 7967d563bcbaac60711f09d699bce95e91a7cb5c Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Fri, 22 May 2026 14:59:12 +0800 Subject: [PATCH 19/42] Revert: "f2fs: check in-memory sit version bitmap" Commit ae27d62e6bef ("f2fs: check in-memory sit version bitmap") added a mirror for sit version bitmap, it expects to detect in-memory corruption, however we never got any reports from the check points for almost decade, let's remove the code, it can help to save memories. Cc: wallentx Suggested-by: Jaegeuk Kim Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/segment.c | 6 ------ fs/f2fs/segment.h | 16 ---------------- 2 files changed, 22 deletions(-) diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 9926ba9d77ba..993555042876 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -4882,11 +4882,6 @@ static int build_sit_info(struct f2fs_sb_info *sbi) return -ENOMEM; #ifdef CONFIG_F2FS_CHECK_FS - sit_i->sit_bitmap_mir = kmemdup(src_bitmap, - sit_bitmap_size, GFP_KERNEL); - if (!sit_i->sit_bitmap_mir) - return -ENOMEM; - sit_i->invalid_segmap = f2fs_kvzalloc(sbi, main_bitmap_size, GFP_KERNEL); if (!sit_i->invalid_segmap) @@ -5849,7 +5844,6 @@ static void destroy_sit_info(struct f2fs_sb_info *sbi) SM_I(sbi)->sit_info = NULL; kfree(sit_i->sit_bitmap); #ifdef CONFIG_F2FS_CHECK_FS - kfree(sit_i->sit_bitmap_mir); kvfree(sit_i->invalid_segmap); #endif kfree(sit_i); diff --git a/fs/f2fs/segment.h b/fs/f2fs/segment.h index 38a56b8ab2cc..b0c06b3580b4 100644 --- a/fs/f2fs/segment.h +++ b/fs/f2fs/segment.h @@ -206,8 +206,6 @@ struct sit_info { char *bitmap; /* all bitmaps pointer */ char *sit_bitmap; /* SIT bitmap pointer */ #ifdef CONFIG_F2FS_CHECK_FS - char *sit_bitmap_mir; /* SIT bitmap mirror */ - /* bitmap of segments to be ignored by GC in case of errors */ unsigned long *invalid_segmap; #endif @@ -549,11 +547,6 @@ static inline void get_sit_bitmap(struct f2fs_sb_info *sbi, { struct sit_info *sit_i = SIT_I(sbi); -#ifdef CONFIG_F2FS_CHECK_FS - if (memcmp(sit_i->sit_bitmap, sit_i->sit_bitmap_mir, - sit_i->bitmap_size)) - f2fs_bug_on(sbi, 1); -#endif memcpy(dst_addr, sit_i->sit_bitmap, sit_i->bitmap_size); } @@ -894,12 +887,6 @@ static inline pgoff_t current_sit_addr(struct f2fs_sb_info *sbi, f2fs_bug_on(sbi, !valid_main_segno(sbi, start)); -#ifdef CONFIG_F2FS_CHECK_FS - if (f2fs_test_bit(offset, sit_i->sit_bitmap) != - f2fs_test_bit(offset, sit_i->sit_bitmap_mir)) - f2fs_bug_on(sbi, 1); -#endif - /* calculate sit block address */ if (f2fs_test_bit(offset, sit_i->sit_bitmap)) blk_addr += sit_i->sit_blocks; @@ -925,9 +912,6 @@ static inline void set_to_next_sit(struct sit_info *sit_i, unsigned int start) unsigned int block_off = SIT_BLOCK_OFFSET(start); f2fs_change_bit(block_off, sit_i->sit_bitmap); -#ifdef CONFIG_F2FS_CHECK_FS - f2fs_change_bit(block_off, sit_i->sit_bitmap_mir); -#endif } static inline unsigned long long get_mtime(struct f2fs_sb_info *sbi, From 8712353ed80f87271d732297567dcdbe4b84e8c7 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Fri, 22 May 2026 15:53:29 +0800 Subject: [PATCH 20/42] f2fs: fix to do sanity check on f2fs_get_node_folio_ra() kernel BUG at fs/f2fs/file.c:845! Oops: invalid opcode: 0000 [#1] SMP KASAN NOPTI CPU: 0 UID: 0 PID: 5336 Comm: syz.0.0 Not tainted syzkaller #0 PREEMPT(full) Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014 RIP: 0010:f2fs_do_truncate_blocks+0x1115/0x1140 fs/f2fs/file.c:845 Code: fc fc 90 0f 0b e8 8b 9d 9a fd 90 0f 0b e8 83 9d 9a fd 48 89 df 48 c7 c6 60 d1 1a 8c e8 54 f1 fc fc 90 0f 0b e8 6c 9d 9a fd 90 <0f> 0b e8 64 9d 9a fd 90 0f 0b 90 e9 93 fd ff ff e8 56 9d 9a fd 90 RSP: 0018:ffffc9000e4474c0 EFLAGS: 00010283 RAX: ffffffff842b1d34 RBX: 0000000000000003 RCX: 0000000000100000 RDX: ffffc9000f03a000 RSI: 0000000000035503 RDI: 0000000000035504 RBP: ffffc9000e447608 R08: ffff8880123b0000 R09: 0000000000000002 R10: 00000000fffffffe R11: 0000000000000002 R12: 0000000000000001 R13: 0000000000000000 R14: 1ffff92001c88ea0 R15: 00000000ffff039c FS: 00007f7e02ee36c0(0000) GS:ffff88808c887000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: 00007ff0305c4000 CR3: 0000000012d4c000 CR4: 0000000000352ef0 Call Trace: f2fs_truncate_blocks+0x10a/0x300 fs/f2fs/file.c:882 f2fs_truncate+0x471/0x7c0 fs/f2fs/file.c:940 f2fs_evict_inode+0xa3f/0x1ac0 fs/f2fs/inode.c:907 evict+0x61e/0xb10 fs/inode.c:841 f2fs_fill_super+0x5f43/0x78f0 fs/f2fs/super.c:5224 get_tree_bdev_flags+0x431/0x4f0 fs/super.c:1694 vfs_get_tree+0x92/0x2a0 fs/super.c:1754 fc_mount fs/namespace.c:1193 [inline] do_new_mount_fc fs/namespace.c:3758 [inline] do_new_mount+0x341/0xd30 fs/namespace.c:3834 do_mount fs/namespace.c:4167 [inline] __do_sys_mount fs/namespace.c:4383 [inline] __se_sys_mount+0x31d/0x420 fs/namespace.c:4360 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline] do_syscall_64+0x15f/0xf80 arch/x86/entry/syscall_64.c:94 entry_SYSCALL_64_after_hwframe+0x77/0x7f count = ADDRS_PER_PAGE(dn.node_folio, inode); count -= dn.ofs_in_node; f2fs_bug_on(sbi, count < 0); The fuzz test will trigger above bug_on in f2fs. The root cause should be: in the corrupted inode, there is a direct node which has the same ino and nid in its footer, so in f2fs_do_truncate_blocks(), after f2fs_get_dnode_of_data() finds such dnode: 1) ADDRS_PER_PAGE(dn.node_folio, inode) will return 923 2) once dn.ofs_in_node points to addr[923, 1017] Then it will trigger the system panic. Let's introduce NODE_TYPE_NON_IXNODE to indicate current node should not be an inode or xattr node, and then use it in below path to detect inconsistent node chain in inode mapping table: - f2fs_do_truncate_blocks - f2fs_get_dnode_of_data - f2fs_get_node_folio_ra - __get_node_folio - f2fs_sanity_check_node_footer - case NODE_TYPE_NON_IXNODE -> check whether it is inode|xnode Cc: stable@kernel.org Reported-by: syzbot+2488d8d751b27f7ce268@syzkaller.appspotmail.com Closes: https://lore.kernel.org/all/69fa3697.170a0220.59368.0018.GAE@google.com Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/f2fs.h | 1 + fs/f2fs/node.c | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 9f24287de4c3..086bc5243979 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -1592,6 +1592,7 @@ enum node_type { NODE_TYPE_INODE, NODE_TYPE_XATTR, NODE_TYPE_NON_INODE, + NODE_TYPE_NON_IXNODE, /* non inode and xnode */ }; /* a threshold of maximum elapsed time in critical region to print tracepoint */ diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c index cd5a394f6111..38917e4a7319 100644 --- a/fs/f2fs/node.c +++ b/fs/f2fs/node.c @@ -1548,6 +1548,10 @@ int f2fs_sanity_check_node_footer(struct f2fs_sb_info *sbi, if (is_inode) goto out_err; break; + case NODE_TYPE_NON_IXNODE: + if (is_inode || is_xnode) + goto out_err; + break; default: break; } @@ -1643,7 +1647,7 @@ static struct folio *f2fs_get_node_folio_ra(struct folio *parent, int start) struct f2fs_sb_info *sbi = F2FS_F_SB(parent); nid_t nid = get_nid(parent, start, false); - return __get_node_folio(sbi, nid, parent, start, NODE_TYPE_REGULAR); + return __get_node_folio(sbi, nid, parent, start, NODE_TYPE_NON_IXNODE); } static void flush_inline_data(struct f2fs_sb_info *sbi, nid_t ino) From 28ebb922b99d415e8bf51bf8b065a14fd7672167 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Fri, 22 May 2026 14:12:06 +0800 Subject: [PATCH 21/42] f2fs: honor per-I/O write streams for direct writes io_uring can pass a per-I/O write stream through kiocb->ki_write_stream, and block direct I/O propagates that value to bio->bi_write_stream. F2FS added FDP stream mapping for DATA writes, but its direct write submit hook always rewrites bio->bi_write_stream from the inode write hint and F2FS temperature. As a result, a direct write with an explicit io_uring write_stream is submitted to the F2FS-selected stream instead of the user-requested stream. Validate an explicit write stream before starting F2FS direct I/O, pass the kiocb through the iomap private pointer, and preserve the per-I/O stream in the direct write bio. When no per-I/O stream is supplied, keep using the existing F2FS temperature-to-stream mapping. Fixes: 42f7a7a50a33 ("f2fs: map data writes to FDP streams") Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 6edf0105dbc8..69aad1060c48 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -5101,16 +5101,33 @@ static int f2fs_dio_write_end_io(struct kiocb *iocb, ssize_t size, int error, return 0; } +static bool f2fs_valid_write_stream(struct f2fs_sb_info *sbi, u8 write_stream) +{ + int i; + + if (!write_stream) + return true; + if (!f2fs_is_multi_device(sbi)) + return write_stream <= bdev_max_write_streams(sbi->sb->s_bdev); + + for (i = 0; i < sbi->s_ndevs; i++) + if (write_stream > bdev_max_write_streams(FDEV(i).bdev)) + return false; + return true; +} + static void f2fs_dio_write_submit_io(const struct iomap_iter *iter, struct bio *bio, loff_t file_offset) { struct inode *inode = iter->inode; struct f2fs_sb_info *sbi = F2FS_I_SB(inode); + struct kiocb *iocb = iter->private; enum log_type type = f2fs_rw_hint_to_seg_type(sbi, inode->i_write_hint); enum temp_type temp = f2fs_get_segment_temp(sbi, type); bio->bi_write_hint = f2fs_io_type_to_rw_hint(sbi, DATA, temp); bio->bi_write_stream = + iocb->ki_write_stream ? iocb->ki_write_stream : f2fs_io_type_to_write_stream(bio->bi_bdev, DATA, temp); f2fs_dio_iostat_start(sbi, bio); blk_crypto_submit_bio(bio); @@ -5150,6 +5167,11 @@ static ssize_t f2fs_dio_write_iter(struct kiocb *iocb, struct iov_iter *from, trace_f2fs_direct_IO_enter(inode, iocb, count, WRITE); + if (!f2fs_valid_write_stream(sbi, iocb->ki_write_stream)) { + ret = -EINVAL; + goto out; + } + if (iocb->ki_flags & IOCB_NOWAIT) { /* f2fs_convert_inline_inode() and block allocation can block */ if (f2fs_has_inline_data(inode) || @@ -5187,7 +5209,7 @@ static ssize_t f2fs_dio_write_iter(struct kiocb *iocb, struct iov_iter *from, if (pos + count > inode->i_size) dio_flags |= IOMAP_DIO_FORCE_WAIT; dio = __iomap_dio_rw(iocb, from, &f2fs_iomap_ops, - &f2fs_iomap_dio_write_ops, dio_flags, NULL, 0); + &f2fs_iomap_dio_write_ops, dio_flags, iocb, 0); if (IS_ERR_OR_NULL(dio)) { ret = PTR_ERR_OR_ZERO(dio); if (ret == -ENOTBLK) From 846c499a65816d13f1186e3090e825e8bb8bcb8b Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Tue, 26 May 2026 13:35:57 +0800 Subject: [PATCH 22/42] f2fs: validate orphan inode entry count f2fs_recover_orphan_inodes() trusts the orphan block entry_count when replaying orphan inodes from the checkpoint pack. A corrupted entry_count larger than F2FS_ORPHANS_PER_BLOCK makes the recovery loop read past the ino[] array and interpret footer or following data as inode numbers. On a crafted image, mounting an unpatched kernel can drive orphan recovery into f2fs_bug_on() and panic the kernel. Validate entry_count before consuming entries so corrupted checkpoint data fails the mount with -EFSCORRUPTED and requests fsck instead. Set ERROR_INCONSISTENT_ORPHAN as well, so the corruption reason can be recorded in the superblock s_errors[] field. This gives fsck a persistent hint even though mount-time orphan recovery failure may leave no chance to persist SBI_NEED_FSCK through a checkpoint. Cc: stable@kernel.org Fixes: 127e670abfa7 ("f2fs: add checkpoint operations") Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/checkpoint.c | 14 +++++++++++++- include/linux/f2fs_fs.h | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/checkpoint.c b/fs/f2fs/checkpoint.c index c00a6b6ebcbd..064f5b537423 100644 --- a/fs/f2fs/checkpoint.c +++ b/fs/f2fs/checkpoint.c @@ -943,6 +943,7 @@ int f2fs_recover_orphan_inodes(struct f2fs_sb_info *sbi) for (i = 0; i < orphan_blocks; i++) { struct folio *folio; struct f2fs_orphan_block *orphan_blk; + unsigned int entry_count; folio = f2fs_get_meta_folio(sbi, start_blk + i); if (IS_ERR(folio)) { @@ -951,7 +952,18 @@ int f2fs_recover_orphan_inodes(struct f2fs_sb_info *sbi) } orphan_blk = folio_address(folio); - for (j = 0; j < le32_to_cpu(orphan_blk->entry_count); j++) { + entry_count = le32_to_cpu(orphan_blk->entry_count); + if (entry_count > F2FS_ORPHANS_PER_BLOCK) { + f2fs_err(sbi, "invalid orphan inode entry count %u", + entry_count); + set_sbi_flag(sbi, SBI_NEED_FSCK); + f2fs_handle_error(sbi, ERROR_INCONSISTENT_ORPHAN); + err = -EFSCORRUPTED; + f2fs_folio_put(folio, true); + goto out; + } + + for (j = 0; j < entry_count; j++) { nid_t ino = le32_to_cpu(orphan_blk->ino[j]); err = recover_orphan_inode(sbi, ino); diff --git a/include/linux/f2fs_fs.h b/include/linux/f2fs_fs.h index 829a59399dac..bb2b6cd5d507 100644 --- a/include/linux/f2fs_fs.h +++ b/include/linux/f2fs_fs.h @@ -107,6 +107,7 @@ enum f2fs_error { ERROR_CORRUPTED_XATTR, ERROR_INVALID_NODE_REFERENCE, ERROR_INCONSISTENT_NAT, + ERROR_INCONSISTENT_ORPHAN, ERROR_MAX, }; From 6d874b65aadce56ac78f76129dbcfc2599b638f8 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Wed, 27 May 2026 20:06:28 +0800 Subject: [PATCH 23/42] f2fs: keep atomic write retry from zeroing original data A partial atomic write reserves a block in the COW inode before reading the original data page for the untouched bytes in that page. If that read fails, write_begin returns an error but leaves the COW inode entry as NEW_ADDR. A retry of the same partial write then finds the COW entry, treats it as existing COW data, and f2fs_write_begin() zeroes the whole folio because blkaddr is NEW_ADDR. If the retry is committed, the bytes outside the retried write range are committed as zeroes instead of preserving the original file contents. Only use the COW inode as the read source when it already has a real data block. If the COW entry is still NEW_ADDR, treat it as a reservation to reuse: keep reading the old data from the original inode and avoid reserving or accounting the same atomic block again. Cc: stable@kernel.org Fixes: 3db1de0e582c ("f2fs: change the current atomic write way") Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/data.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index d83a21998ec2..edda2ff72073 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -3862,6 +3862,7 @@ static int prepare_atomic_write_begin(struct f2fs_sb_info *sbi, pgoff_t index = folio->index; int err = 0; block_t ori_blk_addr = NULL_ADDR; + bool cow_has_reserved_block = false; /* If pos is beyond the end of file, reserve a new block in COW inode */ if ((pos & PAGE_MASK) >= i_size_read(inode)) @@ -3871,9 +3872,11 @@ static int prepare_atomic_write_begin(struct f2fs_sb_info *sbi, err = __find_data_block(cow_inode, index, blk_addr); if (err) { return err; - } else if (*blk_addr != NULL_ADDR) { + } else if (__is_valid_data_blkaddr(*blk_addr)) { *use_cow = true; return 0; + } else if (*blk_addr == NEW_ADDR) { + cow_has_reserved_block = true; } if (is_inode_flag_set(inode, FI_ATOMIC_REPLACE)) @@ -3886,10 +3889,13 @@ static int prepare_atomic_write_begin(struct f2fs_sb_info *sbi, reserve_block: /* Finally, we should reserve a new block in COW inode for the update */ - err = __reserve_data_block(cow_inode, index, blk_addr, node_changed); - if (err) - return err; - inc_atomic_write_cnt(inode); + if (!cow_has_reserved_block) { + err = __reserve_data_block(cow_inode, index, blk_addr, + node_changed); + if (err) + return err; + inc_atomic_write_cnt(inode); + } if (ori_blk_addr != NULL_ADDR) *blk_addr = ori_blk_addr; From 222bc257a151cc1b01d926199a0d5a7ba61d2e53 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Fri, 29 May 2026 10:29:24 +0800 Subject: [PATCH 24/42] f2fs: skip inode folio lookup for cached overwrite prepare_write_begin() first gets the inode folio and builds a dnode, then checks the read extent cache. For an ordinary overwrite of a non-inline and non-compressed file, an extent-cache hit already gives the data block address and the following path does not need to allocate or update any node state. Check the read extent cache before fetching the inode folio for that narrow case. Keep the existing paths for inline data, compressed files, and writes that may extend past EOF, where the helper may need inline conversion, compression preparation, or block reservation. This avoids a node-folio lookup in the buffered overwrite fast path when the mapping is already cached. In a QEMU/KASAN x86_64 VM, using a small buffered overwrite workload on an existing 1MiB file, median time improved as follows: 64-byte overwrites: 1724.93 ns/write -> 1560.24 ns/write 256-byte overwrites: 1713.38 ns/write -> 1577.85 ns/write Function profiling of 20k 64-byte overwrites showed f2fs_get_inode_folio() calls drop from 20004 to 4. Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/data.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index edda2ff72073..cea34b4595b1 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -3719,6 +3719,11 @@ static int prepare_write_begin(struct f2fs_sb_info *sbi, int flag = F2FS_GET_BLOCK_PRE_AIO; int err = 0; + if (!f2fs_has_inline_data(inode) && !f2fs_compressed_file(inode) && + (pos & PAGE_MASK) < i_size_read(inode) && + f2fs_lookup_read_extent_cache_block(inode, index, blk_addr)) + return 0; + /* * If a whole page is being written and we already preallocated all the * blocks, then there is no need to get a block address now. From a41075acde0124d2f8a5f563068a5d63e8ffd57b Mon Sep 17 00:00:00 2001 From: Mikhail Lobanov Date: Mon, 15 Jun 2026 14:36:13 +0300 Subject: [PATCH 25/42] f2fs: read COW data with the original inode during atomic write When updating an atomic-write file, f2fs_write_begin() may read the previously written data back from the COW inode: prepare_atomic_write_begin() locates the block in the COW inode and sets use_cow, and the read bio is then built with the COW inode: f2fs_submit_page_read(use_cow ? F2FS_I(inode)->cow_inode : inode, ...); and f2fs_grab_read_bio() decides whether to schedule fs-layer decryption (STEP_DECRYPT) for the bio based on that inode via fscrypt_inode_uses_fs_layer_crypto(). However, the folio being filled belongs to the original inode (folio->mapping->host == inode), and the data stored in the COW block was encrypted (or left as plaintext) using the original inode's context, not the COW inode's -- see f2fs_encrypt_one_page(), which keys off fio->page->mapping->host. fscrypt_decrypt_pagecache_blocks() likewise operates on folio->mapping->host. The COW inode is created as a tmpfile in the parent directory and inherits its encryption policy from there. With test_dummy_encryption the newly created COW inode gets the dummy policy and becomes encrypted, while a pre-existing regular file -- created before the policy applied, e.g. already present in the on-disk image -- stays unencrypted. The read path then sets STEP_DECRYPT based on the encrypted COW inode and calls fscrypt_decrypt_pagecache_blocks() on a folio whose host (the unencrypted original inode) has a NULL ->i_crypt_info, dereferencing it: Oops: general protection fault, probably for non-canonical address ... KASAN: null-ptr-deref in range [0x0000000000000008-0x000000000000000f] RIP: 0010:fscrypt_decrypt_pagecache_blocks+0xa0/0x310 Workqueue: f2fs_post_read_wq f2fs_post_read_work Call Trace: fscrypt_decrypt_bio+0x1eb/0x340 f2fs_post_read_work+0xba/0x140 process_one_work+0x91c/0x1a40 worker_thread+0x677/0xe90 kthread+0x2bc/0x3a0 The COW inode is only needed to locate the on-disk block, and that block address is already resolved into @blkaddr by prepare_atomic_write_begin() via __find_data_block(cow_inode, ...); f2fs_submit_page_read() then reads from that physical @blkaddr directly, so the inode argument only selects the post-read crypto context, not which block is fetched. Reading with @inode therefore returns the same (latest, not-yet-committed) COW data, while making both the fs-layer decryption decision and the inline crypto path use the correct (original inode's) key. With the COW inode no longer used at the read site, the use_cow flag has no remaining consumer; drop it from f2fs_write_begin() and prepare_atomic_write_begin(). Fixes: 591fc34e1f98 ("f2fs: use cow inode data when updating atomic write") Cc: stable@vger.kernel.org Signed-off-by: Mikhail Lobanov Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/data.c | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index cea34b4595b1..60dea95b0295 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -3860,7 +3860,7 @@ static int __reserve_data_block(struct inode *inode, pgoff_t index, static int prepare_atomic_write_begin(struct f2fs_sb_info *sbi, struct folio *folio, loff_t pos, unsigned int len, - block_t *blk_addr, bool *node_changed, bool *use_cow) + block_t *blk_addr, bool *node_changed) { struct inode *inode = folio->mapping->host; struct inode *cow_inode = F2FS_I(inode)->cow_inode; @@ -3875,14 +3875,14 @@ static int prepare_atomic_write_begin(struct f2fs_sb_info *sbi, /* Look for the block in COW inode first */ err = __find_data_block(cow_inode, index, blk_addr); - if (err) { + if (err) return err; - } else if (__is_valid_data_blkaddr(*blk_addr)) { - *use_cow = true; + + if (__is_valid_data_blkaddr(*blk_addr)) return 0; - } else if (*blk_addr == NEW_ADDR) { + + if (*blk_addr == NEW_ADDR) cow_has_reserved_block = true; - } if (is_inode_flag_set(inode, FI_ATOMIC_REPLACE)) goto reserve_block; @@ -3917,7 +3917,6 @@ static int f2fs_write_begin(const struct kiocb *iocb, struct folio *folio; pgoff_t index = pos >> PAGE_SHIFT; bool need_balance = false; - bool use_cow = false; block_t blkaddr = NULL_ADDR; int err = 0; @@ -3980,7 +3979,7 @@ static int f2fs_write_begin(const struct kiocb *iocb, if (f2fs_is_atomic_file(inode)) err = prepare_atomic_write_begin(sbi, folio, pos, len, - &blkaddr, &need_balance, &use_cow); + &blkaddr, &need_balance); else err = prepare_write_begin(sbi, folio, pos, len, &blkaddr, &need_balance); @@ -4020,8 +4019,15 @@ static int f2fs_write_begin(const struct kiocb *iocb, err = -EFSCORRUPTED; goto put_folio; } - f2fs_submit_page_read(use_cow ? F2FS_I(inode)->cow_inode : - inode, + /* + * Although the block may be stored in the COW inode, the folio + * belongs to @inode and its data was encrypted (or not) using + * @inode's context (see f2fs_encrypt_one_page()). Read with + * @inode so the post-read decryption decision matches the + * folio's owner; otherwise an unencrypted @inode whose COW inode + * is encrypted hits a NULL ->i_crypt_info on decryption. + */ + f2fs_submit_page_read(inode, NULL, /* can't write to fsverity files */ folio, blkaddr, 0, true); From cfcd0e49a178b3dac2c0ece656079081dbf5da74 Mon Sep 17 00:00:00 2001 From: Samuel Moelius Date: Wed, 3 Jun 2026 15:11:40 +0000 Subject: [PATCH 26/42] f2fs: validate inline dentry name lengths before conversion Inline dentry conversion copies names out of the inline dentry area before checking that each recorded name length fits in the available filename slots. A corrupted image can therefore make the conversion path read past the inline filename storage while building the regular dentry block. Validate each inline dentry name length against the inline filename area before copying it. Assisted-by: Codex:gpt-5.5-cyber-preview Signed-off-by: Samuel Moelius Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/inline.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/f2fs/inline.c b/fs/f2fs/inline.c index 099f72089701..e2f7bedf1552 100644 --- a/fs/f2fs/inline.c +++ b/fs/f2fs/inline.c @@ -510,6 +510,12 @@ static int f2fs_add_inline_entries(struct inode *dir, void *inline_dentry) bit_pos++; continue; } + if (unlikely(le16_to_cpu(de->name_len) > F2FS_NAME_LEN || + bit_pos + GET_DENTRY_SLOTS(le16_to_cpu(de->name_len)) > + d.max)) { + err = -EFSCORRUPTED; + goto punch_dentry_pages; + } /* * We only need the disk_name and hash to move the dentry. @@ -530,6 +536,7 @@ static int f2fs_add_inline_entries(struct inode *dir, void *inline_dentry) bit_pos += GET_DENTRY_SLOTS(le16_to_cpu(de->name_len)); } return 0; + punch_dentry_pages: truncate_inode_pages(&dir->i_data, 0); f2fs_truncate_blocks(dir, 0, false); From 90e02a8e1b6863c41876473f844c8e24b06d55f7 Mon Sep 17 00:00:00 2001 From: Samuel Moelius Date: Wed, 3 Jun 2026 16:11:26 +0000 Subject: [PATCH 27/42] f2fs: validate dentry name length before lookup compares it The f2fs dentry lookup path can use the on-disk name length before checking that the name fits in the dentry filename area. A corrupted dentry can then make lookup read beyond the filename slots. The bounds check needs to happen before any comparison that consumes the name length from disk. Reject dentries with invalid name lengths before comparing their names. Assisted-by: Codex:gpt-5.5-cyber-preview Signed-off-by: Samuel Moelius Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/dir.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/f2fs/dir.c b/fs/f2fs/dir.c index b1697194c3c4..a9563f7fcd88 100644 --- a/fs/f2fs/dir.c +++ b/fs/f2fs/dir.c @@ -250,6 +250,11 @@ struct f2fs_dir_entry *f2fs_find_target_dentry(const struct f2fs_dentry_ptr *d, continue; } + if (unlikely(le16_to_cpu(de->name_len) > F2FS_NAME_LEN || + bit_pos + GET_DENTRY_SLOTS(le16_to_cpu(de->name_len)) > + d->max)) + return ERR_PTR(-EFSCORRUPTED); + if (!use_hash || de->hash_code == fname->hash) { res = f2fs_match_name(d->inode, fname, d->filename[bit_pos], From 242d30bfc0a84b8b5de0a88821b53c9ad7fd31c4 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Wed, 10 Jun 2026 22:37:35 +0800 Subject: [PATCH 28/42] f2fs: reject setattr size changes on large folio files F2FS large folios are only enabled for immutable non-compressed files. Writable open and writable mmap reject such mappings, but truncate(2) through f2fs_setattr() misses the same guard. If FS_IMMUTABLE_FL is cleared while the inode is still cached, the mapping can keep large-folio support and ATTR_SIZE can change i_size. Reject size changes in that state. Cc: stable@kernel.org Fixes: 05e65c14ea59 ("f2fs: support large folio for immutable non-compressed case") Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 69aad1060c48..d240ca78a31f 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -1098,6 +1098,8 @@ int f2fs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, return -EPERM; if ((attr->ia_valid & ATTR_SIZE)) { + if (mapping_large_folio_support(inode->i_mapping)) + return -EOPNOTSUPP; if (!f2fs_is_compress_backend_ready(inode) || IS_DEVICE_ALIASING(inode)) return -EOPNOTSUPP; From 41b7928813b5db9f61bf55e0ce395f69eb0473eb Mon Sep 17 00:00:00 2001 From: Bart Van Assche Date: Wed, 10 Jun 2026 12:34:17 -0700 Subject: [PATCH 29/42] f2fs: Prepare for supporting delayed bio completion Use bio frontpadding to allocate memory for a work_struct when allocating a bio. Reviewed-by: Chao Yu Signed-off-by: Bart Van Assche Signed-off-by: Jaegeuk Kim --- fs/f2fs/data.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index 60dea95b0295..9bddc8bf0af6 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -41,12 +41,17 @@ struct f2fs_folio_state { unsigned int read_pages_pending; }; +struct f2fs_bio { + struct work_struct work; + struct bio bio; +}; + #define F2FS_BIO_POOL_SIZE NR_CURSEG_TYPE int __init f2fs_init_bioset(void) { return bioset_init(&f2fs_bioset, F2FS_BIO_POOL_SIZE, - 0, BIOSET_NEED_BVECS); + offsetof(struct f2fs_bio, bio), BIOSET_NEED_BVECS); } void f2fs_destroy_bioset(void) From 7ba36a9ea81c018c828138e8071c3658620e2f0f Mon Sep 17 00:00:00 2001 From: Bart Van Assche Date: Wed, 10 Jun 2026 12:34:18 -0700 Subject: [PATCH 30/42] f2fs: Rename f2fs_post_read_wq into f2fs_wq Rename f2fs_post_read_wq into f2fs_wq. Create it unconditionally. Prepare for using this workqueue for completing write bios. Reviewed-by: Chao Yu Signed-off-by: Bart Van Assche Signed-off-by: Jaegeuk Kim --- fs/f2fs/compress.c | 2 +- fs/f2fs/data.c | 22 ++++++++-------------- fs/f2fs/f2fs.h | 6 +++--- fs/f2fs/super.c | 8 ++++---- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/fs/f2fs/compress.c b/fs/f2fs/compress.c index caf522d667d6..372e07c58e21 100644 --- a/fs/f2fs/compress.c +++ b/fs/f2fs/compress.c @@ -1809,7 +1809,7 @@ static void f2fs_put_dic(struct decompress_io_ctx *dic, bool in_task) f2fs_free_dic(dic, false); } else { INIT_WORK(&dic->free_work, f2fs_late_free_dic); - queue_work(dic->sbi->post_read_wq, &dic->free_work); + queue_work(dic->sbi->wq, &dic->free_work); } } } diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index 9bddc8bf0af6..64250e6a94d1 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -342,7 +342,7 @@ static void f2fs_read_end_io(struct bio *bio) f2fs_handle_step_decompress(ctx, intask); } else if (enabled_steps) { INIT_WORK(&ctx->work, f2fs_post_read_work); - queue_work(ctx->sbi->post_read_wq, &ctx->work); + queue_work(ctx->sbi->wq, &ctx->work); return; } } @@ -4527,23 +4527,17 @@ void f2fs_destroy_post_read_processing(void) kmem_cache_destroy(bio_post_read_ctx_cache); } -int f2fs_init_post_read_wq(struct f2fs_sb_info *sbi) +int f2fs_init_wq(struct f2fs_sb_info *sbi) { - if (!f2fs_sb_has_encrypt(sbi) && - !f2fs_sb_has_verity(sbi) && - !f2fs_sb_has_compression(sbi)) - return 0; - - sbi->post_read_wq = alloc_workqueue("f2fs_post_read_wq", - WQ_UNBOUND | WQ_HIGHPRI, - num_online_cpus()); - return sbi->post_read_wq ? 0 : -ENOMEM; + sbi->wq = alloc_workqueue("f2fs_wq", WQ_UNBOUND | WQ_HIGHPRI, + num_online_cpus()); + return sbi->wq ? 0 : -ENOMEM; } -void f2fs_destroy_post_read_wq(struct f2fs_sb_info *sbi) +void f2fs_destroy_wq(struct f2fs_sb_info *sbi) { - if (sbi->post_read_wq) - destroy_workqueue(sbi->post_read_wq); + if (sbi->wq) + destroy_workqueue(sbi->wq); } int __init f2fs_init_bio_entry_cache(void) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 086bc5243979..8f3e632f315c 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -1979,7 +1979,7 @@ struct f2fs_sb_info { /* Precomputed FS UUID checksum for seeding other checksums */ __u32 s_chksum_seed; - struct workqueue_struct *post_read_wq; /* post read workqueue */ + struct workqueue_struct *wq; /* bio completion workqueue */ /* * If we are in irq context, let's update error information into @@ -4214,8 +4214,8 @@ bool f2fs_overwrite_io(struct inode *inode, loff_t pos, size_t len); void f2fs_clear_page_cache_dirty_tag(struct folio *folio); int f2fs_init_post_read_processing(void); void f2fs_destroy_post_read_processing(void); -int f2fs_init_post_read_wq(struct f2fs_sb_info *sbi); -void f2fs_destroy_post_read_wq(struct f2fs_sb_info *sbi); +int f2fs_init_wq(struct f2fs_sb_info *sbi); +void f2fs_destroy_wq(struct f2fs_sb_info *sbi); extern const struct iomap_ops f2fs_iomap_ops; /* diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index b277807c8185..438616597e53 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -2075,7 +2075,7 @@ static void f2fs_put_super(struct super_block *sb) /* flush s_error_work before sbi destroy */ flush_work(&sbi->s_error_work); - f2fs_destroy_post_read_wq(sbi); + f2fs_destroy_wq(sbi); kvfree(sbi->ckpt); @@ -5189,9 +5189,9 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc) goto free_devices; } - err = f2fs_init_post_read_wq(sbi); + err = f2fs_init_wq(sbi); if (err) { - f2fs_err(sbi, "Failed to initialize post read workqueue"); + f2fs_err(sbi, "Failed to create workqueue"); goto free_devices; } @@ -5478,7 +5478,7 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc) f2fs_stop_ckpt_thread(sbi); /* flush s_error_work before sbi destroy */ flush_work(&sbi->s_error_work); - f2fs_destroy_post_read_wq(sbi); + f2fs_destroy_wq(sbi); free_devices: destroy_device_list(sbi); kvfree(sbi->ckpt); From bdc7cfd780c79228099674d419de93be25971ff9 Mon Sep 17 00:00:00 2001 From: Bart Van Assche Date: Wed, 10 Jun 2026 12:34:19 -0700 Subject: [PATCH 31/42] f2fs: Split f2fs_write_end_io() Prepare for running most of the write completion work asynchronously. Reviewed-by: Chao Yu Signed-off-by: Bart Van Assche Signed-off-by: Jaegeuk Kim --- fs/f2fs/data.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index 64250e6a94d1..a765fda71536 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -350,14 +350,11 @@ static void f2fs_read_end_io(struct bio *bio) f2fs_verify_and_finish_bio(bio, intask); } -static void f2fs_write_end_io(struct bio *bio) +static void f2fs_write_end_bio(struct bio *bio) { - struct f2fs_sb_info *sbi; + struct f2fs_sb_info *sbi = bio->bi_private; struct folio_iter fi; - iostat_update_and_unbind_ctx(bio); - sbi = bio->bi_private; - if (time_to_inject(sbi, FAULT_WRITE_IO)) bio->bi_status = BLK_STS_IOERR; @@ -414,6 +411,13 @@ static void f2fs_write_end_io(struct bio *bio) bio_put(bio); } +static void f2fs_write_end_io(struct bio *bio) +{ + iostat_update_and_unbind_ctx(bio); + + f2fs_write_end_bio(bio); +} + #ifdef CONFIG_BLK_DEV_ZONED static void f2fs_zone_write_end_io(struct bio *bio) { From ccaba785821970f422c47770331c7e3271763f17 Mon Sep 17 00:00:00 2001 From: Zhaoyang Huang Date: Mon, 8 Jun 2026 17:09:39 +0800 Subject: [PATCH 32/42] Revert "f2fs: remove non-uptodate folio from the page cache in move_data_block" This reverts commit 9609dd704725a40cd63d915f2ab6c44248a44598. The kernel panics are keeping to be reported especially when the f2fs partition get almost full. By investigation, we find that the reason is one f2fs page got freed to buddy without being deleted from LRU and the root cause is the race happened in [2] which is enrolled by this commit. There are 3 race processes in this scenario, please find below for their main activities. The changed code in move_data_block() lets the GC path evict the tail-end folio from the page cache through folio_end_dropbehind(). Once folio_unmap_invalidate() removes the folio from mapping->i_pages, the page-cache references for all pages in the folio are dropped. The folio is then kept alive only by temporary external references, which allows a later split to operate on a folio whose subpages are no longer protected by page-cache references. After the page-cache references are gone, split_folio_to_order() can split the big folio into individual pages and put the resulting subpages back on the LRU. For tail pages beyond EOF, split removes them from the page cache and drops their page-cache references. A tail page can then remain on the LRU with PG_lru set while holding only the split caller's temporary reference. When free_folio_and_swap_cache() drops that final reference, the page enters the final folio_put() release path. In parallel, folio_isolate_lru() can observe the same tail page with a non-zero refcount and PG_lru set. It clears PG_lru before taking its own reference. If this races with the final folio_put() from the split path, __folio_put() sees PG_lru already cleared and skips lruvec_del_folio(). The page is then freed back to the allocator while its lru links are still present in the LRU list. A later LRU operation on a neighboring page detects the stale link and reports list corruption. [1] [ 22.486082] list_del corruption. next->prev should be fffffffec10e0ac8, but was dead000000000122. (next=fffffffec10e0a88) [ 22.486130] ------------[ cut here ]------------ [ 22.486134] kernel BUG at lib/list_debug.c:67! [ 22.486141] Internal error: Oops - BUG: 00000000f2000800 [#1] SMP [ 22.488502] Tainted: [W]=WARN, [O]=OOT_MODULE [ 22.488506] Hardware name: Spreadtrum UMS9230 1H10 SoC (DT) [ 22.488511] pstate: 604000c5 (nZCv daIF +PAN -UAO -TCO -DIT -SSBS BTYPE=--) [ 22.488517] pc : __list_del_entry_valid_or_report+0x14c/0x154 [ 22.488531] lr : __list_del_entry_valid_or_report+0x14c/0x154 [ 22.488539] sp : ffffffc08006b830 [ 22.488542] x29: ffffffc08006b868 x28: 0000000000003020 x27: 0000000000000000 [ 22.488553] x26: 0000000000000000 x25: 0000000000000004 x24: fffffffec10e0ac0 [ 22.488564] x23: 00000000000000e8 x22: 0000000000000024 x21: dead000000000122 [ 22.488574] x20: fffffffec10e0a88 x19: fffffffec10e0ac8 x18: ffffffc080061060 [ 22.488585] x17: 20747562202c3863 x16: 6130653031636566 x15: 0000000000000058 [ 22.488595] x14: 0000000000000004 x13: ffffff80f91e0000 x12: 0000000000000003 [ 22.488605] x11: 0000000000000003 x10: 0000000000000001 x9 : ffe85721f0e25f00 [ 22.488615] x8 : ffe85721f0e25f00 x7 : 0000000000000000 x6 : 6c65645f7473696c [ 22.488625] x5 : ffffffed39b23026 x4 : 0000000000000000 x3 : 0000000000000010 [ 22.488636] x2 : 0000000000000000 x1 : 0000000000000000 x0 : 000000000000006d [ 22.488647] Call trace: [ 22.488651] __list_del_entry_valid_or_report+0x14c/0x154 (P) [ 22.488661] __folio_put+0x2bc/0x434 [ 22.488670] folio_put+0x28/0x58 [ 22.488678] do_garbage_collect+0x1a34/0x2584 [ 22.488689] f2fs_gc+0x230/0x9b4 [ 22.488697] f2fs_fallocate+0xb90/0xdf4 [ 22.488706] vfs_fallocate+0x1b4/0x2bc [ 22.488716] __arm64_sys_fallocate+0x44/0x78 [ 22.488725] invoke_syscall+0x58/0xe4 [ 22.488732] do_el0_svc+0x48/0xdc [ 22.488739] el0_svc+0x3c/0x98 [ 22.488747] el0t_64_sync_handler+0x20/0x130 [ 22.488754] el0t_64_sync+0x1c4/0x1c8 [2] CPU0 (f2fs GC) CPU1 (split_folio_to_order) CPU2 (folio_isolate_lru) F: pagecache refs = n F: extra refs = GC + split F: PG_lru set move_data_block() folio = f2fs_grab_cache_folio(F) ... __folio_set_dropbehind(F) folio_unlock(F) folio_end_dropbehind(F) folio_unmap_invalidate(F) __filemap_remove_folio(F) folio_put_refs(F, n) folio_put(F) split_folio_to_order(F) folio_ref_freeze(F, 1) ... lru_add_split_folio(T) list_add_tail(&T->lru, &F->lru) folio_set_lru(T) __filemap_remove_folio(T) folio_put_refs(T, 1) /* T refcount == 1, PageLRU set */ folio_isolate_lru(T) folio_test_clear_lru(T) free_folio_and_swap_cache(T) folio_put(T) /* refcount: 1 -> 0 */ __folio_put(T) __page_cache_release(T) folio_test_lru(T) == false /* skip lruvec_del_folio(T) */ free_frozen_pages(T) folio_get(T) lruvec_del_folio(T) later: list_del(adjacent->lru) next == &T->lru next->prev == LIST_POISON / PCP freelist BUG Cc: stable@vger.kernel.org Fixes: 9609dd704725 ("f2fs: remove non-uptodate folio from the page cache in move_data_block") Signed-off-by: Zhaoyang Huang Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/gc.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index 69e0a867219d..56a1c0547d76 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -1499,11 +1499,7 @@ static int move_data_block(struct inode *inode, block_t bidx, put_out: f2fs_put_dnode(&dn); out: - if (!folio_test_uptodate(folio)) - __folio_set_dropbehind(folio); - folio_unlock(folio); - folio_end_dropbehind(folio); - folio_put(folio); + f2fs_folio_put(folio, true); out_iput: if (atomic_inode) iput(atomic_inode); From c4810ada31e80cbe4011467c4f3b1e93f94134f3 Mon Sep 17 00:00:00 2001 From: Zhang Cen Date: Mon, 15 Jun 2026 15:19:54 +0800 Subject: [PATCH 33/42] f2fs: validate ACL entry sizes in f2fs_acl_from_disk() f2fs_acl_count() only validates the aggregate ACL xattr length. A malformed ACL can still place ACL_USER or ACL_GROUP in a slot that only contains struct f2fs_acl_entry_short bytes, and f2fs_acl_from_disk() then reads entry->e_id before verifying that a full entry fits. Require a short entry before reading e_tag and e_perm, and require a full entry before reading e_id for ACL_USER and ACL_GROUP. Return -EFSCORRUPTED from these new truncated-entry checks, while keeping the pre-existing -EINVAL paths unchanged. Validation reproduced this kernel report: KASAN slab-out-of-bounds in __f2fs_get_acl+0x6fb/0x7e0 RIP: 0033:0x7f4b835ea7aa The buggy address belongs to the object at ffff888114589960 which belongs to the cache kmalloc-8 of size 8 The buggy address is located 0 bytes to the right of allocated 8-byte region [ffff888114589960, ffff888114589968) Read of size 4 Call trace: dump_stack_lvl+0x66/0xa0 (?:?) print_report+0xce/0x630 (?:?) __f2fs_get_acl+0x6fb/0x7e0 (fs/f2fs/acl.c:169) srso_alias_return_thunk+0x5/0xfbef5 (?:?) __virt_addr_valid+0x224/0x430 (?:?) kasan_report+0xe0/0x110 (?:?) __f2fs_get_acl+0x5/0x7e0 (fs/f2fs/acl.c:169) __get_acl+0x281/0x380 (?:?) vfs_get_acl+0x10b/0x190 (?:?) do_get_acl+0x2a/0x410 (?:?) do_get_acl+0x9/0x410 (?:?) do_getxattr+0xe8/0x260 (?:?) filename_getxattr+0xd1/0x140 (?:?) do_getname+0x2d/0x2d0 (?:?) path_getxattrat+0x16c/0x200 (?:?) lock_release+0xc8/0x290 (?:?) cgroup_update_frozen+0x9d/0x320 (?:?) lockdep_hardirqs_on_prepare+0xea/0x1a0 (?:?) trace_hardirqs_on+0x1a/0x170 (?:?) _raw_spin_unlock_irq+0x28/0x50 (?:?) do_syscall_64+0x115/0x6a0 (arch/x86/entry/syscall_64.c:87) entry_SYSCALL_64_after_hwframe+0x77/0x7f (?:?) Cc: stable@kernel.org Fixes: af48b85b8cd3 ("f2fs: add xattr and acl functionalities") Assisted-by: Codex:gpt-5.5 Signed-off-by: Zhang Cen Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/acl.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/fs/f2fs/acl.c b/fs/f2fs/acl.c index fa8d81a30fb9..d3253549173e 100644 --- a/fs/f2fs/acl.c +++ b/fs/f2fs/acl.c @@ -47,6 +47,7 @@ static inline int f2fs_acl_count(size_t size) static struct posix_acl *f2fs_acl_from_disk(const char *value, size_t size) { int i, count; + int err = -EINVAL; struct posix_acl *acl; struct f2fs_acl_header *hdr = (struct f2fs_acl_header *)value; struct f2fs_acl_entry *entry = (struct f2fs_acl_entry *)(hdr + 1); @@ -70,8 +71,11 @@ static struct posix_acl *f2fs_acl_from_disk(const char *value, size_t size) for (i = 0; i < count; i++) { - if ((char *)entry > end) + if (unlikely((char *)entry + + sizeof(struct f2fs_acl_entry_short) > end)) { + err = -EFSCORRUPTED; goto fail; + } acl->a_entries[i].e_tag = le16_to_cpu(entry->e_tag); acl->a_entries[i].e_perm = le16_to_cpu(entry->e_perm); @@ -86,6 +90,11 @@ static struct posix_acl *f2fs_acl_from_disk(const char *value, size_t size) break; case ACL_USER: + if (unlikely((char *)entry + + sizeof(struct f2fs_acl_entry) > end)) { + err = -EFSCORRUPTED; + goto fail; + } acl->a_entries[i].e_uid = make_kuid(&init_user_ns, le32_to_cpu(entry->e_id)); @@ -93,6 +102,11 @@ static struct posix_acl *f2fs_acl_from_disk(const char *value, size_t size) sizeof(struct f2fs_acl_entry)); break; case ACL_GROUP: + if (unlikely((char *)entry + + sizeof(struct f2fs_acl_entry) > end)) { + err = -EFSCORRUPTED; + goto fail; + } acl->a_entries[i].e_gid = make_kgid(&init_user_ns, le32_to_cpu(entry->e_id)); @@ -108,7 +122,7 @@ static struct posix_acl *f2fs_acl_from_disk(const char *value, size_t size) return acl; fail: posix_acl_release(acl); - return ERR_PTR(-EINVAL); + return ERR_PTR(err); } static void *f2fs_acl_to_disk(struct f2fs_sb_info *sbi, From 378acf3cf19b6af6cba55e8dd1154c4e1504bae8 Mon Sep 17 00:00:00 2001 From: Bryam Vargas Date: Thu, 11 Jun 2026 23:00:36 -0500 Subject: [PATCH 34/42] f2fs: bound i_inline_xattr_size for non-inline-xattr inodes When the flexible_inline_xattr feature is enabled, do_read_inode() loads the on-disk i_inline_xattr_size unconditionally: if (f2fs_sb_has_flexible_inline_xattr(sbi)) fi->i_inline_xattr_size = le16_to_cpu(ri->i_inline_xattr_size); but sanity_check_inode() only range-checks it when the inode also has the FI_INLINE_XATTR flag set. An inode that carries an inline dentry or inline data but not FI_INLINE_XATTR -- the normal layout for an inline directory -- therefore keeps a fully attacker-controlled i_inline_xattr_size from a crafted image. get_inline_xattr_addrs() returns that value with no flag gating, so it feeds the inode geometry: MAX_INLINE_DATA() = 4 * (CUR_ADDRS_PER_INODE - i_inline_xattr_size - 1) NR_INLINE_DENTRY() = MAX_INLINE_DATA() * BITS_PER_BYTE / (...) addrs_per_page() = CUR_ADDRS_PER_INODE - i_inline_xattr_size A large i_inline_xattr_size drives MAX_INLINE_DATA() and NR_INLINE_DENTRY() negative, so make_dentry_ptr_inline() sets d->max (int) to a negative value. The inline directory walk then compares an unsigned long bit_pos against that negative d->max, which is promoted to a huge unsigned bound, and reads far past the inline area: while (bit_pos < d->max) /* fs/f2fs/dir.c */ ... test_bit_le(bit_pos, d->bitmap) / d->dentry[bit_pos] ... Mounting a crafted image and reading such a directory triggers an out-of-bounds read in f2fs_fill_dentries(); the same underflow also corrupts ADDRS_PER_INODE for regular files. Validate i_inline_xattr_size against MAX_INLINE_XATTR_SIZE whenever the flexible_inline_xattr feature is enabled -- i.e. whenever the value is loaded from disk and consumed -- and keep the lower MIN_INLINE_XATTR_SIZE bound gated on inodes that actually carry an inline xattr, so legitimate inodes with i_inline_xattr_size == 0 are still accepted. Cc: stable@vger.kernel.org Fixes: 6afc662e68b5 ("f2fs: support flexible inline xattr size") Signed-off-by: Bryam Vargas Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/inode.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/f2fs/inode.c b/fs/f2fs/inode.c index 25f30b8eadc5..c95e0b126da4 100644 --- a/fs/f2fs/inode.c +++ b/fs/f2fs/inode.c @@ -325,9 +325,9 @@ static bool sanity_check_inode(struct inode *inode, struct folio *node_folio) } if (f2fs_sb_has_flexible_inline_xattr(sbi) && - f2fs_has_inline_xattr(inode) && - (fi->i_inline_xattr_size < MIN_INLINE_XATTR_SIZE || - fi->i_inline_xattr_size > MAX_INLINE_XATTR_SIZE)) { + (fi->i_inline_xattr_size > MAX_INLINE_XATTR_SIZE || + (f2fs_has_inline_xattr(inode) && + fi->i_inline_xattr_size < MIN_INLINE_XATTR_SIZE))) { f2fs_warn(sbi, "%s: inode (ino=%llx) has corrupted i_inline_xattr_size: %d, min: %zu, max: %lu", __func__, inode->i_ino, fi->i_inline_xattr_size, MIN_INLINE_XATTR_SIZE, MAX_INLINE_XATTR_SIZE); From 98fd20b9cf472d7e0518517ea9e587a9a2b8b311 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 15 Jun 2026 21:08:17 +0800 Subject: [PATCH 35/42] f2fs: fix wrong description in printed log This patch fixes wrong description in printed log: "SSA and SIT" -> "SIT and SSA" Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/gc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index 56a1c0547d76..ffaa7ba76a1b 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -1893,7 +1893,7 @@ static int do_garbage_collect(struct f2fs_sb_info *sbi, sum = SUM_BLK_PAGE_ADDR(sbi, sum_folio, cur_segno); if (type != GET_SUM_TYPE(sum_footer(sbi, sum))) { f2fs_err(sbi, "Inconsistent segment (%u) type " - "[%d, %d] in SSA and SIT", + "[%d, %d] in SIT and SSA", cur_segno, type, GET_SUM_TYPE( sum_footer(sbi, sum))); From d27e4431023770249a341aeb94cb40f4ff12b21e Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 15 Jun 2026 21:08:18 +0800 Subject: [PATCH 36/42] f2fs: misc cleanup in f2fs_record_stop_reason() Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/super.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index 438616597e53..2b8d96411156 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -4641,8 +4641,7 @@ static void f2fs_record_stop_reason(struct f2fs_sb_info *sbi) spin_lock_irqsave(&sbi->error_lock, flags); if (sbi->error_dirty) { - memcpy(F2FS_RAW_SUPER(sbi)->s_errors, sbi->errors, - MAX_F2FS_ERRORS); + memcpy(raw_super->s_errors, sbi->errors, MAX_F2FS_ERRORS); sbi->error_dirty = false; } memcpy(raw_super->s_stop_reason, sbi->stop_reason, MAX_STOP_REASON); From cf716276b0dca934aad5fe3c46df04e1dc596734 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 15 Jun 2026 21:08:19 +0800 Subject: [PATCH 37/42] f2fs: avoid unnecessary sanity check on ckpt_valid_blocks The calculation of sec->ckpt_valid_blocks are the same in both set_ckpt_valid_blocks() and sanity_check_valid_blocks(), so it doesn't necessary to call sanity_check_valid_blocks() right after set_ckpt_valid_blocks(). Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/segment.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 993555042876..d71ddb3ee918 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -4773,10 +4773,8 @@ void f2fs_flush_sit_entries(struct f2fs_sb_info *sbi, struct cp_control *cpc) } /* update ckpt_valid_block */ - if (__is_large_section(sbi)) { + if (__is_large_section(sbi)) set_ckpt_valid_blocks(sbi, segno); - sanity_check_valid_blocks(sbi, segno); - } __clear_bit(segno, bitmap); sit_i->dirty_sentries--; @@ -5090,10 +5088,8 @@ static int build_sit_entries(struct f2fs_sb_info *sbi) if (__is_large_section(sbi)) { unsigned int segno; - for (segno = 0; segno < MAIN_SEGS(sbi); segno += SEGS_PER_SEC(sbi)) { + for (segno = 0; segno < MAIN_SEGS(sbi); segno += SEGS_PER_SEC(sbi)) set_ckpt_valid_blocks(sbi, segno); - sanity_check_valid_blocks(sbi, segno); - } } if (err) From 8b938ae6f0766559dfc4ad5acac958b1eff8664d Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 15 Jun 2026 21:08:20 +0800 Subject: [PATCH 38/42] f2fs: avoid unnecessary fscrypt_finalize_bounce_page() fscrypt_finalize_bounce_page() should be called only if we use fs layer crypto, let's avoid unnecessary fscrypt_finalize_bounce_page() in error path of f2fs_write_compressed_pages(). BTW, fscrypt_finalize_bounce_page() will check mapping of bounced page before retrieving original page, so, previously it won't cause any issue w/ fscrypt_finalize_bounce_page(), but still we'd better avoid coupling w/ any logic inside fscrypt_finalize_bounce_page(). Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/compress.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/f2fs/compress.c b/fs/f2fs/compress.c index 372e07c58e21..6e1c0ea9f6c9 100644 --- a/fs/f2fs/compress.c +++ b/fs/f2fs/compress.c @@ -1455,6 +1455,9 @@ static int f2fs_write_compressed_pages(struct compress_ctx *cc, out_destroy_crypt: page_array_free(sbi, cic->rpages, cc->cluster_size); + if (!fio.encrypted) + goto out_put_cic; + for (--i; i >= 0; i--) { if (!cc->cpages[i]) continue; From 70210492be5ac8e4f42b383b75dfa11810afab86 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 15 Jun 2026 21:08:22 +0800 Subject: [PATCH 39/42] f2fs: remove unneeded f2fs_is_compressed_page() We have checked f2fs_is_compressed_page() before f2fs_compress_write_end_io(), so we don't need to check the status again, remove it. Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/compress.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/f2fs/compress.c b/fs/f2fs/compress.c index 6e1c0ea9f6c9..91855d91bbdd 100644 --- a/fs/f2fs/compress.c +++ b/fs/f2fs/compress.c @@ -1487,8 +1487,7 @@ void f2fs_compress_write_end_io(struct bio *bio, struct folio *folio) struct page *page = &folio->page; struct f2fs_sb_info *sbi = bio->bi_private; struct compress_io_ctx *cic = folio->private; - enum count_type type = WB_DATA_TYPE(folio, - f2fs_is_compressed_page(folio)); + enum count_type type = WB_DATA_TYPE(folio, true); int i; if (unlikely(bio->bi_status != BLK_STS_OK)) From 34636c6dcd6f75570c553a4188b5dbe0f758159a Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Tue, 16 Jun 2026 11:06:55 +0800 Subject: [PATCH 40/42] f2fs: skip direct I/O iostat context when disabled F2FS iostat is optional and is disabled by default. Direct I/O still allocates and binds a bio_iostat_ctx, updates the submit timestamp, and replaces bi_end_io for every DIO bio even when sbi->iostat_enable is false. The byte accounting calls do not need an extra guard because f2fs_update_iostat() already checks sbi->iostat_enable. Only skip the DIO bio context setup when iostat is disabled. If iostat is enabled through sysfs before submission, the existing context allocation and latency accounting path is still used. QEMU benchmark on a 1GiB F2FS virtio-blk image, with iostat_enable=0, 4KiB O_DIRECT I/O over a 64MiB file, 50000 iterations per run: baseline patched direct_read median 65264.50 ns 55470.95 ns direct_read recheck 65553.75 ns 55470.95 ns direct_write median 68054.62 ns 56309.44 ns direct_write recheck 66873.51 ns 56309.44 ns Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index d240ca78a31f..8acdd94272a0 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -4801,6 +4801,9 @@ static void f2fs_dio_iostat_start(struct f2fs_sb_info *sbi, struct bio *bio) { void *bi_private = bio->bi_private; + if (!sbi->iostat_enable) + return; + iostat_alloc_and_bind_ctx(sbi, bio, bi_private); iostat_update_submit_ctx(bio, DATA); bio->bi_end_io = f2fs_dio_end_bio; From 5ef5bc304f23c3fe255d4936472378dcb74d0e94 Mon Sep 17 00:00:00 2001 From: Keshav Verma Date: Mon, 22 Jun 2026 20:44:21 +0530 Subject: [PATCH 41/42] f2fs: fix listxattr handling of corrupted xattr entries Validate the xattr entry before reading its fields in f2fs_listxattr(). Return -EFSCORRUPTED when the entry is outside the valid xattr storage area instead of returning a successful partial result. Fixes: 688078e7f36c ("f2fs: fix to avoid memory leakage in f2fs_listxattr") Cc: stable@kernel.org Reviewed-by: Chao Yu Signed-off-by: Keshav Verma Signed-off-by: Jaegeuk Kim --- fs/f2fs/xattr.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/f2fs/xattr.c b/fs/f2fs/xattr.c index 24cef7e1f56a..ed33e5110f2a 100644 --- a/fs/f2fs/xattr.c +++ b/fs/f2fs/xattr.c @@ -583,8 +583,6 @@ ssize_t f2fs_listxattr(struct dentry *dentry, char *buffer, size_t buffer_size) size_t prefix_len; size_t size; - prefix = f2fs_xattr_prefix(entry->e_name_index, dentry); - if ((void *)(entry) + sizeof(__u32) > last_base_addr || (void *)XATTR_NEXT_ENTRY(entry) > last_base_addr) { f2fs_err(F2FS_I_SB(inode), "list inode (%llu) has corrupted xattr", @@ -594,9 +592,11 @@ ssize_t f2fs_listxattr(struct dentry *dentry, char *buffer, size_t buffer_size) ERROR_CORRUPTED_XATTR); fserror_report_file_metadata(inode, -EFSCORRUPTED, GFP_NOFS); - break; + error = -EFSCORRUPTED; + goto cleanup; } + prefix = f2fs_xattr_prefix(entry->e_name_index, dentry); if (!prefix) continue; From 4275b59673eb60b02eec3997816c83f1f4b909c4 Mon Sep 17 00:00:00 2001 From: Sunmin Jeong Date: Mon, 22 Jun 2026 14:28:17 +0900 Subject: [PATCH 42/42] f2fs: fix to round down start offset of fallocate for pin file Currently, the length of fallocate for pin file is section-aligned to keep allocated sections from being selected as victims of GC. However, for the case that the start offset of fallocate is not aligned in section, the allocated sections can't be fully utilized. It's because a new section is allocated by f2fs_allocate_pinning_section() after using blks_per_sec blocks regardless of the start offset. As a result, several unexpected dirty segments may be created, including blocks assigned to the pinned file. To address this issue, let's round down the start offset of fallocate to the length of section. The reproducing scenario is as below chunk=$(((2<<20)+4096)) # 2MB + 4KB touch test f2fs_io pinfile set test f2fs_io fallocate 0 0 $chunk test f2fs_io fallocate 0 $chunk $chunk test f2fs_io fallocate 0 $((chunk*2)) $chunk test f2fs_io fiemap 0 $((chunk*3)) test Fiemap: offset = 0 len = 12288 logical addr. physical addr. length flags 0 0000000000000000 000000068c600000 0000000000400000 00001088 1 0000000000400000 000000003d400000 0000000000001000 00001088 2 0000000000401000 00000003eb200000 0000000000200000 00001088 3 0000000000601000 00000005e4200000 0000000000001000 00001088 4 0000000000602000 0000000605400000 0000000000200000 00001089 Cc: stable@vger.kernel.org Fixes: f5a53edcf01e ("f2fs: support aligned pinned file") Reviewed-by: Yunji Kang Reviewed-by: Yeongjin Gil Reviewed-by: Sungjong Seo Signed-off-by: Sunmin Jeong Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 8acdd94272a0..4b52c56d71f0 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -1916,8 +1916,15 @@ static int f2fs_expand_inode_data(struct inode *inode, loff_t offset, if (f2fs_is_pinned_file(inode)) { block_t sec_blks = CAP_BLKS_PER_SEC(sbi); - block_t sec_len = roundup(map.m_len, sec_blks); + block_t sec_len; + if (map.m_lblk % sec_blks) { + map.m_lblk = rounddown(map.m_lblk, sec_blks); + map.m_len = pg_end - map.m_lblk; + if (off_end) + map.m_len++; + } + sec_len = roundup(map.m_len, sec_blks); map.m_len = sec_blks; next_alloc: f2fs_down_write(&sbi->pin_sem);