[Bug report] qnx6: unvalidated di_filelevels in an inode drives a shift-out-of-bounds in qnx6_block_map() - UBSAN panic at first read

From: Guikun Yang

Date: Sun Nov 01 2026 - 22:19:59 EST


To: linux-kernel@xxxxxxxxxxxxxxx
Cc: linux-fsdevel@xxxxxxxxxxxxxxx, Christian Brauner
Subject: [Bug report] qnx6: unvalidated di_filelevels in an inode drives a
shift-out-of-bounds in qnx6_block_map() - UBSAN panic at first read

Hi,

While testing on-disk field corruption in the QNX6 filesystem driver we
found that the per-inode di_filelevels byte is copied from disk and used
as a shift exponent without any bounds check, while the same field is
bounds-checked for the superblock's two root nodes. A single crafted inode
therefore panics the kernel (UBSAN shift-out-of-bounds) the first time its
data is read.

The code:

fs/qnx6/inode.c, qnx6_iget():

ei->di_filelevels = raw_inode->di_filelevels; /* disk u8, 0..255 */

fs/qnx6/inode.c, qnx6_block_map():

int ptrbits = sbi->s_ptrbits; /* ilog2(s_blocksize / 4) */
int depth = ei->di_filelevels;
...
bitdelta = ptrbits * depth;
levelptr = no >> bitdelta; /* shift >= 32 is undefined */

sbi->s_ptrbits is set from ilog2(s_blocksize / 4) when the superblock is
read, so with 512-byte blocks ptrbits is 7. Any di_filelevels above 4
already gives a shift exponent of 35 or more, and 200 gives 1400.

The asymmetry: qnx6_fill_super() does check the same field for the
superblock's two root nodes:

if (sb1->Inode.levels > QNX6_PTR_MAX_LEVELS) goto out;
if (sb1->Longfile.levels > QNX6_PTR_MAX_LEVELS) goto out;

with QNX6_PTR_MAX_LEVELS = 5 (include/linux/qnx6_fs.h), but ordinary
inodes get no check at all. Note that even the superblock bound is one
level too loose for a 512-byte block size: ptrbits * 5 = 35 >= 32.

Trigger: a 30KB hand-built QNX6 image (little endian, 512-byte blocks),
a root directory with three entries, one of them pointing at an inode
whose di_filelevels is 200. The mount succeeds and the first read of that
file panics:

UBSAN: shift-out-of-bounds in fs/qnx6/inode.c:128:16
shift exponent 1400 is too large for 32-bit type 'unsigned int'
Call Trace:
__ubsan_handle_shift_out_of_bounds
qnx6_get_block.cold
do_mpage_readpage
mpage_readahead
...
Kernel panic - not syncing: UBSAN: panic_on_warn set ...

Reproduced 3/3 on a self-built 7.3.0 kernel (CONFIG_QNX6FS_FS=y,
CONFIG_UBSAN=y, CONFIG_UBSAN_SHIFT=y) under QEMU/KVM. A control image with
di_filelevels = 4 reads the same file normally. The field and the missing
check are unchanged in current mainline.

A natural fix is to reject the inode in qnx6_iget() when di_filelevels
exceeds what ptrbits allows, and to tighten the superblock check to the
same bound. QNX6_PTR_MAX_LEVELS alone is not sufficient for small block
sizes: with ptrbits = 7 the usable maximum is 31 / 7 = 4. Happy to test a
patch.

We report this as a crafted-image robustness issue; the crash is a plain
UBSAN violation reachable from a normal mount and read.

The reproducer (the crafted image, a control image, the run script and
guest init that mount, open and read, and the image builder with the
exact field offsets) is available on request.

Kernel: self-built 7.3.0, CONFIG_QNX6FS_FS=y, CONFIG_UBSAN=y,
CONFIG_KASAN_GENERIC=y; QEMU/KVM x86_64, 1 vCPU.

Thanks,
Guikun Yang