KASAN: slab-use-after-free in lmLogSync during umount
From: Jianzhou Zhao
Date: Tue Mar 10 2026 - 22:41:17 EST
Subject: [BUG] jfs: KASAN: slab-use-after-free in lmLogSync during umount
Dear JFS Maintainers,
Our fuzzing tool, RacePilot, has detected a slab-use-after-free bug in the JFS subsystem on a 6.18 kernel version (`6.18.0-08691-g2061f18ad76e-dirty`).
This issue occurs during [jfs_umount](file:///home/kfuzz/linux/fs/jfs/jfs_umount.c#26-119) when handling the `jfs_log` metadata. The [jfs_flush_journal](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1498-1615) function attempts to wait for all outstanding transactions to finish, but fails to account for transactions that have been handed over to the lazy commit thread ([jfs_lazycommit](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#2692-2773)) via the `TxAnchor.unlock_queue`. This allows [jfs_umount](file:///home/kfuzz/linux/fs/jfs/jfs_umount.c#26-119) to prematurely free the `jfs_log` object while the [jfs_lazycommit](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#2692-2773) thread is still actively using it.
### Call Trace & Context
The bug was triggered with the following trace. (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 lmLogSync+0x7b0/0x7e0
Write of size 4 at addr ffff888054cd4a20 by task jfsCommit/122
CPU: 0 UID: 0 PID: 122 Comm: jfsCommit Tainted: G L 6.18.0-08691-g2061f18ad76e-dirty #43 PREEMPT(full)
Call Trace:
lmLogSync+0x7b0/0x7e0 fs/jfs/jfs_logmgr.c:1005
jfs_syncpt+0x8d/0xa0 fs/jfs/jfs_logmgr.c:1041
txEnd+0x30a/0x5a0 fs/jfs/jfs_txnmgr.c:550
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 15338 (umount):
...
open_inline_log fs/jfs/jfs_logmgr.c:1159 [inline]
lmLogOpen+0x571/0x1420 fs/jfs/jfs_logmgr.c:1069
jfs_mount_rw+0x2f5/0x6d0 fs/jfs/jfs_mount.c:257
...
Freed by task 9847:
...
kfree+0x2ca/0x6d0 mm/slub.c:6871
lmLogClose+0x5d0/0x750 fs/jfs/jfs_logmgr.c:1460
jfs_umount+0x2ef/0x440 fs/jfs/jfs_umount.c:114
...
```
#### Execution Flow & Code Context:
**1. The Freeing Path ([jfs_umount](file:///home/kfuzz/linux/fs/jfs/jfs_umount.c#26-119) -> [lmLogClose](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1422-1496))**
During unmount, [jfs_umount](file:///home/kfuzz/linux/fs/jfs/jfs_umount.c#26-119) ([fs/jfs/jfs_umount.c](file:///home/kfuzz/linux/fs/jfs/jfs_umount.c)) flushes the journal and then closes/frees the log object:
```c
37: int jfs_umount(struct super_block *sb)
38: {
...
54: if ((log = sbi->log))
55: /*
56: * Wait for outstanding transactions to be written to log:
57: */
58: jfs_flush_journal(log, 2);
...
106: if (log) { /* log = NULL if read-only mount */
107: updateSuper(sb, FM_CLEAN);
108: /*
109: * close log:
110: * remove file system from log active file system list.
111: */
112: rc = lmLogClose(sb);
113: }
```
The issue stems from [jfs_flush_journal](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1498-1615) ([fs/jfs/jfs_txnmgr.c](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c)), which prematurely returns if `log->cqueue` and `log->synclist` are empty, ignoring any transactions that have been handed off to the lazy commit thread (`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](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1498-1615) returns early, [lmLogClose](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1422-1496) ([fs/jfs/jfs_logmgr.c](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c)) is called and frees the [log](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1187-1220) structure via `kfree(log)`:
```c
1423: /*
1424: * NAME: lmLogClose()
...
1455: if (test_bit(log_INLINELOG, &log->flag)) {
1456: /*
1457: * in-line log in host file system
1458: */
1459: rc = lmLogShutdown(log);
1460: kfree(log); // <--- FREE
1461: goto out;
1462: }
...
1486: rc = lmLogShutdown(log);
1488: bdev_fput(bdev_file);
1489:
1490: kfree(log); // <--- FREE
```
**2. The Access Path ([jfs_lazycommit](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#2692-2773) -> [txEnd](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#484-571))**
Concurrently, a background [jfs_lazycommit](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#2692-2773) thread ([fs/jfs/jfs_txnmgr.c](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c)) is still processing transactions from `TxAnchor.unlock_queue`:
```c
2699: int jfs_lazycommit(void *arg)
2700: {
...
2710: while (!list_empty(&TxAnchor.unlock_queue)) {
2711: WorkDone = 0;
2712: list_for_each_entry(tblk, &TxAnchor.unlock_queue,
2713: cqueue) {
...
2729: /* Remove transaction from queue */
2730: list_del(&tblk->cqueue);
2731:
2732: LAZY_UNLOCK(flags);
2733: txLazyCommit(tblk);
```
Once the lazy transaction is finished, [txLazyCommit](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#2643-2691) invokes [txEnd](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#484-571):
```c
2651: static void txLazyCommit(struct tblock * tblk)
2652: {
...
2681: if (tblk->flag & tblkGC_LAZY) {
2682: spin_unlock_irq(&log->gclock); // LOGGC_UNLOCK
2683: txUnlock(tblk);
2684: tblk->flag &= ~tblkGC_LAZY;
2685: txEnd(tblk - TxBlock); /* Convert back to tid */
```
Finally, [txEnd](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#484-571) ([fs/jfs/jfs_txnmgr.c](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c)) accesses the freed [log](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1187-1220) object's members (`log->active` and `log->flag`), resulting in the KASAN Use-After-Free:
```c
493: void txEnd(tid_t tid)
494: {
...
540: /*
541: * mark the tblock not active
542: */
543: if (--log->active == 0) { // <--- UAF access
544: clear_bit(log_FLUSH, &log->flag); // <--- UAF access
545:
546: /*
547: * synchronize with logsync barrier
548: */
549: if (test_bit(log_SYNCBARRIER, &log->flag)) { // <--- UAF access (triggers KASAN report)
550: TXN_UNLOCK();
551:
552: /* write dirty metadata & forward log syncpt */
553: jfs_syncpt(log, 1);
```
### Root Cause Analysis
1. During unmount, [jfs_umount()](file:///home/kfuzz/linux/fs/jfs/jfs_umount.c#26-119) calls [jfs_flush_journal(log, 2)](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1498-1615) to wait for all outstanding transactions to complete.
2. [jfs_flush_journal(log, 2)](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1498-1615) attempts to flush all pending journal entries by polling `log->cqueue` and `log->synclist`. If both lists are empty, it assumes all transactions have completed and breaks its loop.
3. However, if a lazy transaction was already processed by [lmPostGC()](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#790-903) and handed over to [jfs_lazycommit](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#2692-2773) via [txLazyUnlock()](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#2774-2792), it is removed from `log->cqueue` and is pushed onto the global `TxAnchor.unlock_queue`.
4. Therefore, [jfs_flush_journal()](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1498-1615) can find `log->cqueue` empty and prematurely return while the [jfs_lazycommit](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#2692-2773) thread is still processing the transaction from the `unlock_queue`.
5. After [jfs_flush_journal()](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1498-1615) returns, [jfs_umount()](file:///home/kfuzz/linux/fs/jfs/jfs_umount.c#26-119) invokes [lmLogClose(sb)](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1422-1496). This completely frees the `jfs_log` metadata (`kfree(log)`).
6. Concurrently, [jfs_lazycommit](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#2692-2773) finishes the lazy transaction and calls [txEnd()](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#484-571), decrementing `log->active` and accessing `log->flag` to check for a sync barrier, triggering the slab-use-after-free on the freed [log](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1187-1220) object.
### Potential Impact
The impact of this issue is likely limited to a local Denial of Service (kernel crash due to memory corruption/splat) if [jfs_umount](file:///home/kfuzz/linux/fs/jfs/jfs_umount.c#26-119) is run simultaneously with heavy lazy transaction loads. While a Use-After-Free condition technically opens the door to arbitrary code execution, this specific scenario executes highly close to unmount time with standard filesystem APIs, reducing exploitability. The primary concern is stability when lazily committing heavily while a filesystem is unmounted. I estimate the hazard strictly as a limited severity local crash risk.
### Proposed Fix
We can fix this issue by having [jfs_flush_journal(log, wait)](file:///home/kfuzz/linux/fs/jfs/jfs_logmgr.c#1498-1615) also ensure that there are no active transactions by checking `log->active`. `log->active` correctly tracks the number of active transactions on this specific log, including those managed by [jfs_lazycommit](file:///home/kfuzz/linux/fs/jfs/jfs_txnmgr.c#2692-2773).
Here is a proposed patch:
```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