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 06/11] ext4: add ext4_dir_entry_len()
Date: Thu, 29 Oct 2026 12:50:37 +0000	[thread overview]
Message-ID: <sashiko-outbox-189317@kernel.org> (raw)
In-Reply-To: <20261029122413.230409-7-ablagodarenko@thelustrecollective.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] ext4: missing hash size in ext4_dir_rec_len() when dirdata is enabled
- [High] ext4: inconsistent dot_de offset in make_indexed_dir() and dx_get_dx_info()
- [High] ext4: insufficient space check in dx_get_dx_info() and dx_root_limit()
--

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

ext4: add ext4_dir_entry_len()

This commit updates ext4_dir_rec_len() to skip adding sizeof(struct
ext4_dir_entry_hash) when the dirdata feature is enabled, masks the filetype
in get_dtype(), and updates dx_root_limit() and its callers to compute the
root htree limit via dx_get_dx_info().

> diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
> index edb95cbc4439e..2d6d3be4ef7df 100644
> --- a/fs/ext4/ext4.h
> +++ b/fs/ext4/ext4.h
> @@ -2655,7 +2655,15 @@ static inline unsigned int ext4_dir_rec_len(__u8 name_len,
>  {
>  	int rec_len = (name_len + 8 + EXT4_DIR_ROUND);
>  
> -	if (dir && ext4_hash_in_dirent(dir))
> +	/*
> +	 * Without dirdata, the casefold+fscrypt hash lives at a fixed position
> +	 * after the filename and must be reserved explicitly.  With dirdata the
> +	 * hash is stored as a CFHASH extension and is already counted in the
> +	 * name_len argument (via ext4_dirent_get_data_len), so adding it again
> +	 * would double-count.
> +	 */
> +	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);
>  }

[Severity: High]
Can skipping sizeof(struct ext4_dir_entry_hash) whenever
ext4_has_feature_dirdata(dir->i_sb) is set cause out-of-bounds reads and
directory entry corruption on encrypted casefolded directories?

