* [Bug report] ext4: unvalidated hash trailer read past a fake dirent in ext4_inlinedir_to_tree()
@ 2026-11-10 10:00 Guikun Yang
0 siblings, 0 replies; only message 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] only message in thread
only message in thread, other threads:[~2026-11-09 17:26 UTC | newest]
Thread overview: (only message) (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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®