* [Bug report] ext4: unvalidated hash trailer read past a fake dirent in ext4_inlinedir_to_tree()
@ 2026-11-10 10:00 Guikun Yang
2026-11-12 1:13 ` [PATCH] ext4: fix out-of-bounds read " Jiahao Xu
0 siblings, 1 reply; 3+ messages in thread
From: Guikun Yang @ 2026-11-10 10:00 UTC (permalink / raw)
To: Theodore Ts'o; +Cc: linux-ext4, linux-kernel, Andreas Dilger, Jan Kara
Hi,
Reading a crafted inline directory ends in a KASAN slab-out-of-bounds in
ext4_inlinedir_to_tree(), reachable from getdents64() on a mounted image:
BUG: KASAN: slab-out-of-bounds in ext4_inlinedir_to_tree+0xb6c/0xc00
Read of size 4 at addr ffff8880052496bc by task init/1
ext4_inlinedir_to_tree
ext4_htree_fill_tree
ext4_dx_readdir
ext4_readdir
__se_sys_getdents64
The address is at offset 0x3c of a kmalloc(60) object, i.e. exactly at
the end of the buffer that holds the inline directory payload.
## The gap
For a casefolded and encrypted directory, the dirent hash trailer lives
right after the name, at an offset derived from name_len alone:
#define EXT4_DIRENT_HASHES(entry) \
((struct ext4_dir_entry_hash *)(((void *)(entry)) + \
((8 + (entry)->name_len + EXT4_DIR_ROUND) & ~EXT4_DIR_ROUND)))
__ext4_check_dir_entry() decides whether an entry is "fake" first:
static bool is_fake_dir_entry(struct ext4_dir_entry_2 *de)
{
...
if (de->file_type == EXT4_FT_DIR_CSUM)
return true;
return false;
}
and then validates rec_len against the fake variant of the length
helper, which does not account for the 8-byte trailer:
else if (unlikely(rlen < ext4_dir_rec_len(de->name_len,
fake ? NULL : dir)))
error_msg = "rec_len is too small for name_len";
With file_type == 0xDE, a 12-byte entry (name_len <= 4, no trailer)
passes every check, including landing exactly at inline_size. The
readdir path then reads the trailer unconditionally:
if (ext4_hash_in_dirent(dir)) {
hinfo->hash = EXT4_DIRENT_HASH(de);
hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
}
EXT4_DIRENT_HASH(de) evaluates to de + 12 for such an entry, so the
read starts at the end of the dir_buf allocation.
Reaching this requires ext4_hash_in_dirent(), i.e. S_CASEFOLD and
S_ENCRYPTED on the directory inode. Both come from the on-disk i_flags
and are set unconditionally in ext4_set_inode_flags(); no key is
needed, because fscrypt_get_encryption_info() is called with
allow_unsupported from fscrypt_prepare_readdir() and returns 0 both for
an unreadable "c" xattr (-ERANGE) and for a missing key. The only
build-time requirement is CONFIG_FS_ENCRYPTION, which distribution
kernels enable.
## Observed
Deterministic 3/3 on a QEMU x86_64 kernel with KASAN and
CONFIG_FS_ENCRYPTION. The image is a normal inline directory whose
inode carries CASEFOLD and ENCRYPT, with the last dirent given
file_type 0xDE and a rec_len that leaves it flush against the end of
the inline payload (inline_size 60, hence kmalloc(60)). The control
image with i_flags cleared mounts and reads back the same directory
without any report.
## Relation to the lookup-side fix
This is the same read that c7e6b863d298 ("ext4: check dir entry fits
before reading the hash trailer in ext4_search_dir()") closed on the
lookup path:
if (de->name + de->name_len <= dlimit &&
(!ext4_hash_in_dirent(dir) ||
(char *)de + ext4_dir_rec_len(de->name_len, dir) <= dlimit) &&
ext4_match(dir, fname, de))
The readdir path never got the equivalent check, and its leniency about
fake entries makes the gap larger: it accepts a rec_len that excludes
the trailer entirely. The neighbouring inline read path also gained two
header-length checks in 9333cc809f ("ext4: fix out-of-bounds read in
ext4_read_inline_dir()"); ext4_inlinedir_to_tree() has neither.
## Suggested fix
Skip entries whose rec_len cannot hold the trailer, mirroring the
lookup-side condition:
if (ext4_hash_in_dirent(dir)) {
+ if (ext4_dir_rec_len(de->name_len, dir) >
+ ext4_rec_len_from_disk(de->rec_len, inline_size))
+ goto out;
hinfo->hash = EXT4_DIRENT_HASH(de);
hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
}
The same condition applies to any other site that reads the trailer
without a length check; this report covers the one we reproduce.
The reproducer (image builder, ready-made image, serial log) is
available on request.
Tested on 7.3.0; fs/ext4/inline.c is unchanged in mainline as of
2026-11-10 apart from the two fixes mentioned above.
Thanks,
Guikun Yang
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH] ext4: fix out-of-bounds read in ext4_inlinedir_to_tree()
2026-11-10 10:00 [Bug report] ext4: unvalidated hash trailer read past a fake dirent in ext4_inlinedir_to_tree() Guikun Yang
@ 2026-11-12 1:13 ` Jiahao Xu
2026-11-12 1:40 ` sashiko-bot
0 siblings, 1 reply; 3+ messages in thread
From: Jiahao Xu @ 2026-11-12 1:13 UTC (permalink / raw)
To: Theodore Ts'o
Cc: Andreas Dilger, Baokun Li, Jan Kara, Ojaswin Mujoo,
Ritesh Harjani (IBM), Zhang Yi, linux-ext4, linux-kernel,
Guikun Yang, Jiahao Xu
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 <yangguikun19@qq.com>
Closes: https://lore.kernel.org/all/tencent_4F47889D184C7FA2287DC2A33A52F3BE9F09@qq.com/
Cc: stable@vger.kernel.org
Signed-off-by: Jiahao Xu <xujiahao@cmss.chinamobile.com>
---
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
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH] ext4: fix out-of-bounds read in ext4_inlinedir_to_tree()
2026-11-12 1:13 ` [PATCH] ext4: fix out-of-bounds read " Jiahao Xu
@ 2026-11-12 1:40 ` sashiko-bot
0 siblings, 0 replies; 3+ messages in thread
From: sashiko-bot @ 2026-11-12 1:40 UTC (permalink / raw)
To: Jiahao Xu; +Cc: linux-ext4, tytso
> 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
> [ ... ]
> Fixes: 471fbbea7ff7 ("ext4: handle casefolding with encryption")
> Reported-by: Guikun Yang <yangguikun19@qq.com>
> Closes: https://lore.kernel.org/all/tencent_4F47889D184C7FA2287DC2A33A52F3BE9F09@qq.com/
> Cc: stable@vger.kernel.org
> Signed-off-by: Jiahao Xu <xujiahao@cmss.chinamobile.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112091303.723243-1-xujiahao@cmss.chinamobile.com?part=1
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-12 1:40 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-10 10:00 [Bug report] ext4: unvalidated hash trailer read past a fake dirent in ext4_inlinedir_to_tree() Guikun Yang
2026-11-12 1:13 ` [PATCH] ext4: fix out-of-bounds read " Jiahao Xu
2026-11-12 1:40 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox