Linux EXT4 FS development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Artem Blagodarenko" <artem.blagodarenko@gmail.com>,
	linux-ext4@vger.kernel.org
Cc: tytso@mit.edu
Subject: Re: [PATCH v13 08/11] ext4: dirdata feature
Date: Thu, 29 Oct 2026 12:51:34 +0000	[thread overview]
Message-ID: <sashiko-outbox-189320@kernel.org> (raw)
In-Reply-To: <20261029122413.230409-9-ablagodarenko@thelustrecollective.com>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] ext4: missing legacy hash size in ext4_dirent_rec_len() with dirdata
- [High] ext4: out-of-bounds read of htree root dirents in ext4_htree_store_dirent()
- [High] ext4: out-of-bounds read in ext4_dx_csum_verify() via get_dx_countlimit()
- [Medium] ext4: uninitialized stack read of fake dirent in ext4_htree_store_dirent()
--

commit 31cebce918ae1e358449a063cc4a21909275ec8a
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>

ext4: dirdata feature

This commit enables the EXT4_FEATURE_INCOMPAT_DIRDATA feature flag and updates
directory entry handling across htree, inline directory, and directory entry
insertion paths to account for variable-length dirdata records stored after
the filename.

> diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
> index 38911ae2bcfc0..15816f2a5f472 100644
> --- a/fs/ext4/dir.c
> +++ b/fs/ext4/dir.c
> @@ -483,27 +483,43 @@ void ext4_htree_free_dir_info(struct dir_private_info *p)
>   * The decrypted filename is passed in via ent_name.  parameter.
>   */
>  int ext4_htree_store_dirent(struct file *dir_file, __u32 hash,
> -			     __u32 minor_hash,
> +			     __u32 minor_hash, int buf_size,
>  			    struct ext4_dir_entry_2 *dirent,
>  			    struct fscrypt_str *ent_name)
>  {
>  	struct rb_node **p, *parent = NULL;
>  	struct fname *fname, *new_fn;
>  	struct dir_private_info *info;
> +	int extra_data = 0;
>  
>  	info = dir_file->private_data;
>  	p = &info->root.rb_node;
>  
>  	/* Create and allocate the fname structure */
> -	new_fn = kzalloc_flex(*new_fn, name, ent_name->len + 1);
> +	if (dirent->file_type & ~EXT4_FT_MASK) {
> +		unsigned int rec_len =
> +			ext4_rec_len_from_disk(dirent->rec_len, buf_size);
> +		extra_data = ext4_dirent_get_data_len(dirent, rec_len);
> +	}

[Severity: Medium]
Can this read uninitialized stack memory from fake.file_type and fake.name[]
when called from ext4_inlinedir_to_tree()?

In fs/ext4/inline.c, ext4_inlinedir_to_tree() declares struct ext4_dir_entry_2
fake on the stack without initializing it, and populates it for the "." and
".." entries:

fs/ext4/inline.c:ext4_inlinedir_to_tree() {
    struct ext4_dir_entry_2 fake;
    ...
        if (pos == 0) {
            fake.inode = cpu_to_le32(inode->i_ino);
            fake.name_len = 1;
            memcpy(fake.name, ".", 2);
            fake.rec_len = ext4_rec_len_to_disk(
                      ext4_dirent_rec_len(fake.name_len, NULL),
                      inline_size);
            ext4_set_de_type(inode->i_sb, &fake, S_IFDIR);
            de = &fake;
            pos = EXT4_INLINE_DOTDOT_OFFSET;
}

However, ext4_set_de_type() only writes de->file_type when the filetype
feature is enabled:

fs/ext4/ext4.h:ext4_set_de_type() {
    if (ext4_has_feature_filetype(sb))
        de->file_type = ext4_type_by_mode[(mode & S_IFMT)>>S_SHIFT];
}

On a filesystem with inline_data and dir_index enabled but without
EXT4_FEATURE_INCOMPAT_FILETYPE, fake.file_type and fake.name[2..3] remain
uninitialized on the stack when ext4_inlinedir_to_tree() passes de = &fake to
ext4_htree_store_dirent().

If any upper bit of the uninitialized fake.file_type is non-zero,
ext4_dirent_get_data_len(&fake, 12) reads ddh->ddh_length from uninitialized
stack bytes fake.name[2..3] and unsafe_memcpy() copies those uninitialized
bytes into new_fn->name.

The later commit in the series ("ext4: add dirdata set/get helpers") gates
this check on ext4_has_feature_dirdata() and also calls ext4_dirdata_get() on
de in ext4_inlinedir_to_tree(), but neither ext4_feature_set_ok() nor
ext4_dirdata_get() requires EXT4_FEATURE_INCOMPAT_FILETYPE when
EXT4_FEATURE_INCOMPAT_DIRDATA is enabled, and fake.file_type is still left
uninitialized before ext4_set_de_type().

Should ext4_inlinedir_to_tree() initialize fake.file_type = EXT4_FT_UNKNOWN
before calling ext4_set_de_type(), matching ext4_insert_dentry_data()?

[ ... ]

> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index e54608950fb34..30bbf6a56bf64 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c
> @@ -402,23 +402,32 @@ static struct dx_countlimit *get_dx_countlimit(struct inode *inode,
>  {
>  	struct ext4_dir_entry_2 *de;
>  	struct dx_root_info *root;
> -	int count_offset;
> +	int count_offset, dotdot_rec_len;
>  	int blocksize = EXT4_BLOCK_SIZE(inode->i_sb);
>  	unsigned int rlen = ext4_rec_len_from_disk(dirent->rec_len, blocksize);
>  
> -	if (rlen == blocksize)
> +	if (rlen == blocksize) {
>  		count_offset = sizeof(struct dx_node);
> -	else if (rlen == 12) {
> -		de = (struct ext4_dir_entry_2 *)(((void *)dirent) + 12);
> -		if (ext4_rec_len_from_disk(de->rec_len, blocksize) != blocksize - 12)
> +	} else {
> +		if (rlen < EXT4_BASE_DIR_LEN ||
> +		    rlen + EXT4_BASE_DIR_LEN > blocksize)
> +			return NULL;
> +		de = (struct ext4_dir_entry_2 *)(((char *)dirent) + rlen);
> +		if (ext4_rec_len_from_disk(de->rec_len, blocksize) !=
> +		    (unsigned int)(blocksize - rlen))
>  			return NULL;
> -		root = (struct dx_root_info *)(((void *)de + 12));
> +		/* de->rec_len covers whole dx_root block, calculate actual length.
> +		 * This is the '..' entry, which never carries the casefold+fscrypt
> +		 * hash, so pass NULL for dir regardless of the directory's flags */
> +		dotdot_rec_len = ext4_dir_entry_len(de, blocksize, NULL);
> +		if (rlen + dotdot_rec_len + sizeof(struct dx_root_info) > blocksize)
> +			return NULL;
> +		root = (struct dx_root_info *)(((char *)de + dotdot_rec_len));
>  		if (root->reserved_zero ||
>  		    root->info_length != sizeof(struct dx_root_info))
>  			return NULL;
> -		count_offset = 32;
> -	} else
> -		return NULL;
> +		count_offset = root->info_length + rlen + dotdot_rec_len;
> +	}
>  
>  	if (offset)
>  		*offset = count_offset;

[Severity: High]
Does get_dx_countlimit() allow a 4-byte out-of-bounds read past the end of
bh->b_data in ext4_dx_csum_verify() and ext4_dx_csum_set()?

The bounds check in get_dx_countlimit() checks:

    if (rlen + dotdot_rec_len + sizeof(struct dx_root_info) > blocksize)
        return NULL;

without accounting for sizeof(struct dx_countlimit) (unlike dx_get_dx_info(),
which includes sizeof(struct dx_entry) in its upper-bound check).

On a corrupted or crafted image where rlen + dotdot_rec_len +
sizeof(struct dx_root_info) == blocksize, get_dx_countlimit() sets
count_offset = blocksize and returns (struct dx_countlimit *)(((void *)dirent)
+ blocksize), which points directly past the end of the directory block
buffer.

Both ext4_dx_csum_verify() and ext4_dx_csum_set() immediately dereference
c->limit and c->count before validating count_offset against blocksize:

fs/ext4/namei.c:ext4_dx_csum_verify() {
    c = get_dx_countlimit(inode, dirent, &count_offset);
    if (!c) {
        EXT4_ERROR_INODE(inode, "dir seems corrupt?  Run e2fsck -D.");
        return 0;
    }
    limit = le16_to_cpu(c->limit);
    count = le16_to_cpu(c->count);
    if (count_offset + (limit * sizeof(struct dx_entry)) >
        EXT4_BLOCK_SIZE(inode->i_sb) - sizeof(struct ext4_dir_entry_tail))
        ...
}

Should the check in get_dx_countlimit() include sizeof(struct dx_countlimit)
so that the returned struct dx_countlimit pointer is guaranteed to lie within
the block?

[ ... ]

> @@ -1252,6 +1264,7 @@ int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,
>  		tmp_str.name = de->name;
>  		tmp_str.len = de->name_len;
>  		err = ext4_htree_store_dirent(dir_file, 0, 0,
> +					      dir->i_sb->s_blocksize,
>  					      de, &tmp_str);
>  		if (err != 0)
>  			goto errout;
> @@ -1263,6 +1276,7 @@ int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,
>  		tmp_str.name = de->name;
>  		tmp_str.len = de->name_len;
>  		err = ext4_htree_store_dirent(dir_file, 2, 0,
> +					      dir->i_sb->s_blocksize,
>  					      de, &tmp_str);
>  		if (err != 0)
>  			goto errout;

[Severity: High]
Could ext4_htree_fill_tree() and ext4_htree_store_dirent() read past the end
of frames[0].bh->b_data when storing the "." and ".." entries from an htree
root block?

In dx_probe() -> dx_get_dx_info(), the code steps from "." to ".." using
ext4_dir_entry_len():

fs/ext4/namei.c:dx_get_dx_info() {
    if (ext4_rec_len_from_disk(((struct ext4_dir_entry_2 *)de_buf)->rec_len,
                   blocksize) < EXT4_BASE_DIR_LEN)
        return ERR_PTR(-EFSCORRUPTED);
    de_buf += ext4_dir_entry_len(de_buf, blocksize, NULL);
}

By contrast, get_dx_countlimit() and ext4_htree_fill_tree() advance from "."
to ".." using dot_de->rec_len (via ext4_next_entry(de, dir->i_sb->s_blocksize))
without verifying that dot_de->rec_len matches ext4_dir_entry_len(dot_de,
blocksize, NULL).

Even with the additional rlen checks added in the later commit ("ext4: Add
EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries"),
dx_get_dx_info() permits rlen <= blocksize on "." and validates the ".." entry
and dx_root_info at offset ext4_dir_entry_len(dot_de) (for example, offset 12)
rather than at offset rlen.

As a result, on a crafted image where dot_de has ext4_dir_entry_len(dot_de) ==
12 (with valid ".." and dx_root_info at offsets 12 and 24) but dot_de->rec_len
decodes to rlen == blocksize (or blocksize - 4), dx_probe() succeeds and
ext4_htree_fill_tree() then advances de = ext4_next_entry(de, s_blocksize) to
frames[0].bh->b_data + rlen, reading de->name_len, de->inode, de->file_type,
and de->rec_len out of bounds.

Similarly, if dot_de->rec_len decodes to an offset near the end of the block
(such as 4000), ext4_htree_fill_tree() passes "." and ".." to
ext4_htree_store_dirent() without calling ext4_check_dir_entry(). In
ext4_htree_store_dirent(), rec_len = ext4_rec_len_from_disk(dirent->rec_len,
buf_size) is passed directly to ext4_dirent_get_data_len(dirent, rec_len)
without checking that (char *)dirent + rec_len or dirent->name +
dirent->name_len fits within frames[0].bh->b_data + s_blocksize, allowing
memcpy() and unsafe_memcpy() to read past the end of the block buffer.

[ ... ]

> @@ -2118,13 +2136,10 @@ int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
>  	return 0;
>  }
>  
> -void ext4_insert_dentry(struct inode *dir,
> -			struct inode *inode,
> -			struct ext4_dir_entry_2 *de,
> -			int buf_size,
> -			struct ext4_filename *fname)
> +void ext4_insert_dentry_data(struct inode *dir, struct inode *inode,
> +			     struct ext4_dir_entry_2 *de, int buf_size,
> +			     struct ext4_filename *fname, void *data)
>  {
> -
>  	int nlen, rlen;
>  
>  	nlen = ext4_dir_entry_len(de, buf_size, dir);
> @@ -2141,6 +2156,10 @@ void ext4_insert_dentry(struct inode *dir,
>  	ext4_set_de_type(inode->i_sb, de, inode->i_mode);
>  	de->name_len = fname_len(fname);
>  	memcpy(de->name, fname_name(fname), fname_len(fname));
> +	/* 'data' (the LUFID payload) and the CFHASH are written by
> +	 * ext4_dirdata_set(), which is introduced in the next patch that adds
> +	 * the dirdata set/get helpers.  Until then, only the casefold hash is
> +	 * stored at the legacy fixed offset for encrypted+casefolded dirs. */
>  	if (ext4_hash_in_dirent(dir)) {
>  		struct dx_hash_info *hinfo = &fname->hinfo;
>  

[Severity: Critical]
Does enabling EXT4_FEATURE_INCOMPAT_DIRDATA in EXT4_FEATURE_INCOMPAT_SUPP
while ext4_dirent_rec_len() omits sizeof(struct ext4_dir_entry_hash) cause
hash corruption and 8-byte out-of-bounds reads in encrypted, casefolded
directories when EXT4_DIRENT_CFHASH is not set on an entry?

In fs/ext4/ext4.h, ext4_dirent_rec_len() only reserves
sizeof(struct ext4_dir_entry_hash) when dirdata is not enabled on the
superblock:

fs/ext4/ext4.h:ext4_dirent_rec_len() {
    if (dir && ext4_hash_in_dirent(dir) &&
        !ext4_has_feature_dirdata(dir->i_sb))
        rec_len += sizeof(struct ext4_dir_entry_hash);
    return (rec_len & ~EXT4_DIR_ROUND);
}

This commit adds EXT4_FEATURE_INCOMPAT_DIRDATA to EXT4_FEATURE_INCOMPAT_SUPP,
making ext4_has_feature_dirdata(dir->i_sb) true on mounted dirdata
filesystems.

At this commit, ext4_insert_dentry_data() still writes the casefold hash at
EXT4_DIRENT_HASHES(de) without setting EXT4_DIRENT_CFHASH in de->file_type.
Because ext4_dirent_get_data_len() returns 0 and ext4_dirent_rec_len() omits
sizeof(struct ext4_dir_entry_hash), ext4_dir_entry_len(de, buf_size, dir)
returns a length 8 bytes too small, and splitting the entry at nlen =
ext4_dir_entry_len() overwrites the 8-byte hash.

Even after the follow-up commit ("ext4: add dirdata set/get helpers") adds
ext4_dirdata_set() and ext4_dirdata_get(), all hash readers
(ext4_inlinedir_to_tree(), ext4_search_dir() -> ext4_match(), ext4_readdir(),
htree_dirblock_to_tree(), and dx_make_map()) fall back to reading
EXT4_DIRENT_HASH(de) and EXT4_DIRENT_MINOR_HASH(de) at the legacy fixed offset
whenever EXT4_DIRENT_CFHASH is not set in de->file_type (or when
ext4_dirdata_get() returns an error).

However, __ext4_check_dir_entry() and ext4_search_dir() validate entry bounds
using ext4_dir_entry_len() and ext4_dirent_rec_len():

fs/ext4/dir.c:__ext4_check_dir_entry() {
    else if (unlikely(rlen < ext4_dir_entry_len(de, size, fake ? NULL : dir)))
        error_msg = "rec_len is too small for name_len";
}

fs/ext4/namei.c:ext4_search_dir() {
    if (de->name + de->name_len <= dlimit &&
        (!ext4_hash_in_dirent(dir) ||
         (char *)de + ext4_dirent_rec_len(de->name_len, dir) <= dlimit) &&
        ext4_match(dir, fname, de)) {
}

When ext4_has_feature_dirdata(dir->i_sb) is true and an entry in an
encrypted, casefolded directory does not have a valid EXT4_DIRENT_CFHASH
extension (for example, an un-migrated entry or a crafted entry with rec_len =
12 and name_len = 4 at the end of dir_buf in ext4_inlinedir_to_tree() or at
dlimit - 12 in ext4_search_dir()), bounds validation passes without requiring
the 8-byte hash, and the fallback to EXT4_DIRENT_HASHES(de) reads 8 bytes past
the end of the kmalloc(inline_size) buffer or directory block buffer.
Likewise, ext4_dir_entry_len() omits the 8-byte legacy hash for un-migrated
entries lacking EXT4_DIRENT_CFHASH, causing ext4_insert_dentry_data(),
dx_move_dirents(), and dx_pack_dirents() to overwrite their hashes.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261029122413.230409-1-ablagodarenko@thelustrecollective.com?part=8

  reply	other threads:[~2026-10-29 12:51 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-29 12:23 [PATCH v13 00/11] Data in direntry (dirdata) feature Artem Blagodarenko
2026-10-29 12:24 ` [PATCH v13 01/11] ext4: validate count against limit in ext4_dx_csum_verify/_set Artem Blagodarenko
2026-10-29 12:37   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 02/11] ext4: replace ext4_dir_entry with ext4_dir_entry_2 Artem Blagodarenko
2026-10-29 12:34   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 03/11] ext4: add ext4_dir_entry_is_tail() Artem Blagodarenko
2026-10-29 12:31   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 04/11] ext4: add dirdata format definitions and access helpers Artem Blagodarenko
2026-10-29 12:31   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 05/11] ext4: refactor dx_root to support variable dirent sizes Artem Blagodarenko
2026-10-29 12:49   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 06/11] ext4: add ext4_dir_entry_len() Artem Blagodarenko
2026-10-29 12:50   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 07/11] ext4: rename ext4_dir_rec_len() and clarify dirdata usage Artem Blagodarenko
2026-10-29 12:50   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 08/11] ext4: dirdata feature Artem Blagodarenko
2026-10-29 12:51   ` sashiko-bot [this message]
2026-10-29 12:24 ` [PATCH v13 09/11] ext4: add dirdata LUFID support for directory entry rename Artem Blagodarenko
2026-10-29 12:47   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 10/11] ext4: add dirdata set/get helpers Artem Blagodarenko
2026-10-29 12:44   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 11/11] ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries Artem Blagodarenko
2026-10-29 12:58   ` sashiko-bot

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-189320@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=artem.blagodarenko@gmail.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