From 35e5554ec14b12d31886d03de1e9fbbb13073564 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Tue, 7 Jul 2026 19:39:34 +0800 Subject: [PATCH 01/51] f2fs: drop FGP_NOFS in f2fs_write_begin() This patch proposes to drop FGP_NOFS from f2fs_filemap_get_folio() in f2fs_write_begin(), I don't see there is potential deadlock issue when __filemap_get_folio() calling into filesystem reclaim interfaces, e.g. .writepages, evict_inode, shrinker. Cc: Matthew Wilcox Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/data.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index a765fda71536..62ea74e65db8 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -3975,7 +3975,7 @@ static int f2fs_write_begin(const struct kiocb *iocb, * Will wait that below with our IO control. */ folio = f2fs_filemap_get_folio(mapping, index, - FGP_LOCK | FGP_WRITE | FGP_CREAT | FGP_NOFS, + FGP_LOCK | FGP_WRITE | FGP_CREAT, mapping_gfp_mask(mapping)); if (IS_ERR(folio)) { err = PTR_ERR(folio); From b41d9ff965f70f88b2c33089fdb26865914ca92d Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Tue, 7 Jul 2026 19:39:33 +0800 Subject: [PATCH 02/51] f2fs: quota: do not use GFP_NOFS in f2fs_quota_read() Commit 02117b8ae9c0 ("f2fs: Set GF_NOFS in read_cache_page_gfp while doing f2fs_quota_read") adds GFP_NOFS in f2fs_quota_read() to avoid below deadlock: - do_sys_open - vfs_open - dquot_file_open - dquot_initialize - dqget - dquot_acquire : locks &dqopt->dqio_mutex (VFS Quota Mutex) - qtree_read_dquot - f2fs_quota_read - read_mapping_page (GFP_KERNEL / allows GFP_FS) - __alloc_pages_nodemask - try_to_free_pages (Direct Reclaim) - prune_icache_sb - evict - f2fs_evict_inode - dquot_drop - dqput - dquot_commit : tries to lock &dqopt->dqio_mutex again ==> DEADLOCK (waiting for itself) As Jan Kara mentioned, quota system has fixed this issue w/ commit 537e11cdc7a6 ("quota: Prevent memory allocation recursion while holding dq_lock"), so this GFP_NOFS flag should be relic, let's use GFP_KERNEL instead. Cc: Jan Kara Cc: Matthew Wilcox Signed-off-by: Chao Yu Reviewed-by: Jan Kara Signed-off-by: Jaegeuk Kim --- fs/f2fs/super.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index 9760e4efeffe..f41a23828508 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -3168,7 +3168,7 @@ static ssize_t f2fs_quota_read(struct super_block *sb, int type, char *data, repeat: folio = mapping_read_folio_gfp(mapping, off >> PAGE_SHIFT, - GFP_NOFS); + GFP_KERNEL); if (IS_ERR(folio)) { if (PTR_ERR(folio) == -ENOMEM) { memalloc_retry_wait(GFP_NOFS); From 3d7bca9d583793bb7d0bac0d95a24ddd2e129eed Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 6 Jul 2026 12:33:21 +0000 Subject: [PATCH 03/51] f2fs: embed f2fs_gc_kthread in f2fs_sb_info Instead of allocating f2fs_gc_kthread dynamically, embed it in f2fs_sb_info. This simplifies lifetime management and prepares for fixing race conditions during teardown. - __sbi_store - remount|shutdown - f2fs_stop_gc_thread - access sbi->gc_thread - sbi->gc_thread = NULL - access sbi->gc_thread->f2fs_gc_task Fixes: 52190933c37a ("f2fs: sysfs: introduce critical_task_priority") Fixes: 7950e9ac638e ("f2fs: stop gc/discard thread after fs shutdown") Cc: stable@kernel.org Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/debug.c | 4 ---- fs/f2fs/f2fs.h | 29 ++++++++++++++++++++++++++++- fs/f2fs/gc.c | 36 +++++++++++++++--------------------- fs/f2fs/gc.h | 27 +-------------------------- fs/f2fs/segment.c | 9 ++++----- fs/f2fs/super.c | 4 ++-- fs/f2fs/sysfs.c | 22 +++++++++++----------- 7 files changed, 61 insertions(+), 70 deletions(-) diff --git a/fs/f2fs/debug.c b/fs/f2fs/debug.c index af88db8fdb71..ff379aff4472 100644 --- a/fs/f2fs/debug.c +++ b/fs/f2fs/debug.c @@ -352,10 +352,6 @@ static void update_mem_info(struct f2fs_sb_info *sbi) get_cache: si->cache_mem = 0; - /* build gc */ - if (sbi->gc_thread) - si->cache_mem += sizeof(struct f2fs_gc_kthread); - /* build merge flush thread */ if (SM_I(sbi)->fcc_info) si->cache_mem += sizeof(struct flush_cmd_control); diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 8f3e632f315c..8011bbdf2c68 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -1748,6 +1748,33 @@ struct decompress_io_ctx { #define MAX_COMPRESS_LOG_SIZE 8 #define MAX_COMPRESS_WINDOW_SIZE(log_size) ((PAGE_SIZE) << (log_size)) +struct f2fs_gc_kthread { + struct task_struct *f2fs_gc_task; + wait_queue_head_t gc_wait_queue_head; + + /* for gc sleep time */ + unsigned int urgent_sleep_time; + unsigned int min_sleep_time; + unsigned int max_sleep_time; + unsigned int no_gc_sleep_time; + + /* for changing gc mode */ + bool gc_wake; + + /* for GC_MERGE mount option */ + wait_queue_head_t fggc_wq; /* + * caller of f2fs_balance_fs() + * will wait on this wait queue. + */ + + /* for gc control for zoned devices */ + unsigned int no_zoned_gc_percent; + unsigned int boost_zoned_gc_percent; + unsigned int valid_thresh_ratio; + unsigned int boost_gc_multiple; + unsigned int boost_gc_greedy; +}; + struct f2fs_sb_info { struct super_block *sb; /* pointer to VFS super block */ struct proc_dir_entry *s_proc; /* proc entry */ @@ -1883,7 +1910,7 @@ struct f2fs_sb_info { * semaphore for GC, avoid * race between GC and GC or CP */ - struct f2fs_gc_kthread *gc_thread; /* GC thread */ + struct f2fs_gc_kthread gc_thread; /* GC thread */ struct atgc_management am; /* atgc management */ unsigned int cur_victim_sec; /* current victim section num */ unsigned int gc_mode; /* current GC state */ diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index ffaa7ba76a1b..d04633f872ef 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -31,9 +31,9 @@ static unsigned int count_bits(const unsigned long *addr, static int gc_thread_func(void *data) { struct f2fs_sb_info *sbi = data; - struct f2fs_gc_kthread *gc_th = sbi->gc_thread; - wait_queue_head_t *wq = &sbi->gc_thread->gc_wait_queue_head; - wait_queue_head_t *fggc_wq = &sbi->gc_thread->fggc_wq; + struct f2fs_gc_kthread *gc_th = &sbi->gc_thread; + wait_queue_head_t *wq = &sbi->gc_thread.gc_wait_queue_head; + wait_queue_head_t *fggc_wq = &sbi->gc_thread.fggc_wq; unsigned int wait_ms; struct f2fs_gc_control gc_control = { .victim_segno = NULL_SEGNO, @@ -193,13 +193,9 @@ static int gc_thread_func(void *data) int f2fs_start_gc_thread(struct f2fs_sb_info *sbi) { - struct f2fs_gc_kthread *gc_th; + struct f2fs_gc_kthread *gc_th = &sbi->gc_thread; dev_t dev = sbi->sb->s_bdev->bd_dev; - gc_th = f2fs_kmalloc(sbi, sizeof(struct f2fs_gc_kthread), GFP_KERNEL); - if (!gc_th) - return -ENOMEM; - gc_th->urgent_sleep_time = DEF_GC_THREAD_URGENT_SLEEP_TIME; gc_th->valid_thresh_ratio = DEF_GC_THREAD_VALID_THRESH_RATIO; gc_th->boost_gc_multiple = BOOST_GC_MULTIPLE; @@ -221,16 +217,14 @@ int f2fs_start_gc_thread(struct f2fs_sb_info *sbi) gc_th->gc_wake = false; - sbi->gc_thread = gc_th; - init_waitqueue_head(&sbi->gc_thread->gc_wait_queue_head); - init_waitqueue_head(&sbi->gc_thread->fggc_wq); - sbi->gc_thread->f2fs_gc_task = kthread_run(gc_thread_func, sbi, + init_waitqueue_head(&gc_th->gc_wait_queue_head); + init_waitqueue_head(&gc_th->fggc_wq); + gc_th->f2fs_gc_task = kthread_run(gc_thread_func, sbi, "f2fs_gc-%u:%u", MAJOR(dev), MINOR(dev)); if (IS_ERR(gc_th->f2fs_gc_task)) { int err = PTR_ERR(gc_th->f2fs_gc_task); - kfree(gc_th); - sbi->gc_thread = NULL; + gc_th->f2fs_gc_task = NULL; return err; } @@ -241,14 +235,14 @@ int f2fs_start_gc_thread(struct f2fs_sb_info *sbi) void f2fs_stop_gc_thread(struct f2fs_sb_info *sbi) { - struct f2fs_gc_kthread *gc_th = sbi->gc_thread; + struct f2fs_gc_kthread *gc_th = &sbi->gc_thread; - if (!gc_th) + if (!gc_th->f2fs_gc_task) return; + kthread_stop(gc_th->f2fs_gc_task); + gc_th->f2fs_gc_task = NULL; wake_up_all(&gc_th->fggc_wq); - kfree(gc_th); - sbi->gc_thread = NULL; } static int select_gc_type(struct f2fs_sb_info *sbi, int gc_type) @@ -796,7 +790,7 @@ int f2fs_get_victim(struct f2fs_sb_info *sbi, unsigned int *result, if (one_time) { p.one_time_gc = one_time; if (has_enough_free_secs(sbi, 0, NR_PERSISTENT_LOG)) - valid_thresh_ratio = sbi->gc_thread->valid_thresh_ratio; + valid_thresh_ratio = sbi->gc_thread.valid_thresh_ratio; } retry: @@ -1807,9 +1801,9 @@ static int do_garbage_collect(struct f2fs_sb_info *sbi, if (f2fs_sb_has_blkzoned(sbi) && !has_enough_free_blocks(sbi, - sbi->gc_thread->boost_zoned_gc_percent)) + sbi->gc_thread.boost_zoned_gc_percent)) window_granularity *= - sbi->gc_thread->boost_gc_multiple; + sbi->gc_thread.boost_gc_multiple; end_segno = start_segno + window_granularity; } diff --git a/fs/f2fs/gc.h b/fs/f2fs/gc.h index 6c4d4567571e..b015742fb455 100644 --- a/fs/f2fs/gc.h +++ b/fs/f2fs/gc.h @@ -45,32 +45,7 @@ #define NR_GC_CHECKPOINT_SECS (3) /* data/node/dentry sections */ -struct f2fs_gc_kthread { - struct task_struct *f2fs_gc_task; - wait_queue_head_t gc_wait_queue_head; - /* for gc sleep time */ - unsigned int urgent_sleep_time; - unsigned int min_sleep_time; - unsigned int max_sleep_time; - unsigned int no_gc_sleep_time; - - /* for changing gc mode */ - bool gc_wake; - - /* for GC_MERGE mount option */ - wait_queue_head_t fggc_wq; /* - * caller of f2fs_balance_fs() - * will wait on this wait queue. - */ - - /* for gc control for zoned devices */ - unsigned int no_zoned_gc_percent; - unsigned int boost_zoned_gc_percent; - unsigned int valid_thresh_ratio; - unsigned int boost_gc_multiple; - unsigned int boost_gc_greedy; -}; struct gc_inode_list { struct list_head ilist; @@ -197,6 +172,6 @@ static inline bool need_to_boost_gc(struct f2fs_sb_info *sbi) { if (f2fs_sb_has_blkzoned(sbi)) return !has_enough_free_blocks(sbi, - sbi->gc_thread->boost_zoned_gc_percent); + sbi->gc_thread.boost_zoned_gc_percent); return has_enough_invalid_blocks(sbi); } diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index d71ddb3ee918..0b706568b034 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -452,15 +452,14 @@ void f2fs_balance_fs(struct f2fs_sb_info *sbi, bool need) 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) { + if (test_opt(sbi, GC_MERGE) && sbi->gc_thread.f2fs_gc_task) { DEFINE_WAIT(wait); - prepare_to_wait(&sbi->gc_thread->fggc_wq, &wait, + prepare_to_wait(&sbi->gc_thread.fggc_wq, &wait, TASK_UNINTERRUPTIBLE); - wake_up(&sbi->gc_thread->gc_wait_queue_head); + wake_up(&sbi->gc_thread.gc_wait_queue_head); io_schedule(); - finish_wait(&sbi->gc_thread->fggc_wq, &wait); + finish_wait(&sbi->gc_thread.fggc_wq, &wait); } else { struct f2fs_gc_control gc_control = { .victim_segno = NULL_SEGNO, diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index f41a23828508..90a47cf86378 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -2943,11 +2943,11 @@ static int __f2fs_remount(struct fs_context *fc, struct super_block *sb) if ((flags & SB_RDONLY) || (F2FS_OPTION(sbi).bggc_mode == BGGC_MODE_OFF && !test_opt(sbi, GC_MERGE))) { - if (sbi->gc_thread) { + if (sbi->gc_thread.f2fs_gc_task) { f2fs_stop_gc_thread(sbi); need_restart_gc = true; } - } else if (!sbi->gc_thread) { + } else if (!sbi->gc_thread.f2fs_gc_task) { err = f2fs_start_gc_thread(sbi); if (err) goto restore_opts; diff --git a/fs/f2fs/sysfs.c b/fs/f2fs/sysfs.c index 665687244c93..be92c05a5420 100644 --- a/fs/f2fs/sysfs.c +++ b/fs/f2fs/sysfs.c @@ -75,7 +75,7 @@ static ssize_t f2fs_sbi_show(struct f2fs_attr *a, static unsigned char *__struct_ptr(struct f2fs_sb_info *sbi, int struct_type) { if (struct_type == GC_THREAD) - return (unsigned char *)sbi->gc_thread; + return (unsigned char *)&sbi->gc_thread; else if (struct_type == SM_INFO) return (unsigned char *)SM_I(sbi); else if (struct_type == DCC_INFO) @@ -664,20 +664,20 @@ static ssize_t __sbi_store(struct f2fs_attr *a, sbi->gc_mode = GC_NORMAL; } else if (t == 1) { sbi->gc_mode = GC_URGENT_HIGH; - if (sbi->gc_thread) { - sbi->gc_thread->gc_wake = true; + if (sbi->gc_thread.f2fs_gc_task) { + sbi->gc_thread.gc_wake = true; wake_up_interruptible_all( - &sbi->gc_thread->gc_wait_queue_head); + &sbi->gc_thread.gc_wait_queue_head); wake_up_discard_thread(sbi, true); } } else if (t == 2) { sbi->gc_mode = GC_URGENT_LOW; } else if (t == 3) { sbi->gc_mode = GC_URGENT_MID; - if (sbi->gc_thread) { - sbi->gc_thread->gc_wake = true; + if (sbi->gc_thread.f2fs_gc_task) { + sbi->gc_thread.gc_wake = true; wake_up_interruptible_all( - &sbi->gc_thread->gc_wait_queue_head); + &sbi->gc_thread.gc_wait_queue_head); } } else { return -EINVAL; @@ -934,14 +934,14 @@ static ssize_t __sbi_store(struct f2fs_attr *a, if (!strcmp(a->attr.name, "gc_boost_gc_multiple")) { if (t < 1 || t > SEGS_PER_SEC(sbi)) return -EINVAL; - sbi->gc_thread->boost_gc_multiple = (unsigned int)t; + sbi->gc_thread.boost_gc_multiple = (unsigned int)t; return count; } if (!strcmp(a->attr.name, "gc_boost_gc_greedy")) { if (t > GC_GREEDY) return -EINVAL; - sbi->gc_thread->boost_gc_greedy = (unsigned int)t; + sbi->gc_thread.boost_gc_greedy = (unsigned int)t; return count; } @@ -989,8 +989,8 @@ static ssize_t __sbi_store(struct f2fs_attr *a, if (sbi->cprc_info.f2fs_issue_ckpt) set_user_nice(sbi->cprc_info.f2fs_issue_ckpt, PRIO_TO_NICE(sbi->critical_task_priority)); - if (sbi->gc_thread && sbi->gc_thread->f2fs_gc_task) - set_user_nice(sbi->gc_thread->f2fs_gc_task, + if (sbi->gc_thread.f2fs_gc_task) + set_user_nice(sbi->gc_thread.f2fs_gc_task, PRIO_TO_NICE(sbi->critical_task_priority)); return count; } From d0a481fad5c7a3a56ecf54a099651216869f4d0a Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 29 Jun 2026 11:49:18 +0000 Subject: [PATCH 04/51] f2fs: fix to avoid potential section-unaligned pinfile Blocks of pinfile may not aligned to section size due to wrong use on pinfile, result in heavy overhead of GC, let avoid this by adding additional check condition in f2fs_setattr(). - truncate -s 8mb pinfile : random checkpoint may persist filesize w/ inode - fallocate -o 0 -l 8mb pinfile - f2fs_fallocate - f2fs_expand_inode_data - f2fs_allocate_pinning_section - f2fs_map_blocks - f2fs_map_lock - __allocate_data_block - file_need_truncate : w/ FADVISE_TRUNC_BIT, we can expect unaligned mapping can be truncated while open() if f2fs is not umount abnormally - f2fs_map_unlock : following f2fs checkpoint and sudden power-cut - mount - open pinfile - f2fs_file_open - finish_preallocate_blocks - truncate_setsize : filesize is 8mb - f2fs_truncate : can only truncate block outside filesize, rather than truncating unaligned blocks inside filesize Fixes: f5a53edcf01e ("f2fs: support aligned pinned file") Cc: stable@kernel.org Cc: Daeho Jeong Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 4b52c56d71f0..c54897a25981 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -1107,17 +1107,23 @@ int f2fs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, !IS_ALIGNED(attr->ia_size, F2FS_BLK_TO_BYTES(fi->i_cluster_size))) return -EINVAL; - /* - * To prevent scattered pin block generation, we don't allow - * smaller/equal size unaligned truncation for pinned file. - * We only support overwrite IO to pinned file, so don't - * care about larger size truncation. - */ - if (f2fs_is_pinned_file(inode) && - attr->ia_size <= i_size_read(inode) && - !IS_ALIGNED(attr->ia_size, - F2FS_BLK_TO_BYTES(CAP_BLKS_PER_SEC(sbi)))) - return -EINVAL; + + if (f2fs_is_pinned_file(inode)) { + /* + * It may break section-aligned fallocate recovery + * mechanism, so do not allow larger size truncation. + */ + if (attr->ia_size > i_size_read(inode)) + return -EINVAL; + /* + * To prevent scattered pin block generation, we don't + * allow smaller/equal size unaligned truncation for + * pinned file. + */ + else if (!IS_ALIGNED(attr->ia_size, + F2FS_BLK_TO_BYTES(CAP_BLKS_PER_SEC(sbi)))) + return -EINVAL; + } } if (is_quota_modification(idmap, inode, attr)) { From 0dab71381f1b4d12dc2056f8bd5aaa9d93ce9082 Mon Sep 17 00:00:00 2001 From: Zhaoyang Huang Date: Mon, 13 Jul 2026 19:19:44 +0800 Subject: [PATCH 05/51] f2fs: fix folio_nr_pages() race after put in large folio invalidate Our v6.18 based Android system is continuely suffering livelock and bad page stat as shown in[1] which related to broken xarray slot status. By investigating big folio operations within f2fs, we find below races and fix it by get the nr_pages before drop the refcount and folio_lock. f2fs_get_read_data_folio() calls f2fs_folio_put() before folio_nr_pages() when invalidating a large folio from the page cache. That unlocks the folio and drops the caller reference, leaving a window where a concurrent truncate or folio split can shrink the compound folio or free it before the invalidate range is computed. An undersized range then leaves split sub-folios in mapping->i_pages, which can later interact badly with truncate and reclaim (stale xarray entries and bad page state when folio->mapping no longer matches the mapping being truncated). [1] PID: 2594 TASK: ffffff8169b81580 CPU: 7 COMMAND: "Thread-3" #0 [ffffffc08ef2b8a0] xas_load at ffffffe52d1f42a4 #1 [ffffffc08ef2b900] find_get_entries at ffffffe52c185798 #2 [ffffffc08ef2bb60] truncate_inode_pages_range at ffffffe52c19e83c #3 [ffffffc08ef2bbc0] truncate_inode_pages_final at ffffffe52c19ec2c #4 [ffffffc08ef2bc20] f2fs_evict_inode at ffffffe52c4c8400 #5 [ffffffc08ef2bcc0] evict at ffffffe52c2de9f4 #6 [ffffffc08ef2bd00] iput at ffffffe52c2db1b4 #7 [ffffffc08ef2bd30] dentry_unlink_inode at ffffffe52c2d7204 #8 [ffffffc08ef2bd50] __dentry_kill at ffffffe52c2d3dcc #9 [ffffffc08ef2bd80] dput at ffffffe52c2d3c3c #10 [ffffffc08ef2bda0] __fput at ffffffe52c2b0a7c #11 [ffffffc08ef2bde0] ____fput at ffffffe52c2b1034 #12 [ffffffc08ef2bdf0] task_work_run at ffffffe52beea200 #13 [ffffffc08ef2be20] exit_to_user_mode_loop at ffffffe52bfbc17c #14 [ffffffc08ef2be80] el0_svc at ffffffe52d1f8e54 #15 [ffffffc08ef2beb0] el0t_64_sync_handler at ffffffe52d1f8d10 Cc: stable@kernel.org Fixes: 05e65c14ea59 ("f2fs: support large folio for immutable non-compressed case") Reviewed-by: Chao Yu Signed-off-by: Zhaoyang Huang Signed-off-by: Jaegeuk Kim --- fs/f2fs/data.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index 62ea74e65db8..042ed8ad9cc3 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -1323,10 +1323,11 @@ struct folio *f2fs_get_read_data_folio(struct inode *inode, pgoff_t index, if (folio_test_large(folio)) { pgoff_t folio_index = mapping_align_index(mapping, index); + unsigned long nr_pages = folio_nr_pages(folio); f2fs_folio_put(folio, true); invalidate_inode_pages2_range(mapping, folio_index, - folio_index + folio_nr_pages(folio) - 1); + folio_index + nr_pages - 1); f2fs_schedule_timeout(DEFAULT_SCHEDULE_TIMEOUT); goto retry; } From cca7d3e30bf30333314e31bc70b9a739f1342167 Mon Sep 17 00:00:00 2001 From: Guanghui Yang <3497809730@qq.com> Date: Sun, 19 Jul 2026 16:45:14 +0800 Subject: [PATCH 06/51] f2fs: fix dentry folio leak in find_in_level find_in_level() gets a dentry folio with f2fs_find_data_folio() before calling find_in_block(). If find_in_block() returns an error, the function stores the error in res_folio and breaks out of the loop without dropping the dentry folio. This leaks the folio reference on the find_in_block() error path. Drop the dentry folio before returning the error to the caller. Fixes: 7ad08a58bf67 ("f2fs: Handle casefolding with Encryption") Cc: stable@vger.kernel.org Reviewed-by: Chao Yu Signed-off-by: Guanghui Yang <3497809730@qq.com> Signed-off-by: Jaegeuk Kim --- fs/f2fs/dir.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/f2fs/dir.c b/fs/f2fs/dir.c index a9563f7fcd88..0562ad211351 100644 --- a/fs/f2fs/dir.c +++ b/fs/f2fs/dir.c @@ -320,6 +320,7 @@ static struct f2fs_dir_entry *find_in_level(struct inode *dir, de = find_in_block(dir, dentry_folio, fname, &max_slots, use_hash); if (IS_ERR(de)) { + f2fs_folio_put(dentry_folio, false); *res_folio = ERR_CAST(de); de = NULL; break; From a54ffce4637acb0db8e695188a6c7f99f14c3576 Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Thu, 16 Jul 2026 10:59:08 +0800 Subject: [PATCH 07/51] f2fs: use the mount idmap for the owner check in f2fs_xattr_advise_set() f2fs_xattr_advise_set() calls inode_owner_or_capable() with &nop_mnt_idmap before allowing the "system.advise" xattr to be set, instead of the idmap that the VFS passes to the ->set() handler. f2fs supports idmapped mounts, so on such a mount this checks the caller's fsuid against the unmapped on-disk owner rather than the mapped owner: the actual owner can be wrongly denied with -EPERM and an unrelated caller wrongly allowed. Pass the handler's idmap instead. Fixes: 01beba7957a2 ("fs: port inode_owner_or_capable() to mnt_idmap") Cc: stable@vger.kernel.org Signed-off-by: Zhan Xusheng Reviewed-by: Chao Yu Acked-by: Christian Brauner (Amutable) Signed-off-by: Jaegeuk Kim --- fs/f2fs/xattr.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/f2fs/xattr.c b/fs/f2fs/xattr.c index ed33e5110f2a..6728d1488cad 100644 --- a/fs/f2fs/xattr.c +++ b/fs/f2fs/xattr.c @@ -119,7 +119,7 @@ static int f2fs_xattr_advise_set(const struct xattr_handler *handler, unsigned char old_advise = F2FS_I(inode)->i_advise; unsigned char new_advise; - if (!inode_owner_or_capable(&nop_mnt_idmap, inode)) + if (!inode_owner_or_capable(idmap, inode)) return -EPERM; if (value == NULL) return -EINVAL; From e533889fc26aea0cd83c90327063f272061dd820 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Tue, 30 Jun 2026 11:17:00 +0800 Subject: [PATCH 08/51] f2fs: validate MOVE_RANGE destination size F2FS_IOC_MOVE_RANGE checks the source range, but not the destination end before updating i_size. A source hole can expose this: __clone_blkaddrs() skips NULL_ADDR entries and returns success, so the caller can still extend the destination inode with unchecked pos_out + len. Reject destination overflow and use inode_newsize_ok() before extending the destination inode. Fixes: 4dd6f977fc77 ("f2fs: support an ioctl to move a range of data blocks") Cc: stable@kernel.org Assisted-by: Codex:gpt-5.5 Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index c54897a25981..170c485eafaa 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -3127,8 +3127,9 @@ static int f2fs_move_file_range(struct file *file_in, loff_t pos_in, struct inode *dst = file_inode(file_out); struct f2fs_sb_info *sbi = F2FS_I_SB(src); struct f2fs_lock_context lc; - size_t olen = len, dst_max_i_size = 0; - size_t dst_osize; + size_t olen = len; + loff_t dst_max_i_size = 0; + loff_t dst_osize, dst_end; int ret; if (file_in->f_path.mnt != file_out->f_path.mnt || @@ -3185,8 +3186,15 @@ static int f2fs_move_file_range(struct file *file_in, loff_t pos_in, } dst_osize = dst->i_size; - if (pos_out + olen > dst->i_size) - dst_max_i_size = pos_out + olen; + if (olen > LLONG_MAX - pos_out) + goto out_unlock; + dst_end = pos_out + olen; + if (dst_end > dst->i_size) { + ret = inode_newsize_ok(dst, dst_end); + if (ret) + goto out_unlock; + dst_max_i_size = dst_end; + } /* verify the end result is block aligned */ if (!IS_ALIGNED(pos_in, F2FS_BLKSIZE) || From 01027b2fcb74dade59fb833b51023f6593b6a9a2 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Tue, 30 Jun 2026 16:23:30 +0800 Subject: [PATCH 09/51] f2fs: limit recovery filename logging to stored length F2FS stores recovery filenames as a length plus a fixed-size i_name buffer. The buffer is not NUL-terminated, but recover_inode() and recover_dentry() print it with %s. For a 255-byte filename, recovery logging can read past i_name into the following raw inode fields. Print the name with a precision bounded by i_namelen and F2FS_NAME_LEN. Fixes: f356fe0cba0e ("f2fs: add debug msgs in the recovery routine") Cc: stable@kernel.org Assisted-by: Codex:gpt-5.5 Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/recovery.c | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/fs/f2fs/recovery.c b/fs/f2fs/recovery.c index 89af8407b667..3fecfdbd5958 100644 --- a/fs/f2fs/recovery.c +++ b/fs/f2fs/recovery.c @@ -158,6 +158,22 @@ static int init_recovered_filename(const struct inode *dir, return 0; } +static const char *recover_printable_name(struct inode *inode, + struct f2fs_inode *raw, + int *name_len) +{ + static const char encrypted_name[] = ""; + + if (file_enc_name(inode)) { + *name_len = sizeof(encrypted_name) - 1; + return encrypted_name; + } + + *name_len = min_t(unsigned int, le32_to_cpu(raw->i_namelen), + F2FS_NAME_LEN); + return raw->i_name; +} + static int recover_dentry(struct inode *inode, struct folio *ifolio, struct list_head *dir_list) { @@ -170,7 +186,8 @@ static int recover_dentry(struct inode *inode, struct folio *ifolio, struct inode *dir, *einode; struct fsync_inode_entry *entry; int err = 0; - char *name; + const char *name; + int name_len; entry = get_fsync_inode(dir_list, pino); if (!entry) { @@ -229,12 +246,9 @@ static int recover_dentry(struct inode *inode, struct folio *ifolio, out_put: f2fs_folio_put(folio, false); out: - if (file_enc_name(inode)) - name = ""; - else - name = raw_inode->i_name; - f2fs_notice(F2FS_I_SB(inode), "%s: ino = %x, name = %s, dir = %llu, err = %d", - __func__, ino_of_node(ifolio), name, + name = recover_printable_name(inode, raw_inode, &name_len); + f2fs_notice(F2FS_I_SB(inode), "%s: ino = %x, name = %.*s, dir = %llu, err = %d", + __func__, ino_of_node(ifolio), name_len, name, IS_ERR(dir) ? 0 : dir->i_ino, err); return err; } @@ -282,7 +296,8 @@ static int recover_inode(struct inode *inode, struct folio *folio) { struct f2fs_inode *raw = F2FS_INODE(folio); struct f2fs_inode_info *fi = F2FS_I(inode); - char *name; + const char *name; + int name_len; int err; inode->i_mode = le16_to_cpu(raw->i_mode); @@ -331,13 +346,11 @@ static int recover_inode(struct inode *inode, struct folio *folio) f2fs_mark_inode_dirty_sync(inode, true); - if (file_enc_name(inode)) - name = ""; - else - name = F2FS_INODE(folio)->i_name; + name = recover_printable_name(inode, raw, &name_len); - f2fs_notice(F2FS_I_SB(inode), "recover_inode: ino = %x, name = %s, inline = %x", - ino_of_node(folio), name, raw->i_inline); + f2fs_notice(F2FS_I_SB(inode), "%s: ino = %x, name = %.*s, inline = %x", + __func__, ino_of_node(folio), name_len, name, + raw->i_inline); return 0; } From 85171332742e741ccd6f401c69b6e0d698119e72 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Mon, 27 Jul 2026 21:04:29 +0800 Subject: [PATCH 10/51] f2fs: only redirty pinned folios in redirty_blocks redirty_blocks() pins folios with read_cache_folio() and then walks the same range again with filemap_lock_folio() to redirty them and drop the references it took. Commit 5951fee46bef ("f2fs: Use a folio in redirty_blocks()") changed the second pass to a do/while loop. If read_cache_folio() fails before anything is pinned, page_idx does not advance but the cleanup loop still runs once. If readahead has already populated the failed folio in page cache, that extra iteration finds it and folio_put_refs(folio, 2) drops one reference too many. Later drop_caches or reclaim can then report "BUG: Bad page state". Only redirty the range that was pinned successfully. Fixes: 5951fee46bef ("f2fs: Use a folio in redirty_blocks()") Cc: stable@kernel.org Assisted-by: Codex:gpt-5.5 Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 170c485eafaa..2773c6ce7ec1 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -4487,7 +4487,7 @@ static int redirty_blocks(struct inode *inode, pgoff_t page_idx, int len) page_idx = folio_next_index(folio); } while (page_len < len); - do { + while (redirty_idx < page_idx) { folio = filemap_lock_folio(mapping, redirty_idx); /* It will never fail, when folio has pinned above */ @@ -4500,7 +4500,7 @@ static int redirty_blocks(struct inode *inode, pgoff_t page_idx, int len) redirty_idx = folio_next_index(folio); folio_unlock(folio); folio_put_refs(folio, 2); - } while (redirty_idx < page_idx); + } return ret; } From f8a4108800254d6f7b2755515fbbd9d0caac561e Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Mon, 27 Jul 2026 15:49:10 +0800 Subject: [PATCH 11/51] f2fs: return writeback error from collapse range f2fs_collapse_range() writes back pages moved by f2fs_do_collapse(), but ignores the return value. If writeback fails, the ioctl can still truncate page cache, shrink blocks, and report success. Return the error before truncating page cache or updating the file size. Fixes: b4ace3370324 ("f2fs: support FALLOC_FL_COLLAPSE_RANGE") Cc: stable@kernel.org Assisted-by: Codex:gpt-5.5 Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 2773c6ce7ec1..c19f0de271a9 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -1623,11 +1623,14 @@ static int f2fs_collapse_range(struct inode *inode, loff_t offset, loff_t len) /* write out all moved pages, if possible */ filemap_invalidate_lock(inode->i_mapping); - filemap_write_and_wait_range(inode->i_mapping, offset, LLONG_MAX); + ret = filemap_write_and_wait_range(inode->i_mapping, offset, LLONG_MAX); + if (ret) + goto out_unlock; truncate_pagecache(inode, offset); new_size = i_size_read(inode) - len; ret = f2fs_truncate_blocks(inode, new_size, true); +out_unlock: filemap_invalidate_unlock(inode->i_mapping); if (!ret) f2fs_i_size_write(inode, new_size); From 28c1ef094e7c86977d9bf570dc0362fc54e36437 Mon Sep 17 00:00:00 2001 From: Hao-Qun Huang Date: Wed, 8 Jul 2026 14:54:39 +0800 Subject: [PATCH 12/51] f2fs: reject overlapping move range after len expansion F2FS_IOC_MOVE_RANGE treats a zero length as a request to move data from pos_in to EOF. However, the same-file overlap check runs before that expansion, so a request with len == 0 bypasses the overlap rejection added for same-file moves. For example, with a four-block file, moving from block 0 to block 1 with len == 0 is accepted by the old check because pos_in + len is still pos_in at that point. The code then expands len to cover the rest of the file and calls __exchange_data_block() on overlapping source and destination ranges in the same inode, which is the data-corruption case the overlap check was meant to reject. Move the overlap check after the source range has been validated and len == 0 has been expanded, so it sees the effective length. This is a no-op for non-zero len (the value is unchanged there) and keeps the existing early return for identical positions. Fixes: d95fd91c1ac1 ("f2fs: exclude special cases for f2fs_move_file_range") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-fable-5 Signed-off-by: Hao-Qun Huang Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index c19f0de271a9..f0b74984b658 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -3154,8 +3154,6 @@ static int f2fs_move_file_range(struct file *file_in, loff_t pos_in, if (src == dst) { if (pos_in == pos_out) return 0; - if (pos_out > pos_in && pos_out < pos_in + len) - return -EINVAL; } inode_lock(src); @@ -3181,6 +3179,8 @@ static int f2fs_move_file_range(struct file *file_in, loff_t pos_in, goto out_unlock; if (len == 0) olen = len = src->i_size - pos_in; + if (src == dst && pos_out > pos_in && pos_out < pos_in + len) + goto out_unlock; if (pos_in + len == src->i_size) len = ALIGN(src->i_size, F2FS_BLKSIZE) - pos_in; if (len == 0) { From 1b4db09b7f10895dc1a0bc32704c7de05188fb1d Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Tue, 23 Jun 2026 20:48:23 +0800 Subject: [PATCH 13/51] f2fs: fix to avoid move_range and defragment on device_alias file It's forbidden to migrate blocks of device alias file. Cc: stable@kernel.org Fixes: 128d333f0dff ("f2fs: introduce device aliasing file") Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index f0b74984b658..b99d9cdf9ba7 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -3092,6 +3092,9 @@ static int f2fs_ioc_defragment(struct file *filp, unsigned long arg) if (f2fs_readonly(sbi->sb)) return -EROFS; + if (IS_DEVICE_ALIASING(inode)) + return -EOPNOTSUPP; + if (copy_from_user(&range, (struct f2fs_defragment __user *)arg, sizeof(range))) return -EFAULT; @@ -3145,7 +3148,8 @@ static int f2fs_move_file_range(struct file *file_in, loff_t pos_in, if (!S_ISREG(src->i_mode) || !S_ISREG(dst->i_mode)) return -EINVAL; - if (IS_ENCRYPTED(src) || IS_ENCRYPTED(dst)) + if (IS_ENCRYPTED(src) || IS_ENCRYPTED(dst) || + IS_DEVICE_ALIASING(src) || IS_DEVICE_ALIASING(dst)) return -EOPNOTSUPP; if (pos_out < 0 || pos_in < 0) From 575f6f8ef2abe8552af96e42c65d70964db53563 Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Tue, 23 Jun 2026 15:26:41 +0800 Subject: [PATCH 14/51] f2fs: don't drop the top folio order in the f2fs_iostat tracepoint The f2fs_iostat tracepoint stores the per-order read folio counts in a fixed-size array and prints a fixed number of buckets, both hardcoded to 11. The sysfs iostat accounting array is instead sized by NR_PAGE_ORDERS (= MAX_PAGE_ORDER + 1), which is not always 11: arm64 16K pages -> MAX_PAGE_ORDER 11 -> NR_PAGE_ORDERS 12 arm64 64K pages -> MAX_PAGE_ORDER 13 -> NR_PAGE_ORDERS 14 f2fs enables large folios for immutable, non-compressed files, and the read folio order is bounded by MAX_PAGECACHE_ORDER, i.e. min(MAX_XAS_ORDER, PREFERRED_MAX_PAGECACHE_ORDER). With THP enabled this reaches order 11 on 16K/64K base-page kernels (MAX_XAS_ORDER caps it at 11). So an order-11 read folio is possible there and is accounted into index 11 of the array. On those configurations the sysfs file reports the order-11 count correctly, but the tracepoint silently drops it: the memcpy is capped at min(NR_PAGE_ORDERS, 11), so index 11 is never copied and the trace disagrees with sysfs. There is no memory-safety issue, only the order-11 bucket missing from the trace; 4K-page kernels (NR_PAGE_ORDERS == 11, max order <= 9) are unaffected. Size the array and the printed buckets by a ceiling that covers the largest possible NR_PAGE_ORDERS (14) with headroom, and add a BUILD_BUG_ON() so any future growth of NR_PAGE_ORDERS fails the build loudly instead of silently truncating again. The human-readable "order=count" output is preserved. Fixes: cb8ff3ead9a3 ("f2fs: add page-order information for large folio reads in iostat") Cc: stable@vger.kernel.org Signed-off-by: Zhan Xusheng Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/iostat.c | 6 ++++++ include/trace/events/f2fs.h | 20 ++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/fs/f2fs/iostat.c b/fs/f2fs/iostat.c index ae265e3e9b2c..12d4e18a6a50 100644 --- a/fs/f2fs/iostat.c +++ b/fs/f2fs/iostat.c @@ -332,6 +332,12 @@ void f2fs_destroy_iostat_processing(void) int f2fs_init_iostat(struct f2fs_sb_info *sbi) { + /* + * The f2fs_iostat tracepoint emits a fixed number of read folio order + * buckets; make sure every order fits so none is silently dropped. + */ + BUILD_BUG_ON(NR_PAGE_ORDERS > F2FS_IOSTAT_RD_FOLIO_ORDERS); + /* init iostat info */ spin_lock_init(&sbi->iostat_lock); spin_lock_init(&sbi->iostat_lat_lock); diff --git a/include/trace/events/f2fs.h b/include/trace/events/f2fs.h index 270c1a2c24c4..1dd9fc5afc46 100644 --- a/include/trace/events/f2fs.h +++ b/include/trace/events/f2fs.h @@ -2114,6 +2114,14 @@ DEFINE_EVENT(f2fs_zip_end, f2fs_decompress_pages_end, ); #ifdef CONFIG_F2FS_IOSTAT +/* + * Number of read folio order buckets emitted by the f2fs_iostat tracepoint. + * TP_printk() cannot loop, so the field count is fixed here and must be >= + * the largest possible NR_PAGE_ORDERS (14 on arm64 with 64K pages). The + * BUILD_BUG_ON() in f2fs_update_read_folio_count() enforces this. + */ +#define F2FS_IOSTAT_RD_FOLIO_ORDERS 16 + TRACE_EVENT(f2fs_iostat, TP_PROTO(struct f2fs_sb_info *sbi, unsigned long long *iostat, @@ -2151,7 +2159,7 @@ TRACE_EVENT(f2fs_iostat, __field(unsigned long long, fs_mrio) __field(unsigned long long, fs_discard) __field(unsigned long long, fs_reset_zone) - __array(unsigned long long, read_folio_count, 11) + __array(unsigned long long, read_folio_count, F2FS_IOSTAT_RD_FOLIO_ORDERS) ), TP_fast_assign( @@ -2186,7 +2194,8 @@ TRACE_EVENT(f2fs_iostat, __entry->fs_reset_zone = iostat[FS_ZONE_RESET_IO]; memset(__entry->read_folio_count, 0, sizeof(__entry->read_folio_count)); memcpy(__entry->read_folio_count, read_folio_count, - sizeof(unsigned long long) * min_t(int, NR_PAGE_ORDERS, 11)); + sizeof(unsigned long long) * + min_t(int, NR_PAGE_ORDERS, F2FS_IOSTAT_RD_FOLIO_ORDERS)); ), TP_printk("dev = (%d,%d), " @@ -2201,7 +2210,8 @@ TRACE_EVENT(f2fs_iostat, "fs [data=%llu, (gc_data=%llu, cdata=%llu), " "node=%llu, meta=%llu], " "read_folio_count [0=%llu, 1=%llu, 2=%llu, 3=%llu, 4=%llu, " - "5=%llu, 6=%llu, 7=%llu, 8=%llu, 9=%llu, 10=%llu]", + "5=%llu, 6=%llu, 7=%llu, 8=%llu, 9=%llu, 10=%llu, 11=%llu, " + "12=%llu, 13=%llu, 14=%llu, 15=%llu]", show_dev(__entry->dev), __entry->app_wio, __entry->app_dio, __entry->app_bio, __entry->app_mio, __entry->app_bcdio, __entry->app_mcdio, __entry->fs_dio, __entry->fs_cdio, @@ -2218,7 +2228,9 @@ TRACE_EVENT(f2fs_iostat, __entry->read_folio_count[4], __entry->read_folio_count[5], __entry->read_folio_count[6], __entry->read_folio_count[7], __entry->read_folio_count[8], __entry->read_folio_count[9], - __entry->read_folio_count[10]) + __entry->read_folio_count[10], __entry->read_folio_count[11], + __entry->read_folio_count[12], __entry->read_folio_count[13], + __entry->read_folio_count[14], __entry->read_folio_count[15]) ); #ifndef __F2FS_IOSTAT_LATENCY_TYPE From 9ec09d5f4b317a417c8655c14056f70cbe71eb6c Mon Sep 17 00:00:00 2001 From: Joanne Chang Date: Tue, 23 Jun 2026 06:34:28 +0000 Subject: [PATCH 15/51] f2fs: dirty directory inodes on mtime/ctime update Xfstests generic/547 sometimes fail with mismatched directory metadata before and after a power failure. This happens because when a directory entry is added, renamed, or deleted, its mtime and ctime are updated and the inode is marked dirty via f2fs_mark_inode_dirty_sync(dir, sync=false). The sync=false flag means the dirty inode is not added to the global DIRTY_META list. Therefore, subsequent checkpoints skip flushing these updated directory blocks, causing directory timestamps to revert to stale values after a sudden power failure. Address this by changing the dirtying parameter to sync=true during directory entry mutations and renames. This forces F2FS to immediately queue the updated directory blocks on the global DIRTY_META list, ensuring timestamps are committed to checkpoints. Fixes: 7c45729a4d6d ("f2fs: keep dirty inodes selectively for checkpoint") Cc: stable@vger.kernel.org Signed-off-by: Joanne Chang Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/dir.c | 6 +++--- fs/f2fs/inline.c | 2 +- fs/f2fs/namei.c | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/fs/f2fs/dir.c b/fs/f2fs/dir.c index 0562ad211351..fd0e2cd31a81 100644 --- a/fs/f2fs/dir.c +++ b/fs/f2fs/dir.c @@ -461,7 +461,7 @@ void f2fs_set_link(struct inode *dir, struct f2fs_dir_entry *de, folio_mark_dirty(folio); inode_set_mtime_to_ts(dir, inode_set_ctime_current(dir)); - f2fs_mark_inode_dirty_sync(dir, false); + f2fs_mark_inode_dirty_sync(dir, true); f2fs_folio_put(folio, true); } @@ -616,7 +616,7 @@ void f2fs_update_parent_metadata(struct inode *dir, struct inode *inode, clear_inode_flag(inode, FI_NEW_INODE); } inode_set_mtime_to_ts(dir, inode_set_ctime_current(dir)); - f2fs_mark_inode_dirty_sync(dir, false); + f2fs_mark_inode_dirty_sync(dir, true); if (F2FS_I(dir)->i_current_depth != current_depth) f2fs_i_depth_write(dir, current_depth); @@ -928,7 +928,7 @@ void f2fs_delete_entry(struct f2fs_dir_entry *dentry, struct folio *folio, f2fs_folio_put(folio, true); inode_set_mtime_to_ts(dir, inode_set_ctime_current(dir)); - f2fs_mark_inode_dirty_sync(dir, false); + f2fs_mark_inode_dirty_sync(dir, true); if (inode) f2fs_drop_nlink(dir, inode); diff --git a/fs/f2fs/inline.c b/fs/f2fs/inline.c index e2f7bedf1552..aec06fb4fd76 100644 --- a/fs/f2fs/inline.c +++ b/fs/f2fs/inline.c @@ -732,7 +732,7 @@ void f2fs_delete_inline_entry(struct f2fs_dir_entry *dentry, f2fs_folio_put(folio, true); inode_set_mtime_to_ts(dir, inode_set_ctime_current(dir)); - f2fs_mark_inode_dirty_sync(dir, false); + f2fs_mark_inode_dirty_sync(dir, true); if (inode) f2fs_drop_nlink(dir, inode); diff --git a/fs/f2fs/namei.c b/fs/f2fs/namei.c index cac03b8e91a1..7ffdf23cea5e 100644 --- a/fs/f2fs/namei.c +++ b/fs/f2fs/namei.c @@ -1076,7 +1076,7 @@ static int f2fs_rename(struct mnt_idmap *idmap, struct inode *old_dir, f2fs_up_write(&F2FS_I(old_inode)->i_sem); inode_set_ctime_current(old_inode); - f2fs_mark_inode_dirty_sync(old_inode, false); + f2fs_mark_inode_dirty_sync(old_inode, true); f2fs_delete_entry(old_entry, old_folio, old_dir, NULL); old_folio = NULL; @@ -1246,7 +1246,7 @@ static int f2fs_cross_rename(struct inode *old_dir, struct dentry *old_dentry, f2fs_i_links_write(old_dir, old_nlink > 0); f2fs_up_write(&F2FS_I(old_dir)->i_sem); } - f2fs_mark_inode_dirty_sync(old_dir, false); + f2fs_mark_inode_dirty_sync(old_dir, true); /* update directory entry info of new dir inode */ f2fs_set_link(new_dir, new_entry, new_folio, old_inode); @@ -1265,7 +1265,7 @@ static int f2fs_cross_rename(struct inode *old_dir, struct dentry *old_dentry, f2fs_i_links_write(new_dir, new_nlink > 0); f2fs_up_write(&F2FS_I(new_dir)->i_sem); } - f2fs_mark_inode_dirty_sync(new_dir, false); + f2fs_mark_inode_dirty_sync(new_dir, true); if (F2FS_OPTION(sbi).fsync_mode == FSYNC_MODE_STRICT) { f2fs_add_ino_entry(sbi, old_dir->i_ino, TRANS_DIR_INO); From 0f046e45afb783bbf8c5a2eb9abe1f84db270203 Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Fri, 26 Jun 2026 11:07:28 +0800 Subject: [PATCH 16/51] f2fs: Remove unused curseg_segno() and curseg_alloc_type() The only callers of curseg_segno() and curseg_alloc_type() were removed by commit 5a4fed7cd97a ("f2fs: simplify do_checkpoint"); both helpers have been unused since then. Being static inline functions they do not trigger -Wunused-function, so the dead code has gone unnoticed. Remove them. No functional change. Signed-off-by: Zhan Xusheng Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/segment.h | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/fs/f2fs/segment.h b/fs/f2fs/segment.h index b0c06b3580b4..33a2257da1e6 100644 --- a/fs/f2fs/segment.h +++ b/fs/f2fs/segment.h @@ -796,20 +796,6 @@ F2FS_IPU_POLICY(F2FS_IPU_ASYNC); F2FS_IPU_POLICY(F2FS_IPU_NOCACHE); F2FS_IPU_POLICY(F2FS_IPU_HONOR_OPU_WRITE); -static inline unsigned int curseg_segno(struct f2fs_sb_info *sbi, - int type) -{ - struct curseg_info *curseg = CURSEG_I(sbi, type); - return curseg->segno; -} - -static inline unsigned char curseg_alloc_type(struct f2fs_sb_info *sbi, - int type) -{ - struct curseg_info *curseg = CURSEG_I(sbi, type); - return curseg->alloc_type; -} - static inline bool valid_main_segno(struct f2fs_sb_info *sbi, unsigned int segno) { From 6dc2b804a042d2ff0fdf4c8e3db78669c36b688d Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Mon, 3 Aug 2026 19:41:53 +0800 Subject: [PATCH 17/51] f2fs: reject invalid recovered filename lengths Recovery uses raw_inode->i_namelen directly when rebuilding fsynced dentries. A zero-length name uses no dentry slots, so recovery can report success without recreating the dentry. Treat zero-length and oversized recovered names as corruption, mark NEED_FSCK, and stop recovery with -EFSCORRUPTED. Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/recovery.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/fs/f2fs/recovery.c b/fs/f2fs/recovery.c index 3fecfdbd5958..aaa5227739c8 100644 --- a/fs/f2fs/recovery.c +++ b/fs/f2fs/recovery.c @@ -116,18 +116,26 @@ static void del_fsync_inode(struct fsync_inode_entry *entry, int drop) } static int init_recovered_filename(const struct inode *dir, + struct inode *inode, struct f2fs_inode *raw_inode, struct f2fs_filename *fname, struct qstr *usr_fname) { + struct f2fs_sb_info *sbi = F2FS_I_SB(inode); int err; memset(fname, 0, sizeof(*fname)); fname->disk_name.len = le32_to_cpu(raw_inode->i_namelen); fname->disk_name.name = raw_inode->i_name; - if (WARN_ON(fname->disk_name.len > F2FS_NAME_LEN)) - return -ENAMETOOLONG; + if (unlikely(!fname->disk_name.len || + fname->disk_name.len > F2FS_NAME_LEN)) { + f2fs_err(sbi, "invalid recovered filename length %u for ino %llu", + fname->disk_name.len, inode->i_ino); + set_sbi_flag(sbi, SBI_NEED_FSCK); + f2fs_handle_error(sbi, ERROR_CORRUPTED_INODE); + return -EFSCORRUPTED; + } if (!IS_ENCRYPTED(dir)) { usr_fname->name = fname->disk_name.name; @@ -201,7 +209,7 @@ static int recover_dentry(struct inode *inode, struct folio *ifolio, } dir = entry->inode; - err = init_recovered_filename(dir, raw_inode, &fname, &usr_fname); + err = init_recovered_filename(dir, inode, raw_inode, &fname, &usr_fname); if (err) goto out; retry: From 3de6b80941152a384ee1b9cf88ac1c9dd4eec6dd Mon Sep 17 00:00:00 2001 From: Bart Van Assche Date: Mon, 3 Aug 2026 09:52:22 -0700 Subject: [PATCH 18/51] f2fs: Run f2fs_write_end_io() asynchronously The bio_for_each_segment_all() loop can take more than 10 ms for a large bio on an ARM little core. This is too much for interrupt context. Hence perform the write bio completion work asynchronously if a bio is large and if f2fs_write_end_io() is called from atomic context. This patch reduces the time spent in f2fs_write_end_io() from about 10 ms to about 150 microseconds on an Arm Cortex-A520 core if the max_atc_write_bio_size parameter is changed to 16384. Signed-off-by: Bart Van Assche Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- Documentation/ABI/testing/sysfs-fs-f2fs | 10 ++++++++++ fs/f2fs/data.c | 23 +++++++++++++++++++++-- fs/f2fs/f2fs.h | 2 ++ fs/f2fs/super.c | 1 + fs/f2fs/sysfs.c | 2 ++ 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-fs-f2fs b/Documentation/ABI/testing/sysfs-fs-f2fs index 1b58c029abd0..f4e6a7415cde 100644 --- a/Documentation/ABI/testing/sysfs-fs-f2fs +++ b/Documentation/ABI/testing/sysfs-fs-f2fs @@ -1002,3 +1002,13 @@ Description: It can be used to tune priority of f2fs critical task, e.g. f2fs_ck threads, limitation as below: - it requires user has CAP_SYS_NICE capability. - the range is [100, 139], by default the value is 120. + +What: /sys/fs/f2fs//max_atc_write_bio_size +Date: June 2026 +Contact: Bart Van Assche +Description: Every time a write operation completes f2fs_write_end_io() is + called. This function may be called from an atomic context, + e.g. from inside an interrupt handler. This attribute controls + the maximum size of a write bio that is completed in atomic + (atc) context. The default value for this attribute is UINT_MAX + which means that this functionality is disabled by default. diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index 042ed8ad9cc3..ef2a567acdac 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -411,13 +411,32 @@ static void f2fs_write_end_bio(struct bio *bio) bio_put(bio); } -static void f2fs_write_end_io(struct bio *bio) +static void f2fs_write_end_io_work(struct work_struct *work) { - iostat_update_and_unbind_ctx(bio); + struct bio *bio = &container_of(work, struct f2fs_bio, work)->bio; f2fs_write_end_bio(bio); } +static void f2fs_write_end_io(struct bio *bio) +{ + struct f2fs_sb_info *sbi; + + iostat_update_and_unbind_ctx(bio); + + sbi = bio->bi_private; + + if (in_atomic() && bio->bi_iter.bi_size > sbi->max_atc_write_bio_size) { + struct work_struct *w; + + w = &container_of(bio, struct f2fs_bio, bio)->work; + INIT_WORK(w, f2fs_write_end_io_work); + queue_work(sbi->wq, w); + } else { + f2fs_write_end_bio(bio); + } +} + #ifdef CONFIG_BLK_DEV_ZONED static void f2fs_zone_write_end_io(struct bio *bio) { diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 8011bbdf2c68..8e2fb0bda467 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -1800,6 +1800,8 @@ struct f2fs_sb_info { struct f2fs_sm_info *sm_info; /* segment manager */ /* for bio operations */ + /* Largest write bio size completed in atomic context (atc). */ + u32 max_atc_write_bio_size; struct f2fs_bio_info *write_io[NR_PAGE_TYPE]; /* for write bios */ /* keep migration IO order for LFS mode */ struct f2fs_rwsem io_order_lock; diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index 90a47cf86378..25309e6d4156 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -5068,6 +5068,7 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc) sb->s_fs_info = sbi; sbi->raw_super = raw_super; + sbi->max_atc_write_bio_size = UINT_MAX; INIT_WORK(&sbi->s_error_work, f2fs_record_error_work); memcpy(sbi->errors, raw_super->s_errors, MAX_F2FS_ERRORS); diff --git a/fs/f2fs/sysfs.c b/fs/f2fs/sysfs.c index be92c05a5420..d9f81edca04a 100644 --- a/fs/f2fs/sysfs.c +++ b/fs/f2fs/sysfs.c @@ -1266,6 +1266,7 @@ F2FS_SBI_RW_ATTR(gc_idle_interval, interval_time[GC_TIME]); F2FS_SBI_RW_ATTR(umount_discard_timeout, interval_time[UMOUNT_DISCARD_TIMEOUT]); F2FS_SBI_RW_ATTR(gc_pin_file_thresh, gc_pin_file_threshold); F2FS_SBI_RW_ATTR(gc_reclaimed_segments, gc_reclaimed_segs); +F2FS_SBI_RW_ATTR(max_atc_write_bio_size, max_atc_write_bio_size); F2FS_SBI_GENERAL_RW_ATTR(max_victim_search); F2FS_SBI_GENERAL_RW_ATTR(migration_granularity); F2FS_SBI_GENERAL_RW_ATTR(migration_window_granularity); @@ -1509,6 +1510,7 @@ static struct attribute *f2fs_attrs[] = { ATTR_LIST(seq_file_ra_mul), ATTR_LIST(gc_segment_mode), ATTR_LIST(gc_reclaimed_segments), + ATTR_LIST(max_atc_write_bio_size), ATTR_LIST(max_fragment_chunk), ATTR_LIST(max_fragment_hole), ATTR_LIST(current_atomic_write), From 5cb33b00c8fbb6e8f1fa3d281c3036d5f7c7c41f Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Tue, 4 Aug 2026 09:48:48 +0800 Subject: [PATCH 19/51] f2fs: avoid NULL checkpoint thread access in sysfs checkpoint_merge can be enabled even when no checkpoint merge thread is running. A read-only mount is one case: f2fs does not start f2fs_issue_ckpt there, but ckpt_thread_ioprio is still writable through sysfs. The ckpt_thread_ioprio store path updates the saved ioprio value and, when checkpoint_merge is enabled, calls set_task_ioprio() for the checkpoint thread. If cprc->f2fs_issue_ckpt is NULL, that dereferences a NULL task pointer. Protect ckpt_thread_ioprio sysfs writes with s_umount as well, so the checkpoint thread cannot disappear under the store path while updating its ioprio. Fixes: e65920661708 ("f2fs: add ckpt_thread_ioprio sysfs node") Cc: stable@kernel.org Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/sysfs.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fs/f2fs/sysfs.c b/fs/f2fs/sysfs.c index d9f81edca04a..c976549ff1bc 100644 --- a/fs/f2fs/sysfs.c +++ b/fs/f2fs/sysfs.c @@ -557,7 +557,7 @@ static ssize_t __sbi_store(struct f2fs_attr *a, return -EINVAL; cprc->ckpt_thread_ioprio = IOPRIO_PRIO_VALUE(class, level); - if (test_opt(sbi, MERGE_CHECKPOINT)) { + if (cprc->f2fs_issue_ckpt) { ret = set_task_ioprio(cprc->f2fs_issue_ckpt, cprc->ckpt_thread_ioprio); if (ret) @@ -1007,13 +1007,14 @@ static ssize_t f2fs_sbi_store(struct f2fs_attr *a, ssize_t ret; bool gc_entry = (!strcmp(a->attr.name, "gc_urgent") || a->struct_type == GC_THREAD); + bool thread_entry = !strcmp(a->attr.name, "ckpt_thread_ioprio"); - if (gc_entry) { + if (gc_entry || thread_entry) { if (!down_read_trylock(&sbi->sb->s_umount)) return -EAGAIN; } ret = __sbi_store(a, sbi, buf, count); - if (gc_entry) + if (gc_entry || thread_entry) up_read(&sbi->sb->s_umount); return ret; From 8e4692c6c165e81b2cbb847d8da4b45a53483b33 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Tue, 4 Aug 2026 09:48:49 +0800 Subject: [PATCH 20/51] f2fs: protect critical_task_priority updates with s_umount The sysfs store path already takes s_umount for GC thread control entries, and ckpt_thread_ioprio is covered as well. critical_task_priority also updates checkpoint or GC kthread scheduling state, but it is not covered by that serialization. It can race with remount or teardown paths that are stopping those threads. Protect critical_task_priority sysfs writes with s_umount too. Fixes: 52190933c37a ("f2fs: sysfs: introduce critical_task_priority") Cc: stable@kernel.org Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/sysfs.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/sysfs.c b/fs/f2fs/sysfs.c index c976549ff1bc..3201e2185fea 100644 --- a/fs/f2fs/sysfs.c +++ b/fs/f2fs/sysfs.c @@ -1007,7 +1007,8 @@ static ssize_t f2fs_sbi_store(struct f2fs_attr *a, ssize_t ret; bool gc_entry = (!strcmp(a->attr.name, "gc_urgent") || a->struct_type == GC_THREAD); - bool thread_entry = !strcmp(a->attr.name, "ckpt_thread_ioprio"); + bool thread_entry = !strcmp(a->attr.name, "ckpt_thread_ioprio") || + !strcmp(a->attr.name, "critical_task_priority"); if (gc_entry || thread_entry) { if (!down_read_trylock(&sbi->sb->s_umount)) From 0f9af07ecc1ab486038373db6ae0436c5d674b19 Mon Sep 17 00:00:00 2001 From: Chen Changcheng Date: Tue, 4 Aug 2026 08:54:02 +0800 Subject: [PATCH 21/51] f2fs: fix valid block count leak on data block allocation failure In __allocate_data_block(), when allocating a new data block (dn->data_blkaddr == NULL_ADDR), inc_valid_block_count() is called first to increment total_valid_block_count and i_blocks. If the subsequent f2fs_allocate_data_block() fails, the function returns the error directly without rolling back the already-incremented block counts, causing a permanent leak. Fix this by calling dec_valid_block_count() to undo the increment before returning the error. The condition old_blkaddr == NULL_ADDR precisely identifies the case where inc_valid_block_count() was called. Fixes: 7d009e048d7c ("f2fs: fix to handle segment allocation failure correctly") Cc: Reviewed-by: Chao Yu Signed-off-by: Chen Changcheng Signed-off-by: Jaegeuk Kim --- fs/f2fs/data.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index ef2a567acdac..30e8084da313 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -1552,8 +1552,11 @@ static int __allocate_data_block(struct dnode_of_data *dn, int seg_type) old_blkaddr = dn->data_blkaddr; err = f2fs_allocate_data_block(sbi, NULL, old_blkaddr, &dn->data_blkaddr, &sum, seg_type, NULL); - if (err) + if (err) { + if (old_blkaddr == NULL_ADDR) + dec_valid_block_count(sbi, dn->inode, count); return err; + } if (GET_SEGNO(sbi, old_blkaddr) != NULL_SEGNO) f2fs_invalidate_internal_cache(sbi, old_blkaddr, 1); From 026d7aeadf27291f961893165edb6079a7078f6f Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Wed, 5 Aug 2026 12:20:08 +0000 Subject: [PATCH 22/51] f2fs: fix to return -EFSCORRUPTED in f2fs_get_node_info() correctly Otherwise, it will cache wrong nat info in cache. Cc: stable@kernel.org Fixes: 3cb396a2c790 ("f2fs: fix to do sanity check on nat entry of quota inode") Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/node.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c index 38917e4a7319..9775fa90636a 100644 --- a/fs/f2fs/node.c +++ b/fs/f2fs/node.c @@ -660,6 +660,7 @@ int f2fs_get_node_info(struct f2fs_sb_info *sbi, nid_t nid, __builtin_return_address(0), ni->ino, ni->nid, ni->blk_addr, ni->version, ni->flag); f2fs_handle_error(sbi, ERROR_INCONSISTENT_NAT); + return -EFSCORRUPTED; } /* cache nat entry */ From 7173538c41d6046f1a92d1f92bc6d0c7b7e314a2 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Wed, 5 Aug 2026 12:20:50 +0000 Subject: [PATCH 23/51] f2fs: avoid unnecessary shrink in f2fs_shrink_scan() In f2fs_shrink_scan(), let's check if we have already shrinked enough number of memory before calling f2fs_shrink_read_extent_tree(). Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/shrinker.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/shrinker.c b/fs/f2fs/shrinker.c index b88babcf6ab4..4f6bf5926de4 100644 --- a/fs/f2fs/shrinker.c +++ b/fs/f2fs/shrinker.c @@ -109,7 +109,8 @@ unsigned long f2fs_shrink_scan(struct shrinker *shrink, freed += f2fs_shrink_age_extent_tree(sbi, nr >> 2); /* shrink read extent cache entries */ - freed += f2fs_shrink_read_extent_tree(sbi, nr >> 2); + if (freed < nr) + freed += f2fs_shrink_read_extent_tree(sbi, nr >> 2); /* shrink clean nat cache entries */ if (freed < nr) From 5b86eab84ac8e9289b5afc52ef88ab18ba5bacab Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Wed, 5 Aug 2026 12:29:20 +0000 Subject: [PATCH 24/51] f2fs: fix to clear dirty flag on folio in error path If node block is corrupted due to chksum mismatch or inconsistent footer info, it needs to drop clear flag of node folio, in order to persist inconsistent node data to storage. Cc: stable@kernel.org Fixes: b42b179bda9f ("f2fs: fix to do checksum even if inode page is uptodate") Signed-off-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 9775fa90636a..a98855b8cc5e 100644 --- a/fs/f2fs/node.c +++ b/fs/f2fs/node.c @@ -1618,7 +1618,7 @@ static struct folio *__get_node_folio(struct f2fs_sb_info *sbi, pgoff_t nid, if (!err) return folio; out_err: - folio_clear_uptodate(folio); + clear_node_folio_dirty(folio); out_put_err: /* ENOENT comes from read_node_folio which is not an error. */ if (err != -ENOENT) From 5d59e6a3777c365dd63cf6b40e579460aed7afd6 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Thu, 6 Aug 2026 10:52:24 +0800 Subject: [PATCH 25/51] f2fs: print error information in f2fs_put_super() So that we can know in which path we may missed to account the reference correclty: normal path or error handling path. Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/super.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index 25309e6d4156..5902b2da7ea4 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -2058,7 +2058,8 @@ static void f2fs_put_super(struct super_block *sb) if (!get_pages(sbi, i)) continue; f2fs_err(sbi, "detect filesystem reference count leak during " - "umount, type: %d, count: %lld", i, get_pages(sbi, i)); + "umount, type: %d, count: %lld, err: %d, cp_err: %d", + i, get_pages(sbi, i), err, f2fs_cp_error(sbi)); f2fs_bug_on(sbi, 1); } From e1116f8e98eaf76020561e8ab71a51bebfd23334 Mon Sep 17 00:00:00 2001 From: Yonggil Song Date: Thu, 6 Aug 2026 12:39:18 +0900 Subject: [PATCH 26/51] f2fs: issue multi-device flushes in parallel On a multi-device setup, submit_flush_wait() walked the dirty devices in order and aborted the whole loop on the first device whose flush failed, leaving the remaining dirty devices un-flushed. Each device still needs its own data made durable, so a failure on one device must not skip the others. It also waited for one device's flush to complete before issuing the next, even though the devices have independent flush queues and could be flushed concurrently. Flush every dirty device best-effort and in parallel instead: build one PREFLUSH bio per dirty device, submit them all, then wait for every completion, returning the first error seen (0 if all succeed). This bounds the flush window by the slowest device rather than the sum of all of them. No caller depends on the previous early-abort behaviour -- fsync only checks whether the return value is zero (fs/f2fs/file.c). The checkpoint path (f2fs_flush_device_cache) is unaffected; this only touches the fsync flush path. The per-device bio/completion array is small and bounded (at most MAX_DEVICES entries), so allocate it with __GFP_NOFAIL rather than keeping a separate serial fallback path for allocation failure. Signed-off-by: Yonggil Song Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/segment.c | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 0b706568b034..00cc2af45ff9 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -565,21 +565,59 @@ static int __submit_flush_wait(struct f2fs_sb_info *sbi, return ret; } +static void f2fs_flush_end_io(struct bio *bio) +{ + complete(bio->bi_private); +} + +struct f2fs_flush_bio { + struct bio bio; + struct completion wait; +}; + static int submit_flush_wait(struct f2fs_sb_info *sbi, nid_t ino) { + struct f2fs_flush_bio *flush_bio; + unsigned long devices = 0; int ret = 0; int i; if (!f2fs_is_multi_device(sbi)) return __submit_flush_wait(sbi, sbi->sb->s_bdev); + flush_bio = kmalloc(array_size(sbi->s_ndevs, sizeof(*flush_bio)), + GFP_NOFS | __GFP_NOFAIL); + for (i = 0; i < sbi->s_ndevs; i++) { if (!f2fs_is_dirty_device(sbi, ino, i, FLUSH_INO)) continue; - ret = __submit_flush_wait(sbi, FDEV(i).bdev); - if (ret) - break; + + bio_init(&flush_bio[i].bio, FDEV(i).bdev, NULL, 0, + REQ_OP_WRITE | REQ_SYNC | REQ_PREFLUSH); + init_completion(&flush_bio[i].wait); + flush_bio[i].bio.bi_private = &flush_bio[i].wait; + flush_bio[i].bio.bi_end_io = f2fs_flush_end_io; + devices |= BIT(i); + submit_bio(&flush_bio[i].bio); } + + for (i = 0; i < sbi->s_ndevs; i++) { + int err; + + if (!(devices & BIT(i))) + continue; + + wait_for_completion(&flush_bio[i].wait); + err = blk_status_to_errno(flush_bio[i].bio.bi_status); + trace_f2fs_issue_flush(FDEV(i).bdev, test_opt(sbi, NOBARRIER), + test_opt(sbi, FLUSH_MERGE), err); + if (!err) + f2fs_update_iostat(sbi, NULL, FS_FLUSH_IO, 0); + else if (!ret) + ret = err; + bio_uninit(&flush_bio[i].bio); + } + kfree(flush_bio); return ret; } From 7e188e9f9437ab47c3237d609f1b26348d6fea1a Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Thu, 6 Aug 2026 20:35:37 +0800 Subject: [PATCH 27/51] f2fs: fix to pass folio->index to f2fs_sanity_check_node_footer() Otherwise in f2fs_sanity_check_node_footer(), it will check the same nid incorrectly. Cc: stable@kernel.org Fixes: 0a736109c9d2 ("f2fs: fix to do sanity check on node footer in __write_node_folio()") Signed-off-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 a98855b8cc5e..46bea52e35c3 100644 --- a/fs/f2fs/node.c +++ b/fs/f2fs/node.c @@ -1790,7 +1790,7 @@ static bool __write_node_folio(struct folio *folio, bool atomic, bool do_fsync, /* get old block addr of this node page */ nid = nid_of_node(folio); - if (f2fs_sanity_check_node_footer(sbi, folio, nid, + if (f2fs_sanity_check_node_footer(sbi, folio, folio->index, NODE_TYPE_REGULAR, false)) { fserror_report_metadata(sbi->sb, -EFSCORRUPTED, GFP_NOFS); f2fs_stop_checkpoint(sbi, false, STOP_CP_REASON_CORRUPTED_NID); From eae3faf210bdc69181be717ce90437eadc6b3c80 Mon Sep 17 00:00:00 2001 From: Daeho Jeong Date: Wed, 5 Aug 2026 18:48:13 -0700 Subject: [PATCH 28/51] f2fs: support dynamic reserve/release for device aliasing This patch adds a dynamic management feature to the existing device aliasing functionality. It allows users to dynamically reserve or release specific devices from the filesystem's free pool at runtime through new ioctls. To support this, three new ioctls are introduced: - F2FS_IOC_RESERVE_DEV_ALIAS: This reclaims the space occupied by a device aliasing file. It first performs a capacity check, resets GC victim information for the target range, marks the segments as in-use to prevent new allocations, and then triggers GC to migrate existing valid data out of the range. Finally, it reserves these blocks in the SIT to effectively exclude the device from the usable capacity. - F2FS_IOC_RELEASE_DEV_ALIAS: This releases the reserved space of a previously reserved device aliasing file. It truncates the blocks associated with the file, which makes them available for general filesystem allocation again. - F2FS_IOC_GET_DEV_ALIAS_STATUS: This retrieves the current aliasing status of a device aliasing file, returning whether the file is released (inactive alias) or reserved (active alias, with blocks fully allocated on the device). Signed-off-by: Daeho Jeong Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- Documentation/filesystems/f2fs.rst | 35 ++++ fs/f2fs/data.c | 4 +- fs/f2fs/extent_cache.c | 9 + fs/f2fs/f2fs.h | 19 +- fs/f2fs/file.c | 273 ++++++++++++++++++++++++++++- fs/f2fs/gc.c | 30 ++-- fs/f2fs/namei.c | 14 ++ fs/f2fs/segment.c | 178 +++++++++++++------ fs/f2fs/segment.h | 22 +++ fs/f2fs/super.c | 36 ++++ include/uapi/linux/f2fs.h | 7 + 11 files changed, 558 insertions(+), 69 deletions(-) diff --git a/Documentation/filesystems/f2fs.rst b/Documentation/filesystems/f2fs.rst index 8c4a14ae444f..1a5fd4afe609 100644 --- a/Documentation/filesystems/f2fs.rst +++ b/Documentation/filesystems/f2fs.rst @@ -1045,6 +1045,41 @@ So, the key idea is, user can do any file operations on /dev/vdc, and reclaim the space after the use, while the space is counted as /data. That doesn't require modifying partition size and filesystem format. +Dynamic Device Aliasing Management +---------------------------------- + +In addition to static device aliasing by deleting the aliasing file, F2FS +supports dynamic management of device aliasing. This mechanism allows the system +to dynamically transition partition ownership between F2FS userdata and external +entities (e.g., zRAM, raw partition) based on system requirements without +deleting the master aliasing file or requiring unmount/remount. + +The master aliasing file is created during the initial format of the file system +and remains as a persistent control entity (ioctl gateway) in the root directory. + +- Partition Reservation (In-service to Aliased) + When a specific partition needs to be dedicated to external services (e.g., zRAM), + a user can reserve the device alias range via ioctl. The kernel resets GC victim + information for the target range, marks segments as in-use to prevent new + allocations, and triggers forced GC to migrate existing valid data out of the + range. Finally, it reserves these blocks in the SIT to effectively exclude the + device from the usable capacity. + +- Partition Release (Aliased to In-service) + When external usage concludes, the space is reclaimed not by deleting the file, + but through the release ioctl. The kernel truncates blocks associated with + the file, releasing them back to general filesystem allocation. + +.. code-block:: + + # f2fs_io dev_alias release /mnt/f2fs/vdc.file + # df -h + /dev/vdb 64G 753M 64G 2% /mnt/f2fs + + # f2fs_io dev_alias reserve /mnt/f2fs/vdc.file + # df -h + /dev/vdb 64G 33G 32G 52% /mnt/f2fs + Per-file Read-Only Large Folio Support -------------------------------------- diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index 30e8084da313..c219ea76a3a7 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -1270,7 +1270,7 @@ int f2fs_reserve_new_blocks(struct dnode_of_data *dn, blkcnt_t count) if (unlikely(is_inode_flag_set(dn->inode, FI_NO_ALLOC))) return -EPERM; - err = inc_valid_block_count(sbi, dn->inode, &count, true); + err = inc_valid_block_count(sbi, dn->inode, &count, true, false); if (unlikely(err)) return err; @@ -1543,7 +1543,7 @@ static int __allocate_data_block(struct dnode_of_data *dn, int seg_type) dn->data_blkaddr = f2fs_data_blkaddr(dn); if (dn->data_blkaddr == NULL_ADDR) { - err = inc_valid_block_count(sbi, dn->inode, &count, true); + err = inc_valid_block_count(sbi, dn->inode, &count, true, false); if (unlikely(err)) return err; } diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c index 61f6b9714366..37cf9fa8d537 100644 --- a/fs/f2fs/extent_cache.c +++ b/fs/f2fs/extent_cache.c @@ -17,6 +17,7 @@ #include "f2fs.h" #include "node.h" +#include "segment.h" #include bool sanity_check_extent_cache(struct inode *inode, struct folio *ifolio) @@ -62,6 +63,14 @@ bool sanity_check_extent_cache(struct inode *inode, struct folio *ifolio) __func__, inode->i_ino, ei.blk, ei.fofs, ei.len); return false; } + + if ((GET_SEGOFF_FROM_SEG0(sbi, ei.blk) % BLKS_PER_SEC(sbi)) || + (ei.len % BLKS_PER_SEC(sbi))) { + f2fs_warn(sbi, "%s: device alias inode (ino=%llx)'s extent info [%u, %u, %u] is not aligned to section size %u", + __func__, inode->i_ino, ei.blk, ei.fofs, ei.len, + BLKS_PER_SEC(sbi)); + return false; + } return true; } diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 8e2fb0bda467..a380b8be8819 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -1404,6 +1404,8 @@ struct f2fs_dev_info { unsigned int total_segments; block_t start_blk; block_t end_blk; + bool has_alias; + bool is_reserving; #ifdef CONFIG_BLK_DEV_ZONED unsigned int nr_blkz; /* Total number of zones */ unsigned long *blkz_seq; /* Bitmap indicating sequential zones */ @@ -1884,6 +1886,7 @@ struct f2fs_sb_info { block_t last_valid_block_count; /* for recovery */ block_t reserved_blocks; /* configurable reserved blocks */ block_t current_reserved_blocks; /* current reserved blocks */ + block_t alias_reserved_blocks; /* reserved blocks for device alias */ /* Additional tracking for no checkpoint mode */ block_t unusable_block_count; /* # of blocks saved by last cp */ @@ -2586,7 +2589,8 @@ static inline unsigned int get_available_block_count(struct f2fs_sb_info *sbi, block_t avail_user_block_count; avail_user_block_count = sbi->user_block_count - - sbi->current_reserved_blocks; + sbi->current_reserved_blocks - + sbi->alias_reserved_blocks; if (test_opt(sbi, RESERVE_ROOT) && !__allow_reserved_root(sbi, inode, cap)) avail_user_block_count -= F2FS_OPTION(sbi).root_reserved_blocks; @@ -2603,7 +2607,8 @@ static inline unsigned int get_available_block_count(struct f2fs_sb_info *sbi, static inline void f2fs_i_blocks_write(struct inode *, block_t, bool, bool); static inline int inc_valid_block_count(struct f2fs_sb_info *sbi, - struct inode *inode, blkcnt_t *count, bool partial) + struct inode *inode, blkcnt_t *count, + bool partial, bool alias_reserved) { long long diff = 0, release = 0; block_t avail_user_block_count; @@ -2626,10 +2631,16 @@ static inline int inc_valid_block_count(struct f2fs_sb_info *sbi, spin_lock(&sbi->stat_lock); + if (alias_reserved) + sbi->alias_reserved_blocks -= *count; + avail_user_block_count = get_available_block_count(sbi, inode, true); diff = (long long)sbi->total_valid_block_count + *count - avail_user_block_count; if (unlikely(diff > 0)) { + if (alias_reserved) + sbi->alias_reserved_blocks += *count; + if (!partial) { spin_unlock(&sbi->stat_lock); release = *count; @@ -4037,6 +4048,8 @@ int f2fs_flush_device_cache(struct f2fs_sb_info *sbi); void f2fs_destroy_flush_cmd_control(struct f2fs_sb_info *sbi, bool free); void f2fs_invalidate_blocks(struct f2fs_sb_info *sbi, block_t addr, unsigned int len); +void f2fs_reserve_device_alias(struct f2fs_sb_info *sbi, block_t addr, + unsigned int len); bool f2fs_is_checkpointed_data(struct f2fs_sb_info *sbi, block_t blkaddr); int f2fs_start_discard_thread(struct f2fs_sb_info *sbi); void f2fs_drop_discard_cmd(struct f2fs_sb_info *sbi); @@ -4258,6 +4271,8 @@ void f2fs_build_gc_manager(struct f2fs_sb_info *sbi); int f2fs_gc_range(struct f2fs_sb_info *sbi, unsigned int start_seg, unsigned int end_seg, bool dry_run, unsigned int dry_run_sections); +void f2fs_reset_gc_victim_resource(struct f2fs_sb_info *sbi, + unsigned int start, unsigned int end); int f2fs_resize_fs(struct file *filp, __u64 block_count); int __init f2fs_create_garbage_collection_cache(void); void f2fs_destroy_garbage_collection_cache(void); diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index b99d9cdf9ba7..56529a82e027 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -813,13 +813,19 @@ int f2fs_do_truncate_blocks(struct inode *inode, u64 from, bool lock) if (IS_DEVICE_ALIASING(inode)) { struct extent_tree *et = F2FS_I(inode)->extent_tree[EX_READ]; - struct extent_info ei = et->largest; + struct extent_info ei; + + read_lock(&et->lock); + ei = et->largest; + read_unlock(&et->lock); f2fs_invalidate_blocks(sbi, ei.blk, ei.len); dec_valid_block_count(sbi, inode, ei.len); f2fs_update_time(sbi, REQ_TIME); + f2fs_drop_extent_tree(inode); + f2fs_folio_put(ifolio, true); goto out; } @@ -1100,8 +1106,9 @@ int f2fs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, 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)) + if (IS_DEVICE_ALIASING(inode)) + return -EPERM; + if (!f2fs_is_compress_backend_ready(inode)) return -EOPNOTSUPP; if (is_inode_flag_set(inode, FI_COMPRESS_RELEASED) && !IS_ALIGNED(attr->ia_size, @@ -2139,6 +2146,9 @@ static int f2fs_setflags_common(struct inode *inode, u32 iflags, u32 mask) if (IS_NOQUOTA(inode)) return -EPERM; + if (IS_DEVICE_ALIASING(inode)) + return -EPERM; + if ((iflags ^ masked_flags) & F2FS_CASEFOLD_FL) { if (!f2fs_sb_has_casefold(F2FS_I_SB(inode))) return -EOPNOTSUPP; @@ -2687,6 +2697,17 @@ static int f2fs_ioc_get_encryption_policy(struct file *filp, unsigned long arg) return fscrypt_ioctl_get_policy(filp, (void __user *)arg); } +static int f2fs_ioc_get_dev_alias_status(struct file *filp, unsigned long arg) +{ + struct inode *inode = file_inode(filp); + + if (!IS_DEVICE_ALIASING(inode)) + return -EINVAL; + + return put_user(F2FS_HAS_BLOCKS(inode) ? F2FS_DEV_ALIAS_STATUS_RESERVED : + F2FS_DEV_ALIAS_STATUS_RELEASED, (u32 __user *)arg); +} + static int f2fs_ioc_get_encryption_pwsalt(struct file *filp, unsigned long arg) { struct inode *inode = file_inode(filp); @@ -3637,6 +3658,241 @@ static int f2fs_ioc_get_dev_alias_file(struct file *filp, unsigned long arg) (u32 __user *)arg); } +static bool f2fs_get_dev_alias_extent(struct f2fs_sb_info *sbi, + struct dentry *dentry, + struct extent_info *ei) +{ + int i; + + for (i = 1; i < sbi->s_ndevs; i++) { + char *name = strrchr(FDEV(i).path, '/'); + + name = name ? name + 1 : FDEV(i).path; + if (strcmp(name, dentry->d_name.name)) + continue; + + ei->blk = FDEV(i).start_blk; + ei->len = FDEV(i).total_segments << sbi->log_blocks_per_seg; + ei->fofs = 0; + return true; + } + return false; +} + +static int f2fs_ioc_reserve_dev_alias(struct file *filp) +{ + struct inode *inode = file_inode(filp); + struct f2fs_sb_info *sbi = F2FS_I_SB(inode); + struct extent_tree *et = F2FS_I(inode)->extent_tree[EX_READ]; + struct extent_info ei; + struct cp_control cpc = { CP_SYNC, 0, 0, 0 }; + struct f2fs_lock_context lc, glc; + blkcnt_t count; + unsigned int start, end; + int type, err; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (unlikely(is_sbi_flag_set(sbi, SBI_CP_DISABLED))) + return -EINVAL; + + err = mnt_want_write_file(filp); + if (err) + return err; + + inode_lock(inode); + + if (!IS_DEVICE_ALIASING(inode)) { + err = -EINVAL; + goto out_inode_unlock; + } + + if (F2FS_HAS_BLOCKS(inode)) { + err = 0; + goto out_inode_unlock; + } + + if (!f2fs_get_dev_alias_extent(sbi, filp->f_path.dentry, &ei)) { + f2fs_warn(sbi, "device alias file (%s, ino=%llu) has no matching device", + filp->f_path.dentry->d_name.name, + (unsigned long long)inode->i_ino); + set_sbi_flag(sbi, SBI_NEED_FSCK); + f2fs_handle_error(sbi, ERROR_CORRUPTED_INODE); + err = -EFSCORRUPTED; + goto out_inode_unlock; + } + + spin_lock(&sbi->stat_lock); + if (sbi->total_valid_block_count + ei.len > + get_available_block_count(sbi, inode, true)) { + spin_unlock(&sbi->stat_lock); + err = -ENOSPC; + goto out_inode_unlock; + } + sbi->alias_reserved_blocks += ei.len; + spin_unlock(&sbi->stat_lock); + + spin_lock(&FREE_I(sbi)->segmap_lock); + FDEV(f2fs_target_device_index(sbi, ei.blk)).is_reserving = true; + spin_unlock(&FREE_I(sbi)->segmap_lock); + + start = GET_SEGNO(sbi, ei.blk); + end = GET_SEGNO(sbi, ei.blk + ei.len - 1); + + /* Acquire gc_lock for victim reset, curseg resize, and range GC */ + f2fs_down_write_trace(&sbi->gc_lock, &glc); + + /* Reset the victim information to prevent GC from targeting the range */ + f2fs_reset_gc_victim_resource(sbi, start, end); + + /* Move out cursegs from the target range */ + for (type = CURSEG_HOT_DATA; type < NR_CURSEG_PERSIST_TYPE; type++) { + err = f2fs_allocate_segment_for_resize(sbi, type, start, end); + if (err) + goto out_gc_unlock; + } + + f2fs_lock_op(sbi, &lc); + + if (unlikely(is_sbi_flag_set(sbi, SBI_CP_DISABLED))) { + err = -EINVAL; + f2fs_unlock_op(sbi, &lc); + goto out_gc_unlock; + } + + /* do GC to move out valid blocks in the range all at once! */ + err = f2fs_gc_range(sbi, start, end, false, 0); + if (err) { + f2fs_unlock_op(sbi, &lc); + goto out_gc_unlock; + } + + count = ei.len; + err = inc_valid_block_count(sbi, inode, &count, false, true); + if (err) { + f2fs_unlock_op(sbi, &lc); + goto out_gc_unlock; + } + + write_lock(&et->lock); + et->largest = ei; + write_unlock(&et->lock); + clear_inode_flag(inode, FI_NO_EXTENT); + + f2fs_reserve_device_alias(sbi, ei.blk, ei.len); + + i_size_write(inode, (loff_t)ei.len << sbi->log_blocksize); + f2fs_update_inode_page(inode); + + spin_lock(&FREE_I(sbi)->segmap_lock); + FDEV(f2fs_target_device_index(sbi, ei.blk)).is_reserving = false; + spin_unlock(&FREE_I(sbi)->segmap_lock); + + f2fs_unlock_op(sbi, &lc); + f2fs_up_write_trace(&sbi->gc_lock, &glc); + + inode_unlock(inode); + mnt_drop_write_file(filp); + + return f2fs_write_checkpoint(sbi, &cpc); + +out_gc_unlock: + spin_lock(&sbi->stat_lock); + sbi->alias_reserved_blocks -= ei.len; + spin_unlock(&sbi->stat_lock); + + spin_lock(&FREE_I(sbi)->segmap_lock); + FDEV(f2fs_target_device_index(sbi, ei.blk)).is_reserving = false; + spin_unlock(&FREE_I(sbi)->segmap_lock); + f2fs_up_write_trace(&sbi->gc_lock, &glc); + +out_inode_unlock: + inode_unlock(inode); + mnt_drop_write_file(filp); + return err; +} + +static int f2fs_ioc_release_dev_alias(struct file *filp) +{ + struct inode *inode = file_inode(filp); + struct f2fs_sb_info *sbi = F2FS_I_SB(inode); + struct extent_tree *et = F2FS_I(inode)->extent_tree[EX_READ]; + struct extent_info ei = {0, }; + struct cp_control cpc = { CP_SYNC, 0, 0, 0 }; + struct f2fs_lock_context lc, glc; + int err; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (unlikely(is_sbi_flag_set(sbi, SBI_CP_DISABLED))) + return -EINVAL; + + err = mnt_want_write_file(filp); + if (err) + return err; + + inode_lock(inode); + + if (!IS_DEVICE_ALIASING(inode)) { + err = -EINVAL; + goto out_inode_unlock; + } + + if (!F2FS_HAS_BLOCKS(inode)) { + err = 0; + goto out_inode_unlock; + } + + err = filemap_write_and_wait(inode->i_mapping); + if (err) + goto out_inode_unlock; + + read_lock(&et->lock); + ei = et->largest; + read_unlock(&et->lock); + + f2fs_down_write_trace(&sbi->gc_lock, &glc); + f2fs_lock_op(sbi, &lc); + + if (unlikely(is_sbi_flag_set(sbi, SBI_CP_DISABLED))) { + err = -EINVAL; + f2fs_unlock_op(sbi, &lc); + f2fs_up_write_trace(&sbi->gc_lock, &glc); + goto out_inode_unlock; + } + + filemap_invalidate_lock(inode->i_mapping); + truncate_setsize(inode, 0); + + err = f2fs_truncate_blocks(inode, 0, false); + if (err) + i_size_write(inode, (loff_t)ei.len << sbi->log_blocksize); + filemap_invalidate_unlock(inode->i_mapping); + + if (err) { + f2fs_unlock_op(sbi, &lc); + f2fs_up_write_trace(&sbi->gc_lock, &glc); + goto out_inode_unlock; + } + + f2fs_update_inode_page(inode); + + f2fs_unlock_op(sbi, &lc); + f2fs_up_write_trace(&sbi->gc_lock, &glc); + + inode_unlock(inode); + mnt_drop_write_file(filp); + + return f2fs_write_checkpoint(sbi, &cpc); + +out_inode_unlock: + inode_unlock(inode); + mnt_drop_write_file(filp); + return err; +} + static int f2fs_ioc_io_prio(struct file *filp, unsigned long arg) { struct inode *inode = file_inode(filp); @@ -4062,7 +4318,7 @@ static int reserve_compress_blocks(struct dnode_of_data *dn, pgoff_t count, } ret = inc_valid_block_count(sbi, dn->inode, - &to_reserved, false); + &to_reserved, false, false); if (unlikely(ret)) return ret; @@ -4763,8 +5019,14 @@ static long __f2fs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) return f2fs_ioc_compress_file(filp); case F2FS_IOC_GET_DEV_ALIAS_FILE: return f2fs_ioc_get_dev_alias_file(filp, arg); + case F2FS_IOC_GET_DEV_ALIAS_STATUS: + return f2fs_ioc_get_dev_alias_status(filp, arg); case F2FS_IOC_IO_PRIO: return f2fs_ioc_io_prio(filp, arg); + case F2FS_IOC_RESERVE_DEV_ALIAS: + return f2fs_ioc_reserve_dev_alias(filp); + case F2FS_IOC_RELEASE_DEV_ALIAS: + return f2fs_ioc_release_dev_alias(filp); default: return -ENOTTY; } @@ -5551,7 +5813,10 @@ long f2fs_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg) case F2FS_IOC_DECOMPRESS_FILE: case F2FS_IOC_COMPRESS_FILE: case F2FS_IOC_GET_DEV_ALIAS_FILE: + case F2FS_IOC_GET_DEV_ALIAS_STATUS: case F2FS_IOC_IO_PRIO: + case F2FS_IOC_RESERVE_DEV_ALIAS: + case F2FS_IOC_RELEASE_DEV_ALIAS: break; default: return -ENOIOCTLCMD; diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index d04633f872ef..51fc8c69eb25 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -2191,29 +2191,37 @@ int f2fs_gc_range(struct f2fs_sb_info *sbi, return 0; } +void f2fs_reset_gc_victim_resource(struct f2fs_sb_info *sbi, + unsigned int start, unsigned int end) +{ + int i; + + mutex_lock(&DIRTY_I(sbi)->seglist_lock); + for (i = 0; i < MAX_GC_POLICY; i++) + if (SIT_I(sbi)->last_victim[i] >= start && + SIT_I(sbi)->last_victim[i] <= end) + SIT_I(sbi)->last_victim[i] = 0; + + for (i = BG_GC; i <= FG_GC; i++) + if (sbi->next_victim_seg[i] >= start && + sbi->next_victim_seg[i] <= end) + sbi->next_victim_seg[i] = NULL_SEGNO; + mutex_unlock(&DIRTY_I(sbi)->seglist_lock); +} + static int free_segment_range(struct f2fs_sb_info *sbi, unsigned int secs, bool dry_run) { unsigned int next_inuse, start, end; struct cp_control cpc = { CP_RESIZE, 0, 0, 0 }; - int gc_mode, gc_type; int err = 0; int type; - /* Force block allocation for GC */ MAIN_SECS(sbi) -= secs; start = MAIN_SECS(sbi) * SEGS_PER_SEC(sbi); end = MAIN_SEGS(sbi) - 1; - mutex_lock(&DIRTY_I(sbi)->seglist_lock); - for (gc_mode = 0; gc_mode < MAX_GC_POLICY; gc_mode++) - if (SIT_I(sbi)->last_victim[gc_mode] >= start) - SIT_I(sbi)->last_victim[gc_mode] = 0; - - for (gc_type = BG_GC; gc_type <= FG_GC; gc_type++) - if (sbi->next_victim_seg[gc_type] >= start) - sbi->next_victim_seg[gc_type] = NULL_SEGNO; - mutex_unlock(&DIRTY_I(sbi)->seglist_lock); + f2fs_reset_gc_victim_resource(sbi, start, end); /* Move out cursegs from the target range */ for (type = CURSEG_HOT_DATA; type < NR_CURSEG_PERSIST_TYPE; type++) { diff --git a/fs/f2fs/namei.c b/fs/f2fs/namei.c index 7ffdf23cea5e..784f636244e1 100644 --- a/fs/f2fs/namei.c +++ b/fs/f2fs/namei.c @@ -425,6 +425,9 @@ static int f2fs_link(struct dentry *old_dentry, struct inode *dir, if (!f2fs_is_checkpoint_ready(sbi)) return -ENOSPC; + if (IS_DEVICE_ALIASING(inode)) + return -EPERM; + err = fscrypt_prepare_link(old_dentry, dir, dentry); if (err) return err; @@ -568,6 +571,9 @@ static int f2fs_unlink(struct inode *dir, struct dentry *dentry) trace_f2fs_unlink_enter(dir, dentry); + if (IS_DEVICE_ALIASING(inode)) + return -EPERM; + if (unlikely(f2fs_cp_error(sbi))) { err = -EIO; goto out; @@ -946,6 +952,9 @@ static int f2fs_rename(struct mnt_idmap *idmap, struct inode *old_dir, bool old_is_dir = S_ISDIR(old_inode->i_mode); int err; + if (IS_DEVICE_ALIASING(old_inode)) + return -EPERM; + if (unlikely(f2fs_cp_error(sbi))) return -EIO; if (!f2fs_is_checkpoint_ready(sbi)) @@ -1016,6 +1025,8 @@ static int f2fs_rename(struct mnt_idmap *idmap, struct inode *old_dir, } if (new_inode) { + if (IS_DEVICE_ALIASING(new_inode)) + return -EPERM; err = -ENOTEMPTY; if (old_is_dir && !f2fs_empty_dir(new_inode)) @@ -1143,6 +1154,9 @@ static int f2fs_cross_rename(struct inode *old_dir, struct dentry *old_dentry, int old_nlink = 0, new_nlink = 0; int err; + if (IS_DEVICE_ALIASING(old_inode) || IS_DEVICE_ALIASING(new_inode)) + return -EPERM; + if (unlikely(f2fs_cp_error(sbi))) return -EIO; if (!f2fs_is_checkpoint_ready(sbi)) diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 00cc2af45ff9..b81245ddd490 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -261,7 +261,7 @@ static int __replace_atomic_write_block(struct inode *inode, pgoff_t index, } else { blkcnt_t count = 1; - err = inc_valid_block_count(sbi, inode, &count, true); + err = inc_valid_block_count(sbi, inode, &count, true, false); if (err) { f2fs_put_dnode(&dn); return err; @@ -2539,35 +2539,42 @@ 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; + int del_count = del; + int i; - exist = f2fs_test_and_set_bit(offset, se->cur_valid_map); - if (unlikely(exist)) { - f2fs_err(sbi, "Bitmap was wrongly set, blk:%u", blkaddr); - f2fs_bug_on(sbi, 1); - se->valid_blocks--; - del = 0; - } + f2fs_bug_on(sbi, GET_SEGNO(sbi, blkaddr) != GET_SEGNO(sbi, blkaddr + del_count - 1)); - if (f2fs_block_unit_discard(sbi) && - !f2fs_test_and_set_bit(offset, se->discard_map)) - sbi->discard_blks--; - - /* - * SSR should never reuse block which is checkpointed - * or newly invalidated. - */ - if (!is_sbi_flag_set(sbi, SBI_CP_DISABLED)) { - if (!f2fs_test_and_set_bit(offset, se->ckpt_valid_map)) { - se->ckpt_valid_blocks++; - if (__is_large_section(sbi)) - get_sec_entry(sbi, segno)->ckpt_valid_blocks++; + for (i = 0; i < del_count; i++) { + exist = f2fs_test_and_set_bit(offset + i, se->cur_valid_map); + if (unlikely(exist)) { + f2fs_err(sbi, "Bitmap was wrongly set, blk:%u", blkaddr + i); + f2fs_bug_on(sbi, 1); + se->valid_blocks--; + del -= 1; + continue; } - } - if (!f2fs_test_bit(offset, se->ckpt_valid_map)) { - se->ckpt_valid_blocks += del; - if (__is_large_section(sbi)) - get_sec_entry(sbi, segno)->ckpt_valid_blocks += del; + if (f2fs_block_unit_discard(sbi) && + !f2fs_test_and_set_bit(offset + i, se->discard_map)) + sbi->discard_blks--; + + /* + * SSR should never reuse block which is checkpointed + * or newly invalidated. + */ + if (!is_sbi_flag_set(sbi, SBI_CP_DISABLED)) { + if (!f2fs_test_and_set_bit(offset + i, se->ckpt_valid_map)) { + se->ckpt_valid_blocks++; + if (__is_large_section(sbi)) + get_sec_entry(sbi, segno)->ckpt_valid_blocks++; + } + } + + if (!f2fs_test_bit(offset + i, se->ckpt_valid_map)) { + se->ckpt_valid_blocks += 1; + if (__is_large_section(sbi)) + get_sec_entry(sbi, segno)->ckpt_valid_blocks += 1; + } } if (__is_large_section(sbi)) @@ -2622,9 +2629,14 @@ void f2fs_invalidate_blocks(struct f2fs_sb_info *sbi, block_t addr, unsigned int segno = GET_SEGNO(sbi, addr); struct sit_info *sit_i = SIT_I(sbi); block_t addr_start = addr, addr_end = addr + len - 1; - unsigned int seg_num = GET_SEGNO(sbi, addr_end) - segno + 1; + unsigned int seg_num; unsigned int i = 1, max_blocks = sbi->blocks_per_seg, cnt; + if (len == 0) + return; + + seg_num = GET_SEGNO(sbi, addr_end) - segno + 1; + f2fs_bug_on(sbi, addr == NULL_ADDR); if (addr == NEW_ADDR || addr == COMPRESS_ADDR) return; @@ -2657,6 +2669,52 @@ void f2fs_invalidate_blocks(struct f2fs_sb_info *sbi, block_t addr, up_write(&sit_i->sentry_lock); } +void f2fs_reserve_device_alias(struct f2fs_sb_info *sbi, block_t addr, + unsigned int len) +{ + unsigned int segno = GET_SEGNO(sbi, addr); + struct sit_info *sit_i = SIT_I(sbi); + block_t addr_start = addr, addr_end = addr + len - 1; + unsigned int seg_num; + unsigned int i = 1, max_blocks = sbi->blocks_per_seg, cnt; + + if (len == 0) + return; + + seg_num = GET_SEGNO(sbi, addr_end) - segno + 1; + + down_write(&sit_i->sentry_lock); + + if (seg_num == 1) + cnt = len; + else + cnt = max_blocks - GET_BLKOFF_FROM_SEG0(sbi, addr); + + do { + update_segment_mtime(sbi, addr_start, 0); + update_sit_entry(sbi, addr_start, cnt); + __set_test_and_inuse(sbi, segno); + + /* Remove the segment from PRE (prefree) to prevent checkpoint from freeing it! */ + mutex_lock(&DIRTY_I(sbi)->seglist_lock); + if (test_and_clear_bit(segno, DIRTY_I(sbi)->dirty_segmap[PRE])) + DIRTY_I(sbi)->nr_dirty[PRE]--; + mutex_unlock(&DIRTY_I(sbi)->seglist_lock); + + /* add it into dirty seglist */ + locate_dirty_segment(sbi, segno); + + /* update @addr_start and @cnt and @segno */ + addr_start = START_BLOCK(sbi, ++segno); + if (++i == seg_num) + cnt = GET_BLKOFF_FROM_SEG0(sbi, addr_end) + 1; + else + cnt = max_blocks; + } while (i <= seg_num); + + up_write(&sit_i->sentry_lock); +} + bool f2fs_is_checkpointed_data(struct f2fs_sb_info *sbi, block_t blkaddr) { struct sit_info *sit_i = SIT_I(sbi); @@ -2795,8 +2853,13 @@ static int is_next_segment_free(struct f2fs_sb_info *sbi, unsigned int segno = curseg->segno + 1; struct free_segmap_info *free_i = FREE_I(sbi); - if (segno < MAIN_SEGS(sbi) && segno % SEGS_PER_SEC(sbi)) + if (segno < MAIN_SEGS(sbi) && segno % SEGS_PER_SEC(sbi)) { + int devi = f2fs_target_device_index(sbi, START_BLOCK(sbi, segno)); + + if (f2fs_dev_is_reserving(sbi, devi)) + return 0; return !test_bit(segno, free_i->free_segmap); + } return 0; } @@ -2815,7 +2878,8 @@ static int get_new_segment(struct f2fs_sb_info *sbi, unsigned int alloc_policy = sbi->allocate_section_policy; unsigned int alloc_hint = sbi->allocate_section_hint; bool init = true; - int i; + bool looped = false; + int i, devi; int ret = 0; spin_lock(&free_i->segmap_lock); @@ -2828,8 +2892,13 @@ static int get_new_segment(struct f2fs_sb_info *sbi, if (!new_sec && ((*newseg + 1) % SEGS_PER_SEC(sbi))) { segno = find_next_zero_bit(free_i->free_segmap, GET_SEG_FROM_SEC(sbi, hint + 1), *newseg + 1); - if (segno < GET_SEG_FROM_SEC(sbi, hint + 1)) + if (segno < GET_SEG_FROM_SEC(sbi, hint + 1)) { + devi = f2fs_target_device_index(sbi, START_BLOCK(sbi, segno)); + + if (f2fs_dev_is_alloc_blocked(sbi, devi, pinning)) + goto find_other_zone; goto got_it; + } } #ifdef CONFIG_BLK_DEV_ZONED @@ -2865,33 +2934,42 @@ static int get_new_segment(struct f2fs_sb_info *sbi, find_other_zone: secno = find_next_zero_bit(free_i->free_secmap, MAIN_SECS(sbi), hint); -#ifdef CONFIG_BLK_DEV_ZONED - if (secno >= MAIN_SECS(sbi) && f2fs_sb_has_blkzoned(sbi)) { - /* Write only to sequential zones */ - if (sbi->blkzone_alloc_policy == BLKZONE_ALLOC_ONLY_SEQ) { - hint = GET_SEC_FROM_SEG(sbi, sbi->first_seq_zone_segno); - secno = find_next_zero_bit(free_i->free_secmap, MAIN_SECS(sbi), hint); - } else - secno = find_first_zero_bit(free_i->free_secmap, - MAIN_SECS(sbi)); - if (secno >= MAIN_SECS(sbi)) { - ret = -ENOSPC; - f2fs_bug_on(sbi, 1); - goto out_unlock; - } - } -#endif - if (secno >= MAIN_SECS(sbi)) { - secno = find_first_zero_bit(free_i->free_secmap, - MAIN_SECS(sbi)); - if (secno >= MAIN_SECS(sbi)) { + if (looped) { ret = -ENOSPC; f2fs_bug_on(sbi, !pinning); goto out_unlock; } + hint = 0; +#ifdef CONFIG_BLK_DEV_ZONED + /* Write only to sequential zones */ + if (f2fs_sb_has_blkzoned(sbi) && + sbi->blkzone_alloc_policy == BLKZONE_ALLOC_ONLY_SEQ) + hint = GET_SEC_FROM_SEG(sbi, sbi->first_seq_zone_segno); +#endif + looped = true; + goto find_other_zone; } + segno = GET_SEG_FROM_SEC(sbi, secno); + + devi = f2fs_target_device_index(sbi, START_BLOCK(sbi, segno)); + + if (f2fs_dev_is_alloc_blocked(sbi, devi, pinning)) { + while (devi < sbi->s_ndevs && + f2fs_dev_is_alloc_blocked(sbi, devi, pinning)) { + unsigned int end_segno = GET_SEGNO(sbi, FDEV(devi).end_blk); + + hint = GET_SEC_FROM_SEG(sbi, end_segno) + 1; + devi++; + } + goto find_other_zone; + } + + if (sec_usage_check(sbi, secno)) { + hint = secno + 1; + goto find_other_zone; + } zoneno = GET_ZONE_FROM_SEC(sbi, secno); /* give up on finding another zone */ diff --git a/fs/f2fs/segment.h b/fs/f2fs/segment.h index 33a2257da1e6..db1079169a23 100644 --- a/fs/f2fs/segment.h +++ b/fs/f2fs/segment.h @@ -940,10 +940,32 @@ static inline block_t sum_blk_addr(struct f2fs_sb_info *sbi, int base, int type) - (base + 1) + type; } +static inline bool f2fs_dev_is_reserving(struct f2fs_sb_info *sbi, int devi) +{ + if (!f2fs_sb_has_device_alias(sbi)) + return false; + return FDEV(devi).is_reserving; +} + +static inline bool f2fs_dev_is_alloc_blocked(struct f2fs_sb_info *sbi, + int devi, bool pinning) +{ + if (!f2fs_sb_has_device_alias(sbi)) + return false; + return (pinning && FDEV(devi).has_alias) || FDEV(devi).is_reserving; +} + static inline bool sec_usage_check(struct f2fs_sb_info *sbi, unsigned int secno) { if (is_cursec(sbi, secno) || (sbi->cur_victim_sec == secno)) return true; + if (f2fs_sb_has_device_alias(sbi)) { + block_t start_blk = START_BLOCK(sbi, GET_SEG_FROM_SEC(sbi, secno)); + int devi = f2fs_target_device_index(sbi, start_blk); + + if (f2fs_dev_is_reserving(sbi, devi)) + return true; + } return false; } diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index 5902b2da7ea4..ed9e0ba9cf0b 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -5005,6 +5005,39 @@ static void f2fs_tuning_parameters(struct f2fs_sb_info *sbi) sbi->readdir_ra = true; } +static void f2fs_restore_device_alias(struct f2fs_sb_info *sbi) +{ + struct inode *root = d_inode(sbi->sb->s_root); + struct f2fs_dir_entry *de; + struct folio *folio; + int i; + + if (!f2fs_sb_has_device_alias(sbi)) + return; + + for (i = 1; i < sbi->s_ndevs; i++) { + char *name = strrchr(FDEV(i).path, '/'); + struct inode *inode; + struct qstr qstr; + + name = name ? name + 1 : FDEV(i).path; + qstr.name = name; + qstr.len = strlen(name); + + de = f2fs_find_entry(root, &qstr, &folio); + if (!de) + continue; + + inode = f2fs_iget(sbi->sb, le32_to_cpu(de->ino)); + if (!IS_ERR(inode)) { + if (IS_DEVICE_ALIASING(inode)) + FDEV(i).has_alias = true; + iput(inode); + } + f2fs_folio_put(folio, 0); + } +} + static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc) { struct f2fs_fs_context *ctx = fc->fs_private; @@ -5209,6 +5242,7 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc) sbi->last_valid_block_count = sbi->total_valid_block_count; sbi->reserved_blocks = 0; sbi->current_reserved_blocks = 0; + sbi->alias_reserved_blocks = 0; limit_reserve_root(sbi); adjust_unusable_cap_perc(sbi); @@ -5436,6 +5470,8 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc) f2fs_update_time(sbi, REQ_TIME); clear_sbi_flag(sbi, SBI_CP_DISABLED_QUICK); + f2fs_restore_device_alias(sbi); + sbi->umount_lock_holder = NULL; return 0; diff --git a/include/uapi/linux/f2fs.h b/include/uapi/linux/f2fs.h index 795e26258355..4409ada2fecb 100644 --- a/include/uapi/linux/f2fs.h +++ b/include/uapi/linux/f2fs.h @@ -45,6 +45,9 @@ #define F2FS_IOC_START_ATOMIC_REPLACE _IO(F2FS_IOCTL_MAGIC, 25) #define F2FS_IOC_GET_DEV_ALIAS_FILE _IOR(F2FS_IOCTL_MAGIC, 26, __u32) #define F2FS_IOC_IO_PRIO _IOW(F2FS_IOCTL_MAGIC, 27, __u32) +#define F2FS_IOC_RESERVE_DEV_ALIAS _IO(F2FS_IOCTL_MAGIC, 28) +#define F2FS_IOC_RELEASE_DEV_ALIAS _IO(F2FS_IOCTL_MAGIC, 29) +#define F2FS_IOC_GET_DEV_ALIAS_STATUS _IOR(F2FS_IOCTL_MAGIC, 30, __u32) /* * should be same as XFS_IOC_GOINGDOWN. @@ -70,6 +73,10 @@ enum { F2FS_IOPRIO_MAX, }; +/* for F2FS_IOC_GET_DEV_ALIAS_STATUS */ +#define F2FS_DEV_ALIAS_STATUS_RELEASED 0 +#define F2FS_DEV_ALIAS_STATUS_RESERVED 1 + struct f2fs_gc_range { __u32 sync; __u64 start; From 0d7477640f44851a0bcb82d220e2f451fa67864e Mon Sep 17 00:00:00 2001 From: Jaegeuk Kim Date: Fri, 7 Aug 2026 21:31:11 +0000 Subject: [PATCH 29/51] f2fs: refactor f2fs_evict_inode having three major parts 1. f2fs_pre_evict_inode() : drop all in-memory structures 2. f2fs_delete_inode() : truncate inode blocks, if it was unlinked. 3. f2fs_post_evict_inode() : update inode records for future access Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/inode.c | 132 +++++++++++++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 53 deletions(-) diff --git a/fs/f2fs/inode.c b/fs/f2fs/inode.c index c95e0b126da4..553b1e338aa1 100644 --- a/fs/f2fs/inode.c +++ b/fs/f2fs/inode.c @@ -855,15 +855,12 @@ void f2fs_remove_donate_inode(struct inode *inode) } /* - * Called at the last iput() if i_nlink is zero + * Return true, if we shouldn't go through post_evict_inode. */ -void f2fs_evict_inode(struct inode *inode) +static bool f2fs_pre_evict_inode(struct inode *inode) { struct f2fs_sb_info *sbi = F2FS_I_SB(inode); struct f2fs_inode_info *fi = F2FS_I(inode); - nid_t xnid = fi->i_xattr_nid; - int err = 0; - bool freeze_protected = false; f2fs_abort_atomic_write(inode, true); @@ -883,13 +880,13 @@ void f2fs_evict_inode(struct inode *inode) truncate_inode_pages_final(&inode->i_data); if ((inode->i_nlink || is_bad_inode(inode)) && - test_opt(sbi, COMPRESS_CACHE) && f2fs_compressed_file(inode)) + test_opt(sbi, COMPRESS_CACHE) && f2fs_compressed_file(inode)) f2fs_invalidate_compress_pages(sbi, inode->i_ino); if (inode->i_ino == F2FS_NODE_INO(sbi) || - inode->i_ino == F2FS_META_INO(sbi) || - inode->i_ino == F2FS_COMPRESS_INO(sbi)) - goto out_clear; + inode->i_ino == F2FS_META_INO(sbi) || + inode->i_ino == F2FS_COMPRESS_INO(sbi)) + return true; f2fs_bug_on(sbi, get_dirty_pages(inode)); f2fs_remove_dirty_inode(inode); @@ -898,14 +895,18 @@ void f2fs_evict_inode(struct inode *inode) if (!IS_DEVICE_ALIASING(inode)) f2fs_destroy_extent_tree(inode); - if (inode->i_nlink || is_bad_inode(inode)) - goto no_delete; + return false; +} - err = f2fs_dquot_initialize(inode); - if (err) { - err = 0; +static void f2fs_delete_inode(struct inode *inode) +{ + struct f2fs_sb_info *sbi = F2FS_I_SB(inode); + bool freeze_protected = false; + struct f2fs_lock_context lc; + int err = 0; + + if (f2fs_dquot_initialize(inode)) set_sbi_flag(sbi, SBI_QUOTA_NEED_REPAIR); - } f2fs_remove_ino_entry(sbi, inode->i_ino, APPEND_INO); f2fs_remove_ino_entry(sbi, inode->i_ino, UPDATE_INO); @@ -924,30 +925,30 @@ void f2fs_evict_inode(struct inode *inode) if (time_to_inject(sbi, FAULT_EVICT_INODE)) err = -EIO; - if (!err) { - struct f2fs_lock_context lc; + if (err) + goto error_check; - f2fs_lock_op(sbi, &lc); - err = f2fs_remove_inode_page(inode); - f2fs_unlock_op(sbi, &lc); - if (err == -ENOENT) { - err = 0; + f2fs_lock_op(sbi, &lc); + err = f2fs_remove_inode_page(inode); + f2fs_unlock_op(sbi, &lc); - /* - * in fuzzed image, another node may has the same - * block address as inode's, if it was truncated - * previously, truncation of inode node will fail. - */ - if (is_inode_flag_set(inode, FI_DIRTY_INODE)) { - f2fs_warn(F2FS_I_SB(inode), - "f2fs_evict_inode: inconsistent node id, ino:%llu", - inode->i_ino); - f2fs_inode_synced(inode); - set_sbi_flag(sbi, SBI_NEED_FSCK); - } + if (err == -ENOENT) { + err = 0; + + /* + * in fuzzed image, another node may has the same + * block address as inode's, if it was truncated + * previously, truncation of inode node will fail. + */ + if (is_inode_flag_set(inode, FI_DIRTY_INODE)) { + f2fs_warn(F2FS_I_SB(inode), + "f2fs_evict_inode: inconsistent node id, ino:%llu", + inode->i_ino); + f2fs_inode_synced(inode); + set_sbi_flag(sbi, SBI_NEED_FSCK); } } - +error_check: /* give more chances, if ENOMEM case */ if (err == -ENOMEM) { err = 0; @@ -957,27 +958,37 @@ void f2fs_evict_inode(struct inode *inode) if (IS_DEVICE_ALIASING(inode)) f2fs_destroy_extent_tree(inode); - if (err) { - f2fs_update_inode_page(inode); - if (dquot_initialize_needed(inode)) - set_sbi_flag(sbi, SBI_QUOTA_NEED_REPAIR); + if (!err) + goto unfreeze_out; - /* - * If both f2fs_truncate() and f2fs_update_inode_page() failed - * due to fuzzed corrupted inode, call f2fs_inode_synced() to - * avoid triggering later f2fs_bug_on(). - */ - if (is_inode_flag_set(inode, FI_DIRTY_INODE)) { - f2fs_warn(sbi, - "f2fs_evict_inode: inode is dirty, ino:%llu", - inode->i_ino); - f2fs_inode_synced(inode); - set_sbi_flag(sbi, SBI_NEED_FSCK); - } + f2fs_update_inode_page(inode); + + if (dquot_initialize_needed(inode)) + set_sbi_flag(sbi, SBI_QUOTA_NEED_REPAIR); + + /* + * If both f2fs_truncate() and f2fs_update_inode_page() failed + * due to fuzzed corrupted inode, call f2fs_inode_synced() to + * avoid triggering later f2fs_bug_on(). + */ + if (is_inode_flag_set(inode, FI_DIRTY_INODE)) { + f2fs_warn(sbi, + "f2fs_evict_inode: inode is dirty, ino:%llu", + inode->i_ino); + f2fs_inode_synced(inode); + set_sbi_flag(sbi, SBI_NEED_FSCK); } +unfreeze_out: if (freeze_protected) sb_end_intwrite(inode->i_sb); -no_delete: +} + +static void f2fs_post_evict_inode(struct inode *inode) +{ + struct f2fs_sb_info *sbi = F2FS_I_SB(inode); + struct f2fs_inode_info *fi = F2FS_I(inode); + nid_t xnid = fi->i_xattr_nid; + dquot_drop(inode); stat_dec_inline_xattr(inode); @@ -1019,7 +1030,22 @@ void f2fs_evict_inode(struct inode *inode) * In that case, f2fs_check_nid_range() is enough to give a clue. */ } -out_clear: +} + +/* + * Called at the last iput() if i_nlink is zero + */ +void f2fs_evict_inode(struct inode *inode) +{ + if (f2fs_pre_evict_inode(inode)) + goto clear_out; + + if (!inode->i_nlink && !is_bad_inode(inode)) + f2fs_delete_inode(inode); + + f2fs_post_evict_inode(inode); + +clear_out: fscrypt_put_encryption_info(inode); clear_inode(inode); } From 314c9e476ffcc77289c25d354c611aa6d1209784 Mon Sep 17 00:00:00 2001 From: Jaegeuk Kim Date: Mon, 3 Aug 2026 20:32:30 +0000 Subject: [PATCH 30/51] f2fs: call __add_ino_entry out of the eviction path The f2fs_evict_inode() can be called during the direct reclaim path, but __add_ino_entry requires allocating some memory. Since we don't need to do that in that context, let's migrate it in other workqueue context. Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/checkpoint.c | 11 ++++++ fs/f2fs/data.c | 13 ++++++- fs/f2fs/f2fs.h | 4 ++ fs/f2fs/inode.c | 92 +++++++++++++++++++++++++++++++++++++++++--- fs/f2fs/super.c | 9 ++++- 5 files changed, 122 insertions(+), 7 deletions(-) diff --git a/fs/f2fs/checkpoint.c b/fs/f2fs/checkpoint.c index 064f5b537423..4413eccb5ecb 100644 --- a/fs/f2fs/checkpoint.c +++ b/fs/f2fs/checkpoint.c @@ -766,6 +766,15 @@ static void __remove_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, int type) spin_unlock(&im->ino_lock); } +static void f2fs_wait_for_inode_record(struct f2fs_sb_info *sbi, int mode) +{ + if (mode != APPEND_INO && mode != UPDATE_INO) + return; + + /* Let's wait for some pending updates for APPEND_INO and UPDATE_INO. */ + flush_workqueue(sbi->evict_wq); +} + void f2fs_add_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, int type) { /* add new dirty ino entry into list */ @@ -798,6 +807,8 @@ void f2fs_release_ino_entry(struct f2fs_sb_info *sbi, bool all) for (i = all ? ORPHAN_INO : APPEND_INO; i < MAX_INO_ENTRY; i++) { struct inode_management *im = &sbi->im[i]; + f2fs_wait_for_inode_record(sbi, i); + spin_lock(&im->ino_lock); list_for_each_entry_safe(e, tmp, &im->ino_list, list) { list_del(&e->list); diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c index c219ea76a3a7..6ae0eb37d20f 100644 --- a/fs/f2fs/data.c +++ b/fs/f2fs/data.c @@ -4558,13 +4558,24 @@ int f2fs_init_wq(struct f2fs_sb_info *sbi) { sbi->wq = alloc_workqueue("f2fs_wq", WQ_UNBOUND | WQ_HIGHPRI, num_online_cpus()); - return sbi->wq ? 0 : -ENOMEM; + if (!sbi->wq) + return -ENOMEM; + + sbi->evict_wq = alloc_workqueue("f2fs_evict_wq", + WQ_UNBOUND | WQ_HIGHPRI, num_online_cpus()); + if (!sbi->evict_wq) { + destroy_workqueue(sbi->wq); + return -ENOMEM; + } + return 0; } void f2fs_destroy_wq(struct f2fs_sb_info *sbi) { if (sbi->wq) destroy_workqueue(sbi->wq); + if (sbi->evict_wq) + destroy_workqueue(sbi->evict_wq); } int __init f2fs_init_bio_entry_cache(void) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index a380b8be8819..7fd3a181af61 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -2013,6 +2013,8 @@ struct f2fs_sb_info { struct workqueue_struct *wq; /* bio completion workqueue */ + struct workqueue_struct *evict_wq; /* inode eviction workqueue */ + /* * If we are in irq context, let's update error information into * on-disk superblock in the work. @@ -3872,6 +3874,8 @@ int f2fs_write_inode(struct inode *inode, struct writeback_control *wbc); void f2fs_remove_donate_inode(struct inode *inode); void f2fs_evict_inode(struct inode *inode); void f2fs_handle_failed_inode(struct inode *inode, struct f2fs_lock_context *lc); +int f2fs_init_evict_inode_work(void); +void f2fs_destroy_evict_inode_work(void); /* * namei.c diff --git a/fs/f2fs/inode.c b/fs/f2fs/inode.c index 553b1e338aa1..bac1e360d966 100644 --- a/fs/f2fs/inode.c +++ b/fs/f2fs/inode.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,18 @@ extern const struct address_space_operations f2fs_compress_aops; #endif +#define NUM_PREALLOC_EVICT_INODE_WORK 8 + +static struct kmem_cache *evict_inode_work_cache; +static mempool_t *evict_inode_work_pool; + +struct evict_inode_work { + struct work_struct work; + struct f2fs_sb_info *sbi; + nid_t ino; + unsigned int add_ino_entry_bits; +}; + void f2fs_mark_inode_dirty_sync(struct inode *inode, bool sync) { if (is_inode_flag_set(inode, FI_NEW_INODE)) @@ -637,6 +650,9 @@ struct inode *f2fs_iget(struct super_block *sb, unsigned long ino) inode->i_fop = &f2fs_dir_operations; inode->i_mapping->a_ops = &f2fs_dblock_aops; mapping_set_gfp_mask(inode->i_mapping, GFP_NOFS); + + /* Let's prepare APPEND/UPDATE_INO before future access. */ + flush_workqueue(sbi->evict_wq); } else if (S_ISLNK(inode->i_mode)) { if (file_is_encrypt(inode)) inode->i_op = &f2fs_encrypted_symlink_inode_operations; @@ -854,6 +870,25 @@ void f2fs_remove_donate_inode(struct inode *inode) spin_unlock(&sbi->inode_lock[DONATE_INODE]); } +static void f2fs_record_inode_state(struct f2fs_sb_info *sbi, nid_t ino, + unsigned int bits) +{ + if (bits & BIT(APPEND_INO)) + f2fs_add_ino_entry(sbi, ino, APPEND_INO); + if (bits & BIT(UPDATE_INO)) + f2fs_add_ino_entry(sbi, ino, UPDATE_INO); +} + +static void f2fs_evict_inode_work(struct work_struct *work) +{ + struct evict_inode_work *ew = + container_of(work, struct evict_inode_work, work); + + f2fs_record_inode_state(ew->sbi, ew->ino, ew->add_ino_entry_bits); + + mempool_free(ew, evict_inode_work_pool); +} + /* * Return true, if we shouldn't go through post_evict_inode. */ @@ -988,6 +1023,7 @@ static void f2fs_post_evict_inode(struct inode *inode) struct f2fs_sb_info *sbi = F2FS_I_SB(inode); struct f2fs_inode_info *fi = F2FS_I(inode); nid_t xnid = fi->i_xattr_nid; + unsigned int record_bits = 0; dquot_drop(inode); @@ -1014,12 +1050,32 @@ static void f2fs_post_evict_inode(struct inode *inode) inode->i_ino); if (xnid) invalidate_mapping_pages(NODE_MAPPING(sbi), xnid, xnid); - if (inode->i_nlink) { - if (is_inode_flag_set(inode, FI_APPEND_WRITE)) - f2fs_add_ino_entry(sbi, inode->i_ino, APPEND_INO); - if (is_inode_flag_set(inode, FI_UPDATE_WRITE)) - f2fs_add_ino_entry(sbi, inode->i_ino, UPDATE_INO); + + if (!inode->i_nlink) + goto skip_record; + + if (is_inode_flag_set(inode, FI_APPEND_WRITE)) + record_bits = BIT(APPEND_INO); + if (is_inode_flag_set(inode, FI_UPDATE_WRITE)) + record_bits = BIT(UPDATE_INO); + + if (!record_bits) + goto skip_record; + + /* Let's do this in workqueue out of the direct reclaim path. */ + if (current_is_kswapd()) { + f2fs_record_inode_state(sbi, inode->i_ino, record_bits); + } else { + struct evict_inode_work *ew = + mempool_alloc(evict_inode_work_pool, GFP_NOFS); + + ew->sbi = sbi; + ew->ino = inode->i_ino; + ew->add_ino_entry_bits = record_bits; + INIT_WORK(&ew->work, f2fs_evict_inode_work); + queue_work(sbi->evict_wq, &ew->work); } +skip_record: if (is_inode_flag_set(inode, FI_FREE_NID)) { f2fs_alloc_nid_failed(sbi, inode->i_ino); clear_inode_flag(inode, FI_FREE_NID); @@ -1105,3 +1161,29 @@ void f2fs_handle_failed_inode(struct inode *inode, struct f2fs_lock_context *lc) /* iput will drop the inode object */ iput(inode); } + +int __init f2fs_init_evict_inode_work(void) +{ + evict_inode_work_cache = + kmem_cache_create("f2fs_evict_inode_work", + sizeof(struct evict_inode_work), 0, 0, NULL); + if (!evict_inode_work_cache) + goto fail; + evict_inode_work_pool = + mempool_create_slab_pool(NUM_PREALLOC_EVICT_INODE_WORK, + evict_inode_work_cache); + if (!evict_inode_work_pool) + goto fail_free_cache; + return 0; + +fail_free_cache: + kmem_cache_destroy(evict_inode_work_cache); +fail: + return -ENOMEM; +} + +void f2fs_destroy_evict_inode_work(void) +{ + mempool_destroy(evict_inode_work_pool); + kmem_cache_destroy(evict_inode_work_cache); +} diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index ed9e0ba9cf0b..fa4a7a7288d5 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -5739,10 +5739,16 @@ static int __init init_f2fs_fs(void) err = f2fs_init_xattr_cache(); if (err) goto free_casefold_cache; - err = register_filesystem(&f2fs_fs_type); + err = f2fs_init_evict_inode_work(); if (err) goto free_xattr_cache; + err = register_filesystem(&f2fs_fs_type); + if (err) + goto free_evict_inode_cache; return 0; + +free_evict_inode_cache: + f2fs_destroy_evict_inode_work(); free_xattr_cache: f2fs_destroy_xattr_cache(); free_casefold_cache: @@ -5785,6 +5791,7 @@ static int __init init_f2fs_fs(void) static void __exit exit_f2fs_fs(void) { unregister_filesystem(&f2fs_fs_type); + f2fs_destroy_evict_inode_work(); f2fs_destroy_xattr_cache(); f2fs_destroy_casefold_cache(); f2fs_destroy_compress_cache(); From b2205d3cfd6c76fd5c5443ee9fdb498cfb0e1c66 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Tue, 11 Aug 2026 15:16:36 +0800 Subject: [PATCH 31/51] f2fs: fix to off-by-one issue in f2fs_zero_post_eof_page() Otherwise, it will drop one more page after new_size which is not necessary. Cc: stable@kernel.org Fixes: ba8dac350faf ("f2fs: fix to zero post-eof page") Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 56529a82e027..9dbe509926b2 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -50,7 +50,7 @@ static void f2fs_zero_post_eof_page(struct inode *inode, if (lock) filemap_invalidate_lock(inode->i_mapping); /* zero or drop pages only in range of [old_size, new_size] */ - truncate_inode_pages_range(inode->i_mapping, old_size, new_size); + truncate_inode_pages_range(inode->i_mapping, old_size, new_size - 1); if (lock) filemap_invalidate_unlock(inode->i_mapping); } From 5eced87b7d19dbc76ebdddaf322046f9ac582fcb Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Tue, 11 Aug 2026 15:16:37 +0800 Subject: [PATCH 32/51] f2fs: fix to zero post-EOF data when extending file size generic/794 4s ... - output mismatch (see /share/git/fstests/results//generic/794.out.bad) --- tests/generic/794.out 2026-06-12 08:46:32.766426241 +0800 +++ /share/git/fstests/results//generic/794.out.bad 2026-07-05 18:32:55.000000000 +0800 @@ -1,4 +1,16 @@ QA output created by 794 append_write +FAIL: non-zero data in gap [4080,4096) after shutdown+remount +000000 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a >ZZZZZZZZZZZZZZZZ< +* +001000 truncate_up ... (Run 'diff -u /share/git/fstests/tests/generic/794.out /share/git/fstests/results//generic/794.out.bad' to see the entire diff) Ran: generic/794 Failures: generic/794 Failed 1 of 1 tests Steps of generic/794: 1. write 4096 bytes to file w/ 0x5a 2. use fiemap to get PBA of first block in file 3. truncate file to 4080 4. umount; write 4096 bytes to file w/ 0x5a directly via PBA; mount 5. extend filesize via a) append 4096 from offset 4096, or b) truncate 8192, or c) fallocate 4096 from offset 4096 6. verify the gap is zeroed in memory [4080,4096) 7. sync range 4096 from offset 4096; shutdown -f (flush meta before shutdown) 8. umount; mount; verify [4080,4096) is zeroed or not. When extending file size (e.g. via truncate, fallocate, or write) across an unaligned EOF boundary, we need to ensure that post-EOF data in the partial page is zeroed out in pagecache and marked dirty, then writeback the cache to persist zeroed data before committing inode w/ updated i_size. This help to prevent stale disk data beyond the previous EOF from being exposed after remounting or crash recovery. Since f2fs is a LFS filesystem, we only support direct write via PBA in pinfile, and pinfile has section-aligned filesize, so in Android, there should no problem, but for other usage in different environment, let's fix this w/ fsync_mode=strict mount option. Cc: stable@kernel.org Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 100 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 9dbe509926b2..76316e537f65 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -36,16 +36,52 @@ #include #include -static void f2fs_zero_post_eof_page(struct inode *inode, - loff_t new_size, bool lock) +static int fill_zero(struct inode *inode, pgoff_t index, + loff_t start, loff_t len); + +static int do_zero_post_eof_page(struct inode *inode, loff_t new_size) { loff_t old_size = i_size_read(inode); + unsigned int offset, len; + pgoff_t index; + int err; + + offset = old_size & (PAGE_SIZE - 1); + + if (!offset) + return 0; + + len = min_t(loff_t, PAGE_SIZE - offset, new_size - old_size); + index = old_size >> PAGE_SHIFT; + + if (f2fs_has_inline_data(inode)) { + /* data post eof should be always zero */ + if (new_size <= MAX_INLINE_DATA(inode)) + return 0; + err = f2fs_convert_inline_inode(inode); + if (err) + return err; + } + + err = fill_zero(inode, index, offset, len); + if (err) + return err; + return filemap_write_and_wait_range(inode->i_mapping, + old_size, old_size + len - 1); +} + +static int f2fs_zero_post_eof_page(struct inode *inode, + loff_t new_size, bool lock, bool writeback) +{ + loff_t old_size = i_size_read(inode); + bool strict = + F2FS_OPTION(F2FS_I_SB(inode)).fsync_mode == FSYNC_MODE_STRICT; if (old_size >= new_size) - return; + return 0; - if (mapping_empty(inode->i_mapping)) - return; + if (!strict && mapping_empty(inode->i_mapping)) + return 0; if (lock) filemap_invalidate_lock(inode->i_mapping); @@ -53,6 +89,16 @@ static void f2fs_zero_post_eof_page(struct inode *inode, truncate_inode_pages_range(inode->i_mapping, old_size, new_size - 1); if (lock) filemap_invalidate_unlock(inode->i_mapping); + + if (!writeback || !strict) + return 0; + /* + * In fsync_mode=strict, when we expand an unaligned EOF size, we + * should zero post EOF data and writeback the data immediately, + * so that it can avoid exposing stale data after metadata flush + * and POR. + */ + return do_zero_post_eof_page(inode, new_size); } static vm_fault_t f2fs_filemap_fault(struct vm_fault *vmf) @@ -132,7 +178,10 @@ static vm_fault_t f2fs_vm_page_mkwrite(struct vm_fault *vmf) f2fs_bug_on(sbi, f2fs_has_inline_data(inode)); - f2fs_zero_post_eof_page(inode, (folio->index + 1) << PAGE_SHIFT, true); + err = f2fs_zero_post_eof_page(inode, + (folio->index + 1) << PAGE_SHIFT, true, false); + if (err) + goto out_pagefault; file_update_time(vmf->vma->vm_file); filemap_invalidate_lock_shared(inode->i_mapping); @@ -189,7 +238,7 @@ static vm_fault_t f2fs_vm_page_mkwrite(struct vm_fault *vmf) out_sem: filemap_invalidate_unlock_shared(inode->i_mapping); - +out_pagefault: sb_end_pagefault(inode->i_sb); out: ret = vmf_fs_error(err); @@ -1182,8 +1231,12 @@ int f2fs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, f2fs_down_write(&fi->i_gc_rwsem[WRITE]); filemap_invalidate_lock(inode->i_mapping); - if (attr->ia_size > old_size) - f2fs_zero_post_eof_page(inode, attr->ia_size, false); + if (attr->ia_size > old_size) { + err = f2fs_zero_post_eof_page(inode, + attr->ia_size, false, true); + if (err) + goto err_out; + } truncate_setsize(inode, attr->ia_size); if (attr->ia_size <= old_size) @@ -1192,6 +1245,7 @@ int f2fs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, * do not trim all blocks after i_size if target size is * larger than i_size. */ +err_out: filemap_invalidate_unlock(inode->i_mapping); f2fs_up_write(&fi->i_gc_rwsem[WRITE]); if (err) @@ -1303,7 +1357,9 @@ static int f2fs_punch_hole(struct inode *inode, loff_t offset, loff_t len) if (ret) return ret; - f2fs_zero_post_eof_page(inode, offset + len, true); + ret = f2fs_zero_post_eof_page(inode, offset + len, true, false); + if (ret) + return ret; pg_start = ((unsigned long long) offset) >> PAGE_SHIFT; pg_end = ((unsigned long long) offset + len) >> PAGE_SHIFT; @@ -1590,7 +1646,9 @@ static int f2fs_do_collapse(struct inode *inode, loff_t offset, loff_t len) f2fs_down_write(&F2FS_I(inode)->i_gc_rwsem[WRITE]); filemap_invalidate_lock(inode->i_mapping); - f2fs_zero_post_eof_page(inode, offset + len, false); + ret = f2fs_zero_post_eof_page(inode, offset + len, false, false); + if (ret) + goto out_unlock; f2fs_lock_op(sbi, &lc); f2fs_drop_extent_tree(inode); @@ -1598,6 +1656,7 @@ static int f2fs_do_collapse(struct inode *inode, loff_t offset, loff_t len) ret = __exchange_data_block(inode, inode, end, start, nrpages - end, true); f2fs_unlock_op(sbi, &lc); +out_unlock: filemap_invalidate_unlock(inode->i_mapping); f2fs_up_write(&F2FS_I(inode)->i_gc_rwsem[WRITE]); return ret; @@ -1719,7 +1778,9 @@ static int f2fs_zero_range(struct inode *inode, loff_t offset, loff_t len, if (ret) return ret; - f2fs_zero_post_eof_page(inode, offset + len, true); + ret = f2fs_zero_post_eof_page(inode, offset + len, true, false); + if (ret) + return ret; pg_start = ((unsigned long long) offset) >> PAGE_SHIFT; pg_end = ((unsigned long long) offset + len) >> PAGE_SHIFT; @@ -1854,7 +1915,9 @@ static int f2fs_insert_range(struct inode *inode, loff_t offset, loff_t len) f2fs_down_write(&F2FS_I(inode)->i_gc_rwsem[WRITE]); filemap_invalidate_lock(mapping); - f2fs_zero_post_eof_page(inode, offset + len, false); + ret = f2fs_zero_post_eof_page(inode, offset + len, false, false); + if (ret) + goto out_unlock; truncate_pagecache(inode, offset); while (!ret && idx > pg_start) { @@ -1872,6 +1935,7 @@ static int f2fs_insert_range(struct inode *inode, loff_t offset, loff_t len) idx + delta, nr, false); f2fs_unlock_op(sbi, &lc); } +out_unlock: filemap_invalidate_unlock(mapping); f2fs_up_write(&F2FS_I(inode)->i_gc_rwsem[WRITE]); if (ret) @@ -1914,7 +1978,9 @@ static int f2fs_expand_inode_data(struct inode *inode, loff_t offset, if (err) return err; - f2fs_zero_post_eof_page(inode, offset + len, true); + err = f2fs_zero_post_eof_page(inode, offset + len, true, true); + if (err) + return err; f2fs_balance_fs(sbi, true); @@ -5285,8 +5351,10 @@ static ssize_t f2fs_write_checks(struct kiocb *iocb, struct iov_iter *from) if (err) return err; - f2fs_zero_post_eof_page(inode, - iocb->ki_pos + iov_iter_count(from), true); + err = f2fs_zero_post_eof_page(inode, + iocb->ki_pos + iov_iter_count(from), true, true); + if (err) + return err; return count; } From d3915a0a356e58b52ed05f4f22a90b02b663688a Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Sun, 9 Aug 2026 09:26:38 +0800 Subject: [PATCH 33/51] f2fs: unify add/remove ino entry API for all ino types - Call f2fs_add_ino_entry() and f2fs_remove_ino_entry() for ORPHAN_INO - introduce __f2fs_add_ino_entry() to wrap __add_ino_entry(), so that both f2fs_add_ino_entry() and f2fs_set_dirty_device() will call __f2fs_add_ino_entry(). So, after this change: add delete lookup ORPHAN_INO f2fs_add_ino_entry f2fs_remove_ino_entry N/A FLUSH_INO f2fs_set_dirty_device f2fs_remove_ino_entry f2fs_is_dirty_device APPEND_INO f2fs_add_ino_entry f2fs_remove_ino_entry f2fs_exist_written_data UPDATA_INO f2fs_add_ino_entry f2fs_remove_ino_entry f2fs_exist_written_data TRANS_DIR_INO f2fs_add_ino_entry N/A f2fs_exist_written_data XATTR_DIR_INO f2fs_add_ino_entry N/A f2fs_exist_written_data Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/checkpoint.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/fs/f2fs/checkpoint.c b/fs/f2fs/checkpoint.c index 4413eccb5ecb..eb2f955b1e2a 100644 --- a/fs/f2fs/checkpoint.c +++ b/fs/f2fs/checkpoint.c @@ -775,10 +775,16 @@ static void f2fs_wait_for_inode_record(struct f2fs_sb_info *sbi, int mode) flush_workqueue(sbi->evict_wq); } -void f2fs_add_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, int type) +static void __f2fs_add_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, + unsigned int devidx, int type) { /* add new dirty ino entry into list */ - __add_ino_entry(sbi, ino, 0, type); + __add_ino_entry(sbi, ino, devidx, type); +} + +void f2fs_add_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, int type) +{ + __f2fs_add_ino_entry(sbi, ino, 0, type); } void f2fs_remove_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, int type) @@ -823,7 +829,7 @@ void f2fs_release_ino_entry(struct f2fs_sb_info *sbi, bool all) void f2fs_set_dirty_device(struct f2fs_sb_info *sbi, nid_t ino, unsigned int devidx, int type) { - __add_ino_entry(sbi, ino, devidx, type); + __f2fs_add_ino_entry(sbi, ino, devidx, type); } bool f2fs_is_dirty_device(struct f2fs_sb_info *sbi, nid_t ino, @@ -875,14 +881,14 @@ void f2fs_release_orphan_inode(struct f2fs_sb_info *sbi) void f2fs_add_orphan_inode(struct inode *inode) { /* add new orphan ino entry into list */ - __add_ino_entry(F2FS_I_SB(inode), inode->i_ino, 0, ORPHAN_INO); + f2fs_add_ino_entry(F2FS_I_SB(inode), inode->i_ino, ORPHAN_INO); f2fs_update_inode_page(inode); } void f2fs_remove_orphan_inode(struct f2fs_sb_info *sbi, nid_t ino) { /* remove orphan entry from orphan list */ - __remove_ino_entry(sbi, ino, ORPHAN_INO); + f2fs_remove_ino_entry(sbi, ino, ORPHAN_INO); } static int recover_orphan_inode(struct f2fs_sb_info *sbi, nid_t ino) From fa487f56efba6344aef67d871688f0908fe08af3 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Wed, 12 Aug 2026 12:17:54 +0000 Subject: [PATCH 34/51] f2fs: fix to reclaim space in f2fs_allocate_pinning_section() It needs to trigger checkpoint to free space reclaimed by f2fs_gc_range(), otherwise, fallocate() on pinfile will fail easily even there is slash space in conventional zone. [Testcase] nullblk_create.sh 512 2 1024 1024 mkfs.f2fs /dev/nullb0 -f -m mount /dev/nullb0 /mnt/f2fs/ touch /mnt/f2fs/pinfile f2fs_io pinfile set /mnt/f2fs/pinfile mkdir /mnt/f2fs/dir/ for((i=0;i<3934;i++)) do { dd if=/dev/zero of=/mnt/f2fs/dir/$i bs=1M count=1;} done sync for((i=0;i<3934;i+=2)) do { rm /mnt/f2fs/dir/$i;} done for((i=0;i<1950;i++)) do { rm /mnt/f2fs/dir/$i;} done sync f2fs_io fallocate 0 0 $((1024*1024*1024)) /mnt/f2fs/pinfile sync stat /mnt/f2fs/pinfile f2fs_io fiemap 0 $((1024*1024*1024)) /mnt/f2fs/pinfile [Before] fallocate failed: Resource temporarily unavailable File: /mnt/f2fs/pinfile Size: 109051904 Blocks: 213208 IO Block: 4096 regular file Device: 250,0 Inode: 4 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2026-08-12 20:04:02.264000000 +0800 Modify: 2026-08-12 20:04:26.784000000 +0800 Change: 2026-08-12 20:04:26.784000000 +0800 Birth: - root@localhost:~# root@localhost:~# root@localhost:~# root@localhost:~# f2fs_io fiemap 0 $((1024*1024*1024)) /mnt/f2fs/pinfile Fiemap: offset = 0 len = 1073741824 logical addr. physical addr. length flags 0 0000000000000000 0000000002e00000 0000000000200000 00001000 1 0000000000200000 000000002dc00000 0000000000400000 00001000 2 0000000000600000 000000002e400000 0000000000600000 00001000 3 0000000000c00000 000000007a400000 0000000005c00000 00001001 [After] File: /mnt/f2fs/pinfile Size: 1073741824 Blocks: 2099216 IO Block: 4096 regular file Device: 250,0 Inode: 4 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2026-08-12 19:47:49.428000000 +0800 Modify: 2026-08-12 19:49:06.808000000 +0800 Change: 2026-08-12 19:49:06.808000000 +0800 Birth: - Fiemap: offset = 0 len = 1073741824 logical addr. physical addr. length flags 0 0000000000000000 0000000002e00000 0000000000200000 00001000 1 0000000000200000 000000003aa00000 0000000000400000 00001000 2 0000000000600000 000000003b400000 0000000000200000 00001000 3 0000000000800000 000000007a200000 0000000005e00000 00001000 4 0000000006600000 0000000002800000 0000000000200000 00001000 5 0000000006800000 0000000003200000 0000000000400000 00001000 6 0000000006c00000 0000000003000000 0000000000200000 00001000 7 0000000006e00000 0000000003600000 0000000037200000 00001000 8 000000003e000000 000000003b200000 0000000000200000 00001000 9 000000003e200000 000000003a800000 0000000000200000 00001000 10 000000003e400000 000000003ae00000 0000000000400000 00001000 11 000000003e800000 000000003b600000 0000000001800000 00001001 Cc: stable@kernel.org Fixes: 9703d69d9d15 ("f2fs: support file pinning for zoned devices") Cc: Daeho Jeong Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/segment.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index b81245ddd490..7bc779c8e935 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -3477,10 +3477,13 @@ int f2fs_allocate_pinning_section(struct f2fs_sb_info *sbi) err = f2fs_gc_range(sbi, 0, sbi->first_seq_zone_segno - 1, true, ZONED_PIN_SEC_REQUIRED_COUNT); f2fs_up_write_trace(&sbi->gc_lock, &lc); - - gc_required = false; - if (!err) + if (err) + return err; + err = f2fs_sync_fs(sbi->sb, 1); + if (!err) { + gc_required = false; goto retry; + } } return err; From 5d49025a4e596c4c9ac0c519ef9f9a2c91396856 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Wed, 12 Aug 2026 12:17:55 +0000 Subject: [PATCH 35/51] f2fs: fix to shrink gc_lock coverage in f2fs_gc_range() In f2fs_allocate_pinning_section(), we will hold gc_lock before calling f2fs_gc_range() to migrate section in conventional zone, we may suffer worse case because we may need to traverse and migrate multiple sections if we failed to move blocks in section due to lot of reasons: ENOMEM, fail to migrate block of pinfile, racing on i_gc_rwsem. To avoid hold gc_lock for long time to block checkpoint, let's hold the lock and only try to migrate one section. Cc: Daeho Jeong Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/f2fs.h | 2 +- fs/f2fs/file.c | 2 +- fs/f2fs/gc.c | 34 +++++++++++++++++++++++++--------- fs/f2fs/segment.c | 4 +--- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 7fd3a181af61..3e69b94e9465 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -4274,7 +4274,7 @@ int f2fs_gc(struct f2fs_sb_info *sbi, struct f2fs_gc_control *gc_control); void f2fs_build_gc_manager(struct f2fs_sb_info *sbi); int f2fs_gc_range(struct f2fs_sb_info *sbi, unsigned int start_seg, unsigned int end_seg, - bool dry_run, unsigned int dry_run_sections); + bool dry_run, unsigned int dry_run_sections, bool lock); void f2fs_reset_gc_victim_resource(struct f2fs_sb_info *sbi, unsigned int start, unsigned int end); int f2fs_resize_fs(struct file *filp, __u64 block_count); diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 76316e537f65..a54b3ab52f1a 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -3828,7 +3828,7 @@ static int f2fs_ioc_reserve_dev_alias(struct file *filp) } /* do GC to move out valid blocks in the range all at once! */ - err = f2fs_gc_range(sbi, start, end, false, 0); + err = f2fs_gc_range(sbi, start, end, false, 0, false); if (err) { f2fs_unlock_op(sbi, &lc); goto out_gc_unlock; diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index 51fc8c69eb25..2f97a7a98517 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -2150,8 +2150,9 @@ void f2fs_build_gc_manager(struct f2fs_sb_info *sbi) int f2fs_gc_range(struct f2fs_sb_info *sbi, unsigned int start_seg, unsigned int end_seg, - bool dry_run, unsigned int dry_run_sections) + bool dry_run, unsigned int dry_run_sections, bool lock) { + struct f2fs_lock_context lc; unsigned int segno; unsigned int gc_secs = dry_run_sections; @@ -2164,28 +2165,43 @@ int f2fs_gc_range(struct f2fs_sb_info *sbi, .ilist = LIST_HEAD_INIT(gc_list.ilist), .iroot = RADIX_TREE_INIT(gc_list.iroot, GFP_NOFS), }; + int err = 0; + + if (lock) + f2fs_down_write_trace(&sbi->gc_lock, &lc); /* * avoid migrating empty section, as it can be allocated by * log in parallel. */ if (!get_valid_blocks(sbi, segno, true)) - continue; + goto next; if (is_cursec(sbi, GET_SEC_FROM_SEG(sbi, segno))) - continue; + goto next; do_garbage_collect(sbi, segno, &gc_list, FG_GC, true, false); put_gc_inode(&gc_list); - if (!dry_run && get_valid_blocks(sbi, segno, true)) - return -EAGAIN; + if (!dry_run && get_valid_blocks(sbi, segno, true)) { + err = -EAGAIN; + goto next; + } if (dry_run && dry_run_sections && - !get_valid_blocks(sbi, segno, true) && --gc_secs == 0) - break; + !get_valid_blocks(sbi, segno, true)) { + --gc_secs; + goto next; + } if (fatal_signal_pending(current)) - return -ERESTARTSYS; + err = -ERESTARTSYS; +next: + if (lock) + f2fs_up_write_trace(&sbi->gc_lock, &lc); + if (err) + return err; + if (dry_run && dry_run_sections && !gc_secs) + return 0; } return 0; @@ -2231,7 +2247,7 @@ static int free_segment_range(struct f2fs_sb_info *sbi, } /* do GC to move out valid blocks in the range */ - err = f2fs_gc_range(sbi, start, end, dry_run, 0); + err = f2fs_gc_range(sbi, start, end, dry_run, 0, false); if (err || dry_run) goto out; diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 7bc779c8e935..caa46c866443 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -3473,10 +3473,8 @@ int f2fs_allocate_pinning_section(struct f2fs_sb_info *sbi) f2fs_unlock_op(sbi, &lc); if (f2fs_sb_has_blkzoned(sbi) && err == -EAGAIN && gc_required) { - f2fs_down_write_trace(&sbi->gc_lock, &lc); err = f2fs_gc_range(sbi, 0, sbi->first_seq_zone_segno - 1, - true, ZONED_PIN_SEC_REQUIRED_COUNT); - f2fs_up_write_trace(&sbi->gc_lock, &lc); + true, ZONED_PIN_SEC_REQUIRED_COUNT, true); if (err) return err; err = f2fs_sync_fs(sbi->sb, 1); From 6da62bbde487d4678702ef004a911cd1c7a5c746 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Wed, 12 Aug 2026 12:20:07 +0000 Subject: [PATCH 36/51] f2fs: cleanup w/ f2fs_need_rand_{blk, seg, seg_blk} No logic changes. Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/f2fs.h | 13 +++++++++++-- fs/f2fs/gc.c | 2 +- fs/f2fs/segment.c | 8 ++++---- fs/f2fs/super.c | 4 ++-- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 3e69b94e9465..2c439ad4f36a 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -4142,10 +4142,19 @@ static inline struct inode *fio_inode(struct f2fs_io_info *fio) #define MIN_FRAGMENT_SIZE 1 #define MAX_FRAGMENT_SIZE 512 +static inline bool f2fs_need_rand_blk(struct f2fs_sb_info *sbi) +{ + return F2FS_OPTION(sbi).fs_mode == FS_MODE_FRAGMENT_BLK; +} + static inline bool f2fs_need_rand_seg(struct f2fs_sb_info *sbi) { - return F2FS_OPTION(sbi).fs_mode == FS_MODE_FRAGMENT_SEG || - F2FS_OPTION(sbi).fs_mode == FS_MODE_FRAGMENT_BLK; + return F2FS_OPTION(sbi).fs_mode == FS_MODE_FRAGMENT_SEG; +} + +static inline bool f2fs_need_rand_seg_blk(struct f2fs_sb_info *sbi) +{ + return f2fs_need_rand_blk(sbi) || f2fs_need_rand_seg(sbi); } /* diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index 2f97a7a98517..e6758adc5da1 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -310,7 +310,7 @@ static void select_policy(struct f2fs_sb_info *sbi, int gc_type, p->max_search = sbi->max_victim_search; /* let's select beginning hot/small space first. */ - if (f2fs_need_rand_seg(sbi)) { + if (f2fs_need_rand_seg_blk(sbi)) { p->offset = get_random_u32_below(MAIN_SECS(sbi) * SEGS_PER_SEC(sbi)); SIT_I(sbi)->last_victim[p->gc_mode] = p->offset; diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index caa46c866443..76c6f6fe503a 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -3052,7 +3052,7 @@ static unsigned int __get_next_segno(struct f2fs_sb_info *sbi, int type) sanity_check_seg_type(sbi, seg_type); if (__is_large_section(sbi)) { - if (f2fs_need_rand_seg(sbi)) { + if (f2fs_need_rand_seg_blk(sbi)) { unsigned int hint = GET_SEC_FROM_SEG(sbi, curseg->segno); if (GET_SEC_FROM_SEG(sbi, curseg->segno + 1) != hint) @@ -3061,7 +3061,7 @@ static unsigned int __get_next_segno(struct f2fs_sb_info *sbi, int type) GET_SEG_FROM_SEC(sbi, hint + 1) - 1); } return curseg->segno; - } else if (f2fs_need_rand_seg(sbi)) { + } else if (f2fs_need_rand_seg_blk(sbi)) { return get_random_u32_below(MAIN_SECS(sbi) * SEGS_PER_SEC(sbi)); } @@ -3117,7 +3117,7 @@ static int new_curseg(struct f2fs_sb_info *sbi, int type, bool new_sec) curseg->next_segno = segno; reset_curseg(sbi, type, 1); curseg->alloc_type = LFS; - if (F2FS_OPTION(sbi).fs_mode == FS_MODE_FRAGMENT_BLK) + if (f2fs_need_rand_blk(sbi)) curseg->fragment_remained_chunk = get_random_u32_inclusive(1, sbi->max_fragment_chunk); return 0; @@ -3936,7 +3936,7 @@ int f2fs_allocate_data_block(struct f2fs_sb_info *sbi, struct folio *folio, curseg->next_blkoff = f2fs_find_next_ssr_block(sbi, curseg); } else { curseg->next_blkoff++; - if (F2FS_OPTION(sbi).fs_mode == FS_MODE_FRAGMENT_BLK) + if (f2fs_need_rand_blk(sbi)) f2fs_randomize_chunk(sbi, curseg); } if (curseg->next_blkoff >= f2fs_usable_blks_in_seg(sbi, curseg->segno)) diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index fa4a7a7288d5..8dd656a7d3dc 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -2457,9 +2457,9 @@ static int f2fs_show_options(struct seq_file *seq, struct dentry *root) seq_puts(seq, "adaptive"); else if (F2FS_OPTION(sbi).fs_mode == FS_MODE_LFS) seq_puts(seq, "lfs"); - else if (F2FS_OPTION(sbi).fs_mode == FS_MODE_FRAGMENT_SEG) + else if (f2fs_need_rand_seg(sbi)) seq_puts(seq, "fragment:segment"); - else if (F2FS_OPTION(sbi).fs_mode == FS_MODE_FRAGMENT_BLK) + else if (f2fs_need_rand_blk(sbi)) seq_puts(seq, "fragment:block"); seq_printf(seq, ",active_logs=%u", F2FS_OPTION(sbi).active_logs); if (test_opt(sbi, RESERVE_ROOT) || test_opt(sbi, RESERVE_NODE)) From 0a1703eba23707e3b2edfa2a2329352e7abc0ffa Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Wed, 12 Aug 2026 12:20:08 +0000 Subject: [PATCH 37/51] f2fs: fix to avoid pinfile fragment on fragment:{block, segment} mode pinfile fallocate() conflicts w/ mode=fragment:{block,segment} mount option, result in fragment blocks in pinfile, it violate semantics of pinfile introduced in commit f5a53edcf01e ("f2fs: support aligned pinned file"). mkfs.f2fs -f /dev/vdb mount -t f2fs -o mode=fragment:block /dev/vdb /mnt/f2fs/ dd if=/dev/zero of=/mnt/f2fs/file bs=1M count=3900 sync touch /mnt/f2fs/pinfile f2fs_io pinfile set /mnt/f2fs/pinfile f2fs_io fallocate 0 0 $((1024*1024*16)) /mnt/f2fs/pinfile sync f2fs_io fiemap 0 $((1024*1024*16)) /mnt/f2fs/pinfile [Before] fallocate failed: No space left on device Fiemap: offset = 0 len = 16777216 logical addr. physical addr. length flags 0 0000000000000000 00000000d7200000 0000000000004000 00001000 1 0000000000004000 00000000d7207000 0000000000001000 00001000 2 0000000000005000 00000000d720c000 0000000000002000 00001000 3 0000000000007000 00000000d7211000 0000000000001000 00001000 4 0000000000008000 00000000d7214000 0000000000001000 00001000 5 0000000000009000 00000000d7218000 0000000000001000 00001000 6 000000000000a000 00000000d721d000 0000000000001000 00001000 7 000000000000b000 00000000d721f000 0000000000004000 00001000 ... 96 00000000000f1000 00000000d73e9000 0000000000004000 00001000 97 00000000000f5000 00000000d73f1000 0000000000003000 00001000 98 00000000000f8000 00000000d73f5000 0000000000004000 00001000 99 00000000000fc000 00000000d73fa000 0000000000001000 00001000 100 00000000000fd000 00000000d73ff000 0000000000001000 00001001 [After] fallocated a file: i_size=16777216, i_blocks=32808 Fiemap: offset = 0 len = 16777216 logical addr. physical addr. length flags 0 0000000000000000 0000000018a00000 0000000000400000 00001000 1 0000000000400000 0000000019000000 0000000000400000 00001000 2 0000000000800000 0000000032400000 0000000000200000 00001000 3 0000000000a00000 0000000038000000 0000000000200000 00001000 4 0000000000c00000 0000000039c00000 0000000000200000 00001000 5 0000000000e00000 0000000044c00000 0000000000200000 00001001 Let's ignore mode=fragment:{block,segment} mount option while fallocate() on pinfile. Fixes: 6691d940b0e0 ("f2fs: introduce fragment allocation mode mount option") Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/f2fs.h | 15 +++++++++++---- fs/f2fs/gc.c | 2 +- fs/f2fs/segment.c | 8 ++++---- fs/f2fs/super.c | 4 ++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 2c439ad4f36a..c925b9207a37 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -4142,19 +4142,26 @@ static inline struct inode *fio_inode(struct f2fs_io_info *fio) #define MIN_FRAGMENT_SIZE 1 #define MAX_FRAGMENT_SIZE 512 -static inline bool f2fs_need_rand_blk(struct f2fs_sb_info *sbi) +static inline bool f2fs_need_rand_blk(struct f2fs_sb_info *sbi, + enum log_type type) { + if (type == CURSEG_COLD_DATA_PINNED) + return false; return F2FS_OPTION(sbi).fs_mode == FS_MODE_FRAGMENT_BLK; } -static inline bool f2fs_need_rand_seg(struct f2fs_sb_info *sbi) +static inline bool f2fs_need_rand_seg(struct f2fs_sb_info *sbi, + enum log_type type) { + if (type == CURSEG_COLD_DATA_PINNED) + return false; return F2FS_OPTION(sbi).fs_mode == FS_MODE_FRAGMENT_SEG; } -static inline bool f2fs_need_rand_seg_blk(struct f2fs_sb_info *sbi) +static inline bool f2fs_need_rand_seg_blk(struct f2fs_sb_info *sbi, + enum log_type type) { - return f2fs_need_rand_blk(sbi) || f2fs_need_rand_seg(sbi); + return f2fs_need_rand_blk(sbi, type) || f2fs_need_rand_seg(sbi, type); } /* diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index e6758adc5da1..c4da2f31805b 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -310,7 +310,7 @@ static void select_policy(struct f2fs_sb_info *sbi, int gc_type, p->max_search = sbi->max_victim_search; /* let's select beginning hot/small space first. */ - if (f2fs_need_rand_seg_blk(sbi)) { + if (f2fs_need_rand_seg_blk(sbi, type)) { p->offset = get_random_u32_below(MAIN_SECS(sbi) * SEGS_PER_SEC(sbi)); SIT_I(sbi)->last_victim[p->gc_mode] = p->offset; diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 76c6f6fe503a..56decf9c691c 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -3052,7 +3052,7 @@ static unsigned int __get_next_segno(struct f2fs_sb_info *sbi, int type) sanity_check_seg_type(sbi, seg_type); if (__is_large_section(sbi)) { - if (f2fs_need_rand_seg_blk(sbi)) { + if (f2fs_need_rand_seg_blk(sbi, type)) { unsigned int hint = GET_SEC_FROM_SEG(sbi, curseg->segno); if (GET_SEC_FROM_SEG(sbi, curseg->segno + 1) != hint) @@ -3061,7 +3061,7 @@ static unsigned int __get_next_segno(struct f2fs_sb_info *sbi, int type) GET_SEG_FROM_SEC(sbi, hint + 1) - 1); } return curseg->segno; - } else if (f2fs_need_rand_seg_blk(sbi)) { + } else if (f2fs_need_rand_seg_blk(sbi, type)) { return get_random_u32_below(MAIN_SECS(sbi) * SEGS_PER_SEC(sbi)); } @@ -3117,7 +3117,7 @@ static int new_curseg(struct f2fs_sb_info *sbi, int type, bool new_sec) curseg->next_segno = segno; reset_curseg(sbi, type, 1); curseg->alloc_type = LFS; - if (f2fs_need_rand_blk(sbi)) + if (f2fs_need_rand_blk(sbi, type)) curseg->fragment_remained_chunk = get_random_u32_inclusive(1, sbi->max_fragment_chunk); return 0; @@ -3936,7 +3936,7 @@ int f2fs_allocate_data_block(struct f2fs_sb_info *sbi, struct folio *folio, curseg->next_blkoff = f2fs_find_next_ssr_block(sbi, curseg); } else { curseg->next_blkoff++; - if (f2fs_need_rand_blk(sbi)) + if (f2fs_need_rand_blk(sbi, type)) f2fs_randomize_chunk(sbi, curseg); } if (curseg->next_blkoff >= f2fs_usable_blks_in_seg(sbi, curseg->segno)) diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index 8dd656a7d3dc..0c8f60b7242f 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -2457,9 +2457,9 @@ static int f2fs_show_options(struct seq_file *seq, struct dentry *root) seq_puts(seq, "adaptive"); else if (F2FS_OPTION(sbi).fs_mode == FS_MODE_LFS) seq_puts(seq, "lfs"); - else if (f2fs_need_rand_seg(sbi)) + else if (f2fs_need_rand_seg(sbi, NO_CHECK_TYPE)) seq_puts(seq, "fragment:segment"); - else if (f2fs_need_rand_blk(sbi)) + else if (f2fs_need_rand_blk(sbi, NO_CHECK_TYPE)) seq_puts(seq, "fragment:block"); seq_printf(seq, ",active_logs=%u", F2FS_OPTION(sbi).active_logs); if (test_opt(sbi, RESERVE_ROOT) || test_opt(sbi, RESERVE_NODE)) From 258c95150c26a30db1896eacd3bdef26314f2ca1 Mon Sep 17 00:00:00 2001 From: Daeho Jeong Date: Thu, 20 Aug 2026 09:43:40 -0700 Subject: [PATCH 38/51] f2fs: avoid setting SBI_NEED_FSCK on transient resize failure When free_segment_range() fails in f2fs_resize_fs(), no on-disk superblock or filesystem metadata has been modified yet, and free_segment_range() safely restores all in-memory counters before returning. However, the current error recovery path unconditionally sets the SBI_NEED_FSCK flag and prints a scary error message on any error, forcing an unnecessary and time-consuming fsck.f2fs repair on the subsequent mount/reboot. Fix this by separating the error recovery path with a dedicated recover_user_blocks label to bypass setting SBI_NEED_FSCK on free_segment_range() failures. Signed-off-by: Daeho Jeong Signed-off-by: Sunmin Jeong Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/gc.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index c4da2f31805b..3215630c353d 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -2440,7 +2440,7 @@ int f2fs_resize_fs(struct file *filp, __u64 block_count) set_sbi_flag(sbi, SBI_IS_RESIZEFS); err = free_segment_range(sbi, secs, false); if (err) - goto recover_out; + goto recover_user_blocks; update_sb_metadata(sbi, -secs); @@ -2462,11 +2462,14 @@ int f2fs_resize_fs(struct file *filp, __u64 block_count) f2fs_commit_super(sbi, false); } recover_out: - clear_sbi_flag(sbi, SBI_IS_RESIZEFS); if (err) { + f2fs_bug_on(sbi, err == -EAGAIN); set_sbi_flag(sbi, SBI_NEED_FSCK); f2fs_err(sbi, "resize_fs failed, should run fsck to repair!"); - + } +recover_user_blocks: + clear_sbi_flag(sbi, SBI_IS_RESIZEFS); + if (err) { spin_lock(&sbi->stat_lock); sbi->user_block_count += shrunk_blocks; spin_unlock(&sbi->stat_lock); From 8ec06f50ddd8d201bd7e55b896ae28ed9d4cb7d1 Mon Sep 17 00:00:00 2001 From: Daeho Jeong Date: Wed, 19 Aug 2026 11:06:35 -0700 Subject: [PATCH 39/51] f2fs: fix to migrate all curseg types during free_segment_range In free_segment_range(), the curseg evacuation loop only iterates up to NR_CURSEG_PERSIST_TYPE (0..5), missing non-persistent in-memory curseg types such as CURSEG_COLD_DATA_PINNED and CURSEG_ALL_DATA_ATGC. Even though these in-memory curseg types are not saved in the on-disk checkpoint header, they still occupy active physical segments at runtime. If an active in-memory curseg happens to be allocated within the segment range being truncated during filesystem shrink, failing to evacuate it will cause subsequent writes to the curseg attempting out-of-bounds I/O on the truncated storage range. Fix this by expanding the curseg evacuation loop upper bound to NR_CURSEG_TYPE to ensure all active curseg types are safely migrated out of the target range. Fixes: d0b9e42ab615 ("f2fs: introduce inmem curseg") Cc: stable@vger.kernel.org Signed-off-by: Daeho Jeong Signed-off-by: Sunmin Jeong Reviewed-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 3215630c353d..5917ee917d87 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -2240,7 +2240,7 @@ static int free_segment_range(struct f2fs_sb_info *sbi, f2fs_reset_gc_victim_resource(sbi, start, end); /* Move out cursegs from the target range */ - for (type = CURSEG_HOT_DATA; type < NR_CURSEG_PERSIST_TYPE; type++) { + for (type = CURSEG_HOT_DATA; type < NR_CURSEG_TYPE; type++) { err = f2fs_allocate_segment_for_resize(sbi, type, start, end); if (err) goto out; From 0f448bb3767ef6119f5cdeabcae3f10d6e75aed6 Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Tue, 18 Aug 2026 22:55:35 +0800 Subject: [PATCH 40/51] f2fs: fix i_size when pinned fallocate partially fails From: Zhan Xusheng Commit 4275b59673eb ("f2fs: fix to round down start offset of fallocate for pin file") moved the allocation loop's start down to a section boundary, but the error path still converts @expanded against @pg_start, which holds the unrounded start. @pg_start exists for that conversion: commit 88f2cfc5fa90 ("f2fs: fix to update last i_size if fallocate partially succeeds") added it as an immutable base because map.m_lblk moves every round. Each round now maps exactly sec_blks blocks starting from rounddown(pg_start, sec_blks), so pg_start + expanded overshoots the last allocated block by pg_start % sec_blks, and a partial failure leaves i_size covering a tail that was never allocated. Nothing corrects that afterwards either, since file_dont_truncate() has already cleared FADVISE_TRUNC_BIT. It needs a start offset that is not section aligned plus a fallocate that hits ENOSPC partway, so the error path runs with expanded > 0. On an 80 MiB image with 2 MiB sections: truncate -s 80M img mkfs.f2fs -s 1 -f img mount -o loop img /mnt touch /mnt/pinned f2fs_io pinfile set /mnt/pinned # 2093056 = block 511, so pg_start % sec_blks = 511 f2fs_io fallocate 0 2093056 536870912 /mnt/pinned stat -c %s /mnt/pinned filefrag -v /mnt/pinned The last extent ends at block 10737 either way. Before, i_size is 46075904, block 11249, so 511 blocks of it were never allocated, and filefrag does not mark the last extent eof. After, i_size is 43982848, block 10738, and eof is back. A kernel from before that commit also shows no overshoot. Keep @pg_start pointing at where allocation actually begins. Fixes: 4275b59673eb ("f2fs: fix to round down start offset of fallocate for pin file") Cc: stable@vger.kernel.org Signed-off-by: Zhan Xusheng Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index a54b3ab52f1a..d82be8c1502a 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -2001,8 +2001,9 @@ static int f2fs_expand_inode_data(struct inode *inode, loff_t offset, 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; + pg_start = rounddown(map.m_lblk, sec_blks); + map.m_lblk = pg_start; + map.m_len = pg_end - pg_start; if (off_end) map.m_len++; } From 9a9ee7408a1f8271bd1978baff2dd09457054ef4 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Wed, 19 Aug 2026 09:31:07 +0800 Subject: [PATCH 41/51] f2fs: reduce memory footprint of ino management Currently, ino entries for APPEND_INO, UPDATE_INO, TRANS_DIR_INO, and XATTR_DIR_INO allocate a 'struct ino_entry' slab object and attach it to both a list and a radix tree solely for existence checks via f2fs_exist_written_data(). Since these ino types only track binary existence status, we can embed the information directly into radix tree value entries as a bitmap: - The Linux radix tree/XArray supports in-place value entries via xa_mk_value() / xa_to_value(), which tag the least significant bit to store an unallocated integer value of BITS_PER_XA_VALUE bits (BITS_PER_LONG - 1) directly in the slot pointer. - For each inode, (ino / BITS_PER_XA_VALUE) serves as the radix tree slot index, and (ino % BITS_PER_XA_VALUE) is used as the bit offset within the slot's bitmap. For example, when tracking ino = 7: - Before: Allocate a 'struct ino_entry' ({ .ino = 7 }), insert its pointer into the radix tree at index = 7, and link it to im->ino_list. - After: Compute slot_index = 7 / BITS_PER_XA_VALUE (index 0) and bit_offset = 7 % BITS_PER_XA_VALUE (bit 7), then set bit 7 in the value entry via xa_mk_value(bitmap) at index 0, without allocating a slab object or linking to a list. Additionally: - In-place slot updates are performed via radix_tree_replace_slot(), and slots are deleted with radix_tree_delete() once the bitmap is zeroed. - Reorder the ino list enum so ORPHAN_INO and FLUSH_INO (which still require struct ino_entry and list traversal) remain separated, while bitmap-based trees are torn down using xa_destroy(). This eliminates 'struct ino_entry' slab allocations and linked-list tracking for these ino types, significantly reducing memory consumption. Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/checkpoint.c | 99 ++++++++++++++++++++++++++++++++++++++++---- fs/f2fs/f2fs.h | 6 ++- 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/fs/f2fs/checkpoint.c b/fs/f2fs/checkpoint.c index eb2f955b1e2a..c73999e39a39 100644 --- a/fs/f2fs/checkpoint.c +++ b/fs/f2fs/checkpoint.c @@ -766,6 +766,64 @@ static void __remove_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, int type) spin_unlock(&im->ino_lock); } +static void __set_ino_bitmap(struct f2fs_sb_info *sbi, nid_t ino, int type) +{ + struct inode_management *im = &sbi->im[type]; + unsigned long index = INO_SLOT_INDEX(ino); + unsigned int ofs = INO_BIT_OFFSET(ino); + void **slot, *entry; + unsigned long bitmap = 0; + int ret; + + ret = radix_tree_preload(GFP_NOFS | __GFP_NOFAIL); + f2fs_bug_on(sbi, ret); + + spin_lock(&im->ino_lock); + slot = radix_tree_lookup_slot(&im->ino_root, index); + if (slot) { + entry = radix_tree_deref_slot_protected(slot, &im->ino_lock); + bitmap = xa_to_value(entry); + if (!(bitmap & (1UL << ofs))) { + bitmap |= (1UL << ofs); + entry = xa_mk_value(bitmap); + radix_tree_replace_slot(&im->ino_root, slot, entry); + } + } else { + bitmap |= (1UL << ofs); + entry = xa_mk_value(bitmap); + if (unlikely(radix_tree_insert(&im->ino_root, index, entry))) + f2fs_bug_on(sbi, 1); + } + spin_unlock(&im->ino_lock); + radix_tree_preload_end(); +} + +static void __clear_ino_bitmap(struct f2fs_sb_info *sbi, nid_t ino, int type) +{ + struct inode_management *im = &sbi->im[type]; + unsigned long index = INO_SLOT_INDEX(ino); + unsigned int ofs = INO_BIT_OFFSET(ino); + void **slot, *entry; + unsigned long bitmap; + + spin_lock(&im->ino_lock); + slot = radix_tree_lookup_slot(&im->ino_root, index); + if (slot) { + entry = radix_tree_deref_slot_protected(slot, &im->ino_lock); + bitmap = xa_to_value(entry); + if (bitmap & (1UL << ofs)) + bitmap &= ~(1UL << ofs); + + if (bitmap) { + entry = xa_mk_value(bitmap); + radix_tree_replace_slot(&im->ino_root, slot, entry); + } else { + radix_tree_delete(&im->ino_root, index); + } + } + spin_unlock(&im->ino_lock); +} + static void f2fs_wait_for_inode_record(struct f2fs_sb_info *sbi, int mode) { if (mode != APPEND_INO && mode != UPDATE_INO) @@ -778,8 +836,10 @@ static void f2fs_wait_for_inode_record(struct f2fs_sb_info *sbi, int mode) static void __f2fs_add_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, unsigned int devidx, int type) { - /* add new dirty ino entry into list */ - __add_ino_entry(sbi, ino, devidx, type); + if (type <= FLUSH_INO) + __add_ino_entry(sbi, ino, devidx, type); + else + __set_ino_bitmap(sbi, ino, type); } void f2fs_add_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, int type) @@ -789,20 +849,33 @@ void f2fs_add_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, int type) void f2fs_remove_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, int type) { - /* remove dirty ino entry from list */ - __remove_ino_entry(sbi, ino, type); + if (type <= FLUSH_INO) + __remove_ino_entry(sbi, ino, type); + else + __clear_ino_bitmap(sbi, ino, type); } -/* mode should be APPEND_INO, UPDATE_INO or TRANS_DIR_INO */ +/* mode should be APPEND_INO, UPDATE_INO, TRANS_DIR_INO and XATTR_DIR_INO */ bool f2fs_exist_written_data(struct f2fs_sb_info *sbi, nid_t ino, int mode) { struct inode_management *im = &sbi->im[mode]; - struct ino_entry *e; + unsigned long index = INO_SLOT_INDEX(ino); + unsigned int ofs = INO_BIT_OFFSET(ino); + void *entry; + unsigned long bitmap; + + f2fs_bug_on(sbi, mode <= FLUSH_INO); spin_lock(&im->ino_lock); - e = radix_tree_lookup(&im->ino_root, ino); + entry = radix_tree_lookup(&im->ino_root, index); + if (!entry) { + spin_unlock(&im->ino_lock); + return false; + } + bitmap = xa_to_value(entry); spin_unlock(&im->ino_lock); - return e ? true : false; + + return bitmap & (1UL << ofs); } void f2fs_release_ino_entry(struct f2fs_sb_info *sbi, bool all) @@ -810,7 +883,7 @@ void f2fs_release_ino_entry(struct f2fs_sb_info *sbi, bool all) struct ino_entry *e, *tmp; int i; - for (i = all ? ORPHAN_INO : APPEND_INO; i < MAX_INO_ENTRY; i++) { + for (i = all ? ORPHAN_INO : FLUSH_INO; i <= FLUSH_INO; i++) { struct inode_management *im = &sbi->im[i]; f2fs_wait_for_inode_record(sbi, i); @@ -824,6 +897,14 @@ void f2fs_release_ino_entry(struct f2fs_sb_info *sbi, bool all) } spin_unlock(&im->ino_lock); } + + for (i = APPEND_INO; i < MAX_INO_ENTRY; i++) { + struct inode_management *im = &sbi->im[i]; + + spin_lock(&im->ino_lock); + xa_destroy(&im->ino_root); + spin_unlock(&im->ino_lock); + } } void f2fs_set_dirty_device(struct f2fs_sb_info *sbi, nid_t ino, diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index c925b9207a37..1b96d8718c5c 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -388,14 +388,18 @@ enum { /* for the list of ino */ enum { ORPHAN_INO, /* for orphan ino list */ + FLUSH_INO, /* for multiple device flushing */ APPEND_INO, /* for append ino list */ UPDATE_INO, /* for update ino list */ TRANS_DIR_INO, /* for transactions dir ino list */ XATTR_DIR_INO, /* for xattr updated dir ino list */ - FLUSH_INO, /* for multiple device flushing */ MAX_INO_ENTRY, /* max. list */ }; +#define INO_BITS_PER_SLOT BITS_PER_XA_VALUE +#define INO_SLOT_INDEX(ino) ((ino) / INO_BITS_PER_SLOT) +#define INO_BIT_OFFSET(ino) ((ino) % INO_BITS_PER_SLOT) + struct ino_entry { struct list_head list; /* list head */ nid_t ino; /* inode number */ From 46d4246d8dcde75aff707976cddc546f22184cac Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 17 Aug 2026 11:19:16 +0000 Subject: [PATCH 42/51] f2fs: use f2fs_{down, up}_(read, write}_trace() for nat_tree_lock Under heavy workloads or during background GC/fallocate operations, nat_tree_lock can experience high lock contention between background readers (e.g. f2fs_get_node_info() in gc_data_segment) and writers (e.g. flush_nat_entries, set_node_addr, shrinker). [375067.327986][T13777] schedule+0x4c/0x114 [375067.327997][T13777] f2fs_get_node_info+0x438/0x5c4 [375067.328002][T13777] f2fs_get_inode_page+0x1e0/0x3f0 [375067.328013][T13777] f2fs_iget+0x88/0x1180 [375067.328024][T13777] f2fs_lookup+0x168/0x3a8 [375067.328035][T13777] path_openat+0xa28/0x1b04 [375067.328046][T13777] do_filp_open+0xac/0x130 [375067.328056][T13777] do_sys_openat2+0x140/0x21c [375067.328066][T13777] __arm64_sys_openat+0x70/0x9c [375067.330299][T13777] schedule+0x4c/0x114 [375067.330310][T13777] schedule_preempt_disabled+0x24/0x40 [375067.330321][T13777] rwsem_down_write_slowpath+0x3b4/0x9d0 [375067.330332][T13777] down_write+0x98/0x170 [375067.330343][T13777] set_node_addr+0x74/0x4b4 [375067.330354][T13777] f2fs_new_node_page+0xb0/0x280 [375067.330444][T13777] f2fs_new_inode_page+0x3c/0x64 [375067.330455][T13777] f2fs_init_inode_metadata+0x4c/0x47c [375067.330461][T13777] f2fs_add_regular_entry+0x258/0x5b8 [375067.330471][T13777] f2fs_add_dentry+0x100/0x158 [375067.330476][T13777] f2fs_do_add_link+0x84/0x140 [375067.330487][T13777] f2fs_create+0xec/0x250 [375067.331759][T13777] schedule+0x4c/0x114 [375067.331770][T13777] f2fs_down_read+0x9c/0xc4 [375067.331781][T13777] f2fs_need_inode_block_update+0x20/0x10c [375067.331792][T13777] f2fs_do_sync_file+0x478/0x830 [375067.331802][T13777] f2fs_sync_file+0x2c/0x40 This patch converts nat_tree_lock to use the f2fs_{down,up}_{read,write}_trace infrastructure. Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- Documentation/ABI/testing/sysfs-fs-f2fs | 1 + fs/f2fs/checkpoint.c | 1 + fs/f2fs/f2fs.h | 1 + fs/f2fs/node.c | 71 ++++++++++++++----------- include/trace/events/f2fs.h | 3 +- 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-fs-f2fs b/Documentation/ABI/testing/sysfs-fs-f2fs index f4e6a7415cde..85194e4c7f01 100644 --- a/Documentation/ABI/testing/sysfs-fs-f2fs +++ b/Documentation/ABI/testing/sysfs-fs-f2fs @@ -986,6 +986,7 @@ Description: This sysfs entry can be used to enable/disable to adjust priority f 0x00000008 gc_lock 0x00000010 cp_global 0x00000020 io_rwsem + 0x00000040 nat_tree_lock ========== ================== What: /sys/fs/f2fs//lock_duration_priority diff --git a/fs/f2fs/checkpoint.c b/fs/f2fs/checkpoint.c index c73999e39a39..4b59f30ef45d 100644 --- a/fs/f2fs/checkpoint.c +++ b/fs/f2fs/checkpoint.c @@ -107,6 +107,7 @@ static bool need_uplift_priority(struct f2fs_rwsem *sem, bool is_write) case LOCK_NAME_GC_LOCK: case LOCK_NAME_CP_GLOBAL: case LOCK_NAME_IO_RWSEM: + case LOCK_NAME_NAT_TREE_LOCK: return true; default: f2fs_bug_on(sem->sbi, 1); diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 1b96d8718c5c..a1f5f375045a 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -193,6 +193,7 @@ enum f2fs_lock_name { LOCK_NAME_GC_LOCK, LOCK_NAME_CP_GLOBAL, LOCK_NAME_IO_RWSEM, + LOCK_NAME_NAT_TREE_LOCK, LOCK_NAME_MAX, }; diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c index 46bea52e35c3..968e5ed38816 100644 --- a/fs/f2fs/node.c +++ b/fs/f2fs/node.c @@ -401,15 +401,16 @@ bool f2fs_need_dentry_mark(struct f2fs_sb_info *sbi, nid_t nid) struct f2fs_nm_info *nm_i = NM_I(sbi); struct nat_entry *e; bool need = false; + struct f2fs_lock_context lc; - f2fs_down_read(&nm_i->nat_tree_lock); + f2fs_down_read_trace(&nm_i->nat_tree_lock, &lc); e = __lookup_nat_cache(nm_i, nid, false); if (e) { if (!get_nat_flag(e, IS_CHECKPOINTED) && !get_nat_flag(e, HAS_FSYNCED_INODE)) need = true; } - f2fs_up_read(&nm_i->nat_tree_lock); + f2fs_up_read_trace(&nm_i->nat_tree_lock, &lc); return need; } @@ -418,12 +419,13 @@ bool f2fs_is_checkpointed_node(struct f2fs_sb_info *sbi, nid_t nid) struct f2fs_nm_info *nm_i = NM_I(sbi); struct nat_entry *e; bool is_cp = true; + struct f2fs_lock_context lc; - f2fs_down_read(&nm_i->nat_tree_lock); + f2fs_down_read_trace(&nm_i->nat_tree_lock, &lc); e = __lookup_nat_cache(nm_i, nid, false); if (e && !get_nat_flag(e, IS_CHECKPOINTED)) is_cp = false; - f2fs_up_read(&nm_i->nat_tree_lock); + f2fs_up_read_trace(&nm_i->nat_tree_lock, &lc); return is_cp; } @@ -432,16 +434,16 @@ bool f2fs_need_inode_block_update(struct f2fs_sb_info *sbi, nid_t ino) struct f2fs_nm_info *nm_i = NM_I(sbi); struct nat_entry *e; bool need_update = true; - struct f2fs_lock_context lc; + struct f2fs_lock_context lc, nlc; f2fs_down_read_trace(&sbi->node_write, &lc); - f2fs_down_read(&nm_i->nat_tree_lock); + f2fs_down_read_trace(&nm_i->nat_tree_lock, &nlc); e = __lookup_nat_cache(nm_i, ino, false); if (e && get_nat_flag(e, HAS_LAST_FSYNC) && (get_nat_flag(e, IS_CHECKPOINTED) || get_nat_flag(e, HAS_FSYNCED_INODE))) need_update = false; - f2fs_up_read(&nm_i->nat_tree_lock); + f2fs_up_read_trace(&nm_i->nat_tree_lock, &nlc); f2fs_up_read_trace(&sbi->node_write, &lc); return need_update; } @@ -452,6 +454,7 @@ static void cache_nat_entry(struct f2fs_sb_info *sbi, nid_t nid, { struct f2fs_nm_info *nm_i = NM_I(sbi); struct nat_entry *new, *e; + struct f2fs_lock_context lc; /* Let's mitigate lock contention of nat_tree_lock during checkpoint */ if (f2fs_rwsem_is_locked(&sbi->cp_global_sem)) @@ -461,7 +464,7 @@ static void cache_nat_entry(struct f2fs_sb_info *sbi, nid_t nid, if (!new) return; - f2fs_down_write(&nm_i->nat_tree_lock); + f2fs_down_write_trace(&nm_i->nat_tree_lock, &lc); e = __lookup_nat_cache(nm_i, nid, false); if (!e) e = __init_nat_entry(nm_i, new, ne, false, false); @@ -470,7 +473,7 @@ static void cache_nat_entry(struct f2fs_sb_info *sbi, nid_t nid, nat_get_blkaddr(e) != le32_to_cpu(ne->block_addr) || nat_get_version(e) != ne->version); - f2fs_up_write(&nm_i->nat_tree_lock); + f2fs_up_write_trace(&nm_i->nat_tree_lock, &lc); if (e != new) __free_nat_entry(new); } @@ -482,8 +485,9 @@ static void set_node_addr(struct f2fs_sb_info *sbi, struct node_info *ni, struct nat_entry *e; struct nat_entry *new = __alloc_nat_entry(sbi, ni->nid, true); bool init_dirty = false; + struct f2fs_lock_context lc; - f2fs_down_write(&nm_i->nat_tree_lock); + f2fs_down_write_trace(&nm_i->nat_tree_lock, &lc); e = __lookup_nat_cache(nm_i, ni->nid, true); if (!e) { init_dirty = true; @@ -533,15 +537,16 @@ static void set_node_addr(struct f2fs_sb_info *sbi, struct node_info *ni, set_nat_flag(e, HAS_FSYNCED_INODE, true); set_nat_flag(e, HAS_LAST_FSYNC, fsync_done); } - f2fs_up_write(&nm_i->nat_tree_lock); + f2fs_up_write_trace(&nm_i->nat_tree_lock, &lc); } int f2fs_try_to_free_nats(struct f2fs_sb_info *sbi, int nr_shrink) { struct f2fs_nm_info *nm_i = NM_I(sbi); int nr = nr_shrink; + struct f2fs_lock_context lc; - if (!f2fs_down_write_trylock(&nm_i->nat_tree_lock)) + if (!f2fs_down_write_trylock_trace(&nm_i->nat_tree_lock, &lc)) return 0; spin_lock(&nm_i->nat_list_lock); @@ -563,7 +568,7 @@ int f2fs_try_to_free_nats(struct f2fs_sb_info *sbi, int nr_shrink) } spin_unlock(&nm_i->nat_list_lock); - f2fs_up_write(&nm_i->nat_tree_lock); + f2fs_up_write_trace(&nm_i->nat_tree_lock, &lc); return nr - nr_shrink; } @@ -581,18 +586,19 @@ int f2fs_get_node_info(struct f2fs_sb_info *sbi, nid_t nid, pgoff_t index; int i; bool need_cache = true; + struct f2fs_lock_context lc; ni->flag = 0; ni->nid = nid; retry: /* Check nat cache */ - f2fs_down_read(&nm_i->nat_tree_lock); + f2fs_down_read_trace(&nm_i->nat_tree_lock, &lc); e = __lookup_nat_cache(nm_i, nid, false); if (e) { ni->ino = nat_get_ino(e); ni->blk_addr = nat_get_blkaddr(e); ni->version = nat_get_version(e); - f2fs_up_read(&nm_i->nat_tree_lock); + f2fs_up_read_trace(&nm_i->nat_tree_lock, &lc); if (IS_ENABLED(CONFIG_F2FS_CHECK_FS)) { need_cache = false; goto sanity_check; @@ -610,7 +616,7 @@ int f2fs_get_node_info(struct f2fs_sb_info *sbi, nid_t nid, down_read(&curseg->journal_rwsem); } else if (f2fs_rwsem_is_contended(&nm_i->nat_tree_lock) || !down_read_trylock(&curseg->journal_rwsem)) { - f2fs_up_read(&nm_i->nat_tree_lock); + f2fs_up_read_trace(&nm_i->nat_tree_lock, &lc); goto retry; } @@ -621,13 +627,13 @@ int f2fs_get_node_info(struct f2fs_sb_info *sbi, nid_t nid, } up_read(&curseg->journal_rwsem); if (i >= 0) { - f2fs_up_read(&nm_i->nat_tree_lock); + f2fs_up_read_trace(&nm_i->nat_tree_lock, &lc); goto sanity_check; } /* Fill node_info from nat page */ index = current_nat_addr(sbi, nid); - f2fs_up_read(&nm_i->nat_tree_lock); + f2fs_up_read_trace(&nm_i->nat_tree_lock, &lc); folio = f2fs_get_meta_folio(sbi, index); if (IS_ERR(folio)) @@ -2567,8 +2573,9 @@ static void scan_free_nid_bits(struct f2fs_sb_info *sbi) struct f2fs_nm_info *nm_i = NM_I(sbi); unsigned int i, idx; nid_t nid; + struct f2fs_lock_context lc; - f2fs_down_read(&nm_i->nat_tree_lock); + f2fs_down_read_trace(&nm_i->nat_tree_lock, &lc); for (i = 0; i < nm_i->nat_blocks; i++) { if (!test_bit_le(i, nm_i->nat_block_bitmap)) @@ -2591,7 +2598,7 @@ static void scan_free_nid_bits(struct f2fs_sb_info *sbi) out: scan_curseg_cache(sbi); - f2fs_up_read(&nm_i->nat_tree_lock); + f2fs_up_read_trace(&nm_i->nat_tree_lock, &lc); } static int __f2fs_build_free_nids(struct f2fs_sb_info *sbi, @@ -2600,6 +2607,7 @@ static int __f2fs_build_free_nids(struct f2fs_sb_info *sbi, struct f2fs_nm_info *nm_i = NM_I(sbi); int i = 0, ret; nid_t nid = nm_i->next_scan_nid; + struct f2fs_lock_context lc; if (unlikely(nid >= nm_i->max_nid)) nid = 0; @@ -2626,7 +2634,7 @@ static int __f2fs_build_free_nids(struct f2fs_sb_info *sbi, f2fs_ra_meta_pages(sbi, NAT_BLOCK_OFFSET(nid), FREE_NID_PAGES, META_NAT, true); - f2fs_down_read(&nm_i->nat_tree_lock); + f2fs_down_read_trace(&nm_i->nat_tree_lock, &lc); while (1) { if (!test_bit_le(NAT_BLOCK_OFFSET(nid), @@ -2642,7 +2650,7 @@ static int __f2fs_build_free_nids(struct f2fs_sb_info *sbi, } if (ret) { - f2fs_up_read(&nm_i->nat_tree_lock); + f2fs_up_read_trace(&nm_i->nat_tree_lock, &lc); if (ret == -EFSCORRUPTED) { f2fs_err(sbi, "NAT is corrupt, run fsck to fix it"); @@ -2669,7 +2677,7 @@ static int __f2fs_build_free_nids(struct f2fs_sb_info *sbi, /* find free nids from current sum_pages */ scan_curseg_cache(sbi); - f2fs_up_read(&nm_i->nat_tree_lock); + f2fs_up_read_trace(&nm_i->nat_tree_lock, &lc); f2fs_ra_meta_pages(sbi, NAT_BLOCK_OFFSET(nm_i->next_scan_nid), nm_i->ra_nid_pages, META_NAT, false); @@ -3206,21 +3214,22 @@ int f2fs_flush_nat_entries(struct f2fs_sb_info *sbi, struct cp_control *cpc) nid_t set_idx = 0; LIST_HEAD(sets); int err = 0; + struct f2fs_lock_context lc; /* * during unmount, let's flush nat_bits before checking * nat_cnt[DIRTY_NAT]. */ if (enabled_nat_bits(sbi, cpc)) { - f2fs_down_write(&nm_i->nat_tree_lock); + f2fs_down_write_trace(&nm_i->nat_tree_lock, &lc); remove_nats_in_journal(sbi); - f2fs_up_write(&nm_i->nat_tree_lock); + f2fs_up_write_trace(&nm_i->nat_tree_lock, &lc); } if (!nm_i->nat_cnt[DIRTY_NAT]) return 0; - f2fs_down_write(&nm_i->nat_tree_lock); + f2fs_down_write_trace(&nm_i->nat_tree_lock, &lc); /* * if there are no enough space in journal to store dirty nat @@ -3261,7 +3270,7 @@ int f2fs_flush_nat_entries(struct f2fs_sb_info *sbi, struct cp_control *cpc) break; } - f2fs_up_write(&nm_i->nat_tree_lock); + f2fs_up_write_trace(&nm_i->nat_tree_lock, &lc); /* Allow dirty nats by node block allocation in write_begin */ return err; @@ -3380,7 +3389,8 @@ static int init_node_manager(struct f2fs_sb_info *sbi) mutex_init(&nm_i->build_lock); spin_lock_init(&nm_i->nid_list_lock); - init_f2fs_rwsem(&nm_i->nat_tree_lock); + init_f2fs_rwsem_trace(&nm_i->nat_tree_lock, sbi, + LOCK_NAME_NAT_TREE_LOCK); nm_i->next_scan_nid = le32_to_cpu(sbi->ckpt->next_free_nid); nm_i->bitmap_size = __bitmap_size(sbi, NAT_BITMAP); @@ -3472,6 +3482,7 @@ void f2fs_destroy_node_manager(struct f2fs_sb_info *sbi) struct nat_entry_set **setvec = (struct nat_entry_set **)vec; nid_t nid = 0; unsigned int found; + struct f2fs_lock_context lc; if (!nm_i) return; @@ -3490,7 +3501,7 @@ void f2fs_destroy_node_manager(struct f2fs_sb_info *sbi) spin_unlock(&nm_i->nid_list_lock); /* destroy nat cache */ - f2fs_down_write(&nm_i->nat_tree_lock); + f2fs_down_write_trace(&nm_i->nat_tree_lock, &lc); while ((found = __gang_lookup_nat_cache(nm_i, nid, NAT_VEC_SIZE, natvec))) { unsigned idx; @@ -3521,7 +3532,7 @@ void f2fs_destroy_node_manager(struct f2fs_sb_info *sbi) kmem_cache_free(nat_entry_set_slab, setvec[idx]); } } - f2fs_up_write(&nm_i->nat_tree_lock); + f2fs_up_write_trace(&nm_i->nat_tree_lock, &lc); kvfree(nm_i->nat_block_bitmap); if (nm_i->free_nid_bitmap) { diff --git a/include/trace/events/f2fs.h b/include/trace/events/f2fs.h index 1dd9fc5afc46..d53be932df01 100644 --- a/include/trace/events/f2fs.h +++ b/include/trace/events/f2fs.h @@ -191,7 +191,8 @@ TRACE_DEFINE_ENUM(CP_PHASE_FINISH_CHECKPOINT); { LOCK_NAME_NODE_WRITE, "node_write" }, \ { LOCK_NAME_GC_LOCK, "gc_lock" }, \ { LOCK_NAME_CP_GLOBAL, "cp_global" }, \ - { LOCK_NAME_IO_RWSEM, "io_rwsem" }) + { LOCK_NAME_IO_RWSEM, "io_rwsem" }, \ + { LOCK_NAME_NAT_TREE_LOCK, "nat_tree_lock" }) struct f2fs_sb_info; struct f2fs_io_info; From 2b8704b6a8b2896ccad1f5941d9a3e2c5031a470 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 17 Aug 2026 02:39:27 +0000 Subject: [PATCH 43/51] f2fs: fix to reset all pinned status during fggc Otherwise, the pinned status may affect latter flow of fggc. Cc: stable@kernel.org Fixes: 9703d69d9d15 ("f2fs: support file pinning for zoned devices") Cc: Daeho Jeong Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/gc.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index 5917ee917d87..0c17038fcfd7 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -2183,6 +2183,9 @@ int f2fs_gc_range(struct f2fs_sb_info *sbi, do_garbage_collect(sbi, segno, &gc_list, FG_GC, true, false); put_gc_inode(&gc_list); + /* reset all pinned status during fggc */ + f2fs_unpin_all_sections(sbi, true); + if (!dry_run && get_valid_blocks(sbi, segno, true)) { err = -EAGAIN; goto next; From 11d56d7a8e60d7ee4969b56403da700745aea1af Mon Sep 17 00:00:00 2001 From: Daeho Jeong Date: Fri, 21 Aug 2026 07:17:42 -0700 Subject: [PATCH 44/51] f2fs: fix error handling on device alias check in rename and unlink In f2fs_rename() and f2fs_unlink(), directly returning -EPERM when encountering a device aliasing file bypasses the cleanup path. Fix this by setting err to -EPERM and jumping to the proper cleanup labels (out_dir and out) instead of returning immediately. Reported-by: Christophe JAILLET Signed-off-by: Daeho Jeong Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/namei.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/fs/f2fs/namei.c b/fs/f2fs/namei.c index 784f636244e1..b9b15c5d28de 100644 --- a/fs/f2fs/namei.c +++ b/fs/f2fs/namei.c @@ -571,8 +571,10 @@ static int f2fs_unlink(struct inode *dir, struct dentry *dentry) trace_f2fs_unlink_enter(dir, dentry); - if (IS_DEVICE_ALIASING(inode)) - return -EPERM; + if (IS_DEVICE_ALIASING(inode)) { + err = -EPERM; + goto out; + } if (unlikely(f2fs_cp_error(sbi))) { err = -EIO; @@ -1025,8 +1027,10 @@ static int f2fs_rename(struct mnt_idmap *idmap, struct inode *old_dir, } if (new_inode) { - if (IS_DEVICE_ALIASING(new_inode)) - return -EPERM; + if (IS_DEVICE_ALIASING(new_inode)) { + err = -EPERM; + goto out_dir; + } err = -ENOTEMPTY; if (old_is_dir && !f2fs_empty_dir(new_inode)) From a2c73a7a677afdaa8b16d775188f9ef5cfbfd8b2 Mon Sep 17 00:00:00 2001 From: Wenjie Qi Date: Mon, 10 Aug 2026 21:38:32 +0800 Subject: [PATCH 45/51] f2fs: return symlink writeback errors F2FS writes long symlink data with page_symlink() and then flushes the symlink mapping to reduce the chance of exposing a broken symlink. That flush result is currently ignored. If the writeback fails, symlink() still returns success even though the symlink is not durable and the same operation can already surface -EIO through syncfs(). Return the writeback error to userspace and skip the dirsync flush once the symlink data flush has failed. Fixes: d0cae97cb600 ("f2fs: flush symlink path to avoid broken symlink after POR") Cc: stable@kernel.org Signed-off-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/namei.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/fs/f2fs/namei.c b/fs/f2fs/namei.c index b9b15c5d28de..a0e902895dd3 100644 --- a/fs/f2fs/namei.c +++ b/fs/f2fs/namei.c @@ -715,15 +715,16 @@ static int f2fs_symlink(struct mnt_idmap *idmap, struct inode *dir, * performance regression. */ if (!err) { - filemap_write_and_wait_range(inode->i_mapping, 0, - disk_link.len - 1); + err = filemap_write_and_wait_range(inode->i_mapping, 0, + disk_link.len - 1); - if (IS_DIRSYNC(dir)) + if (!err && IS_DIRSYNC(dir)) f2fs_sync_fs(sbi->sb, 1); - } else { - f2fs_unlink(dir, dentry); } + if (err) + f2fs_unlink(dir, dentry); + f2fs_balance_fs(sbi, true); goto out_free_encrypted_link; From dafb84f092a6387748b2df4c8f647d46873bc1a0 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Wed, 12 Aug 2026 12:20:43 +0000 Subject: [PATCH 46/51] f2fs: fix to propagate error from f2fs_sync_fs() So that caller can detect any failure from f2fs_sync_fs(). Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/namei.c | 51 ++++++++++++++++++++++++++++++++++--------------- fs/f2fs/super.c | 4 +++- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/fs/f2fs/namei.c b/fs/f2fs/namei.c index a0e902895dd3..7d9f8e92de06 100644 --- a/fs/f2fs/namei.c +++ b/fs/f2fs/namei.c @@ -402,8 +402,11 @@ static int f2fs_create(struct mnt_idmap *idmap, struct inode *dir, d_instantiate_new(dentry, inode); - if (IS_DIRSYNC(dir)) - f2fs_sync_fs(sbi->sb, 1); + if (IS_DIRSYNC(dir)) { + err = f2fs_sync_fs(sbi->sb, 1); + if (err) + return err; + } f2fs_balance_fs(sbi, true); return 0; @@ -455,8 +458,11 @@ static int f2fs_link(struct dentry *old_dentry, struct inode *dir, d_instantiate(dentry, inode); - if (IS_DIRSYNC(dir)) - f2fs_sync_fs(sbi->sb, 1); + if (IS_DIRSYNC(dir)) { + err = f2fs_sync_fs(sbi->sb, 1); + if (err) + return err; + } return 0; out: clear_inode_flag(inode, FI_INC_LINK); @@ -626,8 +632,11 @@ static int f2fs_unlink(struct inode *dir, struct dentry *dentry) if (IS_ENABLED(CONFIG_UNICODE) && IS_CASEFOLDED(dir)) d_invalidate(dentry); - if (IS_DIRSYNC(dir)) - f2fs_sync_fs(sbi->sb, 1); + if (IS_DIRSYNC(dir)) { + err = f2fs_sync_fs(sbi->sb, 1); + if (err) + goto out; + } goto out; corrupted: @@ -719,7 +728,7 @@ static int f2fs_symlink(struct mnt_idmap *idmap, struct inode *dir, disk_link.len - 1); if (!err && IS_DIRSYNC(dir)) - f2fs_sync_fs(sbi->sb, 1); + err = f2fs_sync_fs(sbi->sb, 1); } if (err) @@ -771,8 +780,11 @@ static struct dentry *f2fs_mkdir(struct mnt_idmap *idmap, struct inode *dir, d_instantiate_new(dentry, inode); - if (IS_DIRSYNC(dir)) - f2fs_sync_fs(sbi->sb, 1); + if (IS_DIRSYNC(dir)) { + err = f2fs_sync_fs(sbi->sb, 1); + if (err) + return ERR_PTR(err); + } f2fs_balance_fs(sbi, true); return NULL; @@ -826,8 +838,11 @@ static int f2fs_mknod(struct mnt_idmap *idmap, struct inode *dir, d_instantiate_new(dentry, inode); - if (IS_DIRSYNC(dir)) - f2fs_sync_fs(sbi->sb, 1); + if (IS_DIRSYNC(dir)) { + err = f2fs_sync_fs(sbi->sb, 1); + if (err) + return err; + } f2fs_balance_fs(sbi, true); return 0; @@ -1126,8 +1141,11 @@ static int f2fs_rename(struct mnt_idmap *idmap, struct inode *old_dir, f2fs_unlock_op(sbi, &lc); - if (IS_DIRSYNC(old_dir) || IS_DIRSYNC(new_dir)) - f2fs_sync_fs(sbi->sb, 1); + if (IS_DIRSYNC(old_dir) || IS_DIRSYNC(new_dir)) { + err = f2fs_sync_fs(sbi->sb, 1); + if (err) + return err; + } f2fs_update_time(sbi, REQ_TIME); return 0; @@ -1293,8 +1311,11 @@ static int f2fs_cross_rename(struct inode *old_dir, struct dentry *old_dentry, f2fs_unlock_op(sbi, &lc); - if (IS_DIRSYNC(old_dir) || IS_DIRSYNC(new_dir)) - f2fs_sync_fs(sbi->sb, 1); + if (IS_DIRSYNC(old_dir) || IS_DIRSYNC(new_dir)) { + err = f2fs_sync_fs(sbi->sb, 1); + if (err) + return err; + } f2fs_update_time(sbi, REQ_TIME); return 0; diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index 0c8f60b7242f..3bdb0f891c35 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -2960,7 +2960,9 @@ static int __f2fs_remount(struct fs_context *fc, struct super_block *sb) set_sbi_flag(sbi, SBI_IS_DIRTY); set_sbi_flag(sbi, SBI_IS_CLOSE); - f2fs_sync_fs(sb, 1); + err = f2fs_sync_fs(sb, 1); + if (err) + goto restore_gc; clear_sbi_flag(sbi, SBI_IS_CLOSE); } From 3b2c5d35cf4398edf47c4a3ad076838654a30015 Mon Sep 17 00:00:00 2001 From: Seongjae Jeong Date: Mon, 24 Aug 2026 02:32:31 +0000 Subject: [PATCH 47/51] f2fs: use adjusted write range after f2fs_write_checks() generic_write_checks() in f2fs_write_checks() can adjust iocb->ki_pos for append writes and truncate the iterator to limit the number of bytes to write. In f2fs_file_write_iter(), the pinned-file overwrite check currently uses the position and count saved before f2fs_write_checks(), so it can check a range different from the actual write range. The forced buffered I/O cleanup also uses orig_pos saved before f2fs_write_checks(). For O_APPEND writes, this can make the cleanup flush and invalidate the wrong page cache range. Move the pinned-file overwrite check after f2fs_write_checks() and use the adjusted iocb->ki_pos and iov_iter_count(from). Also save the adjusted write position and use it for the forced buffered I/O cleanup. Fixes: 3fdd89b452c2 ("f2fs: prevent writing without fallocate() for pinned files") Fixes: 92318f20d703 ("f2fs: preserve direct write semantics when buffering is forced") Signed-off-by: Seongjae Jeong Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/file.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index d82be8c1502a..d440231b8cb9 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -5641,9 +5641,8 @@ static ssize_t f2fs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) bool dio; bool may_need_sync = true; int preallocated; - const loff_t pos = iocb->ki_pos; - const ssize_t count = iov_iter_count(from); ssize_t ret; + loff_t bufio_start_pos; if (unlikely(f2fs_cp_error(F2FS_I_SB(inode)))) { ret = -EIO; @@ -5664,15 +5663,17 @@ static ssize_t f2fs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) inode_lock(inode); } + ret = f2fs_write_checks(iocb, from); + if (ret <= 0) + goto out_unlock; + if (f2fs_is_pinned_file(inode) && - !f2fs_overwrite_io(inode, pos, count)) { + !f2fs_overwrite_io(inode, iocb->ki_pos, iov_iter_count(from))) { ret = -EIO; goto out_unlock; } - ret = f2fs_write_checks(iocb, from); - if (ret <= 0) - goto out_unlock; + bufio_start_pos = iocb->ki_pos; /* Determine whether we will do a direct write or a buffered write. */ dio = f2fs_should_use_dio(inode, iocb, from); @@ -5727,8 +5728,8 @@ static ssize_t f2fs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) */ if (ret > 0 && !dio && (iocb->ki_flags & IOCB_DIRECT)) f2fs_flush_buffered_write(iocb->ki_filp->f_mapping, - orig_pos, - orig_pos + ret - 1); + bufio_start_pos, + bufio_start_pos + ret - 1); return ret; } From ce366bfa821ec81dd45bde547ee31e659306cc61 Mon Sep 17 00:00:00 2001 From: Chao Yu Date: Mon, 24 Aug 2026 21:17:29 +0800 Subject: [PATCH 48/51] f2fs: fix to avoid potential deadloop in f2fs_fsync_node_pages() There is potential deadloop in race condition: Thread A Thread B - fsync - f2fs_do_sync_file - f2fs_fsync_node_pages - last_fsync_dnode - folio_get(last_folio) - f2fs_setattr - f2fs_truncate - f2fs_truncate_blocks - f2fs_do_truncate_blocks - f2fs_truncate_inode_blocks - truncate_dnode - truncate_node - invalidate_mapping_pages - folio->mapping = NULL - is_node_folio alwasy return false - atomic && !marked is always true, then goto retry Cc: stable@kernel.org Fixes: 608514deba38 ("f2fs: set fsync mark only for the last dnode") Signed-off-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/node.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c index 968e5ed38816..86c2e67e43b6 100644 --- a/fs/f2fs/node.c +++ b/fs/f2fs/node.c @@ -2016,6 +2016,11 @@ int f2fs_fsync_node_pages(struct f2fs_sb_info *sbi, struct inode *inode, f2fs_debug(sbi, "Retry to write fsync mark: ino=%u, idx=%lx", ino, last_folio->index); folio_lock(last_folio); + if (unlikely(!is_node_folio(last_folio))) { + f2fs_folio_put(last_folio, true); + ret = -EAGAIN; + goto out; + } f2fs_folio_wait_writeback(last_folio, NODE, true, true); folio_mark_dirty(last_folio); folio_unlock(last_folio); From 8c963d1738fdca400082ff5f9d99e083de4f4e70 Mon Sep 17 00:00:00 2001 From: Daeho Jeong Date: Mon, 24 Aug 2026 18:53:03 -0700 Subject: [PATCH 49/51] f2fs: accurately adjust free_sections during free_segment_range In free_segment_range(), MAIN_SECS(sbi) is temporarily reduced by `secs` to restrict block allocation to the safe remaining main area while valid blocks in the truncated range are evacuated by GC. However, FREE_I(sbi)->free_sections tracks the total number of free sections across the whole filesystem. If any sections within the truncated range were already free upon entering free_segment_range(), failing to deduct them from free_sections causes the filesystem to overestimate available free sections in the active, reduced main area. This leads to inconsistent free section accounting during GC data migration and can trigger unexpected allocation failures or assertion errors when space is tight. Fix this by calculating the number of already-free sections in the truncated range, deducting them from free_sections upon entering free_segment_range(), and restoring them on exit. Fixes: b4b10061ef98 ("f2fs: refactor resize_fs to avoid meta updates in progress") Cc: stable@vger.kernel.org Signed-off-by: Daeho Jeong Signed-off-by: Sunmin Jeong Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- fs/f2fs/gc.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index 0c17038fcfd7..0a00180c21dc 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -2231,17 +2231,27 @@ void f2fs_reset_gc_victim_resource(struct f2fs_sb_info *sbi, static int free_segment_range(struct f2fs_sb_info *sbi, unsigned int secs, bool dry_run) { - unsigned int next_inuse, start, end; + unsigned int secno, next_inuse, start, end, end_secno; struct cp_control cpc = { CP_RESIZE, 0, 0, 0 }; + unsigned int freed_secs = 0; int err = 0; int type; MAIN_SECS(sbi) -= secs; start = MAIN_SECS(sbi) * SEGS_PER_SEC(sbi); end = MAIN_SEGS(sbi) - 1; + end_secno = GET_SEC_FROM_SEG(sbi, end); f2fs_reset_gc_victim_resource(sbi, start, end); + spin_lock(&FREE_I(sbi)->segmap_lock); + for (secno = MAIN_SECS(sbi); secno <= end_secno; secno++) { + if (!test_bit(secno, FREE_I(sbi)->free_secmap)) + freed_secs++; + } + FREE_I(sbi)->free_sections -= freed_secs; + spin_unlock(&FREE_I(sbi)->segmap_lock); + /* Move out cursegs from the target range */ for (type = CURSEG_HOT_DATA; type < NR_CURSEG_TYPE; type++) { err = f2fs_allocate_segment_for_resize(sbi, type, start, end); @@ -2266,6 +2276,9 @@ static int free_segment_range(struct f2fs_sb_info *sbi, f2fs_bug_on(sbi, 1); } out: + spin_lock(&FREE_I(sbi)->segmap_lock); + FREE_I(sbi)->free_sections += freed_secs; + spin_unlock(&FREE_I(sbi)->segmap_lock); MAIN_SECS(sbi) += secs; return err; } From 24c1a47f1ed28f8b31db9a36eb26d1ffbd089d7a Mon Sep 17 00:00:00 2001 From: Jaegeuk Kim Date: Tue, 18 Aug 2026 19:37:23 +0000 Subject: [PATCH 50/51] f2fs: don't leave the hashed inode while it's unlinked f2fs_symlink() 1. f2fs_new_inode 2. f2fs_add_link 3. write_being|end to fill the symlink path 4. flush dirty pages and or checkpoint Step 4 is nice to succeed, which doesn't become a reason to roll back the created symlink. OTOH, if we get an error till step 3, don't leave its dentry and its inode. Reviewed-by: Chao Yu Reviewed-by: Wenjie Qi Signed-off-by: Jaegeuk Kim --- fs/f2fs/f2fs.h | 3 +- fs/f2fs/inode.c | 6 ++- fs/f2fs/namei.c | 118 +++++++++++++++++++++++++----------------------- 3 files changed, 69 insertions(+), 58 deletions(-) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index a1f5f375045a..b0a9c14de595 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -3878,7 +3878,8 @@ void f2fs_update_inode_page(struct inode *inode); int f2fs_write_inode(struct inode *inode, struct writeback_control *wbc); void f2fs_remove_donate_inode(struct inode *inode); void f2fs_evict_inode(struct inode *inode); -void f2fs_handle_failed_inode(struct inode *inode, struct f2fs_lock_context *lc); +void f2fs_handle_failed_inode(struct inode *inode, + struct f2fs_lock_context *lc, bool add_orphan); int f2fs_init_evict_inode_work(void); void f2fs_destroy_evict_inode_work(void); diff --git a/fs/f2fs/inode.c b/fs/f2fs/inode.c index bac1e360d966..96cc0e777567 100644 --- a/fs/f2fs/inode.c +++ b/fs/f2fs/inode.c @@ -1107,7 +1107,8 @@ void f2fs_evict_inode(struct inode *inode) } /* caller should call f2fs_lock_op() */ -void f2fs_handle_failed_inode(struct inode *inode, struct f2fs_lock_context *lc) +void f2fs_handle_failed_inode(struct inode *inode, + struct f2fs_lock_context *lc, bool orphan_free) { struct f2fs_sb_info *sbi = F2FS_I_SB(inode); struct node_info ni; @@ -1129,6 +1130,9 @@ void f2fs_handle_failed_inode(struct inode *inode, struct f2fs_lock_context *lc) /* don't make bad inode, since it becomes a regular file. */ unlock_new_inode(inode); + if (!orphan_free) + goto out; + /* * Note: we should add inode to orphan list before f2fs_unlock_op() * so we can prevent losing this orphan when encoutering checkpoint diff --git a/fs/f2fs/namei.c b/fs/f2fs/namei.c index 7d9f8e92de06..afaab7739283 100644 --- a/fs/f2fs/namei.c +++ b/fs/f2fs/namei.c @@ -411,7 +411,7 @@ static int f2fs_create(struct mnt_idmap *idmap, struct inode *dir, f2fs_balance_fs(sbi, true); return 0; out: - f2fs_handle_failed_inode(inode, &lc); + f2fs_handle_failed_inode(inode, &lc, true); return err; } @@ -566,40 +566,31 @@ static struct dentry *f2fs_lookup(struct inode *dir, struct dentry *dentry, return ERR_PTR(err); } -static int f2fs_unlink(struct inode *dir, struct dentry *dentry) +static int __do_unlink(struct inode *dir, struct inode *inode, + const struct qstr *name) { struct f2fs_sb_info *sbi = F2FS_I_SB(dir); - struct inode *inode = d_inode(dentry); struct f2fs_dir_entry *de; struct f2fs_lock_context lc; struct folio *folio; int err; - trace_f2fs_unlink_enter(dir, dentry); + if (IS_DEVICE_ALIASING(inode)) + return -EPERM; - if (IS_DEVICE_ALIASING(inode)) { - err = -EPERM; - goto out; - } - - if (unlikely(f2fs_cp_error(sbi))) { - err = -EIO; - goto out; - } + if (unlikely(f2fs_cp_error(sbi))) + return -EIO; err = f2fs_dquot_initialize(dir); if (err) - goto out; + return err; err = f2fs_dquot_initialize(inode); if (err) - goto out; + return err; - de = f2fs_find_entry(dir, &dentry->d_name, &folio); - if (!de) { - if (IS_ERR(folio)) - err = PTR_ERR(folio); - goto out; - } + de = f2fs_find_entry(dir, name, &folio); + if (!de) + return IS_ERR(folio) ? PTR_ERR(folio) : 0; if (unlikely(inode->i_nlink == 0)) { f2fs_warn(sbi, "%s: inode (ino=%llx) has zero i_nlink", @@ -617,11 +608,28 @@ static int f2fs_unlink(struct inode *dir, struct dentry *dentry) err = f2fs_acquire_orphan_inode(sbi); if (err) { f2fs_unlock_op(sbi, &lc); - f2fs_folio_put(folio, false); - goto out; + goto err_out; } f2fs_delete_entry(de, folio, dir, inode); f2fs_unlock_op(sbi, &lc); + return 0; + +corrupted: + err = -EFSCORRUPTED; + set_sbi_flag(sbi, SBI_NEED_FSCK); +err_out: + f2fs_folio_put(folio, false); + return err; +} + +static int f2fs_unlink(struct inode *dir, struct dentry *dentry) +{ + int err; + + trace_f2fs_unlink_enter(dir, dentry); + err = __do_unlink(dir, d_inode(dentry), &dentry->d_name); + if (err) + goto out; /* VFS negative dentries are incompatible with Encoding and * Case-insensitiveness. Eventually we'll want avoid @@ -632,19 +640,10 @@ static int f2fs_unlink(struct inode *dir, struct dentry *dentry) if (IS_ENABLED(CONFIG_UNICODE) && IS_CASEFOLDED(dir)) d_invalidate(dentry); - if (IS_DIRSYNC(dir)) { - err = f2fs_sync_fs(sbi->sb, 1); - if (err) - goto out; - } - - goto out; -corrupted: - err = -EFSCORRUPTED; - set_sbi_flag(sbi, SBI_NEED_FSCK); - f2fs_folio_put(folio, false); + if (IS_DIRSYNC(dir)) + err = f2fs_sync_fs(F2FS_I_SB(dir)->sb, 1); out: - trace_f2fs_unlink_exit(inode, err); + trace_f2fs_unlink_exit(d_inode(dentry), err); return err; } @@ -671,6 +670,8 @@ static int f2fs_symlink(struct mnt_idmap *idmap, struct inode *dir, struct inode *inode; size_t len = strlen(symname); struct fscrypt_str disk_link; + bool orphan_free = true; + int ret = -EAGAIN; int err; if (unlikely(f2fs_cp_error(sbi))) @@ -701,17 +702,19 @@ static int f2fs_symlink(struct mnt_idmap *idmap, struct inode *dir, f2fs_lock_op(sbi, &lc); err = f2fs_add_link(dentry, inode); if (err) - goto out_f2fs_handle_failed_inode; + goto free_inode; f2fs_unlock_op(sbi, &lc); f2fs_alloc_nid_done(sbi, inode->i_ino); + /* Write the symlink path to the new inode. */ err = fscrypt_encrypt_symlink(inode, symname, len, &disk_link); if (err) - goto err_out; + goto unlink_free_inode; err = page_symlink(inode, disk_link.name, disk_link.len); + if (err) + goto unlink_free_inode; -err_out: d_instantiate_new(dentry, inode); /* @@ -723,26 +726,29 @@ static int f2fs_symlink(struct mnt_idmap *idmap, struct inode *dir, * If the symlink path is stored into inline_data, there is no * performance regression. */ - if (!err) { - err = filemap_write_and_wait_range(inode->i_mapping, 0, - disk_link.len - 1); - - if (!err && IS_DIRSYNC(dir)) - err = f2fs_sync_fs(sbi->sb, 1); - } - - if (err) - f2fs_unlink(dir, dentry); + ret = filemap_write_and_wait_range(inode->i_mapping, 0, + disk_link.len - 1); + if (!ret && IS_DIRSYNC(dir)) + err = f2fs_sync_fs(sbi->sb, 1); f2fs_balance_fs(sbi, true); - goto out_free_encrypted_link; - -out_f2fs_handle_failed_inode: - f2fs_handle_failed_inode(inode, &lc); -out_free_encrypted_link: +out: if (disk_link.name != (unsigned char *)symname) kfree(disk_link.name); return err; + +unlink_free_inode: + ret = __do_unlink(dir, inode, &dentry->d_name); + if (ret) { + /* Give up and leave a broken symlink. */ + d_instantiate_new(dentry, inode); + goto out; + } + orphan_free = false; + f2fs_lock_op(sbi, &lc); +free_inode: + f2fs_handle_failed_inode(inode, &lc, orphan_free); + goto out; } static struct dentry *f2fs_mkdir(struct mnt_idmap *idmap, struct inode *dir, @@ -791,7 +797,7 @@ static struct dentry *f2fs_mkdir(struct mnt_idmap *idmap, struct inode *dir, out_fail: clear_inode_flag(inode, FI_INC_LINK); - f2fs_handle_failed_inode(inode, &lc); + f2fs_handle_failed_inode(inode, &lc, true); return ERR_PTR(err); } @@ -847,7 +853,7 @@ static int f2fs_mknod(struct mnt_idmap *idmap, struct inode *dir, f2fs_balance_fs(sbi, true); return 0; out: - f2fs_handle_failed_inode(inode, &lc); + f2fs_handle_failed_inode(inode, &lc, true); return err; } @@ -918,7 +924,7 @@ static int __f2fs_tmpfile(struct mnt_idmap *idmap, struct inode *dir, release_out: f2fs_release_orphan_inode(sbi); out: - f2fs_handle_failed_inode(inode, &lc); + f2fs_handle_failed_inode(inode, &lc, true); return err; } From c966d29e01bbf829f8bb4a39a49811c56cdb49c3 Mon Sep 17 00:00:00 2001 From: Daeho Jeong Date: Wed, 26 Aug 2026 08:36:44 -0700 Subject: [PATCH 51/51] f2fs: support resizable tail section and unify pinned allocation Currently, zoned block devices restrict pinned file allocations to conventional zones at the beginning of the storage (before first_seq_zone_segno), triggering range GC when conventional space is exhausted. On regular block devices, when preparing for future online filesystem resizing (e.g. partition shrinking), pinned files must not be allocated in the tail area that will be truncated, as pinned files cannot be relocated by GC. Specifying the resizable tail area size (in sections) allows uniform mount configuration across devices of different storage capacities. To support this, introduce a unified `pinned_area_max_secno` boundary abstraction in `f2fs_sb_info`: 1. Add `-o resizable_tail_secno=%u` mount option to specify the number of sections at the tail of the filesystem reserved for resizing. 2. In `f2fs_fill_super()`, initialize `sbi->pinned_area_max_secno` as: min(MAIN_SECS(sbi) - resizable_tail_sec, zoned_max_sec). 3. In `get_new_segment()`, restrict segment allocation for pinned files (`pinning == true`) to `0 .. sbi->pinned_area_max_secno - 1`. If no free section is available in the pinned area, return -EAGAIN. 4. In `f2fs_allocate_pinning_section()`, unify the range GC trigger to run `f2fs_gc_range()` up to `sbi->pinned_area_max_secno` whenever `sbi->pinned_area_max_secno < MAIN_SECS(sbi)` and allocation returns -EAGAIN. 5. Expose `/sys/fs/f2fs//pinned_area_max_secno` as a read-only sysfs node. Signed-off-by: Daeho Jeong Signed-off-by: Sunmin Jeong Reviewed-by: Wenjie Qi Reviewed-by: Chao Yu Signed-off-by: Jaegeuk Kim --- Documentation/ABI/testing/sysfs-fs-f2fs | 7 +++++ Documentation/filesystems/f2fs.rst | 8 +++++- fs/f2fs/f2fs.h | 2 ++ fs/f2fs/segment.c | 35 ++++++++++++------------ fs/f2fs/segment.h | 1 + fs/f2fs/super.c | 36 +++++++++++++++++++++++++ fs/f2fs/sysfs.c | 2 ++ 7 files changed, 73 insertions(+), 18 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-fs-f2fs b/Documentation/ABI/testing/sysfs-fs-f2fs index 85194e4c7f01..0cebc89799dd 100644 --- a/Documentation/ABI/testing/sysfs-fs-f2fs +++ b/Documentation/ABI/testing/sysfs-fs-f2fs @@ -1013,3 +1013,10 @@ Description: Every time a write operation completes f2fs_write_end_io() is the maximum size of a write bio that is completed in atomic (atc) context. The default value for this attribute is UINT_MAX which means that this functionality is disabled by default. + +What: /sys/fs/f2fs//pinned_area_max_secno +Date: August 2026 +Contact: "Daeho Jeong" +Description: This is a read-only entry to show the upper bound section number + for pinned files. Pinned files will only be allocated within + sections 0 to pinned_area_max_secno - 1. diff --git a/Documentation/filesystems/f2fs.rst b/Documentation/filesystems/f2fs.rst index 1a5fd4afe609..a3c3b6948734 100644 --- a/Documentation/filesystems/f2fs.rst +++ b/Documentation/filesystems/f2fs.rst @@ -417,7 +417,13 @@ lookup_mode=%s Control the directory lookup behavior for casefolded auto F2FS determines the mode based on the on-disk `SB_ENC_NO_COMPAT_FALLBACK_FL` flag. - ================== ======================================== +resizable_tail_secno=%u Control the number of sections at the tail of the + filesystem reserved for online resizing. Pinned files + will only be allocated within sections 0 to + (MAIN_SECS - resizable_tail_secno) - 1. If set to 0 + (default), there is no tail restriction unless running + on a zoned block device where conventional zones are + used. ======================== ============================================================ Debugfs Entries diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index b0a9c14de595..16720f1f0a9c 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -255,6 +255,7 @@ struct f2fs_mount_info { block_t unusable_cap; /* Amount of space allowed to be * unusable when disabling checkpoint */ + unsigned int resizable_tail_secno; /* number of resizable tail sections */ /* For compression */ unsigned char compress_algorithm; /* algorithm type */ @@ -2005,6 +2006,7 @@ struct f2fs_sb_info { spinlock_t dev_lock; /* protect dirty_device */ bool aligned_blksize; /* all devices has the same logical blksize */ unsigned int first_seq_zone_segno; /* first segno in sequential zone */ + unsigned int pinned_area_max_secno; /* upper bound section for pinned files */ unsigned int bggc_io_aware; /* For adjust the BG_GC priority when pending IO */ unsigned int allocate_section_hint; /* the boundary position between devices */ unsigned int allocate_section_policy; /* determine the section writing priority */ diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 56decf9c691c..1e7e745be71d 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -2877,6 +2877,7 @@ static int get_new_segment(struct f2fs_sb_info *sbi, unsigned int old_zoneno = GET_ZONE_FROM_SEG(sbi, *newseg); unsigned int alloc_policy = sbi->allocate_section_policy; unsigned int alloc_hint = sbi->allocate_section_hint; + unsigned int max_secno = MAIN_SECS(sbi); bool init = true; bool looped = false; int i, devi; @@ -2908,7 +2909,7 @@ static int get_new_segment(struct f2fs_sb_info *sbi, */ if (f2fs_sb_has_blkzoned(sbi)) { /* Prioritize writing to conventional zones */ - if (sbi->blkzone_alloc_policy == BLKZONE_ALLOC_PRIOR_CONV || pinning) + if (sbi->blkzone_alloc_policy == BLKZONE_ALLOC_PRIOR_CONV) segno = 0; else segno = max(sbi->first_seq_zone_segno, *newseg); @@ -2924,19 +2925,24 @@ static int get_new_segment(struct f2fs_sb_info *sbi, alloc_hint > MAIN_SECS(sbi)) alloc_hint = MAIN_SECS(sbi); - if (alloc_policy == ALLOCATE_FORWARD_FROM_HINT && - hint < alloc_hint) - hint = alloc_hint; - else if (alloc_policy == ALLOCATE_FORWARD_WITHIN_HINT && - hint >= alloc_hint) + if (pinning) { + max_secno = sbi->pinned_area_max_secno; hint = 0; + } else if (alloc_policy == ALLOCATE_FORWARD_FROM_HINT && + hint < alloc_hint) { + hint = alloc_hint; + } else if (alloc_policy == ALLOCATE_FORWARD_WITHIN_HINT && + hint >= alloc_hint) { + hint = 0; + } find_other_zone: - secno = find_next_zero_bit(free_i->free_secmap, MAIN_SECS(sbi), hint); + secno = find_next_zero_bit(free_i->free_secmap, max_secno, hint); - if (secno >= MAIN_SECS(sbi)) { + if (secno >= max_secno) { if (looped) { - ret = -ENOSPC; + ret = (pinning && has_unpinned_area(sbi)) ? + -EAGAIN : -ENOSPC; f2fs_bug_on(sbi, !pinning); goto out_unlock; } @@ -3001,12 +3007,6 @@ static int get_new_segment(struct f2fs_sb_info *sbi, goto out_unlock; } - /* no free section in conventional device or conventional zone */ - if (new_sec && pinning && - f2fs_is_sequential_zone_area(sbi, START_BLOCK(sbi, segno))) { - ret = -EAGAIN; - goto out_unlock; - } __set_inuse(sbi, segno); *newseg = segno; out_unlock: @@ -3472,8 +3472,9 @@ int f2fs_allocate_pinning_section(struct f2fs_sb_info *sbi) err = f2fs_allocate_new_section(sbi, CURSEG_COLD_DATA_PINNED, false); f2fs_unlock_op(sbi, &lc); - if (f2fs_sb_has_blkzoned(sbi) && err == -EAGAIN && gc_required) { - err = f2fs_gc_range(sbi, 0, sbi->first_seq_zone_segno - 1, + if (has_unpinned_area(sbi) && err == -EAGAIN && gc_required) { + err = f2fs_gc_range(sbi, 0, + sbi->pinned_area_max_secno * SEGS_PER_SEC(sbi) - 1, true, ZONED_PIN_SEC_REQUIRED_COUNT, true); if (err) return err; diff --git a/fs/f2fs/segment.h b/fs/f2fs/segment.h index db1079169a23..5949aa5200ac 100644 --- a/fs/f2fs/segment.h +++ b/fs/f2fs/segment.h @@ -43,6 +43,7 @@ static inline void sanity_check_seg_type(struct f2fs_sb_info *sbi, #define MAIN_SEGS(sbi) (SM_I(sbi)->main_segments) #define MAIN_SECS(sbi) ((sbi)->total_sections) +#define has_unpinned_area(sbi) ((sbi)->pinned_area_max_secno < MAIN_SECS(sbi)) #define TOTAL_SEGS(sbi) \ (SM_I(sbi) ? SM_I(sbi)->segment_count : \ diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index 3bdb0f891c35..253a579e9d5b 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -235,6 +235,7 @@ enum { Opt_jqfmt, Opt_checkpoint, Opt_lookup_mode, + Opt_resizable_tail_secno, Opt_err, }; @@ -366,6 +367,7 @@ static const struct fs_parameter_spec f2fs_param_specs[] = { fsparam_flag("age_extent_cache", Opt_age_extent_cache), fsparam_enum("errors", Opt_errors, f2fs_param_errors), fsparam_enum("lookup_mode", Opt_lookup_mode, f2fs_param_lookup_mode), + fsparam_u32("resizable_tail_secno", Opt_resizable_tail_secno), {} }; @@ -404,6 +406,7 @@ static match_table_t f2fs_checkpoint_tokens = { #define F2FS_SPEC_errors (1 << 23) #define F2FS_SPEC_lookup_mode (1 << 24) #define F2FS_SPEC_reserve_node (1 << 25) +#define F2FS_SPEC_resizable_tail_secno (1 << 26) struct f2fs_fs_context { struct f2fs_mount_info info; @@ -551,6 +554,17 @@ static inline void adjust_unusable_cap_perc(struct f2fs_sb_info *sbi) F2FS_OPTION(sbi).unusable_cap_perc); } +static inline void adjust_pinned_area_boundary(struct f2fs_sb_info *sbi) +{ + sbi->pinned_area_max_secno = MAIN_SECS(sbi); + if (f2fs_sb_has_blkzoned(sbi) && sbi->first_seq_zone_segno != NULL_SEGNO) + sbi->pinned_area_max_secno = min(sbi->pinned_area_max_secno, + GET_SEC_FROM_SEG(sbi, sbi->first_seq_zone_segno)); + if (F2FS_OPTION(sbi).resizable_tail_secno) + sbi->pinned_area_max_secno = min(sbi->pinned_area_max_secno, + MAIN_SECS(sbi) - F2FS_OPTION(sbi).resizable_tail_secno); +} + static void init_once(void *foo) { struct f2fs_inode_info *fi = (struct f2fs_inode_info *) foo; @@ -1235,6 +1249,10 @@ static int f2fs_parse_param(struct fs_context *fc, struct fs_parameter *param) F2FS_CTX_INFO(ctx).lookup_mode = result.uint_32; ctx->spec_mask |= F2FS_SPEC_lookup_mode; break; + case Opt_resizable_tail_secno: + F2FS_CTX_INFO(ctx).resizable_tail_secno = result.uint_32; + ctx->spec_mask |= F2FS_SPEC_resizable_tail_secno; + break; } return 0; } @@ -1763,6 +1781,9 @@ static void f2fs_apply_options(struct fs_context *fc, struct super_block *sb) F2FS_OPTION(sbi).errors = F2FS_CTX_INFO(ctx).errors; if (ctx->spec_mask & F2FS_SPEC_lookup_mode) F2FS_OPTION(sbi).lookup_mode = F2FS_CTX_INFO(ctx).lookup_mode; + if (ctx->spec_mask & F2FS_SPEC_resizable_tail_secno) + F2FS_OPTION(sbi).resizable_tail_secno = + F2FS_CTX_INFO(ctx).resizable_tail_secno; f2fs_apply_compression(fc, sb); f2fs_apply_test_dummy_encryption(fc, sb); @@ -1771,6 +1792,13 @@ static void f2fs_apply_options(struct fs_context *fc, struct super_block *sb) static int f2fs_sanity_check_options(struct f2fs_sb_info *sbi, bool remount) { + unsigned int total_sections = le32_to_cpu(sbi->raw_super->section_count); + + if (F2FS_OPTION(sbi).resizable_tail_secno >= total_sections) { + f2fs_err(sbi, "Option resizable_tail_secno is larger than or equal to total sections (%u >= %u)", + F2FS_OPTION(sbi).resizable_tail_secno, total_sections); + return -EINVAL; + } if (f2fs_sb_has_device_alias(sbi) && !test_opt(sbi, READ_EXTENT_CACHE)) { f2fs_err(sbi, "device aliasing requires extent cache"); @@ -2544,6 +2572,10 @@ static int f2fs_show_options(struct seq_file *seq, struct dentry *root) else if (F2FS_OPTION(sbi).lookup_mode == LOOKUP_AUTO) seq_show_option(seq, "lookup_mode", "auto"); + if (F2FS_OPTION(sbi).resizable_tail_secno) + seq_printf(seq, ",resizable_tail_secno=%u", + F2FS_OPTION(sbi).resizable_tail_secno); + return 0; } @@ -2586,6 +2618,7 @@ static void default_options(struct f2fs_sb_info *sbi, bool remount) F2FS_OPTION(sbi).bggc_mode = BGGC_MODE_ON; F2FS_OPTION(sbi).memory_mode = MEMORY_MODE_NORMAL; F2FS_OPTION(sbi).errors = MOUNT_ERRORS_CONTINUE; + F2FS_OPTION(sbi).resizable_tail_secno = 0; set_opt(sbi, INLINE_XATTR); set_opt(sbi, INLINE_DATA); @@ -3045,6 +3078,7 @@ static int __f2fs_remount(struct fs_context *fc, struct super_block *sb) sb->s_flags = (sb->s_flags & ~SB_POSIXACL) | (test_opt(sbi, POSIX_ACL) ? SB_POSIXACL : 0); + adjust_pinned_area_boundary(sbi); limit_reserve_root(sbi); fc->sb_flags = (flags & ~SB_LAZYTIME) | (sb->s_flags & SB_LAZYTIME); @@ -5287,6 +5321,8 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc) /* get segno of first zoned block device */ sbi->first_seq_zone_segno = get_first_seq_zone_segno(sbi); + adjust_pinned_area_boundary(sbi); + sbi->reserved_pin_section = f2fs_sb_has_blkzoned(sbi) ? ZONED_PIN_SEC_REQUIRED_COUNT : GET_SEC_FROM_SEG(sbi, overprovision_segments(sbi)); diff --git a/fs/f2fs/sysfs.c b/fs/f2fs/sysfs.c index 3201e2185fea..811e350a1430 100644 --- a/fs/f2fs/sysfs.c +++ b/fs/f2fs/sysfs.c @@ -1313,6 +1313,7 @@ F2FS_SBI_GENERAL_RW_ATTR(blkzone_alloc_policy); #endif F2FS_SBI_GENERAL_RW_ATTR(carve_out); F2FS_SBI_GENERAL_RW_ATTR(reserved_pin_section); +F2FS_SBI_GENERAL_RO_ATTR(pinned_area_max_secno); F2FS_SBI_GENERAL_RW_ATTR(bggc_io_aware); F2FS_SBI_GENERAL_RW_ATTR(max_lock_elapsed_time); F2FS_SBI_GENERAL_RW_ATTR(lock_duration_priority); @@ -1525,6 +1526,7 @@ static struct attribute *f2fs_attrs[] = { ATTR_LIST(max_read_extent_count), ATTR_LIST(carve_out), ATTR_LIST(reserved_pin_section), + ATTR_LIST(pinned_area_max_secno), ATTR_LIST(allocate_section_hint), ATTR_LIST(allocate_section_policy), ATTR_LIST(max_lock_elapsed_time),