KASAN: slab-use-after-free in txEnd during umount

From: Jianzhou Zhao

Date: Tue Mar 10 2026 - 22:57:12 EST




Subject: [BUG] jfs: KASAN: slab-use-after-free in txEnd during umount (`jfs_sb_info`)

Dear Maintainers,

Our fuzzing tool, RacePilot, has detected another slab-use-after-free bug in the JFS subsystem on a 6.18 kernel version (`6.18.0-08691-g2061f18ad76e-dirty`).

This issue is closely related to the previously reported UAF in `lmLogSync`. It shares the same root cause: the `jfs_flush_journal` function failing to wait for lazy transactions handled by the `jfs_lazycommit` thread before allowing the unmount process to continue. In this specific trace, it manifests as a Use-After-Free on the `jfs_sb_info` (`sbi`) structure inside `txEnd()`, rather than on the `jfs_log` structure.

### Call Trace & Context
The KASAN splat reveals a read use-after-free on an object constructed by `jfs_fill_super`. (Note: Due to source-level instrumentation with RacePilot, the line numbers in the trace correspond to the instrumented source block provided below).

```
BUG: KASAN: slab-use-after-free in txEnd+0x53e/0x5a0 fs/jfs/jfs_txnmgr.c:507
Read of size 8 at addr ffff88804f850c30 by task jfsCommit/121

CPU: 0 UID: 0 PID: 121 Comm: jfsCommit Tainted: G L 6.18.0-08691-g2061f18ad76e-dirty #43 PREEMPT(full)
Call Trace:
txEnd+0x53e/0x5a0 fs/jfs/jfs_txnmgr.c:507
txLazyCommit fs/jfs/jfs_txnmgr.c:2685 [inline]
jfs_lazycommit+0x6f0/0xb10 fs/jfs/jfs_txnmgr.c:2734
kthread+0x3d0/0x780 kernel/kthread.c:463

Allocated by task 27991 (mount):
...
jfs_fill_super+0xd1/0x1030 fs/jfs/super.c:452
get_tree_bdev_flags+0x389/0x620 fs/super.c:1699
...

Freed by task 9895 (umount):
...
kfree+0x2ca/0x6d0 mm/slub.c:6871
generic_shutdown_super+0x156/0x390 fs/super.c:643
kill_block_super+0x3b/0x90 fs/super.c:1730
...
```

#### Execution Flow & Code Context:

**1. The Freeing Path (`generic_shutdown_super` -> `jfs_put_super`)**

During unmount, `jfs_umount` flushes the journal via `jfs_flush_journal(log, 2)`. However, `jfs_flush_journal` (`fs/jfs/jfs_txnmgr.c`) prematurely returns if `log->cqueue` and `log->synclist` are empty. It fails to check if there are remaining transactions in the lazy commit queue (`TxAnchor.unlock_queue`):
```c
1498: /*
1499: * NAME: jfs_flush_journal()
...
1577: if ((!list_empty(&log->cqueue)) || !list_empty(&log->synclist)) {
...
1581: if (list_empty(&log->cqueue) &&
1582: list_empty(&log->synclist))
1583: break; // <--- Premature return
```

Because `jfs_flush_journal` returns early, the unmount process resumes and eventually invokes `generic_shutdown_super`, calling the filesystem's `put_super` callback (`jfs_put_super` in `fs/jfs/super.c`). This gracefully frees the `jfs_sb_info` memory allocation (`kfree(sbi)`):
```c
190: static void jfs_put_super(struct super_block *sb)
191: {
...
194: kfree(sbi); // <--- sbi is freed here
195: }
```

**2. The Access Path (`jfs_lazycommit` -> `txEnd`)**

Concurrently, a background `jfs_lazycommit` thread (`fs/jfs/jfs_txnmgr.c`) is still processing transactions pushed to the `TxAnchor.unlock_queue` before unmount:
```c
2699: int jfs_lazycommit(void *arg)
2700: {
...
2710: while (!list_empty(&TxAnchor.unlock_queue)) {
...
2732: LAZY_UNLOCK(flags);
2733: txLazyCommit(tblk);
```

When finishing the lazily executed transaction, `txLazyCommit` invokes `txEnd()`.
```c
2651: static void txLazyCommit(struct tblock * tblk)
2652: {
...
2684: tblk->flag &= ~tblkGC_LAZY;
2685: txEnd(tblk - TxBlock); /* Convert back to tid */
```

Finally, `txEnd` (`fs/jfs/jfs_txnmgr.c`) attempts to fetch the `log` object pointer by dereferencing the private `sb->s_fs_info` pointer (`sbi`). Since `sbi` was already freed in `jfs_put_super()`, this triggers the KASAN Use-After-Free:
```c
493: void txEnd(tid_t tid)
494: {
...
507: log = JFS_SBI(tblk->sb)->log; // <--- UAF access to freed sbi block
```

### Root Cause Analysis

This bug shares the exact identical root cause with the previously reported `lmLogSync` UAF bug. `jfs_flush_journal(log, 2)` fails to properly check `TxAnchor.unlock_queue` (or active transactions handled by the lazy thread) before returning during the unmount process. Because of this race condition, varying timing can lead to `jfs_lazycommit` either dereferencing the freed `sbi` (when `txEnd` starts execution) or the freed `log` metadata (subsequently in `txEnd`). Regrettably, we were unable to create a reproduction program for this bug.

### Potential Impact

Similar to the previous report, the primary hazard boils down to a local Denial of Service (memory corruption/splat) if lazy transactions coincide intensely with filesystem unmounting operations. We gauge the exploitability risk to be low, as standard interactions around the filesystem lifecycle unmount mechanism heavily restrain the scope to trigger arbitrary execution, resulting in predominantly a local crash risk.

### Proposed Fix

Just as before, resolving the early return of `jfs_flush_journal` effectively resolves this UAF as well, since it correctly blocks the umount sequence until the `jfs_lazycommit` thread has finished processing on both `log` and `sbi`.

We can evaluate `log->active` inside `jfs_flush_journal` to fully capture any transactions handled by `jfs_lazycommit` before permitting the unmount to proceed.

```diff
--- a/fs/jfs/jfs_txnmgr.c
+++ b/fs/jfs/jfs_txnmgr.c
@@ -1574,11 +1574,12 @@ void jfs_flush_journal(struct jfs_log *log, int wait)
/*
* If there was recent activity, we may need to wait
* for the lazycommit thread to catch up
*/
- if ((!list_empty(&log->cqueue)) || !list_empty(&log->synclist)) {
+ if ((!list_empty(&log->cqueue)) || !list_empty(&log->synclist) ||
+ log->active) {
for (i = 0; i < 200; i++) { /* Too much? */
msleep(250);
write_special_inodes(log, filemap_fdatawrite);
if (list_empty(&log->cqueue) &&
- list_empty(&log->synclist))
+ list_empty(&log->synclist) &&
+ !log->active)
break;
}
}
```

We would be highly honored if this could be of any help.

Best regards,
RacePilot Team