[PATCH v5] ext4: fix slab-use-after-free in do_split()
From: Shuai Liu
Date: Wed Nov 04 2026 - 05:14:01 EST
do_split() allocates a new directory block with ext4_append() to hold
the second half of the entries of the leaf block being split, then
dx_move_dirents() copies entries from the old block (*bh) into the new
one (bh2). Nothing verifies that the newly allocated block differs
from the block being split.
On a filesystem whose block bitmap is inconsistent with the directory
tree, a block already referenced as a directory leaf or as an htree
index block can still be marked free, so ext4_append() can return that
same block. With concurrent operations on such a filesystem (as
exercised by the syzbot reproducer), the block can even be freed
between allocations, resulting in bh2 aliasing *bh. dx_move_dirents()
then copies the block onto itself, corrupting directory entries
mid-copy: a rec_len field can be overwritten with name bytes, so the
subsequent wipe of the stale entry memset()s a bogus length, causing
the slab-use-after-free reported by syzbot.
The same aliasing applies to the htree index blocks: if the leaf block
or the new block aliases any index frame (the parent index block
frame->bh or one of its ancestors in the frames[] array),
dx_move_dirents() and dx_pack_dirents() overwrite the index header,
dx_insert_block() then memmove()s with a corrupted count and writes
out of bounds, and dx_release() can loop past the frames[] array (or
leak frames) with a corrupted indirect_levels.
Fix this by checking for all of these aliases in do_split() and
bailing out with -EFSCORRUPTED before any directory block contents are
modified, reporting the corruption via ext4_error_inode_block().
Reported-by: syzbot+09bec78ee77613a3efdd@xxxxxxxxxxxxxxxxxxxxxxxxx
Closes: https://syzkaller.appspot.com/bug?extid=09bec78ee77613a3efdd
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Shuai Liu <847807867@xxxxxx>
Changes in v5:
- The v4 posting mistakenly carried the v3 diff (the code changes
were left unstaged when the commit was amended), so the code did
not match the v4 changelog. The code now actually implements
those changes: do_split() takes the frames[] array and checks *bh
and bh2 against all frames[0..level] (spotted by Sashiko)
---
fs/ext4/namei.c | 39 +++++++++++++++++++++++++++++++++++----
1 file changed, 35 insertions(+), 4 deletions(-)
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 354fc2d22df1..5f2790a0b8b4 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -1903,13 +1903,14 @@ static struct ext4_dir_entry_2 *dx_pack_dirents(struct inode *dir, char *base,
* Returns pointer to de in block into which the new entry will be inserted.
*/
static struct ext4_dir_entry_2 *do_split(handle_t *handle, struct inode *dir,
- struct buffer_head **bh,struct dx_frame *frame,
- struct dx_hash_info *hinfo)
+ struct buffer_head **bh, struct dx_frame *frames,
+ struct dx_frame *frame, struct dx_hash_info *hinfo)
{
unsigned blocksize = dir->i_sb->s_blocksize;
unsigned continued;
int count;
struct buffer_head *bh2;
+ struct dx_frame *f;
ext4_lblk_t newblock;
u32 hash2;
struct dx_map_entry *map;
@@ -1922,12 +1923,42 @@ static struct ext4_dir_entry_2 *do_split(handle_t *handle, struct inode *dir,
if (ext4_has_feature_metadata_csum(dir->i_sb))
csum_size = sizeof(struct ext4_dir_entry_tail);
+ for (f = frames; f <= frame; f++) {
+ if (*bh == f->bh) {
+ ext4_error_inode_block(dir, (*bh)->b_blocknr,
+ EFSCORRUPTED,
+ "leaf block aliased with index block");
+ brelse(*bh);
+ *bh = NULL;
+ return ERR_PTR(-EFSCORRUPTED);
+ }
+ }
+
bh2 = ext4_append(handle, dir, &newblock);
if (IS_ERR(bh2)) {
brelse(*bh);
*bh = NULL;
return ERR_CAST(bh2);
}
+ if (bh2 == *bh) {
+ ext4_error_inode_block(dir, (*bh)->b_blocknr, EFSCORRUPTED,
+ "cannot split aliased directory block");
+ brelse(bh2);
+ brelse(*bh);
+ *bh = NULL;
+ return ERR_PTR(-EFSCORRUPTED);
+ }
+ for (f = frames; f <= frame; f++) {
+ if (bh2 == f->bh) {
+ ext4_error_inode_block(dir, bh2->b_blocknr,
+ EFSCORRUPTED,
+ "leaf block aliased with index block");
+ brelse(bh2);
+ brelse(*bh);
+ *bh = NULL;
+ return ERR_PTR(-EFSCORRUPTED);
+ }
+ }
BUFFER_TRACE(*bh, "get_write_access");
err = ext4_journal_get_write_access(handle, dir->i_sb, *bh,
@@ -2325,7 +2356,7 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
if (retval)
goto out_frames;
- de = do_split(handle,dir, &bh2, frame, &fname->hinfo);
+ de = do_split(handle, dir, &bh2, frames, frame, &fname->hinfo);
if (IS_ERR(de)) {
retval = PTR_ERR(de);
goto out_frames;
@@ -2631,7 +2662,7 @@ static int ext4_dx_add_entry(handle_t *handle, struct ext4_filename *fname,
goto journal_error;
}
}
- de = do_split(handle, dir, &bh, frame, &fname->hinfo);
+ de = do_split(handle, dir, &bh, frames, frame, &fname->hinfo);
if (IS_ERR(de)) {
err = PTR_ERR(de);
goto cleanup;
--
2.43.0