* [PATCH v14 01/12] ext4: validate count against limit in ext4_dx_csum_verify/_set
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 13:56 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 02/12] ext4: use the e2fsck limit for htree index nodes Artem Blagodarenko
` (10 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4
Cc: tytso, jack, adilger.kernel, ablagodarenko, xiaowu.417,
Artem Blagodarenko
dx_countlimit's count field was read from disk and used directly to
compute the checksummed range (count_offset + count * sizeof(dx_entry))
without ever being checked against limit -- only limit itself was
bounds-checked against the block size. A corrupted or maliciously
crafted filesystem image that sets count to a large value (e.g. 65535)
makes ext4_chksum() read far past the end of the directory block
buffer, hitting adjacent slab objects.
Reported-by: xiaowu.417@qq.com
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
---
fs/ext4/namei.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 231a274b814e..76b9aa3fb03c 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -477,6 +477,10 @@ static int ext4_dx_csum_verify(struct inode *inode,
warn_no_space_for_csum(inode);
return 0;
}
+ if (count > limit) {
+ EXT4_ERROR_INODE(inode, "dir seems corrupt? Run e2fsck -D.");
+ return 0;
+ }
t = (struct dx_tail *)(((struct dx_entry *)c) + limit);
if (t->dt_checksum != ext4_dx_csum(inode, dirent, count_offset,
@@ -506,6 +510,10 @@ static void ext4_dx_csum_set(struct inode *inode, struct ext4_dir_entry *dirent)
warn_no_space_for_csum(inode);
return;
}
+ if (count > limit) {
+ EXT4_ERROR_INODE(inode, "dir seems corrupt? Run e2fsck -D.");
+ return;
+ }
t = (struct dx_tail *)(((struct dx_entry *)c) + limit);
t->dt_checksum = ext4_dx_csum(inode, dirent, count_offset, count, t);
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v14 02/12] ext4: use the e2fsck limit for htree index nodes
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
2026-11-03 13:49 ` [PATCH v14 01/12] ext4: validate count against limit in ext4_dx_csum_verify/_set Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 13:56 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 03/12] ext4: replace ext4_dir_entry with ext4_dir_entry_2 Artem Blagodarenko
` (9 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4; +Cc: tytso, jack, adilger.kernel, ablagodarenko, Artem Blagodarenko
From: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Since commit 471fbbea7ff7 ("ext4: handle casefolding with encryption"),
dx_node_limit() passes the directory to ext4_dir_rec_len(), which for a
casefolded+encrypted directory reserves 8 bytes for a hash in the fake
dirent at the start of an index node. That dirent never holds a name or
a hash, and e2fsck computes the limit without it. e2fsck also only
treats a block as an index node when its limit has that value.
As a result, on a casefolded+encrypted directory deep enough to have
index nodes, e2fsck reports the index nodes the kernel wrote as corrupt
leaf blocks ("directory corrupted"), and the kernel refuses the index
nodes that e2fsck -D wrote ("dx entry: limit 510 != node limit 509",
4k blocks with metadata_csum).
Compute the limit without the hash, as e2fsck does, and keep accepting
the old value so that directories written by older kernels can still be
read. Older kernels already reject index nodes written by e2fsck -D;
they will reject those written by this kernel in the same way.
Fixes: 471fbbea7ff7 ("ext4: handle casefolding with encryption")
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Assisted-by: Claude:claude-opus-5-5
---
fs/ext4/namei.c | 27 ++++++++++++++++++++++++---
1 file changed, 24 insertions(+), 3 deletions(-)
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 76b9aa3fb03c..3d69b6739cfe 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -593,16 +593,37 @@ static inline unsigned dx_root_limit(struct inode *dir, unsigned infosize)
return entry_space / sizeof(struct dx_entry);
}
-static inline unsigned dx_node_limit(struct inode *dir)
+static inline unsigned int __dx_node_limit(struct inode *dir,
+ const struct inode *hash_dir)
{
unsigned int entry_space = dir->i_sb->s_blocksize -
- ext4_dir_rec_len(0, dir);
+ ext4_dir_rec_len(0, hash_dir);
if (ext4_has_feature_metadata_csum(dir->i_sb))
entry_space -= sizeof(struct dx_tail);
return entry_space / sizeof(struct dx_entry);
}
+/*
+ * The fake dirent at the start of an index node never holds a name, so no
+ * room is reserved for a casefold+fscrypt hash in it. This is the limit
+ * e2fsck computes and expects.
+ */
+static inline unsigned dx_node_limit(struct inode *dir)
+{
+ return __dx_node_limit(dir, NULL);
+}
+
+/*
+ * Older kernels did reserve the hash in casefolded+encrypted directories,
+ * so their index nodes have a limit one smaller. Accept those too.
+ */
+static inline bool dx_node_limit_ok(struct inode *dir, unsigned int limit)
+{
+ return limit == dx_node_limit(dir) ||
+ limit == __dx_node_limit(dir, dir);
+}
+
/*
* Debug
*/
@@ -924,7 +945,7 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,
entries = ((struct dx_node *) frame->bh->b_data)->entries;
- if (dx_get_limit(entries) != dx_node_limit(dir)) {
+ if (!dx_node_limit_ok(dir, dx_get_limit(entries))) {
ext4_warning_inode(dir,
"dx entry: limit %u != node limit %u",
dx_get_limit(entries), dx_node_limit(dir));
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v14 02/12] ext4: use the e2fsck limit for htree index nodes
2026-11-03 13:49 ` [PATCH v14 02/12] ext4: use the e2fsck limit for htree index nodes Artem Blagodarenko
@ 2026-11-03 13:56 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-03 13:56 UTC (permalink / raw)
To: Artem Blagodarenko, linux-ext4; +Cc: tytso
> From: Artem Blagodarenko <artem.blagodarenko@gmail.com>
>
> Since commit 471fbbea7ff7 ("ext4: handle casefolding with encryption"),
> dx_node_limit() passes the directory to ext4_dir_rec_len(), which for a
> casefolded+encrypted directory reserves 8 bytes for a hash in the fake
> dirent at the start of an index node. That dirent never holds a name or
> a hash, and e2fsck computes the limit without it. e2fsck also only
> treats a block as an index node when its limit has that value.
>
> As a result, on a casefolded+encrypted directory deep enough to have
> index nodes, e2fsck reports the index nodes the kernel wrote as corrupt
> leaf blocks ("directory corrupted"), and the kernel refuses the index
> nodes that e2fsck -D wrote ("dx entry: limit 510 != node limit 509",
> 4k blocks with metadata_csum).
>
> Compute the limit without the hash, as e2fsck does, and keep accepting
> the old value so that directories written by older kernels can still be
> read. Older kernels already reject index nodes written by e2fsck -D;
> they will reject those written by this kernel in the same way.
>
> Fixes: 471fbbea7ff7 ("ext4: handle casefolding with encryption")
> Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103134921.610047-1-ablagodarenko@thelustrecollective.com?part=2
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v14 03/12] ext4: replace ext4_dir_entry with ext4_dir_entry_2
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
2026-11-03 13:49 ` [PATCH v14 01/12] ext4: validate count against limit in ext4_dx_csum_verify/_set Artem Blagodarenko
2026-11-03 13:49 ` [PATCH v14 02/12] ext4: use the e2fsck limit for htree index nodes Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 14:05 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 04/12] ext4: add ext4_dir_entry_is_tail() Artem Blagodarenko
` (8 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4
Cc: tytso, jack, adilger.kernel, ablagodarenko, Artem Blagodarenko,
Andreas Dilger
From: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Replace remaining uses of struct ext4_dir_entry in namei.c
with struct ext4_dir_entry_2.
The code paths affected by this change already depend on the
filetype feature, so using struct ext4_dir_entry_2 is
appropriate and avoids mixing the two directory entry types
unnecessarily.
This change does not affect support for 16-bit rec_len.
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Reviewed-by: Andreas Dilger <adilger@dilger.ca>
---
fs/ext4/ext4.h | 6 ++----
fs/ext4/namei.c | 38 +++++++++++++++++++-------------------
2 files changed, 21 insertions(+), 23 deletions(-)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 8d3af062a830..16df13874559 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -2507,10 +2507,8 @@ static inline int ext4_emergency_state(struct super_block *sb)
* Structure of a directory entry
*/
#define EXT4_NAME_LEN 255
-/*
- * Base length of the ext4 directory entry excluding the name length
- */
-#define EXT4_BASE_DIR_LEN (sizeof(struct ext4_dir_entry_2) - EXT4_NAME_LEN)
+/* offsetof avoids sizeof()+padding giving 9 instead of the correct on-disk 8 */
+#define EXT4_BASE_DIR_LEN offsetof(struct ext4_dir_entry_2, name)
struct ext4_dir_entry {
__le32 inode; /* Inode number */
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 3d69b6739cfe..dc8035707feb 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -102,7 +102,7 @@ static struct buffer_head *ext4_append(handle_t *handle,
}
static int ext4_dx_csum_verify(struct inode *inode,
- struct ext4_dir_entry *dirent);
+ struct ext4_dir_entry_2 *dirent);
/*
* Hints to ext4_read_dirblock regarding whether we expect a directory
@@ -128,7 +128,7 @@ static struct buffer_head *__ext4_read_dirblock(struct inode *inode,
unsigned int line)
{
struct buffer_head *bh;
- struct ext4_dir_entry *dirent;
+ struct ext4_dir_entry_2 *dirent;
int is_dx_block = 0;
if (block >= inode->i_size >> inode->i_blkbits) {
@@ -160,7 +160,7 @@ static struct buffer_head *__ext4_read_dirblock(struct inode *inode,
}
if (!bh)
return NULL;
- dirent = (struct ext4_dir_entry *) bh->b_data;
+ dirent = (struct ext4_dir_entry_2 *) bh->b_data;
/* Determine whether or not we have an index block */
if (is_dx(inode)) {
if (block == 0)
@@ -317,13 +317,13 @@ static struct ext4_dir_entry_tail *get_dirent_tail(struct inode *inode,
int blocksize = EXT4_BLOCK_SIZE(inode->i_sb);
#ifdef PARANOID
- struct ext4_dir_entry *d, *top;
+ struct ext4_dir_entry_2 *d, *top;
- d = (struct ext4_dir_entry *)bh->b_data;
- top = (struct ext4_dir_entry *)(bh->b_data +
+ d = (struct ext4_dir_entry_2 *)bh->b_data;
+ top = (struct ext4_dir_entry_2 *)(bh->b_data +
(blocksize - sizeof(struct ext4_dir_entry_tail)));
while (d < top && ext4_rec_len_from_disk(d->rec_len, blocksize))
- d = (struct ext4_dir_entry *)(((void *)d) +
+ d = (struct ext4_dir_entry_2 *)(((void *)d) +
ext4_rec_len_from_disk(d->rec_len, blocksize));
if (d != top)
@@ -410,22 +410,22 @@ int ext4_handle_dirty_dirblock(handle_t *handle,
}
static struct dx_countlimit *get_dx_countlimit(struct inode *inode,
- struct ext4_dir_entry *dirent,
+ struct ext4_dir_entry_2 *dirent,
int *offset)
{
- struct ext4_dir_entry *dp;
+ struct ext4_dir_entry_2 *de;
struct dx_root_info *root;
int count_offset;
int blocksize = EXT4_BLOCK_SIZE(inode->i_sb);
unsigned int rlen = ext4_rec_len_from_disk(dirent->rec_len, blocksize);
if (rlen == blocksize)
- count_offset = 8;
+ count_offset = sizeof(struct dx_node);
else if (rlen == 12) {
- dp = (struct ext4_dir_entry *)(((void *)dirent) + 12);
- if (ext4_rec_len_from_disk(dp->rec_len, blocksize) != blocksize - 12)
+ de = (struct ext4_dir_entry_2 *)(((void *)dirent) + 12);
+ if (ext4_rec_len_from_disk(de->rec_len, blocksize) != blocksize - 12)
return NULL;
- root = (struct dx_root_info *)(((void *)dp + 12));
+ root = (struct dx_root_info *)(((void *)de + 12));
if (root->reserved_zero ||
root->info_length != sizeof(struct dx_root_info))
return NULL;
@@ -438,7 +438,7 @@ static struct dx_countlimit *get_dx_countlimit(struct inode *inode,
return (struct dx_countlimit *)(((void *)dirent) + count_offset);
}
-static __le32 ext4_dx_csum(struct inode *inode, struct ext4_dir_entry *dirent,
+static __le32 ext4_dx_csum(struct inode *inode, struct ext4_dir_entry_2 *dirent,
int count_offset, int count, struct dx_tail *t)
{
struct ext4_inode_info *ei = EXT4_I(inode);
@@ -456,7 +456,7 @@ static __le32 ext4_dx_csum(struct inode *inode, struct ext4_dir_entry *dirent,
}
static int ext4_dx_csum_verify(struct inode *inode,
- struct ext4_dir_entry *dirent)
+ struct ext4_dir_entry_2 *dirent)
{
struct dx_countlimit *c;
struct dx_tail *t;
@@ -489,7 +489,7 @@ static int ext4_dx_csum_verify(struct inode *inode,
return 1;
}
-static void ext4_dx_csum_set(struct inode *inode, struct ext4_dir_entry *dirent)
+static void ext4_dx_csum_set(struct inode *inode, struct ext4_dir_entry_2 *dirent)
{
struct dx_countlimit *c;
struct dx_tail *t;
@@ -523,7 +523,7 @@ static inline int ext4_handle_dirty_dx_node(handle_t *handle,
struct inode *inode,
struct buffer_head *bh)
{
- ext4_dx_csum_set(inode, (struct ext4_dir_entry *)bh->b_data);
+ ext4_dx_csum_set(inode, (struct ext4_dir_entry_2 *)bh->b_data);
return ext4_handle_dirty_metadata(handle, inode, bh);
}
@@ -1519,7 +1519,7 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
}
static int is_dx_internal_node(struct inode *dir, ext4_lblk_t block,
- struct ext4_dir_entry *de)
+ struct ext4_dir_entry_2 *de)
{
struct super_block *sb = dir->i_sb;
@@ -1650,7 +1650,7 @@ static struct buffer_head *__ext4_find_entry(struct inode *dir,
}
if (!buffer_verified(bh) &&
!is_dx_internal_node(dir, block,
- (struct ext4_dir_entry *)bh->b_data) &&
+ (struct ext4_dir_entry_2 *)bh->b_data) &&
!ext4_dirblock_csum_verify(dir, bh)) {
EXT4_ERROR_INODE_ERR(dir, EFSBADCRC,
"checksumming directory "
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v14 04/12] ext4: add ext4_dir_entry_is_tail()
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
` (2 preceding siblings ...)
2026-11-03 13:49 ` [PATCH v14 03/12] ext4: replace ext4_dir_entry with ext4_dir_entry_2 Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 13:58 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 05/12] ext4: add dirdata format definitions and access helpers Artem Blagodarenko
` (7 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4
Cc: tytso, jack, adilger.kernel, ablagodarenko, Artem Blagodarenko,
Andreas Dilger
From: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Replace open-coded checks for directory tail entries with a call
to ext4_dir_entry_is_tail(). This helper will also be used by
upcoming changes.
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Reviewed-by: Andreas Dilger <adilger@dilger.ca>
---
fs/ext4/ext4.h | 16 ++++++++++++++++
fs/ext4/namei.c | 14 ++++++--------
2 files changed, 22 insertions(+), 8 deletions(-)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 16df13874559..41152914aa6c 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -4101,6 +4101,22 @@ static inline void ext4_clear_io_unwritten_flag(ext4_io_end_t *io_end)
io_end->flag &= ~EXT4_IO_END_UNWRITTEN;
}
+/*
+ * ext4_dir_entry_is_tail() - Check if a directory entry is a tail entry.
+ * @de: directory entry to check
+ *
+ * Returns true if @de is a directory block tail entry (checksum record).
+ */
+static inline bool ext4_dir_entry_is_tail(struct ext4_dir_entry_2 *de)
+{
+ struct ext4_dir_entry_tail *t = (struct ext4_dir_entry_tail *)de;
+
+ return !t->det_reserved_zero1 &&
+ le16_to_cpu(t->det_rec_len) == sizeof(*t) &&
+ !t->det_reserved_zero2 &&
+ t->det_reserved_ft == EXT4_FT_DIR_CSUM;
+}
+
extern const struct iomap_ops ext4_iomap_ops;
extern const struct iomap_ops ext4_iomap_report_ops;
extern const struct iomap_ops ext4_iomap_buffered_write_ops;
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index dc8035707feb..6e934e2732fe 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -331,14 +331,10 @@ static struct ext4_dir_entry_tail *get_dirent_tail(struct inode *inode,
t = (struct ext4_dir_entry_tail *)d;
#else
- t = EXT4_DIRENT_TAIL(bh->b_data, EXT4_BLOCK_SIZE(inode->i_sb));
+ t = EXT4_DIRENT_TAIL(bh->b_data, blocksize);
#endif
- if (t->det_reserved_zero1 ||
- (ext4_rec_len_from_disk(t->det_rec_len, blocksize) !=
- sizeof(struct ext4_dir_entry_tail)) ||
- t->det_reserved_zero2 ||
- t->det_reserved_ft != EXT4_FT_DIR_CSUM)
+ if (!ext4_dir_entry_is_tail((struct ext4_dir_entry_2 *)t))
return NULL;
return t;
@@ -478,7 +474,8 @@ static int ext4_dx_csum_verify(struct inode *inode,
return 0;
}
if (count > limit) {
- EXT4_ERROR_INODE(inode, "dir seems corrupt? Run e2fsck -D.");
+ EXT4_ERROR_INODE(inode, "dir seems corrupt (%i > %i)? "
+ "Run e2fsck -D.", count, limit);
return 0;
}
t = (struct dx_tail *)(((struct dx_entry *)c) + limit);
@@ -511,7 +508,8 @@ static void ext4_dx_csum_set(struct inode *inode, struct ext4_dir_entry_2 *diren
return;
}
if (count > limit) {
- EXT4_ERROR_INODE(inode, "dir seems corrupt? Run e2fsck -D.");
+ EXT4_ERROR_INODE(inode, "dir seems corrupt (%i > %i)? "
+ " Run e2fsck -D.", count, limit);
return;
}
t = (struct dx_tail *)(((struct dx_entry *)c) + limit);
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v14 04/12] ext4: add ext4_dir_entry_is_tail()
2026-11-03 13:49 ` [PATCH v14 04/12] ext4: add ext4_dir_entry_is_tail() Artem Blagodarenko
@ 2026-11-03 13:58 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-03 13:58 UTC (permalink / raw)
To: Artem Blagodarenko, linux-ext4; +Cc: tytso
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] ext4: unrelated error message changes in ext4_dx_csum_verify() and _set()
--
commit b38526265cad346b429002b55eb969cb4c97bc1c
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>
ext4: add ext4_dir_entry_is_tail()
This commit adds the ext4_dir_entry_is_tail() helper and replaces the
open-coded check in get_dirent_tail() with a call to it.
> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index dc8035707feb6..6e934e2732fe9 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c
[ ... ]
> @@ -478,7 +474,8 @@ static int ext4_dx_csum_verify(struct inode *inode,
> return 0;
> }
> if (count > limit) {
> - EXT4_ERROR_INODE(inode, "dir seems corrupt? Run e2fsck -D.");
> + EXT4_ERROR_INODE(inode, "dir seems corrupt (%i > %i)? "
> + "Run e2fsck -D.", count, limit);
> return 0;
> }
> t = (struct dx_tail *)(((struct dx_entry *)c) + limit);
> @@ -511,7 +508,8 @@ static void ext4_dx_csum_set(struct inode *inode, struct ext4_dir_entry_2 *diren
> return;
> }
> if (count > limit) {
> - EXT4_ERROR_INODE(inode, "dir seems corrupt? Run e2fsck -D.");
> + EXT4_ERROR_INODE(inode, "dir seems corrupt (%i > %i)? "
> + " Run e2fsck -D.", count, limit);
> return;
> }
[Severity: Low]
This isn't a bug, but were these error message updates in
ext4_dx_csum_verify() and ext4_dx_csum_set() meant to be folded into
commit 70f1e1d58c97 ("ext4: validate count against limit in
ext4_dx_csum_verify/_set") instead of included in this patch without mention
in the commit message?
Also, the continuation line in ext4_dx_csum_set() has an extra leading space
compared to ext4_dx_csum_verify(), resulting in two spaces before
"Run e2fsck -D.".
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103134921.610047-1-ablagodarenko@thelustrecollective.com?part=4
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v14 05/12] ext4: add dirdata format definitions and access helpers
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
` (3 preceding siblings ...)
2026-11-03 13:49 ` [PATCH v14 04/12] ext4: add ext4_dir_entry_is_tail() Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 13:56 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 06/12] ext4: refactor dx_root to support variable dirent sizes Artem Blagodarenko
` (6 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4
Cc: tytso, jack, adilger.kernel, ablagodarenko, Pravin Shelar,
Artem Blagodarenko
Define the on-disk format for ext4 directory entry extension data.
The upper four bits of de->file_type indicate the presence of
optional data stored after the filename NUL terminator. This patch
defines flags for LUFID, 64-bit inode numbers, and casefold hash
data stored in that area.
Add struct ext4_dirent_data_header to describe variable-length
extension records and struct ext4_dirent_hash for hash storage used
by casefold and fscrypt.
Provide ext4_dirdata_next() to advance to the next extension record
and ext4_dirent_get_data_len() to compute the total extension data
length associated with a directory entry.
No functional changes.
Signed-off-by: Pravin Shelar <pravin.shelar@sun.com>
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Assisted-by: Claude:claude-opus-5-5
---
fs/ext4/ext4.h | 123 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 123 insertions(+)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 41152914aa6c..d224132fc80b 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -2589,6 +2589,55 @@ struct ext4_dir_entry_tail {
#define EXT4_FT_SYMLINK 7
#define EXT4_FT_MAX 8
+#define EXT4_FT_MASK 0xf
+
+#if EXT4_FT_MAX > EXT4_FT_MASK
+#error "conflicting EXT4_FT_MAX and EXT4_FT_MASK"
+#endif
+
+/*
+ * d_type has 4 unused bits, so it can hold four types of data. These different
+ * types of data (e.g. fscypt hash, high 32 bits of 64-bit inode number) can be
+ * stored, in flag order, after file-name in ext4 dirent.
+ *
+ * These flags are added to d_type if ext4 dirent has extra data after
+ * filename. This data length is variable and length is stored in first byte
+ * of data. Data starts after filename NUL byte.
+ */
+#define EXT4_DIRENT_LUFID 0x10
+#define EXT4_DIRENT_INO64 0x20
+#define EXT4_DIRENT_CFHASH 0x40
+
+struct ext4_fid {
+ char fid[16]; /* 128-bit unique file identifier */
+};
+
+struct ext4_dirent_data_header {
+ /* length of this header + the whole data blob */
+ __u8 ddh_length;
+} __packed;
+
+struct ext4_dirent_fid {
+ struct ext4_dirent_data_header df_header;
+ struct ext4_fid df_fid[];
+};
+
+#define EXT4_LUFID_MAGIC 0xAD200907UL
+struct ext4_dentry_param {
+ __u32 edp_magic; /* EXT4_LUFID_MAGIC */
+ __u8 edp_dfid[]; /* struct ext4_dirent_fid payload */
+};
+
+/*
+ * The hash words are declared as scalars rather than as a nested
+ * struct ext4_dir_entry_hash: a 4-byte aligned struct inside a packed
+ * struct makes clang warn about unaligned access (-Wunaligned-access).
+ */
+struct ext4_dirent_hash {
+ struct ext4_dirent_data_header dh_header;
+ __le32 dh_hash;
+ __le32 dh_minor_hash;
+} __packed;
#define EXT4_FT_DIR_CSUM 0xDE
@@ -4117,6 +4166,80 @@ static inline bool ext4_dir_entry_is_tail(struct ext4_dir_entry_2 *de)
t->det_reserved_ft == EXT4_FT_DIR_CSUM;
}
+/*
+ * Advance to the next dirdata record header starting from @ddh.
+ *
+ * Uses READ_ONCE() on ddh_length to prevent the compiler from re-fetching this
+ * on-disk field and to guard against a concurrent write racing with any bounds
+ * check the caller performs on the returned pointer.
+ */
+static inline struct ext4_dirent_data_header *
+ext4_dirdata_next(struct ext4_dirent_data_header *ddh)
+{
+ return (struct ext4_dirent_data_header *)
+ ((char *)ddh + READ_ONCE(ddh->ddh_length));
+}
+
+/*
+ * ext4_dirent_get_data_len() - Compute the total dirdata length for an entry.
+ * @de: directory entry
+ * @rec_len: the record length of the directory entry (decoded)
+ *
+ * Computes the length of optional data stored after the filename (and its
+ * implicit NUL terminator). Each extension is indicated by a bit in the
+ * high 4 bits of de->file_type; the first byte of each extension is its
+ * length (including that length byte itself).
+ *
+ * Returns 0 for tail entries and for entries with no dirdata.
+ */
+static inline int ext4_dirent_get_data_len(struct ext4_dir_entry_2 *de,
+ unsigned int rec_len)
+{
+ __u8 extra_data_flags;
+ struct ext4_dirent_data_header *ddh;
+ int dlen = 0;
+ unsigned int offset;
+
+ if (ext4_dir_entry_is_tail(de))
+ return 0;
+
+ extra_data_flags = (de->file_type & ~EXT4_FT_MASK) >> 4;
+ /* offset from start of entry to after filename + NUL */
+ offset = EXT4_BASE_DIR_LEN + de->name_len + 1;
+
+ /* bounds check: ensure we start reading within the entry */
+ if (offset >= rec_len)
+ return 0;
+
+ ddh = (struct ext4_dirent_data_header *)((char *)de + offset);
+
+ while (extra_data_flags) {
+ if (extra_data_flags & 1) {
+ /* bounds check before reading ddh_length */
+ if (offset + sizeof(*ddh) > rec_len)
+ return 0;
+
+ /* Use READ_ONCE to prevent the compiler from re-fetching
+ * this disk field and to guard against a concurrent
+ * write racing with our bounds check below (TOCTOU). */
+ {
+ u8 ddh_len = READ_ONCE(ddh->ddh_length);
+
+ if (ddh_len == 0 || ddh_len > rec_len - offset)
+ return 0;
+ dlen += ddh_len + (dlen == 0);
+ offset += ddh_len;
+ /* advance using the already-validated local copy
+ * to avoid re-fetching ddh->ddh_length */
+ ddh = (struct ext4_dirent_data_header *)
+ ((char *)ddh + ddh_len);
+ }
+ }
+ extra_data_flags >>= 1;
+ }
+ return dlen;
+}
+
extern const struct iomap_ops ext4_iomap_ops;
extern const struct iomap_ops ext4_iomap_report_ops;
extern const struct iomap_ops ext4_iomap_buffered_write_ops;
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v14 06/12] ext4: refactor dx_root to support variable dirent sizes
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
` (4 preceding siblings ...)
2026-11-03 13:49 ` [PATCH v14 05/12] ext4: add dirdata format definitions and access helpers Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 14:18 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 07/12] ext4: add ext4_dir_entry_len() Artem Blagodarenko
` (5 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4
Cc: tytso, jack, adilger.kernel, ablagodarenko, Artem Blagodarenko,
Pravin Shelar
From: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Split monolithic definition of dx_root struct to separate dx_root_info
from fake struct ext4_dir_entry2 for improved code readability.
This allows "." and ".." dirents to have different sizes if necessary,
since we can't assume the rec_len 12 if dx_root dirents have dirdata.
Adds dx_get_dx_info() accessor instead of complex typecast at callers.
Also add the ext4_dir_entry_len() helper, which dx_get_dx_info() uses
to step over the ".." entry now that it may carry dirdata.
dx_get_dx_info() finds ".." through the rec_len of ".", as the other
users of the root block do, and checks that the root has room for
dx_root_info, two dx_entry slots and the dx_tail. make_indexed_dir()
calls it before it changes the directory, so a failure leaves the
directory as it was.
Signed-off-by: Pravin Shelar <pravin.shelar@sun.com>
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Assisted-by: Claude:claude-opus-5-5
---
fs/ext4/ext4.h | 31 +++++++
fs/ext4/namei.c | 224 ++++++++++++++++++++++++++++++------------------
2 files changed, 173 insertions(+), 82 deletions(-)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index d224132fc80b..cebbfac05fa5 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -4240,6 +4240,37 @@ static inline int ext4_dirent_get_data_len(struct ext4_dir_entry_2 *de,
return dlen;
}
+/*
+ * ext4_dir_entry_len() - Compute the required rec_len for a directory entry.
+ * @de: directory entry (used to read name_len and any dirdata length)
+ * @blocksize: size of the buffer @de lives in (the real directory block
+ * size, or the smaller inline-data buffer size for inline
+ * directories) -- used only to decode @de->rec_len's "0/65535
+ * means rest of buffer" sentinel correctly.
+ * @dir: directory inode (may be NULL for '.' and '..' entries, which
+ * never carry the casefold+fscrypt hash regardless of the
+ * directory's feature flags)
+ *
+ * Returns the minimum record length needed to hold @de, rounded up to the
+ * directory alignment and including room for the casefold+fscrypt hash if
+ * the directory requires it. An entry that carries the hash as a CFHASH
+ * record has it counted in its dirdata length. Any other entry in such a
+ * directory may hold the hash at the fixed offset after the name, which is
+ * where readers look when there is no valid CFHASH record, so the result
+ * always covers that too.
+ */
+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 max(ext4_dir_rec_len(de->name_len + dirdata, NULL),
+ ext4_dir_rec_len(de->name_len, dir));
+}
+
+
extern const struct iomap_ops ext4_iomap_ops;
extern const struct iomap_ops ext4_iomap_report_ops;
extern const struct iomap_ops ext4_iomap_buffered_write_ops;
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 6e934e2732fe..c221bcd562cc 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -244,22 +244,13 @@ struct dx_entry
* hash version mod 4 should never be 0. Sincerely, the paranoia department.
*/
-struct dx_root
+struct dx_root_info
{
- struct fake_dirent dot;
- char dot_name[4];
- struct fake_dirent dotdot;
- char dotdot_name[4];
- struct dx_root_info
- {
- __le32 reserved_zero;
- u8 hash_version;
- u8 info_length; /* 8 */
- u8 indirect_levels;
- u8 unused_flags;
- }
- info;
- struct dx_entry entries[];
+ __le32 reserved_zero;
+ u8 hash_version;
+ u8 info_length; /* 8 */
+ u8 indirect_levels;
+ u8 unused_flags;
};
struct dx_node
@@ -535,6 +526,46 @@ ext4_next_entry(struct ext4_dir_entry_2 *p, unsigned long blocksize)
ext4_rec_len_from_disk(p->rec_len, blocksize));
}
+/*
+ * dx_get_dx_info() - locate the dx_root_info in an htree root block
+ * @dir: directory inode
+ * @de_buf: start of the root block (the '.' entry)
+ *
+ * '..' is found through the rec_len of '.', as every other user of the
+ * root block does. The rec_len of '..' covers the rest of the block, so
+ * dx_root_info starts at the real length of '..'. '.' and '..' never carry
+ * the casefold+fscrypt hash, so their length is computed with a NULL @dir.
+ * The root must have room for dx_root_info, two dx_entry slots (the first
+ * split needs them) and, with metadata_csum, the dx_tail.
+ */
+static struct dx_root_info *dx_get_dx_info(struct inode *dir, void *de_buf)
+{
+ unsigned int blocksize = dir->i_sb->s_blocksize;
+ struct ext4_dir_entry_2 *de = de_buf;
+ unsigned int off, rlen, need;
+
+ rlen = ext4_rec_len_from_disk(de->rec_len, blocksize);
+ if (rlen < EXT4_BASE_DIR_LEN || rlen + EXT4_BASE_DIR_LEN > blocksize ||
+ ext4_dir_entry_len(de, blocksize, NULL) > rlen)
+ return ERR_PTR(-EFSCORRUPTED);
+ off = rlen;
+
+ de = (struct ext4_dir_entry_2 *)((char *)de_buf + off);
+ rlen = ext4_rec_len_from_disk(de->rec_len, blocksize);
+ if (rlen < EXT4_BASE_DIR_LEN || off + rlen > blocksize ||
+ ext4_dir_entry_len(de, blocksize, NULL) > rlen)
+ return ERR_PTR(-EFSCORRUPTED);
+ off += ext4_dir_entry_len(de, blocksize, NULL);
+
+ need = sizeof(struct dx_root_info) + 2 * sizeof(struct dx_entry);
+ if (ext4_has_feature_metadata_csum(dir->i_sb))
+ need += sizeof(struct dx_tail);
+ if (off + need > blocksize)
+ return ERR_PTR(-EFSCORRUPTED);
+
+ return (struct dx_root_info *)((char *)de_buf + off);
+}
+
/*
* Future: use high four bits of block for coalesce-on-delete flags
* Mask them off for now.
@@ -807,7 +838,7 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,
{
unsigned count, indirect, level, i;
struct dx_entry *at, *entries, *p, *q, *m;
- struct dx_root *root;
+ struct dx_root_info *info;
struct dx_frame *frame = frame_in;
struct dx_frame *ret_err = ERR_PTR(ERR_BAD_DX_DIR);
u32 hash;
@@ -819,23 +850,31 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,
if (IS_ERR(frame->bh))
return (struct dx_frame *) frame->bh;
- root = (struct dx_root *) frame->bh->b_data;
- if (root->info.hash_version != DX_HASH_TEA &&
- root->info.hash_version != DX_HASH_HALF_MD4 &&
- root->info.hash_version != DX_HASH_LEGACY &&
- root->info.hash_version != DX_HASH_SIPHASH) {
- ext4_warning_inode(dir, "Unrecognised inode hash code %u",
- root->info.hash_version);
+ info = dx_get_dx_info(dir, (struct ext4_dir_entry_2 *)frame->bh->b_data);
+ if (IS_ERR(info))
+ goto fail;
+ if (info->info_length != sizeof(struct dx_root_info)) {
+ ext4_warning_inode(dir, "corrupted htree: info_length %u",
+ info->info_length);
+ goto fail;
+ }
+ if (info->hash_version != DX_HASH_TEA &&
+ info->hash_version != DX_HASH_HALF_MD4 &&
+ info->hash_version != DX_HASH_LEGACY &&
+ info->hash_version != DX_HASH_SIPHASH) {
+ ext4_warning(dir->i_sb,
+ "Unrecognised inode hash code %d for directory #%llu",
+ info->hash_version, dir->i_ino);
goto fail;
}
if (ext4_hash_in_dirent(dir)) {
- if (root->info.hash_version != DX_HASH_SIPHASH) {
+ if (info->hash_version != DX_HASH_SIPHASH) {
ext4_warning_inode(dir,
"Hash in dirent, but hash is not SIPHASH");
goto fail;
}
} else {
- if (root->info.hash_version == DX_HASH_SIPHASH) {
+ if (info->hash_version == DX_HASH_SIPHASH) {
ext4_warning_inode(dir,
"Hash code is SIPHASH, but hash not in dirent");
goto fail;
@@ -843,7 +882,7 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,
}
if (fname)
hinfo = &fname->hinfo;
- hinfo->hash_version = root->info.hash_version;
+ hinfo->hash_version = info->hash_version;
if (hinfo->hash_version <= DX_HASH_TEA)
hinfo->hash_version += EXT4_SB(dir->i_sb)->s_hash_unsigned;
hinfo->seed = EXT4_SB(dir->i_sb)->s_hash_seed;
@@ -859,13 +898,13 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,
}
hash = hinfo->hash;
- if (root->info.unused_flags & 1) {
+ if (info->unused_flags & 1) {
ext4_warning_inode(dir, "Unimplemented hash flags: %#06x",
- root->info.unused_flags);
+ info->unused_flags);
goto fail;
}
- indirect = root->info.indirect_levels;
+ indirect = info->indirect_levels;
if (indirect >= ext4_dir_htree_level(dir->i_sb)) {
ext4_warning(dir->i_sb,
"Directory (ino: %llu) htree depth %#06x exceed"
@@ -878,14 +917,12 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,
goto fail;
}
- entries = (struct dx_entry *)(((char *)&root->info) +
- root->info.info_length);
+ entries = (struct dx_entry *)(((char *)info) + info->info_length);
- if (dx_get_limit(entries) != dx_root_limit(dir,
- root->info.info_length)) {
+ if (dx_get_limit(entries) != dx_root_limit(dir, info->info_length)) {
ext4_warning_inode(dir, "dx entry: limit %u != root limit %u",
dx_get_limit(entries),
- dx_root_limit(dir, root->info.info_length));
+ dx_root_limit(dir, info->info_length));
goto fail;
}
@@ -962,7 +999,7 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,
return ret_err;
}
-static void dx_release(struct dx_frame *frames)
+static void dx_release(struct inode *dir, struct dx_frame *frames)
{
struct dx_root_info *info;
int i;
@@ -971,7 +1008,18 @@ static void dx_release(struct dx_frame *frames)
if (frames[0].bh == NULL)
return;
- info = &((struct dx_root *)frames[0].bh->b_data)->info;
+ info = dx_get_dx_info(dir, (struct ext4_dir_entry_2 *)frames[0].bh->b_data);
+ if (IS_ERR(info)) {
+ /* Cannot determine indirect_levels; release all frame buffers
+ * that may have been loaded to avoid leaking them. */
+ for (i = 0; i < EXT4_HTREE_LEVEL; i++) {
+ if (!frames[i].bh)
+ break;
+ brelse(frames[i].bh);
+ frames[i].bh = NULL;
+ }
+ return;
+ }
/* save local copy, "info" may be freed after brelse() */
indirect_levels = info->indirect_levels;
for (i = 0; i <= indirect_levels; i++) {
@@ -1277,12 +1325,12 @@ int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,
(count && ((hashval & 1) == 0)))
break;
}
- dx_release(frames);
+ dx_release(dir, frames);
dxtrace(printk(KERN_DEBUG "Fill tree: returned %d entries, "
"next hash: %x\n", count, *next_hash));
return count;
errout:
- dx_release(frames);
+ dx_release(dir, frames);
return (err);
}
@@ -1782,7 +1830,7 @@ static struct buffer_head * ext4_dx_find_entry(struct inode *dir,
errout:
dxtrace(printk(KERN_DEBUG "%s not found\n", fname->usr_fname->name));
success:
- dx_release(frames);
+ dx_release(dir, frames);
return bh;
}
@@ -2185,44 +2233,38 @@ static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,
return err ? err : err2;
}
-static bool ext4_check_dx_root(struct inode *dir, struct dx_root *root)
+static bool ext4_check_dx_root(struct inode *dir,
+ struct ext4_dir_entry_2 *dot_de,
+ struct ext4_dir_entry_2 *dotdot_de,
+ struct ext4_dir_entry_2 **entry)
{
- struct fake_dirent *fde;
const char *error_msg;
- unsigned int rlen;
unsigned int blocksize = dir->i_sb->s_blocksize;
- char *blockend = (char *)root + dir->i_sb->s_blocksize;
+ struct ext4_dir_entry_2 *de = NULL;
- fde = &root->dot;
- if (unlikely(fde->name_len != 1)) {
+ if (unlikely(dot_de->name_len != 1)) {
error_msg = "invalid name_len for '.'";
goto corrupted;
}
- if (unlikely(strncmp(root->dot_name, ".", fde->name_len))) {
+ if (unlikely(strncmp(dot_de->name, ".", dot_de->name_len))) {
error_msg = "invalid name for '.'";
goto corrupted;
}
- rlen = ext4_rec_len_from_disk(fde->rec_len, blocksize);
- if (unlikely((char *)fde + rlen >= blockend)) {
- error_msg = "invalid rec_len for '.'";
- goto corrupted;
- }
- fde = &root->dotdot;
- if (unlikely(fde->name_len != 2)) {
+ if (unlikely(dotdot_de->name_len != 2)) {
error_msg = "invalid name_len for '..'";
goto corrupted;
}
- if (unlikely(strncmp(root->dotdot_name, "..", fde->name_len))) {
+ if (unlikely(strncmp(dotdot_de->name, "..", dotdot_de->name_len))) {
error_msg = "invalid name for '..'";
goto corrupted;
}
- rlen = ext4_rec_len_from_disk(fde->rec_len, blocksize);
- if (unlikely((char *)fde + rlen >= blockend)) {
+ de = ext4_next_entry(dotdot_de, blocksize);
+ if ((char *)de >= (((char *)dot_de) + blocksize)) {
error_msg = "invalid rec_len for '..'";
goto corrupted;
}
-
+ *entry = de;
return true;
corrupted:
@@ -2240,16 +2282,15 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
struct inode *inode, struct buffer_head *bh)
{
struct buffer_head *bh2;
- struct dx_root *root;
struct dx_frame frames[EXT4_HTREE_LEVEL], *frame;
struct dx_entry *entries;
- struct ext4_dir_entry_2 *de, *de2;
+ struct ext4_dir_entry_2 *de, *de2, *dot_de, *dotdot_de;
char *data2, *top;
unsigned len;
int retval;
unsigned blocksize;
ext4_lblk_t block;
- struct fake_dirent *fde;
+ struct dx_root_info *dx_info;
int csum_size = 0;
if (ext4_has_feature_metadata_csum(inode->i_sb))
@@ -2266,17 +2307,28 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
return retval;
}
- root = (struct dx_root *) bh->b_data;
- if (!ext4_check_dx_root(dir, root)) {
+ dot_de = (struct ext4_dir_entry_2 *)bh->b_data;
+ if (ext4_rec_len_from_disk(dot_de->rec_len, blocksize) < EXT4_BASE_DIR_LEN ||
+ ext4_rec_len_from_disk(dot_de->rec_len, blocksize) >= blocksize - EXT4_BASE_DIR_LEN) {
brelse(bh);
return -EFSCORRUPTED;
}
+ dotdot_de = ext4_next_entry(dot_de, blocksize);
+ if (!ext4_check_dx_root(dir, dot_de, dotdot_de, &de)) {
+ brelse(bh);
+ return -EFSCORRUPTED;
+ }
+
+ /* Find room for the hashing info before the directory is changed */
+ dx_info = dx_get_dx_info(dir, dot_de);
+ if (IS_ERR(dx_info)) {
+ EXT4_ERROR_INODE(dir, "no room for htree root info");
+ brelse(bh);
+ return PTR_ERR(dx_info);
+ }
/* The 0th block becomes the root, move the dirents out */
- fde = &root->dotdot;
- de = (struct ext4_dir_entry_2 *)((char *)fde +
- ext4_rec_len_from_disk(fde->rec_len, blocksize));
- len = ((char *) root) + (blocksize - csum_size) - (char *) de;
+ len = ((char *)dot_de) + (blocksize - csum_size) - (char *)de;
/* Allocate new block for the 0th block's dirents */
bh2 = ext4_append(handle, dir, &block);
@@ -2307,24 +2359,26 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
ext4_initialize_dirent_tail(bh2, blocksize);
/* Initialize the root; the dot dirents already exist */
- de = (struct ext4_dir_entry_2 *) (&root->dotdot);
- de->rec_len = ext4_rec_len_to_disk(
- blocksize - ext4_dir_rec_len(2, NULL), blocksize);
- memset (&root->info, 0, sizeof(root->info));
- root->info.info_length = sizeof(root->info);
+ dotdot_de->rec_len =
+ ext4_rec_len_to_disk(blocksize - le16_to_cpu(dot_de->rec_len),
+ blocksize);
+
+ /* initialize hashing info */
+ memset(dx_info, 0, sizeof(*dx_info));
+ dx_info->info_length = sizeof(*dx_info);
if (ext4_hash_in_dirent(dir))
- root->info.hash_version = DX_HASH_SIPHASH;
+ dx_info->hash_version = DX_HASH_SIPHASH;
else
- root->info.hash_version =
+ dx_info->hash_version =
EXT4_SB(dir->i_sb)->s_def_hash_version;
- entries = root->entries;
+ entries = (void *)dx_info + sizeof(*dx_info);
dx_set_block(entries, 1);
dx_set_count(entries, 1);
- dx_set_limit(entries, dx_root_limit(dir, sizeof(root->info)));
+ dx_set_limit(entries, dx_root_limit(dir, sizeof(*dx_info)));
/* Initialize as for dx_probe */
- fname->hinfo.hash_version = root->info.hash_version;
+ fname->hinfo.hash_version = dx_info->hash_version;
if (fname->hinfo.hash_version <= DX_HASH_TEA)
fname->hinfo.hash_version += EXT4_SB(dir->i_sb)->s_hash_unsigned;
fname->hinfo.seed = EXT4_SB(dir->i_sb)->s_hash_seed;
@@ -2367,7 +2421,7 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
*/
if (retval)
ext4_mark_inode_dirty(handle, dir);
- dx_release(frames);
+ dx_release(dir, frames);
brelse(bh2);
return retval;
}
@@ -2634,7 +2688,7 @@ static int ext4_dx_add_entry(handle_t *handle, struct ext4_filename *fname,
if (restart || err)
goto journal_error;
} else {
- struct dx_root *dxroot;
+ struct dx_root_info *info;
memcpy((char *) entries2, (char *) entries,
icount * sizeof(struct dx_entry));
dx_set_limit(entries2, dx_node_limit(dir));
@@ -2642,11 +2696,17 @@ static int ext4_dx_add_entry(handle_t *handle, struct ext4_filename *fname,
/* Set up root */
dx_set_count(entries, 1);
dx_set_block(entries + 0, newblock);
- dxroot = (struct dx_root *)frames[0].bh->b_data;
- dxroot->info.indirect_levels += 1;
+ info = dx_get_dx_info(dir, (struct ext4_dir_entry_2 *)
+ frames[0].bh->b_data);
+ if (IS_ERR(info)) {
+ err = PTR_ERR(info);
+ brelse(bh2);
+ goto journal_error;
+ }
+ info->indirect_levels += 1;
dxtrace(printk(KERN_DEBUG
"Creating %d level index...\n",
- dxroot->info.indirect_levels));
+ info->indirect_levels));
err = ext4_handle_dirty_dx_node(handle, dir, frame->bh);
if (err) {
brelse(bh2);
@@ -2670,7 +2730,7 @@ static int ext4_dx_add_entry(handle_t *handle, struct ext4_filename *fname,
ext4_std_error(dir->i_sb, err); /* this is a no-op if err == 0 */
cleanup:
brelse(bh);
- dx_release(frames);
+ dx_release(dir, frames);
/* @restart is true means htree-path has been changed, we need to
* repeat dx_probe() to find out valid htree-path
*/
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v14 06/12] ext4: refactor dx_root to support variable dirent sizes
2026-11-03 13:49 ` [PATCH v14 06/12] ext4: refactor dx_root to support variable dirent sizes Artem Blagodarenko
@ 2026-11-03 14:18 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-03 14:18 UTC (permalink / raw)
To: Artem Blagodarenko, linux-ext4; +Cc: tytso
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] ext4: dx_get_dx_info() offset shifts after dotdot rec_len update
--
commit 73068d5241d832480befa7394ccb6a8830dc1e0f
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>
ext4: refactor dx_root to support variable dirent sizes
This commit splits struct dx_root_info out of struct dx_root and introduces
ext4_dir_entry_len() and dx_get_dx_info() so that the htree root header can
follow variable-sized "." and ".." entries carrying dirdata.
[Severity: High]
In fs/ext4/namei.c, can the offset returned by dx_get_dx_info() shift after
make_indexed_dir() expands dotdot_de->rec_len to cover the rest of the block?
When __ext4_add_entry() calls make_indexed_dir() on a full one-block
unindexed directory, make_indexed_dir() calls dx_get_dx_info(dir, dot_de) at
line 2323 while dotdot_de->rec_len still holds its small pre-conversion
length (for example, 12 bytes).
Inside dx_get_dx_info() at line 556, the end of the ".." entry is computed by
calling ext4_dir_entry_len(de, blocksize, NULL), which in turn calls
ext4_dirent_get_data_len(de, rec_len) in fs/ext4/ext4.h. In
ext4_dirent_get_data_len(), the dirdata header for ".." starts at offset 11
(EXT4_BASE_DIR_LEN + 2 + 1). If dotdot_de->file_type has a dirdata flag bit
set in ~EXT4_FT_MASK and byte 11 of dotdot_de (ddh_length) is 2 or greater,
the check ddh_len > rec_len - offset evaluates to true while rec_len is 12,
causing ext4_dirent_get_data_len() to return 0 instead of an error.
As a result, dx_get_dx_info() treats dotdot_de as a 12-byte entry with no
dirdata and places dx_info immediately after it (for example, at offset 24).
make_indexed_dir() then wipes the block starting from offset 24, leaving
dotdot_de intact, and at line 2362 updates dotdot_de->rec_len to cover the
remainder of the block (for example, 4084 bytes) before writing dx_info at
offset 24 and the root dx_entry array at offset 32.
Once dotdot_de->rec_len is expanded to 4084, the bounds check ddh_len <=
rec_len - 11 in ext4_dirent_get_data_len() now succeeds, so subsequent calls
to ext4_dir_entry_len() and dx_get_dx_info() (such as in dx_release() at line
2424 and dx_probe() at line 853, as well as dx_root_limit() and
get_dx_countlimit() in later patches of this series) see a non-zero dirdata
length and compute a shifted dx_root_info offset pointing into the dx_entry
array or fail with -EFSCORRUPTED.
Should make_indexed_dir() or ext4_check_dx_root() validate or clear any
inconsistent dirdata flags on "." and ".." before expanding dotdot_de->rec_len
so that dx_get_dx_info() computes the same offset before and after conversion?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103134921.610047-1-ablagodarenko@thelustrecollective.com?part=6
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v14 07/12] ext4: add ext4_dir_entry_len()
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
` (5 preceding siblings ...)
2026-11-03 13:49 ` [PATCH v14 06/12] ext4: refactor dx_root to support variable dirent sizes Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 14:08 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 08/12] ext4: rename ext4_dir_rec_len() and clarify dirdata usage Artem Blagodarenko
` (4 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4; +Cc: tytso, jack, adilger.kernel, ablagodarenko, Artem Blagodarenko
From: Artem Blagodarenko <artem.blagodarenko@gmail.com>
ext4_dir_entry_len() (added in the previous patch, where
dx_get_dx_info() first needs it) computes the required rec_len for
a directory entry, taking into account dirdata and casefold+fscrypt
hash space.
Update dx_root_limit() to use ext4_dir_entry_len() instead of
open-coded ext4_dir_rec_len() for '.' and '..' entries.
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Assisted-by: Claude:claude-opus-5-5
---
fs/ext4/ext4.h | 8 +++++---
fs/ext4/namei.c | 28 +++++++++++++++++++---------
2 files changed, 24 insertions(+), 12 deletions(-)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index cebbfac05fa5..fe6b8fe4634e 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3085,12 +3085,14 @@ static const unsigned char ext4_filetype_table[] = {
DT_UNKNOWN, DT_REG, DT_DIR, DT_CHR, DT_BLK, DT_FIFO, DT_SOCK, DT_LNK
};
-static inline unsigned char get_dtype(struct super_block *sb, int filetype)
+static inline unsigned char get_dtype(struct super_block *sb, int filetype)
{
- if (!ext4_has_feature_filetype(sb) || filetype >= EXT4_FT_MAX)
+ unsigned char fl_index = filetype & EXT4_FT_MASK;
+
+ if (!ext4_has_feature_filetype(sb) || fl_index >= EXT4_FT_MAX)
return DT_UNKNOWN;
- return ext4_filetype_table[filetype];
+ return ext4_filetype_table[fl_index];
}
extern int ext4_check_all_de(struct inode *dir, struct buffer_head *bh,
void *buf, int buf_size);
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index c221bcd562cc..9953583ad6c3 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -611,14 +611,21 @@ 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, used;
+ info = dx_get_dx_info(dir, dot_de);
+ if (IS_ERR(info))
+ return 0;
+ used = (char *)info - (char *)dot_de + info->info_length;
if (ext4_has_feature_metadata_csum(dir->i_sb))
- entry_space -= sizeof(struct dx_tail);
+ used += sizeof(struct dx_tail);
+ if (used > dir->i_sb->s_blocksize)
+ return 0;
+ entry_space = dir->i_sb->s_blocksize - used;
return entry_space / sizeof(struct dx_entry);
}
@@ -919,10 +926,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;
}
@@ -2372,10 +2382,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));
/* Initialize as for dx_probe */
fname->hinfo.hash_version = dx_info->hash_version;
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v14 07/12] ext4: add ext4_dir_entry_len()
2026-11-03 13:49 ` [PATCH v14 07/12] ext4: add ext4_dir_entry_len() Artem Blagodarenko
@ 2026-11-03 14:08 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-03 14:08 UTC (permalink / raw)
To: Artem Blagodarenko, linux-ext4; +Cc: tytso
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] ext4: inconsistent dx_get_dx_info() offset in make_indexed_dir()
- [Low] ext4: commit message mismatch with dx_root_limit() and get_dtype()
--
commit e8626c9249ed17bdd04a7586fe5be141bfa96520
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>
ext4: add ext4_dir_entry_len()
This commit updates dx_root_limit() to calculate the root htree entry limit
using dx_get_dx_info() and masks filetype with EXT4_FT_MASK in get_dtype().
> ext4: add ext4_dir_entry_len()
>
> ext4_dir_entry_len() (added in the previous patch, where
> dx_get_dx_info() first needs it) computes the required rec_len for
> a directory entry, taking into account dirdata and casefold+fscrypt
> hash space.
>
> Update dx_root_limit() to use ext4_dir_entry_len() instead of
> open-coded ext4_dir_rec_len() for '.' and '..' entries.
>
> Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
> Assisted-by: Claude:claude-opus-5-5
[Severity: Low]
This isn't a bug, but does the commit title and message match the changes in
this patch?
As the commit message notes, ext4_dir_entry_len() was already added in the
previous commit (73068d5241d8, "ext4: refactor dx_root to support variable
dirent sizes").
In addition, dx_root_limit() is updated here to call dx_get_dx_info() rather
than ext4_dir_entry_len() directly, and the change to get_dtype() in
fs/ext4/ext4.h (masking filetype with EXT4_FT_MASK before comparing against
EXT4_FT_MAX) is not mentioned in the commit message.
> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index c221bcd562cc9..9953583ad6c3f 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c
> @@ -611,14 +611,21 @@ 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, used;
>
> + info = dx_get_dx_info(dir, dot_de);
> + if (IS_ERR(info))
> + return 0;
> + used = (char *)info - (char *)dot_de + info->info_length;
> if (ext4_has_feature_metadata_csum(dir->i_sb))
> - entry_space -= sizeof(struct dx_tail);
> + used += sizeof(struct dx_tail);
> + if (used > dir->i_sb->s_blocksize)
> + return 0;
> + entry_space = dir->i_sb->s_blocksize - used;
> return entry_space / sizeof(struct dx_entry);
> }
[ ... ]
> @@ -2372,10 +2382,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: Medium]
Can calling dx_root_limit(dir, dot_de) after dotdot_de->rec_len has been
expanded cause dx_get_dx_info() to resolve a different dx_root_info offset
than the initial call in make_indexed_dir()?
Earlier in make_indexed_dir(), dx_info is computed before modifying the
directory block, while dotdot_de->rec_len still holds its original unindexed
entry length (for example, 12 bytes):
dx_info = dx_get_dx_info(dir, dot_de);
Inside dx_get_dx_info() -> ext4_dir_entry_len() -> ext4_dirent_get_data_len(),
if dotdot_de->file_type has upper dirdata flag bits set (such as on a crafted
or corrupted image) and the dirdata length byte at offset 11 exceeds the
remaining space in the 12-byte entry, ext4_dirent_get_data_len() sees:
if (ddh_len == 0 || ddh_len > rec_len - offset)
return 0;
and returns 0, placing dx_info right after the 12-byte '..' entry.
Then make_indexed_dir() wipes the block starting at de (leaving the bytes of
dotdot_de intact) and expands dotdot_de->rec_len to cover the rest of the
block:
dotdot_de->rec_len =
ext4_rec_len_to_disk(blocksize - le16_to_cpu(dot_de->rec_len),
blocksize);
When dx_root_limit(dir, dot_de) calls dx_get_dx_info(dir, dot_de) a second
time, rec_len is now blocksize - 12 (e.g. 4084). In ext4_dirent_get_data_len(),
ddh_len > rec_len - offset is no longer true, so it returns a non-zero
dirdata length. That shifts the info pointer inside dx_root_limit() past the
dx_info structure that was just initialized (or causes dx_get_dx_info() to
return ERR_PTR(-EFSCORRUPTED)), writing a wrong or zero limit into entries.
Since both dx_probe() and make_indexed_dir() already have the validated
struct dx_root_info pointer, could dx_root_limit() take info (or its offset)
directly instead of calling dx_get_dx_info() again?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103134921.610047-1-ablagodarenko@thelustrecollective.com?part=7
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v14 08/12] ext4: rename ext4_dir_rec_len() and clarify dirdata usage
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
` (6 preceding siblings ...)
2026-11-03 13:49 ` [PATCH v14 07/12] ext4: add ext4_dir_entry_len() Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 14:20 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 09/12] ext4: dirdata feature Artem Blagodarenko
` (3 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4; +Cc: tytso, jack, adilger.kernel, ablagodarenko, Artem Blagodarenko
From: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Rename ext4_dir_rec_len() to ext4_dirent_rec_len() to better
reflect that it computes the record length for a directory
entry based on the provided name length.
Update the comment to clarify handling of dirdata-enabled
directories and document the use of ext4_dir_entry_len()
when dirdata is present.
ext4_dir_entry_len() reads the dirdata of an entry, so the length of
the entry that do_split() hands to add_dirent_to_buf() now depends on
on-disk data. add_dirent_to_buf() checks that this entry has room for
the new one before anything is written, instead of overrunning the
block when the directory is corrupted.
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Assisted-by: Claude:claude-opus-5-5
---
fs/ext4/dir.c | 15 +++++++-------
fs/ext4/ext4.h | 16 ++++++++++-----
fs/ext4/inline.c | 21 +++++++++++---------
fs/ext4/namei.c | 51 +++++++++++++++++++++++++++++++++---------------
4 files changed, 65 insertions(+), 38 deletions(-)
diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
index 8d7b81e6948e..38911ae2bcfc 100644
--- a/fs/ext4/dir.c
+++ b/fs/ext4/dir.c
@@ -89,16 +89,15 @@ int __ext4_check_dir_entry(const char *function, unsigned int line,
bool fake = is_fake_dir_entry(de);
bool has_csum = ext4_has_feature_metadata_csum(dir->i_sb);
- if (unlikely(rlen < ext4_dir_rec_len(1, fake ? NULL : dir)))
+ if (unlikely(rlen < ext4_dirent_rec_len(1, fake ? NULL : dir)))
error_msg = "rec_len is smaller than minimal";
else if (unlikely(rlen % 4 != 0))
error_msg = "rec_len % 4 != 0";
- else if (unlikely(rlen < ext4_dir_rec_len(de->name_len,
- fake ? NULL : dir)))
- error_msg = "rec_len is too small for name_len";
else if (unlikely(next_offset > size))
error_msg = "directory entry overrun";
- else if (unlikely(next_offset > size - ext4_dir_rec_len(1,
+ else if (unlikely(rlen < ext4_dir_entry_len(de, size, fake ? NULL : dir)))
+ error_msg = "rec_len is too small for name_len";
+ else if (unlikely(next_offset > size - ext4_dirent_rec_len(1,
has_csum ? NULL : dir) &&
next_offset != size))
error_msg = "directory entry too close to block end";
@@ -238,7 +237,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
if (!inode_eq_iversion(inode, info->cookie)) {
for (i = 0;
i <= sb->s_blocksize -
- ext4_dir_rec_len(1, has_csum ? NULL : inode) &&
+ ext4_dirent_rec_len(1, has_csum ? NULL : inode) &&
i < offset;) {
de = (struct ext4_dir_entry_2 *)
(bh->b_data + i);
@@ -249,7 +248,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
* failure will be detected in the
* dirent test below. */
if (ext4_rec_len_from_disk(de->rec_len,
- sb->s_blocksize) < ext4_dir_rec_len(1,
+ sb->s_blocksize) < ext4_dirent_rec_len(1,
inode))
break;
i += ext4_rec_len_from_disk(de->rec_len,
@@ -263,7 +262,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
if (unlikely(offset < sb->s_blocksize &&
offset > sb->s_blocksize -
- ext4_dir_rec_len(1, has_csum ? NULL : inode))) {
+ ext4_dirent_rec_len(1, has_csum ? NULL : inode))) {
EXT4_ERROR_FILE(file, bh->b_blocknr,
"bad entry in directory: %s - offset=%u, size=%lu",
"directory entry too close to block end",
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index fe6b8fe4634e..e42e5bcc2f28 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -2655,11 +2655,16 @@ struct ext4_dirent_hash {
* casefolded and encrypted need to store the hash as well, so we add room for
* ext4_extended_dir_entry_2. For all entries related to '.' or '..' you should
* pass NULL for dir, as those entries do not use the extra fields.
+ *
+ * For directories with the dirdata feature, extra data may follow the filename.
+ * Use ext4_dir_entry_len() to compute the length of a directory entry
+ * including any dirdata, or ext4_dirent_rec_len() directly when the total
+ * name_len (including dirdata length) is already known.
*/
-static inline unsigned int ext4_dir_rec_len(__u8 name_len,
+static inline unsigned int ext4_dirent_rec_len(unsigned int name_len,
const struct inode *dir)
{
- int rec_len = (name_len + 8 + EXT4_DIR_ROUND);
+ unsigned int rec_len = (name_len + 8 + EXT4_DIR_ROUND);
if (dir && ext4_hash_in_dirent(dir))
rec_len += sizeof(struct ext4_dir_entry_hash);
@@ -3067,7 +3072,8 @@ extern void ext4_htree_free_dir_info(struct dir_private_info *p);
extern int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
void *buf, int buf_size,
struct ext4_filename *fname,
- struct ext4_dir_entry_2 **dest_de);
+ struct ext4_dir_entry_2 **dest_de,
+ int dlen);
void ext4_insert_dentry(struct inode *dir, struct inode *inode,
struct ext4_dir_entry_2 *de,
int buf_size,
@@ -4268,8 +4274,8 @@ static inline unsigned int ext4_dir_entry_len(struct ext4_dir_entry_2 *de,
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 max(ext4_dir_rec_len(de->name_len + dirdata, NULL),
- ext4_dir_rec_len(de->name_len, dir));
+ return max(ext4_dirent_rec_len(de->name_len + dirdata, NULL),
+ ext4_dirent_rec_len(de->name_len, dir));
}
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index ceee69a66482..bdf6f45a3de4 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -985,8 +985,11 @@ static int ext4_add_dirent_to_inline(handle_t *handle,
int err;
struct ext4_dir_entry_2 *de;
+ /* 'dlen' reserves space for dirdata extensions (LUFID, CFHASH) in the
+ * slot. The dirdata types are defined in the next patch; until then
+ * dlen stays 0 and ext4_find_dest_de() ignores dirdata overhead. */
err = ext4_find_dest_de(dir, iloc->bh, inline_start,
- inline_size, fname, &de);
+ inline_size, fname, &de, 0);
if (err)
return err;
@@ -1064,7 +1067,7 @@ static int ext4_update_inline_dir(handle_t *handle, struct inode *dir,
int old_size = EXT4_I(dir)->i_inline_size - EXT4_MIN_INLINE_DATA_SIZE;
int new_size = get_max_inline_xattr_value_size(dir, iloc);
- if (new_size - old_size <= ext4_dir_rec_len(1, NULL))
+ if (new_size - old_size <= ext4_dirent_rec_len(1, NULL))
return -ENOSPC;
ret = ext4_update_inline_data(handle, dir,
@@ -1318,7 +1321,7 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
fake.name_len = 1;
memcpy(fake.name, ".", 2);
fake.rec_len = ext4_rec_len_to_disk(
- ext4_dir_rec_len(fake.name_len, NULL),
+ ext4_dirent_rec_len(fake.name_len, NULL),
inline_size);
ext4_set_de_type(inode->i_sb, &fake, S_IFDIR);
de = &fake;
@@ -1328,7 +1331,7 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
fake.name_len = 2;
memcpy(fake.name, "..", 3);
fake.rec_len = ext4_rec_len_to_disk(
- ext4_dir_rec_len(fake.name_len, NULL),
+ ext4_dirent_rec_len(fake.name_len, NULL),
inline_size);
ext4_set_de_type(inode->i_sb, &fake, S_IFDIR);
de = &fake;
@@ -1436,8 +1439,8 @@ int ext4_read_inline_dir(struct file *file,
* So we will use extra_offset and extra_size to indicate them
* during the inline dir iteration.
*/
- dotdot_offset = ext4_dir_rec_len(1, NULL);
- dotdot_size = dotdot_offset + ext4_dir_rec_len(2, NULL);
+ dotdot_offset = ext4_dirent_rec_len(1, NULL);
+ dotdot_size = dotdot_offset + ext4_dirent_rec_len(2, NULL);
extra_offset = dotdot_size - EXT4_INLINE_DOTDOT_SIZE;
extra_size = extra_offset + inline_size;
@@ -1463,7 +1466,7 @@ int ext4_read_inline_dir(struct file *file,
/* for other entry, the real offset in
* the buf has to be tuned accordingly.
*/
- if (i + ext4_dir_rec_len(1, NULL) > extra_size)
+ if (i + ext4_dirent_rec_len(1, NULL) > extra_size)
break;
de = (struct ext4_dir_entry_2 *)
(dir_buf + i - extra_offset);
@@ -1474,7 +1477,7 @@ int ext4_read_inline_dir(struct file *file,
* failure will be detected in the
* dirent test below. */
if (ext4_rec_len_from_disk(de->rec_len, extra_size)
- < ext4_dir_rec_len(1, NULL))
+ < ext4_dirent_rec_len(1, NULL))
break;
i += ext4_rec_len_from_disk(de->rec_len,
extra_size);
@@ -1504,7 +1507,7 @@ int ext4_read_inline_dir(struct file *file,
* kmalloc(inline_size) buffer. Make sure its header fits before
* ext4_check_dir_entry() dereferences de->rec_len.
*/
- if (ctx->pos + ext4_dir_rec_len(1, NULL) > extra_size)
+ if (ctx->pos + ext4_dirent_rec_len(1, NULL) > extra_size)
goto out;
de = (struct ext4_dir_entry_2 *)
(dir_buf + ctx->pos - extra_offset);
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 9953583ad6c3..0c91e4da00fb 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -633,7 +633,7 @@ static inline unsigned int __dx_node_limit(struct inode *dir,
const struct inode *hash_dir)
{
unsigned int entry_space = dir->i_sb->s_blocksize -
- ext4_dir_rec_len(0, hash_dir);
+ ext4_dirent_rec_len(0, hash_dir);
if (ext4_has_feature_metadata_csum(dir->i_sb))
entry_space -= sizeof(struct dx_tail);
@@ -763,7 +763,7 @@ static struct stats dx_show_leaf(struct inode *dir,
(unsigned) ((char *) de - base));
#endif
}
- space += ext4_dir_rec_len(de->name_len, dir);
+ space += ext4_dirent_rec_len(de->name_len, dir);
names++;
}
de = ext4_next_entry(de, size);
@@ -1141,7 +1141,7 @@ static int htree_dirblock_to_tree(struct file *dir_file,
/* csum entries are not larger in the casefolded encrypted case */
top = (struct ext4_dir_entry_2 *) ((char *) de +
dir->i_sb->s_blocksize -
- ext4_dir_rec_len(0,
+ ext4_dirent_rec_len(0,
csum ? NULL : dir));
/* Check if the directory is encrypted */
if (IS_ENCRYPTED(dir)) {
@@ -1553,7 +1553,7 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
/* do minimal checking `by hand' */
if (de->name + de->name_len <= dlimit &&
(!ext4_hash_in_dirent(dir) ||
- (char *)de + ext4_dir_rec_len(de->name_len, dir) <= dlimit) &&
+ (char *)de + ext4_dirent_rec_len(de->name_len, dir) <= dlimit) &&
ext4_match(dir, fname, de)) {
/* found a match - just to be sure, do
* a full check */
@@ -1937,7 +1937,7 @@ dx_move_dirents(struct inode *dir, char *from, char *to,
while (count--) {
struct ext4_dir_entry_2 *de = (struct ext4_dir_entry_2 *)
(from + (map->offs<<2));
- rec_len = ext4_dir_rec_len(de->name_len, dir);
+ rec_len = ext4_dir_entry_len(de, blocksize, dir);
memcpy (to, de, rec_len);
((struct ext4_dir_entry_2 *) to)->rec_len =
@@ -1970,7 +1970,7 @@ static struct ext4_dir_entry_2 *dx_pack_dirents(struct inode *dir, char *base,
while ((char*)de < base + blocksize) {
next = ext4_next_entry(de, blocksize);
if (de->inode && de->name_len) {
- rec_len = ext4_dir_rec_len(de->name_len, dir);
+ rec_len = ext4_dir_entry_len(de, blocksize, dir);
if (de > to)
memmove(to, de, rec_len);
to->rec_len = ext4_rec_len_to_disk(rec_len, blocksize);
@@ -2122,10 +2122,11 @@ static struct ext4_dir_entry_2 *do_split(handle_t *handle, struct inode *dir,
int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
void *buf, int buf_size,
struct ext4_filename *fname,
- struct ext4_dir_entry_2 **dest_de)
+ struct ext4_dir_entry_2 **dest_de,
+ int dlen)
{
struct ext4_dir_entry_2 *de;
- unsigned short reclen = ext4_dir_rec_len(fname_len(fname), dir);
+ unsigned short reclen = ext4_dirent_rec_len(fname_len(fname) + dlen, dir);
int nlen, rlen;
unsigned int offset = 0;
char *top;
@@ -2138,7 +2139,7 @@ int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
return -EFSCORRUPTED;
if (ext4_match(dir, fname, de))
return -EEXIST;
- nlen = ext4_dir_rec_len(de->name_len, dir);
+ nlen = ext4_dir_entry_len(de, buf_size, dir);
rlen = ext4_rec_len_from_disk(de->rec_len, buf_size);
if ((de->inode ? rlen - nlen : rlen) >= reclen)
break;
@@ -2161,7 +2162,7 @@ void ext4_insert_dentry(struct inode *dir,
int nlen, rlen;
- nlen = ext4_dir_rec_len(de->name_len, dir);
+ nlen = ext4_dir_entry_len(de, buf_size, dir);
rlen = ext4_rec_len_from_disk(de->rec_len, buf_size);
if (de->inode) {
struct ext4_dir_entry_2 *de1 =
@@ -2206,9 +2207,27 @@ static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,
if (!de) {
err = ext4_find_dest_de(dir, bh, bh->b_data,
- blocksize - csum_size, fname, &de);
+ blocksize - csum_size, fname, &de, 0);
if (err)
return err;
+ } else {
+ /*
+ * do_split() hands over the last entry of a half. Its length
+ * comes from on-disk dirdata, so check that the new entry fits
+ * before anything is written.
+ */
+ unsigned int nlen = de->inode ?
+ ext4_dir_entry_len(de, blocksize, dir) : 0;
+ unsigned int rlen = ext4_rec_len_from_disk(de->rec_len,
+ blocksize);
+
+ if (nlen > rlen ||
+ rlen - nlen < ext4_dirent_rec_len(fname_len(fname), dir)) {
+ EXT4_ERROR_INODE(dir,
+ "no room for new entry in split block %llu",
+ (unsigned long long)bh->b_blocknr);
+ return -EFSCORRUPTED;
+ }
}
BUFFER_TRACE(bh, "get_write_access");
err = ext4_journal_get_write_access(handle, dir->i_sb, bh,
@@ -3032,7 +3051,7 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,
de->inode = cpu_to_le32(inode->i_ino);
de->name_len = 1;
- de->rec_len = ext4_rec_len_to_disk(ext4_dir_rec_len(de->name_len, NULL),
+ de->rec_len = ext4_rec_len_to_disk(ext4_dirent_rec_len(de->name_len, NULL),
blocksize);
memcpy(de->name, ".", 2);
ext4_set_de_type(inode->i_sb, de, S_IFDIR);
@@ -3044,7 +3063,7 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,
ext4_set_de_type(inode->i_sb, de, S_IFDIR);
if (inline_buf) {
de->rec_len = ext4_rec_len_to_disk(
- ext4_dir_rec_len(de->name_len, NULL),
+ ext4_dirent_rec_len(de->name_len, NULL),
blocksize);
de = ext4_next_entry(de, blocksize);
header_size = (char *)de - bh->b_data;
@@ -3053,7 +3072,7 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,
blocksize - csum_size);
} else {
de->rec_len = ext4_rec_len_to_disk(blocksize -
- (csum_size + ext4_dir_rec_len(1, NULL)),
+ (csum_size + ext4_dirent_rec_len(1, NULL)),
blocksize);
}
@@ -3176,8 +3195,8 @@ bool ext4_empty_dir(struct inode *inode)
}
sb = inode->i_sb;
- if (inode->i_size < ext4_dir_rec_len(1, NULL) +
- ext4_dir_rec_len(2, NULL)) {
+ if (inode->i_size < ext4_dirent_rec_len(1, NULL) +
+ ext4_dirent_rec_len(2, NULL)) {
EXT4_ERROR_INODE(inode, "invalid size");
return false;
}
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v14 08/12] ext4: rename ext4_dir_rec_len() and clarify dirdata usage
2026-11-03 13:49 ` [PATCH v14 08/12] ext4: rename ext4_dir_rec_len() and clarify dirdata usage Artem Blagodarenko
@ 2026-11-03 14:20 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-03 14:20 UTC (permalink / raw)
To: Artem Blagodarenko, linux-ext4; +Cc: tytso
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] ext4: unsigned len underflow in make_indexed_dir() when dot_de has dirdata
- [High] ext4: __ext4_check_dir_entry() misses out-of-bounds dirdata before rec_len grows
- [High] ext4: add_dirent_to_buf() error return after do_split() leaves bh undirtied
--
commit 06e0b22cf991d67d7b351c0723ad5474e253ef41
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>
ext4: rename ext4_dir_rec_len() and clarify dirdata usage
This commit renames ext4_dir_rec_len() to ext4_dirent_rec_len(), updates
directory entry length calculations to use ext4_dir_entry_len() so dirdata
is accounted for, and adds a room check in add_dirent_to_buf() for split
blocks.
> diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
> index 8d7b81e6948e7..38911ae2bcfc0 100644
> --- a/fs/ext4/dir.c
> +++ b/fs/ext4/dir.c
> @@ -89,16 +89,15 @@ int __ext4_check_dir_entry(const char *function, unsigned int line,
> bool fake = is_fake_dir_entry(de);
> bool has_csum = ext4_has_feature_metadata_csum(dir->i_sb);
>
> - if (unlikely(rlen < ext4_dir_rec_len(1, fake ? NULL : dir)))
> + if (unlikely(rlen < ext4_dirent_rec_len(1, fake ? NULL : dir)))
> error_msg = "rec_len is smaller than minimal";
> else if (unlikely(rlen % 4 != 0))
> error_msg = "rec_len % 4 != 0";
> - else if (unlikely(rlen < ext4_dir_rec_len(de->name_len,
> - fake ? NULL : dir)))
> - error_msg = "rec_len is too small for name_len";
> else if (unlikely(next_offset > size))
> error_msg = "directory entry overrun";
> - else if (unlikely(next_offset > size - ext4_dir_rec_len(1,
> + else if (unlikely(rlen < ext4_dir_entry_len(de, size, fake ? NULL : dir)))
> + error_msg = "rec_len is too small for name_len";
[Severity: High]
As reported by Sashiko on v13 [1], can __ext4_check_dir_entry() ever reject
an entry whose on-disk rec_len is too small for its dirdata here, or does a
later rec_len expansion cause stale buffer bytes to be annexed into the
entry?
In ext4_dir_entry_len(), ext4_dirent_get_data_len(de, rec_len) validates
each dirdata header against the entry's current rec_len and returns 0 when
offset >= rec_len, offset + sizeof(*ddh) > rec_len, or ddh_len > rec_len -
offset:
fs/ext4/ext4.h:ext4_dirent_get_data_len() {
...
if (offset >= rec_len)
return 0;
...
if (extra_data_flags & 1) {
if (offset + sizeof(*ddh) > rec_len)
return 0;
{
u8 ddh_len = READ_ONCE(ddh->ddh_length);
if (ddh_len == 0 || ddh_len > rec_len - offset)
return 0;
dlen += ddh_len + (dlen == 0);
offset += ddh_len;
...
}
Because rlen % 4 != 0 is already checked earlier in
__ext4_check_dir_entry(), any non-zero dirdata returned by
ext4_dirent_get_data_len() satisfies 8 + de->name_len + dirdata == offset
<= rlen, so ext4_dirent_rec_len(de->name_len + dirdata, NULL) <= rlen.
When a corrupted entry has dirdata flags set with ddh_length exceeding its
current rec_len, ext4_dirent_get_data_len() returns 0 and
__ext4_check_dir_entry() accepts the entry.
Later, during do_split(), dx_move_dirents() and dx_pack_dirents() compute
rec_len = ext4_dir_entry_len(de, blocksize, dir) while de->rec_len still
has its smaller pre-split value, copying only those bytes before do_split()
expands the last entry's rec_len to the end of the half-block. Once
de->rec_len grows (either in do_split() or after
ext4_generic_delete_entry()), subsequent calls to ext4_dir_entry_len() in
add_dirent_to_buf() and ext4_insert_dentry() see the larger rec_len, so the
previously out-of-bounds ddh_length now satisfies ddh_len <= rec_len -
offset and annexes uncopied or stale buffer bytes into the directory entry.
[ ... ]
> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index 9953583ad6c3f..0c91e4da00fbc 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c
[ ... ]
> @@ -2122,10 +2122,11 @@ static struct ext4_dir_entry_2 *do_split(handle_t *handle, struct inode *dir,
> int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
> void *buf, int buf_size,
> struct ext4_filename *fname,
> - struct ext4_dir_entry_2 **dest_de)
> + struct ext4_dir_entry_2 **dest_de,
> + int dlen)
> {
> struct ext4_dir_entry_2 *de;
> - unsigned short reclen = ext4_dir_rec_len(fname_len(fname), dir);
> + unsigned short reclen = ext4_dirent_rec_len(fname_len(fname) + dlen, dir);
> int nlen, rlen;
> unsigned int offset = 0;
> char *top;
> @@ -2138,7 +2139,7 @@ int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
> return -EFSCORRUPTED;
> if (ext4_match(dir, fname, de))
> return -EEXIST;
> - nlen = ext4_dir_rec_len(de->name_len, dir);
> + nlen = ext4_dir_entry_len(de, buf_size, dir);
> rlen = ext4_rec_len_from_disk(de->rec_len, buf_size);
> if ((de->inode ? rlen - nlen : rlen) >= reclen)
> break;
[Severity: Critical]
Can changing nlen to ext4_dir_entry_len() in ext4_find_dest_de() allow
unsigned len in make_indexed_dir() to underflow and overflow data2 in
memcpy(data2, de, len)?
In __ext4_add_entry() -> add_dirent_to_buf() -> ext4_find_dest_de() on
block 0 of an unindexed directory (where buf_size = blocksize - csum_size),
the loop runs while (char *)de <= buf + buf_size - reclen. Because nlen now
includes dirdata via ext4_dir_entry_len(), the '.' entry (dot_de) on a
corrupted or crafted image can have rlen > buf_size - reclen while its
dirdata makes rlen - nlen < reclen. In that case, ext4_find_dest_de()
checks dot_de, advances de to dotdot_de at an offset past top, exits the
loop without running ext4_check_dir_entry() on dotdot_de, and returns
-ENOSPC.
__ext4_add_entry() then calls make_indexed_dir(), which validates dotdot_de
through ext4_check_dx_root() and dx_get_dx_info(). However, both check the
end of dotdot_de against blocksize rather than blocksize - csum_size:
fs/ext4/namei.c:ext4_check_dx_root() {
...
de = ext4_next_entry(dotdot_de, blocksize);
if ((char *)de >= (((char *)dot_de) + blocksize)) {
error_msg = "invalid rec_len for '..'";
goto corrupted;
}
...
}
When metadata_csum is enabled (csum_size = 12) and dotdot_de ends between
blocksize - csum_size + 1 and blocksize - 1, make_indexed_dir() computes:
fs/ext4/namei.c:make_indexed_dir() {
...
/* The 0th block becomes the root, move the dirents out */
len = ((char *)dot_de) + (blocksize - csum_size) - (char *)de;
...
memcpy(data2, de, len);
}
Since (char *)de > (char *)dot_de + (blocksize - csum_size), unsigned len
underflows and is passed to memcpy(data2, de, len) and memset(de, 0, len).
[ ... ]
> @@ -2206,9 +2207,27 @@ static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,
>
> if (!de) {
> err = ext4_find_dest_de(dir, bh, bh->b_data,
> - blocksize - csum_size, fname, &de);
> + blocksize - csum_size, fname, &de, 0);
> if (err)
> return err;
> + } else {
> + /*
> + * do_split() hands over the last entry of a half. Its length
> + * comes from on-disk dirdata, so check that the new entry fits
> + * before anything is written.
> + */
> + unsigned int nlen = de->inode ?
> + ext4_dir_entry_len(de, blocksize, dir) : 0;
> + unsigned int rlen = ext4_rec_len_from_disk(de->rec_len,
> + blocksize);
> +
> + if (nlen > rlen ||
> + rlen - nlen < ext4_dirent_rec_len(fname_len(fname), dir)) {
> + EXT4_ERROR_INODE(dir,
> + "no room for new entry in split block %llu",
> + (unsigned long long)bh->b_blocknr);
> + return -EFSCORRUPTED;
> + }
> }
[Severity: High]
When this check in add_dirent_to_buf() returns -EFSCORRUPTED for a de
returned by do_split(), does it leave bh modified with a zeroed checksum
tail and without calling ext4_handle_dirty_dirblock()?
Before returning de to make_indexed_dir() or ext4_dx_add_entry(),
do_split() has already obtained write access to *bh, bh2, and frame->bh,
modified *bh and bh2 in-place via dx_move_dirents() and dx_pack_dirents(),
zeroed det_checksum in both blocks via ext4_initialize_dirent_tail(), and
dirtied bh2 and frame->bh:
fs/ext4/namei.c:do_split() {
...
if (csum_size) {
ext4_initialize_dirent_tail(*bh, blocksize);
ext4_initialize_dirent_tail(bh2, blocksize);
}
...
if (hinfo->hash >= hash2) {
swap(*bh, bh2);
de = de2;
}
dx_insert_block(frame, hash2 + continued, newblock);
err = ext4_handle_dirty_dirblock(handle, dir, bh2);
if (err)
goto journal_error;
err = ext4_handle_dirty_dx_node(handle, dir, frame->bh);
if (err)
goto journal_error;
...
}
do_split() relies on add_dirent_to_buf() to checksum and dirty *bh via
ext4_handle_dirty_dirblock(handle, dir, bh). If add_dirent_to_buf() returns
-EFSCORRUPTED early here, bh2 and frame->bh are committed in the journal
with the post-split state (for instance in make_indexed_dir() or when
mounted with errors=continue), while bh is never passed to
ext4_handle_dirty_dirblock().
[1] https://lore.kernel.org/r/20261029122413.230409-8-ablagodarenko@thelustrecollective.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103134921.610047-1-ablagodarenko@thelustrecollective.com?part=8
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v14 09/12] ext4: dirdata feature
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
` (7 preceding siblings ...)
2026-11-03 13:49 ` [PATCH v14 08/12] ext4: rename ext4_dir_rec_len() and clarify dirdata usage Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 14:27 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 10/12] ext4: add dirdata LUFID support for directory entry rename Artem Blagodarenko
` (2 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4
Cc: tytso, jack, adilger.kernel, ablagodarenko, Artem Blagodarenko,
Pravin Shelar
From: Artem Blagodarenko <artem.blagodarenko@gmail.com>
When fscrypt and casefold are enabled together for a directory,
all ext4_dir_entry[_2] in that directory store a n 8-byte hash
of the filename after 'name' between 'name_len' and 'rec_len'.
However, there is no clear indication there is important data
stored in these bytes, which are only for padding and alignment
in other directory entries. This adds complexity to code handling
the on-disk directory entries, and there is no provision for other
metadata to be stored in each dir entry after 'name'.
The dirdata feature adds a mechanism to store multiple metadata
entries in each dir entry after 'name' (including the fchash).
The unused high 4 bits of 'file_type' are used to indicate whether
additional data fields are stored after 'name'. If a bit is set,
the corresponding dirdata record is present, starting after a NUL
filename terminator. If present, a record starts with a 1-byte
length (including the length byte itself) and the data immediately
follows the length byte without any alignment.
This allows up to four different dirdata records to be stored in
each entry, and allows unhandled record bytes to be skipped without
having to process the contents, providing forward compatibility.
If and when the fourth and last dirdata record is needed, it is
recommended to further subdivide it into sub-records, with
the first byte being the total length, and then there being a
second byte that gives the sub-record length, etc. as long as
the total record length is less than 255 bytes. However, this
would not affect compatibility with the current code since the
record length would allow it to be skipped without processing.
Signed-off-by: Pravin Shelar <pravin.shelar@sun.com>
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Assisted-by: Claude:claude-opus-5-5
---
fs/ext4/dir.c | 22 +++++++--
fs/ext4/ext4.h | 79 ++++++++++++++++++++++++++++++---
fs/ext4/fast_commit.c | 1 +
fs/ext4/fast_commit.h | 1 +
fs/ext4/inline.c | 29 ++++++++----
fs/ext4/namei.c | 101 +++++++++++++++++++++++++++++-------------
fs/ext4/sysfs.c | 2 +
7 files changed, 186 insertions(+), 49 deletions(-)
diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
index 38911ae2bcfc..15816f2a5f47 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);
+ }
+
+ /* exta_data contains +1 byte for NULL */
+ new_fn = kzalloc_flex(*new_fn, name, ent_name->len +
+ (extra_data ? extra_data : 1));
if (!new_fn)
return -ENOMEM;
new_fn->hash = hash;
new_fn->minor_hash = minor_hash;
new_fn->inode = le32_to_cpu(dirent->inode);
- new_fn->name_len = ent_name->len;
new_fn->file_type = dirent->file_type;
+ new_fn->name_len = ent_name->len;
memcpy(new_fn->name, ent_name->name, ent_name->len);
+ if (extra_data)
+ /* Dirdata bytes lie past the __counted_by(name_len) bound but
+ * within the kzalloc_flex allocation; use unsafe_memcpy to
+ * avoid a FORTIFY_SOURCE false positive. */
+ unsafe_memcpy(new_fn->name + ent_name->len,
+ dirent->name + dirent->name_len, extra_data,
+ /* within kzalloc_flex allocation */);
while (*p) {
parent = *p;
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index e42e5bcc2f28..4a7b36e1910e 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -2369,6 +2369,7 @@ EXT4_FEATURE_INCOMPAT_FUNCS(casefold, CASEFOLD)
EXT4_FEATURE_INCOMPAT_FLEX_BG| \
EXT4_FEATURE_INCOMPAT_EA_INODE| \
EXT4_FEATURE_INCOMPAT_MMP | \
+ EXT4_FEATURE_INCOMPAT_DIRDATA | \
EXT4_FEATURE_INCOMPAT_INLINE_DATA | \
EXT4_FEATURE_INCOMPAT_ENCRYPT | \
EXT4_FEATURE_INCOMPAT_CASEFOLD | \
@@ -2639,6 +2640,18 @@ struct ext4_dirent_hash {
__le32 dh_minor_hash;
} __packed;
+static inline
+struct ext4_dirent_fid *ext4_dentry_get_fid(struct super_block *sb,
+ struct ext4_dentry_param *p)
+{
+ if (!ext4_has_feature_dirdata(sb))
+ return NULL;
+ if (p && p->edp_magic == EXT4_LUFID_MAGIC)
+ return (struct ext4_dirent_fid *)p->edp_dfid;
+
+ return NULL;
+}
+
#define EXT4_FT_DIR_CSUM 0xDE
/*
@@ -2754,6 +2767,8 @@ struct ext4_filename {
#if IS_ENABLED(CONFIG_UNICODE)
struct qstr cf_name;
#endif
+ /* LUFID/dirdata payload to embed in the new directory entry, or NULL */
+ struct ext4_dirent_fid *dfid;
};
#define fname_name(p) ((p)->disk_name.name)
@@ -3065,7 +3080,7 @@ extern int __ext4_check_dir_entry(const char *, unsigned int, struct inode *,
unlikely(__ext4_check_dir_entry(__func__, __LINE__, (dir), (filp), \
(de), (bh), (buf), (size), (offset)))
extern 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);
extern void ext4_htree_free_dir_info(struct dir_private_info *p);
@@ -3074,10 +3089,18 @@ extern int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
struct ext4_filename *fname,
struct ext4_dir_entry_2 **dest_de,
int dlen);
-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);
+static inline void ext4_insert_dentry(struct inode *dir, struct inode *inode,
+ struct ext4_dir_entry_2 *de,
+ int buf_size,
+ struct ext4_filename *fname)
+{
+ ext4_insert_dentry_data(dir, inode, de, buf_size, fname, NULL);
+}
static inline void ext4_update_dx_flag(struct inode *inode)
{
if (!ext4_has_feature_dir_index(inode->i_sb) &&
@@ -3334,8 +3357,14 @@ extern int ext4_ext_migrate(struct inode *);
extern int ext4_ind_migrate(struct inode *inode);
/* namei.c */
-extern int ext4_init_new_dir(handle_t *handle, struct inode *dir,
- struct inode *inode);
+extern int ext4_init_new_dir_data(handle_t *handle, struct inode *dir,
+ struct inode *inode,
+ const void *data1, const void *data2);
+static inline int ext4_init_new_dir(handle_t *handle, struct inode *dir,
+ struct inode *inode)
+{
+ return ext4_init_new_dir_data(handle, dir, inode, NULL, NULL);
+}
extern int ext4_dirblock_csum_verify(struct inode *inode,
struct buffer_head *bh);
extern int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,
@@ -4278,6 +4307,42 @@ static inline unsigned int ext4_dir_entry_len(struct ext4_dir_entry_2 *de,
ext4_dirent_rec_len(de->name_len, dir));
}
+/*
+ * ext4_dirent_new_data_len() - dirdata length for a new entry in @dir
+ * @dir: directory inode
+ * @dfid: LUFID record to store, or NULL
+ *
+ * Returns the bytes of dirdata records (without the NUL byte after the
+ * name) that ext4_dirdata_set() will write: the LUFID record, and on a
+ * dirdata filesystem the casefold+fscrypt hash as a CFHASH record.
+ */
+static inline unsigned int
+ext4_dirent_new_data_len(const struct inode *dir,
+ const struct ext4_dirent_fid *dfid)
+{
+ unsigned int dlen = dfid ? dfid->df_header.ddh_length : 0;
+
+ if (ext4_hash_in_dirent(dir) && ext4_has_feature_dirdata(dir->i_sb))
+ dlen += sizeof(struct ext4_dirent_hash);
+ return dlen;
+}
+
+/*
+ * ext4_dirent_new_rec_len() - record length for a new entry in @dir
+ * @dir: directory inode
+ * @name_len: length of the name
+ * @dlen: dirdata length from ext4_dirent_new_data_len()
+ */
+static inline unsigned int ext4_dirent_new_rec_len(const struct inode *dir,
+ unsigned int name_len,
+ unsigned int dlen)
+{
+ /* With dirdata the hash, if any, is a record counted in @dlen */
+ if (dlen)
+ return ext4_dirent_rec_len(name_len + 1 + dlen, NULL);
+ return ext4_dirent_rec_len(name_len, dir);
+}
+
extern const struct iomap_ops ext4_iomap_ops;
extern const struct iomap_ops ext4_iomap_report_ops;
diff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c
index 0cac890cf370..876a863d6c01 100644
--- a/fs/ext4/fast_commit.c
+++ b/fs/ext4/fast_commit.c
@@ -2686,6 +2686,7 @@ static const char * const fc_ineligible_reasons[] = {
[EXT4_FC_REASON_MIGRATE] = "Inode format migration",
[EXT4_FC_REASON_VERITY] = "fs-verity enable",
[EXT4_FC_REASON_MOVE_EXT] = "Move extents",
+ [EXT4_FC_REASON_DIRDATA] = "Dirdata LUFID (not recorded by fast-commit)",
};
int ext4_fc_info_show(struct seq_file *seq, void *v)
diff --git a/fs/ext4/fast_commit.h b/fs/ext4/fast_commit.h
index 2f77a37fb101..899faeb4e6e0 100644
--- a/fs/ext4/fast_commit.h
+++ b/fs/ext4/fast_commit.h
@@ -100,6 +100,7 @@ enum {
EXT4_FC_REASON_MIGRATE,
EXT4_FC_REASON_VERITY,
EXT4_FC_REASON_MOVE_EXT,
+ EXT4_FC_REASON_DIRDATA,
EXT4_FC_REASON_MAX
};
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index bdf6f45a3de4..81e0ef1c8833 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -984,12 +984,12 @@ static int ext4_add_dirent_to_inline(handle_t *handle,
{
int err;
struct ext4_dir_entry_2 *de;
+ /* fname->dfid was set in ext4_add_entry() */
+ struct ext4_dirent_fid *dfid = fname->dfid;
+ unsigned int dlen = ext4_dirent_new_data_len(dir, dfid);
- /* 'dlen' reserves space for dirdata extensions (LUFID, CFHASH) in the
- * slot. The dirdata types are defined in the next patch; until then
- * dlen stays 0 and ext4_find_dest_de() ignores dirdata overhead. */
err = ext4_find_dest_de(dir, iloc->bh, inline_start,
- inline_size, fname, &de, 0);
+ inline_size, fname, &de, dlen);
if (err)
return err;
@@ -998,7 +998,7 @@ static int ext4_add_dirent_to_inline(handle_t *handle,
EXT4_JTR_NONE);
if (err)
return err;
- ext4_insert_dentry(dir, inode, de, inline_size, fname);
+ ext4_insert_dentry_data(dir, inode, de, inline_size, fname, dfid);
ext4_show_inline_dir(dir, iloc->bh, inline_start, inline_size);
@@ -1276,12 +1276,14 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
int err = 0, count = 0;
unsigned int parent_ino;
int pos;
+ unsigned int de_len;
struct ext4_dir_entry_2 *de;
struct inode *inode = file_inode(dir_file);
int ret, inline_size = 0;
struct ext4_iloc iloc;
void *dir_buf = NULL;
- struct ext4_dir_entry_2 fake;
+ /* zeroed: without the filetype feature, file_type is never set */
+ struct ext4_dir_entry_2 fake = {};
struct fscrypt_str tmp_str;
ret = ext4_get_inode_loc(inode, &iloc);
@@ -1337,14 +1339,24 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
de = &fake;
pos = EXT4_INLINE_DOTDOT_SIZE;
} else {
+ if (pos + EXT4_BASE_DIR_LEN > inline_size) {
+ ret = count;
+ goto out;
+ }
de = (struct ext4_dir_entry_2 *)(dir_buf + pos);
- pos += ext4_rec_len_from_disk(de->rec_len, inline_size);
+ de_len = ext4_rec_len_from_disk(de->rec_len, inline_size);
+ if (de_len < EXT4_BASE_DIR_LEN ||
+ pos + de_len > (unsigned int)inline_size) {
+ ret = count;
+ goto out;
+ }
if (ext4_check_dir_entry(inode, dir_file, de,
iloc.bh, dir_buf,
inline_size, pos)) {
ret = count;
goto out;
}
+ pos += de_len;
}
if (ext4_hash_in_dirent(dir)) {
@@ -1366,7 +1378,8 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
tmp_str.name = de->name;
tmp_str.len = de->name_len;
err = ext4_htree_store_dirent(dir_file, hinfo->hash,
- hinfo->minor_hash, de, &tmp_str);
+ hinfo->minor_hash, inline_size,
+ de, &tmp_str);
if (err) {
ret = err;
goto out;
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 0c91e4da00fb..4f0465d49887 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -402,23 +402,33 @@ 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;
+ /* 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) +
+ sizeof(struct dx_countlimit) > blocksize)
return NULL;
- root = (struct dx_root_info *)(((void *)de + 12));
+ 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;
@@ -763,7 +773,9 @@ static struct stats dx_show_leaf(struct inode *dir,
(unsigned) ((char *) de - base));
#endif
}
- space += ext4_dirent_rec_len(de->name_len, dir);
+ if ((char *)de + EXT4_BASE_DIR_LEN > base + size)
+ break;
+ space += ext4_dir_entry_len(de, size, dir);
names++;
}
de = ext4_next_entry(de, size);
@@ -1192,8 +1204,8 @@ static int htree_dirblock_to_tree(struct file *dir_file,
tmp_str.name = de->name;
tmp_str.len = de->name_len;
err = ext4_htree_store_dirent(dir_file,
- hinfo->hash, hinfo->minor_hash, de,
- &tmp_str);
+ hinfo->hash, hinfo->minor_hash,
+ dir->i_sb->s_blocksize, de, &tmp_str);
} else {
int save_len = fname_crypto_str.len;
struct fscrypt_str de_name = FSTR_INIT(de->name,
@@ -1208,8 +1220,9 @@ static int htree_dirblock_to_tree(struct file *dir_file,
goto errout;
}
err = ext4_htree_store_dirent(dir_file,
- hinfo->hash, hinfo->minor_hash, de,
- &fname_crypto_str);
+ hinfo->hash, hinfo->minor_hash,
+ dir->i_sb->s_blocksize, de,
+ &fname_crypto_str);
fname_crypto_str.len = save_len;
}
if (err != 0) {
@@ -1287,6 +1300,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;
@@ -1298,6 +1312,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;
@@ -2126,7 +2141,8 @@ int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
int dlen)
{
struct ext4_dir_entry_2 *de;
- unsigned short reclen = ext4_dirent_rec_len(fname_len(fname) + dlen, dir);
+ unsigned short reclen = ext4_dirent_new_rec_len(dir, fname_len(fname),
+ dlen);
int nlen, rlen;
unsigned int offset = 0;
char *top;
@@ -2153,13 +2169,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);
@@ -2176,6 +2189,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;
@@ -2200,14 +2217,20 @@ static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,
{
unsigned int blocksize = dir->i_sb->s_blocksize;
int csum_size = 0;
- int err, err2;
+ int err, err2, dlen = 0;
+ struct ext4_dirent_fid *dfid;
if (ext4_has_feature_metadata_csum(inode->i_sb))
csum_size = sizeof(struct ext4_dir_entry_tail);
+ /* dfid is resolved once by ext4_add_entry() and stored in fname so
+ * that all add_dirent_to_buf() calls within the same add operation
+ * use a consistent value without touching the shared i_dirdata field. */
+ dfid = fname->dfid;
+ dlen = ext4_dirent_new_data_len(dir, dfid);
if (!de) {
err = ext4_find_dest_de(dir, bh, bh->b_data,
- blocksize - csum_size, fname, &de, 0);
+ blocksize - csum_size, fname, &de, dlen);
if (err)
return err;
} else {
@@ -2221,8 +2244,8 @@ static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,
unsigned int rlen = ext4_rec_len_from_disk(de->rec_len,
blocksize);
- if (nlen > rlen ||
- rlen - nlen < ext4_dirent_rec_len(fname_len(fname), dir)) {
+ if (nlen > rlen || rlen - nlen <
+ ext4_dirent_new_rec_len(dir, fname_len(fname), dlen)) {
EXT4_ERROR_INODE(dir,
"no room for new entry in split block %llu",
(unsigned long long)bh->b_blocknr);
@@ -2238,7 +2261,7 @@ static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,
}
/* By now the buffer is marked for journaling */
- ext4_insert_dentry(dir, inode, de, blocksize, fname);
+ ext4_insert_dentry_data(dir, inode, de, blocksize, fname, dfid);
/*
* XXX shouldn't update any times until successful
@@ -2467,7 +2490,8 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
*/
static int __ext4_add_entry(handle_t *handle, struct inode *dir,
const struct qstr *d_name,
- struct inode *inode)
+ struct inode *inode,
+ struct ext4_dentry_param *edp)
{
struct buffer_head *bh = NULL;
struct ext4_dir_entry_2 *de;
@@ -2492,6 +2516,13 @@ static int __ext4_add_entry(handle_t *handle, struct inode *dir,
if (retval)
return retval;
+ /* Resolve the LUFID payload once here and store it in fname so every
+ * add_dirent_to_buf() call in this add operation uses the same value
+ * without touching the shared EXT4_I(inode)->i_dirdata field. */
+ fname.dfid = ext4_dentry_get_fid(sb, edp);
+ if (fname.dfid)
+ ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_DIRDATA, handle);
+
if (ext4_has_inline_data(dir)) {
retval = ext4_try_add_inline_entry(handle, &fname, dir, inode);
if (retval < 0)
@@ -2576,7 +2607,8 @@ static int ext4_add_entry(handle_t *handle, struct dentry *dentry,
if (fscrypt_is_nokey_name(dentry))
return -ENOKEY;
- return __ext4_add_entry(handle, dir, &dentry->d_name, inode);
+ return __ext4_add_entry(handle, dir, &dentry->d_name, inode,
+ dentry->d_fsdata);
}
/*
@@ -3084,8 +3116,9 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,
return ext4_handle_dirty_dirblock(handle, inode, bh);
}
-int ext4_init_new_dir(handle_t *handle, struct inode *dir,
- struct inode *inode)
+int ext4_init_new_dir_data(handle_t *handle, struct inode *dir,
+ struct inode *inode,
+ const void *data1, const void *data2)
{
struct buffer_head *dir_block = NULL;
ext4_lblk_t block = 0;
@@ -3586,10 +3619,16 @@ int __ext4_link(struct inode *dir, struct inode *inode,
if (IS_DIRSYNC(dir))
ext4_handle_sync(handle);
+ /* Fast-commit does not record dirdata in its journal format; force a
+ * full journal commit so the LUFID survives crash+replay. */
+ if (ext4_dentry_get_fid(dir->i_sb, dentry ? dentry->d_fsdata : NULL))
+ ext4_fc_mark_ineligible(dir->i_sb, EXT4_FC_REASON_DIRDATA, handle);
+
inode_set_ctime_current(inode);
ext4_inc_count(inode);
- err = __ext4_add_entry(handle, dir, d_name, inode);
+ err = __ext4_add_entry(handle, dir, d_name, inode,
+ dentry ? dentry->d_fsdata : NULL);
if (!err) {
err = ext4_mark_inode_dirty(handle, inode);
/* this can happen only for tmpfile being
diff --git a/fs/ext4/sysfs.c b/fs/ext4/sysfs.c
index 923b375e017f..80074fb15ee9 100644
--- a/fs/ext4/sysfs.c
+++ b/fs/ext4/sysfs.c
@@ -362,6 +362,7 @@ EXT4_ATTR_FEATURE(verity);
#endif
EXT4_ATTR_FEATURE(metadata_csum_seed);
EXT4_ATTR_FEATURE(fast_commit);
+EXT4_ATTR_FEATURE(dirdata);
#if IS_ENABLED(CONFIG_UNICODE) && defined(CONFIG_FS_ENCRYPTION)
EXT4_ATTR_FEATURE(encrypted_casefold);
#endif
@@ -385,6 +386,7 @@ static struct attribute *ext4_feat_attrs[] = {
#endif
ATTR_LIST(metadata_csum_seed),
ATTR_LIST(fast_commit),
+ ATTR_LIST(dirdata),
#if IS_ENABLED(CONFIG_UNICODE) && defined(CONFIG_FS_ENCRYPTION)
ATTR_LIST(encrypted_casefold),
#endif
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v14 09/12] ext4: dirdata feature
2026-11-03 13:49 ` [PATCH v14 09/12] ext4: dirdata feature Artem Blagodarenko
@ 2026-11-03 14:27 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-03 14:27 UTC (permalink / raw)
To: Artem Blagodarenko, linux-ext4; +Cc: tytso
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] ext4: integer underflow and buffer overflow in make_indexed_dir()
- [High] ext4: inconsistent dx_root_info offset in make_indexed_dir()
- [Medium] ext4: malformed dirdata ignored in ext4_dirent_get_data_len()
- [Medium] ext4: silent corruption ignore in ext4_inlinedir_to_tree()
- [Low] ext4: dead dirdata allocation and copy in ext4_htree_store_dirent()
--
commit fdb43b700545f38bf31e5431287beaf2f0e358b1
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>
ext4: dirdata feature
This commit enables the EXT4_FEATURE_INCOMPAT_DIRDATA feature flag and
updates directory entry sizing, insertion, and htree handling 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);
> + }
> +
> + /* exta_data contains +1 byte for NULL */
> + new_fn = kzalloc_flex(*new_fn, name, ent_name->len +
> + (extra_data ? extra_data : 1));
> if (!new_fn)
> return -ENOMEM;
> new_fn->hash = hash;
> new_fn->minor_hash = minor_hash;
> new_fn->inode = le32_to_cpu(dirent->inode);
> - new_fn->name_len = ent_name->len;
> new_fn->file_type = dirent->file_type;
> + new_fn->name_len = ent_name->len;
> memcpy(new_fn->name, ent_name->name, ent_name->len);
> + if (extra_data)
> + /* Dirdata bytes lie past the __counted_by(name_len) bound but
> + * within the kzalloc_flex allocation; use unsafe_memcpy to
> + * avoid a FORTIFY_SOURCE false positive. */
> + unsafe_memcpy(new_fn->name + ent_name->len,
> + dirent->name + dirent->name_len, extra_data,
> + /* within kzalloc_flex allocation */);
[Severity: Low]
Is there any consumer of the extra_data bytes copied into new_fn->name?
In fs/ext4/dir.c, struct fname is defined privately with:
__u8 name_len;
__u8 file_type;
char name[] __counted_by(name_len);
and its only reader is call_filldir(), which only passes fname->name and
fname->name_len to dir_emit():
while (fname) {
if (!dir_emit(ctx, fname->name,
fname->name_len,
fname->inode,
get_dtype(sb, fname->file_type))) {
info->extra_fname = fname;
return 1;
}
fname = fname->next;
}
In addition, kzalloc_flex() internally runs __set_flex_counter() to store
ent_name->len + extra_data into __u8 new_fn->name_len (which truncates if
the sum exceeds 255) before new_fn->name_len = ent_name->len overwrites it.
Since nothing reads the trailing dirdata bytes from struct fname, can the
extra_data allocation, unsafe_memcpy(), and the buf_size parameter to
ext4_htree_store_dirent() be dropped?
> diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
> index e42e5bcc2f28f..4a7b36e1910ee 100644
> --- a/fs/ext4/ext4.h
> +++ b/fs/ext4/ext4.h
> @@ -2369,6 +2369,7 @@ EXT4_FEATURE_INCOMPAT_FUNCS(casefold, CASEFOLD)
> EXT4_FEATURE_INCOMPAT_FLEX_BG| \
> EXT4_FEATURE_INCOMPAT_EA_INODE| \
> EXT4_FEATURE_INCOMPAT_MMP | \
> + EXT4_FEATURE_INCOMPAT_DIRDATA | \
> EXT4_FEATURE_INCOMPAT_INLINE_DATA | \
> EXT4_FEATURE_INCOMPAT_ENCRYPT | \
> EXT4_FEATURE_INCOMPAT_CASEFOLD | \
[ ... ]
> @@ -4278,6 +4307,42 @@ static inline unsigned int ext4_dir_entry_len(struct ext4_dir_entry_2 *de,
> ext4_dirent_rec_len(de->name_len, dir));
> }
[Severity: Medium]
With EXT4_FEATURE_INCOMPAT_DIRDATA now enabled in EXT4_FEATURE_INCOMPAT_SUPP,
what happens when a directory entry has dirdata flags set in de->file_type
(~EXT4_FT_MASK), but a dirdata header is truncated, zero-length, or exceeds
the entry's current rec_len?
In ext4_dirent_get_data_len():
if (offset >= rec_len)
return 0;
...
while (extra_data_flags) {
if (extra_data_flags & 1) {
if (offset + sizeof(*ddh) > rec_len)
return 0;
{
u8 ddh_len = READ_ONCE(ddh->ddh_length);
if (ddh_len == 0 || ddh_len > rec_len - offset)
return 0;
returning 0 on malformed dirdata causes ext4_dir_entry_len() to return the
bare filename record length, so __ext4_check_dir_entry() accepts the entry:
else if (unlikely(rlen < ext4_dir_entry_len(de, size, fake ? NULL : dir)))
error_msg = "rec_len is too small for name_len";
This seems to allow two forms of corruption on subsequent directory
modifications:
1. Because ext4_dirent_get_data_len() depends on the entry's current
rec_len, expanding rec_len later (for example, in make_indexed_dir(),
do_split(), or ext4_generic_delete_entry()) can cause a previously
out-of-bounds ddh_length byte in trailing space to become <= rec_len - offset,
increasing ext4_dir_entry_len(de, ...) after the entry was already validated.
2. If an entry has multiple dirdata flags set (such as EXT4_DIRENT_LUFID |
EXT4_DIRENT_CFHASH) where the first record is valid and the second has an
invalid ddh_length, ext4_dirent_get_data_len() returns 0 and drops the first
record's length as well. When ext4_insert_dentry_data(), dx_move_dirents(),
or dx_pack_dirents() then splits or compacts the entry using
ext4_dir_entry_len(), can it shrink de->rec_len to the bare filename length
and overwrite the valid first record while leaving the dirdata flags set in
de->file_type?
> diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
> index bdf6f45a3de48..81e0ef1c88337 100644
> --- a/fs/ext4/inline.c
> +++ b/fs/ext4/inline.c
[ ... ]
> @@ -1337,14 +1339,24 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
> de = &fake;
> pos = EXT4_INLINE_DOTDOT_SIZE;
> } else {
> + if (pos + EXT4_BASE_DIR_LEN > inline_size) {
> + ret = count;
> + goto out;
> + }
> de = (struct ext4_dir_entry_2 *)(dir_buf + pos);
> - pos += ext4_rec_len_from_disk(de->rec_len, inline_size);
> + de_len = ext4_rec_len_from_disk(de->rec_len, inline_size);
> + if (de_len < EXT4_BASE_DIR_LEN ||
> + pos + de_len > (unsigned int)inline_size) {
> + ret = count;
> + goto out;
> + }
> if (ext4_check_dir_entry(inode, dir_file, de,
> iloc.bh, dir_buf,
> inline_size, pos)) {
> ret = count;
> goto out;
> }
> + pos += de_len;
> }
[Severity: Medium]
Does checking de_len < EXT4_BASE_DIR_LEN and pos + de_len > inline_size
before ext4_check_dir_entry() suppress filesystem corruption reporting in
ext4_inlinedir_to_tree()?
In __ext4_check_dir_entry(), rlen < ext4_dirent_rec_len(1, ...),
rlen % 4 != 0, and next_offset > size are already checked before calling
ext4_dir_entry_len(), and any failure logs the error via ext4_error_file()
and triggers the filesystem's error policy:
if (unlikely(rlen < ext4_dirent_rec_len(1, fake ? NULL : dir)))
error_msg = "rec_len is smaller than minimal";
else if (unlikely(rlen % 4 != 0))
error_msg = "rec_len % 4 != 0";
else if (unlikely(next_offset > size))
error_msg = "directory entry overrun";
else if (unlikely(rlen < ext4_dir_entry_len(de, size, fake ? NULL : dir)))
error_msg = "rec_len is too small for name_len";
Because ext4_rec_len_from_disk(de->rec_len, inline_size) maps rec_len == 0
or 0xffff to inline_size (making pos + de_len > inline_size since pos >= 4)
and shifts unaligned low bits by 16 (making de_len >= 65536 > inline_size),
jumping to out on de_len < EXT4_BASE_DIR_LEN or pos + de_len > inline_size
before calling ext4_check_dir_entry() causes corrupted inline directory
entries (such as rec_len == 0, rec_len == 4, unaligned rec_len, or entry
overruns) to silently return a partial entry count without calling
ext4_check_dir_entry().
> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index 0c91e4da00fbc..4f0465d498877 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c
> @@ -402,23 +402,33 @@ 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;
> - root = (struct dx_root_info *)(((void *)de + 12));
> + 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;
> + /* 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) +
> + sizeof(struct dx_countlimit) > 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]
Can make_indexed_dir() and get_dx_countlimit() disagree on the offset of
dx_root_info when converting a single-block directory to an htree-indexed
directory?
In __ext4_add_entry() -> make_indexed_dir(), dx_get_dx_info(dir, dot_de) is
first called before dotdot_de->rec_len is expanded:
/* Find room for the hashing info before the directory is changed */
dx_info = dx_get_dx_info(dir, dot_de);
...
memcpy(data2, de, len);
memset(de, 0, len); /* wipe old data */
...
/* Initialize the root; the dot dirents already exist */
dotdot_de->rec_len =
ext4_rec_len_to_disk(blocksize - le16_to_cpu(dot_de->rec_len),
blocksize);
...
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, dot_de));
If dotdot_de initially has rec_len == 12, a dirdata flag set in
dotdot_de->file_type, and a byte at offset 11 of dotdot_de (ddh_length) > 1,
then during the first dx_get_dx_info(dir, dot_de) call,
ext4_dirent_get_data_len(dotdot_de, 12) sees ddh_len > 12 - 11 and returns 0.
Thus dotdot_rec_len is 12 and dx_info is placed immediately after dotdot_de
(at offset 24), while memset(de, 0, len) only wipes from offset 24 onwards,
leaving byte 11 of dotdot_de untouched.
Once dotdot_de->rec_len is expanded to blocksize - le16_to_cpu(dot_de->rec_len),
calling dx_root_limit(dir, dot_de) -> dx_get_dx_info(dir, dot_de) and
ext4_handle_dirty_dx_node() -> get_dx_countlimit() evaluates
ext4_dir_entry_len(dotdot_de, blocksize, NULL) with the expanded rec_len.
Now ddh_len <= rec_len - 11 is satisfied, so ext4_dir_entry_len() returns a
larger dotdot_rec_len and looks for dx_root_info and dx_countlimit at a
shifted offset instead of where make_indexed_dir() just wrote dx_info and
entries.
[ ... ]
> @@ -2467,7 +2490,8 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
> */
> static int __ext4_add_entry(handle_t *handle, struct inode *dir,
> const struct qstr *d_name,
> - struct inode *inode)
> + struct inode *inode,
> + struct ext4_dentry_param *edp)
[Severity: Critical]
Can len underflow in make_indexed_dir() when metadata_csum is enabled and
dot_de / dotdot_de carry dirdata or large rec_len values?
In __ext4_add_entry(), when add_dirent_to_buf() -> ext4_find_dest_de()
searches block 0 of a 1-block directory with metadata_csum enabled
(csum_size = 12), top is set to:
top = buf + buf_size - reclen;
where buf_size is blocksize - csum_size. If dot_de at offset 0 has a large
rec_len filled by dirdata (for example, blocksize = 1024, buf_size = 1012,
dot_de rec_len = 808 with dirdata nlen = 808) and a new entry has
reclen = 208 (so top = buf + 804), ext4_find_dest_de() checks dot_de on the
first iteration, advances de to buf + 808 > top, and returns -ENOSPC without
ever validating dotdot_de at offset 808.
__ext4_add_entry() then calls make_indexed_dir(), where ext4_check_dx_root()
and dx_get_dx_info() check dotdot_de against blocksize without accounting for
csum_size at the end of the block:
de = ext4_next_entry(dotdot_de, blocksize);
if ((char *)de >= (((char *)dot_de) + blocksize)) {
error_msg = "invalid rec_len for '..'";
goto corrupted;
}
If dotdot_de ends in the checksum tail region between blocksize - csum_size
and blocksize (for example, offset 808 + rec_len 212 = 1020 > 1012), both
ext4_check_dx_root() and dx_get_dx_info() succeed, and make_indexed_dir()
computes an unsigned len:
/* The 0th block becomes the root, move the dirents out */
len = ((char *)dot_de) + (blocksize - csum_size) - (char *)de;
...
memcpy(data2, de, len);
memset(de, 0, len); /* wipe old data */
Because (char *)de > (char *)dot_de + (blocksize - csum_size), unsigned len
underflows (e.g. 1012 - 1020 = 0xfffffff8) and overflows data2 (bh2->b_data)
in memcpy() and de (bh->b_data) in memset(). Even when (char *)de equals
(char *)dot_de + (blocksize - csum_size), len becomes 0, leaving bh2 with no
directory entries for do_split().
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103134921.610047-1-ablagodarenko@thelustrecollective.com?part=9
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v14 10/12] ext4: add dirdata LUFID support for directory entry rename
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
` (8 preceding siblings ...)
2026-11-03 13:49 ` [PATCH v14 09/12] ext4: dirdata feature Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 14:17 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 11/12] ext4: add dirdata set/get helpers Artem Blagodarenko
2026-11-03 13:49 ` [PATCH v14 12/12] ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries Artem Blagodarenko
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4; +Cc: tytso, jack, adilger.kernel, ablagodarenko, Artem Blagodarenko
ext4_setent() updates the inode number and file_type in an existing
directory entry but did not handle the dirdata extension payload.
Extend it with a src_fid parameter so that the source LUFID travels
with the inode through rename.
When the destination entry has a LUFID record of the same size, the
source record is copied over it. Otherwise the LUFID record of the
destination is removed, moving any records that follow it down, and
the source record is added in place if the entry has room for it. If
it has not, the entry is left without a LUFID.
CFHASH is a property of the destination name, not of the inode, so it
is kept.
Both ext4_rename() and ext4_cross_rename() snapshot the source LUFID
into a stack buffer before any setent() call, so that a cross-directory
swap reads both sides before either is overwritten. When a
RENAME_WHITEOUT rename fails after the old entry became the whiteout,
ext4_resetent() puts the snapshot back into the old entry.
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Assisted-by: Claude:claude-opus-5-5
---
fs/ext4/namei.c | 257 ++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 239 insertions(+), 18 deletions(-)
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 4f0465d49887..bd0f4760f1e3 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -3619,11 +3619,6 @@ int __ext4_link(struct inode *dir, struct inode *inode,
if (IS_DIRSYNC(dir))
ext4_handle_sync(handle);
- /* Fast-commit does not record dirdata in its journal format; force a
- * full journal commit so the LUFID survives crash+replay. */
- if (ext4_dentry_get_fid(dir->i_sb, dentry ? dentry->d_fsdata : NULL))
- ext4_fc_mark_ineligible(dir->i_sb, EXT4_FC_REASON_DIRDATA, handle);
-
inode_set_ctime_current(inode);
ext4_inc_count(inode);
@@ -3796,8 +3791,81 @@ static int ext4_rename_dir_finish(handle_t *handle, struct ext4_renament *ent,
return 0;
}
+/*
+ * ext4_dirent_drop_lufid() - remove the LUFID record from a directory entry
+ * @de: directory entry
+ * @rec_len: decoded rec_len of @de
+ *
+ * The records that follow LUFID (such as CFHASH) are moved down so they
+ * stay where the parser expects them. If the records cannot be parsed,
+ * none of them can be trusted and all dirdata flags are cleared.
+ */
+static void ext4_dirent_drop_lufid(struct ext4_dir_entry_2 *de,
+ unsigned int rec_len)
+{
+ unsigned int off = EXT4_BASE_DIR_LEN + de->name_len + 1;
+ unsigned int dlen, end, llen;
+
+ if (!(de->file_type & EXT4_DIRENT_LUFID))
+ return;
+ dlen = ext4_dirent_get_data_len(de, rec_len);
+ if (!dlen) {
+ de->file_type &= EXT4_FT_MASK;
+ return;
+ }
+ /* ext4_dirent_get_data_len() checked every record against rec_len */
+ llen = *((u8 *)de + off);
+ end = EXT4_BASE_DIR_LEN + de->name_len + dlen;
+ memmove((char *)de + off, (char *)de + off + llen, end - off - llen);
+ de->file_type &= ~EXT4_DIRENT_LUFID;
+}
+
+/*
+ * ext4_dirent_add_lufid() - add a LUFID record to a directory entry in place
+ * @dir: directory inode
+ * @de: directory entry without a LUFID record
+ * @rec_len: decoded rec_len of @de
+ * @fid: LUFID record to add
+ *
+ * LUFID is the first record, so any records already present are moved up.
+ * Returns false, leaving @de untouched, when the entry has no room.
+ */
+static bool ext4_dirent_add_lufid(struct inode *dir,
+ struct ext4_dir_entry_2 *de,
+ unsigned int rec_len,
+ const struct ext4_dirent_fid *fid)
+{
+ unsigned int off = EXT4_BASE_DIR_LEN + de->name_len + 1;
+ unsigned int flen = fid->df_header.ddh_length;
+ unsigned int dlen = ext4_dirent_get_data_len(de, rec_len);
+ unsigned int end;
+
+ if ((de->file_type & ~EXT4_FT_MASK) && !dlen)
+ return false;
+ /* without a CFHASH record the hash sits right after the name */
+ if (ext4_hash_in_dirent(dir) && !(de->file_type & EXT4_DIRENT_CFHASH))
+ return false;
+ end = dlen ? EXT4_BASE_DIR_LEN + de->name_len + dlen : off;
+ if (end + flen > rec_len)
+ return false;
+ memmove((char *)de + off + flen, (char *)de + off, end - off);
+ *((char *)de + EXT4_BASE_DIR_LEN + de->name_len) = 0;
+ memcpy((char *)de + off, fid, flen);
+ de->file_type |= EXT4_DIRENT_LUFID;
+ return true;
+}
+
+/*
+ * ext4_setent() - point a directory entry at another inode
+ * @src_fid: LUFID record of that inode, or NULL
+ *
+ * LUFID is inode-specific, so the record of @ent is replaced by @src_fid.
+ * Other records (CFHASH) belong to the name and are kept. If the entry
+ * has no room for @src_fid, the entry is left without a LUFID.
+ */
static int ext4_setent(handle_t *handle, struct ext4_renament *ent,
- unsigned ino, unsigned file_type)
+ unsigned ino, unsigned file_type,
+ const struct ext4_dirent_fid *src_fid)
{
int retval, retval2;
@@ -3807,8 +3875,33 @@ static int ext4_setent(handle_t *handle, struct ext4_renament *ent,
if (retval)
return retval;
ent->de->inode = cpu_to_le32(ino);
- if (ext4_has_feature_filetype(ent->dir->i_sb))
- ent->de->file_type = file_type;
+ if (ext4_has_feature_filetype(ent->dir->i_sb)) {
+ if (ext4_has_feature_dirdata(ent->dir->i_sb)) {
+ unsigned int rec_len =
+ ext4_rec_len_from_disk(ent->de->rec_len,
+ ent->dir->i_sb->s_blocksize);
+ unsigned int off =
+ EXT4_BASE_DIR_LEN + ent->de->name_len + 1;
+ struct ext4_dirent_fid *fid =
+ (struct ext4_dirent_fid *)((char *)ent->de + off);
+
+ if (src_fid &&
+ (ent->de->file_type & EXT4_DIRENT_LUFID) &&
+ ext4_dirent_get_data_len(ent->de, rec_len) &&
+ fid->df_header.ddh_length ==
+ src_fid->df_header.ddh_length) {
+ memcpy(fid, src_fid,
+ src_fid->df_header.ddh_length);
+ } else {
+ ext4_dirent_drop_lufid(ent->de, rec_len);
+ if (src_fid)
+ ext4_dirent_add_lufid(ent->dir, ent->de,
+ rec_len, src_fid);
+ }
+ }
+ ent->de->file_type = (file_type & EXT4_FT_MASK) |
+ (ent->de->file_type & ~EXT4_FT_MASK);
+ }
inode_inc_iversion(ent->dir);
inode_set_mtime_to_ts(ent->dir, inode_set_ctime_current(ent->dir));
retval = ext4_mark_inode_dirty(handle, ent->dir);
@@ -3824,7 +3917,8 @@ static int ext4_setent(handle_t *handle, struct ext4_renament *ent,
}
static void ext4_resetent(handle_t *handle, struct ext4_renament *ent,
- unsigned ino, unsigned file_type)
+ unsigned ino, unsigned file_type,
+ const struct ext4_dirent_fid *fid)
{
struct ext4_renament old = *ent;
int retval = 0;
@@ -3845,7 +3939,7 @@ static void ext4_resetent(handle_t *handle, struct ext4_renament *ent,
return;
}
- ext4_setent(handle, &old, ino, file_type);
+ ext4_setent(handle, &old, ino, file_type, fid);
brelse(old.bh);
}
@@ -3944,6 +4038,69 @@ static struct inode *ext4_whiteout_for_rename(struct mnt_idmap *idmap,
return wh;
}
+/*
+ * Stack buffer sized for 1-byte header + 4 × 16-byte FIDs, covering virtually
+ * all real-world LUFID payloads without a heap allocation. ddh_length is u8
+ * so the absolute maximum is 255 bytes; larger payloads fall back to kmalloc.
+ */
+#define EXT4_LUFID_INLINE_SIZE 65
+
+struct ext4_lufid_snap {
+ struct ext4_dirent_fid *fid; /* NULL or valid; never ERR_PTR */
+ bool on_heap;
+ u8 buf[EXT4_LUFID_INLINE_SIZE];
+};
+
+/*
+ * ext4_lufid_snapshot - copy the LUFID record from a directory entry
+ *
+ * Fills @snap with a copy of the on-disk LUFID record. For payloads up to
+ * EXT4_LUFID_INLINE_SIZE bytes the inline buffer is used; larger payloads
+ * fall back to a heap allocation. Returns 0 on success (snap->fid is NULL
+ * when no LUFID is present), or -ENOMEM when a heap allocation fails.
+ * Call ext4_lufid_snap_release() when done.
+ */
+static int ext4_lufid_snapshot(struct ext4_dir_entry_2 *de,
+ unsigned int blocksize,
+ struct ext4_lufid_snap *snap)
+{
+ unsigned int ddh_off = EXT4_BASE_DIR_LEN + de->name_len + 1;
+ unsigned int rec_len = ext4_rec_len_from_disk(de->rec_len, blocksize);
+ struct ext4_dirent_fid *disk_fid;
+ unsigned int dlen;
+
+ snap->fid = NULL;
+ snap->on_heap = false;
+
+ if (!(de->file_type & EXT4_DIRENT_LUFID))
+ return 0;
+ if (ddh_off + sizeof(disk_fid->df_header) > rec_len)
+ return 0;
+ disk_fid = (struct ext4_dirent_fid *)((char *)de + ddh_off);
+ dlen = READ_ONCE(disk_fid->df_header.ddh_length);
+ if (dlen < sizeof(disk_fid->df_header) || ddh_off + dlen > rec_len)
+ return 0;
+
+ if (dlen <= sizeof(snap->buf)) {
+ memcpy(snap->buf, disk_fid, dlen);
+ snap->fid = (struct ext4_dirent_fid *)snap->buf;
+ } else {
+ snap->fid = kmemdup(disk_fid, dlen, GFP_NOFS);
+ if (!snap->fid)
+ return -ENOMEM;
+ snap->on_heap = true;
+ }
+ /* the length byte may have changed under us: keep the checked one */
+ snap->fid->df_header.ddh_length = dlen;
+ return 0;
+}
+
+static void ext4_lufid_snap_release(struct ext4_lufid_snap *snap)
+{
+ if (snap->on_heap)
+ kfree(snap->fid);
+}
+
/*
* Anybody can rename anything with this: the permission checks are left to the
* higher-level routines.
@@ -3970,8 +4127,10 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
int force_reread;
int retval;
struct inode *whiteout = NULL;
+ struct ext4_dentry_param *edp = NULL;
int credits;
u8 old_file_type;
+ struct ext4_lufid_snap old_snap = {};
if (new.inode && new.inode->i_nlink == 0) {
EXT4_ERROR_INODE(new.inode,
@@ -4077,29 +4236,65 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
force_reread = (new.dir->i_ino == old.dir->i_ino &&
ext4_test_inode_flag(new.dir, EXT4_INODE_INLINE_DATA));
+ if (ext4_has_feature_dirdata(old.dir->i_sb)) {
+ /* Snapshot before whiteout's ext4_setent() clears the LUFID
+ * flag in old.de; the snapshot is also needed for the !new.bh
+ * path below where we must carry the LUFID to the new entry. */
+ retval = ext4_lufid_snapshot(old.de,
+ old.dir->i_sb->s_blocksize,
+ &old_snap);
+ if (retval)
+ goto end_rename;
+ if (!old_snap.fid)
+ old_file_type &= ~EXT4_DIRENT_LUFID;
+ }
+
+ /* Allocate the LUFID wrapper for ext4_add_entry() before any
+ * destructive changes to the old entry. An OOM failure after
+ * ext4_setent() has already compacted the extensions would trigger
+ * the lossy resetent path; pre-allocating here avoids that. */
+ if (!new.bh && old_snap.fid) {
+ unsigned fid_size = old_snap.fid->df_header.ddh_length;
+
+ edp = kmalloc(offsetof(struct ext4_dentry_param,
+ edp_dfid) + fid_size, GFP_NOFS);
+ if (!edp) {
+ retval = -ENOMEM;
+ goto end_rename;
+ }
+ edp->edp_magic = EXT4_LUFID_MAGIC;
+ memcpy(&edp->edp_dfid, old_snap.fid, fid_size);
+ }
+
if (whiteout) {
/*
* Do this before adding a new entry, so the old entry is sure
* to be still pointing to the valid old entry.
*/
retval = ext4_setent(handle, &old, whiteout->i_ino,
- EXT4_FT_CHRDEV);
+ EXT4_FT_CHRDEV, NULL);
if (retval)
goto end_rename;
retval = ext4_mark_inode_dirty(handle, whiteout);
if (unlikely(retval))
goto end_rename;
-
}
if (!new.bh) {
+ new.dentry->d_fsdata = edp;
retval = ext4_add_entry(handle, new.dentry, old.inode);
+ new.dentry->d_fsdata = NULL;
if (retval)
goto end_rename;
} else {
retval = ext4_setent(handle, &new,
- old.inode->i_ino, old_file_type);
+ old.inode->i_ino, old_file_type, old_snap.fid);
if (retval)
goto end_rename;
+ /* ext4_setent() writes the LUFID in-place without going through
+ * __ext4_add_entry(), so mark ineligible here if needed. */
+ if (old_snap.fid)
+ ext4_fc_mark_ineligible(old.dir->i_sb,
+ EXT4_FC_REASON_DIRDATA, handle);
}
if (force_reread)
force_reread = !ext4_test_inode_flag(new.dir,
@@ -4184,10 +4379,11 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
retval = 0;
end_rename:
+ kfree(edp);
if (whiteout) {
if (retval) {
- ext4_resetent(handle, &old,
- old.inode->i_ino, old_file_type);
+ ext4_resetent(handle, &old, old.inode->i_ino,
+ old_file_type, old_snap.fid);
drop_nlink(whiteout);
ext4_mark_inode_dirty(handle, whiteout);
ext4_orphan_add(handle, whiteout);
@@ -4199,6 +4395,7 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
ext4_journal_stop(handle);
}
release_bh:
+ ext4_lufid_snap_release(&old_snap);
brelse(old.dir_bh);
brelse(old.bh);
brelse(new.bh);
@@ -4220,8 +4417,9 @@ static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry,
.dentry = new_dentry,
.inode = d_inode(new_dentry),
};
- u8 new_file_type;
+ u8 new_file_type, old_de_file_type;
int retval;
+ struct ext4_lufid_snap old_snap = {}, new_snap = {};
if ((ext4_test_inode_flag(new_dir, EXT4_INODE_PROJINHERIT) &&
!projid_eq(EXT4_I(new_dir)->i_projid,
@@ -4300,12 +4498,33 @@ static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry,
goto end_rename;
}
+ if (ext4_has_feature_dirdata(old.dir->i_sb)) {
+ retval = ext4_lufid_snapshot(old.de, old.dir->i_sb->s_blocksize,
+ &old_snap);
+ if (retval)
+ goto end_rename;
+ retval = ext4_lufid_snapshot(new.de, new.dir->i_sb->s_blocksize,
+ &new_snap);
+ if (retval)
+ goto end_rename;
+ if (old_snap.fid || new_snap.fid)
+ ext4_fc_mark_ineligible(old.dir->i_sb,
+ EXT4_FC_REASON_DIRDATA, handle);
+ }
+
+ old_de_file_type = old.de->file_type;
+ if (!old_snap.fid)
+ old_de_file_type &= ~EXT4_DIRENT_LUFID;
new_file_type = new.de->file_type;
- retval = ext4_setent(handle, &new, old.inode->i_ino, old.de->file_type);
+ if (!new_snap.fid)
+ new_file_type &= ~EXT4_DIRENT_LUFID;
+ retval = ext4_setent(handle, &new, old.inode->i_ino, old_de_file_type,
+ old_snap.fid);
if (retval)
goto end_rename;
- retval = ext4_setent(handle, &old, new.inode->i_ino, new_file_type);
+ retval = ext4_setent(handle, &old, new.inode->i_ino, new_file_type,
+ new_snap.fid);
if (retval)
goto end_rename;
@@ -4338,6 +4557,8 @@ static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry,
retval = 0;
end_rename:
+ ext4_lufid_snap_release(&old_snap);
+ ext4_lufid_snap_release(&new_snap);
brelse(old.dir_bh);
brelse(new.dir_bh);
brelse(old.bh);
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v14 10/12] ext4: add dirdata LUFID support for directory entry rename
2026-11-03 13:49 ` [PATCH v14 10/12] ext4: add dirdata LUFID support for directory entry rename Artem Blagodarenko
@ 2026-11-03 14:17 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-03 14:17 UTC (permalink / raw)
To: Artem Blagodarenko, linux-ext4; +Cc: tytso
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] ext4: ext4_resetent() drops existing LUFID on pre-whiteout error in ext4_rename()
--
commit 134d71d4fc529ad068469d7e4f0b83ba20d737e8
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>
ext4: add dirdata LUFID support for directory entry rename
This commit extends ext4_setent(), ext4_rename(), and ext4_cross_rename()
to preserve the source directory entry's LUFID dirdata payload across
renames.
> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index 4f0465d498877..bd0f4760f1e31 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c
[ ... ]
> @@ -4077,29 +4236,65 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
> force_reread = (new.dir->i_ino == old.dir->i_ino &&
> ext4_test_inode_flag(new.dir, EXT4_INODE_INLINE_DATA));
>
> + if (ext4_has_feature_dirdata(old.dir->i_sb)) {
> + /* Snapshot before whiteout's ext4_setent() clears the LUFID
> + * flag in old.de; the snapshot is also needed for the !new.bh
> + * path below where we must carry the LUFID to the new entry. */
> + retval = ext4_lufid_snapshot(old.de,
> + old.dir->i_sb->s_blocksize,
> + &old_snap);
> + if (retval)
> + goto end_rename;
> + if (!old_snap.fid)
> + old_file_type &= ~EXT4_DIRENT_LUFID;
> + }
[ ... ]
> @@ -4184,10 +4379,11 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
> retval = 0;
>
> end_rename:
> + kfree(edp);
> if (whiteout) {
> if (retval) {
> - ext4_resetent(handle, &old,
> - old.inode->i_ino, old_file_type);
> + ext4_resetent(handle, &old, old.inode->i_ino,
> + old_file_type, old_snap.fid);
[Severity: High]
Can this drop the existing LUFID from old.de if a RENAME_WHITEOUT rename
fails before old.de is overwritten with the whiteout?
In ext4_rename(), whiteout is allocated before the S_ISDIR(old.inode->i_mode)
checks and before ext4_lufid_snapshot() is called:
fs/ext4/namei.c:ext4_rename() {
...
whiteout = ext4_whiteout_for_rename(idmap, &old, credits, &handle);
...
if (S_ISDIR(old.inode->i_mode)) {
if (new.inode) {
retval = -ENOTEMPTY;
if (!ext4_empty_dir(new.inode))
goto end_rename;
} else {
retval = -EMLINK;
if (new.dir != old.dir && EXT4_DIR_LINK_MAX(new.dir))
goto end_rename;
}
retval = ext4_rename_dir_prepare(handle, &old, new.dir != old.dir);
if (retval)
goto end_rename;
}
...
if (ext4_has_feature_dirdata(old.dir->i_sb)) {
retval = ext4_lufid_snapshot(old.de,
old.dir->i_sb->s_blocksize,
&old_snap);
if (retval)
goto end_rename;
...
}
If any of those directory checks fail (such as -ENOTEMPTY or -EMLINK), or
if kmemdup() in ext4_lufid_snapshot() fails with -ENOMEM on a LUFID larger
than EXT4_LUFID_INLINE_SIZE, execution jumps to end_rename while
old_snap.fid is still NULL and before ext4_setent() has modified old.de.
At end_rename, whiteout is non-NULL, so ext4_resetent() is called with
old_snap.fid == NULL:
ext4_rename()
-> ext4_resetent(handle, &old, old.inode->i_ino, old_file_type, NULL)
-> ext4_setent(handle, &old, ino, file_type, NULL)
-> ext4_dirent_drop_lufid(ent->de, rec_len)
This strips the LUFID record and clears EXT4_DIRENT_LUFID on the untouched
source directory entry, then commits that modification when
ext4_journal_stop(handle) runs.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103134921.610047-1-ablagodarenko@thelustrecollective.com?part=10
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v14 11/12] ext4: add dirdata set/get helpers
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
` (9 preceding siblings ...)
2026-11-03 13:49 ` [PATCH v14 10/12] ext4: add dirdata LUFID support for directory entry rename Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 14:18 ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 12/12] ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries Artem Blagodarenko
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4; +Cc: tytso, jack, adilger.kernel, ablagodarenko, Artem Blagodarenko
From: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Add helpers to set and retrieve dirdata payload and hook them up at
the appropriate call sites.
Enable dirdata for casefold+encryption hashes and storing unique
128-bit file identifier in the directory entry for testing.
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Assisted-by: Claude:claude-opus-5-5
---
fs/ext4/dir.c | 15 ++-
fs/ext4/ext4.h | 4 +
fs/ext4/inline.c | 22 ++--
fs/ext4/namei.c | 322 ++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 307 insertions(+), 56 deletions(-)
diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
index 15816f2a5f47..12df2d2a0997 100644
--- a/fs/ext4/dir.c
+++ b/fs/ext4/dir.c
@@ -302,8 +302,19 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
u32 minor_hash;
if (IS_CASEFOLDED(inode)) {
- hash = EXT4_DIRENT_HASH(de);
- minor_hash = EXT4_DIRENT_MINOR_HASH(de);
+ if (ext4_has_feature_dirdata(sb) &&
+ (de->file_type &
+ EXT4_DIRENT_CFHASH)) {
+ struct dx_hash_info hi = {};
+ ext4_dirdata_get(de, inode,
+ sb->s_blocksize,
+ NULL, &hi);
+ hash = hi.hash;
+ minor_hash = hi.minor_hash;
+ } else {
+ hash = EXT4_DIRENT_HASH(de);
+ minor_hash = EXT4_DIRENT_MINOR_HASH(de);
+ }
} else {
hash = 0;
minor_hash = 0;
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 4a7b36e1910e..32119ab18ac4 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3978,6 +3978,10 @@ extern int __ext4_unlink(struct inode *dir, const struct qstr *d_name,
struct inode *inode, struct dentry *dentry);
extern int __ext4_link(struct inode *dir, struct inode *inode,
const struct qstr *d_name, struct dentry *dentry);
+extern unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de,
+ struct inode *dir, int buf_size,
+ struct ext4_dirent_fid *lufid,
+ struct dx_hash_info *hinfo);
#define S_SHIFT 12
static const unsigned char ext4_type_by_mode[(S_IFMT >> S_SHIFT) + 1] = {
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 81e0ef1c8833..801fa6555ea9 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -1359,14 +1359,20 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
pos += de_len;
}
- if (ext4_hash_in_dirent(dir)) {
- hinfo->hash = EXT4_DIRENT_HASH(de);
- hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
- } else {
- err = ext4fs_dirhash(dir, de->name, de->name_len, hinfo);
- if (err) {
- ret = err;
- goto out;
+ if (!(ext4_dirdata_get(de, dir, inline_size, NULL, hinfo) &
+ EXT4_DIRENT_CFHASH)) {
+ if (ext4_hash_in_dirent(dir)) {
+ /* Un-migrated entry: hash at legacy fixed
+ * offset. */
+ hinfo->hash = EXT4_DIRENT_HASH(de);
+ hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
+ } else {
+ err = ext4fs_dirhash(dir, de->name,
+ de->name_len, hinfo);
+ if (err) {
+ ret = err;
+ goto out;
+ }
}
}
if ((hinfo->hash < start_hash) ||
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index bd0f4760f1e3..8f1a6c000373 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -1178,22 +1178,34 @@ static int htree_dirblock_to_tree(struct file *dir_file,
/* silently ignore the rest of the block */
break;
}
- if (ext4_hash_in_dirent(dir)) {
- if (de->name_len && de->inode) {
- hinfo->hash = EXT4_DIRENT_HASH(de);
- hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
- } else {
- hinfo->hash = 0;
- hinfo->minor_hash = 0;
- }
+ if (de->name_len && de->inode) {
+ /* check for saved hash first, or generate it from name */
+ if (!(ext4_dirdata_get(de, dir, dir->i_sb->s_blocksize,
+ NULL, hinfo) &
+ EXT4_DIRENT_CFHASH)) {
+ if (ext4_hash_in_dirent(dir)) {
+ /* Un-migrated entry: hash is at the
+ * legacy fixed offset, not in a CFHASH
+ * extension. Read it directly rather
+ * than hashing the encrypted name. */
+ hinfo->hash = EXT4_DIRENT_HASH(de);
+ hinfo->minor_hash =
+ EXT4_DIRENT_MINOR_HASH(de);
+ } else {
+ err = ext4fs_dirhash(dir, de->name,
+ de->name_len,
+ hinfo);
+ if (err < 0) {
+ count = err;
+ goto errout;
+ }
+ }
+ }
} else {
- err = ext4fs_dirhash(dir, de->name,
- de->name_len, hinfo);
- if (err < 0) {
- count = err;
- goto errout;
- }
+ hinfo->hash = 0;
+ hinfo->minor_hash = 0;
}
+
if ((hinfo->hash < start_hash) ||
((hinfo->hash == start_hash) &&
(hinfo->minor_hash < start_minor_hash)))
@@ -1373,6 +1385,204 @@ static inline int search_dirblock(struct buffer_head *bh,
* Directory block splitting, compacting
*/
+/*
+ * ext4_dirdata_get() - Read dirdata fields from a directory entry.
+ * @de: directory entry
+ * @dir: directory inode (used for fscrypt+casefold hash fallback)
+ * @dfid: if non-NULL and EXT4_DIRENT_LUFID is set, LUFID data is copied
+ * here
+ * @hinfo: if non-NULL, receives the casefold hash and minor hash
+ *
+ * Reads any dirdata stored in @de. If the dirdata feature is not enabled,
+ * falls back to reading the hash stored inline after the filename (for
+ * compatibility with the older casefold+fscrypt format).
+ *
+ * Returns a bitmask of EXT4_DIRENT_* flags indicating which fields were read.
+ *
+ * Compatibility note: enabling EXT4_FEATURE_INCOMPAT_DIRDATA on a filesystem
+ * that already has casefolded+encrypted directories is NOT safe without a
+ * prior migration pass. Before dirdata, the casefold+fscrypt hash was stored
+ * as a raw 8 bytes immediately after the filename with no flag in file_type.
+ * After dirdata is enabled, this function expects the hash to be present only
+ * when EXT4_DIRENT_CFHASH (0x40) is set in file_type; existing entries that
+ * carry the raw hash are silently misread as having no hash at all, breaking
+ * directory lookups. e2fsck must be run to convert all affected entries to
+ * the EXT4_DIRENT_CFHASH extension format before the feature flag is set with
+ * tune2fs. Detection heuristic: in a casefold+encrypted directory, an entry
+ * with rec_len >= round_up(name_len, 4) + 8 and no EXT4_DIRENT_CFHASH bit
+ * carries a raw pre-dirdata hash that must be migrated.
+ */
+unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de, struct inode *dir,
+ int buf_size,
+ struct ext4_dirent_fid *dfid,
+ struct dx_hash_info *hinfo)
+{
+ unsigned char ret = 0;
+ unsigned int data_offset = de->name_len + 1;
+ unsigned int rec_len = ext4_rec_len_from_disk(de->rec_len, buf_size);
+
+ /* data_offset is relative to de->name, which itself starts
+ * EXT4_BASE_DIR_LEN bytes into the entry -- rec_len is relative to
+ * the start of the entry, so add the header size before comparing,
+ * or this lets reads run EXT4_BASE_DIR_LEN bytes past the entry. */
+ if (EXT4_BASE_DIR_LEN + data_offset > rec_len)
+ return ret;
+
+ /* compatibility: hash stored inline after filename (no dirdata) */
+ if (hinfo && !ext4_has_feature_dirdata(dir->i_sb) &&
+ ext4_hash_in_dirent(dir)) {
+ if (ext4_dirent_rec_len(de->name_len, dir) > rec_len)
+ return ret;
+ hinfo->hash = EXT4_DIRENT_HASH(de);
+ hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
+ ret |= EXT4_DIRENT_CFHASH;
+ return ret;
+ }
+
+ /* EXT4_DIRENT_* bits are only meaningful when the feature is enabled */
+ if (!ext4_has_feature_dirdata(dir->i_sb))
+ return ret;
+
+ if (de->file_type & EXT4_DIRENT_LUFID) {
+ struct ext4_dirent_fid *disk_fid =
+ (struct ext4_dirent_fid *)((char *)de +
+ EXT4_BASE_DIR_LEN + data_offset);
+ unsigned int dlen;
+ /* struct ext4_fid df_fid[] does not provide the array size.
+ * First, verify that the header lies within the valid area, then
+ * verify that the entire record fits within it. */
+ if (EXT4_BASE_DIR_LEN + data_offset +
+ sizeof(disk_fid->df_header) > rec_len)
+ return ret;
+
+ dlen = disk_fid->df_header.ddh_length;
+ if (dlen == 0 ||
+ EXT4_BASE_DIR_LEN + data_offset + dlen > rec_len)
+ return ret;
+
+ if (dfid) {
+ memcpy(dfid, disk_fid, dlen);
+ ret |= EXT4_DIRENT_LUFID;
+ }
+ data_offset += dlen;
+ }
+
+ /* Skip INO64 for now*/
+ if (de->file_type & EXT4_DIRENT_INO64) {
+ struct ext4_dirent_data_header *ddh =
+ (struct ext4_dirent_data_header *)((char *)de + EXT4_BASE_DIR_LEN + data_offset);
+ unsigned int dlen;
+
+ if (EXT4_BASE_DIR_LEN + data_offset + sizeof(*ddh) > rec_len)
+ return ret;
+
+ dlen = ddh->ddh_length;
+ if (dlen < sizeof(*ddh) ||
+ EXT4_BASE_DIR_LEN + data_offset + dlen > rec_len)
+ return ret;
+
+ data_offset += dlen;
+ }
+
+ if (!hinfo)
+ return ret;
+
+ if (de->file_type & EXT4_DIRENT_CFHASH) {
+ struct ext4_dirent_hash *dh =
+ (struct ext4_dirent_hash *)((char *)de + EXT4_BASE_DIR_LEN + data_offset);
+ unsigned int dlen;
+
+ if (EXT4_BASE_DIR_LEN + data_offset + sizeof(*dh) > rec_len)
+ return ret;
+ dlen = dh->dh_header.ddh_length;
+ if (dlen < sizeof(*dh) ||
+ EXT4_BASE_DIR_LEN + data_offset + dlen > rec_len)
+ return ret;
+
+ hinfo->hash = le32_to_cpu(dh->dh_hash);
+ hinfo->minor_hash = le32_to_cpu(dh->dh_minor_hash);
+ ret |= EXT4_DIRENT_CFHASH;
+ }
+
+ return ret;
+}
+
+/*
+ * ext4_dirdata_set() - Write dirdata fields into a directory entry.
+ * @de: directory entry (name must already be set)
+ * @dir: directory inode
+ * @data: LUFID data to store (or NULL)
+ * @fname: filename info carrying the casefold hash
+ *
+ * Writes any required dirdata into @de after the filename. If the dirdata
+ * feature is not enabled, falls back to writing the hash inline after the
+ * filename (for compatibility with the older casefold+fscrypt format).
+ *
+ * See ext4_dirdata_get() for the compatibility constraint: enabling the
+ * dirdata feature on a filesystem with existing casefolded+encrypted
+ * directories requires an e2fsck migration pass before tune2fs sets the
+ * EXT4_FEATURE_INCOMPAT_DIRDATA superblock flag.
+ */
+static void ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir,
+ struct ext4_dirent_fid *dfid,
+ struct ext4_filename *fname)
+{
+ struct dx_hash_info *hinfo = &fname->hinfo;
+ unsigned int data_offset = de->name_len + 1;
+ unsigned int rec_len = ext4_rec_len_from_disk(de->rec_len,
+ dir->i_sb->s_blocksize);
+
+ /* Clear the gap byte between the filename and the first dirdata
+ * extension to avoid leaking stale memory to disk. Use pointer
+ * arithmetic rather than de->name[name_len] to stay within the
+ * declared name[] array bounds under FORTIFY_SOURCE. Only write it
+ * when there is actually room (entries that exactly fill their slot
+ * have rec_len == EXT4_BASE_DIR_LEN + name_len with no gap). */
+ if (EXT4_BASE_DIR_LEN + data_offset <= rec_len)
+ *((char *)de + EXT4_BASE_DIR_LEN + de->name_len) = 0;
+
+ if (dfid) {
+ unsigned int dlen = dfid->df_header.ddh_length;
+
+ if (EXT4_BASE_DIR_LEN + data_offset + dlen > rec_len) {
+ EXT4_ERROR_INODE(dir, "Can not insert FID");
+ return;
+ }
+
+ memcpy((char *)de + EXT4_BASE_DIR_LEN + data_offset, dfid, dlen);
+ de->file_type |= EXT4_DIRENT_LUFID;
+ data_offset += dlen;
+ }
+
+ if (ext4_hash_in_dirent(dir)) {
+ if (ext4_has_feature_dirdata(dir->i_sb)) {
+ struct ext4_dirent_hash *dh =
+ (struct ext4_dirent_hash *)((char *)de + EXT4_BASE_DIR_LEN + data_offset);
+
+ if (EXT4_BASE_DIR_LEN + data_offset + sizeof(*dh) > rec_len) {
+ EXT4_ERROR_INODE(dir, "Can not insert dhash dirdata");
+ return;
+ }
+
+ dh->dh_header.ddh_length = sizeof(*dh);
+ dh->dh_hash = cpu_to_le32(hinfo->hash);
+ dh->dh_minor_hash = cpu_to_le32(hinfo->minor_hash);
+ de->file_type |= EXT4_DIRENT_CFHASH;
+ } else {
+ /* Compatibility: store hash inline after filename */
+ if (EXT4_BASE_DIR_LEN + data_offset +
+ sizeof(struct ext4_dir_entry_hash) > rec_len) {
+ EXT4_ERROR_INODE(dir, "Can not insert dhash");
+ return;
+ }
+
+ EXT4_DIRENT_HASHES(de)->hash = cpu_to_le32(hinfo->hash);
+ EXT4_DIRENT_HASHES(de)->minor_hash =
+ cpu_to_le32(hinfo->minor_hash);
+ }
+ }
+}
+
/*
* Create map of hash values, offsets, and sizes, stored at end of block.
* Returns number of entries mapped.
@@ -1396,13 +1606,21 @@ static int dx_make_map(struct inode *dir, struct buffer_head *bh,
((char *)de) - base))
return -EFSCORRUPTED;
if (de->name_len && de->inode) {
- if (ext4_hash_in_dirent(dir))
- h.hash = EXT4_DIRENT_HASH(de);
- else {
- int err = ext4fs_dirhash(dir, de->name,
- de->name_len, &h);
- if (err < 0)
- return err;
+ if (!(ext4_dirdata_get(de, dir, dir->i_sb->s_blocksize,
+ NULL, &h) &
+ EXT4_DIRENT_CFHASH)) {
+ if (ext4_hash_in_dirent(dir)) {
+ /* Un-migrated entry: hash at legacy
+ * fixed offset. */
+ h.hash = EXT4_DIRENT_HASH(de);
+ h.minor_hash =
+ EXT4_DIRENT_MINOR_HASH(de);
+ } else {
+ int err = ext4fs_dirhash(dir, de->name,
+ de->name_len, &h);
+ if (err < 0)
+ return err;
+ }
}
map_tail--;
map_tail->hash = h.hash;
@@ -1503,8 +1721,9 @@ int ext4_fname_setup_ci_filename(struct inode *dir, const struct qstr *iname,
* Return: %true if the directory entry matches, otherwise %false.
*/
static bool ext4_match(struct inode *parent,
- const struct ext4_filename *fname,
- struct ext4_dir_entry_2 *de)
+ const struct ext4_filename *fname,
+ struct ext4_dir_entry_2 *de,
+ int buf_size)
{
struct fscrypt_name f;
@@ -1531,10 +1750,29 @@ static bool ext4_match(struct inode *parent,
* considering the calculated hash.
*/
if (sb_no_casefold_compat_fallback(parent->i_sb) &&
- IS_ENCRYPTED(parent) && fname->cf_name.name &&
- (fname->hinfo.hash != EXT4_DIRENT_HASH(de) ||
- fname->hinfo.minor_hash != EXT4_DIRENT_MINOR_HASH(de)))
- return false;
+ IS_ENCRYPTED(parent) && fname->cf_name.name) {
+ __u32 de_hash, de_minor_hash;
+
+ if (ext4_has_feature_dirdata(parent->i_sb) &&
+ (de->file_type & EXT4_DIRENT_CFHASH)) {
+ /* Hash is in a CFHASH extension at a variable
+ * offset (past any LUFID bytes). Read it via
+ * ext4_dirdata_get() to get the correct offset. */
+ struct dx_hash_info dirent_hinfo = {};
+
+ ext4_dirdata_get(de, parent,
+ buf_size,
+ NULL, &dirent_hinfo);
+ de_hash = dirent_hinfo.hash;
+ de_minor_hash = dirent_hinfo.minor_hash;
+ } else {
+ de_hash = EXT4_DIRENT_HASH(de);
+ de_minor_hash = EXT4_DIRENT_MINOR_HASH(de);
+ }
+ if (fname->hinfo.hash != de_hash ||
+ fname->hinfo.minor_hash != de_minor_hash)
+ return false;
+ }
/*
* Treat comparison errors as not a match. The
* only case where it happens is on a disk
@@ -1565,11 +1803,18 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
dlimit = search_buf + buf_size;
while ((char *) de < dlimit - EXT4_BASE_DIR_LEN) {
/* this code is executed quadratically often */
- /* do minimal checking `by hand' */
+ /* Decode rec_len before ext4_match() so that ext4_dirdata_get()
+ * inside ext4_match() never uses an unvalidated rec_len as a
+ * read bound. A crafted entry with rec_len > buf_size would
+ * let ext4_dirdata_get() read past the block buffer. */
+ de_len = ext4_rec_len_from_disk(de->rec_len,
+ dir->i_sb->s_blocksize);
+ if (de_len <= 0 || (char *)de + de_len > dlimit)
+ return -EFSCORRUPTED;
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)) {
+ ext4_match(dir, fname, de, buf_size)) {
/* found a match - just to be sure, do
* a full check */
if (ext4_check_dir_entry(dir, NULL, de, bh, search_buf,
@@ -1578,11 +1823,6 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
*res_dir = de;
return 1;
}
- /* prevent looping on a bad block */
- de_len = ext4_rec_len_from_disk(de->rec_len,
- dir->i_sb->s_blocksize);
- if (de_len <= 0)
- return -EFSCORRUPTED;
offset += de_len;
de = (struct ext4_dir_entry_2 *) ((char *) de + de_len);
}
@@ -2153,7 +2393,7 @@ int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
if (ext4_check_dir_entry(dir, NULL, de, bh,
buf, buf_size, offset))
return -EFSCORRUPTED;
- if (ext4_match(dir, fname, de))
+ if (ext4_match(dir, fname, de, buf_size))
return -EEXIST;
nlen = ext4_dir_entry_len(de, buf_size, dir);
rlen = ext4_rec_len_from_disk(de->rec_len, buf_size);
@@ -2189,17 +2429,7 @@ void ext4_insert_dentry_data(struct inode *dir, struct inode *inode,
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;
-
- EXT4_DIRENT_HASHES(de)->hash = cpu_to_le32(hinfo->hash);
- EXT4_DIRENT_HASHES(de)->minor_hash =
- cpu_to_le32(hinfo->minor_hash);
- }
+ ext4_dirdata_set(de, dir, data, fname);
}
/*
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v14 11/12] ext4: add dirdata set/get helpers
2026-11-03 13:49 ` [PATCH v14 11/12] ext4: add dirdata set/get helpers Artem Blagodarenko
@ 2026-11-03 14:18 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-03 14:18 UTC (permalink / raw)
To: Artem Blagodarenko, linux-ext4; +Cc: tytso
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] ext4: unchecked EXT4_DIRENT_HASH fallback read after ext4_dirdata_get()
--
commit b0e6360d0312737d9514b8c174a474f487aed85e
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>
ext4: add dirdata set/get helpers
This commit introduces ext4_dirdata_get() and ext4_dirdata_set() to read and
write directory entry payload extensions such as LUFID and casefold hashes,
and wires them into directory lookup, insertion, and readdir paths.
> diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
> index 15816f2a5f472..12df2d2a09978 100644
> --- a/fs/ext4/dir.c
> +++ b/fs/ext4/dir.c
> @@ -302,8 +302,19 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
> u32 minor_hash;
>
> if (IS_CASEFOLDED(inode)) {
> - hash = EXT4_DIRENT_HASH(de);
> - minor_hash = EXT4_DIRENT_MINOR_HASH(de);
> + if (ext4_has_feature_dirdata(sb) &&
> + (de->file_type &
> + EXT4_DIRENT_CFHASH)) {
> + struct dx_hash_info hi = {};
> + ext4_dirdata_get(de, inode,
> + sb->s_blocksize,
> + NULL, &hi);
> + hash = hi.hash;
> + minor_hash = hi.minor_hash;
> + } else {
> + hash = EXT4_DIRENT_HASH(de);
> + minor_hash = EXT4_DIRENT_MINOR_HASH(de);
> + }
> } else {
> hash = 0;
> minor_hash = 0;
> diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
> index 81e0ef1c88337..801fa6555ea96 100644
> --- a/fs/ext4/inline.c
> +++ b/fs/ext4/inline.c
> @@ -1359,14 +1359,20 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
> pos += de_len;
> }
>
> - if (ext4_hash_in_dirent(dir)) {
> - hinfo->hash = EXT4_DIRENT_HASH(de);
> - hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
> - } else {
> - err = ext4fs_dirhash(dir, de->name, de->name_len, hinfo);
> - if (err) {
> - ret = err;
> - goto out;
> + if (!(ext4_dirdata_get(de, dir, inline_size, NULL, hinfo) &
> + EXT4_DIRENT_CFHASH)) {
> + if (ext4_hash_in_dirent(dir)) {
> + /* Un-migrated entry: hash at legacy fixed
> + * offset. */
> + hinfo->hash = EXT4_DIRENT_HASH(de);
> + hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
> + } else {
> + err = ext4fs_dirhash(dir, de->name,
> + de->name_len, hinfo);
> + if (err) {
> + ret = err;
> + goto out;
> + }
> }
> }
> if ((hinfo->hash < start_hash) ||
> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index bd0f4760f1e31..8f1a6c000373e 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c
> @@ -1178,22 +1178,34 @@ static int htree_dirblock_to_tree(struct file *dir_file,
> /* silently ignore the rest of the block */
> break;
> }
> - if (ext4_hash_in_dirent(dir)) {
> - if (de->name_len && de->inode) {
> - hinfo->hash = EXT4_DIRENT_HASH(de);
> - hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
> - } else {
> - hinfo->hash = 0;
> - hinfo->minor_hash = 0;
> - }
> + if (de->name_len && de->inode) {
> + /* check for saved hash first, or generate it from name */
> + if (!(ext4_dirdata_get(de, dir, dir->i_sb->s_blocksize,
> + NULL, hinfo) &
> + EXT4_DIRENT_CFHASH)) {
> + if (ext4_hash_in_dirent(dir)) {
> + /* Un-migrated entry: hash is at the
> + * legacy fixed offset, not in a CFHASH
> + * extension. Read it directly rather
> + * than hashing the encrypted name. */
> + hinfo->hash = EXT4_DIRENT_HASH(de);
> + hinfo->minor_hash =
> + EXT4_DIRENT_MINOR_HASH(de);
[ ... ]
> +unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de, struct inode *dir,
> + int buf_size,
> + struct ext4_dirent_fid *dfid,
> + struct dx_hash_info *hinfo)
> +{
> + unsigned char ret = 0;
> + unsigned int data_offset = de->name_len + 1;
> + unsigned int rec_len = ext4_rec_len_from_disk(de->rec_len, buf_size);
> +
> + /* data_offset is relative to de->name, which itself starts
> + * EXT4_BASE_DIR_LEN bytes into the entry -- rec_len is relative to
> + * the start of the entry, so add the header size before comparing,
> + * or this lets reads run EXT4_BASE_DIR_LEN bytes past the entry. */
> + if (EXT4_BASE_DIR_LEN + data_offset > rec_len)
> + return ret;
> +
> + /* compatibility: hash stored inline after filename (no dirdata) */
> + if (hinfo && !ext4_has_feature_dirdata(dir->i_sb) &&
> + ext4_hash_in_dirent(dir)) {
> + if (ext4_dirent_rec_len(de->name_len, dir) > rec_len)
> + return ret;
> + hinfo->hash = EXT4_DIRENT_HASH(de);
> + hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
> + ret |= EXT4_DIRENT_CFHASH;
> + return ret;
> + }
[ ... ]
> @@ -1396,13 +1606,21 @@ static int dx_make_map(struct inode *dir, struct buffer_head *bh,
> ((char *)de) - base))
> return -EFSCORRUPTED;
> if (de->name_len && de->inode) {
> - if (ext4_hash_in_dirent(dir))
> - h.hash = EXT4_DIRENT_HASH(de);
> - else {
> - int err = ext4fs_dirhash(dir, de->name,
> - de->name_len, &h);
> - if (err < 0)
> - return err;
> + if (!(ext4_dirdata_get(de, dir, dir->i_sb->s_blocksize,
> + NULL, &h) &
> + EXT4_DIRENT_CFHASH)) {
> + if (ext4_hash_in_dirent(dir)) {
> + /* Un-migrated entry: hash at legacy
> + * fixed offset. */
> + h.hash = EXT4_DIRENT_HASH(de);
> + h.minor_hash =
> + EXT4_DIRENT_MINOR_HASH(de);
[ ... ]
> @@ -1531,10 +1750,29 @@ static bool ext4_match(struct inode *parent,
> * considering the calculated hash.
> */
> if (sb_no_casefold_compat_fallback(parent->i_sb) &&
> - IS_ENCRYPTED(parent) && fname->cf_name.name &&
> - (fname->hinfo.hash != EXT4_DIRENT_HASH(de) ||
> - fname->hinfo.minor_hash != EXT4_DIRENT_MINOR_HASH(de)))
> - return false;
> + IS_ENCRYPTED(parent) && fname->cf_name.name) {
> + __u32 de_hash, de_minor_hash;
> +
> + if (ext4_has_feature_dirdata(parent->i_sb) &&
> + (de->file_type & EXT4_DIRENT_CFHASH)) {
> + /* Hash is in a CFHASH extension at a variable
> + * offset (past any LUFID bytes). Read it via
> + * ext4_dirdata_get() to get the correct offset. */
> + struct dx_hash_info dirent_hinfo = {};
> +
> + ext4_dirdata_get(de, parent,
> + buf_size,
> + NULL, &dirent_hinfo);
> + de_hash = dirent_hinfo.hash;
> + de_minor_hash = dirent_hinfo.minor_hash;
> + } else {
> + de_hash = EXT4_DIRENT_HASH(de);
> + de_minor_hash = EXT4_DIRENT_MINOR_HASH(de);
> + }
[Severity: High]
As reported by Sashiko on v13 [1], can the fallback reads of
EXT4_DIRENT_HASH(de) and EXT4_DIRENT_MINOR_HASH(de) read 8 bytes past rec_len
and out of bounds of dir_buf or bh->b_data?
In fs/ext4/namei.c:ext4_dirdata_get(), the legacy compatibility branch checks
that rec_len is large enough to hold the inline hash before reading it:
if (hinfo && !ext4_has_feature_dirdata(dir->i_sb) &&
ext4_hash_in_dirent(dir)) {
if (ext4_dirent_rec_len(de->name_len, dir) > rec_len)
return ret;
hinfo->hash = EXT4_DIRENT_HASH(de);
hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
ret |= EXT4_DIRENT_CFHASH;
return ret;
}
When ext4_has_feature_dirdata(dir->i_sb) is enabled, ext4_dirdata_get() also
verifies that any EXT4_DIRENT_LUFID, EXT4_DIRENT_INO64, and
EXT4_DIRENT_CFHASH records fit within rec_len, returning without
EXT4_DIRENT_CFHASH set in the return mask if the flag is absent or if the
entry fails the bounds checks.
However, when ext4_dirdata_get() returns without EXT4_DIRENT_CFHASH set, the
callers in fs/ext4/inline.c:ext4_inlinedir_to_tree(),
fs/ext4/namei.c:htree_dirblock_to_tree(), and fs/ext4/namei.c:dx_make_map()
unconditionally fall back to reading EXT4_DIRENT_HASH(de) and
EXT4_DIRENT_MINOR_HASH(de) when ext4_hash_in_dirent(dir) is true, without
checking that ext4_dirent_rec_len(de->name_len, dir) <= rec_len. Similarly,
the else branches in fs/ext4/dir.c:ext4_readdir() and
fs/ext4/namei.c:ext4_match() (when called from ext4_find_dest_de()) read
EXT4_DIRENT_HASH(de) and EXT4_DIRENT_MINOR_HASH(de) without checking rec_len.
On a valid casefolded and encrypted inline directory in
ext4_inlinedir_to_tree(), the synthesized "." and ".." entries in fake have
fake.rec_len = ext4_dirent_rec_len(fake.name_len, NULL) = 12, which is
smaller than ext4_dirent_rec_len(fake.name_len, dir) = 20. ext4_dirdata_get()
returns 0, and the fallback reads 8 bytes at offset 12..19 past fake.rec_len.
Likewise, on a 1-block casefolded and encrypted directory in
htree_dirblock_to_tree(), the "." entry has rec_len = 12, so after
ext4_dirdata_get() returns 0, the fallback reads 8 bytes from the adjacent
".." entry header.
On a crafted or corrupted casefolded and encrypted directory image,
__ext4_check_dir_entry() sets fake = is_fake_dir_entry(de) (which is true for
"." or ".." or when de->file_type == EXT4_FT_DIR_CSUM) and validates rlen
against ext4_dirent_rec_len(1, NULL) (12 bytes) and
ext4_dir_entry_len(de, size, NULL) (12 bytes for name_len <= 4). This allows
a 12-byte fake entry with de->inode != 0 at offset = size - 12 at the end of
the kmalloc(inline_size) buffer dir_buf or block buffer bh->b_data. When
ext4_dirdata_get() rejects the entry and returns 0, the unchecked fallback in
ext4_inlinedir_to_tree(), htree_dirblock_to_tree(), dx_make_map(),
ext4_readdir(), and ext4_match() reads 8 bytes at (char *)de + 12, past the
end of dir_buf or bh->b_data.
Furthermore, on a dirdata-enabled filesystem where de->file_type &
EXT4_DIRENT_CFHASH is set, if the CFHASH extension fails the bounds check in
ext4_dirdata_get(), htree_dirblock_to_tree(), dx_make_map(), and
ext4_inlinedir_to_tree() still fall back to reading EXT4_DIRENT_HASH(de) at
the legacy fixed offset, misinterpreting LUFID or out-of-bounds bytes as the
casefold hash.
[1] https://lore.kernel.org/r/20261029122413.230409-11-ablagodarenko@thelustrecollective.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103134921.610047-1-ablagodarenko@thelustrecollective.com?part=11
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v14 12/12] ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
` (10 preceding siblings ...)
2026-11-03 13:49 ` [PATCH v14 11/12] ext4: add dirdata set/get helpers Artem Blagodarenko
@ 2026-11-03 13:49 ` Artem Blagodarenko
2026-11-03 14:29 ` sashiko-bot
11 siblings, 1 reply; 25+ messages in thread
From: Artem Blagodarenko @ 2026-11-03 13:49 UTC (permalink / raw)
To: linux-ext4; +Cc: tytso, jack, adilger.kernel, ablagodarenko, Artem Blagodarenko
From: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Add a new ioctl command that allows setting LUFID (Locally Unique File ID)
data on existing directory entries. This includes:
- ext4_ioctl_set_lufid(): ioctl handler that validates parameters and
calls the underlying implementation
- ext4_dirdata_set_lufid(): Core function that performs the operation by:
* Looking up the target directory entry
* Retrieving the associated inode
* Deleting the old entry and re-creating it with LUFID data attached
The LUFID payload is limited to 48 bytes (three FIDs), so that a
directory block split always has room for the new entry, even with
1k blocks, a 255-byte name and a CFHASH record.
The entry is re-created under the name it is stored with, which in a
casefolded directory may differ in case from the name passed in; in an
encrypted directory that name is decrypted first, so the key is needed.
Both names are set up before the old entry is deleted, so a failed
re-add can put the old entry back without allocating memory. The
directory is locked for the whole operation, its quota is initialized
before the transaction starts, and DIRSYNC is honoured.
This implementation requires the dirdata feature to be enabled on the
filesystem.
Signed-off-by: Artem Blagodarenko <artem.blagodarenko@gmail.com>
Assisted-by: Claude:claude-opus-5-5
---
fs/ext4/dir.c | 3 +-
fs/ext4/ext4.h | 36 ++--
fs/ext4/inline.c | 4 +-
fs/ext4/ioctl.c | 87 +++++++++
fs/ext4/namei.c | 358 +++++++++++++++++++++++++++++++++-----
include/uapi/linux/ext4.h | 14 ++
6 files changed, 447 insertions(+), 55 deletions(-)
diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
index 12df2d2a0997..dca6796d5fe2 100644
--- a/fs/ext4/dir.c
+++ b/fs/ext4/dir.c
@@ -507,7 +507,8 @@ int ext4_htree_store_dirent(struct file *dir_file, __u32 hash,
p = &info->root.rb_node;
/* Create and allocate the fname structure */
- if (dirent->file_type & ~EXT4_FT_MASK) {
+ if (ext4_has_feature_dirdata(dir_file->f_inode->i_sb) &&
+ (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);
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 32119ab18ac4..95e81179e435 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -2624,6 +2624,14 @@ struct ext4_dirent_fid {
};
#define EXT4_LUFID_MAGIC 0xAD200907UL
+
+/*
+ * Largest LUFID payload accepted from EXT4_IOC_SET_LUFID. do_split() needs
+ * every entry to fit in the free part of either half of a split block; with
+ * a 255-byte name, the NUL byte, a CFHASH record and 1k blocks that limits a
+ * LUFID record to about 57 bytes. Three FIDs leave some headroom.
+ */
+#define EXT4_LUFID_MAX_DATA_LEN (3 * sizeof(struct ext4_fid))
struct ext4_dentry_param {
__u32 edp_magic; /* EXT4_LUFID_MAGIC */
__u8 edp_dfid[]; /* struct ext4_dirent_fid payload */
@@ -2679,6 +2687,11 @@ static inline unsigned int ext4_dirent_rec_len(unsigned int name_len,
{
unsigned int rec_len = (name_len + 8 + EXT4_DIR_ROUND);
+ /*
+ * Reserve the casefold+fscrypt hash at its fixed position after the
+ * name. Callers whose length already includes the hash as a CFHASH
+ * dirdata record pass a NULL @dir (see ext4_dir_entry_len()).
+ */
if (dir && ext4_hash_in_dirent(dir))
rec_len += sizeof(struct ext4_dir_entry_hash);
return (rec_len & ~EXT4_DIR_ROUND);
@@ -3089,17 +3102,17 @@ extern int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
struct ext4_filename *fname,
struct ext4_dir_entry_2 **dest_de,
int dlen);
-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);
-static inline void ext4_insert_dentry(struct inode *dir, struct inode *inode,
- struct ext4_dir_entry_2 *de,
- int buf_size,
- struct ext4_filename *fname)
+int 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);
+static inline int ext4_insert_dentry(struct inode *dir, struct inode *inode,
+ struct ext4_dir_entry_2 *de,
+ int buf_size,
+ struct ext4_filename *fname)
{
- ext4_insert_dentry_data(dir, inode, de, buf_size, fname, NULL);
+ return ext4_insert_dentry_data(dir, inode, de, buf_size, fname, NULL);
}
static inline void ext4_update_dx_flag(struct inode *inode)
{
@@ -3367,6 +3380,9 @@ static inline int ext4_init_new_dir(handle_t *handle, struct inode *dir,
}
extern int ext4_dirblock_csum_verify(struct inode *inode,
struct buffer_head *bh);
+extern int ext4_dirdata_set_lufid(struct mnt_idmap *idmap, struct inode *dir,
+ const char *filename, int namelen,
+ struct ext4_dentry_param *edp);
extern int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,
__u32 start_minor_hash, __u32 *next_hash);
extern int ext4_search_dir(struct buffer_head *bh,
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 801fa6555ea9..1667c68c683e 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -998,7 +998,9 @@ static int ext4_add_dirent_to_inline(handle_t *handle,
EXT4_JTR_NONE);
if (err)
return err;
- ext4_insert_dentry_data(dir, inode, de, inline_size, fname, dfid);
+ err = ext4_insert_dentry_data(dir, inode, de, inline_size, fname, dfid);
+ if (err)
+ return err;
ext4_show_inline_dir(dir, iloc->bh, inline_start, inline_size);
diff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c
index 1f6d7afb62ec..68054e9f8d77 100644
--- a/fs/ext4/ioctl.c
+++ b/fs/ext4/ioctl.c
@@ -1536,6 +1536,90 @@ static int ext4_ioctl_set_tune_sb(struct file *filp,
return ret;
}
+/*
+ * ext4_ioctl_set_lufid() - Set LUFID on a directory entry
+ * @filp: file pointer (parent directory)
+ * @arg: pointer to ext4_set_lufid structure with filename and LUFID data
+ *
+ * This ioctl allows setting LUFID data on an existing
+ * directory entry. It is called on the parent directory with a filename and
+ * LUFID data.
+ */
+static long ext4_ioctl_set_lufid(struct file *filp, unsigned long arg)
+{
+ struct inode *dir = file_inode(filp);
+ struct mnt_idmap *idmap = file_mnt_idmap(filp);
+ struct ext4_set_lufid lufid_args;
+ struct {
+ __u32 edp_magic;
+ struct ext4_dirent_data_header df_header;
+ char df_fid[EXT4_LUFID_MAX_DATA_LEN];
+ } edp;
+ int err;
+
+ /* Check if parent is a directory */
+ if (!S_ISDIR(dir->i_mode))
+ return -ENOTDIR;
+
+ /* This ioctl mutates directory entries; merely having the directory
+ * open (which only ever requires read access) is not enough.
+ * MAY_EXEC is required for entry lookup; MAY_WRITE for modification. */
+ err = inode_permission(idmap, dir, MAY_WRITE | MAY_EXEC);
+ if (err)
+ return err;
+
+ /* Copy arguments from user space */
+ if (copy_from_user(&lufid_args, (struct ext4_set_lufid __user *)arg,
+ sizeof(lufid_args)))
+ return -EFAULT;
+
+ /* Validate parameters. esl_name_len is NUL-excluded length (1-255). */
+ if (lufid_args.esl_name_len == 0 || lufid_args.esl_name_len > EXT4_NAME_LEN)
+ return -EINVAL;
+
+ /*
+ * The payload is capped so that a directory block split always has
+ * room for the new entry, see EXT4_LUFID_MAX_DATA_LEN
+ */
+ if (lufid_args.esl_data_len == 0 ||
+ lufid_args.esl_data_len > EXT4_LUFID_MAX_DATA_LEN)
+ return -EINVAL;
+
+ /* Ensure filename is NUL-terminated at exactly esl_name_len */
+ if (lufid_args.esl_name[lufid_args.esl_name_len] != '\0')
+ return -EINVAL;
+
+ /* '.' and '..' are not ordinary entries -- they must stay the first
+ * two entries in the directory's first block, so they can't go
+ * through the general delete+re-add path this ioctl uses */
+ if (!strcmp(lufid_args.esl_name, ".") || !strcmp(lufid_args.esl_name, ".."))
+ return -EINVAL;
+
+ /* Prepare the dentry param struct with LUFID data. ddh_length is
+ * documented (see struct ext4_dirent_data_header) as the length of
+ * the header plus the whole data blob -- include the header here so
+ * every dirdata reader/writer that takes ddh_length at face value
+ * (e.g. ext4_dirdata_set()'s memcpy) copies the full LUFID payload
+ * instead of silently dropping its last byte. */
+ edp.edp_magic = EXT4_LUFID_MAGIC;
+ edp.df_header.ddh_length = lufid_args.esl_data_len +
+ sizeof(edp.df_header);
+ memcpy(edp.df_fid, lufid_args.esl_data, lufid_args.esl_data_len);
+
+ /* Want write access */
+ err = mnt_want_write_file(filp);
+ if (err)
+ return err;
+
+ /* Call the helper function to do the actual work */
+ err = ext4_dirdata_set_lufid(idmap, dir, lufid_args.esl_name,
+ lufid_args.esl_name_len,
+ (struct ext4_dentry_param *)&edp);
+
+ mnt_drop_write_file(filp);
+ return err;
+}
+
static long __ext4_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct inode *inode = file_inode(filp);
@@ -1922,6 +2006,8 @@ static long __ext4_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
(void __user *)arg);
case EXT4_IOC_SET_TUNE_SB_PARAM:
return ext4_ioctl_set_tune_sb(filp, (void __user *)arg);
+ case EXT4_IOC_SET_LUFID:
+ return ext4_ioctl_set_lufid(filp, arg);
default:
return -ENOTTY;
}
@@ -2001,6 +2087,7 @@ long ext4_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
case FS_IOC_SETFSLABEL:
case EXT4_IOC_GETFSUUID:
case EXT4_IOC_SETFSUUID:
+ case EXT4_IOC_SET_LUFID:
break;
default:
return -ENOIOCTLCMD;
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 8f1a6c000373..68c5caff4707 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -1523,9 +1523,9 @@ unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de, struct inode *dir,
* directories requires an e2fsck migration pass before tune2fs sets the
* EXT4_FEATURE_INCOMPAT_DIRDATA superblock flag.
*/
-static void ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir,
- struct ext4_dirent_fid *dfid,
- struct ext4_filename *fname)
+static int ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir,
+ struct ext4_dirent_fid *dfid,
+ struct ext4_filename *fname)
{
struct dx_hash_info *hinfo = &fname->hinfo;
unsigned int data_offset = de->name_len + 1;
@@ -1546,7 +1546,7 @@ static void ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir,
if (EXT4_BASE_DIR_LEN + data_offset + dlen > rec_len) {
EXT4_ERROR_INODE(dir, "Can not insert FID");
- return;
+ return -EIO;
}
memcpy((char *)de + EXT4_BASE_DIR_LEN + data_offset, dfid, dlen);
@@ -1561,7 +1561,7 @@ static void ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir,
if (EXT4_BASE_DIR_LEN + data_offset + sizeof(*dh) > rec_len) {
EXT4_ERROR_INODE(dir, "Can not insert dhash dirdata");
- return;
+ return -EIO;
}
dh->dh_header.ddh_length = sizeof(*dh);
@@ -1569,11 +1569,18 @@ static void ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir,
dh->dh_minor_hash = cpu_to_le32(hinfo->minor_hash);
de->file_type |= EXT4_DIRENT_CFHASH;
} else {
- /* Compatibility: store hash inline after filename */
- if (EXT4_BASE_DIR_LEN + data_offset +
- sizeof(struct ext4_dir_entry_hash) > rec_len) {
+ /* Compatibility: store hash inline after filename.
+ * EXT4_DIRENT_HASHES places the hash at the
+ * 4-byte-aligned offset (8 + name_len + 3) & ~3, which
+ * may be less than EXT4_BASE_DIR_LEN + data_offset when
+ * name_len is a multiple of 4 (gap byte counted twice).
+ * Use the same aligned offset for the bounds check. */
+ unsigned int hash_off =
+ (EXT4_BASE_DIR_LEN + de->name_len + 3) & ~3;
+
+ if (hash_off + sizeof(struct ext4_dir_entry_hash) > rec_len) {
EXT4_ERROR_INODE(dir, "Can not insert dhash");
- return;
+ return -EIO;
}
EXT4_DIRENT_HASHES(de)->hash = cpu_to_le32(hinfo->hash);
@@ -1581,6 +1588,7 @@ static void ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir,
cpu_to_le32(hinfo->minor_hash);
}
}
+ return 0;
}
/*
@@ -2409,9 +2417,9 @@ int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
return 0;
}
-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 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;
@@ -2429,7 +2437,7 @@ void ext4_insert_dentry_data(struct inode *dir, struct inode *inode,
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));
- ext4_dirdata_set(de, dir, data, fname);
+ return ext4_dirdata_set(de, dir, data, fname);
}
/*
@@ -2491,7 +2499,11 @@ static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,
}
/* By now the buffer is marked for journaling */
- ext4_insert_dentry_data(dir, inode, de, blocksize, fname, dfid);
+ err = ext4_insert_dentry_data(dir, inode, de, blocksize, fname, dfid);
+ if (err) {
+ ext4_std_error(dir->i_sb, err);
+ return err;
+ }
/*
* XXX shouldn't update any times until successful
@@ -2718,15 +2730,17 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
* may not sleep between calling this and putting something into
* the entry, as someone else might have used it while you slept.
*/
-static int __ext4_add_entry(handle_t *handle, struct inode *dir,
- const struct qstr *d_name,
- struct inode *inode,
- struct ext4_dentry_param *edp)
+/*
+ * ext4_add_fname() - add an entry for a name that is already set up
+ *
+ * @fname->dfid is the LUFID record to store with the entry, or NULL.
+ */
+static int ext4_add_fname(handle_t *handle, struct inode *dir,
+ struct ext4_filename *fname, struct inode *inode)
{
struct buffer_head *bh = NULL;
struct ext4_dir_entry_2 *de;
struct super_block *sb;
- struct ext4_filename fname;
int retval;
int dx_fallback=0;
unsigned blocksize;
@@ -2739,22 +2753,11 @@ static int __ext4_add_entry(handle_t *handle, struct inode *dir,
sb = dir->i_sb;
blocksize = sb->s_blocksize;
- if (!generic_ci_validate_strict_name(dir, d_name))
- return -EINVAL;
-
- retval = ext4_fname_setup_filename(dir, d_name, 0, &fname);
- if (retval)
- return retval;
-
- /* Resolve the LUFID payload once here and store it in fname so every
- * add_dirent_to_buf() call in this add operation uses the same value
- * without touching the shared EXT4_I(inode)->i_dirdata field. */
- fname.dfid = ext4_dentry_get_fid(sb, edp);
- if (fname.dfid)
+ if (fname->dfid)
ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_DIRDATA, handle);
if (ext4_has_inline_data(dir)) {
- retval = ext4_try_add_inline_entry(handle, &fname, dir, inode);
+ retval = ext4_try_add_inline_entry(handle, fname, dir, inode);
if (retval < 0)
goto out;
if (retval == 1) {
@@ -2764,7 +2767,7 @@ static int __ext4_add_entry(handle_t *handle, struct inode *dir,
}
if (is_dx(dir)) {
- retval = ext4_dx_add_entry(handle, &fname, dir, inode);
+ retval = ext4_dx_add_entry(handle, fname, dir, inode);
if (!retval || (retval != ERR_BAD_DX_DIR))
goto out;
/* Can we just ignore htree data? */
@@ -2793,14 +2796,14 @@ static int __ext4_add_entry(handle_t *handle, struct inode *dir,
bh = NULL;
goto out;
}
- retval = add_dirent_to_buf(handle, &fname, dir, inode,
+ retval = add_dirent_to_buf(handle, fname, dir, inode,
NULL, bh);
if (retval != -ENOSPC)
goto out;
if (blocks == 1 && !dx_fallback &&
ext4_has_feature_dir_index(sb)) {
- retval = make_indexed_dir(handle, &fname, dir,
+ retval = make_indexed_dir(handle, fname, dir,
inode, bh);
bh = NULL; /* make_indexed_dir releases bh */
goto out;
@@ -2821,15 +2824,39 @@ static int __ext4_add_entry(handle_t *handle, struct inode *dir,
if (csum_size)
ext4_initialize_dirent_tail(bh, blocksize);
- retval = add_dirent_to_buf(handle, &fname, dir, inode, de, bh);
+ retval = add_dirent_to_buf(handle, fname, dir, inode, de, bh);
out:
- ext4_fname_free_filename(&fname);
brelse(bh);
if (retval == 0)
ext4_set_inode_state(inode, EXT4_STATE_NEWENTRY);
return retval;
}
+static int __ext4_add_entry(handle_t *handle, struct inode *dir,
+ const struct qstr *d_name,
+ struct inode *inode,
+ struct ext4_dentry_param *edp)
+{
+ struct ext4_filename fname;
+ int retval;
+
+ if (!generic_ci_validate_strict_name(dir, d_name))
+ return -EINVAL;
+
+ retval = ext4_fname_setup_filename(dir, d_name, 0, &fname);
+ if (retval)
+ return retval;
+
+ /* Resolve the LUFID payload once here and store it in fname so every
+ * add_dirent_to_buf() call in this add operation uses the same value
+ * without touching the shared EXT4_I(inode)->i_dirdata field.
+ */
+ fname.dfid = ext4_dentry_get_fid(dir->i_sb, edp);
+ retval = ext4_add_fname(handle, dir, &fname, inode);
+ ext4_fname_free_filename(&fname);
+ return retval;
+}
+
static int ext4_add_entry(handle_t *handle, struct dentry *dentry,
struct inode *inode)
{
@@ -3307,14 +3334,15 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,
struct ext4_dir_entry_2 *de = (struct ext4_dir_entry_2 *) bh->b_data;
size_t blocksize = bh->b_size;
int csum_size = 0, header_size;
+ unsigned int dot_rec_len;
if (ext4_has_feature_metadata_csum(inode->i_sb))
csum_size = sizeof(struct ext4_dir_entry_tail);
de->inode = cpu_to_le32(inode->i_ino);
de->name_len = 1;
- de->rec_len = ext4_rec_len_to_disk(ext4_dirent_rec_len(de->name_len, NULL),
- blocksize);
+ dot_rec_len = ext4_dirent_rec_len(de->name_len, NULL);
+ de->rec_len = ext4_rec_len_to_disk(dot_rec_len, blocksize);
memcpy(de->name, ".", 2);
ext4_set_de_type(inode->i_sb, de, S_IFDIR);
@@ -3334,7 +3362,7 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,
blocksize - csum_size);
} else {
de->rec_len = ext4_rec_len_to_disk(blocksize -
- (csum_size + ext4_dirent_rec_len(1, NULL)),
+ (csum_size + dot_rec_len),
blocksize);
}
@@ -3367,6 +3395,12 @@ int ext4_init_new_dir_data(handle_t *handle, struct inode *dir,
dir_block = ext4_append(handle, inode, &block);
if (IS_ERR(dir_block))
return PTR_ERR(dir_block);
+ /*
+ * data1 and data2 are reserved for callers that need to embed
+ * dirdata into the '.' and '..' entries of a new directory.
+ * That path is not yet implemented here; a caller requiring it
+ * must write the dirdata entries after this function returns.
+ */
err = ext4_init_dirblock(handle, inode, dir_block, dir->i_ino, NULL, 0);
out:
brelse(dir_block);
@@ -4486,14 +4520,13 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
if (!new.bh && old_snap.fid) {
unsigned fid_size = old_snap.fid->df_header.ddh_length;
- edp = kmalloc(offsetof(struct ext4_dentry_param,
- edp_dfid) + fid_size, GFP_NOFS);
+ edp = kmalloc(sizeof(*edp) + fid_size, GFP_NOFS);
if (!edp) {
retval = -ENOMEM;
goto end_rename;
}
edp->edp_magic = EXT4_LUFID_MAGIC;
- memcpy(&edp->edp_dfid, old_snap.fid, fid_size);
+ memcpy(edp->edp_dfid, old_snap.fid, fid_size);
}
if (whiteout) {
@@ -4825,6 +4858,245 @@ static int ext4_rename2(struct mnt_idmap *idmap,
return ext4_rename(idmap, old_dir, old_dentry, new_dir, new_dentry, flags);
}
+/*
+ * ext4_dirdata_set_lufid() - Set LUFID data on an existing directory entry
+ * @dir: parent directory inode
+ * @filename: name of the file in the directory
+ * @namelen: length of filename
+ * @edp: pointer to initialized dentry param with LUFID data
+ *
+ * This function finds an existing directory entry, deletes it, and re-creates it
+ * with LUFID data attached. Used by the EXT4_IOC_SET_LUFID ioctl.
+ *
+ * Returns 0 on success, negative error code on failure.
+ */
+int ext4_dirdata_set_lufid(struct mnt_idmap *idmap, struct inode *dir,
+ const char *filename, int namelen,
+ struct ext4_dentry_param *edp)
+{
+ struct super_block *sb = dir->i_sb;
+ /* zero-init: safe to free on any path */
+ struct ext4_filename new_fname = {}, old_fname = {};
+ struct ext4_dir_entry_2 *de = NULL;
+ struct buffer_head *bh = NULL;
+ struct inode *inode = NULL;
+ handle_t *handle = NULL;
+ struct ext4_lufid_snap old_snap = {};
+ struct qstr d_name = QSTR_INIT(filename, namelen);
+ /* the name as stored on disk, for casefolded directories */
+ struct fscrypt_str stored = FSTR_INIT(NULL, 0);
+ struct qstr stored_name;
+ const struct qstr *add_name = &d_name;
+ __u32 ino;
+ int err, err2;
+
+ if (!ext4_has_feature_dirdata(sb))
+ return -EOPNOTSUPP;
+
+ if (namelen > EXT4_NAME_LEN)
+ return -ENAMETOOLONG;
+ if (namelen != strnlen(filename, namelen + 1))
+ return -EINVAL;
+
+ /* the entry is re-created from its plaintext name */
+ if (IS_ENCRYPTED(dir) && !fscrypt_has_encryption_key(dir))
+ return -ENOKEY;
+
+ err = dquot_initialize(dir);
+ if (err)
+ return err;
+
+ /*
+ * The lookup and the change must both be under the directory lock.
+ * The entry is deleted and re-added in the same directory, so the
+ * target inode itself is not changed and is not locked.
+ */
+ inode_lock_nested(dir, I_MUTEX_PARENT);
+
+ /*
+ * Look up the entry first to get the inode for the permission
+ * checks, without holding a journal handle.
+ */
+ bh = ext4_find_entry(dir, &d_name, &de, NULL);
+ if (IS_ERR(bh)) {
+ err = PTR_ERR(bh);
+ bh = NULL;
+ goto out_unlock_dir;
+ }
+ if (!bh) {
+ err = -ENOENT;
+ goto out_unlock_dir;
+ }
+ ino = le32_to_cpu(de->inode);
+ brelse(bh);
+ bh = NULL;
+ de = NULL;
+
+ inode = ext4_iget(sb, ino, EXT4_IGET_NORMAL);
+ if (IS_ERR(inode)) {
+ err = PTR_ERR(inode);
+ inode = NULL;
+ goto out_unlock_dir;
+ }
+
+ /* Enforce sticky-bit restriction: on a sticky directory only the
+ * directory owner, the file owner, or a privileged process may
+ * modify entries. inode_permission(MAY_WRITE) alone does not cover
+ * this, so check it explicitly here as may_delete() does. */
+ err = check_sticky(idmap, dir, inode);
+ if (err)
+ goto out_iput;
+
+ /* The ioctl calls ext4_delete_entry() on the parent directory, which
+ * bypasses the VFS may_delete() path. inode_permission(MAY_WRITE)
+ * above blocks IS_IMMUTABLE(dir) but does NOT block IS_APPEND(dir):
+ * append-only directories allow new entries but forbid deletion, and
+ * that restriction lives only in may_delete(), not in
+ * inode_permission(). Enforce it explicitly here. */
+ if (IS_APPEND(dir)) {
+ err = -EPERM;
+ goto out_iput;
+ }
+
+ /* The ioctl deletes and re-creates the directory entry, which
+ * effectively modifies the target file's namespace presence.
+ * Refuse if the target itself is immutable or append-only. */
+ if (IS_IMMUTABLE(inode) || IS_APPEND(inode)) {
+ err = -EPERM;
+ goto out_iput;
+ }
+
+ handle = ext4_journal_start(dir, EXT4_HT_DIR,
+ 3 * EXT4_DATA_TRANS_BLOCKS(sb) +
+ 2 * EXT4_INDEX_EXTRA_TRANS_BLOCKS);
+ if (IS_ERR(handle)) {
+ err = PTR_ERR(handle);
+ handle = NULL;
+ goto out_iput;
+ }
+ if (IS_DIRSYNC(dir))
+ ext4_handle_sync(handle);
+
+ /*
+ * Look up the entry again inside the handle and check that it still
+ * refers to the same inode (an unlink and create could have run in
+ * between).
+ */
+ bh = ext4_find_entry(dir, &d_name, &de, NULL);
+ if (IS_ERR(bh)) {
+ err = PTR_ERR(bh);
+ bh = NULL;
+ goto out_journal;
+ }
+ if (!bh) {
+ err = -ENOENT;
+ goto out_journal;
+ }
+ if (le32_to_cpu(de->inode) != ino) {
+ err = -ENOENT;
+ goto out_brelse;
+ }
+
+ /*
+ * In a casefolded directory the lookup ignores case, so the caller's
+ * name may differ from the stored one. Re-add the stored name, or
+ * the entry would be renamed. In an encrypted directory the stored
+ * name is ciphertext, so decrypt it. Elsewhere the names match.
+ */
+ if (IS_CASEFOLDED(dir)) {
+ if (IS_ENCRYPTED(dir)) {
+ struct fscrypt_str disk = FSTR_INIT(de->name,
+ de->name_len);
+
+ err = fscrypt_fname_alloc_buffer(EXT4_NAME_LEN, &stored);
+ if (err)
+ goto out_brelse;
+ err = fscrypt_fname_disk_to_usr(dir, 0, 0, &disk,
+ &stored);
+ if (err)
+ goto out_brelse;
+ } else {
+ stored.name = kmemdup(de->name, de->name_len, GFP_NOFS);
+ if (!stored.name) {
+ err = -ENOMEM;
+ goto out_brelse;
+ }
+ stored.len = de->name_len;
+ }
+ stored_name = (struct qstr)QSTR_INIT(stored.name, stored.len);
+ add_name = &stored_name;
+ }
+
+ /* The add checks this too, but only after the entry is gone */
+ if (!generic_ci_validate_strict_name(dir, add_name)) {
+ err = -EINVAL;
+ goto out_brelse;
+ }
+
+ /* Copy the old LUFID so a failed re-add can restore it */
+ err = ext4_lufid_snapshot(de, sb->s_blocksize, &old_snap);
+ if (err)
+ goto out_brelse;
+
+ /*
+ * Set up both names before the entry is deleted, so neither the
+ * re-add nor the rollback has to allocate memory afterwards.
+ */
+ err = ext4_fname_setup_filename(dir, add_name, 0, &new_fname);
+ if (err)
+ goto out_brelse;
+ new_fname.dfid = ext4_dentry_get_fid(sb, edp);
+ err = ext4_fname_setup_filename(dir, add_name, 0, &old_fname);
+ if (err)
+ goto out_brelse;
+ old_fname.dfid = old_snap.fid;
+
+ err = ext4_delete_entry(handle, dir, de, bh);
+ if (err)
+ goto out_brelse;
+
+ brelse(bh);
+ bh = NULL;
+
+ err = ext4_add_fname(handle, dir, &new_fname, inode);
+ if (err) {
+ /*
+ * The old entry fits in the space it was deleted from, so
+ * this needs no new block.
+ */
+ err2 = ext4_add_fname(handle, dir, &old_fname, inode);
+ if (err2)
+ EXT4_ERROR_INODE(dir,
+ "Failed to set LUFID on '%.*s' (err=%d) and failed to restore the original directory entry (err=%d); inode %llu may be orphaned",
+ namelen, filename, err, err2,
+ (unsigned long long)inode->i_ino);
+ goto out_journal;
+ }
+
+ inode_set_ctime_current(dir);
+ inode_inc_iversion(dir);
+ ext4_mark_inode_dirty(handle, dir);
+
+out_brelse:
+ brelse(bh);
+out_journal:
+ ext4_fname_free_filename(&new_fname);
+ ext4_fname_free_filename(&old_fname);
+ kfree(stored.name);
+ ext4_lufid_snap_release(&old_snap);
+ if (handle) {
+ err2 = ext4_journal_stop(handle);
+ if (!err)
+ err = err2;
+ }
+out_iput:
+ iput(inode);
+out_unlock_dir:
+ inode_unlock(dir);
+
+ return err;
+}
+
/*
* directories can handle most operations...
*/
diff --git a/include/uapi/linux/ext4.h b/include/uapi/linux/ext4.h
index 9c683991c32f..5d5c8c3df2a4 100644
--- a/include/uapi/linux/ext4.h
+++ b/include/uapi/linux/ext4.h
@@ -35,6 +35,7 @@
#define EXT4_IOC_SETFSUUID _IOW('f', 44, struct fsuuid)
#define EXT4_IOC_GET_TUNE_SB_PARAM _IOR('f', 45, struct ext4_tune_sb_params)
#define EXT4_IOC_SET_TUNE_SB_PARAM _IOW('f', 46, struct ext4_tune_sb_params)
+#define EXT4_IOC_SET_LUFID _IOW('f', 47, struct ext4_set_lufid)
#define EXT4_IOC_SHUTDOWN _IOR('X', 125, __u32)
@@ -92,6 +93,19 @@ struct move_extent {
__u64 moved_len; /* moved block length */
};
+/*
+ * Structure for EXT4_IOC_SET_LUFID
+ * Sets LUFID on a directory entry
+ * Called on parent directory with filename and LUFID data as arguments
+ */
+struct ext4_set_lufid {
+ __u8 esl_name_len; /* length of filename, NOT including NUL terminator
+ * (valid range: 1-255, matching EXT4_NAME_LEN) */
+ char esl_name[255 + 1]; /* filename (NUL-terminated) */
+ __u8 esl_data_len; /* length of LUFID data (1-48) */
+ char esl_data[255]; /* LUFID data (raw bytes) */
+};
+
/*
* Flags used by EXT4_IOC_SHUTDOWN
*/
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v14 12/12] ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries
2026-11-03 13:49 ` [PATCH v14 12/12] ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries Artem Blagodarenko
@ 2026-11-03 14:29 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-03 14:29 UTC (permalink / raw)
To: Artem Blagodarenko, linux-ext4; +Cc: tytso
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] ext4: rollback in ext4_dirdata_set_lufid() can fail and lose directory entry
- [High] ext4: premature fscrypt_has_encryption_key() check in ext4_dirdata_set_lufid()
- [Medium] ext4: missing validation of looked-up inode in ext4_dirdata_set_lufid()
- [Medium] ext4: check_sticky() positive return value leaked in ext4_dirdata_set_lufid()
--
commit 8661288e629af06ada9624f600ed1251cebe9539
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>
ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries
This commit introduces the EXT4_IOC_SET_LUFID ioctl and
ext4_dirdata_set_lufid() to attach LUFID dirdata to existing directory
entries by looking up the entry, deleting it, and re-creating it with the
LUFID payload.
> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index 8f1a6c000373e..68c5caff4707e 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c
[ ... ]
> @@ -4825,6 +4858,245 @@ static int ext4_rename2(struct mnt_idmap *idmap,
> return ext4_rename(idmap, old_dir, old_dentry, new_dir, new_dentry, flags);
> }
>
> +/*
> + * ext4_dirdata_set_lufid() - Set LUFID data on an existing directory entry
> + * @dir: parent directory inode
> + * @filename: name of the file in the directory
> + * @namelen: length of filename
> + * @edp: pointer to initialized dentry param with LUFID data
> + *
> + * This function finds an existing directory entry, deletes it, and re-creates it
> + * with LUFID data attached. Used by the EXT4_IOC_SET_LUFID ioctl.
> + *
> + * Returns 0 on success, negative error code on failure.
> + */
> +int ext4_dirdata_set_lufid(struct mnt_idmap *idmap, struct inode *dir,
> + const char *filename, int namelen,
> + struct ext4_dentry_param *edp)
> +{
> + struct super_block *sb = dir->i_sb;
> + /* zero-init: safe to free on any path */
> + struct ext4_filename new_fname = {}, old_fname = {};
> + struct ext4_dir_entry_2 *de = NULL;
> + struct buffer_head *bh = NULL;
> + struct inode *inode = NULL;
> + handle_t *handle = NULL;
> + struct ext4_lufid_snap old_snap = {};
> + struct qstr d_name = QSTR_INIT(filename, namelen);
> + /* the name as stored on disk, for casefolded directories */
> + struct fscrypt_str stored = FSTR_INIT(NULL, 0);
> + struct qstr stored_name;
> + const struct qstr *add_name = &d_name;
> + __u32 ino;
> + int err, err2;
> +
> + if (!ext4_has_feature_dirdata(sb))
> + return -EOPNOTSUPP;
> +
> + if (namelen > EXT4_NAME_LEN)
> + return -ENAMETOOLONG;
> + if (namelen != strnlen(filename, namelen + 1))
> + return -EINVAL;
> +
> + /* the entry is re-created from its plaintext name */
> + if (IS_ENCRYPTED(dir) && !fscrypt_has_encryption_key(dir))
> + return -ENOKEY;
[Severity: High]
Won't this fscrypt_has_encryption_key(dir) check fail with -ENOKEY on an
encrypted directory even when the key is present in the keyring?
fscrypt_has_encryption_key() only checks whether dir->i_crypt_info has
already been populated:
include/linux/fscrypt.h:fscrypt_has_encryption_key() {
return fscrypt_get_inode_info(inode) != NULL;
}
Opening a directory via ext4_dir_open() does not set up i_crypt_info, nor
does looking up a top-level encrypted directory in an unencrypted parent or
adding a key via FS_IOC_ADD_ENCRYPTION_KEY on an already-open directory fd.
Should ext4_dirdata_set_lufid() call fscrypt_prepare_readdir(dir) (or
fscrypt_get_encryption_info(dir)) before checking
fscrypt_has_encryption_key(dir)?
> +
> + err = dquot_initialize(dir);
> + if (err)
> + return err;
> +
> + /*
> + * The lookup and the change must both be under the directory lock.
> + * The entry is deleted and re-added in the same directory, so the
> + * target inode itself is not changed and is not locked.
> + */
> + inode_lock_nested(dir, I_MUTEX_PARENT);
> +
> + /*
> + * Look up the entry first to get the inode for the permission
> + * checks, without holding a journal handle.
> + */
> + bh = ext4_find_entry(dir, &d_name, &de, NULL);
> + if (IS_ERR(bh)) {
> + err = PTR_ERR(bh);
> + bh = NULL;
> + goto out_unlock_dir;
> + }
> + if (!bh) {
> + err = -ENOENT;
> + goto out_unlock_dir;
> + }
> + ino = le32_to_cpu(de->inode);
> + brelse(bh);
> + bh = NULL;
> + de = NULL;
> +
> + inode = ext4_iget(sb, ino, EXT4_IGET_NORMAL);
> + if (IS_ERR(inode)) {
> + err = PTR_ERR(inode);
> + inode = NULL;
> + goto out_unlock_dir;
> + }
[Severity: Medium]
Should this lookup validate ino and the returned inode the same way
ext4_lookup() does?
In ext4_lookup(), before and after calling ext4_iget(), it checks for
invalid inode numbers, entries pointing back to the parent directory itself,
and deleted inodes returning -ESTALE:
fs/ext4/namei.c:ext4_lookup() {
if (!ext4_valid_inum(dir->i_sb, ino)) {
EXT4_ERROR_INODE(dir, "bad inode number: %u", ino);
return ERR_PTR(-EFSCORRUPTED);
}
if (unlikely(ino == dir->i_ino)) {
EXT4_ERROR_INODE(dir, "'%pd' linked to parent dir",
dentry);
return ERR_PTR(-EFSCORRUPTED);
}
inode = ext4_iget(dir->i_sb, ino, EXT4_IGET_NORMAL);
if (inode == ERR_PTR(-ESTALE)) {
EXT4_ERROR_INODE(dir,
"deleted inode referenced: %u",
ino);
return ERR_PTR(-EFSCORRUPTED);
}
}
Also, if a corrupted directory entry references an unlinked inode that is
still open in memory, ext4_iget() will return the cached inode with
inode->i_nlink == 0. Should ext4_dirdata_set_lufid() check for these cases
and report -EFSCORRUPTED instead of deleting and re-creating a corrupted
entry?
> +
> + /* Enforce sticky-bit restriction: on a sticky directory only the
> + * directory owner, the file owner, or a privileged process may
> + * modify entries. inode_permission(MAY_WRITE) alone does not cover
> + * this, so check it explicitly here as may_delete() does. */
> + err = check_sticky(idmap, dir, inode);
> + if (err)
> + goto out_iput;
[Severity: Medium]
Does this leak a positive return value (+1) to userspace when sticky-bit
permission is denied?
check_sticky() calls __check_sticky(), which returns 0 when allowed and 1
(not -EPERM) when denied:
fs/namei.c:__check_sticky() {
if (vfsuid_eq_kuid(i_uid_into_vfsuid(idmap, inode), fsuid))
return 0;
if (vfsuid_eq_kuid(i_uid_into_vfsuid(idmap, dir), fsuid))
return 0;
return !capable_wrt_inode_uidgid(idmap, inode, CAP_FOWNER);
}
Assigning that return value to err and jumping to out_iput causes
ext4_ioctl_set_lufid() to return 1 to userspace instead of -EPERM. Should
this set err = -EPERM when check_sticky(idmap, dir, inode) is non-zero?
[ ... ]
> + err = ext4_delete_entry(handle, dir, de, bh);
> + if (err)
> + goto out_brelse;
> +
> + brelse(bh);
> + bh = NULL;
> +
> + err = ext4_add_fname(handle, dir, &new_fname, inode);
> + if (err) {
> + /*
> + * The old entry fits in the space it was deleted from, so
> + * this needs no new block.
> + */
> + err2 = ext4_add_fname(handle, dir, &old_fname, inode);
> + if (err2)
> + EXT4_ERROR_INODE(dir,
> + "Failed to set LUFID on '%.*s' (err=%d) and failed to restore the original directory entry (err=%d); inode %llu may be orphaned",
> + namelen, filename, err, err2,
> + (unsigned long long)inode->i_ino);
> + goto out_journal;
> + }
[Severity: Critical]
Can the rollback call to ext4_add_fname(handle, dir, &old_fname, inode) fail
in an HTree-indexed directory and permanently lose the directory entry?
When looking up the existing entry, ext4_find_entry() ->
ext4_dx_find_entry() walks continuation leaf blocks via
ext4_htree_next_block() when a hash collision chain spans multiple blocks
(and __ext4_find_entry() can also fall back to a linear scan across all
directory blocks on casefolded directories or on ERR_BAD_DX_DIR).
However, when re-adding the entry, ext4_add_fname() -> ext4_dx_add_entry()
only probes the single primary leaf block returned by dx_probe() and never
walks continuation blocks:
fs/ext4/namei.c:ext4_dx_add_entry() {
frame = dx_probe(fname, dir, NULL, frames);
if (IS_ERR(frame))
return PTR_ERR(frame);
entries = frame->entries;
at = frame->at;
bh = ext4_read_dirblock(dir, dx_get_block(frame->at), DIRENT_HTREE);
...
err = add_dirent_to_buf(handle, fname, dir, inode, NULL, bh);
if (err != -ENOSPC)
goto cleanup;
...
de = do_split(handle, dir, &bh, frame, &fname->hinfo);
}
If the old entry resided in a continuation block (or was found via linear
fallback), ext4_delete_entry() frees space in that continuation block, while
the primary block returned by dx_probe() remains full. If splitting that
primary block in ext4_add_fname(&new_fname) then fails (for example due to
-ENOSPC, -EDQUOT, or reaching the maximum HTree level), the rollback call
ext4_add_fname(&old_fname) will probe the same full primary block, fail to
split it for the same reason, and commit the transaction with the entry
deleted and inode's link count leaked.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103134921.610047-1-ablagodarenko@thelustrecollective.com?part=12
^ permalink raw reply [flat|nested] 25+ messages in thread