AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/08/01 10:15 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "EXT4_FS"
  ],
  "FocusSymbols": [
    "ext4_dirdata_set_lufid",
    "ext4_insert_dentry_data",
    "ext4_dirdata_get",
    "ext4_init_new_dir_data"
  ],
  "Reasoning": "The patch introduces a new ioctl EXT4_IOC_SET_LUFID and modifies the ext4 directory entry format to support 'dirdata' extensions (LUFID, INO64, CFHASH). This adds new reachable core filesystem logic for directory entry manipulation, lookup, and modification, which should be fuzzed.",
  "WorthFuzzing": true
}

1/1 2026/08/01 10:15 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit a24e41981bf68ae6ead3515d9750998d99ccdd73\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Sat Aug 1 10:15:43 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/fs/ext4/dir.c b/fs/ext4/dir.c\nindex 17edd678fa87b..81697fce5e303 100644\n--- a/fs/ext4/dir.c\n+++ b/fs/ext4/dir.c\n@@ -89,16 +89,15 @@ int __ext4_check_dir_entry(const char *function, unsigned int line,\n \tbool fake = is_fake_dir_entry(de);\n \tbool has_csum = ext4_has_feature_metadata_csum(dir-\u003ei_sb);\n \n-\tif (unlikely(rlen \u003c ext4_dir_rec_len(1, fake ? NULL : dir)))\n+\tif (unlikely(rlen \u003c ext4_dirent_rec_len(1, fake ? NULL : dir)))\n \t\terror_msg = \"rec_len is smaller than minimal\";\n \telse if (unlikely(rlen % 4 != 0))\n \t\terror_msg = \"rec_len % 4 != 0\";\n-\telse if (unlikely(rlen \u003c ext4_dir_rec_len(de-\u003ename_len,\n-\t\t\t\t\t\t\tfake ? NULL : dir)))\n-\t\terror_msg = \"rec_len is too small for name_len\";\n \telse if (unlikely(next_offset \u003e size))\n \t\terror_msg = \"directory entry overrun\";\n-\telse if (unlikely(next_offset \u003e size - ext4_dir_rec_len(1,\n+\telse if (unlikely(rlen \u003c ext4_dir_entry_len(de, size, fake ? NULL : dir)))\n+\t\terror_msg = \"rec_len is too small for name_len\";\n+\telse if (unlikely(next_offset \u003e size - ext4_dirent_rec_len(1,\n \t\t\t\t\t\t  has_csum ? NULL : dir) \u0026\u0026\n \t\t\t  next_offset != size))\n \t\terror_msg = \"directory entry too close to block end\";\n@@ -245,7 +244,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)\n \t\t\t\t * failure will be detected in the\n \t\t\t\t * dirent test below. */\n \t\t\t\tif (ext4_rec_len_from_disk(de-\u003erec_len,\n-\t\t\t\t\tsb-\u003es_blocksize) \u003c ext4_dir_rec_len(1,\n+\t\t\t\t\tsb-\u003es_blocksize) \u003c ext4_dirent_rec_len(1,\n \t\t\t\t\t\t\t\t\tinode))\n \t\t\t\t\tbreak;\n \t\t\t\ti += ext4_rec_len_from_disk(de-\u003erec_len,\n@@ -288,8 +287,19 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)\n \t\t\t\t\tu32 minor_hash;\n \n \t\t\t\t\tif (IS_CASEFOLDED(inode)) {\n-\t\t\t\t\t\thash = EXT4_DIRENT_HASH(de);\n-\t\t\t\t\t\tminor_hash = EXT4_DIRENT_MINOR_HASH(de);\n+\t\t\t\t\t\tif (ext4_has_feature_dirdata(sb) \u0026\u0026\n+\t\t\t\t\t\t    (de-\u003efile_type \u0026\n+\t\t\t\t\t\t     EXT4_DIRENT_CFHASH)) {\n+\t\t\t\t\t\t\tstruct dx_hash_info hi = {};\n+\t\t\t\t\t\t\text4_dirdata_get(de, inode,\n+\t\t\t\t\t\t\t\tsb-\u003es_blocksize,\n+\t\t\t\t\t\t\t\tNULL, \u0026hi);\n+\t\t\t\t\t\t\thash = hi.hash;\n+\t\t\t\t\t\t\tminor_hash = hi.minor_hash;\n+\t\t\t\t\t\t} else {\n+\t\t\t\t\t\t\thash = EXT4_DIRENT_HASH(de);\n+\t\t\t\t\t\t\tminor_hash = EXT4_DIRENT_MINOR_HASH(de);\n+\t\t\t\t\t\t}\n \t\t\t\t\t} else {\n \t\t\t\t\t\thash = 0;\n \t\t\t\t\t\tminor_hash = 0;\n@@ -468,27 +478,44 @@ void ext4_htree_free_dir_info(struct dir_private_info *p)\n  * The decrypted filename is passed in via ent_name.  parameter.\n  */\n int ext4_htree_store_dirent(struct file *dir_file, __u32 hash,\n-\t\t\t     __u32 minor_hash,\n+\t\t\t     __u32 minor_hash, int buf_size,\n \t\t\t    struct ext4_dir_entry_2 *dirent,\n \t\t\t    struct fscrypt_str *ent_name)\n {\n \tstruct rb_node **p, *parent = NULL;\n \tstruct fname *fname, *new_fn;\n \tstruct dir_private_info *info;\n+\tint extra_data = 0;\n \n \tinfo = dir_file-\u003eprivate_data;\n \tp = \u0026info-\u003eroot.rb_node;\n \n \t/* Create and allocate the fname structure */\n-\tnew_fn = kzalloc_flex(*new_fn, name, ent_name-\u003elen + 1);\n+\tif (ext4_has_feature_dirdata(dir_file-\u003ef_inode-\u003ei_sb) \u0026\u0026\n+\t    (dirent-\u003efile_type \u0026 ~EXT4_FT_MASK)) {\n+\t\tunsigned int rec_len =\n+\t\t\text4_rec_len_from_disk(dirent-\u003erec_len, buf_size);\n+\t\textra_data = ext4_dirent_get_data_len(dirent, rec_len);\n+\t}\n+\n+\t/* exta_data contains +1 byte for NULL */\n+\tnew_fn = kzalloc_flex(*new_fn, name, ent_name-\u003elen +\n+\t\t\t      (extra_data ? extra_data : 1));\n \tif (!new_fn)\n \t\treturn -ENOMEM;\n \tnew_fn-\u003ehash = hash;\n \tnew_fn-\u003eminor_hash = minor_hash;\n \tnew_fn-\u003einode = le32_to_cpu(dirent-\u003einode);\n-\tnew_fn-\u003ename_len = ent_name-\u003elen;\n \tnew_fn-\u003efile_type = dirent-\u003efile_type;\n+\tnew_fn-\u003ename_len = ent_name-\u003elen;\n \tmemcpy(new_fn-\u003ename, ent_name-\u003ename, ent_name-\u003elen);\n+\tif (extra_data)\n+\t\t/* Dirdata bytes lie past the __counted_by(name_len) bound but\n+\t\t * within the kzalloc_flex allocation; use unsafe_memcpy to\n+\t\t * avoid a FORTIFY_SOURCE false positive. */\n+\t\tunsafe_memcpy(new_fn-\u003ename + ent_name-\u003elen,\n+\t\t\t      dirent-\u003ename + dirent-\u003ename_len, extra_data,\n+\t\t\t      /* within kzalloc_flex allocation */);\n \n \twhile (*p) {\n \t\tparent = *p;\ndiff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h\nindex b37c136ea3ab3..e9447fb3f22c2 100644\n--- a/fs/ext4/ext4.h\n+++ b/fs/ext4/ext4.h\n@@ -2334,6 +2334,7 @@ EXT4_FEATURE_INCOMPAT_FUNCS(casefold,\t\tCASEFOLD)\n \t\t\t\t\t EXT4_FEATURE_INCOMPAT_FLEX_BG| \\\n \t\t\t\t\t EXT4_FEATURE_INCOMPAT_EA_INODE| \\\n \t\t\t\t\t EXT4_FEATURE_INCOMPAT_MMP | \\\n+\t\t\t\t\t EXT4_FEATURE_INCOMPAT_DIRDATA | \\\n \t\t\t\t\t EXT4_FEATURE_INCOMPAT_INLINE_DATA | \\\n \t\t\t\t\t EXT4_FEATURE_INCOMPAT_ENCRYPT | \\\n \t\t\t\t\t EXT4_FEATURE_INCOMPAT_CASEFOLD | \\\n@@ -2472,10 +2473,8 @@ static inline int ext4_emergency_state(struct super_block *sb)\n  * Structure of a directory entry\n  */\n #define EXT4_NAME_LEN 255\n-/*\n- * Base length of the ext4 directory entry excluding the name length\n- */\n-#define EXT4_BASE_DIR_LEN (sizeof(struct ext4_dir_entry_2) - EXT4_NAME_LEN)\n+/* offsetof avoids sizeof()+padding giving 9 instead of the correct on-disk 8 */\n+#define EXT4_BASE_DIR_LEN offsetof(struct ext4_dir_entry_2, name)\n \n struct ext4_dir_entry {\n \t__le32\tinode;\t\t\t/* Inode number */\n@@ -2556,6 +2555,61 @@ struct ext4_dir_entry_tail {\n #define EXT4_FT_SYMLINK\t\t7\n \n #define EXT4_FT_MAX\t\t8\n+#define EXT4_FT_MASK\t\t0xf\n+\n+#if EXT4_FT_MAX \u003e EXT4_FT_MASK\n+#error \"conflicting EXT4_FT_MAX and EXT4_FT_MASK\"\n+#endif\n+\n+/*\n+ * d_type has 4 unused bits, so it can hold four types of data. These different\n+ * types of data (e.g. fscypt hash, high 32 bits of 64-bit inode number) can be\n+ * stored, in flag order, after file-name in ext4 dirent.\n+ *\n+ * These flags are added to d_type if ext4 dirent has extra data after\n+ * filename. This data length is variable and length is stored in first byte\n+ * of data. Data starts after filename NUL byte.\n+ */\n+#define EXT4_DIRENT_LUFID\t\t0x10\n+#define EXT4_DIRENT_INO64\t\t0x20\n+#define EXT4_DIRENT_CFHASH\t\t0x40\n+\n+struct ext4_fid {\n+\tchar    fid[16];     /* 128-bit unique file identifier */\n+};\n+\n+struct ext4_dirent_data_header {\n+\t/* length of this header + the whole data blob */\n+\t__u8\tddh_length;\n+} __packed;\n+\n+struct ext4_dirent_fid {\n+\tstruct ext4_dirent_data_header df_header;\n+\tstruct ext4_fid                df_fid[];\n+};\n+\n+#define EXT4_LUFID_MAGIC    0xAD200907UL\n+struct ext4_dentry_param {\n+\t__u32\tedp_magic;\t/* EXT4_LUFID_MAGIC */\n+\t__u8\tedp_dfid[];\t/* struct ext4_dirent_fid payload */\n+};\n+\n+struct ext4_dirent_hash {\n+\tstruct ext4_dirent_data_header\tdh_header;\n+\tstruct ext4_dir_entry_hash\tdh_hash;\n+} __packed;\n+\n+static inline\n+struct ext4_dirent_fid *ext4_dentry_get_fid(struct super_block *sb,\n+\t\t\t\t\t    struct ext4_dentry_param *p)\n+{\n+\tif (!ext4_has_feature_dirdata(sb))\n+\t\treturn NULL;\n+\tif (p \u0026\u0026 p-\u003eedp_magic == EXT4_LUFID_MAGIC)\n+\t\treturn (struct ext4_dirent_fid *)p-\u003eedp_dfid;\n+\n+\treturn NULL;\n+}\n \n #define EXT4_FT_DIR_CSUM\t0xDE\n \n@@ -2573,13 +2627,26 @@ struct ext4_dir_entry_tail {\n  * casefolded and encrypted need to store the hash as well, so we add room for\n  * ext4_extended_dir_entry_2. For all entries related to '.' or '..' you should\n  * pass NULL for dir, as those entries do not use the extra fields.\n+ *\n+ * For directories with the dirdata feature, extra data may follow the filename.\n+ * Use ext4_dir_entry_len() to compute the length of a directory entry\n+ * including any dirdata, or ext4_dirent_rec_len() directly when the total\n+ * name_len (including dirdata length) is already known.\n  */\n-static inline unsigned int ext4_dir_rec_len(__u8 name_len,\n+static inline unsigned int ext4_dirent_rec_len(unsigned int name_len,\n \t\t\t\t\t\tconst struct inode *dir)\n {\n-\tint rec_len = (name_len + 8 + EXT4_DIR_ROUND);\n+\tunsigned int rec_len = (name_len + 8 + EXT4_DIR_ROUND);\n \n-\tif (dir \u0026\u0026 ext4_hash_in_dirent(dir))\n+\t/*\n+\t * Without dirdata, the casefold+fscrypt hash lives at a fixed position\n+\t * after the filename and must be reserved explicitly.  With dirdata the\n+\t * hash is stored as a CFHASH extension and is already counted in the\n+\t * name_len argument (via ext4_dirent_get_data_len), so adding it again\n+\t * would double-count.\n+\t */\n+\tif (dir \u0026\u0026 ext4_hash_in_dirent(dir) \u0026\u0026\n+\t    !ext4_has_feature_dirdata(dir-\u003ei_sb))\n \t\trec_len += sizeof(struct ext4_dir_entry_hash);\n \treturn (rec_len \u0026 ~EXT4_DIR_ROUND);\n }\n@@ -2667,6 +2734,8 @@ struct ext4_filename {\n #if IS_ENABLED(CONFIG_UNICODE)\n \tstruct qstr cf_name;\n #endif\n+\t/* LUFID/dirdata payload to embed in the new directory entry, or NULL */\n+\tstruct ext4_dirent_fid *dfid;\n };\n \n #define fname_name(p) ((p)-\u003edisk_name.name)\n@@ -2978,18 +3047,27 @@ extern int __ext4_check_dir_entry(const char *, unsigned int, struct inode *,\n \tunlikely(__ext4_check_dir_entry(__func__, __LINE__, (dir), (filp), \\\n \t\t\t\t(de), (bh), (buf), (size), (offset)))\n extern int ext4_htree_store_dirent(struct file *dir_file, __u32 hash,\n-\t\t\t\t__u32 minor_hash,\n+\t\t\t\t__u32 minor_hash, int buf_size,\n \t\t\t\tstruct ext4_dir_entry_2 *dirent,\n \t\t\t\tstruct fscrypt_str *ent_name);\n extern void ext4_htree_free_dir_info(struct dir_private_info *p);\n extern int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,\n \t\t\t     void *buf, int buf_size,\n \t\t\t     struct ext4_filename *fname,\n-\t\t\t     struct ext4_dir_entry_2 **dest_de);\n-void ext4_insert_dentry(struct inode *dir, struct inode *inode,\n-\t\t\tstruct ext4_dir_entry_2 *de,\n-\t\t\tint buf_size,\n-\t\t\tstruct ext4_filename *fname);\n+\t\t\t     struct ext4_dir_entry_2 **dest_de,\n+\t\t\t     int dlen);\n+int ext4_insert_dentry_data(struct inode *dir, struct inode *inode,\n+\t\t\t    struct ext4_dir_entry_2 *de,\n+\t\t\t    int buf_size,\n+\t\t\t    struct ext4_filename *fname,\n+\t\t\t    void *data);\n+static inline int ext4_insert_dentry(struct inode *dir, struct inode *inode,\n+\t\t\t\t     struct ext4_dir_entry_2 *de,\n+\t\t\t\t     int buf_size,\n+\t\t\t\t     struct ext4_filename *fname)\n+{\n+\treturn ext4_insert_dentry_data(dir, inode, de, buf_size, fname, NULL);\n+}\n static inline void ext4_update_dx_flag(struct inode *inode)\n {\n \tif (!ext4_has_feature_dir_index(inode-\u003ei_sb) \u0026\u0026\n@@ -3003,12 +3081,14 @@ static const unsigned char ext4_filetype_table[] = {\n \tDT_UNKNOWN, DT_REG, DT_DIR, DT_CHR, DT_BLK, DT_FIFO, DT_SOCK, DT_LNK\n };\n \n-static inline  unsigned char get_dtype(struct super_block *sb, int filetype)\n+static inline unsigned char get_dtype(struct super_block *sb, int filetype)\n {\n-\tif (!ext4_has_feature_filetype(sb) || filetype \u003e= EXT4_FT_MAX)\n+\tunsigned char fl_index = filetype \u0026 EXT4_FT_MASK;\n+\n+\tif (!ext4_has_feature_filetype(sb) || fl_index \u003e= EXT4_FT_MAX)\n \t\treturn DT_UNKNOWN;\n \n-\treturn ext4_filetype_table[filetype];\n+\treturn ext4_filetype_table[fl_index];\n }\n extern int ext4_check_all_de(struct inode *dir, struct buffer_head *bh,\n \t\t\t     void *buf, int buf_size);\n@@ -3231,10 +3311,19 @@ extern int ext4_ext_migrate(struct inode *);\n extern int ext4_ind_migrate(struct inode *inode);\n \n /* namei.c */\n-extern int ext4_init_new_dir(handle_t *handle, struct inode *dir,\n-\t\t\t     struct inode *inode);\n+extern int ext4_init_new_dir_data(handle_t *handle, struct inode *dir,\n+\t\t\t\t  struct inode *inode,\n+\t\t\t\t  const void *data1, const void *data2);\n+static inline int ext4_init_new_dir(handle_t *handle, struct inode *dir,\n+\t\t\t\t    struct inode *inode)\n+{\n+\treturn ext4_init_new_dir_data(handle, dir, inode, NULL, NULL);\n+}\n extern int ext4_dirblock_csum_verify(struct inode *inode,\n \t\t\t\t     struct buffer_head *bh);\n+extern int ext4_dirdata_set_lufid(struct mnt_idmap *idmap, struct inode *dir,\n+\t\t\t   const char *filename, int namelen,\n+\t\t\t   struct ext4_dentry_param *edp);\n extern int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,\n \t\t\t\t__u32 start_minor_hash, __u32 *next_hash);\n extern int ext4_search_dir(struct buffer_head *bh,\n@@ -3807,6 +3896,10 @@ extern int __ext4_unlink(struct inode *dir, const struct qstr *d_name,\n \t\t\t struct inode *inode, struct dentry *dentry);\n extern int __ext4_link(struct inode *dir, struct inode *inode,\n \t\t       const struct qstr *d_name, struct dentry *dentry);\n+extern unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de,\n+\t\t\t\t      struct inode *dir, int buf_size,\n+\t\t\t\t      struct ext4_dirent_fid  *lufid,\n+\t\t\t\t      struct dx_hash_info *hinfo);\n \n #define S_SHIFT 12\n static const unsigned char ext4_type_by_mode[(S_IFMT \u003e\u003e S_SHIFT) + 1] = {\n@@ -4004,6 +4097,107 @@ static inline void ext4_clear_io_unwritten_flag(ext4_io_end_t *io_end)\n \t\tio_end-\u003eflag \u0026= ~EXT4_IO_END_UNWRITTEN;\n }\n \n+/*\n+ * ext4_dir_entry_is_tail() - Check if a directory entry is a tail entry.\n+ * @de: directory entry to check\n+ *\n+ * Returns true if @de is a directory block tail entry (checksum record).\n+ */\n+static inline bool ext4_dir_entry_is_tail(struct ext4_dir_entry_2 *de)\n+{\n+\tstruct ext4_dir_entry_tail *t = (struct ext4_dir_entry_tail *)de;\n+\n+\treturn !t-\u003edet_reserved_zero1 \u0026\u0026\n+\t       le16_to_cpu(t-\u003edet_rec_len) == sizeof(*t) \u0026\u0026\n+\t       !t-\u003edet_reserved_zero2 \u0026\u0026\n+\t       t-\u003edet_reserved_ft == EXT4_FT_DIR_CSUM;\n+}\n+\n+/*\n+ * ext4_dirent_get_data_len() - Compute the total dirdata length for an entry.\n+ * @de: directory entry\n+ * @rec_len: the record length of the directory entry (decoded)\n+ *\n+ * Computes the length of optional data stored after the filename (and its\n+ * implicit NUL terminator).  Each extension is indicated by a bit in the\n+ * high 4 bits of de-\u003efile_type; the first byte of each extension is its\n+ * length (including that length byte itself).\n+ *\n+ * Returns 0 for tail entries and for entries with no dirdata.\n+ */\n+static inline int ext4_dirent_get_data_len(struct ext4_dir_entry_2 *de,\n+\t\t\t\t\t   unsigned int rec_len)\n+{\n+\t__u8 extra_data_flags;\n+\tstruct ext4_dirent_data_header *ddh;\n+\tint dlen = 0;\n+\tunsigned int offset;\n+\n+\tif (ext4_dir_entry_is_tail(de))\n+\t\treturn 0;\n+\n+\textra_data_flags = (de-\u003efile_type \u0026 ~EXT4_FT_MASK) \u003e\u003e 4;\n+\t/* offset from start of entry to after filename + NUL */\n+\toffset = EXT4_BASE_DIR_LEN + de-\u003ename_len + 1;\n+\n+\t/* bounds check: ensure we start reading within the entry */\n+\tif (offset \u003e= rec_len)\n+\t\treturn 0;\n+\n+\tddh = (struct ext4_dirent_data_header *)((char *)de + offset);\n+\n+\twhile (extra_data_flags) {\n+\t\tif (extra_data_flags \u0026 1) {\n+\t\t\t/* bounds check before reading ddh_length */\n+\t\t\tif (offset + sizeof(*ddh) \u003e rec_len)\n+\t\t\t\treturn 0;\n+\n+\t\t\t/* Use READ_ONCE to prevent the compiler from re-fetching\n+\t\t\t * this disk field and to guard against a concurrent\n+\t\t\t * write racing with our bounds check below (TOCTOU). */\n+\t\t\t{\n+\t\t\t\tu8 ddh_len = READ_ONCE(ddh-\u003eddh_length);\n+\n+\t\t\t\tif (ddh_len == 0 || ddh_len \u003e rec_len - offset)\n+\t\t\t\t\treturn 0;\n+\t\t\t\tdlen += ddh_len + (dlen == 0);\n+\t\t\t\toffset += ddh_len;\n+\t\t\t\t/* advance using the already-validated local copy\n+\t\t\t\t * to avoid re-fetching ddh-\u003eddh_length */\n+\t\t\t\tddh = (struct ext4_dirent_data_header *)\n+\t\t\t\t\t((char *)ddh + ddh_len);\n+\t\t\t}\n+\t\t}\n+\t\textra_data_flags \u003e\u003e= 1;\n+\t}\n+\treturn dlen;\n+}\n+\n+/*\n+ * ext4_dir_entry_len() - Compute the required rec_len for a directory entry.\n+ * @de:        directory entry (used to read name_len and any dirdata length)\n+ * @blocksize: size of the buffer @de lives in (the real directory block\n+ *             size, or the smaller inline-data buffer size for inline\n+ *             directories) -- used only to decode @de-\u003erec_len's \"0/65535\n+ *             means rest of buffer\" sentinel correctly.\n+ * @dir:       directory inode (may be NULL for '.' and '..' entries, which\n+ *             never carry the casefold+fscrypt hash regardless of the\n+ *             directory's feature flags)\n+ *\n+ * Returns the minimum record length needed to hold @de, rounded up to the\n+ * directory alignment and including room for the casefold+fscrypt hash if\n+ * the directory requires it.\n+ */\n+static inline unsigned int ext4_dir_entry_len(struct ext4_dir_entry_2 *de,\n+\t\t\t\t\t      unsigned int blocksize,\n+\t\t\t\t\t      const struct inode *dir)\n+{\n+\tunsigned int rec_len = ext4_rec_len_from_disk(de-\u003erec_len, blocksize);\n+\tunsigned int dirdata = ext4_dirent_get_data_len(de, rec_len);\n+\n+\treturn ext4_dirent_rec_len(de-\u003ename_len + dirdata, dir);\n+}\n+\n extern const struct iomap_ops ext4_iomap_ops;\n extern const struct iomap_ops ext4_iomap_report_ops;\n \ndiff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c\nindex 8e2259799614e..006d45e9ce879 100644\n--- a/fs/ext4/fast_commit.c\n+++ b/fs/ext4/fast_commit.c\n@@ -2697,6 +2697,7 @@ static const char * const fc_ineligible_reasons[] = {\n \t[EXT4_FC_REASON_MIGRATE] = \"Inode format migration\",\n \t[EXT4_FC_REASON_VERITY] = \"fs-verity enable\",\n \t[EXT4_FC_REASON_MOVE_EXT] = \"Move extents\",\n+\t[EXT4_FC_REASON_DIRDATA] = \"Dirdata LUFID (not recorded by fast-commit)\",\n };\n \n int ext4_fc_info_show(struct seq_file *seq, void *v)\ndiff --git a/fs/ext4/fast_commit.h b/fs/ext4/fast_commit.h\nindex 2f77a37fb1018..899faeb4e6e06 100644\n--- a/fs/ext4/fast_commit.h\n+++ b/fs/ext4/fast_commit.h\n@@ -100,6 +100,7 @@ enum {\n \tEXT4_FC_REASON_MIGRATE,\n \tEXT4_FC_REASON_VERITY,\n \tEXT4_FC_REASON_MOVE_EXT,\n+\tEXT4_FC_REASON_DIRDATA,\n \tEXT4_FC_REASON_MAX\n };\n \ndiff --git a/fs/ext4/inline.c b/fs/ext4/inline.c\nindex 8045e4ff270c7..ac984e576898b 100644\n--- a/fs/ext4/inline.c\n+++ b/fs/ext4/inline.c\n@@ -973,11 +973,20 @@ static int ext4_add_dirent_to_inline(handle_t *handle,\n \t\t\t\t     struct ext4_iloc *iloc,\n \t\t\t\t     void *inline_start, int inline_size)\n {\n-\tint\t\terr;\n+\tint\t\terr, dlen = 0;\n \tstruct ext4_dir_entry_2 *de;\n+\tstruct ext4_dirent_fid *dfid = NULL;\n+\n+\t/* fname-\u003edfid was set in ext4_add_entry() */\n+\tdfid = fname-\u003edfid;\n+\tif (dfid)\n+\t\tdlen = dfid-\u003edf_header.ddh_length;\n+\tif (ext4_hash_in_dirent(dir) \u0026\u0026\n+\t    ext4_has_feature_dirdata(dir-\u003ei_sb))\n+\t\tdlen += sizeof(struct ext4_dirent_hash);\n \n \terr = ext4_find_dest_de(dir, iloc-\u003ebh, inline_start,\n-\t\t\t\tinline_size, fname, \u0026de);\n+\t\t\t\tinline_size, fname, \u0026de, dlen);\n \tif (err)\n \t\treturn err;\n \n@@ -986,7 +995,9 @@ static int ext4_add_dirent_to_inline(handle_t *handle,\n \t\t\t\t\t    EXT4_JTR_NONE);\n \tif (err)\n \t\treturn err;\n-\text4_insert_dentry(dir, inode, de, inline_size, fname);\n+\terr = ext4_insert_dentry_data(dir, inode, de, inline_size, fname, dfid);\n+\tif (err)\n+\t\treturn err;\n \n \text4_show_inline_dir(dir, iloc-\u003ebh, inline_start, inline_size);\n \n@@ -1055,7 +1066,7 @@ static int ext4_update_inline_dir(handle_t *handle, struct inode *dir,\n \tint old_size = EXT4_I(dir)-\u003ei_inline_size - EXT4_MIN_INLINE_DATA_SIZE;\n \tint new_size = get_max_inline_xattr_value_size(dir, iloc);\n \n-\tif (new_size - old_size \u003c= ext4_dir_rec_len(1, NULL))\n+\tif (new_size - old_size \u003c= ext4_dirent_rec_len(1, NULL))\n \t\treturn -ENOSPC;\n \n \tret = ext4_update_inline_data(handle, dir,\n@@ -1264,6 +1275,7 @@ int ext4_inlinedir_to_tree(struct file *dir_file,\n \tint err = 0, count = 0;\n \tunsigned int parent_ino;\n \tint pos;\n+\tunsigned int de_len;\n \tstruct ext4_dir_entry_2 *de;\n \tstruct inode *inode = file_inode(dir_file);\n \tint ret, inline_size = 0;\n@@ -1309,7 +1321,7 @@ int ext4_inlinedir_to_tree(struct file *dir_file,\n \t\t\tfake.name_len = 1;\n \t\t\tmemcpy(fake.name, \".\", 2);\n \t\t\tfake.rec_len = ext4_rec_len_to_disk(\n-\t\t\t\t\t  ext4_dir_rec_len(fake.name_len, NULL),\n+\t\t\t\t\t  ext4_dirent_rec_len(fake.name_len, NULL),\n \t\t\t\t\t  inline_size);\n \t\t\text4_set_de_type(inode-\u003ei_sb, \u0026fake, S_IFDIR);\n \t\t\tde = \u0026fake;\n@@ -1319,30 +1331,46 @@ int ext4_inlinedir_to_tree(struct file *dir_file,\n \t\t\tfake.name_len = 2;\n \t\t\tmemcpy(fake.name, \"..\", 3);\n \t\t\tfake.rec_len = ext4_rec_len_to_disk(\n-\t\t\t\t\t  ext4_dir_rec_len(fake.name_len, NULL),\n+\t\t\t\t\t  ext4_dirent_rec_len(fake.name_len, NULL),\n \t\t\t\t\t  inline_size);\n \t\t\text4_set_de_type(inode-\u003ei_sb, \u0026fake, S_IFDIR);\n \t\t\tde = \u0026fake;\n \t\t\tpos = EXT4_INLINE_DOTDOT_SIZE;\n \t\t} else {\n+\t\t\tif (pos + EXT4_BASE_DIR_LEN \u003e inline_size) {\n+\t\t\t\tret = count;\n+\t\t\t\tgoto out;\n+\t\t\t}\n \t\t\tde = (struct ext4_dir_entry_2 *)(dir_buf + pos);\n-\t\t\tpos += ext4_rec_len_from_disk(de-\u003erec_len, inline_size);\n+\t\t\tde_len = ext4_rec_len_from_disk(de-\u003erec_len, inline_size);\n+\t\t\tif (de_len \u003c EXT4_BASE_DIR_LEN ||\n+\t\t\t    pos + de_len \u003e (unsigned int)inline_size) {\n+\t\t\t\tret = count;\n+\t\t\t\tgoto out;\n+\t\t\t}\n \t\t\tif (ext4_check_dir_entry(inode, dir_file, de,\n \t\t\t\t\t iloc.bh, dir_buf,\n \t\t\t\t\t inline_size, pos)) {\n \t\t\t\tret = count;\n \t\t\t\tgoto out;\n \t\t\t}\n+\t\t\tpos += de_len;\n \t\t}\n \n-\t\tif (ext4_hash_in_dirent(dir)) {\n-\t\t\thinfo-\u003ehash = EXT4_DIRENT_HASH(de);\n-\t\t\thinfo-\u003eminor_hash = EXT4_DIRENT_MINOR_HASH(de);\n-\t\t} else {\n-\t\t\terr = ext4fs_dirhash(dir, de-\u003ename, de-\u003ename_len, hinfo);\n-\t\t\tif (err) {\n-\t\t\t\tret = err;\n-\t\t\t\tgoto out;\n+\t\tif (!(ext4_dirdata_get(de, dir, inline_size, NULL, hinfo) \u0026\n+\t\t\t\t\t\t\tEXT4_DIRENT_CFHASH)) {\n+\t\t\tif (ext4_hash_in_dirent(dir)) {\n+\t\t\t\t/* Un-migrated entry: hash at legacy fixed\n+\t\t\t\t * offset. */\n+\t\t\t\thinfo-\u003ehash = EXT4_DIRENT_HASH(de);\n+\t\t\t\thinfo-\u003eminor_hash = EXT4_DIRENT_MINOR_HASH(de);\n+\t\t\t} else {\n+\t\t\t\terr = ext4fs_dirhash(dir, de-\u003ename,\n+\t\t\t\t\t\t     de-\u003ename_len, hinfo);\n+\t\t\t\tif (err) {\n+\t\t\t\t\tret = err;\n+\t\t\t\t\tgoto out;\n+\t\t\t\t}\n \t\t\t}\n \t\t}\n \t\tif ((hinfo-\u003ehash \u003c start_hash) ||\n@@ -1354,7 +1382,8 @@ int ext4_inlinedir_to_tree(struct file *dir_file,\n \t\ttmp_str.name = de-\u003ename;\n \t\ttmp_str.len = de-\u003ename_len;\n \t\terr = ext4_htree_store_dirent(dir_file, hinfo-\u003ehash,\n-\t\t\t\t\t      hinfo-\u003eminor_hash, de, \u0026tmp_str);\n+\t\t\t\t\t      hinfo-\u003eminor_hash, inline_size,\n+\t\t\t\t\t      de, \u0026tmp_str);\n \t\tif (err) {\n \t\t\tret = err;\n \t\t\tgoto out;\n@@ -1427,8 +1456,8 @@ int ext4_read_inline_dir(struct file *file,\n \t * So we will use extra_offset and extra_size to indicate them\n \t * during the inline dir iteration.\n \t */\n-\tdotdot_offset = ext4_dir_rec_len(1, NULL);\n-\tdotdot_size = dotdot_offset + ext4_dir_rec_len(2, NULL);\n+\tdotdot_offset = ext4_dirent_rec_len(1, NULL);\n+\tdotdot_size = dotdot_offset + ext4_dirent_rec_len(2, NULL);\n \textra_offset = dotdot_size - EXT4_INLINE_DOTDOT_SIZE;\n \textra_size = extra_offset + inline_size;\n \n@@ -1463,7 +1492,7 @@ int ext4_read_inline_dir(struct file *file,\n \t\t\t * failure will be detected in the\n \t\t\t * dirent test below. */\n \t\t\tif (ext4_rec_len_from_disk(de-\u003erec_len, extra_size)\n-\t\t\t\t\u003c ext4_dir_rec_len(1, NULL))\n+\t\t\t\t\u003c ext4_dirent_rec_len(1, NULL))\n \t\t\t\tbreak;\n \t\t\ti += ext4_rec_len_from_disk(de-\u003erec_len,\n \t\t\t\t\t\t    extra_size);\ndiff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c\nindex c8387e6a2c6e9..5bac4ad834ad1 100644\n--- a/fs/ext4/ioctl.c\n+++ b/fs/ext4/ioctl.c\n@@ -1535,6 +1535,88 @@ static int ext4_ioctl_set_tune_sb(struct file *filp,\n \treturn ret;\n }\n \n+/*\n+ * ext4_ioctl_set_lufid() - Set LUFID on a directory entry\n+ * @filp:\tfile pointer (parent directory)\n+ * @arg:\tpointer to ext4_set_lufid structure with filename and LUFID data\n+ *\n+ * This ioctl allows setting LUFID data on an existing\n+ * directory entry. It is called on the parent directory with a filename and\n+ * LUFID data.\n+ */\n+static long ext4_ioctl_set_lufid(struct file *filp, unsigned long arg)\n+{\n+\tstruct inode *dir = file_inode(filp);\n+\tstruct mnt_idmap *idmap = file_mnt_idmap(filp);\n+\tstruct ext4_set_lufid lufid_args;\n+\tstruct {\n+\t\t__u32 edp_magic;\n+\t\tstruct ext4_dirent_data_header df_header;\n+\t\tchar df_fid[255];\n+\t} edp;\n+\tint err;\n+\n+\t/* Check if parent is a directory */\n+\tif (!S_ISDIR(dir-\u003ei_mode))\n+\t\treturn -ENOTDIR;\n+\n+\t/* This ioctl mutates directory entries; merely having the directory\n+\t * open (which only ever requires read access) is not enough.\n+\t * MAY_EXEC is required for entry lookup; MAY_WRITE for modification. */\n+\terr = inode_permission(idmap, dir, MAY_WRITE | MAY_EXEC);\n+\tif (err)\n+\t\treturn err;\n+\n+\t/* Copy arguments from user space */\n+\tif (copy_from_user(\u0026lufid_args, (struct ext4_set_lufid __user *)arg,\n+\t\t\t   sizeof(lufid_args)))\n+\t\treturn -EFAULT;\n+\n+\t/* Validate parameters.  esl_name_len is NUL-excluded length (1-255). */\n+\tif (lufid_args.esl_name_len == 0 || lufid_args.esl_name_len \u003e EXT4_NAME_LEN)\n+\t\treturn -EINVAL;\n+\n+\t/* ddh_length (esl_data_len + the header byte below) must itself fit\n+\t * in the __u8 ddh_length field without wrapping */\n+\tif (lufid_args.esl_data_len == 0 ||\n+\t    lufid_args.esl_data_len \u003e 255 - sizeof(edp.df_header))\n+\t\treturn -EINVAL;\n+\n+\t/* Ensure filename is NUL-terminated at exactly esl_name_len */\n+\tif (lufid_args.esl_name[lufid_args.esl_name_len] != '\\0')\n+\t\treturn -EINVAL;\n+\n+\t/* '.' and '..' are not ordinary entries -- they must stay the first\n+\t * two entries in the directory's first block, so they can't go\n+\t * through the general delete+re-add path this ioctl uses */\n+\tif (!strcmp(lufid_args.esl_name, \".\") || !strcmp(lufid_args.esl_name, \"..\"))\n+\t\treturn -EINVAL;\n+\n+\t/* Prepare the dentry param struct with LUFID data. ddh_length is\n+\t * documented (see struct ext4_dirent_data_header) as the length of\n+\t * the header plus the whole data blob -- include the header here so\n+\t * every dirdata reader/writer that takes ddh_length at face value\n+\t * (e.g. ext4_dirdata_set()'s memcpy) copies the full LUFID payload\n+\t * instead of silently dropping its last byte. */\n+\tedp.edp_magic = EXT4_LUFID_MAGIC;\n+\tedp.df_header.ddh_length = lufid_args.esl_data_len +\n+\t\t\t\t    sizeof(edp.df_header);\n+\tmemcpy(edp.df_fid, lufid_args.esl_data, lufid_args.esl_data_len);\n+\n+\t/* Want write access */\n+\terr = mnt_want_write_file(filp);\n+\tif (err)\n+\t\treturn err;\n+\n+\t/* Call the helper function to do the actual work */\n+\terr = ext4_dirdata_set_lufid(idmap, dir, lufid_args.esl_name,\n+\t\t\t\t    lufid_args.esl_name_len,\n+\t\t\t\t    (struct ext4_dentry_param *)\u0026edp);\n+\n+\tmnt_drop_write_file(filp);\n+\treturn err;\n+}\n+\n static long __ext4_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)\n {\n \tstruct inode *inode = file_inode(filp);\n@@ -1921,6 +2003,8 @@ static long __ext4_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)\n \t\t\t\t\t      (void __user *)arg);\n \tcase EXT4_IOC_SET_TUNE_SB_PARAM:\n \t\treturn ext4_ioctl_set_tune_sb(filp, (void __user *)arg);\n+\tcase EXT4_IOC_SET_LUFID:\n+\t\treturn ext4_ioctl_set_lufid(filp, arg);\n \tdefault:\n \t\treturn -ENOTTY;\n \t}\n@@ -2000,6 +2084,7 @@ long ext4_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)\n \tcase FS_IOC_SETFSLABEL:\n \tcase EXT4_IOC_GETFSUUID:\n \tcase EXT4_IOC_SETFSUUID:\n+\tcase EXT4_IOC_SET_LUFID:\n \t\tbreak;\n \tdefault:\n \t\treturn -ENOIOCTLCMD;\ndiff --git a/fs/ext4/namei.c b/fs/ext4/namei.c\nindex cc49ae04a6f64..2024de1efa715 100644\n--- a/fs/ext4/namei.c\n+++ b/fs/ext4/namei.c\n@@ -102,7 +102,7 @@ static struct buffer_head *ext4_append(handle_t *handle,\n }\n \n static int ext4_dx_csum_verify(struct inode *inode,\n-\t\t\t       struct ext4_dir_entry *dirent);\n+\t\t\t       struct ext4_dir_entry_2 *dirent);\n \n /*\n  * Hints to ext4_read_dirblock regarding whether we expect a directory\n@@ -128,7 +128,7 @@ static struct buffer_head *__ext4_read_dirblock(struct inode *inode,\n \t\t\t\t\t\tunsigned int line)\n {\n \tstruct buffer_head *bh;\n-\tstruct ext4_dir_entry *dirent;\n+\tstruct ext4_dir_entry_2 *dirent;\n \tint is_dx_block = 0;\n \n \tif (block \u003e= inode-\u003ei_size \u003e\u003e inode-\u003ei_blkbits) {\n@@ -160,7 +160,7 @@ static struct buffer_head *__ext4_read_dirblock(struct inode *inode,\n \t}\n \tif (!bh)\n \t\treturn NULL;\n-\tdirent = (struct ext4_dir_entry *) bh-\u003eb_data;\n+\tdirent = (struct ext4_dir_entry_2 *) bh-\u003eb_data;\n \t/* Determine whether or not we have an index block */\n \tif (is_dx(inode)) {\n \t\tif (block == 0)\n@@ -244,22 +244,13 @@ struct dx_entry\n  * hash version mod 4 should never be 0.  Sincerely, the paranoia department.\n  */\n \n-struct dx_root\n+struct dx_root_info\n {\n-\tstruct fake_dirent dot;\n-\tchar dot_name[4];\n-\tstruct fake_dirent dotdot;\n-\tchar dotdot_name[4];\n-\tstruct dx_root_info\n-\t{\n-\t\t__le32 reserved_zero;\n-\t\tu8 hash_version;\n-\t\tu8 info_length; /* 8 */\n-\t\tu8 indirect_levels;\n-\t\tu8 unused_flags;\n-\t}\n-\tinfo;\n-\tstruct dx_entry\tentries[];\n+\t__le32 reserved_zero;\n+\tu8 hash_version;\n+\tu8 info_length; /* 8 */\n+\tu8 indirect_levels;\n+\tu8 unused_flags;\n };\n \n struct dx_node\n@@ -317,13 +308,13 @@ static struct ext4_dir_entry_tail *get_dirent_tail(struct inode *inode,\n \tint blocksize = EXT4_BLOCK_SIZE(inode-\u003ei_sb);\n \n #ifdef PARANOID\n-\tstruct ext4_dir_entry *d, *top;\n+\tstruct ext4_dir_entry_2 *d, *top;\n \n-\td = (struct ext4_dir_entry *)bh-\u003eb_data;\n-\ttop = (struct ext4_dir_entry *)(bh-\u003eb_data +\n+\td = (struct ext4_dir_entry_2 *)bh-\u003eb_data;\n+\ttop = (struct ext4_dir_entry_2 *)(bh-\u003eb_data +\n \t\t(blocksize - sizeof(struct ext4_dir_entry_tail)));\n \twhile (d \u003c top \u0026\u0026 ext4_rec_len_from_disk(d-\u003erec_len, blocksize))\n-\t\td = (struct ext4_dir_entry *)(((void *)d) +\n+\t\td = (struct ext4_dir_entry_2 *)(((void *)d) +\n \t\t    ext4_rec_len_from_disk(d-\u003erec_len, blocksize));\n \n \tif (d != top)\n@@ -331,14 +322,10 @@ static struct ext4_dir_entry_tail *get_dirent_tail(struct inode *inode,\n \n \tt = (struct ext4_dir_entry_tail *)d;\n #else\n-\tt = EXT4_DIRENT_TAIL(bh-\u003eb_data, EXT4_BLOCK_SIZE(inode-\u003ei_sb));\n+\tt = EXT4_DIRENT_TAIL(bh-\u003eb_data, blocksize);\n #endif\n \n-\tif (t-\u003edet_reserved_zero1 ||\n-\t    (ext4_rec_len_from_disk(t-\u003edet_rec_len, blocksize) !=\n-\t     sizeof(struct ext4_dir_entry_tail)) ||\n-\t    t-\u003edet_reserved_zero2 ||\n-\t    t-\u003edet_reserved_ft != EXT4_FT_DIR_CSUM)\n+\tif (!ext4_dir_entry_is_tail((struct ext4_dir_entry_2 *)t))\n \t\treturn NULL;\n \n \treturn t;\n@@ -410,35 +397,44 @@ int ext4_handle_dirty_dirblock(handle_t *handle,\n }\n \n static struct dx_countlimit *get_dx_countlimit(struct inode *inode,\n-\t\t\t\t\t       struct ext4_dir_entry *dirent,\n+\t\t\t\t\t       struct ext4_dir_entry_2 *dirent,\n \t\t\t\t\t       int *offset)\n {\n-\tstruct ext4_dir_entry *dp;\n+\tstruct ext4_dir_entry_2 *de;\n \tstruct dx_root_info *root;\n-\tint count_offset;\n+\tint count_offset, dotdot_rec_len;\n \tint blocksize = EXT4_BLOCK_SIZE(inode-\u003ei_sb);\n \tunsigned int rlen = ext4_rec_len_from_disk(dirent-\u003erec_len, blocksize);\n \n-\tif (rlen == blocksize)\n-\t\tcount_offset = 8;\n-\telse if (rlen == 12) {\n-\t\tdp = (struct ext4_dir_entry *)(((void *)dirent) + 12);\n-\t\tif (ext4_rec_len_from_disk(dp-\u003erec_len, blocksize) != blocksize - 12)\n+\tif (rlen == blocksize) {\n+\t\tcount_offset = sizeof(struct dx_node);\n+\t} else {\n+\t\tif (rlen \u003c EXT4_BASE_DIR_LEN ||\n+\t\t    rlen + EXT4_BASE_DIR_LEN \u003e blocksize)\n \t\t\treturn NULL;\n-\t\troot = (struct dx_root_info *)(((void *)dp + 12));\n+\t\tde = (struct ext4_dir_entry_2 *)(((char *)dirent) + rlen);\n+\t\tif (ext4_rec_len_from_disk(de-\u003erec_len, blocksize) !=\n+\t\t    (unsigned int)(blocksize - rlen))\n+\t\t\treturn NULL;\n+\t\t/* de-\u003erec_len covers whole dx_root block, calculate actual length.\n+\t\t * This is the '..' entry, which never carries the casefold+fscrypt\n+\t\t * hash, so pass NULL for dir regardless of the directory's flags */\n+\t\tdotdot_rec_len = ext4_dir_entry_len(de, blocksize, NULL);\n+\t\tif (rlen + dotdot_rec_len + sizeof(struct dx_root_info) \u003e blocksize)\n+\t\t\treturn NULL;\n+\t\troot = (struct dx_root_info *)(((char *)de + dotdot_rec_len));\n \t\tif (root-\u003ereserved_zero ||\n \t\t    root-\u003einfo_length != sizeof(struct dx_root_info))\n \t\t\treturn NULL;\n-\t\tcount_offset = 32;\n-\t} else\n-\t\treturn NULL;\n+\t\tcount_offset = root-\u003einfo_length + rlen + dotdot_rec_len;\n+\t}\n \n \tif (offset)\n \t\t*offset = count_offset;\n \treturn (struct dx_countlimit *)(((void *)dirent) + count_offset);\n }\n \n-static __le32 ext4_dx_csum(struct inode *inode, struct ext4_dir_entry *dirent,\n+static __le32 ext4_dx_csum(struct inode *inode, struct ext4_dir_entry_2 *dirent,\n \t\t\t   int count_offset, int count, struct dx_tail *t)\n {\n \tstruct ext4_inode_info *ei = EXT4_I(inode);\n@@ -456,7 +452,7 @@ static __le32 ext4_dx_csum(struct inode *inode, struct ext4_dir_entry *dirent,\n }\n \n static int ext4_dx_csum_verify(struct inode *inode,\n-\t\t\t       struct ext4_dir_entry *dirent)\n+\t\t\t       struct ext4_dir_entry_2 *dirent)\n {\n \tstruct dx_countlimit *c;\n \tstruct dx_tail *t;\n@@ -477,6 +473,11 @@ static int ext4_dx_csum_verify(struct inode *inode,\n \t\twarn_no_space_for_csum(inode);\n \t\treturn 0;\n \t}\n+\tif (count \u003e limit) {\n+\t\tEXT4_ERROR_INODE(inode, \"dir seems corrupt (%i \u003e %i)? \"\n+\t\t\t\t \"Run e2fsck -D.\", count, limit);\n+\t\treturn 0;\n+\t}\n \tt = (struct dx_tail *)(((struct dx_entry *)c) + limit);\n \n \tif (t-\u003edt_checksum != ext4_dx_csum(inode, dirent, count_offset,\n@@ -485,7 +486,7 @@ static int ext4_dx_csum_verify(struct inode *inode,\n \treturn 1;\n }\n \n-static void ext4_dx_csum_set(struct inode *inode, struct ext4_dir_entry *dirent)\n+static void ext4_dx_csum_set(struct inode *inode, struct ext4_dir_entry_2 *dirent)\n {\n \tstruct dx_countlimit *c;\n \tstruct dx_tail *t;\n@@ -506,6 +507,11 @@ static void ext4_dx_csum_set(struct inode *inode, struct ext4_dir_entry *dirent)\n \t\twarn_no_space_for_csum(inode);\n \t\treturn;\n \t}\n+\tif (count \u003e limit) {\n+\t\tEXT4_ERROR_INODE(inode, \"dir seems corrupt (%i \u003e %i)? \"\n+\t\t\t\t \" Run e2fsck -D.\", count, limit);\n+\t\treturn;\n+\t}\n \tt = (struct dx_tail *)(((struct dx_entry *)c) + limit);\n \n \tt-\u003edt_checksum = ext4_dx_csum(inode, dirent, count_offset, count, t);\n@@ -515,7 +521,7 @@ static inline int ext4_handle_dirty_dx_node(handle_t *handle,\n \t\t\t\t\t    struct inode *inode,\n \t\t\t\t\t    struct buffer_head *bh)\n {\n-\text4_dx_csum_set(inode, (struct ext4_dir_entry *)bh-\u003eb_data);\n+\text4_dx_csum_set(inode, (struct ext4_dir_entry_2 *)bh-\u003eb_data);\n \treturn ext4_handle_dirty_metadata(handle, inode, bh);\n }\n \n@@ -533,6 +539,37 @@ ext4_next_entry(struct ext4_dir_entry_2 *p, unsigned long blocksize)\n  * Future: use high four bits of block for coalesce-on-delete flags\n  * Mask them off for now.\n  */\n+static struct dx_root_info *dx_get_dx_info(struct inode *dir, void *de_buf)\n+{\n+\tunsigned int blocksize = dir-\u003ei_sb-\u003es_blocksize;\n+\tvoid *base = de_buf;\n+\tunsigned int rlen;\n+\n+\t/* '.' and '..' never carry the casefold+fscrypt hash, so pass NULL\n+\t * for dir regardless of the directory's flags */\n+\trlen = ext4_rec_len_from_disk(((struct ext4_dir_entry_2 *)de_buf)-\u003erec_len,\n+\t\t\t\t      blocksize);\n+\tif (rlen \u003c EXT4_BASE_DIR_LEN || rlen \u003e blocksize)\n+\t\treturn ERR_PTR(-EFSCORRUPTED);\n+\tde_buf += ext4_dir_entry_len(de_buf, blocksize, NULL);\n+\n+\t/* dx root info is after dotdot entry */\n+\tif (de_buf \u003c base || (char *)de_buf - (char *)base +\n+\t    EXT4_BASE_DIR_LEN \u003e blocksize)\n+\t\treturn ERR_PTR(-EFSCORRUPTED);\n+\trlen = ext4_rec_len_from_disk(((struct ext4_dir_entry_2 *)de_buf)-\u003erec_len,\n+\t\t\t\t      blocksize);\n+\tif (rlen \u003c EXT4_BASE_DIR_LEN ||\n+\t    (char *)de_buf - (char *)base + rlen \u003e blocksize)\n+\t\treturn ERR_PTR(-EFSCORRUPTED);\n+\tde_buf += ext4_dir_entry_len(de_buf, blocksize, NULL);\n+\n+\tif (de_buf \u003c base || (char *)de_buf - (char *)base +\n+\t\t\t      sizeof(struct dx_root_info) \u003e blocksize)\n+\t\treturn ERR_PTR(-EFSCORRUPTED);\n+\n+\treturn (struct dx_root_info *)de_buf;\n+}\n \n static inline ext4_lblk_t dx_get_block(struct dx_entry *entry)\n {\n@@ -574,11 +611,20 @@ static inline void dx_set_limit(struct dx_entry *entries, unsigned value)\n \t((struct dx_countlimit *) entries)-\u003elimit = cpu_to_le16(value);\n }\n \n-static inline unsigned dx_root_limit(struct inode *dir, unsigned infosize)\n+static inline unsigned dx_root_limit(struct inode *dir,\n+\tstruct ext4_dir_entry_2 *dot_de)\n {\n-\tunsigned int entry_space = dir-\u003ei_sb-\u003es_blocksize -\n-\t\t\text4_dir_rec_len(1, NULL) -\n-\t\t\text4_dir_rec_len(2, NULL) - infosize;\n+\tstruct dx_root_info *info;\n+\tunsigned int entry_space;\n+\n+\tinfo = dx_get_dx_info(dir, dot_de);\n+\tif (IS_ERR(info))\n+\t\treturn 0;\n+\tif ((char *)info - (char *)dot_de + info-\u003einfo_length \u003e\n+\t    dir-\u003ei_sb-\u003es_blocksize)\n+\t\treturn 0;\n+\tentry_space = dir-\u003ei_sb-\u003es_blocksize - ((char *)info - (char *)dot_de) -\n+\t\tinfo-\u003einfo_length;\n \n \tif (ext4_has_feature_metadata_csum(dir-\u003ei_sb))\n \t\tentry_space -= sizeof(struct dx_tail);\n@@ -587,8 +633,10 @@ static inline unsigned dx_root_limit(struct inode *dir, unsigned infosize)\n \n static inline unsigned dx_node_limit(struct inode *dir)\n {\n+\t/* dx_node fake_dirent is always 8 bytes — it never carries a casefold\n+\t * hash, so pass NULL to suppress the hash-size term. */\n \tunsigned int entry_space = dir-\u003ei_sb-\u003es_blocksize -\n-\t\t\text4_dir_rec_len(0, dir);\n+\t\t\text4_dirent_rec_len(0, NULL);\n \n \tif (ext4_has_feature_metadata_csum(dir-\u003ei_sb))\n \t\tentry_space -= sizeof(struct dx_tail);\n@@ -698,7 +746,9 @@ static struct stats dx_show_leaf(struct inode *dir,\n \t\t\t\t       (unsigned) ((char *) de - base));\n #endif\n \t\t\t}\n-\t\t\tspace += ext4_dir_rec_len(de-\u003ename_len, dir);\n+\t\t\tif ((char *)de + EXT4_BASE_DIR_LEN \u003e base + size)\n+\t\t\t\tbreak;\n+\t\t\tspace += ext4_dir_entry_len(de, size, dir);\n \t\t\tnames++;\n \t\t}\n \t\tde = ext4_next_entry(de, size);\n@@ -780,7 +830,7 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,\n {\n \tunsigned count, indirect, level, i;\n \tstruct dx_entry *at, *entries, *p, *q, *m;\n-\tstruct dx_root *root;\n+\tstruct dx_root_info *info;\n \tstruct dx_frame *frame = frame_in;\n \tstruct dx_frame *ret_err = ERR_PTR(ERR_BAD_DX_DIR);\n \tu32 hash;\n@@ -792,23 +842,31 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,\n \tif (IS_ERR(frame-\u003ebh))\n \t\treturn (struct dx_frame *) frame-\u003ebh;\n \n-\troot = (struct dx_root *) frame-\u003ebh-\u003eb_data;\n-\tif (root-\u003einfo.hash_version != DX_HASH_TEA \u0026\u0026\n-\t    root-\u003einfo.hash_version != DX_HASH_HALF_MD4 \u0026\u0026\n-\t    root-\u003einfo.hash_version != DX_HASH_LEGACY \u0026\u0026\n-\t    root-\u003einfo.hash_version != DX_HASH_SIPHASH) {\n-\t\text4_warning_inode(dir, \"Unrecognised inode hash code %u\",\n-\t\t\t\t   root-\u003einfo.hash_version);\n+\tinfo = dx_get_dx_info(dir, (struct ext4_dir_entry_2 *)frame-\u003ebh-\u003eb_data);\n+\tif (IS_ERR(info))\n+\t\tgoto fail;\n+\tif (info-\u003einfo_length != sizeof(struct dx_root_info)) {\n+\t\text4_warning_inode(dir, \"corrupted htree: info_length %u\",\n+\t\t\t\t   info-\u003einfo_length);\n+\t\tgoto fail;\n+\t}\n+\tif (info-\u003ehash_version != DX_HASH_TEA \u0026\u0026\n+\t    info-\u003ehash_version != DX_HASH_HALF_MD4 \u0026\u0026\n+\t    info-\u003ehash_version != DX_HASH_LEGACY \u0026\u0026\n+\t    info-\u003ehash_version != DX_HASH_SIPHASH) {\n+\t\text4_warning(dir-\u003ei_sb,\n+\t\t\t\"Unrecognised inode hash code %d for directory #%llu\",\n+\t\t\tinfo-\u003ehash_version, dir-\u003ei_ino);\n \t\tgoto fail;\n \t}\n \tif (ext4_hash_in_dirent(dir)) {\n-\t\tif (root-\u003einfo.hash_version != DX_HASH_SIPHASH) {\n+\t\tif (info-\u003ehash_version != DX_HASH_SIPHASH) {\n \t\t\text4_warning_inode(dir,\n \t\t\t\t\"Hash in dirent, but hash is not SIPHASH\");\n \t\t\tgoto fail;\n \t\t}\n \t} else {\n-\t\tif (root-\u003einfo.hash_version == DX_HASH_SIPHASH) {\n+\t\tif (info-\u003ehash_version == DX_HASH_SIPHASH) {\n \t\t\text4_warning_inode(dir,\n \t\t\t\t\"Hash code is SIPHASH, but hash not in dirent\");\n \t\t\tgoto fail;\n@@ -816,7 +874,7 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,\n \t}\n \tif (fname)\n \t\thinfo = \u0026fname-\u003ehinfo;\n-\thinfo-\u003ehash_version = root-\u003einfo.hash_version;\n+\thinfo-\u003ehash_version = info-\u003ehash_version;\n \tif (hinfo-\u003ehash_version \u003c= DX_HASH_TEA)\n \t\thinfo-\u003ehash_version += EXT4_SB(dir-\u003ei_sb)-\u003es_hash_unsigned;\n \thinfo-\u003eseed = EXT4_SB(dir-\u003ei_sb)-\u003es_hash_seed;\n@@ -832,13 +890,13 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,\n \t}\n \thash = hinfo-\u003ehash;\n \n-\tif (root-\u003einfo.unused_flags \u0026 1) {\n+\tif (info-\u003eunused_flags \u0026 1) {\n \t\text4_warning_inode(dir, \"Unimplemented hash flags: %#06x\",\n-\t\t\t\t   root-\u003einfo.unused_flags);\n+\t\t\t\t   info-\u003eunused_flags);\n \t\tgoto fail;\n \t}\n \n-\tindirect = root-\u003einfo.indirect_levels;\n+\tindirect = info-\u003eindirect_levels;\n \tif (indirect \u003e= ext4_dir_htree_level(dir-\u003ei_sb)) {\n \t\text4_warning(dir-\u003ei_sb,\n \t\t\t     \"Directory (ino: %llu) htree depth %#06x exceed\"\n@@ -851,14 +909,15 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,\n \t\tgoto fail;\n \t}\n \n-\tentries = (struct dx_entry *)(((char *)\u0026root-\u003einfo) +\n-\t\t\t\t      root-\u003einfo.info_length);\n+\tentries = (struct dx_entry *)(((char *)info) + info-\u003einfo_length);\n \n-\tif (dx_get_limit(entries) != dx_root_limit(dir,\n-\t\t\t\t\t\t   root-\u003einfo.info_length)) {\n+\tif (dx_get_limit(entries) !=\n+\t    dx_root_limit(dir, (struct ext4_dir_entry_2 *)frame-\u003ebh-\u003eb_data)) {\n \t\text4_warning_inode(dir, \"dx entry: limit %u != root limit %u\",\n \t\t\t\t   dx_get_limit(entries),\n-\t\t\t\t   dx_root_limit(dir, root-\u003einfo.info_length));\n+\t\t\t\t   dx_root_limit(dir,\n+\t\t\t\t   (struct ext4_dir_entry_2 *)frame-\u003ebh-\u003eb_data\n+\t\t\t\t   ));\n \t\tgoto fail;\n \t}\n \n@@ -935,7 +994,7 @@ dx_probe(struct ext4_filename *fname, struct inode *dir,\n \treturn ret_err;\n }\n \n-static void dx_release(struct dx_frame *frames)\n+static void dx_release(struct inode *dir, struct dx_frame *frames)\n {\n \tstruct dx_root_info *info;\n \tint i;\n@@ -944,7 +1003,18 @@ static void dx_release(struct dx_frame *frames)\n \tif (frames[0].bh == NULL)\n \t\treturn;\n \n-\tinfo = \u0026((struct dx_root *)frames[0].bh-\u003eb_data)-\u003einfo;\n+\tinfo = dx_get_dx_info(dir, (struct ext4_dir_entry_2 *)frames[0].bh-\u003eb_data);\n+\tif (IS_ERR(info)) {\n+\t\t/* Cannot determine indirect_levels; release all frame buffers\n+\t\t * that may have been loaded to avoid leaking them. */\n+\t\tfor (i = 0; i \u003c EXT4_HTREE_LEVEL; i++) {\n+\t\t\tif (!frames[i].bh)\n+\t\t\t\tbreak;\n+\t\t\tbrelse(frames[i].bh);\n+\t\t\tframes[i].bh = NULL;\n+\t\t}\n+\t\treturn;\n+\t}\n \t/* save local copy, \"info\" may be freed after brelse() */\n \tindirect_levels = info-\u003eindirect_levels;\n \tfor (i = 0; i \u003c= indirect_levels; i++) {\n@@ -1056,7 +1126,7 @@ static int htree_dirblock_to_tree(struct file *dir_file,\n \t/* csum entries are not larger in the casefolded encrypted case */\n \ttop = (struct ext4_dir_entry_2 *) ((char *) de +\n \t\t\t\t\t   dir-\u003ei_sb-\u003es_blocksize -\n-\t\t\t\t\t   ext4_dir_rec_len(0,\n+\t\t\t\t\t   ext4_dirent_rec_len(0,\n \t\t\t\t\t\t\t   csum ? NULL : dir));\n \t/* Check if the directory is encrypted */\n \tif (IS_ENCRYPTED(dir)) {\n@@ -1081,22 +1151,34 @@ static int htree_dirblock_to_tree(struct file *dir_file,\n \t\t\t/* silently ignore the rest of the block */\n \t\t\tbreak;\n \t\t}\n-\t\tif (ext4_hash_in_dirent(dir)) {\n-\t\t\tif (de-\u003ename_len \u0026\u0026 de-\u003einode) {\n-\t\t\t\thinfo-\u003ehash = EXT4_DIRENT_HASH(de);\n-\t\t\t\thinfo-\u003eminor_hash = EXT4_DIRENT_MINOR_HASH(de);\n-\t\t\t} else {\n-\t\t\t\thinfo-\u003ehash = 0;\n-\t\t\t\thinfo-\u003eminor_hash = 0;\n-\t\t\t}\n+\t\tif (de-\u003ename_len \u0026\u0026 de-\u003einode) {\n+\t\t\t/* check for saved hash first, or generate it from name */\n+\t\t\tif (!(ext4_dirdata_get(de, dir, dir-\u003ei_sb-\u003es_blocksize,\n+\t\t\t\t\t       NULL, hinfo) \u0026\n+\t\t\t      EXT4_DIRENT_CFHASH)) {\n+\t\t\t\tif (ext4_hash_in_dirent(dir)) {\n+\t\t\t\t\t/* Un-migrated entry: hash is at the\n+\t\t\t\t\t * legacy fixed offset, not in a CFHASH\n+\t\t\t\t\t * extension.  Read it directly rather\n+\t\t\t\t\t * than hashing the encrypted name. */\n+\t\t\t\t\thinfo-\u003ehash = EXT4_DIRENT_HASH(de);\n+\t\t\t\t\thinfo-\u003eminor_hash =\n+\t\t\t\t\t\tEXT4_DIRENT_MINOR_HASH(de);\n+\t\t\t\t} else {\n+\t\t\t\t\terr = ext4fs_dirhash(dir, de-\u003ename,\n+\t\t\t\t\t\t\t     de-\u003ename_len,\n+\t\t\t\t\t\t\t     hinfo);\n+\t\t\t\t\tif (err \u003c 0) {\n+\t\t\t\t\t\tcount = err;\n+\t\t\t\t\t\tgoto errout;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t }\n \t\t} else {\n-\t\t\terr = ext4fs_dirhash(dir, de-\u003ename,\n-\t\t\t\t\t     de-\u003ename_len, hinfo);\n-\t\t\tif (err \u003c 0) {\n-\t\t\t\tcount = err;\n-\t\t\t\tgoto errout;\n-\t\t\t}\n+\t\t\thinfo-\u003ehash = 0;\n+\t\t\thinfo-\u003eminor_hash = 0;\n \t\t}\n+\n \t\tif ((hinfo-\u003ehash \u003c start_hash) ||\n \t\t    ((hinfo-\u003ehash == start_hash) \u0026\u0026\n \t\t     (hinfo-\u003eminor_hash \u003c start_minor_hash)))\n@@ -1107,8 +1189,8 @@ static int htree_dirblock_to_tree(struct file *dir_file,\n \t\t\ttmp_str.name = de-\u003ename;\n \t\t\ttmp_str.len = de-\u003ename_len;\n \t\t\terr = ext4_htree_store_dirent(dir_file,\n-\t\t\t\t   hinfo-\u003ehash, hinfo-\u003eminor_hash, de,\n-\t\t\t\t   \u0026tmp_str);\n+\t\t\t\t   hinfo-\u003ehash, hinfo-\u003eminor_hash,\n+\t\t\t\t   dir-\u003ei_sb-\u003es_blocksize, de, \u0026tmp_str);\n \t\t} else {\n \t\t\tint save_len = fname_crypto_str.len;\n \t\t\tstruct fscrypt_str de_name = FSTR_INIT(de-\u003ename,\n@@ -1123,8 +1205,9 @@ static int htree_dirblock_to_tree(struct file *dir_file,\n \t\t\t\tgoto errout;\n \t\t\t}\n \t\t\terr = ext4_htree_store_dirent(dir_file,\n-\t\t\t\t   hinfo-\u003ehash, hinfo-\u003eminor_hash, de,\n-\t\t\t\t\t\u0026fname_crypto_str);\n+\t\t\t\t\t\thinfo-\u003ehash, hinfo-\u003eminor_hash,\n+\t\t\t\t\t\tdir-\u003ei_sb-\u003es_blocksize, de,\n+\t\t\t\t\t\t\u0026fname_crypto_str);\n \t\t\tfname_crypto_str.len = save_len;\n \t\t}\n \t\tif (err != 0) {\n@@ -1202,6 +1285,7 @@ int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,\n \t\ttmp_str.name = de-\u003ename;\n \t\ttmp_str.len = de-\u003ename_len;\n \t\terr = ext4_htree_store_dirent(dir_file, 0, 0,\n+\t\t\t\t\t      dir-\u003ei_sb-\u003es_blocksize,\n \t\t\t\t\t      de, \u0026tmp_str);\n \t\tif (err != 0)\n \t\t\tgoto errout;\n@@ -1213,6 +1297,7 @@ int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,\n \t\ttmp_str.name = de-\u003ename;\n \t\ttmp_str.len = de-\u003ename_len;\n \t\terr = ext4_htree_store_dirent(dir_file, 2, 0,\n+\t\t\t\t\t      dir-\u003ei_sb-\u003es_blocksize,\n \t\t\t\t\t      de, \u0026tmp_str);\n \t\tif (err != 0)\n \t\t\tgoto errout;\n@@ -1250,12 +1335,12 @@ int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,\n \t\t    (count \u0026\u0026 ((hashval \u0026 1) == 0)))\n \t\t\tbreak;\n \t}\n-\tdx_release(frames);\n+\tdx_release(dir, frames);\n \tdxtrace(printk(KERN_DEBUG \"Fill tree: returned %d entries, \"\n \t\t       \"next hash: %x\\n\", count, *next_hash));\n \treturn count;\n errout:\n-\tdx_release(frames);\n+\tdx_release(dir, frames);\n \treturn (err);\n }\n \n@@ -1273,6 +1358,210 @@ static inline int search_dirblock(struct buffer_head *bh,\n  * Directory block splitting, compacting\n  */\n \n+/*\n+ * ext4_dirdata_get() - Read dirdata fields from a directory entry.\n+ * @de:         directory entry\n+ * @dir:        directory inode (used for fscrypt+casefold hash fallback)\n+ * @dfid:      if non-NULL and EXT4_DIRENT_LUFID is set, LUFID data is copied\n+ * \t\there\n+ * @hinfo:\tif non-NULL, receives the casefold hash and minor hash\n+ *\n+ * Reads any dirdata stored in @de.  If the dirdata feature is not enabled,\n+ * falls back to reading the hash stored inline after the filename (for\n+ * compatibility with the older casefold+fscrypt format).\n+ *\n+ * Returns a bitmask of EXT4_DIRENT_* flags indicating which fields were read.\n+ *\n+ * Compatibility note: enabling EXT4_FEATURE_INCOMPAT_DIRDATA on a filesystem\n+ * that already has casefolded+encrypted directories is NOT safe without a\n+ * prior migration pass.  Before dirdata, the casefold+fscrypt hash was stored\n+ * as a raw 8 bytes immediately after the filename with no flag in file_type.\n+ * After dirdata is enabled, this function expects the hash to be present only\n+ * when EXT4_DIRENT_CFHASH (0x40) is set in file_type; existing entries that\n+ * carry the raw hash are silently misread as having no hash at all, breaking\n+ * directory lookups.  e2fsck must be run to convert all affected entries to\n+ * the EXT4_DIRENT_CFHASH extension format before the feature flag is set with\n+ * tune2fs.  Detection heuristic: in a casefold+encrypted directory, an entry\n+ * with rec_len \u003e= round_up(name_len, 4) + 8 and no EXT4_DIRENT_CFHASH bit\n+ * carries a raw pre-dirdata hash that must be migrated.\n+ */\n+unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de, struct inode *dir,\n+\t\t\t       int buf_size,\n+\t\t\t       struct ext4_dirent_fid *dfid,\n+\t\t\t       struct dx_hash_info *hinfo)\n+{\n+\tunsigned char ret = 0;\n+\tunsigned int data_offset = de-\u003ename_len + 1;\n+\tunsigned int rec_len = ext4_rec_len_from_disk(de-\u003erec_len, buf_size);\n+\n+\t/* data_offset is relative to de-\u003ename, which itself starts\n+\t * EXT4_BASE_DIR_LEN bytes into the entry -- rec_len is relative to\n+\t * the start of the entry, so add the header size before comparing,\n+\t * or this lets reads run EXT4_BASE_DIR_LEN bytes past the entry. */\n+\tif (EXT4_BASE_DIR_LEN + data_offset \u003e rec_len)\n+\t\treturn ret;\n+\n+\t/* compatibility: hash stored inline after filename (no dirdata) */\n+\tif (hinfo \u0026\u0026 !ext4_has_feature_dirdata(dir-\u003ei_sb) \u0026\u0026\n+\t    ext4_hash_in_dirent(dir)) {\n+\t\thinfo-\u003ehash = EXT4_DIRENT_HASH(de);\n+\t\thinfo-\u003eminor_hash = EXT4_DIRENT_MINOR_HASH(de);\n+\t\tret |= EXT4_DIRENT_CFHASH;\n+\t\treturn ret;\n+\t}\n+\n+\t/* EXT4_DIRENT_* bits are only meaningful when the feature is enabled */\n+\tif (!ext4_has_feature_dirdata(dir-\u003ei_sb))\n+\t\treturn ret;\n+\n+\tif (de-\u003efile_type \u0026 EXT4_DIRENT_LUFID) {\n+\t\tstruct ext4_dirent_fid *disk_fid =\n+\t\t\t(struct ext4_dirent_fid *)((char *)de +\n+\t\t\tEXT4_BASE_DIR_LEN + data_offset);\n+\t\tunsigned int dlen;\n+\t\t/* struct ext4_fid df_fid[] does not provide the array size.\n+\t\t * First, verify that the header lies within the valid area, then\n+ \t\t * verify that the entire record fits within it. */\n+\t\tif (EXT4_BASE_DIR_LEN + data_offset +\n+\t\t    sizeof(disk_fid-\u003edf_header) \u003e rec_len)\n+\t\t\treturn ret;\n+\n+\t\tdlen = disk_fid-\u003edf_header.ddh_length;\n+\t\tif (dlen == 0 ||\n+\t\t    EXT4_BASE_DIR_LEN + data_offset + dlen \u003e rec_len)\n+\t\t\treturn ret;\n+\n+\t\tif (dfid) {\n+\t\t\tmemcpy(dfid, disk_fid, dlen);\n+\t\t\tret |= EXT4_DIRENT_LUFID;\n+\t\t}\n+\t\tdata_offset += dlen;\n+\t}\n+\n+\t/* Skip INO64 for now*/\n+\tif (de-\u003efile_type \u0026 EXT4_DIRENT_INO64) {\n+\t\tstruct ext4_dirent_data_header *ddh =\n+\t\t       (struct ext4_dirent_data_header *)((char *)de + EXT4_BASE_DIR_LEN + data_offset);\n+\t\tunsigned int dlen;\n+\n+\t\tif (EXT4_BASE_DIR_LEN + data_offset + sizeof(*ddh) \u003e rec_len)\n+\t\t\treturn ret;\n+\n+\t\tdlen = ddh-\u003eddh_length;\n+\t\tif (dlen \u003c sizeof(*ddh) ||\n+\t\t    EXT4_BASE_DIR_LEN + data_offset + dlen \u003e rec_len)\n+\t\t\treturn ret;\n+\n+\t\tdata_offset += dlen;\n+\t}\n+\n+\tif (!hinfo)\n+\t\treturn ret;\n+\n+\tif (de-\u003efile_type \u0026 EXT4_DIRENT_CFHASH) {\n+\t\tstruct ext4_dirent_hash *dh =\n+\t\t\t(struct ext4_dirent_hash *)((char *)de + EXT4_BASE_DIR_LEN + data_offset);\n+\t\tunsigned int dlen;\n+\n+\t\tif (EXT4_BASE_DIR_LEN + data_offset + sizeof(*dh) \u003e rec_len)\n+\t\t\treturn ret;\n+\t\tdlen = dh-\u003edh_header.ddh_length;\n+\t\tif (dlen \u003c sizeof(*dh) ||\n+\t\t    EXT4_BASE_DIR_LEN + data_offset + dlen \u003e rec_len)\n+\t\t\treturn ret;\n+\n+\t\thinfo-\u003ehash = le32_to_cpu(dh-\u003edh_hash.hash);\n+\t\thinfo-\u003eminor_hash = le32_to_cpu(dh-\u003edh_hash.minor_hash);\n+\t\tret |= EXT4_DIRENT_CFHASH;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+/*\n+ * ext4_dirdata_set() - Write dirdata fields into a directory entry.\n+ * @de:    directory entry (name must already be set)\n+ * @dir:   directory inode\n+ * @data:  LUFID data to store (or NULL)\n+ * @fname: filename info carrying the casefold hash\n+ *\n+ * Writes any required dirdata into @de after the filename.  If the dirdata\n+ * feature is not enabled, falls back to writing the hash inline after the\n+ * filename (for compatibility with the older casefold+fscrypt format).\n+ *\n+ * See ext4_dirdata_get() for the compatibility constraint: enabling the\n+ * dirdata feature on a filesystem with existing casefolded+encrypted\n+ * directories requires an e2fsck migration pass before tune2fs sets the\n+ * EXT4_FEATURE_INCOMPAT_DIRDATA superblock flag.\n+ */\n+static int ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir,\n+\t\t\t    struct ext4_dirent_fid *dfid,\n+\t\t\t    struct ext4_filename *fname)\n+{\n+\tstruct dx_hash_info *hinfo = \u0026fname-\u003ehinfo;\n+\tunsigned int data_offset = de-\u003ename_len + 1;\n+\tunsigned int rec_len = ext4_rec_len_from_disk(de-\u003erec_len,\n+\t\t\t\t\t\t       dir-\u003ei_sb-\u003es_blocksize);\n+\n+\t/* Clear the gap byte between the filename and the first dirdata\n+\t * extension to avoid leaking stale memory to disk.  Use pointer\n+\t * arithmetic rather than de-\u003ename[name_len] to stay within the\n+\t * declared name[] array bounds under FORTIFY_SOURCE.  Only write it\n+\t * when there is actually room (entries that exactly fill their slot\n+\t * have rec_len == EXT4_BASE_DIR_LEN + name_len with no gap). */\n+\tif (EXT4_BASE_DIR_LEN + data_offset \u003c= rec_len)\n+\t\t*((char *)de + EXT4_BASE_DIR_LEN + de-\u003ename_len) = 0;\n+\n+\tif (dfid) {\n+\t\tunsigned int dlen = dfid-\u003edf_header.ddh_length;\n+\n+\t\tif (EXT4_BASE_DIR_LEN + data_offset + dlen \u003e rec_len) {\n+\t\t\tEXT4_ERROR_INODE(dir, \"Can not insert FID\");\n+\t\t\treturn -EIO;\n+\t\t}\n+\n+\t\tmemcpy((char *)de + EXT4_BASE_DIR_LEN + data_offset, dfid, dlen);\n+\t\tde-\u003efile_type |= EXT4_DIRENT_LUFID;\n+\t\tdata_offset += dlen;\n+\t}\n+\n+\tif (ext4_hash_in_dirent(dir)) {\n+\t\tif (ext4_has_feature_dirdata(dir-\u003ei_sb)) {\n+\t\t\tstruct ext4_dirent_hash *dh =\n+\t\t\t    (struct ext4_dirent_hash *)((char *)de + EXT4_BASE_DIR_LEN + data_offset);\n+\n+\t\t\tif (EXT4_BASE_DIR_LEN + data_offset + sizeof(*dh) \u003e rec_len) {\n+\t\t\t\tEXT4_ERROR_INODE(dir, \"Can not insert dhash dirdata\");\n+\t\t\t\treturn -EIO;\n+\t\t\t}\n+\n+\t\t\tdh-\u003edh_header.ddh_length = sizeof(*dh);\n+\t\t\tdh-\u003edh_hash.hash = cpu_to_le32(hinfo-\u003ehash);\n+\t\t\tdh-\u003edh_hash.minor_hash = cpu_to_le32(hinfo-\u003eminor_hash);\n+\t\t\tde-\u003efile_type |= EXT4_DIRENT_CFHASH;\n+\t\t} else {\n+\t\t\t/* Compatibility: store hash inline after filename.\n+\t\t\t * EXT4_DIRENT_HASHES places the hash at the\n+\t\t\t * 4-byte-aligned offset (8 + name_len + 3) \u0026 ~3, which\n+\t\t\t * may be less than EXT4_BASE_DIR_LEN + data_offset when\n+\t\t\t * name_len is a multiple of 4 (gap byte counted twice).\n+\t\t\t * Use the same aligned offset for the bounds check. */\n+\t\t\tunsigned int hash_off =\n+\t\t\t\t(EXT4_BASE_DIR_LEN + de-\u003ename_len + 3) \u0026 ~3;\n+\n+\t\t\tif (hash_off + sizeof(struct ext4_dir_entry_hash) \u003e rec_len) {\n+\t\t\t\tEXT4_ERROR_INODE(dir, \"Can not insert dhash\");\n+\t\t\t\treturn -EIO;\n+\t\t\t}\n+\n+\t\t\tEXT4_DIRENT_HASHES(de)-\u003ehash = cpu_to_le32(hinfo-\u003ehash);\n+\t\t\tEXT4_DIRENT_HASHES(de)-\u003eminor_hash =\n+\t\t\t\t\t\tcpu_to_le32(hinfo-\u003eminor_hash);\n+\t\t}\n+\t}\n+\treturn 0;\n+}\n+\n /*\n  * Create map of hash values, offsets, and sizes, stored at end of block.\n  * Returns number of entries mapped.\n@@ -1296,13 +1585,21 @@ static int dx_make_map(struct inode *dir, struct buffer_head *bh,\n \t\t\t\t\t ((char *)de) - base))\n \t\t\treturn -EFSCORRUPTED;\n \t\tif (de-\u003ename_len \u0026\u0026 de-\u003einode) {\n-\t\t\tif (ext4_hash_in_dirent(dir))\n-\t\t\t\th.hash = EXT4_DIRENT_HASH(de);\n-\t\t\telse {\n-\t\t\t\tint err = ext4fs_dirhash(dir, de-\u003ename,\n-\t\t\t\t\t\t     de-\u003ename_len, \u0026h);\n-\t\t\t\tif (err \u003c 0)\n-\t\t\t\t\treturn err;\n+\t\t\tif (!(ext4_dirdata_get(de, dir, dir-\u003ei_sb-\u003es_blocksize,\n+\t\t\t\t\t       NULL, \u0026h) \u0026\n+\t\t\t\t\t\tEXT4_DIRENT_CFHASH)) {\n+\t\t\t\tif (ext4_hash_in_dirent(dir)) {\n+\t\t\t\t\t/* Un-migrated entry: hash at legacy\n+\t\t\t\t\t * fixed offset. */\n+\t\t\t\t\th.hash = EXT4_DIRENT_HASH(de);\n+\t\t\t\t\th.minor_hash =\n+\t\t\t\t\t\tEXT4_DIRENT_MINOR_HASH(de);\n+\t\t\t\t} else {\n+\t\t\t\t\tint err = ext4fs_dirhash(dir, de-\u003ename,\n+\t\t\t\t\t\t\t     de-\u003ename_len, \u0026h);\n+\t\t\t\t\tif (err \u003c 0)\n+\t\t\t\t\t\treturn err;\n+\t\t\t\t}\n \t\t\t}\n \t\t\tmap_tail--;\n \t\t\tmap_tail-\u003ehash = h.hash;\n@@ -1403,8 +1700,9 @@ int ext4_fname_setup_ci_filename(struct inode *dir, const struct qstr *iname,\n  * Return: %true if the directory entry matches, otherwise %false.\n  */\n static bool ext4_match(struct inode *parent,\n-\t\t\t      const struct ext4_filename *fname,\n-\t\t\t      struct ext4_dir_entry_2 *de)\n+\t\t       const struct ext4_filename *fname,\n+\t\t       struct ext4_dir_entry_2 *de,\n+\t\t       int buf_size)\n {\n \tstruct fscrypt_name f;\n \n@@ -1431,10 +1729,29 @@ static bool ext4_match(struct inode *parent,\n \t\t * considering the calculated hash.\n \t\t */\n \t\tif (sb_no_casefold_compat_fallback(parent-\u003ei_sb) \u0026\u0026\n-\t\t    IS_ENCRYPTED(parent) \u0026\u0026 fname-\u003ecf_name.name \u0026\u0026\n-\t\t    (fname-\u003ehinfo.hash != EXT4_DIRENT_HASH(de) ||\n-\t\t     fname-\u003ehinfo.minor_hash != EXT4_DIRENT_MINOR_HASH(de)))\n-\t\t\treturn false;\n+\t\t    IS_ENCRYPTED(parent) \u0026\u0026 fname-\u003ecf_name.name) {\n+\t\t\t__u32 de_hash, de_minor_hash;\n+\n+\t\t\tif (ext4_has_feature_dirdata(parent-\u003ei_sb) \u0026\u0026\n+\t\t\t    (de-\u003efile_type \u0026 EXT4_DIRENT_CFHASH)) {\n+\t\t\t\t/* Hash is in a CFHASH extension at a variable\n+\t\t\t\t * offset (past any LUFID bytes). Read it via\n+\t\t\t\t * ext4_dirdata_get() to get the correct offset. */\n+\t\t\t\tstruct dx_hash_info dirent_hinfo = {};\n+\n+\t\t\t\text4_dirdata_get(de, parent,\n+\t\t\t\t\t\t buf_size,\n+\t\t\t\t\t\t NULL, \u0026dirent_hinfo);\n+\t\t\t\tde_hash = dirent_hinfo.hash;\n+\t\t\t\tde_minor_hash = dirent_hinfo.minor_hash;\n+\t\t\t} else {\n+\t\t\t\tde_hash = EXT4_DIRENT_HASH(de);\n+\t\t\t\tde_minor_hash = EXT4_DIRENT_MINOR_HASH(de);\n+\t\t\t}\n+\t\t\tif (fname-\u003ehinfo.hash != de_hash ||\n+\t\t\t    fname-\u003ehinfo.minor_hash != de_minor_hash)\n+\t\t\t\treturn false;\n+\t\t}\n \t\t/*\n \t\t * Treat comparison errors as not a match.  The\n \t\t * only case where it happens is on a disk\n@@ -1467,7 +1784,7 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,\n \t\t/* this code is executed quadratically often */\n \t\t/* do minimal checking `by hand' */\n \t\tif (de-\u003ename + de-\u003ename_len \u003c= dlimit \u0026\u0026\n-\t\t    ext4_match(dir, fname, de)) {\n+\t\t    ext4_match(dir, fname, de, buf_size)) {\n \t\t\t/* found a match - just to be sure, do\n \t\t\t * a full check */\n \t\t\tif (ext4_check_dir_entry(dir, NULL, de, bh, search_buf,\n@@ -1488,7 +1805,7 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,\n }\n \n static int is_dx_internal_node(struct inode *dir, ext4_lblk_t block,\n-\t\t\t       struct ext4_dir_entry *de)\n+\t\t\t       struct ext4_dir_entry_2 *de)\n {\n \tstruct super_block *sb = dir-\u003ei_sb;\n \n@@ -1619,7 +1936,7 @@ static struct buffer_head *__ext4_find_entry(struct inode *dir,\n \t\t}\n \t\tif (!buffer_verified(bh) \u0026\u0026\n \t\t    !is_dx_internal_node(dir, block,\n-\t\t\t\t\t (struct ext4_dir_entry *)bh-\u003eb_data) \u0026\u0026\n+\t\t\t\t\t (struct ext4_dir_entry_2 *)bh-\u003eb_data) \u0026\u0026\n \t\t    !ext4_dirblock_csum_verify(dir, bh)) {\n \t\t\tEXT4_ERROR_INODE_ERR(dir, EFSBADCRC,\n \t\t\t\t\t     \"checksumming directory \"\n@@ -1753,7 +2070,7 @@ static struct buffer_head * ext4_dx_find_entry(struct inode *dir,\n errout:\n \tdxtrace(printk(KERN_DEBUG \"%s not found\\n\", fname-\u003eusr_fname-\u003ename));\n success:\n-\tdx_release(frames);\n+\tdx_release(dir, frames);\n \treturn bh;\n }\n \n@@ -1850,7 +2167,7 @@ dx_move_dirents(struct inode *dir, char *from, char *to,\n \twhile (count--) {\n \t\tstruct ext4_dir_entry_2 *de = (struct ext4_dir_entry_2 *)\n \t\t\t\t\t\t(from + (map-\u003eoffs\u003c\u003c2));\n-\t\trec_len = ext4_dir_rec_len(de-\u003ename_len, dir);\n+\t\trec_len = ext4_dir_entry_len(de, blocksize, dir);\n \n \t\tmemcpy (to, de, rec_len);\n \t\t((struct ext4_dir_entry_2 *) to)-\u003erec_len =\n@@ -1883,7 +2200,7 @@ static struct ext4_dir_entry_2 *dx_pack_dirents(struct inode *dir, char *base,\n \twhile ((char*)de \u003c base + blocksize) {\n \t\tnext = ext4_next_entry(de, blocksize);\n \t\tif (de-\u003einode \u0026\u0026 de-\u003ename_len) {\n-\t\t\trec_len = ext4_dir_rec_len(de-\u003ename_len, dir);\n+\t\t\trec_len = ext4_dir_entry_len(de, blocksize, dir);\n \t\t\tif (de \u003e to)\n \t\t\t\tmemmove(to, de, rec_len);\n \t\t\tto-\u003erec_len = ext4_rec_len_to_disk(rec_len, blocksize);\n@@ -2035,10 +2352,15 @@ static struct ext4_dir_entry_2 *do_split(handle_t *handle, struct inode *dir,\n int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,\n \t\t      void *buf, int buf_size,\n \t\t      struct ext4_filename *fname,\n-\t\t      struct ext4_dir_entry_2 **dest_de)\n+\t\t      struct ext4_dir_entry_2 **dest_de,\n+\t\t      int dlen)\n {\n \tstruct ext4_dir_entry_2 *de;\n-\tunsigned short reclen = ext4_dir_rec_len(fname_len(fname), dir);\n+\t/* When dirdata extensions are present, a NUL gap byte sits between\n+\t * the filename and the first extension; account for it so the found\n+\t * slot is always large enough for ext4_dirdata_set() to write into. */\n+\tunsigned short reclen = ext4_dirent_rec_len(fname_len(fname) + dlen +\n+\t\t\t\t\t\t    (dlen ? 1 : 0), dir);\n \tint nlen, rlen;\n \tunsigned int offset = 0;\n \tchar *top;\n@@ -2049,9 +2371,9 @@ int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,\n \t\tif (ext4_check_dir_entry(dir, NULL, de, bh,\n \t\t\t\t\t buf, buf_size, offset))\n \t\t\treturn -EFSCORRUPTED;\n-\t\tif (ext4_match(dir, fname, de))\n+\t\tif (ext4_match(dir, fname, de, buf_size))\n \t\t\treturn -EEXIST;\n-\t\tnlen = ext4_dir_rec_len(de-\u003ename_len, dir);\n+\t\tnlen = ext4_dir_entry_len(de, buf_size, dir);\n \t\trlen = ext4_rec_len_from_disk(de-\u003erec_len, buf_size);\n \t\tif ((de-\u003einode ? rlen - nlen : rlen) \u003e= reclen)\n \t\t\tbreak;\n@@ -2065,16 +2387,13 @@ int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,\n \treturn 0;\n }\n \n-void ext4_insert_dentry(struct inode *dir,\n-\t\t\tstruct inode *inode,\n-\t\t\tstruct ext4_dir_entry_2 *de,\n-\t\t\tint buf_size,\n-\t\t\tstruct ext4_filename *fname)\n+int ext4_insert_dentry_data(struct inode *dir, struct inode *inode,\n+\t\t\t    struct ext4_dir_entry_2 *de, int buf_size,\n+\t\t\t    struct ext4_filename *fname, void *data)\n {\n-\n \tint nlen, rlen;\n \n-\tnlen = ext4_dir_rec_len(de-\u003ename_len, dir);\n+\tnlen = ext4_dir_entry_len(de, buf_size, dir);\n \trlen = ext4_rec_len_from_disk(de-\u003erec_len, buf_size);\n \tif (de-\u003einode) {\n \t\tstruct ext4_dir_entry_2 *de1 =\n@@ -2088,13 +2407,7 @@ void ext4_insert_dentry(struct inode *dir,\n \text4_set_de_type(inode-\u003ei_sb, de, inode-\u003ei_mode);\n \tde-\u003ename_len = fname_len(fname);\n \tmemcpy(de-\u003ename, fname_name(fname), fname_len(fname));\n-\tif (ext4_hash_in_dirent(dir)) {\n-\t\tstruct dx_hash_info *hinfo = \u0026fname-\u003ehinfo;\n-\n-\t\tEXT4_DIRENT_HASHES(de)-\u003ehash = cpu_to_le32(hinfo-\u003ehash);\n-\t\tEXT4_DIRENT_HASHES(de)-\u003eminor_hash =\n-\t\t\t\t\t\tcpu_to_le32(hinfo-\u003eminor_hash);\n-\t}\n+\treturn ext4_dirdata_set(de, dir, data, fname);\n }\n \n /*\n@@ -2112,14 +2425,28 @@ static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,\n {\n \tunsigned int\tblocksize = dir-\u003ei_sb-\u003es_blocksize;\n \tint\t\tcsum_size = 0;\n-\tint\t\terr, err2;\n+\tint\t\terr, err2, dlen = 0;\n+\tstruct ext4_dirent_fid *dfid;\n \n \tif (ext4_has_feature_metadata_csum(inode-\u003ei_sb))\n \t\tcsum_size = sizeof(struct ext4_dir_entry_tail);\n \n+\t/* dfid is resolved once by ext4_add_entry() and stored in fname so\n+\t * that all add_dirent_to_buf() calls within the same add operation\n+\t * use a consistent value without touching the shared i_dirdata field. */\n+\tdfid = fname-\u003edfid;\n \tif (!de) {\n+\t\tif (dfid)\n+\t\t\tdlen = dfid-\u003edf_header.ddh_length;\n+\t\t/* ext4_dirent_rec_len() does not add CFHASH size when dirdata is\n+\t\t * enabled (it assumes the caller included it in name_len).  Add\n+\t\t * it here so ext4_find_dest_de() allocates a slot large enough\n+\t\t * for ext4_dirdata_set() to write the hash extension. */\n+\t\tif (ext4_hash_in_dirent(dir) \u0026\u0026\n+\t\t    ext4_has_feature_dirdata(dir-\u003ei_sb))\n+\t\t\tdlen += sizeof(struct ext4_dirent_hash);\n \t\terr = ext4_find_dest_de(dir, bh, bh-\u003eb_data,\n-\t\t\t\t\tblocksize - csum_size, fname, \u0026de);\n+\t\t\t\t\tblocksize - csum_size, fname, \u0026de, dlen);\n \t\tif (err)\n \t\t\treturn err;\n \t}\n@@ -2132,7 +2459,11 @@ static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,\n \t}\n \n \t/* By now the buffer is marked for journaling */\n-\text4_insert_dentry(dir, inode, de, blocksize, fname);\n+\terr = ext4_insert_dentry_data(dir, inode, de, blocksize, fname, dfid);\n+\tif (err) {\n+\t\text4_std_error(dir-\u003ei_sb, err);\n+\t\treturn err;\n+\t}\n \n \t/*\n \t * XXX shouldn't update any times until successful\n@@ -2156,44 +2487,38 @@ static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,\n \treturn err ? err : err2;\n }\n \n-static bool ext4_check_dx_root(struct inode *dir, struct dx_root *root)\n+static bool ext4_check_dx_root(struct inode *dir,\n+\t\t\t       struct ext4_dir_entry_2 *dot_de,\n+\t\t\t       struct ext4_dir_entry_2 *dotdot_de,\n+\t\t\t       struct ext4_dir_entry_2 **entry)\n {\n-\tstruct fake_dirent *fde;\n \tconst char *error_msg;\n-\tunsigned int rlen;\n \tunsigned int blocksize = dir-\u003ei_sb-\u003es_blocksize;\n-\tchar *blockend = (char *)root + dir-\u003ei_sb-\u003es_blocksize;\n+\tstruct ext4_dir_entry_2 *de = NULL;\n \n-\tfde = \u0026root-\u003edot;\n-\tif (unlikely(fde-\u003ename_len != 1)) {\n+\tif (unlikely(dot_de-\u003ename_len != 1)) {\n \t\terror_msg = \"invalid name_len for '.'\";\n \t\tgoto corrupted;\n \t}\n-\tif (unlikely(strncmp(root-\u003edot_name, \".\", fde-\u003ename_len))) {\n+\tif (unlikely(strncmp(dot_de-\u003ename, \".\", dot_de-\u003ename_len))) {\n \t\terror_msg = \"invalid name for '.'\";\n \t\tgoto corrupted;\n \t}\n-\trlen = ext4_rec_len_from_disk(fde-\u003erec_len, blocksize);\n-\tif (unlikely((char *)fde + rlen \u003e= blockend)) {\n-\t\terror_msg = \"invalid rec_len for '.'\";\n-\t\tgoto corrupted;\n-\t}\n \n-\tfde = \u0026root-\u003edotdot;\n-\tif (unlikely(fde-\u003ename_len != 2)) {\n+\tif (unlikely(dotdot_de-\u003ename_len != 2)) {\n \t\terror_msg = \"invalid name_len for '..'\";\n \t\tgoto corrupted;\n \t}\n-\tif (unlikely(strncmp(root-\u003edotdot_name, \"..\", fde-\u003ename_len))) {\n+\tif (unlikely(strncmp(dotdot_de-\u003ename, \"..\", dotdot_de-\u003ename_len))) {\n \t\terror_msg = \"invalid name for '..'\";\n \t\tgoto corrupted;\n \t}\n-\trlen = ext4_rec_len_from_disk(fde-\u003erec_len, blocksize);\n-\tif (unlikely((char *)fde + rlen \u003e= blockend)) {\n+\tde = ext4_next_entry(dotdot_de, blocksize);\n+\tif ((char *)de \u003e= (((char *)dot_de) + blocksize)) {\n \t\terror_msg = \"invalid rec_len for '..'\";\n \t\tgoto corrupted;\n \t}\n-\n+\t*entry = de;\n \treturn true;\n \n corrupted:\n@@ -2211,16 +2536,15 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,\n \t\t\t    struct inode *inode, struct buffer_head *bh)\n {\n \tstruct buffer_head *bh2;\n-\tstruct dx_root\t*root;\n \tstruct dx_frame\tframes[EXT4_HTREE_LEVEL], *frame;\n \tstruct dx_entry *entries;\n-\tstruct ext4_dir_entry_2\t*de, *de2;\n+\tstruct ext4_dir_entry_2\t*de, *de2, *dot_de, *dotdot_de;\n \tchar\t\t*data2, *top;\n \tunsigned\tlen;\n \tint\t\tretval;\n \tunsigned\tblocksize;\n \text4_lblk_t  block;\n-\tstruct fake_dirent *fde;\n+\tstruct dx_root_info *dx_info;\n \tint csum_size = 0;\n \n \tif (ext4_has_feature_metadata_csum(inode-\u003ei_sb))\n@@ -2237,17 +2561,20 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,\n \t\treturn retval;\n \t}\n \n-\troot = (struct dx_root *) bh-\u003eb_data;\n-\tif (!ext4_check_dx_root(dir, root)) {\n+\tdot_de = (struct ext4_dir_entry_2 *)bh-\u003eb_data;\n+\tif (ext4_rec_len_from_disk(dot_de-\u003erec_len, blocksize) \u003c EXT4_BASE_DIR_LEN ||\n+\t    ext4_rec_len_from_disk(dot_de-\u003erec_len, blocksize) \u003e= blocksize - EXT4_BASE_DIR_LEN) {\n+\t\tbrelse(bh);\n+\t\treturn -EFSCORRUPTED;\n+\t}\n+\tdotdot_de = ext4_next_entry(dot_de, blocksize);\n+\tif (!ext4_check_dx_root(dir, dot_de, dotdot_de, \u0026de)) {\n \t\tbrelse(bh);\n \t\treturn -EFSCORRUPTED;\n \t}\n \n \t/* The 0th block becomes the root, move the dirents out */\n-\tfde = \u0026root-\u003edotdot;\n-\tde = (struct ext4_dir_entry_2 *)((char *)fde +\n-\t\text4_rec_len_from_disk(fde-\u003erec_len, blocksize));\n-\tlen = ((char *) root) + (blocksize - csum_size) - (char *) de;\n+\tlen = ((char *)dot_de) + (blocksize - csum_size) - (char *)de;\n \n \t/* Allocate new block for the 0th block's dirents */\n \tbh2 = ext4_append(handle, dir, \u0026block);\n@@ -2278,24 +2605,32 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,\n \t\text4_initialize_dirent_tail(bh2, blocksize);\n \n \t/* Initialize the root; the dot dirents already exist */\n-\tde = (struct ext4_dir_entry_2 *) (\u0026root-\u003edotdot);\n-\tde-\u003erec_len = ext4_rec_len_to_disk(\n-\t\t\tblocksize - ext4_dir_rec_len(2, NULL), blocksize);\n-\tmemset (\u0026root-\u003einfo, 0, sizeof(root-\u003einfo));\n-\troot-\u003einfo.info_length = sizeof(root-\u003einfo);\n+\tdotdot_de-\u003erec_len =\n+\t\text4_rec_len_to_disk(blocksize - le16_to_cpu(dot_de-\u003erec_len),\n+\t\t\t\t     blocksize);\n+\n+\t/* initialize hashing info */\n+\tdx_info = dx_get_dx_info(dir, dot_de);\n+\tif (IS_ERR(dx_info)) {\n+\t\tbrelse(bh2);\n+\t\tbrelse(bh);\n+\t\treturn PTR_ERR(dx_info);\n+\t}\n+\tmemset(dx_info, 0, sizeof(*dx_info));\n+\tdx_info-\u003einfo_length = sizeof(*dx_info);\n \tif (ext4_hash_in_dirent(dir))\n-\t\troot-\u003einfo.hash_version = DX_HASH_SIPHASH;\n+\t\tdx_info-\u003ehash_version = DX_HASH_SIPHASH;\n \telse\n-\t\troot-\u003einfo.hash_version =\n+\t\tdx_info-\u003ehash_version =\n \t\t\t\tEXT4_SB(dir-\u003ei_sb)-\u003es_def_hash_version;\n \n-\tentries = root-\u003eentries;\n+\tentries = (void *)dx_info + dx_info-\u003einfo_length;\n \tdx_set_block(entries, 1);\n \tdx_set_count(entries, 1);\n-\tdx_set_limit(entries, dx_root_limit(dir, sizeof(root-\u003einfo)));\n+\tdx_set_limit(entries, dx_root_limit(dir, dot_de));\n \n \t/* Initialize as for dx_probe */\n-\tfname-\u003ehinfo.hash_version = root-\u003einfo.hash_version;\n+\tfname-\u003ehinfo.hash_version = dx_info-\u003ehash_version;\n \tif (fname-\u003ehinfo.hash_version \u003c= DX_HASH_TEA)\n \t\tfname-\u003ehinfo.hash_version += EXT4_SB(dir-\u003ei_sb)-\u003es_hash_unsigned;\n \tfname-\u003ehinfo.seed = EXT4_SB(dir-\u003ei_sb)-\u003es_hash_seed;\n@@ -2338,7 +2673,7 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,\n \t */\n \tif (retval)\n \t\text4_mark_inode_dirty(handle, dir);\n-\tdx_release(frames);\n+\tdx_release(dir, frames);\n \tbrelse(bh2);\n \treturn retval;\n }\n@@ -2355,7 +2690,8 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,\n  */\n static int __ext4_add_entry(handle_t *handle, struct inode *dir,\n \t\t\t  const struct qstr *d_name,\n-\t\t\t  struct inode *inode)\n+\t\t\t  struct inode *inode,\n+\t\t\t  struct ext4_dentry_param *edp)\n {\n \tstruct buffer_head *bh = NULL;\n \tstruct ext4_dir_entry_2 *de;\n@@ -2380,6 +2716,13 @@ static int __ext4_add_entry(handle_t *handle, struct inode *dir,\n \tif (retval)\n \t\treturn retval;\n \n+\t/* Resolve the LUFID payload once here and store it in fname so every\n+\t * add_dirent_to_buf() call in this add operation uses the same value\n+\t * without touching the shared EXT4_I(inode)-\u003ei_dirdata field. */\n+\tfname.dfid = ext4_dentry_get_fid(sb, edp);\n+\tif (fname.dfid)\n+\t\text4_fc_mark_ineligible(sb, EXT4_FC_REASON_DIRDATA, handle);\n+\n \tif (ext4_has_inline_data(dir)) {\n \t\tretval = ext4_try_add_inline_entry(handle, \u0026fname, dir, inode);\n \t\tif (retval \u003c 0)\n@@ -2464,7 +2807,8 @@ static int ext4_add_entry(handle_t *handle, struct dentry *dentry,\n \n \tif (fscrypt_is_nokey_name(dentry))\n \t\treturn -ENOKEY;\n-\treturn __ext4_add_entry(handle, dir, \u0026dentry-\u003ed_name, inode);\n+\treturn __ext4_add_entry(handle, dir, \u0026dentry-\u003ed_name, inode,\n+\t\t\t\tdentry-\u003ed_fsdata);\n }\n \n /*\n@@ -2605,7 +2949,7 @@ static int ext4_dx_add_entry(handle_t *handle, struct ext4_filename *fname,\n \t\t\tif (restart || err)\n \t\t\t\tgoto journal_error;\n \t\t} else {\n-\t\t\tstruct dx_root *dxroot;\n+\t\t\tstruct dx_root_info *info;\n \t\t\tmemcpy((char *) entries2, (char *) entries,\n \t\t\t       icount * sizeof(struct dx_entry));\n \t\t\tdx_set_limit(entries2, dx_node_limit(dir));\n@@ -2613,11 +2957,17 @@ static int ext4_dx_add_entry(handle_t *handle, struct ext4_filename *fname,\n \t\t\t/* Set up root */\n \t\t\tdx_set_count(entries, 1);\n \t\t\tdx_set_block(entries + 0, newblock);\n-\t\t\tdxroot = (struct dx_root *)frames[0].bh-\u003eb_data;\n-\t\t\tdxroot-\u003einfo.indirect_levels += 1;\n+\t\t\tinfo = dx_get_dx_info(dir, (struct ext4_dir_entry_2 *)\n+\t\t\t\t\t      frames[0].bh-\u003eb_data);\n+\t\t\tif (IS_ERR(info)) {\n+\t\t\t\terr = PTR_ERR(info);\n+\t\t\t\tbrelse(bh2);\n+\t\t\t\tgoto journal_error;\n+\t\t\t}\n+\t\t\tinfo-\u003eindirect_levels += 1;\n \t\t\tdxtrace(printk(KERN_DEBUG\n \t\t\t\t       \"Creating %d level index...\\n\",\n-\t\t\t\t       dxroot-\u003einfo.indirect_levels));\n+\t\t\t\t       info-\u003eindirect_levels));\n \t\t\terr = ext4_handle_dirty_dx_node(handle, dir, frame-\u003ebh);\n \t\t\tif (err) {\n \t\t\t\tbrelse(bh2);\n@@ -2641,7 +2991,7 @@ static int ext4_dx_add_entry(handle_t *handle, struct ext4_filename *fname,\n \text4_std_error(dir-\u003ei_sb, err); /* this is a no-op if err == 0 */\n cleanup:\n \tbrelse(bh);\n-\tdx_release(frames);\n+\tdx_release(dir, frames);\n \t/* @restart is true means htree-path has been changed, we need to\n \t * repeat dx_probe() to find out valid htree-path\n \t */\n@@ -2927,14 +3277,15 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,\n \tstruct ext4_dir_entry_2 *de = (struct ext4_dir_entry_2 *) bh-\u003eb_data;\n \tsize_t\t\t\tblocksize = bh-\u003eb_size;\n \tint\t\t\tcsum_size = 0, header_size;\n+\tunsigned int\t\tdot_rec_len;\n \n \tif (ext4_has_feature_metadata_csum(inode-\u003ei_sb))\n \t\tcsum_size = sizeof(struct ext4_dir_entry_tail);\n \n \tde-\u003einode = cpu_to_le32(inode-\u003ei_ino);\n \tde-\u003ename_len = 1;\n-\tde-\u003erec_len = ext4_rec_len_to_disk(ext4_dir_rec_len(de-\u003ename_len, NULL),\n-\t\t\t\t\t   blocksize);\n+\tdot_rec_len = ext4_dirent_rec_len(de-\u003ename_len, NULL);\n+\tde-\u003erec_len = ext4_rec_len_to_disk(dot_rec_len, blocksize);\n \tmemcpy(de-\u003ename, \".\", 2);\n \text4_set_de_type(inode-\u003ei_sb, de, S_IFDIR);\n \n@@ -2945,7 +3296,7 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,\n \text4_set_de_type(inode-\u003ei_sb, de, S_IFDIR);\n \tif (inline_buf) {\n \t\tde-\u003erec_len = ext4_rec_len_to_disk(\n-\t\t\t\t\text4_dir_rec_len(de-\u003ename_len, NULL),\n+\t\t\t\t\text4_dirent_rec_len(de-\u003ename_len, NULL),\n \t\t\t\t\tblocksize);\n \t\tde = ext4_next_entry(de, blocksize);\n \t\theader_size = (char *)de - bh-\u003eb_data;\n@@ -2954,7 +3305,7 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,\n \t\t\tblocksize - csum_size);\n \t} else {\n \t\tde-\u003erec_len = ext4_rec_len_to_disk(blocksize -\n-\t\t\t\t\t(csum_size + ext4_dir_rec_len(1, NULL)),\n+\t\t\t\t\t(csum_size + dot_rec_len),\n \t\t\t\t\tblocksize);\n \t}\n \n@@ -2966,8 +3317,9 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,\n \treturn ext4_handle_dirty_dirblock(handle, inode, bh);\n }\n \n-int ext4_init_new_dir(handle_t *handle, struct inode *dir,\n-\t\t\t     struct inode *inode)\n+int ext4_init_new_dir_data(handle_t *handle, struct inode *dir,\n+\t\t\t   struct inode *inode,\n+\t\t\t   const void *data1, const void *data2)\n {\n \tstruct buffer_head *dir_block = NULL;\n \text4_lblk_t block = 0;\n@@ -2986,6 +3338,12 @@ int ext4_init_new_dir(handle_t *handle, struct inode *dir,\n \tdir_block = ext4_append(handle, inode, \u0026block);\n \tif (IS_ERR(dir_block))\n \t\treturn PTR_ERR(dir_block);\n+\t/*\n+\t * data1 and data2 are reserved for callers that need to embed\n+\t * dirdata into the '.' and '..' entries of a new directory.\n+\t * That path is not yet implemented here; a caller requiring it\n+\t * must write the dirdata entries after this function returns.\n+\t */\n \terr = ext4_init_dirblock(handle, inode, dir_block, dir-\u003ei_ino, NULL, 0);\n out:\n \tbrelse(dir_block);\n@@ -3077,8 +3435,8 @@ bool ext4_empty_dir(struct inode *inode)\n \t}\n \n \tsb = inode-\u003ei_sb;\n-\tif (inode-\u003ei_size \u003c ext4_dir_rec_len(1, NULL) +\n-\t\t\t\t\text4_dir_rec_len(2, NULL)) {\n+\tif (inode-\u003ei_size \u003c ext4_dirent_rec_len(1, NULL) +\n+\t\t\t\t\text4_dirent_rec_len(2, NULL)) {\n \t\tEXT4_ERROR_INODE(inode, \"invalid size\");\n \t\treturn false;\n \t}\n@@ -3467,10 +3825,16 @@ int __ext4_link(struct inode *dir, struct inode *inode,\n \tif (IS_DIRSYNC(dir))\n \t\text4_handle_sync(handle);\n \n+\t/* Fast-commit does not record dirdata in its journal format; force a\n+\t * full journal commit so the LUFID survives crash+replay. */\n+\tif (ext4_dentry_get_fid(dir-\u003ei_sb, dentry ? dentry-\u003ed_fsdata : NULL))\n+\t\text4_fc_mark_ineligible(dir-\u003ei_sb, EXT4_FC_REASON_DIRDATA, handle);\n+\n \tinode_set_ctime_current(inode);\n \text4_inc_count(inode);\n \n-\terr = __ext4_add_entry(handle, dir, d_name, inode);\n+\terr = __ext4_add_entry(handle, dir, d_name, inode,\n+\t\t\t       dentry ? dentry-\u003ed_fsdata : NULL);\n \tif (!err) {\n \t\terr = ext4_mark_inode_dirty(handle, inode);\n \t\t/* this can happen only for tmpfile being\n@@ -3638,8 +4002,46 @@ static int ext4_rename_dir_finish(handle_t *handle, struct ext4_renament *ent,\n \treturn 0;\n }\n \n+/*\n+ * When LUFID is cleared from an entry that also carries CFHASH, the CFHASH\n+ * bytes are stranded: LUFID precedes CFHASH in the layout, so removing LUFID\n+ * shifts the expected CFHASH offset.  Slide CFHASH into the vacated slot so\n+ * the parser finds it at the right position.  If the headers are malformed,\n+ * clear CFHASH instead of risking a corrupt read.\n+ */\n+static void ext4_setent_compact_exts(struct ext4_renament *ent,\n+\t\t\t\t     struct ext4_dirent_data_header *lddh,\n+\t\t\t\t     unsigned int lufid_off,\n+\t\t\t\t     unsigned int rec_len)\n+{\n+\tstruct ext4_dirent_data_header *cddh;\n+\tunsigned int coff;\n+\n+\tif (!(ent-\u003ede-\u003efile_type \u0026 EXT4_DIRENT_CFHASH))\n+\t\treturn;\n+\tif (lufid_off + sizeof(*lddh) \u003e rec_len ||\n+\t    !lddh-\u003eddh_length ||\n+\t    lufid_off + lddh-\u003eddh_length \u003e rec_len) {\n+\t\tent-\u003ede-\u003efile_type \u0026= ~EXT4_DIRENT_CFHASH;\n+\t\treturn;\n+\t}\n+\n+\tcoff = lufid_off + lddh-\u003eddh_length;\n+\tcddh = (struct ext4_dirent_data_header *)((char *)ent-\u003ede + coff);\n+\n+\tif (coff + sizeof(*cddh) \u003e rec_len ||\n+\t    !cddh-\u003eddh_length ||\n+\t    coff + cddh-\u003eddh_length \u003e rec_len) {\n+\t\tent-\u003ede-\u003efile_type \u0026= ~EXT4_DIRENT_CFHASH;\n+\t\treturn;\n+\t}\n+\n+\tmemmove(lddh, cddh, cddh-\u003eddh_length);\n+}\n+\n static int ext4_setent(handle_t *handle, struct ext4_renament *ent,\n-\t\t       unsigned ino, unsigned file_type)\n+\t\t       unsigned ino, unsigned file_type,\n+\t\t       const struct ext4_dirent_fid *src_fid)\n {\n \tint retval, retval2;\n \n@@ -3649,8 +4051,86 @@ static int ext4_setent(handle_t *handle, struct ext4_renament *ent,\n \tif (retval)\n \t\treturn retval;\n \tent-\u003ede-\u003einode = cpu_to_le32(ino);\n-\tif (ext4_has_feature_filetype(ent-\u003edir-\u003ei_sb))\n-\t\tent-\u003ede-\u003efile_type = file_type;\n+\tif (ext4_has_feature_filetype(ent-\u003edir-\u003ei_sb)) {\n+\t\t/* Copy the source LUFID payload into the destination slot when\n+\t\t * both carry a LUFID of the same on-disk size.  LUFID is\n+\t\t * inode-specific and must travel with the inode through rename.\n+\t\t * CFHASH bytes are left in-place: CFHASH is a function of the\n+\t\t * destination filename, and its bytes sit at the correct offset\n+\t\t * because LUFID always precedes CFHASH in the extension layout.\n+\t\t * When src_fid is NULL, the LUFID flag is synced to the LUFID\n+\t\t * bit already present in file_type: callers that clear LUFID\n+\t\t * (rename of a non-LUFID inode, whiteout) pass file_type with\n+\t\t * LUFID=0; ext4_resetent passes the original file_type so the\n+\t\t * flag is restored for error recovery without overwriting the\n+\t\t * LUFID bytes that are still intact on disk. */\n+\t\tif (ext4_has_feature_dirdata(ent-\u003edir-\u003ei_sb)) {\n+\t\t\tif (src_fid \u0026\u0026 (ent-\u003ede-\u003efile_type \u0026 EXT4_DIRENT_LUFID)) {\n+\t\t\t\tunsigned int rec_len =\n+\t\t\t\t\text4_rec_len_from_disk(ent-\u003ede-\u003erec_len,\n+\t\t\t\t\t\tent-\u003edir-\u003ei_sb-\u003es_blocksize);\n+\t\t\t\tunsigned int ddh_off =\n+\t\t\t\t\tEXT4_BASE_DIR_LEN + ent-\u003ede-\u003ename_len + 1;\n+\t\t\t\tstruct ext4_dirent_data_header *ddh =\n+\t\t\t\t\t(struct ext4_dirent_data_header *)\n+\t\t\t\t\t((char *)ent-\u003ede + ddh_off);\n+\t\t\t\tunsigned int copy_len = src_fid-\u003edf_header.ddh_length;\n+\n+\t\t\t\tif (ddh_off + sizeof(*ddh) \u003e rec_len ||\n+\t\t\t\t    ddh-\u003eddh_length != copy_len ||\n+\t\t\t\t    ddh_off + copy_len \u003e rec_len) {\n+\t\t\t\t\t/* Cannot copy in-place (size mismatch).\n+\t\t\t\t\t * If CFHASH is also present, slide it\n+\t\t\t\t\t * into the LUFID slot so the parser\n+\t\t\t\t\t * finds it at the correct offset after\n+\t\t\t\t\t * the LUFID flag is cleared. */\n+\t\t\t\t\text4_setent_compact_exts(ent, ddh,\n+\t\t\t\t\t\t\tddh_off, rec_len);\n+\t\t\t\t\tent-\u003ede-\u003efile_type \u0026= ~EXT4_DIRENT_LUFID;\n+\t\t\t\t} else {\n+\t\t\t\t\tmemcpy(ddh, src_fid, copy_len);\n+\t\t\t\t}\n+\t\t\t} else if (src_fid) {\n+\t\t\t\t/* Destination has no LUFID slot; cannot\n+\t\t\t\t * propagate in-place.  Clear the flag from\n+\t\t\t\t * the on-disk entry directly — clearing only\n+\t\t\t\t * the local file_type variable has no effect\n+\t\t\t\t * because the final assignment takes the high\n+\t\t\t\t * bits from ent-\u003ede-\u003efile_type, not file_type. */\n+\t\t\t\tent-\u003ede-\u003efile_type \u0026= ~EXT4_DIRENT_LUFID;\n+\t\t\t} else if (!src_fid) {\n+\t\t\t\t/* Sync the LUFID flag with what file_type requests.\n+\t\t\t\t * For normal rename (source has no LUFID) and for\n+\t\t\t\t * the whiteout setent, file_type carries LUFID=0,\n+\t\t\t\t * so we clear the stale flag.  For ext4_resetent\n+\t\t\t\t * (error recovery), file_type is the original\n+\t\t\t\t * file_type with LUFID=1, so we restore the flag —\n+\t\t\t\t * the LUFID bytes are still on disk untouched.\n+\t\t\t\t * When clearing LUFID and CFHASH is present, slide\n+\t\t\t\t * CFHASH bytes into the LUFID slot first so the\n+\t\t\t\t * parser finds CFHASH at the expected offset. */\n+\t\t\t\tif (!(file_type \u0026 EXT4_DIRENT_LUFID) \u0026\u0026\n+\t\t\t\t    (ent-\u003ede-\u003efile_type \u0026 EXT4_DIRENT_LUFID)) {\n+\t\t\t\t\tunsigned int loff =\n+\t\t\t\t\t\tEXT4_BASE_DIR_LEN +\n+\t\t\t\t\t\tent-\u003ede-\u003ename_len + 1;\n+\t\t\t\t\tunsigned int rlen =\n+\t\t\t\t\t\text4_rec_len_from_disk(\n+\t\t\t\t\t\t\tent-\u003ede-\u003erec_len,\n+\t\t\t\t\t\t\tent-\u003edir-\u003ei_sb-\u003es_blocksize);\n+\t\t\t\t\text4_setent_compact_exts(ent,\n+\t\t\t\t\t\t(struct ext4_dirent_data_header *)\n+\t\t\t\t\t\t((char *)ent-\u003ede + loff),\n+\t\t\t\t\t\tloff, rlen);\n+\t\t\t\t}\n+\t\t\t\tent-\u003ede-\u003efile_type =\n+\t\t\t\t\t(ent-\u003ede-\u003efile_type \u0026 ~EXT4_DIRENT_LUFID) |\n+\t\t\t\t\t(file_type \u0026 EXT4_DIRENT_LUFID);\n+\t\t\t}\n+\t\t}\n+\t\tent-\u003ede-\u003efile_type = (file_type \u0026 EXT4_FT_MASK) |\n+\t\t\t\t     (ent-\u003ede-\u003efile_type \u0026 ~EXT4_FT_MASK);\n+\t}\n \tinode_inc_iversion(ent-\u003edir);\n \tinode_set_mtime_to_ts(ent-\u003edir, inode_set_ctime_current(ent-\u003edir));\n \tretval = ext4_mark_inode_dirty(handle, ent-\u003edir);\n@@ -3687,7 +4167,7 @@ static void ext4_resetent(handle_t *handle, struct ext4_renament *ent,\n \t\treturn;\n \t}\n \n-\text4_setent(handle, \u0026old, ino, file_type);\n+\text4_setent(handle, \u0026old, ino, file_type, NULL);\n \tbrelse(old.bh);\n }\n \n@@ -3786,6 +4266,67 @@ static struct inode *ext4_whiteout_for_rename(struct mnt_idmap *idmap,\n \treturn wh;\n }\n \n+/*\n+ * Stack buffer sized for 1-byte header + 4 × 16-byte FIDs, covering virtually\n+ * all real-world LUFID payloads without a heap allocation.  ddh_length is u8\n+ * so the absolute maximum is 255 bytes; larger payloads fall back to kmalloc.\n+ */\n+#define EXT4_LUFID_INLINE_SIZE  65\n+\n+struct ext4_lufid_snap {\n+\tstruct ext4_dirent_fid *fid;\t/* NULL or valid; never ERR_PTR */\n+\tbool on_heap;\n+\tu8 buf[EXT4_LUFID_INLINE_SIZE];\n+};\n+\n+/*\n+ * ext4_lufid_snapshot - copy the LUFID record from a directory entry\n+ *\n+ * Fills @snap with a copy of the on-disk LUFID record.  For payloads up to\n+ * EXT4_LUFID_INLINE_SIZE bytes the inline buffer is used; larger payloads\n+ * fall back to a heap allocation.  Returns 0 on success (snap-\u003efid is NULL\n+ * when no LUFID is present), or -ENOMEM when a heap allocation fails.\n+ * Call ext4_lufid_snap_release() when done.\n+ */\n+static int ext4_lufid_snapshot(struct ext4_dir_entry_2 *de,\n+\t\t\t       unsigned int blocksize,\n+\t\t\t       struct ext4_lufid_snap *snap)\n+{\n+\tunsigned int ddh_off = EXT4_BASE_DIR_LEN + de-\u003ename_len + 1;\n+\tunsigned int rec_len = ext4_rec_len_from_disk(de-\u003erec_len, blocksize);\n+\tstruct ext4_dirent_fid *disk_fid;\n+\tunsigned int dlen;\n+\n+\tsnap-\u003efid = NULL;\n+\tsnap-\u003eon_heap = false;\n+\n+\tif (!(de-\u003efile_type \u0026 EXT4_DIRENT_LUFID))\n+\t\treturn 0;\n+\tif (ddh_off + sizeof(disk_fid-\u003edf_header) \u003e rec_len)\n+\t\treturn 0;\n+\tdisk_fid = (struct ext4_dirent_fid *)((char *)de + ddh_off);\n+\tdlen = disk_fid-\u003edf_header.ddh_length;\n+\tif (dlen \u003c sizeof(disk_fid-\u003edf_header) || ddh_off + dlen \u003e rec_len)\n+\t\treturn 0;\n+\n+\tif (dlen \u003c= sizeof(snap-\u003ebuf)) {\n+\t\tmemcpy(snap-\u003ebuf, disk_fid, dlen);\n+\t\tsnap-\u003efid = (struct ext4_dirent_fid *)snap-\u003ebuf;\n+\t} else {\n+\t\tsnap-\u003efid = kmemdup(disk_fid, dlen, GFP_NOFS);\n+\t\tif (!snap-\u003efid)\n+\t\t\treturn -ENOMEM;\n+\t\tsnap-\u003eon_heap = true;\n+\t}\n+\treturn 0;\n+}\n+\n+static void ext4_lufid_snap_release(struct ext4_lufid_snap *snap)\n+{\n+\tif (snap-\u003eon_heap)\n+\t\tkfree(snap-\u003efid);\n+}\n+\n /*\n  * Anybody can rename anything with this: the permission checks are left to the\n  * higher-level routines.\n@@ -3814,6 +4355,7 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,\n \tstruct inode *whiteout = NULL;\n \tint credits;\n \tu8 old_file_type;\n+\tstruct ext4_lufid_snap old_snap = {};\n \n \tif (new.inode \u0026\u0026 new.inode-\u003ei_nlink == 0) {\n \t\tEXT4_ERROR_INODE(new.inode,\n@@ -3919,27 +4461,59 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,\n \tforce_reread = (new.dir-\u003ei_ino == old.dir-\u003ei_ino \u0026\u0026\n \t\t\text4_test_inode_flag(new.dir, EXT4_INODE_INLINE_DATA));\n \n+\tif (ext4_has_feature_dirdata(old.dir-\u003ei_sb)) {\n+\t\t/* Snapshot before whiteout's ext4_setent() clears the LUFID\n+\t\t * flag in old.de; the snapshot is also needed for the !new.bh\n+\t\t * path below where we must carry the LUFID to the new entry. */\n+\t\tretval = ext4_lufid_snapshot(old.de,\n+\t\t\t\t\t     old.dir-\u003ei_sb-\u003es_blocksize,\n+\t\t\t\t\t     \u0026old_snap);\n+\t\tif (retval)\n+\t\t\tgoto end_rename;\n+\t\tif (old_snap.fid)\n+\t\t\text4_fc_mark_ineligible(old.dir-\u003ei_sb,\n+\t\t\t\t\t\tEXT4_FC_REASON_DIRDATA, handle);\n+\t\telse\n+\t\t\told_file_type \u0026= ~EXT4_DIRENT_LUFID;\n+\t}\n+\n \tif (whiteout) {\n \t\t/*\n \t\t * Do this before adding a new entry, so the old entry is sure\n \t\t * to be still pointing to the valid old entry.\n \t\t */\n \t\tretval = ext4_setent(handle, \u0026old, whiteout-\u003ei_ino,\n-\t\t\t\t     EXT4_FT_CHRDEV);\n+\t\t\t\t     EXT4_FT_CHRDEV, NULL);\n \t\tif (retval)\n \t\t\tgoto end_rename;\n \t\tretval = ext4_mark_inode_dirty(handle, whiteout);\n \t\tif (unlikely(retval))\n \t\t\tgoto end_rename;\n-\n \t}\n \tif (!new.bh) {\n+\t\tstruct ext4_dentry_param *edp = NULL;\n+\t\tif (old_snap.fid) {\n+\t\t\t/* Wrap the snapshotted LUFID in an ext4_dentry_param so\n+\t\t\t * ext4_add_entry() picks it up via d_fsdata. */\n+\t\t\tunsigned fid_size = old_snap.fid-\u003edf_header.ddh_length;\n+\n+\t\t\tedp = kmalloc(sizeof(*edp) + fid_size, GFP_NOFS);\n+\t\t\tif (!edp) {\n+\t\t\t\tretval = -ENOMEM;\n+\t\t\t\tgoto end_rename;\n+\t\t\t}\n+\t\t\tedp-\u003eedp_magic = EXT4_LUFID_MAGIC;\n+\t\t\tmemcpy(edp-\u003eedp_dfid, old_snap.fid, fid_size);\n+\t\t}\n+\t\tnew.dentry-\u003ed_fsdata = edp;\n \t\tretval = ext4_add_entry(handle, new.dentry, old.inode);\n+\t\tnew.dentry-\u003ed_fsdata = NULL;\n+\t\tkfree(edp);\n \t\tif (retval)\n \t\t\tgoto end_rename;\n \t} else {\n \t\tretval = ext4_setent(handle, \u0026new,\n-\t\t\t\t     old.inode-\u003ei_ino, old_file_type);\n+\t\t\t\t     old.inode-\u003ei_ino, old_file_type, old_snap.fid);\n \t\tif (retval)\n \t\t\tgoto end_rename;\n \t}\n@@ -4026,6 +4600,7 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,\n \tretval = 0;\n \n end_rename:\n+\text4_lufid_snap_release(\u0026old_snap);\n \tif (whiteout) {\n \t\tif (retval) {\n \t\t\text4_resetent(handle, \u0026old,\n@@ -4062,8 +4637,9 @@ static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry,\n \t\t.dentry = new_dentry,\n \t\t.inode = d_inode(new_dentry),\n \t};\n-\tu8 new_file_type;\n+\tu8 new_file_type, old_de_file_type;\n \tint retval;\n+\tstruct ext4_lufid_snap old_snap = {}, new_snap = {};\n \n \tif ((ext4_test_inode_flag(new_dir, EXT4_INODE_PROJINHERIT) \u0026\u0026\n \t     !projid_eq(EXT4_I(new_dir)-\u003ei_projid,\n@@ -4142,12 +4718,33 @@ static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry,\n \t\t\tgoto end_rename;\n \t}\n \n+\tif (ext4_has_feature_dirdata(old.dir-\u003ei_sb)) {\n+\t\tretval = ext4_lufid_snapshot(old.de, old.dir-\u003ei_sb-\u003es_blocksize,\n+\t\t\t\t\t     \u0026old_snap);\n+\t\tif (retval)\n+\t\t\tgoto end_rename;\n+\t\tretval = ext4_lufid_snapshot(new.de, new.dir-\u003ei_sb-\u003es_blocksize,\n+\t\t\t\t\t     \u0026new_snap);\n+\t\tif (retval)\n+\t\t\tgoto end_rename;\n+\t\tif (old_snap.fid || new_snap.fid)\n+\t\t\text4_fc_mark_ineligible(old.dir-\u003ei_sb,\n+\t\t\t\t\t\tEXT4_FC_REASON_DIRDATA, handle);\n+\t}\n+\n+\told_de_file_type = old.de-\u003efile_type;\n+\tif (!old_snap.fid)\n+\t\told_de_file_type \u0026= ~EXT4_DIRENT_LUFID;\n \tnew_file_type = new.de-\u003efile_type;\n-\tretval = ext4_setent(handle, \u0026new, old.inode-\u003ei_ino, old.de-\u003efile_type);\n+\tif (!new_snap.fid)\n+\t\tnew_file_type \u0026= ~EXT4_DIRENT_LUFID;\n+\tretval = ext4_setent(handle, \u0026new, old.inode-\u003ei_ino, old_de_file_type,\n+\t\t\t     old_snap.fid);\n \tif (retval)\n \t\tgoto end_rename;\n \n-\tretval = ext4_setent(handle, \u0026old, new.inode-\u003ei_ino, new_file_type);\n+\tretval = ext4_setent(handle, \u0026old, new.inode-\u003ei_ino, new_file_type,\n+\t\t\t     new_snap.fid);\n \tif (retval)\n \t\tgoto end_rename;\n \n@@ -4180,6 +4777,8 @@ static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry,\n \tretval = 0;\n \n end_rename:\n+\text4_lufid_snap_release(\u0026old_snap);\n+\text4_lufid_snap_release(\u0026new_snap);\n \tbrelse(old.dir_bh);\n \tbrelse(new.dir_bh);\n \tbrelse(old.bh);\n@@ -4216,6 +4815,202 @@ static int ext4_rename2(struct mnt_idmap *idmap,\n \treturn ext4_rename(idmap, old_dir, old_dentry, new_dir, new_dentry, flags);\n }\n \n+/*\n+ * ext4_dirdata_set_lufid() - Set LUFID data on an existing directory entry\n+ * @dir:        parent directory inode\n+ * @filename:   name of the file in the directory\n+ * @namelen:    length of filename\n+ * @edp:        pointer to initialized dentry param with LUFID data\n+ *\n+ * This function finds an existing directory entry, deletes it, and re-creates it\n+ * with LUFID data attached. Used by the EXT4_IOC_SET_LUFID ioctl.\n+ *\n+ * Returns 0 on success, negative error code on failure.\n+ */\n+int ext4_dirdata_set_lufid(struct mnt_idmap *idmap, struct inode *dir,\n+\t\t\t    const char *filename, int namelen,\n+\t\t\t    struct ext4_dentry_param *edp)\n+{\n+\tstruct super_block *sb = dir-\u003ei_sb;\n+\t /* zero-init: safe to free on any path */\n+\tstruct ext4_filename fname = {};\n+\tstruct ext4_dir_entry_2 *de = NULL;\n+\tstruct buffer_head *bh = NULL;\n+\tstruct inode *inode = NULL;\n+\thandle_t *handle = NULL;\n+\tstruct qstr d_name;\n+\t__u32 ino = 0;\n+\tbool child_locked = false;\n+\tint err = 0;\n+\n+\tif (!ext4_has_feature_dirdata(sb))\n+\t\treturn -EOPNOTSUPP;\n+\n+\tif (namelen \u003e EXT4_NAME_LEN)\n+\t\treturn -ENAMETOOLONG;\n+\tif (namelen != strnlen(filename, namelen + 1))\n+\t\treturn -EINVAL;\n+\n+\td_name.name = filename;\n+\td_name.len = namelen;\n+\n+\terr = ext4_fname_setup_filename(dir, \u0026d_name, 0, \u0026fname);\n+\tif (err)\n+\t\tgoto out_free;\n+\n+\t/* Lock dir with the VFS parent-mutation subclass.  The lookup and any\n+\t * mutation must be inside this lock to prevent TOCTOU races. */\n+\tinode_lock_nested(dir, I_MUTEX_PARENT);\n+\n+\t/* Phase 1: look up the entry without holding a journal handle to\n+\t * obtain the child inode reference before taking the child lock.\n+\t * VFS locking order requires all inode locks to be acquired before\n+\t * starting a jbd2 transaction; starting the journal while already\n+\t * holding the child lock would invert that order and risk deadlock\n+\t * if jbd2 blocks waiting for a commit that the child lock holder\n+\t * prevents from finishing. */\n+\tbh = ext4_find_entry(dir, \u0026d_name, \u0026de, NULL);\n+\tif (IS_ERR(bh)) {\n+\t\terr = PTR_ERR(bh);\n+\t\tbh = NULL;\n+\t\tgoto out_unlock_dir;\n+\t}\n+\tif (!bh) {\n+\t\terr = -ENOENT;\n+\t\tgoto out_unlock_dir;\n+\t}\n+\tino = le32_to_cpu(de-\u003einode);\n+\tbrelse(bh);\n+\tbh = NULL;\n+\tde = NULL;\n+\n+\tinode = ext4_iget(sb, ino, EXT4_IGET_NORMAL);\n+\tif (IS_ERR(inode)) {\n+\t\terr = PTR_ERR(inode);\n+\t\tinode = NULL;\n+\t\tgoto out_unlock_dir;\n+\t}\n+\n+\t/* Enforce sticky-bit restriction: on a sticky directory only the\n+\t * directory owner, the file owner, or a privileged process may\n+\t * modify entries.  inode_permission(MAY_WRITE) alone does not cover\n+\t * this, so check it explicitly here as may_delete() does. */\n+\terr = check_sticky(idmap, dir, inode);\n+\tif (err)\n+\t\tgoto out_iput;\n+\n+\t/* The ioctl deletes and re-creates the directory entry, which\n+\t * effectively modifies the target file's namespace presence.\n+\t * Refuse if the target itself is immutable or append-only. */\n+\tif (IS_IMMUTABLE(inode) || IS_APPEND(inode)) {\n+\t\terr = -EPERM;\n+\t\tgoto out_iput;\n+\t}\n+\n+\t/* Lock the target inode BEFORE starting the journal (dir-then-child,\n+\t * VFS-locks-before-journal ordering).  Use I_MUTEX_CHILD for directory\n+\t * inodes (consistent with VFS rename/rmdir) and I_MUTEX_NONDIR2 for\n+\t * all others; mixing subclasses on the same inode triggers lockdep\n+\t * cycles with concurrent rename. */\n+\tif (inode != dir) {\n+\t\tif (S_ISDIR(inode-\u003ei_mode))\n+\t\t\tinode_lock_nested(inode, I_MUTEX_CHILD);\n+\t\telse\n+\t\t\tinode_lock_nested(inode, I_MUTEX_NONDIR2);\n+\t\tchild_locked = true;\n+\t}\n+\n+\thandle = ext4_journal_start(dir, EXT4_HT_DIR,\n+\t\t\t\t    3 * EXT4_DATA_TRANS_BLOCKS(sb) +\n+\t\t\t\t    2 * EXT4_INDEX_EXTRA_TRANS_BLOCKS);\n+\tif (IS_ERR(handle)) {\n+\t\terr = PTR_ERR(handle);\n+\t\thandle = NULL;\n+\t\tgoto out_unlock;\n+\t}\n+\n+\t/* Phase 2: re-look up the entry inside the journal to obtain a stable\n+\t * de pointer.  Verify it still refers to the same inode (TOCTOU\n+\t * guard: a concurrent unlink+create between phase 1 and here would\n+\t * change the inode number). */\n+\tbh = ext4_find_entry(dir, \u0026d_name, \u0026de, NULL);\n+\tif (IS_ERR(bh)) {\n+\t\terr = PTR_ERR(bh);\n+\t\tbh = NULL;\n+\t\tgoto out_journal;\n+\t}\n+\tif (!bh) {\n+\t\terr = -ENOENT;\n+\t\tgoto out_journal;\n+\t}\n+\tif (le32_to_cpu(de-\u003einode) != ino) {\n+\t\terr = -ENOENT;\n+\t\tgoto out_brelse;\n+\t}\n+\n+\terr = ext4_delete_entry(handle, dir, de, bh);\n+\tif (err)\n+\t\tgoto out_brelse;\n+\n+\tbrelse(bh);\n+\tbh = NULL;\n+\n+\t/* Re-add with LUFID via dentry-\u003ed_fsdata.  ext4_add_entry() resolves\n+\t * dfid from d_fsdata into fname.dfid and passes it through to\n+\t * add_dirent_to_buf() without touching the shared i_dirdata field,\n+\t * eliminating the race with concurrent link() calls. */\n+\t{\n+\t\tstruct dentry parent_dentry = { .d_inode = dir };\n+\t\tstruct dentry new_dentry = {\n+\t\t\t.d_name = d_name,\n+\t\t\t.d_parent = \u0026parent_dentry,\n+\t\t\t.d_inode = inode,\n+\t\t\t.d_fsdata = edp,\n+\t\t};\n+\t\terr = ext4_add_entry(handle, \u0026new_dentry, inode);\n+\t}\n+\n+\tif (err) {\n+\t\t/* Delete succeeded but re-add failed; try to restore so the\n+\t\t * inode is not left without a directory entry. */\n+\t\tstruct dentry parent_dentry = { .d_inode = dir };\n+\t\tstruct dentry orig_dentry = {\n+\t\t\t.d_name = d_name,\n+\t\t\t.d_parent = \u0026parent_dentry,\n+\t\t\t.d_inode = inode,\n+\t\t};\n+\t\tint rollback_err = ext4_add_entry(handle, \u0026orig_dentry, inode);\n+\n+\t\tif (rollback_err)\n+\t\t\tEXT4_ERROR_INODE(dir,\n+\t\t\t\t\"Failed to set LUFID on '%.*s' (err=%d) and failed to restore the original directory entry (err=%d); inode %llu may be orphaned\",\n+\t\t\t\tnamelen, filename, err, rollback_err,\n+\t\t\t\tinode-\u003ei_ino);\n+\t\tgoto out_journal;\n+\t}\n+\n+\tinode_set_ctime_current(dir);\n+\tinode_inc_iversion(dir);\n+\text4_mark_inode_dirty(handle, dir);\n+\n+out_brelse:\n+\tbrelse(bh);\n+out_journal:\n+\tif (handle)\n+\t\text4_journal_stop(handle);\n+out_unlock:\n+\tif (child_locked)\n+\t\tinode_unlock(inode);\n+out_iput:\n+\tiput(inode);\n+out_unlock_dir:\n+\tinode_unlock(dir);\n+out_free:\n+\text4_fname_free_filename(\u0026fname);\n+\n+\treturn err;\n+}\n+\n /*\n  * directories can handle most operations...\n  */\ndiff --git a/fs/ext4/sysfs.c b/fs/ext4/sysfs.c\nindex 923b375e017fa..80074fb15ee9b 100644\n--- a/fs/ext4/sysfs.c\n+++ b/fs/ext4/sysfs.c\n@@ -362,6 +362,7 @@ EXT4_ATTR_FEATURE(verity);\n #endif\n EXT4_ATTR_FEATURE(metadata_csum_seed);\n EXT4_ATTR_FEATURE(fast_commit);\n+EXT4_ATTR_FEATURE(dirdata);\n #if IS_ENABLED(CONFIG_UNICODE) \u0026\u0026 defined(CONFIG_FS_ENCRYPTION)\n EXT4_ATTR_FEATURE(encrypted_casefold);\n #endif\n@@ -385,6 +386,7 @@ static struct attribute *ext4_feat_attrs[] = {\n #endif\n \tATTR_LIST(metadata_csum_seed),\n \tATTR_LIST(fast_commit),\n+\tATTR_LIST(dirdata),\n #if IS_ENABLED(CONFIG_UNICODE) \u0026\u0026 defined(CONFIG_FS_ENCRYPTION)\n \tATTR_LIST(encrypted_casefold),\n #endif\ndiff --git a/include/uapi/linux/ext4.h b/include/uapi/linux/ext4.h\nindex 9c683991c32f9..9134fe64947a9 100644\n--- a/include/uapi/linux/ext4.h\n+++ b/include/uapi/linux/ext4.h\n@@ -35,6 +35,7 @@\n #define EXT4_IOC_SETFSUUID\t\t_IOW('f', 44, struct fsuuid)\n #define EXT4_IOC_GET_TUNE_SB_PARAM\t_IOR('f', 45, struct ext4_tune_sb_params)\n #define EXT4_IOC_SET_TUNE_SB_PARAM\t_IOW('f', 46, struct ext4_tune_sb_params)\n+#define EXT4_IOC_SET_LUFID\t\t_IOW('f', 47, struct ext4_set_lufid)\n \n #define EXT4_IOC_SHUTDOWN _IOR('X', 125, __u32)\n \n@@ -92,6 +93,19 @@ struct move_extent {\n \t__u64 moved_len;\t/* moved block length */\n };\n \n+/*\n+ * Structure for EXT4_IOC_SET_LUFID\n+ * Sets LUFID on a directory entry\n+ * Called on parent directory with filename and LUFID data as arguments\n+ */\n+struct ext4_set_lufid {\n+\t__u8 esl_name_len;\t/* length of filename, NOT including NUL terminator\n+\t\t\t\t * (valid range: 1-255, matching EXT4_NAME_LEN) */\n+\tchar  esl_name[255 + 1]; /* filename (NUL-terminated) */\n+\t__u8 esl_data_len;\t/* length of LUFID data */\n+\tchar  esl_data[255]; /* LUFID data (raw bytes) */\n+};\n+\n /*\n  * Flags used by EXT4_IOC_SHUTDOWN\n  */\n"
}

