[PATCH] jfs: fix use-after-free in jfs_readdir on legacy non-indexed directories

From: Jenny Guanni Qu

Date: Fri Jun 12 2026 - 15:12:26 EST


In the legacy (non-indexed) readdir path, jfs_readdir calls dtReadNext
to advance through the directory's dtree. When dtReadNext reaches the
last leaf page, it frees the metapage via DT_PUTPAGE(mp), sets bn = -1,
and falls through to the out label, which stores the stale mp pointer
into btstack->top->mp.

Back in jfs_readdir, DT_GETSEARCH dereferences btstack.top->mp to
extract the dtpage pointer. It guards this with "if (BN)", intending to
distinguish root (bn=0) from non-root pages, but the -1 EOF sentinel
also passes this check, causing a read from the freed metapage slab
object.

The existing code already had the correct EOF check ("offset beyond
directory eof?") immediately after DT_GETSEARCH, but the dereference
inside the macro fires first. Fix this by moving the bn < 0 check
before the DT_GETSEARCH call, reading btstack.top->bn directly since
the local bn variable is not yet populated.

Also null out the stale mp pointer in dtReadNext when bn is -1, so the
btstack does not retain a dangling pointer to freed memory.

The buggy ordering predates the git history; the EOF check has followed
DT_GETSEARCH since the initial JFS merge, so there is no specific
commit to reference in a Fixes: tag.

Reported-by: syzbot+a98891ce2318fe7baf05@xxxxxxxxxxxxxxxxxxxxxxxxx
Closes: https://syzkaller.appspot.com/bug?extid=a98891ce2318fe7baf05
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Jenny Guanni Qu <qguanni@xxxxxxxxx>
---
Reproduced the crash on an x86_64 KASAN kernel (v7.1-rc6) using a C
reproducer that mounts a JFS image, clears JFS_DIR_INDEX in the
superblock to force the legacy non-indexed readdir path, and calls
getdents64 past the last leaf page. The faulting access resolves via
faddr2line to the DT_GETSEARCH dereference in jfs_readdir, and the free
to release_metapage in dtReadNext, matching the syzbot report. syzbot's
own published C reproducer produces an identical crash signature on the
same kernel.

With this patch applied (v7.1-rc8), the same reproducer drives the
identical code path (600 entries, repeated getdents64 calls through the
legacy jfs_readdir loop) and completes with no KASAN report.

fs/jfs/jfs_dtree.c | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/fs/jfs/jfs_dtree.c b/fs/jfs/jfs_dtree.c
index ac0f79fafaca..037121587571 100644
--- a/fs/jfs/jfs_dtree.c
+++ b/fs/jfs/jfs_dtree.c
@@ -2874,14 +2874,14 @@ int jfs_readdir(struct file *file, struct dir_context *ctx)
ctx->pos = DIREND;
return 0;
}
- /* get start leaf page and index */
- DT_GETSEARCH(ip, btstack.top, bn, mp, p, index);
-
/* offset beyond directory eof ? */
- if (bn < 0) {
+ if (btstack.top->bn < 0) {
ctx->pos = DIREND;
return 0;
}
+
+ /* get start leaf page and index */
+ DT_GETSEARCH(ip, btstack.top, bn, mp, p, index);
}

dirent_buf = __get_free_page(GFP_KERNEL);
@@ -3293,7 +3293,7 @@ static int dtReadNext(struct inode *ip, loff_t * offset,
btsp = btstack->top;
btsp->bn = bn;
btsp->index = dtoffset->index;
- btsp->mp = mp;
+ btsp->mp = (bn == -1) ? NULL : mp;

return 0;
}
--
2.50.1 (Apple Git-155)