[PATCH 2/2] f2fs: compress: drain decompress contexts at unmount

From: Fan Wu

Date: Fri Sep 04 2026 - 22:02:23 EST


A decompress_io_ctx can release its final reference from several
completion contexts: the cluster's verity work and the in-task
decompression completion that patch 1/2 reorders, but also
f2fs_finish_read_bio() releasing the bio's compressed-page references,
for example when the per-bio verity work of a mixed
compressed/uncompressed bio runs on the global fsverity workqueue,
which unmount does not drain. Whenever the final put happens after
the cluster's pages were unlocked, f2fs_free_dic() can dereference
sbi (page_array_free() on sbi->page_array_slab, and f2fs_put_dic()
reading sbi->post_read_wq in softirq context) after a concurrent
unmount destroyed it.

Track the number of allocated decompress_io_ctx in sbi and make
f2fs_put_super() wait for it to drop to zero before the workqueue and
the page-array slab are destroyed, so f2fs_free_dic() only ever
touches a live sbi. This is the approach proposed in the report
below, and covers the completions patch 1/2 does not reorder.
Waiting there cannot deadlock: a worker still verifying holds the
cluster's page locks, which blocks unmount earlier in evict_inodes(),
and once those pages are unlocked its tail performs no further
filesystem I/O.

Verified with KASAN (QEMU) by parking the per-bio completion right
before it releases a compressed cluster's bio references, using a
readahead that spans an incompressible and a compressible cluster of
a verity file: 14 reports with patch 1/2 alone, none with this wait,
with umount returning only after the parked completion finished.

This issue was found by an in-house static analysis tool.

Fixes: 4c8ff7095bef ("f2fs: support data compression")
Cc: stable@xxxxxxxxxxxxxxx
Suggested-by: Cen Zhang <zzzccc427@xxxxxxxxx>
Assisted-by: Codex:gpt-5.6
Co-developed-by: Song Li <songl@xxxxxxxxxx>
Signed-off-by: Song Li <songl@xxxxxxxxxx>
Signed-off-by: Fan Wu <fanwu01@xxxxxxxxxx>
---
fs/f2fs/compress.c | 15 +++++++++++++++
fs/f2fs/f2fs.h | 6 ++++++
fs/f2fs/super.c | 3 +++
3 files changed, 24 insertions(+)

diff --git a/fs/f2fs/compress.c b/fs/f2fs/compress.c
index e387a74f5c2a..1e63e934925e 100644
--- a/fs/f2fs/compress.c
+++ b/fs/f2fs/compress.c
@@ -1705,6 +1705,7 @@ struct decompress_io_ctx *f2fs_alloc_dic(struct compress_ctx *cc)
dic->log_cluster_size = cc->log_cluster_size;
dic->nr_cpages = cc->nr_cpages;
refcount_set(&dic->refcnt, 1);
+ atomic_inc(&sbi->nr_decompress_ctx);
dic->failed = false;
dic->vi = cc->vi;

@@ -1769,6 +1770,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)
@@ -2063,6 +2066,9 @@ void f2fs_destroy_compress_inode(struct f2fs_sb_info *sbi)

int f2fs_init_page_array_cache(struct f2fs_sb_info *sbi)
{
+ atomic_set(&sbi->nr_decompress_ctx, 0);
+ init_waitqueue_head(&sbi->decompress_io_wait);
+
dev_t dev = sbi->sb->s_bdev->bd_dev;
char slab_name[35];

@@ -2084,6 +2090,15 @@ 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 8376bbe58ee3..b03853172346 100644
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -2025,6 +2025,10 @@ struct f2fs_sb_info {
struct kmem_cache *page_array_slab; /* page array entry */
unsigned int page_array_slab_size; /* default page array slab size */

+ /* For waiting for in-flight decompression contexts at umount */
+ atomic_t nr_decompress_ctx;
+ wait_queue_head_t decompress_io_wait;
+
/* For runtime compression statistics */
u64 compr_written_block;
u64 compr_saved_block;
@@ -4694,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);
@@ -4749,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 be22c31015ef..8951ede2ea48 100644
--- a/fs/f2fs/super.c
+++ b/fs/f2fs/super.c
@@ -2075,6 +2075,9 @@ static void f2fs_put_super(struct super_block *sb)
/* flush s_error_work before sbi destroy */
flush_work(&sbi->s_error_work);

+ /* wait for in-flight decompression contexts before sbi destroy */
+ f2fs_wait_on_decompress_io(sbi);
+
f2fs_destroy_wq(sbi);

kvfree(sbi->ckpt);
--
2.34.1