2/1 2026/08/01 10:15 agent patch-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "EXT4_FS"
  ],
  "FocusSymbols": [
    "ext4_dirdata_set_lufid",
    "ext4_insert_dentry_data",
    "ext4_dirdata_get",
    "ext4_init_new_dir_data"
  ],
  "Reasoning": "The patch introduces a new ioctl EXT4_IOC_SET_LUFID and modifies the ext4 directory entry format to support 'dirdata' extensions (LUFID, INO64, CFHASH). This adds new reachable core filesystem logic for directory entry manipulation, lookup, and modification, which should be fuzzed.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit a24e41981bf68ae6ead3515d9750998d99ccdd73
Author: syz-cluster <triage@syzkaller.com>
Date:   Sat Aug 1 10:15:43 2026 +0000

    syz-cluster: applied patch under review

diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
index 17edd678fa87b..81697fce5e303 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";
@@ -245,7 +244,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,
@@ -288,8 +287,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;
@@ -468,27 +478,44 @@ 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 (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);
+	}
+
+	/* 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 b37c136ea3ab3..e9447fb3f22c2 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -2334,6 +2334,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 | \
@@ -2472,10 +2473,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 */
@@ -2556,6 +2555,61 @@ 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 */
+};
+
+struct ext4_dirent_hash {
+	struct ext4_dirent_data_header	dh_header;
+	struct ext4_dir_entry_hash	dh_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
 
@@ -2573,13 +2627,26 @@ struct ext4_dir_entry_tail {
  * 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))
+	/*
+	 * Without dirdata, the casefold+fscrypt hash lives at a fixed position
+	 * after the filename and must be reserved explicitly.  With dirdata the
+	 * hash is stored as a CFHASH extension and is already counted in the
+	 * name_len argument (via ext4_dirent_get_data_len), so adding it again
+	 * would double-count.
+	 */
+	if (dir && ext4_hash_in_dirent(dir) &&
+	    !ext4_has_feature_dirdata(dir->i_sb))
 		rec_len += sizeof(struct ext4_dir_entry_hash);
 	return (rec_len & ~EXT4_DIR_ROUND);
 }
@@ -2667,6 +2734,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)
@@ -2978,18 +3047,27 @@ 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);
 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);
-void ext4_insert_dentry(struct inode *dir, struct inode *inode,
-			struct ext4_dir_entry_2 *de,
-			int buf_size,
-			struct ext4_filename *fname);
+			     struct ext4_dir_entry_2 **dest_de,
+			     int dlen);
+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)
+{
+	return 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) &&
@@ -3003,12 +3081,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);
@@ -3231,10 +3311,19 @@ 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_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,
@@ -3807,6 +3896,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] = {
@@ -4004,6 +4097,107 @@ 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;
+}
+
+/*
+ * 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;
+}
+
+/*
+ * 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.
+ */
+static inline unsigned int ext4_dir_entry_len(struct ext4_dir_entry_2 *de,
+					      unsigned int blocksize,
+					      const struct inode *dir)
+{
+	unsigned int rec_len = ext4_rec_len_from_disk(de->rec_len, blocksize);
+	unsigned int dirdata = ext4_dirent_get_data_len(de, rec_len);
+
+	return ext4_dirent_rec_len(de->name_len + dirdata, 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 8e2259799614e..006d45e9ce879 100644
--- a/fs/ext4/fast_commit.c
+++ b/fs/ext4/fast_commit.c
@@ -2697,6 +2697,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 2f77a37fb1018..899faeb4e6e06 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 8045e4ff270c7..ac984e576898b 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -973,11 +973,20 @@ static int ext4_add_dirent_to_inline(handle_t *handle,
 				     struct ext4_iloc *iloc,
 				     void *inline_start, int inline_size)
 {
-	int		err;
+	int		err, dlen = 0;
 	struct ext4_dir_entry_2 *de;
+	struct ext4_dirent_fid *dfid = NULL;
+
+	/* fname->dfid was set in ext4_add_entry() */
+	dfid = fname->dfid;
+	if (dfid)
+		dlen = dfid->df_header.ddh_length;
+	if (ext4_hash_in_dirent(dir) &&
+	    ext4_has_feature_dirdata(dir->i_sb))
+		dlen += sizeof(struct ext4_dirent_hash);
 
 	err = ext4_find_dest_de(dir, iloc->bh, inline_start,
-				inline_size, fname, &de);
+				inline_size, fname, &de, dlen);
 	if (err)
 		return err;
 
@@ -986,7 +995,9 @@ 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);
+	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);
 
@@ -1055,7 +1066,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,
@@ -1264,6 +1275,7 @@ 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;
@@ -1309,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;
@@ -1319,30 +1331,46 @@ 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;
 			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)) {
-			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) ||
@@ -1354,7 +1382,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;
@@ -1427,8 +1456,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 +1492,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);
diff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c
index c8387e6a2c6e9..5bac4ad834ad1 100644
--- a/fs/ext4/ioctl.c
+++ b/fs/ext4/ioctl.c
@@ -1535,6 +1535,88 @@ 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[255];
+	} 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;
+
+	/* ddh_length (esl_data_len + the header byte below) must itself fit
+	 * in the __u8 ddh_length field without wrapping */
+	if (lufid_args.esl_data_len == 0 ||
+	    lufid_args.esl_data_len > 255 - sizeof(edp.df_header))
+		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);
@@ -1921,6 +2003,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;
 	}
@@ -2000,6 +2084,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 cc49ae04a6f64..2024de1efa715 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)
@@ -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
@@ -317,13 +308,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)
@@ -331,14 +322,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;
@@ -410,35 +397,44 @@ 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 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)
-		count_offset = 8;
-	else if (rlen == 12) {
-		dp = (struct ext4_dir_entry *)(((void *)dirent) + 12);
-		if (ext4_rec_len_from_disk(dp->rec_len, blocksize) != blocksize - 12)
+	if (rlen == blocksize) {
+		count_offset = sizeof(struct dx_node);
+	} else {
+		if (rlen < EXT4_BASE_DIR_LEN ||
+		    rlen + EXT4_BASE_DIR_LEN > blocksize)
 			return NULL;
-		root = (struct dx_root_info *)(((void *)dp + 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) > 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;
 	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 +452,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;
@@ -477,6 +473,11 @@ 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 (%i > %i)? "
+				 "Run e2fsck -D.", count, limit);
+		return 0;
+	}
 	t = (struct dx_tail *)(((struct dx_entry *)c) + limit);
 
 	if (t->dt_checksum != ext4_dx_csum(inode, dirent, count_offset,
@@ -485,7 +486,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;
@@ -506,6 +507,11 @@ 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 (%i > %i)? "
+				 " Run e2fsck -D.", count, limit);
+		return;
+	}
 	t = (struct dx_tail *)(((struct dx_entry *)c) + limit);
 
 	t->dt_checksum = ext4_dx_csum(inode, dirent, count_offset, count, t);
@@ -515,7 +521,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);
 }
 
@@ -533,6 +539,37 @@ ext4_next_entry(struct ext4_dir_entry_2 *p, unsigned long blocksize)
  * Future: use high four bits of block for coalesce-on-delete flags
  * Mask them off for now.
  */
+static struct dx_root_info *dx_get_dx_info(struct inode *dir, void *de_buf)
+{
+	unsigned int blocksize = dir->i_sb->s_blocksize;
+	void *base = de_buf;
+	unsigned int rlen;
+
+	/* '.' and '..' never carry the casefold+fscrypt hash, so pass NULL
+	 * for dir regardless of the directory's flags */
+	rlen = ext4_rec_len_from_disk(((struct ext4_dir_entry_2 *)de_buf)->rec_len,
+				      blocksize);
+	if (rlen < EXT4_BASE_DIR_LEN || rlen > blocksize)
+		return ERR_PTR(-EFSCORRUPTED);
+	de_buf += ext4_dir_entry_len(de_buf, blocksize, NULL);
+
+	/* dx root info is after dotdot entry */
+	if (de_buf < base || (char *)de_buf - (char *)base +
+	    EXT4_BASE_DIR_LEN > blocksize)
+		return ERR_PTR(-EFSCORRUPTED);
+	rlen = ext4_rec_len_from_disk(((struct ext4_dir_entry_2 *)de_buf)->rec_len,
+				      blocksize);
+	if (rlen < EXT4_BASE_DIR_LEN ||
+	    (char *)de_buf - (char *)base + rlen > blocksize)
+		return ERR_PTR(-EFSCORRUPTED);
+	de_buf += ext4_dir_entry_len(de_buf, blocksize, NULL);
+
+	if (de_buf < base || (char *)de_buf - (char *)base +
+			      sizeof(struct dx_root_info) > blocksize)
+		return ERR_PTR(-EFSCORRUPTED);
+
+	return (struct dx_root_info *)de_buf;
+}
 
 static inline ext4_lblk_t dx_get_block(struct dx_entry *entry)
 {
@@ -574,11 +611,20 @@ static inline void dx_set_limit(struct dx_entry *entries, unsigned value)
 	((struct dx_countlimit *) entries)->limit = cpu_to_le16(value);
 }
 
-static inline unsigned dx_root_limit(struct inode *dir, unsigned infosize)
+static inline unsigned dx_root_limit(struct inode *dir,
+	struct ext4_dir_entry_2 *dot_de)
 {
-	unsigned int entry_space = dir->i_sb->s_blocksize -
-			ext4_dir_rec_len(1, NULL) -
-			ext4_dir_rec_len(2, NULL) - infosize;
+	struct dx_root_info *info;
+	unsigned int entry_space;
+
+	info = dx_get_dx_info(dir, dot_de);
+	if (IS_ERR(info))
+		return 0;
+	if ((char *)info - (char *)dot_de + info->info_length >
+	    dir->i_sb->s_blocksize)
+		return 0;
+	entry_space = dir->i_sb->s_blocksize - ((char *)info - (char *)dot_de) -
+		info->info_length;
 
 	if (ext4_has_feature_metadata_csum(dir->i_sb))
 		entry_space -= sizeof(struct dx_tail);
@@ -587,8 +633,10 @@ static inline unsigned dx_root_limit(struct inode *dir, unsigned infosize)
 
 static inline unsigned dx_node_limit(struct inode *dir)
 {
+	/* dx_node fake_dirent is always 8 bytes — it never carries a casefold
+	 * hash, so pass NULL to suppress the hash-size term. */
 	unsigned int entry_space = dir->i_sb->s_blocksize -
-			ext4_dir_rec_len(0, dir);
+			ext4_dirent_rec_len(0, NULL);
 
 	if (ext4_has_feature_metadata_csum(dir->i_sb))
 		entry_space -= sizeof(struct dx_tail);
@@ -698,7 +746,9 @@ static struct stats dx_show_leaf(struct inode *dir,
 				       (unsigned) ((char *) de - base));
 #endif
 			}
-			space += ext4_dir_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);
@@ -780,7 +830,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;
@@ -792,23 +842,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;
@@ -816,7 +874,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;
@@ -832,13 +890,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"
@@ -851,14 +909,15 @@ 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, (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, root->info.info_length));
+				   dx_root_limit(dir,
+				   (struct ext4_dir_entry_2 *)frame->bh->b_data
+				   ));
 		goto fail;
 	}
 
@@ -935,7 +994,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;
@@ -944,7 +1003,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++) {
@@ -1056,7 +1126,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)) {
@@ -1081,22 +1151,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)))
@@ -1107,8 +1189,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,
@@ -1123,8 +1205,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) {
@@ -1202,6 +1285,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;
@@ -1213,6 +1297,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;
@@ -1250,12 +1335,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);
 }
 
@@ -1273,6 +1358,210 @@ 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)) {
+		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.hash);
+		hinfo->minor_hash = le32_to_cpu(dh->dh_hash.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 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;
+	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 -EIO;
+		}
+
+		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 -EIO;
+			}
+
+			dh->dh_header.ddh_length = sizeof(*dh);
+			dh->dh_hash.hash = cpu_to_le32(hinfo->hash);
+			dh->dh_hash.minor_hash = cpu_to_le32(hinfo->minor_hash);
+			de->file_type |= EXT4_DIRENT_CFHASH;
+		} else {
+			/* 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 -EIO;
+			}
+
+			EXT4_DIRENT_HASHES(de)->hash = cpu_to_le32(hinfo->hash);
+			EXT4_DIRENT_HASHES(de)->minor_hash =
+						cpu_to_le32(hinfo->minor_hash);
+		}
+	}
+	return 0;
+}
+
 /*
  * Create map of hash values, offsets, and sizes, stored at end of block.
  * Returns number of entries mapped.
@@ -1296,13 +1585,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;
@@ -1403,8 +1700,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;
 
@@ -1431,10 +1729,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
@@ -1467,7 +1784,7 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
 		/* this code is executed quadratically often */
 		/* do minimal checking `by hand' */
 		if (de->name + de->name_len <= 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,
@@ -1488,7 +1805,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;
 
@@ -1619,7 +1936,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 "
@@ -1753,7 +2070,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;
 }
 
@@ -1850,7 +2167,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 =
@@ -1883,7 +2200,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);
@@ -2035,10 +2352,15 @@ 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);
+	/* When dirdata extensions are present, a NUL gap byte sits between
+	 * the filename and the first extension; account for it so the found
+	 * slot is always large enough for ext4_dirdata_set() to write into. */
+	unsigned short reclen = ext4_dirent_rec_len(fname_len(fname) + dlen +
+						    (dlen ? 1 : 0), dir);
 	int nlen, rlen;
 	unsigned int offset = 0;
 	char *top;
@@ -2049,9 +2371,9 @@ 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_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;
@@ -2065,16 +2387,13 @@ 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)
+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;
 
-	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 =
@@ -2088,13 +2407,7 @@ 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));
-	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);
-	}
+	return ext4_dirdata_set(de, dir, data, fname);
 }
 
 /*
@@ -2112,14 +2425,28 @@ 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;
 	if (!de) {
+		if (dfid)
+			dlen = dfid->df_header.ddh_length;
+		/* ext4_dirent_rec_len() does not add CFHASH size when dirdata is
+		 * enabled (it assumes the caller included it in name_len).  Add
+		 * it here so ext4_find_dest_de() allocates a slot large enough
+		 * for ext4_dirdata_set() to write the hash extension. */
+		if (ext4_hash_in_dirent(dir) &&
+		    ext4_has_feature_dirdata(dir->i_sb))
+			dlen += sizeof(struct ext4_dirent_hash);
 		err = ext4_find_dest_de(dir, bh, bh->b_data,
-					blocksize - csum_size, fname, &de);
+					blocksize - csum_size, fname, &de, dlen);
 		if (err)
 			return err;
 	}
@@ -2132,7 +2459,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(dir, inode, de, blocksize, fname);
+	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
@@ -2156,44 +2487,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:
@@ -2211,16 +2536,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))
@@ -2237,17 +2561,20 @@ 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;
 	}
 
 	/* 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);
@@ -2278,24 +2605,32 @@ 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 */
+	dx_info = dx_get_dx_info(dir, dot_de);
+	if (IS_ERR(dx_info)) {
+		brelse(bh2);
+		brelse(bh);
+		return PTR_ERR(dx_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 + dx_info->info_length;
 	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, dot_de));
 
 	/* 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;
@@ -2338,7 +2673,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;
 }
@@ -2355,7 +2690,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;
@@ -2380,6 +2716,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)
@@ -2464,7 +2807,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);
 }
 
 /*
@@ -2605,7 +2949,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));
@@ -2613,11 +2957,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);
@@ -2641,7 +2991,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
 	 */
@@ -2927,14 +3277,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_dir_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);
 
@@ -2945,7 +3296,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;
@@ -2954,7 +3305,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 + dot_rec_len),
 					blocksize);
 	}
 
@@ -2966,8 +3317,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;
@@ -2986,6 +3338,12 @@ int ext4_init_new_dir(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);
@@ -3077,8 +3435,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;
 	}
@@ -3467,10 +3825,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
@@ -3638,8 +4002,46 @@ static int ext4_rename_dir_finish(handle_t *handle, struct ext4_renament *ent,
 	return 0;
 }
 
+/*
+ * When LUFID is cleared from an entry that also carries CFHASH, the CFHASH
+ * bytes are stranded: LUFID precedes CFHASH in the layout, so removing LUFID
+ * shifts the expected CFHASH offset.  Slide CFHASH into the vacated slot so
+ * the parser finds it at the right position.  If the headers are malformed,
+ * clear CFHASH instead of risking a corrupt read.
+ */
+static void ext4_setent_compact_exts(struct ext4_renament *ent,
+				     struct ext4_dirent_data_header *lddh,
+				     unsigned int lufid_off,
+				     unsigned int rec_len)
+{
+	struct ext4_dirent_data_header *cddh;
+	unsigned int coff;
+
+	if (!(ent->de->file_type & EXT4_DIRENT_CFHASH))
+		return;
+	if (lufid_off + sizeof(*lddh) > rec_len ||
+	    !lddh->ddh_length ||
+	    lufid_off + lddh->ddh_length > rec_len) {
+		ent->de->file_type &= ~EXT4_DIRENT_CFHASH;
+		return;
+	}
+
+	coff = lufid_off + lddh->ddh_length;
+	cddh = (struct ext4_dirent_data_header *)((char *)ent->de + coff);
+
+	if (coff + sizeof(*cddh) > rec_len ||
+	    !cddh->ddh_length ||
+	    coff + cddh->ddh_length > rec_len) {
+		ent->de->file_type &= ~EXT4_DIRENT_CFHASH;
+		return;
+	}
+
+	memmove(lddh, cddh, cddh->ddh_length);
+}
+
 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;
 
@@ -3649,8 +4051,86 @@ 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)) {
+		/* Copy the source LUFID payload into the destination slot when
+		 * both carry a LUFID of the same on-disk size.  LUFID is
+		 * inode-specific and must travel with the inode through rename.
+		 * CFHASH bytes are left in-place: CFHASH is a function of the
+		 * destination filename, and its bytes sit at the correct offset
+		 * because LUFID always precedes CFHASH in the extension layout.
+		 * When src_fid is NULL, the LUFID flag is synced to the LUFID
+		 * bit already present in file_type: callers that clear LUFID
+		 * (rename of a non-LUFID inode, whiteout) pass file_type with
+		 * LUFID=0; ext4_resetent passes the original file_type so the
+		 * flag is restored for error recovery without overwriting the
+		 * LUFID bytes that are still intact on disk. */
+		if (ext4_has_feature_dirdata(ent->dir->i_sb)) {
+			if (src_fid && (ent->de->file_type & EXT4_DIRENT_LUFID)) {
+				unsigned int rec_len =
+					ext4_rec_len_from_disk(ent->de->rec_len,
+						ent->dir->i_sb->s_blocksize);
+				unsigned int ddh_off =
+					EXT4_BASE_DIR_LEN + ent->de->name_len + 1;
+				struct ext4_dirent_data_header *ddh =
+					(struct ext4_dirent_data_header *)
+					((char *)ent->de + ddh_off);
+				unsigned int copy_len = src_fid->df_header.ddh_length;
+
+				if (ddh_off + sizeof(*ddh) > rec_len ||
+				    ddh->ddh_length != copy_len ||
+				    ddh_off + copy_len > rec_len) {
+					/* Cannot copy in-place (size mismatch).
+					 * If CFHASH is also present, slide it
+					 * into the LUFID slot so the parser
+					 * finds it at the correct offset after
+					 * the LUFID flag is cleared. */
+					ext4_setent_compact_exts(ent, ddh,
+							ddh_off, rec_len);
+					ent->de->file_type &= ~EXT4_DIRENT_LUFID;
+				} else {
+					memcpy(ddh, src_fid, copy_len);
+				}
+			} else if (src_fid) {
+				/* Destination has no LUFID slot; cannot
+				 * propagate in-place.  Clear the flag from
+				 * the on-disk entry directly — clearing only
+				 * the local file_type variable has no effect
+				 * because the final assignment takes the high
+				 * bits from ent->de->file_type, not file_type. */
+				ent->de->file_type &= ~EXT4_DIRENT_LUFID;
+			} else if (!src_fid) {
+				/* Sync the LUFID flag with what file_type requests.
+				 * For normal rename (source has no LUFID) and for
+				 * the whiteout setent, file_type carries LUFID=0,
+				 * so we clear the stale flag.  For ext4_resetent
+				 * (error recovery), file_type is the original
+				 * file_type with LUFID=1, so we restore the flag —
+				 * the LUFID bytes are still on disk untouched.
+				 * When clearing LUFID and CFHASH is present, slide
+				 * CFHASH bytes into the LUFID slot first so the
+				 * parser finds CFHASH at the expected offset. */
+				if (!(file_type & EXT4_DIRENT_LUFID) &&
+				    (ent->de->file_type & EXT4_DIRENT_LUFID)) {
+					unsigned int loff =
+						EXT4_BASE_DIR_LEN +
+						ent->de->name_len + 1;
+					unsigned int rlen =
+						ext4_rec_len_from_disk(
+							ent->de->rec_len,
+							ent->dir->i_sb->s_blocksize);
+					ext4_setent_compact_exts(ent,
+						(struct ext4_dirent_data_header *)
+						((char *)ent->de + loff),
+						loff, rlen);
+				}
+				ent->de->file_type =
+					(ent->de->file_type & ~EXT4_DIRENT_LUFID) |
+					(file_type & EXT4_DIRENT_LUFID);
+			}
+		}
+		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);
@@ -3687,7 +4167,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, NULL);
 	brelse(old.bh);
 }
 
@@ -3786,6 +4266,67 @@ 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 = 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;
+	}
+	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.
@@ -3814,6 +4355,7 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
 	struct inode *whiteout = 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,
@@ -3919,27 +4461,59 @@ 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)
+			ext4_fc_mark_ineligible(old.dir->i_sb,
+						EXT4_FC_REASON_DIRDATA, handle);
+		else
+			old_file_type &= ~EXT4_DIRENT_LUFID;
+	}
+
 	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) {
+		struct ext4_dentry_param *edp = NULL;
+		if (old_snap.fid) {
+			/* Wrap the snapshotted LUFID in an ext4_dentry_param so
+			 * ext4_add_entry() picks it up via d_fsdata. */
+			unsigned fid_size = old_snap.fid->df_header.ddh_length;
+
+			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);
+		}
+		new.dentry->d_fsdata = edp;
 		retval = ext4_add_entry(handle, new.dentry, old.inode);
+		new.dentry->d_fsdata = NULL;
+		kfree(edp);
 		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;
 	}
@@ -4026,6 +4600,7 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
 	retval = 0;
 
 end_rename:
+	ext4_lufid_snap_release(&old_snap);
 	if (whiteout) {
 		if (retval) {
 			ext4_resetent(handle, &old,
@@ -4062,8 +4637,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,
@@ -4142,12 +4718,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;
 
@@ -4180,6 +4777,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);
@@ -4216,6 +4815,202 @@ 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 fname = {};
+	struct ext4_dir_entry_2 *de = NULL;
+	struct buffer_head *bh = NULL;
+	struct inode *inode = NULL;
+	handle_t *handle = NULL;
+	struct qstr d_name;
+	__u32 ino = 0;
+	bool child_locked = false;
+	int err = 0;
+
+	if (!ext4_has_feature_dirdata(sb))
+		return -EOPNOTSUPP;
+
+	if (namelen > EXT4_NAME_LEN)
+		return -ENAMETOOLONG;
+	if (namelen != strnlen(filename, namelen + 1))
+		return -EINVAL;
+
+	d_name.name = filename;
+	d_name.len = namelen;
+
+	err = ext4_fname_setup_filename(dir, &d_name, 0, &fname);
+	if (err)
+		goto out_free;
+
+	/* Lock dir with the VFS parent-mutation subclass.  The lookup and any
+	 * mutation must be inside this lock to prevent TOCTOU races. */
+	inode_lock_nested(dir, I_MUTEX_PARENT);
+
+	/* Phase 1: look up the entry without holding a journal handle to
+	 * obtain the child inode reference before taking the child lock.
+	 * VFS locking order requires all inode locks to be acquired before
+	 * starting a jbd2 transaction; starting the journal while already
+	 * holding the child lock would invert that order and risk deadlock
+	 * if jbd2 blocks waiting for a commit that the child lock holder
+	 * prevents from finishing. */
+	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 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;
+	}
+
+	/* Lock the target inode BEFORE starting the journal (dir-then-child,
+	 * VFS-locks-before-journal ordering).  Use I_MUTEX_CHILD for directory
+	 * inodes (consistent with VFS rename/rmdir) and I_MUTEX_NONDIR2 for
+	 * all others; mixing subclasses on the same inode triggers lockdep
+	 * cycles with concurrent rename. */
+	if (inode != dir) {
+		if (S_ISDIR(inode->i_mode))
+			inode_lock_nested(inode, I_MUTEX_CHILD);
+		else
+			inode_lock_nested(inode, I_MUTEX_NONDIR2);
+		child_locked = true;
+	}
+
+	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_unlock;
+	}
+
+	/* Phase 2: re-look up the entry inside the journal to obtain a stable
+	 * de pointer.  Verify it still refers to the same inode (TOCTOU
+	 * guard: a concurrent unlink+create between phase 1 and here would
+	 * change the inode number). */
+	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;
+	}
+
+	err = ext4_delete_entry(handle, dir, de, bh);
+	if (err)
+		goto out_brelse;
+
+	brelse(bh);
+	bh = NULL;
+
+	/* Re-add with LUFID via dentry->d_fsdata.  ext4_add_entry() resolves
+	 * dfid from d_fsdata into fname.dfid and passes it through to
+	 * add_dirent_to_buf() without touching the shared i_dirdata field,
+	 * eliminating the race with concurrent link() calls. */
+	{
+		struct dentry parent_dentry = { .d_inode = dir };
+		struct dentry new_dentry = {
+			.d_name = d_name,
+			.d_parent = &parent_dentry,
+			.d_inode = inode,
+			.d_fsdata = edp,
+		};
+		err = ext4_add_entry(handle, &new_dentry, inode);
+	}
+
+	if (err) {
+		/* Delete succeeded but re-add failed; try to restore so the
+		 * inode is not left without a directory entry. */
+		struct dentry parent_dentry = { .d_inode = dir };
+		struct dentry orig_dentry = {
+			.d_name = d_name,
+			.d_parent = &parent_dentry,
+			.d_inode = inode,
+		};
+		int rollback_err = ext4_add_entry(handle, &orig_dentry, inode);
+
+		if (rollback_err)
+			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, rollback_err,
+				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:
+	if (handle)
+		ext4_journal_stop(handle);
+out_unlock:
+	if (child_locked)
+		inode_unlock(inode);
+out_iput:
+	iput(inode);
+out_unlock_dir:
+	inode_unlock(dir);
+out_free:
+	ext4_fname_free_filename(&fname);
+
+	return err;
+}
+
 /*
  * directories can handle most operations...
  */
diff --git a/fs/ext4/sysfs.c b/fs/ext4/sysfs.c
index 923b375e017fa..80074fb15ee9b 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
diff --git a/include/uapi/linux/ext4.h b/include/uapi/linux/ext4.h
index 9c683991c32f9..9134fe64947a9 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 */
+	char  esl_data[255]; /* LUFID data (raw bytes) */
+};
+
 /*
  * Flags used by EXT4_IOC_SHUTDOWN
  */

3/2 2026/08/01 10:15 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 39116
							output: 152
							thoughts: 1515
						

Thoughts:
**Fuzzing ext4's New 'Dirdata' Feature**

Okay, so I've got this patch for ext4 that's adding support for "dirdata". Specifically, it introduces the `EXT4_IOC_SET_LUFID` ioctl and tweaks the ext4 directory entry format to handle things like LUFID, INO64, and CFHASH. This is a functional change to the filesystem, so it's a prime target for fuzzing. It's touching a bunch of core files: `dir.c`, `ext4.h`, `fast_commit.c`, `inline.c`, `ioctl.c`, `namei.c`, `sysfs.c`, and `ext4.h`.

The `EXT4_IOC_SET_LUFID` ioctl immediately jumps out as a fuzzing candidate. I need to look into its handler: `ext4_ioctl_set_lufid` in `ioctl.c`. It takes a `struct ext4_set_lufid` from userspace, which is classic fuzzing material.

```c
struct ext4_set_lufid {
	__u8 esl_name_len;
	char  esl_name[255 + 1];
	__u8 esl_data_len;
	char  esl_data[255];
};
```

I'll definitely need to focus on fuzzing that. Beyond the ioctl, directory entry handling is now modified. That's reachable through standard filesystem operations like `mkdir`, `link`, and `rename`.

Let's quickly check for any necessary CONFIGs. Since it's ext4, `EXT4_FS` is a given. No new configs are introduced by this patch.

I initially looked at a bunch of symbols: `ext4_ioctl_set_lufid`, `ext4_dirdata_set_lufid`, `ext4_dirdata_get`, `ext4_dirdata_set`, `ext4_lufid_snapshot`, and `ext4_setent_compact_exts`. Some are static, which isn't ideal for direct fuzzing, but they might be called in the core fuzzed functions.

After thinking about the best approach, I need the main entry points. I should focus on the global, non-inlined functions. `ext4_dirdata_set_lufid`, `ext4_insert_dentry_data`, `ext4_dirdata_get`, and `ext4_init_new_dir_data` are the key functions. These are the entry points to the new features.

To recap, here's what I'm going to focus on:
*   `ext4_dirdata_set_lufid`
*   `ext4_insert_dentry_data`
*   `ext4_dirdata_get`
*   `ext4_init_new_dir_data`

And I will definitely need `EXT4_FS` enabled in the config.

The core reasoning here is that this patch fundamentally changes how ext4 handles directory entries, introducing a new ioctl and potentially complex logic. It's a gold mine for finding new bugs in directory entry operations, so it demands rigorous fuzzing.



4/2 2026/08/01 10:16 tool set-results
0m Args:
{
  "EnableConfigs": [
    "EXT4_FS"
  ],
  "FocusSymbols": [
    "ext4_dirdata_set_lufid",
    "ext4_insert_dentry_data",
    "ext4_dirdata_get",
    "ext4_init_new_dir_data"
  ],
  "Reasoning": "The patch introduces a new ioctl EXT4_IOC_SET_LUFID and modifies the ext4 directory entry format to support 'dirdata' extensions (LUFID, INO64, CFHASH). This adds new reachable core filesystem logic for directory entry manipulation, lookup, and modification, which should be fuzzed.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "EXT4_FS"
  ],
  "FocusSymbols": [
    "ext4_dirdata_set_lufid",
    "ext4_insert_dentry_data",
    "ext4_dirdata_get",
    "ext4_init_new_dir_data"
  ],
  "Reasoning": "The patch introduces a new ioctl EXT4_IOC_SET_LUFID and modifies the ext4 directory entry format to support 'dirdata' extensions (LUFID, INO64, CFHASH). This adds new reachable core filesystem logic for directory entry manipulation, lookup, and modification, which should be fuzzed.",
  "WorthFuzzing": true
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)