[BUG] jfs: unvalidated log superblock fields in lmLogInit() panic the kernel on mount
From: yangguikun
Date: Fri Oct 16 2026 - 03:49:58 EST
Hello Dave,
Mounting a crafted jfs image that uses an external log makes lmLogInit()
trust three on-disk fields in the log superblock without any range check,
which ends in a kernel panic. It is deterministic: 3 out of 3 runs on
7.3-rc7, mount crashes, no write needed. The fields are size, l2bsize
and end; l2bsize is the one that reaches the crash here.
Trigger
-------
mount -t jfs -o loop crafted.img /mnt/x
The image has to be one whose log lives on a separate device (the usual
"external log" layout), and the log's superblock in block 1 carries an
out-of-range l2bsize. Mounting a block device needs CAP_SYS_ADMIN, as
with any filesystem image; nothing else is required. A control image
that differs only in l2bsize = 12 mounts and runs normally.
Root cause
----------
lmLogInit() reads the log superblock and validates only magic, state and
uuid:
fs/jfs/jfs_logmgr.c:1287-1314
if ((rc = lbmRead(log, 1, &bpsuper)))
...
logsuper = (struct logsuper *) bpsuper->l_ldata;
if (logsuper->magic != cpu_to_le32(LOGMAGIC)) { ... }
if (logsuper->state != cpu_to_le32(LOGREDONE)) { ... }
} else {
if (!uuid_equal(&logsuper->uuid, &log->uuid)) { ... }
log->size = le32_to_cpu(logsuper->size);
log->l2bsize = le32_to_cpu(logsuper->l2bsize);
...
log->page = le32_to_cpu(logsuper->end) / LOGPSIZE;
log->eor = le32_to_cpu(logsuper->end) -
(LOGPSIZE * log->page);
Neither size nor l2bsize nor end is range-checked, and l2bsize here
overwrites the sane value that lmLogInit() had just set
(jfs_logmgr.c:1269 sets log->l2bsize = L2LOGPSIZE for external logs). It
is then used directly as a shift count:
fs/jfs/jfs_logmgr.c:1892
bp->l_blkno = log->base + (pn << (L2LOGPSIZE - log->l2bsize));
fs/jfs/jfs_logmgr.c:2018
fs/jfs/jfs_logmgr.c:2088
... << (log->l2bsize - 9);
With l2bsize = 13, "L2LOGPSIZE - l2bsize" is negative and "l2bsize - 9"
is 4, so the computed block number / sector is garbage; the bogus I/O
that follows trips a use-after-free in the same function and the mount
dies.
Note the asymmetry inside the same function: the inline-log branch
(jfs_logmgr.c:1306-1308) does check the on-disk size against the
in-memory one and rejects a mismatch, while the external-log branch only
compares the uuid. The size field is unchecked there too, so
log->logsize = (log->size - 2) << L2LOGPSIZE (jfs_logmgr.c:1385) can go
negative as well; I reproduced the l2bsize case.
Crash
-----
UBSAN: shift-out-of-bounds in fs/jfs/jfs_logmgr.c:1892:32
shift exponent -1 is negative
...
UBSAN: shift-out-of-bounds in fs/jfs/jfs_logmgr.c:2018:28
UBSAN: shift-out-of-bounds in fs/jfs/jfs_logmgr.c:2088:28
...
BUG: KASAN: slab-use-after-free in lmLogInit+0x16dd/0x1a90
Read of size 8 at addr ff110000413e4218 by task mount/8208
Call Trace:
<TASK>
kasan_report+0xca/0x100
lmLogInit+0x16dd/0x1a90
lmLogOpen+0x...
jfs_mount_rw+0x...
jfs_fill_super+0x...
...
Oops: invalid opcode: 0000 [#1] SMP KASAN NOPTI
RIP: 0010:___free_pages+0x15b/0x190
Kernel panic - not syncing: Fatal exception
Three runs on three fresh VMs, all with l2bsize = 13, all panicked; the
control (l2bsize = 12, same image otherwise) mounted with no UBSAN and no
KASAN.
Suggested fix
-------------
Validate the three fields before using them, the way the inline branch
already validates size. For example, require l2bsize to match
L2LOGPSIZE for external logs (or at least bound it), and reject size < 2
and an out-of-range end:
--- a/fs/jfs/jfs_logmgr.c
+++ b/fs/jfs/jfs_logmgr.c
@@ ... (in the external-log branch of lmLogInit, after the uuid check)
+ if (le32_to_cpu(logsuper->l2bsize) != L2LOGPSIZE ||
+ le32_to_cpu(logsuper->size) < 2) {
+ rc = -EINVAL;
+ goto ...; /* existing error path */
+ }
log->size = le32_to_cpu(logsuper->size);
log->l2bsize = le32_to_cpu(logsuper->l2bsize);
(The exact placement and the error label depend on how you prefer to
structure it; the existing "free logsuper and return rc" path is fine.)
Versions
--------
Reproduced on 7.3-rc7. The external-log branch in the 7.2 tree I checked
reads and uses the same three fields the same way, with the same missing
check. I could not produce a Fixes: tag: the kernel trees available to
me are not git repositories, so I have no verifiable commit to point at
and did not want to guess.
Thanks,
yangguikun <yangguikun19@xxxxxx>