[PATCH v12 00/11] Data in direntry (dirdata) feature

Artem Blagodarenko <[email protected]> Sat, 1 Aug 2026 14:12:14 -0400
Newsgroups org.kernel.vger.linux-ext4
Message-ID <[email protected]>
EXT4 currently stores an 8-byte hash in the directory entry immediately
after the file name to support simultaneous fscrypt and casefold
functionality.

The dirdata feature provides a general mechanism to store multiple
metadata records in each directory entry after the NUL filename
terminator. The unused high 4 bits of 'file_type' indicate which
records are present; each record starts with a 1-byte length field,
providing forward compatibility.

The first user of this feature is LUFID (Locally Unique File ID), an
identifier that must survive rename and be accessible from directory
readdir without an inode lookup.

e2fsprogs support is provided in a separate patch series.

Thanks to Sashiko AI review for identifying several correctness and
safety issues in earlier versions of this series.

Changes in v12:

Patch 09 (dirdata LUFID rename): fix late kmalloc(edp) in
ext4_rename().

When RENAME_WHITEOUT renames a LUFID-carrying entry into a slot that
does not yet exist (!new.bh), the edp allocation for ext4_add_entry()
was placed after ext4_setent(&old, ...) had already called
ext4_setent_compact_exts() to overwrite the LUFID bytes with CFHASH
data.  An -ENOMEM failure at that point sent the error path through
ext4_resetent(), which restores the LUFID flag from the saved
file_type but cannot restore the LUFID bytes — the same lossy state
described in the "Known limitation" comment in the whiteout error
path, but triggered deterministically by memory pressure rather than
requiring a journal IO error.

Fix by hoisting the edp allocation before ext4_setent() so that if it
fails nothing has been modified.  Move edp to function scope and free
it unconditionally at end_rename.  Update the "Known limitation"
comment to note that OOM can no longer trigger this path.

Reported-by: sashiko-bot <[email protected]>
Closes: https://sashiko.dev/#/patchset/[email protected]?part=11

Sashiko v11 findings NOT addressed (false positives):

Part 1 [High] — OOB read in ext4_htree_next_block when metadata_csum
is disabled.  Pre-existing, out of scope.

Part 2 [High] — OOB read when verifying a directory entry near the end
of a block buffer.  Pre-existing, not introduced by this series.

Part 4 [High] — get_dtype() broken by high file_type bits.  False
positive: EXT4_FT_MASK masks the upper bits correctly.

Part 4 [Critical] — file_type corrupted during VFS rename.  False
positive: ext4_setent() explicitly manages all dirdata flag transitions
via masking rather than full overwrite.

Part 5 [Low x2] — build failures: ext4_dir_entry_len() declaration and
dot_de reference in dx_root_limit().  False positives: sashiko reviewed
a transient version of the code that differs from the submitted patch.

Part 6 [Critical] — ext4_dir_rec_len() omitting hash space causes
overflow.  False positive: add_dirent_to_buf() explicitly adds
sizeof(struct ext4_dirent_hash) to dlen for dirdata+hash_in_dirent;
including it in both places would double-count.

Part 6 [High x4] — unvalidated rec_len to ext4_dirent_get_data_len();
name_len + dirdata integer truncation; length underflow in
make_indexed_dir() (pre-existing); rename corrupting dirdata flags.
All false positives: bounds are checked before the calls; truncation
cannot occur because dirdata is bounded by rec_len; flag management in
ext4_setent() uses masking.

Part 7 [High] — OOB read in ext4_read_inline_dir() via new
ext4_dir_entry_len() call.  False positive: size argument is correctly
bounded before reaching that path.

Part 7 [Critical] — double-fetch TOCTOU via DMA mutating de->rec_len.
False positive: DMA-mutating block device threat model does not apply
to standard ext4 usage.

Part 7 [Low x2] — build failures from ext4_find_dest_de() prototype
change and dx_show_leaf() missed rename.  False positives: series
builds cleanly; sashiko reviewed different source versions.

Part 8 [Low] — edp_dfid incompatible pointer type.  Fixed in patch 11
with explicit cast.

Part 8 [High x3] — uninitialized stack via fake '.' / '..' entries
(pre-existing); missing sizeof(dx_countlimit) in bounds check
(pre-existing); index corruption for un-migrated entries.  All
pre-existing or false positives.

Part 9 [High] — clearing EXT4_DIRENT_LUFID without compacting leaves
CFHASH at wrong offset.  False positive: ext4_setent_compact_exts()
physically slides CFHASH bytes into the vacated LUFID slot before
clearing the flag.

Part 9 [High] — missing READ_ONCE() on ddh_length in
ext4_lufid_snapshot().  False positive: dlen is a local unsigned int
variable; the compiler cannot re-fetch it from the on-disk structure
after it has been loaded into a register.

Part 10 [High x4] — uninitialized stack via fake entries (pre-existing);
OOB read parsing legacy hashes (pre-existing); memmove() in compact_exts
skips INO64 between LUFID and CFHASH; ext4_resetent() restores LUFID
flag but not bytes after compact_exts.

The INO64 concern is a false positive for the current series:
EXT4_DIRENT_INO64 is defined but no code path in this series writes
INO64 records to directory entries, so LUFID is always immediately
followed by CFHASH when both are present.

The resetent finding is the existing "Known limitation" already
documented in the whiteout error path — not a new finding.  It is
addressed in v12 for the OOM-triggered case (see Changes in v12 above);
the journal-IO-error case remains documented as a known limitation.

Part 11 [High] — unprivileged user can trigger filesystem read-only
remount by inducing memory pressure during SET_LUFID rollback.  Not a
regression; inherent in the delete-then-re-add approach shared with
other ext4 operations; per errors= mount option behavior is the
mitigation.

Part 11 [High] — case-equivalent encrypted filename re-encrypts to
larger ciphertext.  False positive: fscrypt ciphertext length equals
plaintext length for all supported modes, so a case-equivalent name
always produces an identical-length ciphertext.

Artem Blagodarenko (11):
  ext4: validate count against limit in ext4_dx_csum_verify/_set
  ext4: replace ext4_dir_entry with ext4_dir_entry_2
  ext4: add ext4_dir_entry_is_tail()
  ext4: add dirdata format definitions and access helpers
  ext4: refactor dx_root to support variable dirent sizes
  ext4: add ext4_dir_entry_len()
  ext4: rename ext4_dir_rec_len() and clarify dirdata usage
  ext4: dirdata feature
  ext4: add dirdata LUFID support for directory entry rename
  ext4: add dirdata set/get helpers
  ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory
    entries

 fs/ext4/dir.c             |   49 +-
 fs/ext4/ext4.h            |  245 ++++++-
 fs/ext4/fast_commit.c     |    1 +
 fs/ext4/fast_commit.h     |    1 +
 fs/ext4/inline.c          |   67 +-
 fs/ext4/ioctl.c           |   85 +++
 fs/ext4/namei.c           | 1281 +++++++++++++++++++++++++++++++------
 fs/ext4/sysfs.c           |    2 +
 include/uapi/linux/ext4.h |   14 +
 9 files changed, 1495 insertions(+), 250 deletions(-)

--
2.43.7