Linux EXT4 FS development
 help / color / mirror / Atom feed
* [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