[PATCH 1/2] f2fs: compress: fix use-after-free of sbi in read path
From: Fan Wu
Date: Fri Sep 04 2026 - 22:01:38 EST
In f2fs_verify_cluster() and f2fs_decompress_end_io(), all pages of
the cluster are unlocked first, and only afterwards is the reference
to the decompress_io_ctx dropped via f2fs_put_dic(). If that is the
last reference, f2fs_free_dic() still dereferences sbi:
page_array_free() reads sbi->page_array_slab_size and calls
kmem_cache_free() on sbi->page_array_slab, and when not in task
context f2fs_put_dic() itself reads sbi->post_read_wq.
Once the last page of the cluster is unlocked, a concurrent unmount
can evict the inodes and proceed to f2fs_destroy_page_array_cache(sbi)
and kfree(sbi) in kill_f2fs_super(). The read path has no page
counter that f2fs_put_super() waits on, and f2fs_verify_cluster() runs
on the global fsverity_read_workqueue, which unmount does not drain.
read() on a compressed fsverity file returns as soon as the folios it
waits on are unlocked, so a read() followed by close() and umount()
leaves the completion tail exposed: when the worker is preempted
between the last folio_unlock() and the end of f2fs_free_dic() while
another CPU completes the unmount, f2fs_free_dic() accesses the freed
sbi:
BUG: KASAN: slab-use-after-free in f2fs_release_decomp_mem
Workqueue: fsverity_read_queue f2fs_verify_cluster
Freed by: kfree <- kill_f2fs_super
f2fs_decompress_end_io() drops the final reference in task context
when all compressed pages of the cluster were served from the
per-filesystem compress cache; it shares the same sbi-dereference
tail, so the matching reorder is applied there as well. A completion
in which the per-bio verity work or the cluster's verity worker
overtakes the submitting context's compressed-page release drops the
final reference in f2fs_finish_read_bio() instead; that completion
keeps the pre-existing exposure and is covered by patch 2/2.
Drop the decompress_io_ctx before unlocking the last page of the
cluster, so that all accesses to sbi happen before the last unlock.
This mirrors the ordering rule the write path was given in
f2fs_compress_write_end_io() (39d4ee19c1e7), and needs no new state;
the counter approach proposed in the report below is implemented as a
complement in patch 2/2.
This issue was found by an in-house static analysis tool.
Fixes: 4c8ff7095bef ("f2fs: support data compression")
Cc: stable@xxxxxxxxxxxxxxx
Reported-by: Cen Zhang <zzzccc427@xxxxxxxxx>
Closes: https://lore.kernel.org/linux-f2fs-devel/20260629052811.2167181-1-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>
---
Note for reviewers: the KASAN report above was made deterministic with
a 10 s msleep() injected between the last folio_unlock() and
f2fs_put_dic() in f2fs_verify_cluster(); the use-after-free itself is
the unmodified put_dic -> free_dic path, and the trigger is a plain
readahead() + close() + umount(). With the same injection on top of
this patch there are no reports (tested on v6.19 and on the v7.2-rc3
baseline this patch is based on); without the patch, 28 reports on
each. An independently captured report of the same bug is referenced
by Closes:.
---
fs/f2fs/compress.c | 23 ++++++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/fs/f2fs/compress.c b/fs/f2fs/compress.c
index ce88092d9ce2..e387a74f5c2a 100644
--- a/fs/f2fs/compress.c
+++ b/fs/f2fs/compress.c
@@ -1795,6 +1795,7 @@ static void f2fs_verify_cluster(struct work_struct *work)
{
struct decompress_io_ctx *dic =
container_of(work, struct decompress_io_ctx, verity_work);
+ struct folio *last_rfolio = NULL;
int i;
/* Verify, update, and unlock the decompressed pages. */
@@ -1807,10 +1808,20 @@ static void f2fs_verify_cluster(struct work_struct *work)
rfolio = page_folio(rpage);
if (fsverity_verify_folio(dic->vi, rfolio))
folio_mark_uptodate(rfolio);
- folio_unlock(rfolio);
+ if (last_rfolio)
+ folio_unlock(last_rfolio);
+ last_rfolio = rfolio;
}
+ /*
+ * Drop the decompress_io_ctx before unlocking the last folio: the
+ * final put still accesses sbi, and once the last folio is
+ * unlocked, a concurrent unmount can destroy it. Matches the
+ * write-path rule in f2fs_compress_write_end_io().
+ */
f2fs_put_dic(dic, true);
+ if (last_rfolio)
+ folio_unlock(last_rfolio);
}
/*
@@ -1820,6 +1831,7 @@ static void f2fs_verify_cluster(struct work_struct *work)
void f2fs_decompress_end_io(struct decompress_io_ctx *dic, bool failed,
bool in_task)
{
+ struct page *last_rpage = NULL;
int i;
if (IS_ENABLED(CONFIG_FS_VERITY) && !failed && dic->vi) {
@@ -1845,14 +1857,19 @@ void f2fs_decompress_end_io(struct decompress_io_ctx *dic, bool failed,
ClearPageUptodate(rpage);
else
SetPageUptodate(rpage);
- unlock_page(rpage);
+ if (last_rpage)
+ unlock_page(last_rpage);
+ last_rpage = rpage;
}
/*
* Release the reference to the decompress_io_ctx that was being held
- * for I/O completion.
+ * for I/O completion, before the last unlock_page(): same rule
+ * as in f2fs_verify_cluster() above.
*/
f2fs_put_dic(dic, in_task);
+ if (last_rpage)
+ unlock_page(last_rpage);
}
/*
--
2.34.1