At this commit, callers such as ext4_find_dest_de(), ext4_insert_dentry(),
dx_move_dirents(), dx_pack_dirents(), and __ext4_check_dir_entry() still pass
de->name_len or fname_len(fname) directly to ext4_dir_rec_len() without
including ext4_dirent_get_data_len(). Even after later patches in the series
("ext4: rename ext4_dir_rec_len() and clarify dirdata usage", "ext4: dirdata
feature", and "ext4: add dirdata set/get helpers") convert some callers to
ext4_dir_entry_len(), several paths remain broken through the end of the
series:

1. In fs/ext4/namei.c:ext4_search_dir(), the bounds check before ext4_match()
still calls ext4_dir_rec_len() (renamed to ext4_dirent_rec_len()) directly on
de->name_len:

    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)) {

When dirdata and casefold+fscrypt are enabled on dir, ext4_dir_rec_len()
omits the 8-byte hash size. If an entry at the end of search_buf does not
have EXT4_DIRENT_CFHASH set in de->file_type, this bounds check passes when
only de->name_len fits before dlimit, and ext4_match() then falls back to
EXT4_DIRENT_HASH(de) and EXT4_DIRENT_MINOR_HASH(de), reading 8 bytes past
dlimit.

2. In fs/ext4/ext4.h:ext4_dir_entry_len():

    static inline unsigned int ext4_dir_entry_len(struct ext4_dir_entry_2 *de,
                                                  unsigned int blocksize,
                                                  const struct inode *dir)
    {
            unsigned int rec_len = ext4_rec_len_from_disk(de->rec_len, blocksize);
            unsigned int dirdata = ext4_dirent_get_data_len(de, rec_len);

            return ext4_dir_rec_len(de->name_len + dirdata, dir);
    }

If an entry in a casefolded+encrypted directory on a dirdata filesystem does
not have EXT4_DIRENT_CFHASH set in de->file_type (either a legacy un-migrated
entry or a corrupted on-disk entry), ext4_dirent_get_data_len() returns 0 and
ext4_dir_rec_len() also skips adding sizeof(struct ext4_dir_entry_hash).
Consequently, __ext4_check_dir_entry() accepts a trailing entry whose rec_len
only covers de->name_len, and the !EXT4_DIRENT_CFHASH fallback paths in
ext4_readdir(), htree_dirblock_to_tree(), dx_make_map(), and
ext4_inlinedir_to_tree() read EXT4_DIRENT_HASH(de) and
EXT4_DIRENT_MINOR_HASH(de) 8 bytes past the end of the block buffer or
inline directory slab buffer. For un-migrated entries, ext4_dir_entry_len()
also under-reports the entry length by 8 bytes, causing ext4_find_dest_de(),
ext4_insert_dentry(), dx_move_dirents(), and dx_pack_dirents() to overwrite
the legacy hash.

3. Callers passing literal 0 or 1 to compute minimum dirent size
(__ext4_check_dir_entry(), ext4_readdir(), and htree_dirblock_to_tree()) get
8 or 12 bytes instead of 16 or 20 bytes when dirdata is enabled on a
casefolded+encrypted directory.

4. Conversely, if !ext4_has_feature_dirdata(dir->i_sb) and
ext4_hash_in_dirent(dir) is true, an on-disk entry with EXT4_DIRENT_CFHASH
set in de->file_type has its extension counted by ext4_dirent_get_data_len()
and sizeof(struct ext4_dir_entry_hash) added a second time by
ext4_dir_rec_len().

Should the hash size inclusion depend on whether the hash is already accounted
for by EXT4_DIRENT_CFHASH (and whether the caller actually queried dirdata),
rather than solely on !ext4_has_feature_dirdata(dir->i_sb)?

[ ... ]

> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index 59cc01b91c418..b3acab0689c93 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c
> @@ -596,11 +596,17 @@ static inline void dx_set_limit(struct dx_entry *entries, unsigned value)
>  	((struct dx_countlimit *) entries)->limit = cpu_to_le16(value);
>  }
>  
> -static inline unsigned dx_root_limit(struct inode *dir, unsigned infosize)
> +static inline unsigned dx_root_limit(struct inode *dir,
> +	struct ext4_dir_entry_2 *dot_de)
>  {
> -	unsigned int entry_space = dir->i_sb->s_blocksize -
> -			ext4_dir_rec_len(1, NULL) -
> -			ext4_dir_rec_len(2, NULL) - infosize;
> +	struct dx_root_info *info;
> +	unsigned int entry_space;
> +
> +	info = dx_get_dx_info(dir, dot_de);
> +	if (IS_ERR(info))
> +		return 0;
> +	entry_space = dir->i_sb->s_blocksize - ((char *)info - (char *)dot_de) -
> +		info->info_length;
>  
>  	if (ext4_has_feature_metadata_csum(dir->i_sb))
>  		entry_space -= sizeof(struct dx_tail);
> @@ -885,10 +891,13 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,
>  
>  	entries = (struct dx_entry *)(((char *)info) + info->info_length);
>  
> -	if (dx_get_limit(entries) != dx_root_limit(dir, info->info_length)) {
> +	if (dx_get_limit(entries) !=
> +	    dx_root_limit(dir, (struct ext4_dir_entry_2 *)frame->bh->b_data)) {
>  		ext4_warning_inode(dir, "dx entry: limit %u != root limit %u",
>  				   dx_get_limit(entries),
> -				   dx_root_limit(dir, info->info_length));
> +				   dx_root_limit(dir,
> +				   (struct ext4_dir_entry_2 *)frame->bh->b_data
> +				   ));
>  		goto fail;
>  	}
>  
> @@ -2336,10 +2345,10 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
>  		dx_info->hash_version =
>  				EXT4_SB(dir->i_sb)->s_def_hash_version;
>  
> -	entries = (void *)dx_info + sizeof(*dx_info);
> +	entries = (void *)dx_info + dx_info->info_length;
>  	dx_set_block(entries, 1);
>  	dx_set_count(entries, 1);
> -	dx_set_limit(entries, dx_root_limit(dir, sizeof(*dx_info)));
> +	dx_set_limit(entries, dx_root_limit(dir, dot_de));

[Severity: High]
What happens in make_indexed_dir() when dot_de->rec_len does not equal
ext4_dir_entry_len(dot_de, blocksize, NULL)?

In fs/ext4/namei.c:make_indexed_dir(), dotdot_de is located using
dot_de->rec_len via ext4_next_entry(dot_de, blocksize), and left in place
while setting its new rec_len:

    dotdot_de = ext4_next_entry(dot_de, blocksize);
    if (!ext4_check_dx_root(dir, dot_de, dotdot_de, &de)) {
            brelse(bh);
            return -EFSCORRUPTED;
    }
    ...
    dotdot_de->rec_len =
            ext4_rec_len_to_disk(blocksize - le16_to_cpu(dot_de->rec_len),
                                 blocksize);

    /* initialize hashing info */
    dx_info = dx_get_dx_info(dir, dot_de);

However, dx_get_dx_info() (which is now also called by dx_root_limit()) steps
over dot_de using ext4_dir_entry_len(de_buf, blocksize, NULL) instead of
dot_de->rec_len:

    de_buf += ext4_dir_entry_len(de_buf, blocksize, NULL);

If dot_de->rec_len has padding (e.g. 16 or 24 bytes when '.' has no dirdata)
or is 8 bytes, ext4_check_dx_root() still succeeds because it does not verify
that dot_de->rec_len matches ext4_dir_entry_len(dot_de, blocksize, NULL).
dx_get_dx_info() then interprets bytes at dot_de + ext4_dir_entry_len(dot_de)
(inside dot_de's unzeroed slack space or misaligned with dotdot_de) as the
'..' entry, placing dx_info and entries[] at an offset that overwrites
dotdot_de and disagrees with get_dx_countlimit() (which steps over dot_de
using dirent->rec_len in ext4_dx_csum_set()).

In addition, does dx_get_dx_info() or make_indexed_dir() ensure enough space
remains in the root block for struct dx_tail and the second dx_entry created
by do_split()?

In dx_get_dx_info(), the bounds check only requires sizeof(struct
dx_root_info) + sizeof(struct dx_entry) (16 bytes) to remain in the block:

    if (de_buf < base || (char *)de_buf - (char *)base +
                          sizeof(struct dx_root_info) +
                          sizeof(struct dx_entry) > blocksize)
            return ERR_PTR(-EFSCORRUPTED);

When '.' and '..' carry large dirdata extensions (particularly once the next
commit widens ext4_dirent_rec_len()'s length parameter) such that only 16 to
23 bytes remain in the block after '..', dx_get_dx_info() succeeds, but:

1. In dx_root_limit(), subtracting sizeof(struct dx_tail) (8 bytes) when
metadata_csum is enabled leaves entry_space < sizeof(struct dx_entry), so
dx_root_limit() returns 0 (or if info->info_length is corrupted and large on
disk, subtracting info->info_length and sizeof(struct dx_tail) can underflow
unsigned int entry_space).

2. make_indexed_dir() does not check that dx_root_limit(dir, dot_de) >= 2.
When (char *)dx_info - (char *)dot_de == blocksize - 16, entries[0] is
written with count = 1 and limit = 0 (triggering EXT4_ERROR_INODE() in
ext4_handle_dirty_dx_node() -> ext4_dx_csum_set()), and then do_split() ->
dx_insert_block() writes entries[1] at bh->b_data + blocksize, 8 bytes past
the end of the buffer.

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

  reply	other threads:[~2026-10-29 12:50 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 [this message]
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
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-189317@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