ext4_read_inline_dir() reads de->rec_len / de->name past the end of its inline buffer for a crafted or corrupted inline directory, triggering a slab-out-of-bounds read during getdents64(): BUG: KASAN: slab-out-of-bounds in filldir64 (fs/readdir.c:371) Read of size 8 at addr ffff88800fd3da3c by task exploit/146 ... kasan_report (mm/kasan/report.c:595) filldir64 (fs/readdir.c:371) iterate_dir (fs/readdir.c:110) ... The payload is copied into a buffer of exactly inline_size bytes: dir_buf = kmalloc(inline_size, GFP_NOFS); but iteration runs in a logical position space extra_offset bytes larger than the buffer (extra_size = extra_offset + inline_size), so the synthetic "." and ".." entries land at the offsets they would have in a block-based directory. A real dirent is formed at "dir_buf + pos - extra_offset", yet the loop bounds and the ext4_check_dir_entry() length argument are all expressed in the larger extra_size. Two reachable sites dereference a dirent before confirming its physical offset is inside the allocation: In the main loop, ctx->pos is attacker-controlled via lseek() and the entry is validated with extra_size, so ext4_check_dir_entry() accepts a dirent running up to extra_offset bytes past the allocation before its length check fires. ctx->pos is also a signed loff_t: an lseek() to a small value below extra_offset makes "ctx->pos - extra_offset" negative, so a check that only bounds the top of the buffer is bypassed by underflow and de is formed before dir_buf. In the cookie-rescan loop, entered when i_version changed since the last readdir(2), the walk restarts from the beginning with i bounded by extra_size, so as i approaches extra_size the unconditional read of de->rec_len runs past the allocation before any validation. Both are the same defect, logical extra_size space versus the physical inline_size buffer. In each loop, reject a dirent whose header would not fit within inline_size before forming de, and in the main loop also reject a position that underflows below extra_offset. Validate the main-loop entry against inline_size rather than extra_size. Entries that legitimately fill the inline data still pass. Fixes: c4d8b0235aa9 ("ext4: fix readdir error in case inline_data+^dir_index.") Reported-by: Weiming Shi Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Xiang Mei --- v2: check the both bounds fs/ext4/inline.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c index 8045e4ff270c..bbe93f1b56b2 100644 --- a/fs/ext4/inline.c +++ b/fs/ext4/inline.c @@ -1454,6 +1454,9 @@ int ext4_read_inline_dir(struct file *file, /* for other entry, the real offset in * the buf has to be tuned accordingly. */ + if (i - extra_offset + ext4_dir_rec_len(1, NULL) > + inline_size) + break; de = (struct ext4_dir_entry_2 *) (dir_buf + i - extra_offset); /* It's too expensive to do a full @@ -1488,10 +1491,20 @@ int ext4_read_inline_dir(struct file *file, continue; } + /* + * de lives at dir_buf + ctx->pos - extra_offset, so the dirent + * header must fit within inline_size. ctx->pos is a signed, + * lseek()-controlled loff_t: check the lower bound first, or + * ctx->pos < extra_offset underflows and points de before dir_buf. + */ + if (ctx->pos < extra_offset || + ctx->pos - extra_offset + ext4_dir_rec_len(1, NULL) > + inline_size) + goto out; de = (struct ext4_dir_entry_2 *) (dir_buf + ctx->pos - extra_offset); if (ext4_check_dir_entry(inode, file, de, iloc.bh, dir_buf, - extra_size, ctx->pos)) + inline_size, ctx->pos)) goto out; if (le32_to_cpu(de->inode)) { if (!dir_emit(ctx, de->name, de->name_len, -- 2.43.0