[PATCH] f2fs: wait for decompress contexts before teardown
From: Cen Zhang
Date: Mon Jun 29 2026 - 01:28:28 EST
A compressed read owns a decompress_io_ctx (DIC) until cluster
completion drops its final reference. The final cleanup still reads
dic->sbi and releases DIC page arrays through sbi->page_array_slab. If
the final put is not in task context, cleanup is queued on sbi->wq.
The rpage unlocks in f2fs_verify_cluster() and f2fs_decompress_end_io()
can make the inode evictable before f2fs_put_dic() has finished. Then
unmount can pass inode eviction, destroy the per-mount workqueue and
page-array slab, and free the f2fs_sb_info while the DIC cleanup path
still has to run.
The buggy scenario involves two paths, with each column showing the order
within that path:
fsverity_read_queue worker: unmount:
1. verify decompressed rpages 1. observe the inode evictable
2. unlock the last rpage 2. evict and truncate pagecache
3. call f2fs_put_dic(dic, true) 3. destroy sbi->wq/page_array_slab
4. f2fs_free_dic() reads dic->sbi 4. free the f2fs_sb_info
Validation reproduced this kernel report:
KASAN: slab-use-after-free in f2fs_release_decomp_mem+0x1da/0x240
Workqueue: fsverity_read_queue f2fs_verify_cluster
The buggy address belongs to the object at ffff88810267c000 which belongs
to the cache kmalloc-8k of size 8192
The buggy address is located 4868 bytes inside of freed 8192-byte region
[ffff88810267c000, ffff88810267e000)
Read of size 4
Call trace:
dump_stack_lvl+0x66/0xa0
print_report+0xce/0x630
f2fs_release_decomp_mem+0x1da/0x240 (fs/f2fs/compress.c:1685)
srso_alias_return_thunk+0x5/0xfbef5
__virt_addr_valid+0x224/0x430
kasan_report+0xe0/0x110
f2fs_free_dic+0x5b/0x630 (fs/f2fs/compress.c:1766)
process_one_work+0x897/0x1820 (kernel/workqueue.c:3220)
lock_is_held_type+0x8f/0x100
worker_thread+0x575/0xf80
kthread+0x2e7/0x3c0
ret_from_fork+0x576/0x810
__switch_to+0x57e/0xe10
__switch_to_asm+0x33/0x70
ret_from_fork_asm+0x1a/0x30
Allocated by task stack:
kasan_save_stack+0x33/0x60
kasan_save_track+0x14/0x30
__kasan_kmalloc+0x8f/0xa0
__kmalloc_cache_noprof+0x235/0x5c0
f2fs_fill_super+0xd6/0x8840 (fs/f2fs/super.c:5004)
get_tree_bdev_flags+0x312/0x590
vfs_get_tree+0x8d/0x320
fc_mount+0x15/0x1d0
path_mount+0x59b/0x2020
__x64_sys_mount+0x210/0x270
do_syscall_64+0x115/0x6a0 (arch/x86/entry/syscall_64.c:87)
entry_SYSCALL_64_after_hwframe+0x77/0x7f
Freed by task stack:
kasan_save_stack+0x33/0x60
kasan_save_track+0x14/0x30
kasan_save_free_info+0x3b/0x60
__kasan_slab_free+0x43/0x70
kfree+0x2ed/0x520
kill_f2fs_super+0x3bc/0x590 (fs/f2fs/super.c:5560)
deactivate_locked_super+0xa8/0x160
cleanup_mnt+0x1e2/0x3e0
task_work_run+0x11d/0x1f0
exit_to_user_mode_loop+0x17d/0x570
do_syscall_64+0x4f0/0x6a0 (arch/x86/entry/syscall_64.c:87)
entry_SYSCALL_64_after_hwframe+0x77/0x7f
Track live DICs in the per-mount compression state. Increment the count
once a DIC is tied to the mounted sbi, decrement it only after
f2fs_free_dic() has released all sbi-owned DIC resources, and make
f2fs_put_super() wait for the count to reach zero before destroying
sbi->wq, sbi->page_array_slab, or the sbi itself. This preserves the
existing read-completion and page-unlock behavior while extending
teardown to cover the cleanup lifetime that DIC already requires.
Assisted-by: Codex:gpt-5.5
Signed-off-by: Cen Zhang <zzzccc427@xxxxxxxxx>
---
fs/f2fs/compress.c | 14 ++++++++++++++
fs/f2fs/f2fs.h | 4 ++++
fs/f2fs/super.c | 2 ++
3 files changed, 20 insertions(+)
diff --git a/fs/f2fs/compress.c b/fs/f2fs/compress.c
index 91855d91bbdd..bbdc009cfed3 100644
--- a/fs/f2fs/compress.c
+++ b/fs/f2fs/compress.c
@@ -1732,6 +1732,7 @@ struct decompress_io_ctx *f2fs_alloc_dic(struct compress_ctx *cc)
refcount_set(&dic->refcnt, 1);
dic->failed = false;
dic->vi = cc->vi;
+ atomic_inc(&sbi->nr_decompress_ctx);
for (i = 0; i < dic->cluster_size; i++)
dic->rpages[i] = cc->rpages[i];
@@ -1794,6 +1795,8 @@ static void f2fs_free_dic(struct decompress_io_ctx *dic,
page_array_free(sbi, dic->rpages, dic->nr_rpages);
kmem_cache_free(dic_entry_slab, dic);
+ if (atomic_dec_and_test(&sbi->nr_decompress_ctx))
+ wake_up_all(&sbi->decompress_io_wait);
}
static void f2fs_late_free_dic(struct work_struct *work)
@@ -2077,6 +2080,9 @@ int f2fs_init_page_array_cache(struct f2fs_sb_info *sbi)
if (!f2fs_sb_has_compression(sbi))
return 0;
+ atomic_set(&sbi->nr_decompress_ctx, 0);
+ init_waitqueue_head(&sbi->decompress_io_wait);
+
sprintf(slab_name, "f2fs_page_array_entry-%u:%u", MAJOR(dev), MINOR(dev));
sbi->page_array_slab_size = sizeof(struct page *) <<
@@ -2092,6 +2098,14 @@ void f2fs_destroy_page_array_cache(struct f2fs_sb_info *sbi)
kmem_cache_destroy(sbi->page_array_slab);
}
+void f2fs_wait_on_decompress_io(struct f2fs_sb_info *sbi)
+{
+ if (!f2fs_sb_has_compression(sbi))
+ return;
+
+ wait_event(sbi->decompress_io_wait, !atomic_read(&sbi->nr_decompress_ctx));
+}
+
int __init f2fs_init_compress_cache(void)
{
cic_entry_slab = f2fs_kmem_cache_create("f2fs_cic_entry",
diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h
index 8f3e632f315c..63ae46a2d400 100644
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -2025,6 +2025,8 @@ struct f2fs_sb_info {
#ifdef CONFIG_F2FS_FS_COMPRESSION
struct kmem_cache *page_array_slab; /* page array entry */
unsigned int page_array_slab_size; /* default page array slab size */
+ atomic_t nr_decompress_ctx; /* in-flight decompress contexts */
+ wait_queue_head_t decompress_io_wait; /* wait for decompress contexts */
/* For runtime compression statistics */
u64 compr_written_block;
@@ -4696,6 +4698,7 @@ int f2fs_init_compress_inode(struct f2fs_sb_info *sbi);
void f2fs_destroy_compress_inode(struct f2fs_sb_info *sbi);
int f2fs_init_page_array_cache(struct f2fs_sb_info *sbi);
void f2fs_destroy_page_array_cache(struct f2fs_sb_info *sbi);
+void f2fs_wait_on_decompress_io(struct f2fs_sb_info *sbi);
int __init f2fs_init_compress_cache(void);
void f2fs_destroy_compress_cache(void);
struct address_space *COMPRESS_MAPPING(struct f2fs_sb_info *sbi);
@@ -4751,6 +4754,7 @@ static inline int f2fs_init_compress_inode(struct f2fs_sb_info *sbi) { return 0;
static inline void f2fs_destroy_compress_inode(struct f2fs_sb_info *sbi) { }
static inline int f2fs_init_page_array_cache(struct f2fs_sb_info *sbi) { return 0; }
static inline void f2fs_destroy_page_array_cache(struct f2fs_sb_info *sbi) { }
+static inline void f2fs_wait_on_decompress_io(struct f2fs_sb_info *sbi) { }
static inline int __init f2fs_init_compress_cache(void) { return 0; }
static inline void f2fs_destroy_compress_cache(void) { }
static inline void f2fs_invalidate_compress_pages_range(struct f2fs_sb_info *sbi,
diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c
index 2b8d96411156..b279d3eaae53 100644
--- a/fs/f2fs/super.c
+++ b/fs/f2fs/super.c
@@ -2075,6 +2075,8 @@ static void f2fs_put_super(struct super_block *sb)
/* flush s_error_work before sbi destroy */
flush_work(&sbi->s_error_work);
+ f2fs_wait_on_decompress_io(sbi);
+
f2fs_destroy_wq(sbi);
kvfree(sbi->ckpt);
--
2.43.0