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 an inflated 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 therefore formed at the physical address "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. This mismatch lets 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(). The entry is validated with extra_size instead of inline_size, so ext4_check_dir_entry() accepts rec_len/name_len running up to extra_offset bytes past the allocation, and dereferences de before its (too-large) length check fires. In the cookie-rescan loop, entered when i_version changed since the last readdir(2) (or after an lseek resets the cookie), the walk restarts from the beginning with i bounded by extra_size. As i approaches extra_size, "i - extra_offset" approaches inline_size, so 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, so fix them together. In each loop, reject a dirent whose minimum-size header would not fit within inline_size before forming and dereferencing de, and validate the main-loop entry against inline_size rather than extra_size. extra_offset only inflates the logical position of "." and ".."; the dirent itself always lives in the kmalloc(inline_size) buffer. Entries that legitimately fill the inline data still pass; only accesses that would fall outside the allocation are rejected. 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 --- fs/ext4/inline.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c index 8045e4ff270c..0ec85cfcc859 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,21 @@ int ext4_read_inline_dir(struct file *file, continue; } + /* + * ctx->pos can be set to an arbitrary value via lseek(), and + * the rescan above may also advance it. Make sure the dirent + * header lies within the inline_size payload before + * dereferencing it: extra_offset only inflates the logical + * position of "." and "..", the dirent itself always lives in + * the kmalloc(inline_size) buffer. + */ + if (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