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
next prev parent 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