ext4_inlinedir_to_tree() reads the on-disk dirent hash trailer without checking that it lies inside the inline directory payload: if (ext4_hash_in_dirent(dir)) { hinfo->hash = EXT4_DIRENT_HASH(de); hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de); } EXT4_DIRENT_HASHES() derives the offset of the trailer from de->name_len alone, so for an entry whose rec_len ends exactly at inline_size the read starts past the kmalloc(inline_size) buffer that holds the payload. Listing such an image with getdents64() gives: BUG: KASAN: slab-out-of-bounds in ext4_inlinedir_to_tree+0xb6c/0xc00 Read of size 4 at addr ffff8880052496bc by task init/1 ... The address is at offset 0x3c of a kmalloc(60) object A 12-byte entry only gets that far because __ext4_check_dir_entry() classifies it as fake and then validates rec_len against ext4_dir_rec_len(de->name_len, fake ? NULL : dir), a length that does not account for the 8-byte trailer: else if (unlikely(rlen < ext4_dir_rec_len(de->name_len, fake ? NULL : dir))) is_fake_dir_entry() is true for a "."/".." name or for file_type == EXT4_FT_DIR_CSUM (0xDE); the crafted entry in the report uses the latter. An ordinary entry with the same rec_len is rejected by "rec_len is smaller than minimal" instead, because for it ext4_dir_rec_len() does include the trailer. Reaching the read also needs ext4_hash_in_dirent(), i.e. S_CASEFOLD and S_ENCRYPTED on the directory. Guard the read where the entry is known to be a real one, right after ext4_check_dir_entry() in the entry loop, by requiring that the entry including its rounded trailer fits in the payload. This is the same bound ext4_check_dir_entry() enforces for non-fake entries, so no well-formed entry is rejected, and it is the shape of c7e6b863d298 ("ext4: check dir entry fits before reading the hash trailer in ext4_search_dir()") and 9333cc809f ("ext4: fix out-of-bounds read in ext4_read_inline_dir()"), which fixed the lookup path and the plain inline readdir path. The check is deliberately not added to the shared "if (ext4_hash_in_dirent(dir))" block in front of both reads. That block is also reached by the synthesized "." and ".." entries, for which de points at the on-stack fake buffer instead of into dir_buf, and whose fake.rec_len is ext4_dir_rec_len(name_len, NULL) = 12 while the trailer bound is ext4_dir_rec_len(name_len, dir) = 20. A bound written as "rec_len does not cover the trailer" fires on the first loop iteration, and a bound against dir_buf + inline_size compares unrelated addresses. Either way ext4_inlinedir_to_tree() would return 0 for every casefolded and encrypted inline directory, and ext4_dx_readdir() takes a 0 return as end-of-directory, so getdents64() would silently report the directory as empty. Only this read is guarded here. The hash of the synthesized entries above is taken from uninitialized stack bytes, which is a separate issue, and the remaining trailer readers that bypass ext4_check_dir_entry() are left for their own fixes. Fixes: 471fbbea7ff7 ("ext4: handle casefolding with encryption") Reported-by: Guikun Yang Closes: https://lore.kernel.org/all/tencent_4F47889D184C7FA2287DC2A33A52F3BE9F09@qq.com/ Cc: stable@vger.kernel.org Signed-off-by: Jiahao Xu --- fs/ext4/inline.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c index ceee69a66..a48db32f9 100644 --- a/fs/ext4/inline.c +++ b/fs/ext4/inline.c @@ -1342,6 +1342,18 @@ int ext4_inlinedir_to_tree(struct file *dir_file, ret = count; goto out; } + /* + * ext4_check_dir_entry() accepts an entry it treats as + * fake (file_type == EXT4_FT_DIR_CSUM, or "."/"..") + * without room for the hash trailer, so make sure the + * trailer is inside the inline payload. + */ + if (ext4_hash_in_dirent(dir) && + (char *)de + ext4_dir_rec_len(de->name_len, dir) > + (char *)dir_buf + inline_size) { + ret = count; + goto out; + } } if (ext4_hash_in_dirent(dir)) { -- 2.43.0