Linux EXT4 FS development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Shuai Liu" <847807867@qq.com>, linux-ext4@vger.kernel.org
Cc: tytso@mit.edu
Subject: Re: [PATCH v5] ext4: fix slab-use-after-free in do_split()
Date: Wed, 04 Nov 2026 10:24:13 +0000	[thread overview]
Message-ID: <sashiko-outbox-197087@kernel.org> (raw)
In-Reply-To: <tencent_4EAEF2024D7BB83D62E41306EED8D2112C08@qq.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] ext4: late alias check in make_indexed_dir() after buffer modification
- [Critical] ext4: unchecked buffer aliasing in ext4_dx_add_entry() index split
- [Critical] ext4: late leaf-to-index alias check in ext4_dx_add_entry()
--

commit eea9bdf87e428c4c24ab845db976b1362da00cdf
Author: Shuai Liu <847807867@qq.com>

ext4: fix slab-use-after-free in do_split()

This patch adds checks in do_split() to verify that neither the leaf directory
block being split (*bh) nor the newly allocated block (bh2) aliases each other
or any htree index block in frames[0..frame]. If an alias is detected,
do_split() logs an error and returns -EFSCORRUPTED before modifying directory
entries.

> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index 231a274b814ec..878ed25ca4a2b 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);
> +		}
> +	}

[Severity: Critical]
Is checking for *bh == f->bh inside do_split() too late when called from
ext4_dx_add_entry()?

In ext4_dx_add_entry(), after reading the leaf block bh, add_dirent_to_buf()
is called before do_split() is ever reached:

    bh = ext4_read_dirblock(dir, dx_get_block(frame->at), DIRENT_HTREE);
    if (IS_ERR(bh)) {
        err = PTR_ERR(bh);
        bh = NULL;
        goto cleanup;
    }
    ...
    err = add_dirent_to_buf(handle, fname, dir, inode, NULL, bh);
    if (err != -ENOSPC)
        goto cleanup;

If bh aliases one of the index buffers in frames[0..frame].bh, dx_probe() has
already marked that buffer verified, so __ext4_read_dirblock() returns it
without running leaf checksum verification. Because an htree root block has
root->dotdot->rec_len == blocksize - 12 (and an internal dx_node has
node->fake.rec_len == blocksize), on a filesystem without metadata_csum,
ext4_find_dest_de() finds space inside that fake dirent and returns 0 instead
of -ENOSPC.

ext4_insert_dentry() then writes the new directory entry directly over the
htree header (for example, overwriting root->info.indirect_levels at offset 30
of frames[0].bh->b_data with de1->name_len), and ext4_dx_add_entry() jumps to
cleanup without calling do_split(). In dx_release(), the loop:

    indirect_levels = info->indirect_levels;
    for (i = 0; i <= indirect_levels; i++) {
        if (frames[i].bh == NULL)
            break;
        brelse(frames[i].bh);
        frames[i].bh = NULL;
    }

can then read past the end of the frames[EXT4_HTREE_LEVEL] stack array when
all frames are populated, or leak frame buffers if indirect_levels was
overwritten with a smaller value. Should the leaf-to-index alias check happen
in ext4_dx_add_entry() right after ext4_read_dirblock()?

[ ... ]

> @@ -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;

[Severity: Critical]
In make_indexed_dir(), frame->bh is bh (logical block 0) and the buffer passed
to do_split() is bh2, which was allocated earlier in make_indexed_dir() via
ext4_append():

    bh2 = ext4_append(handle, dir, &block);
    if (IS_ERR(bh2)) {
        brelse(bh);
        return PTR_ERR(bh2);
    }
    ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
    data2 = bh2->b_data;

    memcpy(data2, de, len);
    memset(de, 0, len); /* wipe old data */

If block 0 is marked free in a corrupted block bitmap and ext4_append()
returns bh2 == bh, won't make_indexed_dir() perform an overlapping memcpy()
over root->dot and root->dotdot, wipe the copied entries with memset(),
reinitialize bh->b_data as a dx_root header, and dirty both buffers in the
journal before do_split() ever checks *bh == f->bh?

> @@ -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;

[Severity: Critical]
What happens if the index split right above this do_split() call in
ext4_dx_add_entry() receives an aliased buffer from ext4_append()?

When dx_get_count(entries) == dx_get_limit(entries), ext4_dx_add_entry()
allocates a new index block:

    bh2 = ext4_append(handle, dir, &newblock);
    if (IS_ERR(bh2)) {
        err = PTR_ERR(bh2);
        goto cleanup;
    }
    node2 = (struct dx_node *)(bh2->b_data);
    entries2 = node2->entries;
    memset(&node2->fake, 0, sizeof(struct fake_dirent));

Nothing checks whether bh2 aliases frames[0..level].bh or the leaf block bh:

1. In the !add_level branch, if bh2 aliases (frame - 1)->bh, the memcpy() into
   entries2 overwrites (frame - 1)->entries, and the subsequent call to
   dx_insert_block((frame - 1), hash2, newblock) reads the corrupted count and
   can memmove() out of bounds past the end of (frame - 1)->bh->b_data.

2. In the add_level branch, if bh2 aliases frames[0].bh, the memcpy() from
   entries (offset 32) to entries2 (offset 8) is an overlapping copy within
   frames[0].bh->b_data that overwrites dxroot->info.indirect_levels at offset
   30, increments it, and jumps directly to journal_error / cleanup without
   reaching do_split(), causing dx_release(frames) to loop out of bounds past
   frames[EXT4_HTREE_LEVEL] or leak frame references.

3. In the !add_level branch when restart == 0 and at - entries < icount1, if
   bh2 aliases the leaf block bh, bh->b_data is overwritten with a dx_node
   header and index entries, and bh2 is released without swapping into
   frame->bh, so do_split() then runs on the corrupted leaf buffer without
   detecting the alias.

Could the alias checks be performed when bh2 is allocated during the index
split as well?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/tencent_4EAEF2024D7BB83D62E41306EED8D2112C08@qq.com?part=1

      reply	other threads:[~2026-11-04 10:24 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-04  8:12 [PATCH] ext4: fix slab-use-after-free in do_split() Shuai Liu
2026-11-04  8:26 ` sashiko-bot
2026-11-04  8:47   ` [PATCH v2] " Shuai Liu
2026-11-04  9:02     ` sashiko-bot
2026-11-04  9:21       ` [PATCH v3] " Shuai Liu
2026-11-04  9:36         ` sashiko-bot
2026-11-04  9:51           ` [PATCH v4] " Shuai Liu
2026-11-04 10:03             ` sashiko-bot
2026-11-04 10:10               ` [PATCH v5] " Shuai Liu
2026-11-04 10:24                 ` sashiko-bot [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-197087@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=847807867@qq.com \
    --cc=linux-ext4@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=tytso@mit.edu \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox