[Bug report] gfs2: unvalidated lf_depth causes shift-out-of-bounds in dir_e_read, dir_split_leaf and gfs2_dir_exhash_dealloc

From: Guikun Yang

Date: Tue Nov 03 2026 - 01:36:27 EST


Hi,

Mounting a gfs2 image whose leaf blocks carry an lf_depth larger than the
inode's i_depth triggers an undefined shift in three places. i_depth is
validated when the dinode is read; lf_depth is never compared against it.

fs/gfs2/dir.c:1560 dir_e_read()
len = BIT(dip->i_depth - depth); /* depth = lf->lf_depth */

fs/gfs2/dir.c:1050 dir_split_leaf()
if (dip->i_depth == be16_to_cpu(oleaf->lf_depth)) /* equality only */
...
len = BIT(dip->i_depth - be16_to_cpu(oleaf->lf_depth));
half_len = len >> 1;
if (!half_len) /* checked after the shift */

fs/gfs2/dir.c:2137 gfs2_dir_exhash_dealloc()
len = BIT(dip->i_depth - be16_to_cpu(leaf->lf_depth));

Observed with a 4K-block image (mkfs.gfs2 -J 8) whose root directory was
grown to exhash with 400 long-named files, i_depth 8, 16 leaf blocks at
lf_depth 3. After a clean umount, lf_depth in every LF block (block offset
+24) is raised by 12, then the image is mounted and getdents64() issued:

UBSAN: shift-out-of-bounds in fs/gfs2/dir.c:1557:9
shift exponent 4294967289 is too large for 64-bit type 'long unsigned int'
Kernel panic - not syncing: UBSAN: panic_on_warn set ...

4294967289 is (unsigned)(8 - 15). Changing the lf_depth delta changes the
exponent by the same amount, so the chain is unambiguous.

One detail worth noting: BIT() yields unsigned long and the destination in
all three functions is u32, so the truncated len is 0. In dir_e_read()
that makes index = (index & ~(len - 1)) + len evaluate to 0 while
index < hsize is still true; in gfs2_dir_exhash_dealloc() next_index also
stays 0. I have not measured the behaviour with panic_on_warn disabled.

gfs2 metadata blocks carry no checksum, so the image is built with
mkfs.gfs2 plus direct edits at two fixed offsets and needs no checksum
recomputation. A single forged image reaches all three sites depending on
the operation performed afterwards: getdents64() for dir_e_read, creating
an entry in a full leaf for dir_split_leaf, and rmdir of an emptied exhash
directory for gfs2_dir_exhash_dealloc (gfs2_dir_del() decrements
lf_entries but leaves GFS2_DIF_EXHASH and the leaves in place, and
evict_unlinked_inode() calls gfs2_dir_exhash_dealloc() when the flag is
still set).

All three sites obtain their buffer through get_leaf(), which has the
directory inode at hand, so one check there covers them. After the
existing gfs2_metatype_check(), when no error was returned:

struct gfs2_leaf *lf = (struct gfs2_leaf *)(*bhp)->b_data;

if (be16_to_cpu(lf->lf_depth) > dip->i_depth) {
gfs2_consist_inode(dip);
error = -EIO;
}

lf_depth <= i_depth holds for every directory built by the kernel:
dir_split_leaf() returns early when the two are equal, so a leaf is never
given a depth above the inode's.

The reproducer (image builder, VM harness, crash log) is available on
request.

Thanks,
Guikun Yang