| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/10/30 02:40 | flow | patch-triage |
5mArgs:null Results: {
"EnableConfigs": [
"EXT4_FS",
"FS_ENCRYPTION",
"UNICODE"
],
"FocusSymbols": [
"ext4_ioctl_set_lufid",
"ext4_dirdata_set_lufid",
"ext4_dirdata_get",
"ext4_dirdata_set",
"ext4_insert_dentry_data",
"ext4_setent_compact_exts",
"ext4_lufid_snapshot",
"dx_get_dx_info",
"ext4_htree_store_dirent",
"ext4_inlinedir_to_tree"
],
"KMSANReasoning": "The patch series implements support for the EXT4_FEATURE_INCOMPAT_DIRDATA feature (primarily Lustre Unique File Identifiers - LUFID - and casefolded hashes CFHASH), adding the EXT4_IOC_SET_LUFID ioctl and directory entry manipulation logic.\n\nKMSAN vs KASAN Applicability Analysis:\n1. Kernel-to-User Leaks / Outputs:\n - The newly introduced ioctl EXT4_IOC_SET_LUFID is defined with _IOW, meaning it only reads data from userspace via copy_from_user. It does not return any struct or memory back to userspace via copy_to_user or put_user.\n - In readdir/filldir (ext4_readdir, call_filldir, ext4_read_inline_dir), dirent names are emitted strictly up to their validated name length (de-\u003ename_len / ent_name-\u003elen) and file types are masked with EXT4_FT_MASK. No uninitialized padding or dirdata bytes are leaked to userspace.\n\n2. Structure Initialization and Memory Allocations:\n - All stack variables introduced in the patch (e.g. struct dx_hash_info hi = {}, dirent_hinfo = {}, struct ext4_lufid_snap old_snap = {}, struct ext4_filename fname = {}, struct qstr real_name = {}) are explicitly zero-initialized.\n - Dynamic allocations either use kzalloc_flex (guaranteeing zero-initialization before data copy, such as struct fname in ext4_htree_store_dirent), kmemdup, or kmalloc where all fields and flexible payload bytes are immediately fully populated (such as edp and old_edp in ext4_rename and ext4_dirdata_set_lufid).\n\n3. Logic, Offsets, and Bounds:\n - The modifications primarily adjust directory entry offset calculations, record length bounds checks, and in-place extension compaction (ext4_dirent_get_data_len, ext4_dir_entry_len, ext4_setent_compact_exts).\n - Any errors or edge cases in pointer arithmetic, offset calculation, or buffer bounds risk out-of-bounds (OOB) memory reads/writes or corrupted metadata, which are detected by KASAN and FORTIFY_SOURCE. Locking patterns in ext4_dirdata_set_lufid are monitored by LOCKDEP.\n\nBecause the changes do not introduce uninitialized memory reads or information leak risks that would be caught uniquely by KMSAN, a dedicated KMSAN fuzzing session is not warranted.",
"NeedsKMSAN": false,
"Reasoning": "The patch adds support for EXT4_FEATURE_INCOMPAT_DIRDATA and introduces the EXT4_IOC_SET_LUFID ioctl to attach LUFID data to directory entries. It modifies directory traversal, htree indexing, inline directories, rename/whiteout handling, and directory entry validation to accommodate variable-length dirdata extensions. Because this affects core, fully reachable filesystem parsing and directory mutation logic that handles both user input and on-disk structures, it warrants fuzzing.",
"WorthFuzzing": true
} |
| 1/1 | 2026/10/30 02:40 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit 5398d1939b0ee1e5c282614b7866cad1b15130d2\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Fri Oct 30 02:40:18 2026 +0000\n\n syz-cluster: applied patch under review\n\ndiff --git a/fs/ext4/dir.c b/fs/ext4/dir.c\nindex 8d7b81e6948e72..dca6796d5fe271 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@@ -238,7 +237,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)\n \t\tif (!inode_eq_iversion(inode, info-\u003ecookie)) {\n \t\t\tfor (i = 0;\n \t\t\t i \u003c= sb-\u003es_blocksize -\n-\t\t\t\t ext4_dir_rec_len(1, has_csum ? NULL : inode) \u0026\u0026\n+\t\t\t\t ext4_dirent_rec_len(1, has_csum ? NULL : inode) \u0026\u0026\n \t\t\t i \u003c offset;) {\n \t\t\t\tde = (struct ext4_dir_entry_2 *)\n \t\t\t\t\t(bh-\u003eb_data + i);\n@@ -249,7 +248,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@@ -263,7 +262,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)\n \n \t\tif (unlikely(offset \u003c sb-\u003es_blocksize \u0026\u0026\n \t\t\t offset \u003e sb-\u003es_blocksize -\n-\t\t\t ext4_dir_rec_len(1, has_csum ? NULL : inode))) {\n+\t\t\t ext4_dirent_rec_len(1, has_csum ? NULL : inode))) {\n \t\t\tEXT4_ERROR_FILE(file, bh-\u003eb_blocknr,\n \t\t\t\t\t\"bad entry in directory: %s - offset=%u, size=%lu\",\n \t\t\t\t\t\"directory entry too close to block end\",\n@@ -303,8 +302,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@@ -484,27 +494,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 8d3af062a83068..87c55276d67ab4 100644\n--- a/fs/ext4/ext4.h\n+++ b/fs/ext4/ext4.h\n@@ -2369,6 +2369,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@@ -2507,10 +2508,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@@ -2591,6 +2590,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@@ -2608,13 +2662,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@@ -2702,6 +2769,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@@ -3013,18 +3082,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@@ -3038,12 +3116,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@@ -3279,10 +3359,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@@ -3894,6 +3983,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@@ -4103,6 +4196,122 @@ 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+ * Advance to the next dirdata record header starting from @ddh.\n+ *\n+ * Uses READ_ONCE() on ddh_length to prevent the compiler from re-fetching this\n+ * on-disk field and to guard against a concurrent write racing with any bounds\n+ * check the caller performs on the returned pointer.\n+ */\n+static inline struct ext4_dirent_data_header *\n+ext4_dirdata_next(struct ext4_dirent_data_header *ddh)\n+{\n+\treturn (struct ext4_dirent_data_header *)\n+\t\t((char *)ddh + READ_ONCE(ddh-\u003eddh_length));\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+\n extern const struct iomap_ops ext4_iomap_ops;\n extern const struct iomap_ops ext4_iomap_report_ops;\n extern const struct iomap_ops ext4_iomap_buffered_write_ops;\ndiff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c\nindex 0cac890cf37029..876a863d6c0198 100644\n--- a/fs/ext4/fast_commit.c\n+++ b/fs/ext4/fast_commit.c\n@@ -2686,6 +2686,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 2f77a37fb10180..899faeb4e6e064 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 ceee69a66482bc..76b1490490f6f5 100644\n--- a/fs/ext4/inline.c\n+++ b/fs/ext4/inline.c\n@@ -982,11 +982,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@@ -995,7 +1004,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@@ -1064,7 +1075,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@@ -1273,6 +1284,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@@ -1318,7 +1330,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@@ -1328,30 +1340,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@@ -1363,7 +1391,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@@ -1436,8 +1465,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/* for other entry, the real offset in\n \t\t\t * the buf has to be tuned accordingly.\n \t\t\t */\n-\t\t\tif (i + ext4_dir_rec_len(1, NULL) \u003e extra_size)\n+\t\t\tif (i + ext4_dirent_rec_len(1, NULL) \u003e extra_size)\n \t\t\t\tbreak;\n \t\t\tde = (struct ext4_dir_entry_2 *)\n \t\t\t\t(dir_buf + i - extra_offset);\n@@ -1474,7 +1503,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);\n@@ -1504,7 +1533,7 @@ int ext4_read_inline_dir(struct file *file,\n \t\t * kmalloc(inline_size) buffer. Make sure its header fits before\n \t\t * ext4_check_dir_entry() dereferences de-\u003erec_len.\n \t\t */\n-\t\tif (ctx-\u003epos + ext4_dir_rec_len(1, NULL) \u003e extra_size)\n+\t\tif (ctx-\u003epos + ext4_dirent_rec_len(1, NULL) \u003e extra_size)\n \t\t\tgoto out;\n \t\tde = (struct ext4_dir_entry_2 *)\n \t\t\t(dir_buf + ctx-\u003epos - extra_offset);\ndiff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c\nindex 1f6d7afb62ec79..baf6ef48cbf308 100644\n--- a/fs/ext4/ioctl.c\n+++ b/fs/ext4/ioctl.c\n@@ -1536,6 +1536,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@@ -1922,6 +2004,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@@ -2001,6 +2085,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 231a274b814ecb..4ab1f14442b090 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,38 @@ 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) +\n+\t\t\t sizeof(struct dx_entry) \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 +612,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 +634,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 +747,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 +831,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 +843,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 +875,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 +891,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 +910,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 +995,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 +1004,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 +1127,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 +1152,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 +1190,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 +1206,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 +1286,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 +1298,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 +1336,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 +1359,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 +1586,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 +1701,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 +1730,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@@ -1465,11 +1783,18 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,\n \tdlimit = search_buf + buf_size;\n \twhile ((char *) de \u003c dlimit - EXT4_BASE_DIR_LEN) {\n \t\t/* this code is executed quadratically often */\n-\t\t/* do minimal checking `by hand' */\n+\t\t/* Decode rec_len before ext4_match() so that ext4_dirdata_get()\n+\t\t * inside ext4_match() never uses an unvalidated rec_len as a\n+\t\t * read bound. A crafted entry with rec_len \u003e buf_size would\n+\t\t * let ext4_dirdata_get() read past the block buffer. */\n+\t\tde_len = ext4_rec_len_from_disk(de-\u003erec_len,\n+\t\t\t\t\t\tdir-\u003ei_sb-\u003es_blocksize);\n+\t\tif (de_len \u003c= 0 || (char *)de + de_len \u003e dlimit)\n+\t\t\treturn -EFSCORRUPTED;\n \t\tif (de-\u003ename + de-\u003ename_len \u003c= dlimit \u0026\u0026\n \t\t (!ext4_hash_in_dirent(dir) ||\n-\t\t (char *)de + ext4_dir_rec_len(de-\u003ename_len, dir) \u003c= dlimit) \u0026\u0026\n-\t\t ext4_match(dir, fname, de)) {\n+\t\t (char *)de + ext4_dirent_rec_len(de-\u003ename_len, dir) \u003c= dlimit) \u0026\u0026\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@@ -1478,11 +1803,6 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,\n \t\t\t*res_dir = de;\n \t\t\treturn 1;\n \t\t}\n-\t\t/* prevent looping on a bad block */\n-\t\tde_len = ext4_rec_len_from_disk(de-\u003erec_len,\n-\t\t\t\t\t\tdir-\u003ei_sb-\u003es_blocksize);\n-\t\tif (de_len \u003c= 0)\n-\t\t\treturn -EFSCORRUPTED;\n \t\toffset += de_len;\n \t\tde = (struct ext4_dir_entry_2 *) ((char *) de + de_len);\n \t}\n@@ -1490,7 +1810,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@@ -1621,7 +1941,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@@ -1755,7 +2075,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@@ -1852,7 +2172,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@@ -1885,7 +2205,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@@ -2037,10 +2357,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@@ -2051,9 +2376,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@@ -2067,16 +2392,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@@ -2090,13 +2412,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@@ -2114,14 +2430,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@@ -2134,7 +2464,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@@ -2158,44 +2492,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@@ -2213,16 +2541,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@@ -2239,17 +2566,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@@ -2280,24 +2610,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@@ -2340,7 +2678,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@@ -2357,7 +2695,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@@ -2382,6 +2721,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@@ -2466,7 +2812,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@@ -2607,7 +2954,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@@ -2615,11 +2962,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@@ -2643,7 +2996,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@@ -2929,14 +3282,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@@ -2947,7 +3301,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@@ -2956,7 +3310,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@@ -2968,8 +3322,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@@ -2988,6 +3343,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@@ -3079,8 +3440,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@@ -3473,7 +3834,8 @@ int __ext4_link(struct inode *dir, struct inode *inode,\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@@ -3641,8 +4003,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@@ -3652,8 +4052,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@@ -3690,7 +4168,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@@ -3789,6 +4267,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@@ -3815,8 +4354,10 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,\n \tint force_reread;\n \tint retval;\n \tstruct inode *whiteout = NULL;\n+\tstruct ext4_dentry_param *edp = 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@@ -3922,29 +4463,64 @@ 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\told_file_type \u0026= ~EXT4_DIRENT_LUFID;\n+\t}\n+\n+\t/* Allocate the LUFID wrapper for ext4_add_entry() before any\n+\t * destructive changes to the old entry. An OOM failure after\n+\t * ext4_setent() has already compacted the extensions would trigger\n+\t * the lossy resetent path; pre-allocating here avoids that. */\n+\tif (!new.bh \u0026\u0026 old_snap.fid) {\n+\t\tunsigned fid_size = old_snap.fid-\u003edf_header.ddh_length;\n+\n+\t\tedp = kmalloc(sizeof(*edp) + fid_size, GFP_NOFS);\n+\t\tif (!edp) {\n+\t\t\tretval = -ENOMEM;\n+\t\t\tgoto end_rename;\n+\t\t}\n+\t\tedp-\u003eedp_magic = EXT4_LUFID_MAGIC;\n+\t\tmemcpy(edp-\u003eedp_dfid, old_snap.fid, fid_size);\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\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\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\t/* ext4_setent() writes the LUFID in-place without going through\n+\t\t * __ext4_add_entry(), so mark ineligible here if needed. */\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}\n \tif (force_reread)\n \t\tforce_reread = !ext4_test_inode_flag(new.dir,\n@@ -4029,8 +4605,25 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,\n \tretval = 0;\n \n end_rename:\n+\tkfree(edp);\n+\text4_lufid_snap_release(\u0026old_snap);\n \tif (whiteout) {\n \t\tif (retval) {\n+\t\t\t/* Known limitation: if the old entry carried both LUFID\n+\t\t\t * and CFHASH, ext4_setent_compact_exts() slid CFHASH\n+\t\t\t * into the LUFID slot above, and old_snap was released\n+\t\t\t * before this point, so resetent can only restore the\n+\t\t\t * LUFID flag — not the original byte layout. This\n+\t\t\t * leaves the entry with CFHASH data at the LUFID offset\n+\t\t\t * and garbage at the CFHASH offset until the next\n+\t\t\t * SET_LUFID ioctl re-writes it. The scenario requires\n+\t\t\t * RENAME_WHITEOUT on a casefolded+encrypted dirdata\n+\t\t\t * directory whose entry has a LUFID, followed by a\n+\t\t\t * journal IO error in the single ext4_mark_inode_dirty()\n+\t\t\t * call that follows — an extremely unlikely combination\n+\t\t\t * in practice. (OOM during the LUFID wrapper allocation\n+\t\t\t * cannot trigger this path because that allocation is\n+\t\t\t * hoisted before ext4_setent() modifies the entry.) */\n \t\t\text4_resetent(handle, \u0026old,\n \t\t\t\t old.inode-\u003ei_ino, old_file_type);\n \t\t\tdrop_nlink(whiteout);\n@@ -4065,8 +4658,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@@ -4145,12 +4739,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@@ -4183,6 +4798,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@@ -4219,6 +4836,266 @@ 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 ext4_lufid_snap old_snap = {};\n+\tstruct qstr d_name;\n+\t/* on-disk name snapshot for non-encrypted casefolded directories: the\n+\t * on-disk case may differ from what the caller supplied. */\n+\tchar ondisk_name[EXT4_NAME_LEN];\n+\tstruct qstr real_name = {};\n+\t/* name to pass to ext4_add_entry() for the re-add and rollback */\n+\tconst struct qstr *add_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 calls ext4_delete_entry() on the parent directory, which\n+\t * bypasses the VFS may_delete() path. inode_permission(MAY_WRITE)\n+\t * above blocks IS_IMMUTABLE(dir) but does NOT block IS_APPEND(dir):\n+\t * append-only directories allow new entries but forbid deletion, and\n+\t * that restriction lives only in may_delete(), not in\n+\t * inode_permission(). Enforce it explicitly here. */\n+\tif (IS_APPEND(dir)) {\n+\t\terr = -EPERM;\n+\t\tgoto out_iput;\n+\t}\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+\t/* Choose the name to use for re-adding the entry after deletion.\n+\t *\n+\t * For non-encrypted casefolded directories ext4_find_entry() matches\n+\t * case-insensitively, so de-\u003ename may differ in case from the\n+\t * caller-supplied filename. Re-adding with the caller's case would\n+\t * silently rename the entry, so preserve the on-disk bytes.\n+\t *\n+\t * For encrypted directories (with or without casefold) de-\u003ename holds\n+\t * the raw ciphertext. ext4_fname_setup_filename() deterministically\n+\t * re-encrypts d_name (the plaintext) to the identical ciphertext, so\n+\t * using d_name for the re-add is correct. Passing the raw ciphertext\n+\t * bytes would cause double-encryption and make the entry unreachable. */\n+\tif (IS_CASEFOLDED(dir) \u0026\u0026 !IS_ENCRYPTED(dir)) {\n+\t\tmemcpy(ondisk_name, de-\u003ename, de-\u003ename_len);\n+\t\treal_name.name = ondisk_name;\n+\t\treal_name.len = de-\u003ename_len;\n+\t\tadd_name = \u0026real_name;\n+\t} else {\n+\t\tadd_name = \u0026d_name;\n+\t}\n+\n+\t/* Snapshot the old LUFID before deleting so we can restore it if the\n+\t * re-add fails. Without this the rollback path re-adds the entry\n+\t * without any LUFID, silently orphaning the old FID-to-path mapping. */\n+\terr = ext4_lufid_snapshot(de, sb-\u003es_blocksize, \u0026old_snap);\n+\tif (err)\n+\t\tgoto out_brelse;\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 = *add_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. Restore the\n+\t\t * original LUFID too: without it the entry is re-added clean\n+\t\t * and the old FID-to-path mapping is silently lost. */\n+\t\tstruct dentry parent_dentry = { .d_inode = dir };\n+\t\tstruct dentry orig_dentry = {\n+\t\t\t.d_name = *add_name,\n+\t\t\t.d_parent = \u0026parent_dentry,\n+\t\t\t.d_inode = inode,\n+\t\t};\n+\t\tint rollback_err;\n+\n+\t\tif (old_snap.fid) {\n+\t\t\tunsigned int fid_size = old_snap.fid-\u003edf_header.ddh_length;\n+\t\t\tstruct ext4_dentry_param *old_edp =\n+\t\t\t\tkmalloc(sizeof(*old_edp) + fid_size, GFP_NOFS);\n+\n+\t\t\tif (old_edp) {\n+\t\t\t\told_edp-\u003eedp_magic = EXT4_LUFID_MAGIC;\n+\t\t\t\tmemcpy(old_edp-\u003eedp_dfid, old_snap.fid, fid_size);\n+\t\t\t\torig_dentry.d_fsdata = old_edp;\n+\t\t\t}\n+\t\t}\n+\n+\t\trollback_err = ext4_add_entry(handle, \u0026orig_dentry, inode);\n+\t\tkfree(orig_dentry.d_fsdata);\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\t(unsigned long long)inode-\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+\text4_lufid_snap_release(\u0026old_snap);\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 923b375e017faa..80074fb15ee9b7 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 9c683991c32f96..9134fe64947a93 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/10/30 02:40 | agent | patch-evaluator |
0mModel:core Args: null Results: {
"EnableConfigs": [
"EXT4_FS",
"FS_ENCRYPTION",
"UNICODE"
],
"FocusSymbols": [
"ext4_ioctl_set_lufid",
"ext4_dirdata_set_lufid",
"ext4_dirdata_get",
"ext4_dirdata_set",
"ext4_insert_dentry_data",
"ext4_setent_compact_exts",
"ext4_lufid_snapshot",
"dx_get_dx_info",
"ext4_htree_store_dirent",
"ext4_inlinedir_to_tree"
],
"Reasoning": "The patch adds support for EXT4_FEATURE_INCOMPAT_DIRDATA and introduces the EXT4_IOC_SET_LUFID ioctl to attach LUFID data to directory entries. It modifies directory traversal, htree indexing, inline directories, rename/whiteout handling, and directory entry validation to accommodate variable-length dirdata extensions. Because this affects core, fully reachable filesystem parsing and directory mutation logic that handles both user input and on-disk structures, it warrants fuzzing.",
"WorthFuzzing": true
}Instruction: You are an expert Linux kernel maintainer and security engineer.
Your job is to review a provided patch series and evaluate whether it warrants fuzzing with syzkaller.
IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on internal assumptions. You must actively use your code access
tools to inspect the actual source code, callers, and surrounding context.
================================================================================
1. CORE TRIAGE PHILOSOPHY
================================================================================
The goal of patch fuzzing is to discover crashes, regressions, exposed latent bugs,
and newly triggered assertions introduced by the patch series.
- REACHABILITY IS THE PRIMARY GATE:
Fuzzing can only discover bugs in code that can actually execute in standard virtualized
environments (GCE or QEMU, utilizing software-emulated devices like USB gadgets, netdev, tun/tap).
If the modified code is structurally unreachable (see Section 2), it MUST NOT be fuzzed,
regardless of whether it adds assertions or complex logic.
- DO NOT BLINDLY TRUST "NO FUNCTIONAL CHANGE" (NFCI) OR "REFACTORING" CLAIMS:
Patch authors routinely label changes as "cleanups", "refactorings", or state
"No functional change intended". Do NOT take these claims at face value.
Code refactorings that rearrange logic, introduce helper functions, or alter state management
in core subsystems frequently introduce subtle semantic shifts or uncover latent kernel bugs.
If reachable executable code is modified or refactored, it MUST be fuzzed.
- NEW OR MODIFIED ASSERTIONS IN REACHABLE CODE MUST BE FUZZED:
When a patch introduces or modifies runtime checks or assertions (e.g., WARN_ON*, VM_WARN_ON*,
BUG_ON*, lockdep_assert*) in reachable code paths, it enforces new or stricter invariants.
Even if the author believes the invariant always holds, fuzzing is essential to verify whether
an unusual sequence of operations can violate it.
================================================================================
2. WHEN TO RETURN WorthFuzzing=false (NEGATIVE CRITERIA)
================================================================================
Return WorthFuzzing=false ONLY IF all modified code falls strictly into one or more of these categories:
- Non-kernel and non-executable changes:
* Modifications to Documentation/, comments, or spelling fixes.
* User-space directories, self-tests, samples, or scripts (e.g., tools/, samples/, scripts/, usr/)
that do not affect the compiled kernel image (vmlinux) or kernel modules.
* Purely decorative logging (e.g., message strings in pr_err, printk, dev_info) or tracepoints
that do not alter control flow or data structures.
* Build system or Kconfig changes that do not alter compiled C logic.
- Structurally unreachable hardware:
* Vendor-specific PCIe switches, SmartNICs, or GPU drivers (e.g., mlxsw, pds_core, qed,
ionic, amdgpu) requiring physical ASIC/PCIe cards not emulated in standard QEMU.
- Unreachable execution paths:
* Driver teardown callbacks (.remove, .shutdown, pci_unregister_driver) executed only during
physical PCI hot-unplug or manual sysfs driver unbinding.
* Code paths exclusive to architectures other than the target architecture.
================================================================================
3. WHEN TO RETURN WorthFuzzing=true (POSITIVE CRITERIA)
================================================================================
Return WorthFuzzing=true whenever the patch touches reachable executable code, including:
- Core Subsystems:
* Any logic modifications in memory management (mm/), synchronization/locking (kernel/locking/),
BPF, scheduler, core networking, VFS, or syscall handling.
- Refactorings and Code Cleanups:
* Any restructuring of reachable data structures, helper abstractions, or algorithm flows.
- Runtime Assertions and Defensive Checks:
* Any introduction or alteration of assertions (WARN_ON*, VM_WARN_ON*, BUG_ON*, etc.) in reachable paths.
- Reachable Drivers and Protocols:
* Drivers accessible via virtual buses (virtio, USB gadget, loopback, netlink, binder, sockets, etc.).
================================================================================
4. EXTRACTING FocusSymbols (PREVENTING DILUTION)
================================================================================
When WorthFuzzing=true, you must extract specific kernel functions into FocusSymbols to guide the fuzzer:
- AVOID UBIQUITOUS LIFECYCLE HOT-PATHS:
Do NOT list generic, ubiquitous functions called by almost every program in the corpus
(including, but not limited to: general memory allocators and deallocators, page fault
and trap handlers, or core synchronization primitives; this is not an exhaustive list).
Listing ubiquitous functions causes the fuzzer to classify thousands of unrelated tests as "focused",
which severely dilutes fuzzing effort away from the actual changes.
- TARGET SPECIFIC FEATURE LOGIC AND ENTRYPOINTS:
List functions that specifically implement the logic being added or altered, or direct API entrypoints
for the subsystem feature under review.
- HANDLING STATIC INLINE FUNCTIONS IN HEADERS (.h):
Compiler-inlined static functions (such as static inlines in mm/*.h or include/linux/*.h) lack
distinct symbol addresses in vmlinux and cannot be targeted directly by symbol coverage filters.
If the changes are primarily in static inline helpers, identify non-static, feature-specific caller
functions in .c files that exercise them (avoiding ubiquitous lifecycle wrappers).
================================================================================
5. IDENTIFYING EnableConfigs
================================================================================
Identify any specific CONFIG_ options required to properly compile and reach the modified code:
- Inspect Kconfig files and #ifdef guards; do not make assumptions.
- Check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed.
- Strip any '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: Target architecture: amd64
For your convenience, here is the diff of the changes:
commit 5398d1939b0ee1e5c282614b7866cad1b15130d2
Author: syz-cluster <triage@syzkaller.com>
Date: Fri Oct 30 02:40:18 2026 +0000
syz-cluster: applied patch under review
diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
index 8d7b81e6948e72..dca6796d5fe271 100644
--- a/fs/ext4/dir.c
+++ b/fs/ext4/dir.c
@@ -89,16 +89,15 @@ int __ext4_check_dir_entry(const char *function, unsigned int line,
bool fake = is_fake_dir_entry(de);
bool has_csum = ext4_has_feature_metadata_csum(dir->i_sb);
- if (unlikely(rlen < ext4_dir_rec_len(1, fake ? NULL : dir)))
+ if (unlikely(rlen < ext4_dirent_rec_len(1, fake ? NULL : dir)))
error_msg = "rec_len is smaller than minimal";
else if (unlikely(rlen % 4 != 0))
error_msg = "rec_len % 4 != 0";
- else if (unlikely(rlen < ext4_dir_rec_len(de->name_len,
- fake ? NULL : dir)))
- error_msg = "rec_len is too small for name_len";
else if (unlikely(next_offset > size))
error_msg = "directory entry overrun";
- else if (unlikely(next_offset > size - ext4_dir_rec_len(1,
+ else if (unlikely(rlen < ext4_dir_entry_len(de, size, fake ? NULL : dir)))
+ error_msg = "rec_len is too small for name_len";
+ else if (unlikely(next_offset > size - ext4_dirent_rec_len(1,
has_csum ? NULL : dir) &&
next_offset != size))
error_msg = "directory entry too close to block end";
@@ -238,7 +237,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
if (!inode_eq_iversion(inode, info->cookie)) {
for (i = 0;
i <= sb->s_blocksize -
- ext4_dir_rec_len(1, has_csum ? NULL : inode) &&
+ ext4_dirent_rec_len(1, has_csum ? NULL : inode) &&
i < offset;) {
de = (struct ext4_dir_entry_2 *)
(bh->b_data + i);
@@ -249,7 +248,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
* failure will be detected in the
* dirent test below. */
if (ext4_rec_len_from_disk(de->rec_len,
- sb->s_blocksize) < ext4_dir_rec_len(1,
+ sb->s_blocksize) < ext4_dirent_rec_len(1,
inode))
break;
i += ext4_rec_len_from_disk(de->rec_len,
@@ -263,7 +262,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
if (unlikely(offset < sb->s_blocksize &&
offset > sb->s_blocksize -
- ext4_dir_rec_len(1, has_csum ? NULL : inode))) {
+ ext4_dirent_rec_len(1, has_csum ? NULL : inode))) {
EXT4_ERROR_FILE(file, bh->b_blocknr,
"bad entry in directory: %s - offset=%u, size=%lu",
"directory entry too close to block end",
@@ -303,8 +302,19 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
u32 minor_hash;
if (IS_CASEFOLDED(inode)) {
- hash = EXT4_DIRENT_HASH(de);
- minor_hash = EXT4_DIRENT_MINOR_HASH(de);
+ if (ext4_has_feature_dirdata(sb) &&
+ (de->file_type &
+ EXT4_DIRENT_CFHASH)) {
+ struct dx_hash_info hi = {};
+ ext4_dirdata_get(de, inode,
+ sb->s_blocksize,
+ NULL, &hi);
+ hash = hi.hash;
+ minor_hash = hi.minor_hash;
+ } else {
+ hash = EXT4_DIRENT_HASH(de);
+ minor_hash = EXT4_DIRENT_MINOR_HASH(de);
+ }
} else {
hash = 0;
minor_hash = 0;
@@ -484,27 +494,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 8d3af062a83068..87c55276d67ab4 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -2369,6 +2369,7 @@ EXT4_FEATURE_INCOMPAT_FUNCS(casefold, CASEFOLD)
EXT4_FEATURE_INCOMPAT_FLEX_BG| \
EXT4_FEATURE_INCOMPAT_EA_INODE| \
EXT4_FEATURE_INCOMPAT_MMP | \
+ EXT4_FEATURE_INCOMPAT_DIRDATA | \
EXT4_FEATURE_INCOMPAT_INLINE_DATA | \
EXT4_FEATURE_INCOMPAT_ENCRYPT | \
EXT4_FEATURE_INCOMPAT_CASEFOLD | \
@@ -2507,10 +2508,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 */
@@ -2591,6 +2590,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
@@ -2608,13 +2662,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);
}
@@ -2702,6 +2769,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)
@@ -3013,18 +3082,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) &&
@@ -3038,12 +3116,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);
@@ -3279,10 +3359,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,
@@ -3894,6 +3983,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] = {
@@ -4103,6 +4196,122 @@ 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;
+}
+
+/*
+ * Advance to the next dirdata record header starting from @ddh.
+ *
+ * Uses READ_ONCE() on ddh_length to prevent the compiler from re-fetching this
+ * on-disk field and to guard against a concurrent write racing with any bounds
+ * check the caller performs on the returned pointer.
+ */
+static inline struct ext4_dirent_data_header *
+ext4_dirdata_next(struct ext4_dirent_data_header *ddh)
+{
+ return (struct ext4_dirent_data_header *)
+ ((char *)ddh + READ_ONCE(ddh->ddh_length));
+}
+
+/*
+ * ext4_dirent_get_data_len() - Compute the total dirdata length for an entry.
+ * @de: directory entry
+ * @rec_len: the record length of the directory entry (decoded)
+ *
+ * Computes the length of optional data stored after the filename (and its
+ * implicit NUL terminator). Each extension is indicated by a bit in the
+ * high 4 bits of de->file_type; the first byte of each extension is its
+ * length (including that length byte itself).
+ *
+ * Returns 0 for tail entries and for entries with no dirdata.
+ */
+static inline int ext4_dirent_get_data_len(struct ext4_dir_entry_2 *de,
+ unsigned int rec_len)
+{
+ __u8 extra_data_flags;
+ struct ext4_dirent_data_header *ddh;
+ int dlen = 0;
+ unsigned int offset;
+
+ if (ext4_dir_entry_is_tail(de))
+ return 0;
+
+ extra_data_flags = (de->file_type & ~EXT4_FT_MASK) >> 4;
+ /* offset from start of entry to after filename + NUL */
+ offset = EXT4_BASE_DIR_LEN + de->name_len + 1;
+
+ /* bounds check: ensure we start reading within the entry */
+ if (offset >= rec_len)
+ return 0;
+
+ ddh = (struct ext4_dirent_data_header *)((char *)de + offset);
+
+ while (extra_data_flags) {
+ if (extra_data_flags & 1) {
+ /* bounds check before reading ddh_length */
+ if (offset + sizeof(*ddh) > rec_len)
+ return 0;
+
+ /* Use READ_ONCE to prevent the compiler from re-fetching
+ * this disk field and to guard against a concurrent
+ * write racing with our bounds check below (TOCTOU). */
+ {
+ u8 ddh_len = READ_ONCE(ddh->ddh_length);
+
+ if (ddh_len == 0 || ddh_len > rec_len - offset)
+ return 0;
+ dlen += ddh_len + (dlen == 0);
+ offset += ddh_len;
+ /* advance using the already-validated local copy
+ * to avoid re-fetching ddh->ddh_length */
+ ddh = (struct ext4_dirent_data_header *)
+ ((char *)ddh + ddh_len);
+ }
+ }
+ extra_data_flags >>= 1;
+ }
+ return dlen;
+}
+
+/*
+ * 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;
extern const struct iomap_ops ext4_iomap_buffered_write_ops;
diff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c
index 0cac890cf37029..876a863d6c0198 100644
--- a/fs/ext4/fast_commit.c
+++ b/fs/ext4/fast_commit.c
@@ -2686,6 +2686,7 @@ static const char * const fc_ineligible_reasons[] = {
[EXT4_FC_REASON_MIGRATE] = "Inode format migration",
[EXT4_FC_REASON_VERITY] = "fs-verity enable",
[EXT4_FC_REASON_MOVE_EXT] = "Move extents",
+ [EXT4_FC_REASON_DIRDATA] = "Dirdata LUFID (not recorded by fast-commit)",
};
int ext4_fc_info_show(struct seq_file *seq, void *v)
diff --git a/fs/ext4/fast_commit.h b/fs/ext4/fast_commit.h
index 2f77a37fb10180..899faeb4e6e064 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 ceee69a66482bc..76b1490490f6f5 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -982,11 +982,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;
@@ -995,7 +1004,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);
@@ -1064,7 +1075,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,
@@ -1273,6 +1284,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;
@@ -1318,7 +1330,7 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
fake.name_len = 1;
memcpy(fake.name, ".", 2);
fake.rec_len = ext4_rec_len_to_disk(
- ext4_dir_rec_len(fake.name_len, NULL),
+ ext4_dirent_rec_len(fake.name_len, NULL),
inline_size);
ext4_set_de_type(inode->i_sb, &fake, S_IFDIR);
de = &fake;
@@ -1328,30 +1340,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) ||
@@ -1363,7 +1391,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;
@@ -1436,8 +1465,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,
/* for other entry, the real offset in
* the buf has to be tuned accordingly.
*/
- if (i + ext4_dir_rec_len(1, NULL) > extra_size)
+ if (i + ext4_dirent_rec_len(1, NULL) > extra_size)
break;
de = (struct ext4_dir_entry_2 *)
(dir_buf + i - extra_offset);
@@ -1474,7 +1503,7 @@ int ext4_read_inline_dir(struct file *file,
* failure will be detected in the
* dirent test below. */
if (ext4_rec_len_from_disk(de->rec_len, extra_size)
- < ext4_dir_rec_len(1, NULL))
+ < ext4_dirent_rec_len(1, NULL))
break;
i += ext4_rec_len_from_disk(de->rec_len,
extra_size);
@@ -1504,7 +1533,7 @@ int ext4_read_inline_dir(struct file *file,
* kmalloc(inline_size) buffer. Make sure its header fits before
* ext4_check_dir_entry() dereferences de->rec_len.
*/
- if (ctx->pos + ext4_dir_rec_len(1, NULL) > extra_size)
+ if (ctx->pos + ext4_dirent_rec_len(1, NULL) > extra_size)
goto out;
de = (struct ext4_dir_entry_2 *)
(dir_buf + ctx->pos - extra_offset);
diff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c
index 1f6d7afb62ec79..baf6ef48cbf308 100644
--- a/fs/ext4/ioctl.c
+++ b/fs/ext4/ioctl.c
@@ -1536,6 +1536,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);
@@ -1922,6 +2004,8 @@ static long __ext4_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
(void __user *)arg);
case EXT4_IOC_SET_TUNE_SB_PARAM:
return ext4_ioctl_set_tune_sb(filp, (void __user *)arg);
+ case EXT4_IOC_SET_LUFID:
+ return ext4_ioctl_set_lufid(filp, arg);
default:
return -ENOTTY;
}
@@ -2001,6 +2085,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 231a274b814ecb..4ab1f14442b090 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,38 @@ 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) +
+ sizeof(struct dx_entry) > 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 +612,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 +634,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 +747,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 +831,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 +843,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 +875,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 +891,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 +910,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 +995,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 +1004,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 +1127,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 +1152,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 +1190,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 +1206,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 +1286,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 +1298,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 +1336,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 +1359,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 +1586,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 +1701,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 +1730,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
@@ -1465,11 +1783,18 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
dlimit = search_buf + buf_size;
while ((char *) de < dlimit - EXT4_BASE_DIR_LEN) {
/* this code is executed quadratically often */
- /* do minimal checking `by hand' */
+ /* Decode rec_len before ext4_match() so that ext4_dirdata_get()
+ * inside ext4_match() never uses an unvalidated rec_len as a
+ * read bound. A crafted entry with rec_len > buf_size would
+ * let ext4_dirdata_get() read past the block buffer. */
+ de_len = ext4_rec_len_from_disk(de->rec_len,
+ dir->i_sb->s_blocksize);
+ if (de_len <= 0 || (char *)de + de_len > dlimit)
+ return -EFSCORRUPTED;
if (de->name + de->name_len <= dlimit &&
(!ext4_hash_in_dirent(dir) ||
- (char *)de + ext4_dir_rec_len(de->name_len, dir) <= dlimit) &&
- ext4_match(dir, fname, de)) {
+ (char *)de + ext4_dirent_rec_len(de->name_len, dir) <= dlimit) &&
+ 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,
@@ -1478,11 +1803,6 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
*res_dir = de;
return 1;
}
- /* prevent looping on a bad block */
- de_len = ext4_rec_len_from_disk(de->rec_len,
- dir->i_sb->s_blocksize);
- if (de_len <= 0)
- return -EFSCORRUPTED;
offset += de_len;
de = (struct ext4_dir_entry_2 *) ((char *) de + de_len);
}
@@ -1490,7 +1810,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;
@@ -1621,7 +1941,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 "
@@ -1755,7 +2075,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;
}
@@ -1852,7 +2172,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 =
@@ -1885,7 +2205,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);
@@ -2037,10 +2357,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;
@@ -2051,9 +2376,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;
@@ -2067,16 +2392,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 =
@@ -2090,13 +2412,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);
}
/*
@@ -2114,14 +2430,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;
}
@@ -2134,7 +2464,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
@@ -2158,44 +2492,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:
@@ -2213,16 +2541,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))
@@ -2239,17 +2566,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);
@@ -2280,24 +2610,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;
@@ -2340,7 +2678,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;
}
@@ -2357,7 +2695,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;
@@ -2382,6 +2721,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)
@@ -2466,7 +2812,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);
}
/*
@@ -2607,7 +2954,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));
@@ -2615,11 +2962,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);
@@ -2643,7 +2996,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
*/
@@ -2929,14 +3282,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);
@@ -2947,7 +3301,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;
@@ -2956,7 +3310,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);
}
@@ -2968,8 +3322,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;
@@ -2988,6 +3343,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);
@@ -3079,8 +3440,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;
}
@@ -3473,7 +3834,8 @@ int __ext4_link(struct inode *dir, struct inode *inode,
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
@@ -3641,8 +4003,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;
@@ -3652,8 +4052,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);
@@ -3690,7 +4168,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);
}
@@ -3789,6 +4267,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.
@@ -3815,8 +4354,10 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
int force_reread;
int retval;
struct inode *whiteout = NULL;
+ struct ext4_dentry_param *edp = NULL;
int credits;
u8 old_file_type;
+ struct ext4_lufid_snap old_snap = {};
if (new.inode && new.inode->i_nlink == 0) {
EXT4_ERROR_INODE(new.inode,
@@ -3922,29 +4463,64 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
force_reread = (new.dir->i_ino == old.dir->i_ino &&
ext4_test_inode_flag(new.dir, EXT4_INODE_INLINE_DATA));
+ if (ext4_has_feature_dirdata(old.dir->i_sb)) {
+ /* Snapshot before whiteout's ext4_setent() clears the LUFID
+ * flag in old.de; the snapshot is also needed for the !new.bh
+ * path below where we must carry the LUFID to the new entry. */
+ retval = ext4_lufid_snapshot(old.de,
+ old.dir->i_sb->s_blocksize,
+ &old_snap);
+ if (retval)
+ goto end_rename;
+ if (!old_snap.fid)
+ old_file_type &= ~EXT4_DIRENT_LUFID;
+ }
+
+ /* Allocate the LUFID wrapper for ext4_add_entry() before any
+ * destructive changes to the old entry. An OOM failure after
+ * ext4_setent() has already compacted the extensions would trigger
+ * the lossy resetent path; pre-allocating here avoids that. */
+ if (!new.bh && old_snap.fid) {
+ unsigned fid_size = old_snap.fid->df_header.ddh_length;
+
+ edp = kmalloc(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);
+ }
+
if (whiteout) {
/*
* Do this before adding a new entry, so the old entry is sure
* to be still pointing to the valid old entry.
*/
retval = ext4_setent(handle, &old, whiteout->i_ino,
- EXT4_FT_CHRDEV);
+ EXT4_FT_CHRDEV, NULL);
if (retval)
goto end_rename;
retval = ext4_mark_inode_dirty(handle, whiteout);
if (unlikely(retval))
goto end_rename;
-
}
if (!new.bh) {
+ new.dentry->d_fsdata = edp;
retval = ext4_add_entry(handle, new.dentry, old.inode);
+ new.dentry->d_fsdata = NULL;
if (retval)
goto end_rename;
} else {
retval = ext4_setent(handle, &new,
- old.inode->i_ino, old_file_type);
+ old.inode->i_ino, old_file_type, old_snap.fid);
if (retval)
goto end_rename;
+ /* ext4_setent() writes the LUFID in-place without going through
+ * __ext4_add_entry(), so mark ineligible here if needed. */
+ if (old_snap.fid)
+ ext4_fc_mark_ineligible(old.dir->i_sb,
+ EXT4_FC_REASON_DIRDATA, handle);
}
if (force_reread)
force_reread = !ext4_test_inode_flag(new.dir,
@@ -4029,8 +4605,25 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
retval = 0;
end_rename:
+ kfree(edp);
+ ext4_lufid_snap_release(&old_snap);
if (whiteout) {
if (retval) {
+ /* Known limitation: if the old entry carried both LUFID
+ * and CFHASH, ext4_setent_compact_exts() slid CFHASH
+ * into the LUFID slot above, and old_snap was released
+ * before this point, so resetent can only restore the
+ * LUFID flag — not the original byte layout. This
+ * leaves the entry with CFHASH data at the LUFID offset
+ * and garbage at the CFHASH offset until the next
+ * SET_LUFID ioctl re-writes it. The scenario requires
+ * RENAME_WHITEOUT on a casefolded+encrypted dirdata
+ * directory whose entry has a LUFID, followed by a
+ * journal IO error in the single ext4_mark_inode_dirty()
+ * call that follows — an extremely unlikely combination
+ * in practice. (OOM during the LUFID wrapper allocation
+ * cannot trigger this path because that allocation is
+ * hoisted before ext4_setent() modifies the entry.) */
ext4_resetent(handle, &old,
old.inode->i_ino, old_file_type);
drop_nlink(whiteout);
@@ -4065,8 +4658,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,
@@ -4145,12 +4739,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;
@@ -4183,6 +4798,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);
@@ -4219,6 +4836,266 @@ 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 ext4_lufid_snap old_snap = {};
+ struct qstr d_name;
+ /* on-disk name snapshot for non-encrypted casefolded directories: the
+ * on-disk case may differ from what the caller supplied. */
+ char ondisk_name[EXT4_NAME_LEN];
+ struct qstr real_name = {};
+ /* name to pass to ext4_add_entry() for the re-add and rollback */
+ const struct qstr *add_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 calls ext4_delete_entry() on the parent directory, which
+ * bypasses the VFS may_delete() path. inode_permission(MAY_WRITE)
+ * above blocks IS_IMMUTABLE(dir) but does NOT block IS_APPEND(dir):
+ * append-only directories allow new entries but forbid deletion, and
+ * that restriction lives only in may_delete(), not in
+ * inode_permission(). Enforce it explicitly here. */
+ if (IS_APPEND(dir)) {
+ err = -EPERM;
+ goto out_iput;
+ }
+
+ /* The ioctl deletes and re-creates the directory entry, which
+ * effectively modifies the target file's namespace presence.
+ * Refuse if the target itself is immutable or append-only. */
+ if (IS_IMMUTABLE(inode) || IS_APPEND(inode)) {
+ err = -EPERM;
+ goto out_iput;
+ }
+
+ /* 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;
+ }
+
+ /* Choose the name to use for re-adding the entry after deletion.
+ *
+ * For non-encrypted casefolded directories ext4_find_entry() matches
+ * case-insensitively, so de->name may differ in case from the
+ * caller-supplied filename. Re-adding with the caller's case would
+ * silently rename the entry, so preserve the on-disk bytes.
+ *
+ * For encrypted directories (with or without casefold) de->name holds
+ * the raw ciphertext. ext4_fname_setup_filename() deterministically
+ * re-encrypts d_name (the plaintext) to the identical ciphertext, so
+ * using d_name for the re-add is correct. Passing the raw ciphertext
+ * bytes would cause double-encryption and make the entry unreachable. */
+ if (IS_CASEFOLDED(dir) && !IS_ENCRYPTED(dir)) {
+ memcpy(ondisk_name, de->name, de->name_len);
+ real_name.name = ondisk_name;
+ real_name.len = de->name_len;
+ add_name = &real_name;
+ } else {
+ add_name = &d_name;
+ }
+
+ /* Snapshot the old LUFID before deleting so we can restore it if the
+ * re-add fails. Without this the rollback path re-adds the entry
+ * without any LUFID, silently orphaning the old FID-to-path mapping. */
+ err = ext4_lufid_snapshot(de, sb->s_blocksize, &old_snap);
+ if (err)
+ 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 = *add_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. Restore the
+ * original LUFID too: without it the entry is re-added clean
+ * and the old FID-to-path mapping is silently lost. */
+ struct dentry parent_dentry = { .d_inode = dir };
+ struct dentry orig_dentry = {
+ .d_name = *add_name,
+ .d_parent = &parent_dentry,
+ .d_inode = inode,
+ };
+ int rollback_err;
+
+ if (old_snap.fid) {
+ unsigned int fid_size = old_snap.fid->df_header.ddh_length;
+ struct ext4_dentry_param *old_edp =
+ kmalloc(sizeof(*old_edp) + fid_size, GFP_NOFS);
+
+ if (old_edp) {
+ old_edp->edp_magic = EXT4_LUFID_MAGIC;
+ memcpy(old_edp->edp_dfid, old_snap.fid, fid_size);
+ orig_dentry.d_fsdata = old_edp;
+ }
+ }
+
+ rollback_err = ext4_add_entry(handle, &orig_dentry, inode);
+ kfree(orig_dentry.d_fsdata);
+
+ 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,
+ (unsigned long long)inode->i_ino);
+ goto out_journal;
+ }
+
+ inode_set_ctime_current(dir);
+ inode_inc_iversion(dir);
+ ext4_mark_inode_dirty(handle, dir);
+
+out_brelse:
+ brelse(bh);
+out_journal:
+ ext4_lufid_snap_release(&old_snap);
+ 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 923b375e017faa..80074fb15ee9b7 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 9c683991c32f96..9134fe64947a93 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/10/30 02:40 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 42613 output: 33 thoughts: 520 Thoughts: **Fuzzing Decision for the Ext4 Patch** Okay, let's break this down. First, I need to assess whether this patch is a good candidate for syzkaller fuzzing. Looking at the code changes, it's clear we're dealing with extensive modifications to `ext4`, touching `dir.c`, `ext4.h`, `fast_commit.c`, `inline.c`, `ioctl.c`, `namei.c`, and `sysfs.c`, as well as the `uapi` headers. Specifically, it's about `EXT4_FEATURE_INCOMPAT_DIRDATA`, the `EXT4_IOC_SET_LUFID` ioctl, and all the associated directory operations - htree, readdir, inline directories, search, links, renames, and the rest. This immediately screams "high-value target". This is *core* ext4 filesystem code, which is widely fuzzed already. Userspace can *easily* interact with this – mounting ext4 filesystems, manipulating images (including corrupted ones), using that new ioctl, creating directories, and performing operations like casefolding, encryption, renames, readdir, etc. So the attack surface is wide open. Now, let's confirm the "WorthFuzzing" criteria. * **Core Subsystem/Filesystem:** Check. Ext4 hits both of those points. * **Reachability:** Check. Ext4 is readily available in standard virtualization setups. * **Code Modification:** Check. Directory parsing, insertion, lookup, rename, that new ioctl, and plenty of buffer/hash parsing are all being modified. * **New Checks/Parsers:** Check. We're seeing new bounds checks and parsing loops like `ext4_dirent_get_data_len`, `ext4_dirdata_get`, and such. Alright, that's a slam dunk. WorthFuzzing is *definitely* true. This is a prime target. Next, I need to check the `EnableConfigs`. What kernel configs are required for this new functionality? I should check the Kconfig files or the code itself for dependencies. Let's start with `fs/ext4/Kconfig` using grepper to be thorough. I want to make sure we have all the dependencies covered before getting started. |
| 4/2 | 2026/10/30 02:40 | tool | grepper |
0mArgs:{
"Expression": "config EXT4_FS",
"PathPrefixes": [
"fs/ext4/Kconfig"
]
}Results: {
"Output": "fs/ext4/Kconfig-1-# SPDX-License-Identifier: GPL-2.0-only\nfs/ext4/Kconfig:2:config EXT4_FS\nfs/ext4/Kconfig-3-\ttristate \"The Extended 4 (ext4) filesystem\"\n--\nfs/ext4/Kconfig=35=config EXT4_USE_FOR_EXT2\n--\nfs/ext4/Kconfig-45-\nfs/ext4/Kconfig:46:config EXT4_FS_POSIX_ACL\nfs/ext4/Kconfig-47-\tbool \"Ext4 POSIX Access Control Lists\"\n--\nfs/ext4/Kconfig-55-\nfs/ext4/Kconfig:56:config EXT4_FS_SECURITY\nfs/ext4/Kconfig-57-\tbool \"Ext4 Security Labels\"\n"
} |
| 5/2 | 2026/10/30 02:40 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 43880 output: 28 thoughts: 40 Thoughts: Okay, here's my take on those thoughts, framed as my own: **Investigating the Implementation of DIRDATA in ext4** Right, let's get down to brass tacks. I need to understand how `DIRDATA` is implemented in ext4. Is it a compile-time configuration option, a plain old `#define` that's enabled based on some build-time flag, or is it solely controlled by a feature flag within the superblock itself? The distinction is important, of course. Knowing the mechanism helps in understanding its dependencies and potential interaction with other components of the file system. So, the first thing I'll do is dive into the source code. I'll head over to `fs/ext4/` and do a quick `grep` for "DIRDATA". That should give me the lowdown on how the code actually references this feature. Based on the results, I'll be able to quickly determine if it's handled as a CONFIG option (something like `CONFIG_EXT4_DIRDATA`), or is implemented in some other manner. |
| 6/2 | 2026/10/30 02:40 | tool | grepper |
0mArgs:{
"Expression": "DIRDATA",
"PathPrefixes": [
"fs/ext4/"
]
}Results: {
"Output": "fs/ext4/ext4.h=2159=static inline bool ext4_inode_buffered_iomap(struct inode *inode)\n--\nfs/ext4/ext4.h-2240-#define EXT4_FEATURE_INCOMPAT_EA_INODE\t\t0x0400 /* EA in inode */\nfs/ext4/ext4.h:2241:#define EXT4_FEATURE_INCOMPAT_DIRDATA\t\t0x1000 /* data in dirent */\nfs/ext4/ext4.h-2242-#define EXT4_FEATURE_INCOMPAT_CSUM_SEED\t\t0x2000\n--\nfs/ext4/ext4.h=2339=EXT4_FEATURE_INCOMPAT_FUNCS(ea_inode,\t\tEA_INODE)\nfs/ext4/ext4.h:2340:EXT4_FEATURE_INCOMPAT_FUNCS(dirdata,\t\tDIRDATA)\nfs/ext4/ext4.h-2341-EXT4_FEATURE_INCOMPAT_FUNCS(csum_seed,\t\tCSUM_SEED)\n--\nfs/ext4/ext4.h=2345=EXT4_FEATURE_INCOMPAT_FUNCS(casefold,\t\tCASEFOLD)\n--\nfs/ext4/ext4.h-2371-\t\t\t\t\t EXT4_FEATURE_INCOMPAT_MMP | \\\nfs/ext4/ext4.h:2372:\t\t\t\t\t EXT4_FEATURE_INCOMPAT_DIRDATA | \\\nfs/ext4/ext4.h-2373-\t\t\t\t\t EXT4_FEATURE_INCOMPAT_INLINE_DATA | \\\n--\nfs/ext4/fast_commit.c=2675=static const char * const fc_ineligible_reasons[] = {\n--\nfs/ext4/fast_commit.c-2688-\t[EXT4_FC_REASON_MOVE_EXT] = \"Move extents\",\nfs/ext4/fast_commit.c:2689:\t[EXT4_FC_REASON_DIRDATA] = \"Dirdata LUFID (not recorded by fast-commit)\",\nfs/ext4/fast_commit.c-2690-};\n--\nfs/ext4/fast_commit.h=89=enum {\n--\nfs/ext4/fast_commit.h-102-\tEXT4_FC_REASON_MOVE_EXT,\nfs/ext4/fast_commit.h:103:\tEXT4_FC_REASON_DIRDATA,\nfs/ext4/fast_commit.h-104-\tEXT4_FC_REASON_MAX\n--\nfs/ext4/namei.c=1348=static inline int search_dirblock(struct buffer_head *bh,\n--\nfs/ext4/namei.c-1375- *\nfs/ext4/namei.c:1376: * Compatibility note: enabling EXT4_FEATURE_INCOMPAT_DIRDATA on a filesystem\nfs/ext4/namei.c-1377- * that already has casefolded+encrypted directories is NOT safe without a\n--\nfs/ext4/namei.c=1389=unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de, struct inode *dir,\n--\nfs/ext4/namei.c-1495- * directories requires an e2fsck migration pass before tune2fs sets the\nfs/ext4/namei.c:1496: * EXT4_FEATURE_INCOMPAT_DIRDATA superblock flag.\nfs/ext4/namei.c-1497- */\n--\nfs/ext4/namei.c=2696=static int __ext4_add_entry(handle_t *handle, struct inode *dir,\n--\nfs/ext4/namei.c-2728-\tif (fname.dfid)\nfs/ext4/namei.c:2729:\t\text4_fc_mark_ineligible(sb, EXT4_FC_REASON_DIRDATA, handle);\nfs/ext4/namei.c-2730-\n--\nfs/ext4/namei.c=4339=static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,\n--\nfs/ext4/namei.c-4522-\t\t\text4_fc_mark_ineligible(old.dir-\u003ei_sb,\nfs/ext4/namei.c:4523:\t\t\t\t\t\tEXT4_FC_REASON_DIRDATA, handle);\nfs/ext4/namei.c-4524-\t}\n--\nfs/ext4/namei.c=4647=static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry,\n--\nfs/ext4/namei.c-4752-\t\t\text4_fc_mark_ineligible(old.dir-\u003ei_sb,\nfs/ext4/namei.c:4753:\t\t\t\t\t\tEXT4_FC_REASON_DIRDATA, handle);\nfs/ext4/namei.c-4754-\t}\n"
} |
| 7/2 | 2026/10/30 02:40 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 45450 output: 28 thoughts: 48 Thoughts: **Examining the ext4 Configuration** Okay, so I'm diving into this ext4 configuration and it looks like `CONFIG_EXT4_DIRDATA` isn't a direct option. Right, it's probably implicitly enabled as part of the broader `EXT4_FS` setting. That makes sense, keeps things cleaner. Now, I need to consider Unicode and case folding. I can't recall off the top of my head where that is handled. Time to dig into the `fs/ext4/Kconfig` file and see if there are specific options for casefold or unicode support. Gotta make sure everything is configured properly for this use case. Let's see what the config file reveals. |
| 8/2 | 2026/10/30 02:40 | tool | grepper |
0mArgs:{
"Expression": "UNICODE",
"PathPrefixes": [
"fs/ext4/"
]
}Results: {
"Output": "fs/ext4/ext4.h=2762=struct ext4_filename {\n--\nfs/ext4/ext4.h-2768-#endif\nfs/ext4/ext4.h:2769:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/ext4.h-2770-\tstruct qstr cf_name;\n--\nfs/ext4/ext4.h=3004=ext4_fsblk_t ext4_inode_to_goal_block(struct inode *);\nfs/ext4/ext4.h-3005-\nfs/ext4/ext4.h:3006:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/ext4.h-3007-extern int ext4_fname_setup_ci_filename(struct inode *dir,\n--\nfs/ext4/hash-test.c=386=static void test_ext4fs_dirhash_signed_unsigned_differ_on_nonascii(struct kunit *test)\n--\nfs/ext4/hash-test.c-440-\nfs/ext4/hash-test.c:441:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/hash-test.c-442-KUNIT_DEFINE_ACTION_WRAPPER(utf8_unload_action, utf8_unload,\n--\nfs/ext4/hash-test.c=547=static struct kunit_case ext4_hash_test_cases[] = {\n--\nfs/ext4/hash-test.c-552-\tKUNIT_CASE(test_ext4fs_dirhash_signed_unsigned_differ_on_nonascii),\nfs/ext4/hash-test.c:553:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/hash-test.c-554-\tKUNIT_CASE(test_ext4fs_dirhash_casefolded_names_hash_consistently),\n--\nfs/ext4/hash.c=314=int ext4fs_dirhash(const struct inode *dir, const char *name, int len,\n--\nfs/ext4/hash.c-316-{\nfs/ext4/hash.c:317:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/hash.c-318-\tconst struct unicode_map *um = dir-\u003ei_sb-\u003es_encoding;\n--\nfs/ext4/namei.c=1645=static void dx_insert_block(struct dx_frame *frame, u32 hash, ext4_lblk_t block)\n--\nfs/ext4/namei.c-1658-\nfs/ext4/namei.c:1659:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/namei.c-1660-int ext4_fname_setup_ci_filename(struct inode *dir, const struct qstr *iname,\n--\nfs/ext4/namei.c=1703=static bool ext4_match(struct inode *parent,\n--\nfs/ext4/namei.c-1718-\nfs/ext4/namei.c:1719:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/namei.c-1720-\tif (IS_CASEFOLDED(parent) \u0026\u0026\n--\nfs/ext4/namei.c=2082=static struct dentry *ext4_lookup(struct inode *dir, struct dentry *dentry, unsigned int flags)\n--\nfs/ext4/namei.c-2124-\nfs/ext4/namei.c:2125:\tif (IS_ENABLED(CONFIG_UNICODE) \u0026\u0026 !inode \u0026\u0026 IS_CASEFOLDED(dir)) {\nfs/ext4/namei.c-2126-\t\t/* Eventually we want to call d_add_ci(dentry, NULL)\n--\nfs/ext4/namei.c=3499=static int ext4_rmdir(struct inode *dir, struct dentry *dentry)\n--\nfs/ext4/namei.c-3583-\t */\nfs/ext4/namei.c:3584:\tif (!retval \u0026\u0026 IS_ENABLED(CONFIG_UNICODE) \u0026\u0026 IS_CASEFOLDED(dir))\nfs/ext4/namei.c-3585-\t\td_invalidate(dentry);\n--\nfs/ext4/namei.c=3663=static int ext4_unlink(struct inode *dir, struct dentry *dentry)\n--\nfs/ext4/namei.c-3690-\t */\nfs/ext4/namei.c:3691:\tif (IS_ENABLED(CONFIG_UNICODE) \u0026\u0026 IS_CASEFOLDED(dir))\nfs/ext4/namei.c-3692-\t\td_invalidate(dentry);\n--\nfs/ext4/super.c=1293=static void ext4_put_super(struct super_block *sb)\n--\nfs/ext4/super.c-1397-\tfscrypt_free_dummy_policy(\u0026sbi-\u003es_dummy_enc_policy);\nfs/ext4/super.c:1398:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/super.c-1399-\tutf8_unload(sb-\u003es_encoding);\n--\nfs/ext4/super.c=1917=static const struct mount_opts {\n--\nfs/ext4/super.c-1995-\nfs/ext4/super.c:1996:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/super.c-1997-static const struct ext4_sb_encodings {\n--\nfs/ext4/super.c-2001-} ext4_sb_encoding_map[] = {\nfs/ext4/super.c:2002:\t{EXT4_ENC_UTF8_12_1, \"utf8\", UNICODE_AGE(12, 1, 0)},\nfs/ext4/super.c-2003-};\n--\nfs/ext4/super.c=3673=int ext4_feature_set_ok(struct super_block *sb, int readonly)\n--\nfs/ext4/super.c-3683-\nfs/ext4/super.c:3684:\tif (!IS_ENABLED(CONFIG_UNICODE) \u0026\u0026 ext4_has_feature_casefold(sb)) {\nfs/ext4/super.c-3685-\t\text4_msg(sb, KERN_ERR,\nfs/ext4/super.c-3686-\t\t\t \"Filesystem with casefold feature cannot be \"\nfs/ext4/super.c:3687:\t\t\t \"mounted without CONFIG_UNICODE\");\nfs/ext4/super.c-3688-\t\treturn 0;\n--\nfs/ext4/super.c=4608=static int ext4_inode_info_init(struct super_block *sb,\n--\nfs/ext4/super.c-4679-\nfs/ext4/super.c:4680:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/super.c-4681-static int ext4_encoding_init(struct super_block *sb, struct ext4_super_block *es)\n--\nfs/ext4/super.c=5808=failed_mount8: __maybe_unused\n--\nfs/ext4/super.c-5848-failed_mount:\nfs/ext4/super.c:5849:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/super.c-5850-\tutf8_unload(sb-\u003es_encoding);\n--\nfs/ext4/sysfs.c=355=EXT4_ATTR_FEATURE(test_dummy_encryption_v2);\nfs/ext4/sysfs.c-356-#endif\nfs/ext4/sysfs.c:357:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/sysfs.c-358-EXT4_ATTR_FEATURE(casefold);\n--\nfs/ext4/sysfs.c=365=EXT4_ATTR_FEATURE(dirdata);\nfs/ext4/sysfs.c:366:#if IS_ENABLED(CONFIG_UNICODE) \u0026\u0026 defined(CONFIG_FS_ENCRYPTION)\nfs/ext4/sysfs.c-367-EXT4_ATTR_FEATURE(encrypted_casefold);\n--\nfs/ext4/sysfs.c=373=static struct attribute *ext4_feat_attrs[] = {\n--\nfs/ext4/sysfs.c-380-#endif\nfs/ext4/sysfs.c:381:#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/sysfs.c-382-\tATTR_LIST(casefold),\n--\nfs/ext4/sysfs.c-389-\tATTR_LIST(dirdata),\nfs/ext4/sysfs.c:390:#if IS_ENABLED(CONFIG_UNICODE) \u0026\u0026 defined(CONFIG_FS_ENCRYPTION)\nfs/ext4/sysfs.c-391-\tATTR_LIST(encrypted_casefold),\n"
} |
| 9/2 | 2026/10/30 02:40 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 47977 output: 27 thoughts: 2402 Thoughts: **Analysis of dirdata interaction with UNICODE and FS_ENCRYPTION in ext4** Okay, so I'm seeing a deep interaction here between `dirdata`, `UNICODE`, and `FS_ENCRYPTION` within the ext4 filesystem. The code paths in `dir.c` and `ext4_match` are clearly conditioned on these features, specifically the handling of casefolding and encryption within directory entries. This suggests that enabling these configurations will be crucial to reach and test this code. I've reviewed the diff and have a good understanding of the modified and added functions across the various ext4 source files (`dir.c`, `ext4.h`, `inline.c`, `ioctl.c`, and `namei.c`). The changes are quite extensive, spanning directory entry management, IOCTL operations, and handling of both inline directories and indexed directories. I've also identified several static inline functions in ext4.h that are crucial but not directly targetable for fuzzing. My primary focus right now is to identify the most relevant functions to target with the fuzzer. Following the guidelines, I need to choose specific feature-related entrypoints and avoid ubiquitous functions. The following non-ubiquitous functions seem most promising: `ext4_ioctl_set_lufid`, `ext4_dirdata_set_lufid`, `ext4_dirdata_get`, `ext4_dirdata_set`, `ext4_insert_dentry_data`, `ext4_htree_store_dirent`, `ext4_find_dest_de`, `ext4_setent_compact_exts`, `ext4_lufid_snapshot`, `dx_get_dx_info`, `ext4_inlinedir_to_tree`. These seem like direct entrypoints for the new or altered functionality and handle the new dirdata support in namei and ioctl. I've confirmed that all chosen functions are actually defined symbols in vmlinux (or can be targeted, in the case of static functions in .c files). They are all ext4-specific directory/ioctl functions and appear far from ubiquitous. I also considered `make_indexed_dir`, `ext4_setent`, and `htree_dirblock_to_tree`, but I believe the initial list is more focused. Finally, I've confirmed the necessary kernel configuration options are `EXT4_FS`, `UNICODE`, and `FS_ENCRYPTION`. These are clearly essential to even enable the code paths this work touches. |
| 10/2 | 2026/10/30 02:41 | tool | grepper |
0mArgs:{
"Expression": "config UNICODE",
"PathPrefixes": [
"fs/"
]
}Results: {
"Output": "fs/unicode/Kconfig-4-#\nfs/unicode/Kconfig:5:config UNICODE\nfs/unicode/Kconfig-6-\ttristate \"UTF-8 normalization and casefolding support\"\n--\nfs/unicode/Kconfig-12-\nfs/unicode/Kconfig:13:config UNICODE_NORMALIZATION_KUNIT_TEST\nfs/unicode/Kconfig-14-\ttristate \"Test UTF-8 normalization support\"\n"
} |
| 11/2 | 2026/10/30 02:41 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 51068 output: 31 thoughts: 17 Thoughts: Okay, here's my summary, tailored for an expert audience, as if I'm thinking these thoughts: **Checking FS_ENCRYPTION in fs/crypto/Kconfig** Alright, let's dive into this. I need to refresh my understanding of the filesystem encryption configuration options. My primary focus right now is to pinpoint precisely how `FS_ENCRYPTION` is defined and if any dependencies or interactions may affect other options. Given the nature of my work, I want to be extremely clear on what enabling this specific feature actually *does* at a foundational level. I'll need to confirm what other configuration knobs it influences, either directly, or indirectly. The goal is to fully understand its implications on everything from performance considerations to security implications. This will take me to the Kconfig file in `fs/crypto`, and from there, I need to check how it's integrated with the broader filesystem ecosystem. |
| 12/2 | 2026/10/30 02:41 | tool | grepper |
0mArgs:{
"Expression": "config FS_ENCRYPTION",
"PathPrefixes": [
"fs/crypto/"
]
}Results: {
"Output": "fs/crypto/Kconfig-1-# SPDX-License-Identifier: GPL-2.0-only\nfs/crypto/Kconfig:2:config FS_ENCRYPTION\nfs/crypto/Kconfig-3-\tbool \"FS Encryption (Per-file encryption)\"\n--\nfs/crypto/Kconfig-30-# strongly recommended to enable optimized implementations too.\nfs/crypto/Kconfig:31:config FS_ENCRYPTION_ALGS\nfs/crypto/Kconfig-32-\ttristate\n--\nfs/crypto/Kconfig-37-\nfs/crypto/Kconfig:38:config FS_ENCRYPTION_INLINE_CRYPT\nfs/crypto/Kconfig-39-\tbool\n"
} |
| 13/2 | 2026/10/30 02:41 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 51483 output: 268 thoughts: 429 Thoughts: **Analyzing the Potential Fuzzing Target** Okay, let's see. My initial instinct is to focus on these kernel configurations: `CONFIG_EXT4_FS`, `CONFIG_FS_ENCRYPTION`, and `CONFIG_UNICODE`. Let me quickly double-check if there are any *other* relevant ext4 configurations, just to be thorough. Ah, yes, `CONFIG_EXT4_FS_POSIX_ACL` and `CONFIG_EXT4_FS_SECURITY` are worth noting; they're pretty standard ext4 options. But `EXT4_FS` is the *core* one, obviously. Now, let me break down the reasoning behind this assessment. The patch introduces this new `EXT4_FEATURE_INCOMPAT_DIRDATA` feature to ext4, which lets directory entries store additional metadata, specifically after the filename. This is a big deal. We're talking about new functionality to store data, like LUFID and casefold hashes, right on the disk. They're also adding the `EXT4_IOC_SET_LUFID` ioctl command. Furthermore, they are adding a suite of pretty complex directory parsing, formatting, and traversal helpers, like `ext4_dirdata_get`, `ext4_dirdata_set`, and `ext4_dirdata_set_lufid`. It is even modifying the directory creation, lookup, htree indexing, readdir, rename, and inline directory handling. This goes straight to the core reachable filesystem code, handling untrusted on-disk data and user ioctl input. That makes it a *prime* target for fuzzing. It's essentially introducing new attack surfaces. The key focus areas should be these symbols: `ext4_ioctl_set_lufid`, `ext4_dirdata_set_lufid`, `ext4_dirdata_get`, `ext4_dirdata_set`, `ext4_insert_dentry_data`, `ext4_setent_compact_exts`, `ext4_lufid_snapshot`, `dx_get_dx_info`, `ext4_htree_store_dirent`, and `ext4_inlinedir_to_tree`. These are where the real vulnerabilities will likely lie. Alright, time to see if `set-results` is ready for this analysis. All the tools should be primed. I'll go ahead and call `set-results`. |
| 14/2 | 2026/10/30 02:41 | tool | set-results |
0mArgs:{
"EnableConfigs": [
"EXT4_FS",
"FS_ENCRYPTION",
"UNICODE"
],
"FocusSymbols": [
"ext4_ioctl_set_lufid",
"ext4_dirdata_set_lufid",
"ext4_dirdata_get",
"ext4_dirdata_set",
"ext4_insert_dentry_data",
"ext4_setent_compact_exts",
"ext4_lufid_snapshot",
"dx_get_dx_info",
"ext4_htree_store_dirent",
"ext4_inlinedir_to_tree"
],
"Reasoning": "The patch adds support for EXT4_FEATURE_INCOMPAT_DIRDATA and introduces the EXT4_IOC_SET_LUFID ioctl to attach LUFID data to directory entries. It modifies directory traversal, htree indexing, inline directories, rename/whiteout handling, and directory entry validation to accommodate variable-length dirdata extensions. Because this affects core, fully reachable filesystem parsing and directory mutation logic that handles both user input and on-disk structures, it warrants fuzzing.",
"WorthFuzzing": true
}Results: {
"EnableConfigs": [
"EXT4_FS",
"FS_ENCRYPTION",
"UNICODE"
],
"FocusSymbols": [
"ext4_ioctl_set_lufid",
"ext4_dirdata_set_lufid",
"ext4_dirdata_get",
"ext4_dirdata_set",
"ext4_insert_dentry_data",
"ext4_setent_compact_exts",
"ext4_lufid_snapshot",
"dx_get_dx_info",
"ext4_htree_store_dirent",
"ext4_inlinedir_to_tree"
],
"Reasoning": "The patch adds support for EXT4_FEATURE_INCOMPAT_DIRDATA and introduces the EXT4_IOC_SET_LUFID ioctl to attach LUFID data to directory entries. It modifies directory traversal, htree indexing, inline directories, rename/whiteout handling, and directory entry validation to accommodate variable-length dirdata extensions. Because this affects core, fully reachable filesystem parsing and directory mutation logic that handles both user input and on-disk structures, it warrants fuzzing.",
"WorthFuzzing": true
} |
| 15/1 | 2026/10/30 02:41 | action | If |
4mArgs:{
"WorthFuzzing": true
}Results: null |
| 16/2 | 2026/10/30 02:41 | agent | kmsan-evaluator |
4mModel:core Args: null Results: {
"KMSANReasoning": "The patch series implements support for the EXT4_FEATURE_INCOMPAT_DIRDATA feature (primarily Lustre Unique File Identifiers - LUFID - and casefolded hashes CFHASH), adding the EXT4_IOC_SET_LUFID ioctl and directory entry manipulation logic.\n\nKMSAN vs KASAN Applicability Analysis:\n1. Kernel-to-User Leaks / Outputs:\n - The newly introduced ioctl EXT4_IOC_SET_LUFID is defined with _IOW, meaning it only reads data from userspace via copy_from_user. It does not return any struct or memory back to userspace via copy_to_user or put_user.\n - In readdir/filldir (ext4_readdir, call_filldir, ext4_read_inline_dir), dirent names are emitted strictly up to their validated name length (de-\u003ename_len / ent_name-\u003elen) and file types are masked with EXT4_FT_MASK. No uninitialized padding or dirdata bytes are leaked to userspace.\n\n2. Structure Initialization and Memory Allocations:\n - All stack variables introduced in the patch (e.g. struct dx_hash_info hi = {}, dirent_hinfo = {}, struct ext4_lufid_snap old_snap = {}, struct ext4_filename fname = {}, struct qstr real_name = {}) are explicitly zero-initialized.\n - Dynamic allocations either use kzalloc_flex (guaranteeing zero-initialization before data copy, such as struct fname in ext4_htree_store_dirent), kmemdup, or kmalloc where all fields and flexible payload bytes are immediately fully populated (such as edp and old_edp in ext4_rename and ext4_dirdata_set_lufid).\n\n3. Logic, Offsets, and Bounds:\n - The modifications primarily adjust directory entry offset calculations, record length bounds checks, and in-place extension compaction (ext4_dirent_get_data_len, ext4_dir_entry_len, ext4_setent_compact_exts).\n - Any errors or edge cases in pointer arithmetic, offset calculation, or buffer bounds risk out-of-bounds (OOB) memory reads/writes or corrupted metadata, which are detected by KASAN and FORTIFY_SOURCE. Locking patterns in ext4_dirdata_set_lufid are monitored by LOCKDEP.\n\nBecause the changes do not introduce uninitialized memory reads or information leak risks that would be caught uniquely by KMSAN, a dedicated KMSAN fuzzing session is not warranted.",
"NeedsKMSAN": false
}Instruction: You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.
CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
a comprehensive suite of debugging tools and sanitizers, including KASAN
(out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
(locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
or page allocations) and kernel-to-user memory info-leaks.
Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.
Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.
Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
attributes, ioctl output arguments, socket options, or BPF buffers) where fields
or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
uninitialized bytes of existing buffers.
Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
dereferences, locking deadlocks, or use-after-free bugs (these are already caught
by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.
Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
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: Target architecture: amd64
For your convenience, here is the diff of the changes:
commit 5398d1939b0ee1e5c282614b7866cad1b15130d2
Author: syz-cluster <triage@syzkaller.com>
Date: Fri Oct 30 02:40:18 2026 +0000
syz-cluster: applied patch under review
diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
index 8d7b81e6948e72..dca6796d5fe271 100644
--- a/fs/ext4/dir.c
+++ b/fs/ext4/dir.c
@@ -89,16 +89,15 @@ int __ext4_check_dir_entry(const char *function, unsigned int line,
bool fake = is_fake_dir_entry(de);
bool has_csum = ext4_has_feature_metadata_csum(dir->i_sb);
- if (unlikely(rlen < ext4_dir_rec_len(1, fake ? NULL : dir)))
+ if (unlikely(rlen < ext4_dirent_rec_len(1, fake ? NULL : dir)))
error_msg = "rec_len is smaller than minimal";
else if (unlikely(rlen % 4 != 0))
error_msg = "rec_len % 4 != 0";
- else if (unlikely(rlen < ext4_dir_rec_len(de->name_len,
- fake ? NULL : dir)))
- error_msg = "rec_len is too small for name_len";
else if (unlikely(next_offset > size))
error_msg = "directory entry overrun";
- else if (unlikely(next_offset > size - ext4_dir_rec_len(1,
+ else if (unlikely(rlen < ext4_dir_entry_len(de, size, fake ? NULL : dir)))
+ error_msg = "rec_len is too small for name_len";
+ else if (unlikely(next_offset > size - ext4_dirent_rec_len(1,
has_csum ? NULL : dir) &&
next_offset != size))
error_msg = "directory entry too close to block end";
@@ -238,7 +237,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
if (!inode_eq_iversion(inode, info->cookie)) {
for (i = 0;
i <= sb->s_blocksize -
- ext4_dir_rec_len(1, has_csum ? NULL : inode) &&
+ ext4_dirent_rec_len(1, has_csum ? NULL : inode) &&
i < offset;) {
de = (struct ext4_dir_entry_2 *)
(bh->b_data + i);
@@ -249,7 +248,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
* failure will be detected in the
* dirent test below. */
if (ext4_rec_len_from_disk(de->rec_len,
- sb->s_blocksize) < ext4_dir_rec_len(1,
+ sb->s_blocksize) < ext4_dirent_rec_len(1,
inode))
break;
i += ext4_rec_len_from_disk(de->rec_len,
@@ -263,7 +262,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
if (unlikely(offset < sb->s_blocksize &&
offset > sb->s_blocksize -
- ext4_dir_rec_len(1, has_csum ? NULL : inode))) {
+ ext4_dirent_rec_len(1, has_csum ? NULL : inode))) {
EXT4_ERROR_FILE(file, bh->b_blocknr,
"bad entry in directory: %s - offset=%u, size=%lu",
"directory entry too close to block end",
@@ -303,8 +302,19 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
u32 minor_hash;
if (IS_CASEFOLDED(inode)) {
- hash = EXT4_DIRENT_HASH(de);
- minor_hash = EXT4_DIRENT_MINOR_HASH(de);
+ if (ext4_has_feature_dirdata(sb) &&
+ (de->file_type &
+ EXT4_DIRENT_CFHASH)) {
+ struct dx_hash_info hi = {};
+ ext4_dirdata_get(de, inode,
+ sb->s_blocksize,
+ NULL, &hi);
+ hash = hi.hash;
+ minor_hash = hi.minor_hash;
+ } else {
+ hash = EXT4_DIRENT_HASH(de);
+ minor_hash = EXT4_DIRENT_MINOR_HASH(de);
+ }
} else {
hash = 0;
minor_hash = 0;
@@ -484,27 +494,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 8d3af062a83068..87c55276d67ab4 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -2369,6 +2369,7 @@ EXT4_FEATURE_INCOMPAT_FUNCS(casefold, CASEFOLD)
EXT4_FEATURE_INCOMPAT_FLEX_BG| \
EXT4_FEATURE_INCOMPAT_EA_INODE| \
EXT4_FEATURE_INCOMPAT_MMP | \
+ EXT4_FEATURE_INCOMPAT_DIRDATA | \
EXT4_FEATURE_INCOMPAT_INLINE_DATA | \
EXT4_FEATURE_INCOMPAT_ENCRYPT | \
EXT4_FEATURE_INCOMPAT_CASEFOLD | \
@@ -2507,10 +2508,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 */
@@ -2591,6 +2590,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
@@ -2608,13 +2662,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);
}
@@ -2702,6 +2769,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)
@@ -3013,18 +3082,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) &&
@@ -3038,12 +3116,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);
@@ -3279,10 +3359,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,
@@ -3894,6 +3983,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] = {
@@ -4103,6 +4196,122 @@ 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;
+}
+
+/*
+ * Advance to the next dirdata record header starting from @ddh.
+ *
+ * Uses READ_ONCE() on ddh_length to prevent the compiler from re-fetching this
+ * on-disk field and to guard against a concurrent write racing with any bounds
+ * check the caller performs on the returned pointer.
+ */
+static inline struct ext4_dirent_data_header *
+ext4_dirdata_next(struct ext4_dirent_data_header *ddh)
+{
+ return (struct ext4_dirent_data_header *)
+ ((char *)ddh + READ_ONCE(ddh->ddh_length));
+}
+
+/*
+ * ext4_dirent_get_data_len() - Compute the total dirdata length for an entry.
+ * @de: directory entry
+ * @rec_len: the record length of the directory entry (decoded)
+ *
+ * Computes the length of optional data stored after the filename (and its
+ * implicit NUL terminator). Each extension is indicated by a bit in the
+ * high 4 bits of de->file_type; the first byte of each extension is its
+ * length (including that length byte itself).
+ *
+ * Returns 0 for tail entries and for entries with no dirdata.
+ */
+static inline int ext4_dirent_get_data_len(struct ext4_dir_entry_2 *de,
+ unsigned int rec_len)
+{
+ __u8 extra_data_flags;
+ struct ext4_dirent_data_header *ddh;
+ int dlen = 0;
+ unsigned int offset;
+
+ if (ext4_dir_entry_is_tail(de))
+ return 0;
+
+ extra_data_flags = (de->file_type & ~EXT4_FT_MASK) >> 4;
+ /* offset from start of entry to after filename + NUL */
+ offset = EXT4_BASE_DIR_LEN + de->name_len + 1;
+
+ /* bounds check: ensure we start reading within the entry */
+ if (offset >= rec_len)
+ return 0;
+
+ ddh = (struct ext4_dirent_data_header *)((char *)de + offset);
+
+ while (extra_data_flags) {
+ if (extra_data_flags & 1) {
+ /* bounds check before reading ddh_length */
+ if (offset + sizeof(*ddh) > rec_len)
+ return 0;
+
+ /* Use READ_ONCE to prevent the compiler from re-fetching
+ * this disk field and to guard against a concurrent
+ * write racing with our bounds check below (TOCTOU). */
+ {
+ u8 ddh_len = READ_ONCE(ddh->ddh_length);
+
+ if (ddh_len == 0 || ddh_len > rec_len - offset)
+ return 0;
+ dlen += ddh_len + (dlen == 0);
+ offset += ddh_len;
+ /* advance using the already-validated local copy
+ * to avoid re-fetching ddh->ddh_length */
+ ddh = (struct ext4_dirent_data_header *)
+ ((char *)ddh + ddh_len);
+ }
+ }
+ extra_data_flags >>= 1;
+ }
+ return dlen;
+}
+
+/*
+ * 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;
extern const struct iomap_ops ext4_iomap_buffered_write_ops;
diff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c
index 0cac890cf37029..876a863d6c0198 100644
--- a/fs/ext4/fast_commit.c
+++ b/fs/ext4/fast_commit.c
@@ -2686,6 +2686,7 @@ static const char * const fc_ineligible_reasons[] = {
[EXT4_FC_REASON_MIGRATE] = "Inode format migration",
[EXT4_FC_REASON_VERITY] = "fs-verity enable",
[EXT4_FC_REASON_MOVE_EXT] = "Move extents",
+ [EXT4_FC_REASON_DIRDATA] = "Dirdata LUFID (not recorded by fast-commit)",
};
int ext4_fc_info_show(struct seq_file *seq, void *v)
diff --git a/fs/ext4/fast_commit.h b/fs/ext4/fast_commit.h
index 2f77a37fb10180..899faeb4e6e064 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 ceee69a66482bc..76b1490490f6f5 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -982,11 +982,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;
@@ -995,7 +1004,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);
@@ -1064,7 +1075,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,
@@ -1273,6 +1284,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;
@@ -1318,7 +1330,7 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
fake.name_len = 1;
memcpy(fake.name, ".", 2);
fake.rec_len = ext4_rec_len_to_disk(
- ext4_dir_rec_len(fake.name_len, NULL),
+ ext4_dirent_rec_len(fake.name_len, NULL),
inline_size);
ext4_set_de_type(inode->i_sb, &fake, S_IFDIR);
de = &fake;
@@ -1328,30 +1340,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) ||
@@ -1363,7 +1391,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;
@@ -1436,8 +1465,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,
/* for other entry, the real offset in
* the buf has to be tuned accordingly.
*/
- if (i + ext4_dir_rec_len(1, NULL) > extra_size)
+ if (i + ext4_dirent_rec_len(1, NULL) > extra_size)
break;
de = (struct ext4_dir_entry_2 *)
(dir_buf + i - extra_offset);
@@ -1474,7 +1503,7 @@ int ext4_read_inline_dir(struct file *file,
* failure will be detected in the
* dirent test below. */
if (ext4_rec_len_from_disk(de->rec_len, extra_size)
- < ext4_dir_rec_len(1, NULL))
+ < ext4_dirent_rec_len(1, NULL))
break;
i += ext4_rec_len_from_disk(de->rec_len,
extra_size);
@@ -1504,7 +1533,7 @@ int ext4_read_inline_dir(struct file *file,
* kmalloc(inline_size) buffer. Make sure its header fits before
* ext4_check_dir_entry() dereferences de->rec_len.
*/
- if (ctx->pos + ext4_dir_rec_len(1, NULL) > extra_size)
+ if (ctx->pos + ext4_dirent_rec_len(1, NULL) > extra_size)
goto out;
de = (struct ext4_dir_entry_2 *)
(dir_buf + ctx->pos - extra_offset);
diff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c
index 1f6d7afb62ec79..baf6ef48cbf308 100644
--- a/fs/ext4/ioctl.c
+++ b/fs/ext4/ioctl.c
@@ -1536,6 +1536,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);
@@ -1922,6 +2004,8 @@ static long __ext4_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
(void __user *)arg);
case EXT4_IOC_SET_TUNE_SB_PARAM:
return ext4_ioctl_set_tune_sb(filp, (void __user *)arg);
+ case EXT4_IOC_SET_LUFID:
+ return ext4_ioctl_set_lufid(filp, arg);
default:
return -ENOTTY;
}
@@ -2001,6 +2085,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 231a274b814ecb..4ab1f14442b090 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,38 @@ 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) +
+ sizeof(struct dx_entry) > 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 +612,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 +634,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 +747,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 +831,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 +843,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 +875,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 +891,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 +910,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 +995,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 +1004,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 +1127,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 +1152,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 +1190,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 +1206,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 +1286,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 +1298,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 +1336,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 +1359,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 +1586,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 +1701,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 +1730,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
@@ -1465,11 +1783,18 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
dlimit = search_buf + buf_size;
while ((char *) de < dlimit - EXT4_BASE_DIR_LEN) {
/* this code is executed quadratically often */
- /* do minimal checking `by hand' */
+ /* Decode rec_len before ext4_match() so that ext4_dirdata_get()
+ * inside ext4_match() never uses an unvalidated rec_len as a
+ * read bound. A crafted entry with rec_len > buf_size would
+ * let ext4_dirdata_get() read past the block buffer. */
+ de_len = ext4_rec_len_from_disk(de->rec_len,
+ dir->i_sb->s_blocksize);
+ if (de_len <= 0 || (char *)de + de_len > dlimit)
+ return -EFSCORRUPTED;
if (de->name + de->name_len <= dlimit &&
(!ext4_hash_in_dirent(dir) ||
- (char *)de + ext4_dir_rec_len(de->name_len, dir) <= dlimit) &&
- ext4_match(dir, fname, de)) {
+ (char *)de + ext4_dirent_rec_len(de->name_len, dir) <= dlimit) &&
+ 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,
@@ -1478,11 +1803,6 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
*res_dir = de;
return 1;
}
- /* prevent looping on a bad block */
- de_len = ext4_rec_len_from_disk(de->rec_len,
- dir->i_sb->s_blocksize);
- if (de_len <= 0)
- return -EFSCORRUPTED;
offset += de_len;
de = (struct ext4_dir_entry_2 *) ((char *) de + de_len);
}
@@ -1490,7 +1810,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;
@@ -1621,7 +1941,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 "
@@ -1755,7 +2075,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;
}
@@ -1852,7 +2172,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 =
@@ -1885,7 +2205,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);
@@ -2037,10 +2357,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;
@@ -2051,9 +2376,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;
@@ -2067,16 +2392,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 =
@@ -2090,13 +2412,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);
}
/*
@@ -2114,14 +2430,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;
}
@@ -2134,7 +2464,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
@@ -2158,44 +2492,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:
@@ -2213,16 +2541,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))
@@ -2239,17 +2566,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);
@@ -2280,24 +2610,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;
@@ -2340,7 +2678,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;
}
@@ -2357,7 +2695,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;
@@ -2382,6 +2721,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)
@@ -2466,7 +2812,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);
}
/*
@@ -2607,7 +2954,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));
@@ -2615,11 +2962,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);
@@ -2643,7 +2996,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
*/
@@ -2929,14 +3282,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);
@@ -2947,7 +3301,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;
@@ -2956,7 +3310,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);
}
@@ -2968,8 +3322,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;
@@ -2988,6 +3343,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);
@@ -3079,8 +3440,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;
}
@@ -3473,7 +3834,8 @@ int __ext4_link(struct inode *dir, struct inode *inode,
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
@@ -3641,8 +4003,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;
@@ -3652,8 +4052,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);
@@ -3690,7 +4168,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);
}
@@ -3789,6 +4267,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.
@@ -3815,8 +4354,10 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
int force_reread;
int retval;
struct inode *whiteout = NULL;
+ struct ext4_dentry_param *edp = NULL;
int credits;
u8 old_file_type;
+ struct ext4_lufid_snap old_snap = {};
if (new.inode && new.inode->i_nlink == 0) {
EXT4_ERROR_INODE(new.inode,
@@ -3922,29 +4463,64 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
force_reread = (new.dir->i_ino == old.dir->i_ino &&
ext4_test_inode_flag(new.dir, EXT4_INODE_INLINE_DATA));
+ if (ext4_has_feature_dirdata(old.dir->i_sb)) {
+ /* Snapshot before whiteout's ext4_setent() clears the LUFID
+ * flag in old.de; the snapshot is also needed for the !new.bh
+ * path below where we must carry the LUFID to the new entry. */
+ retval = ext4_lufid_snapshot(old.de,
+ old.dir->i_sb->s_blocksize,
+ &old_snap);
+ if (retval)
+ goto end_rename;
+ if (!old_snap.fid)
+ old_file_type &= ~EXT4_DIRENT_LUFID;
+ }
+
+ /* Allocate the LUFID wrapper for ext4_add_entry() before any
+ * destructive changes to the old entry. An OOM failure after
+ * ext4_setent() has already compacted the extensions would trigger
+ * the lossy resetent path; pre-allocating here avoids that. */
+ if (!new.bh && old_snap.fid) {
+ unsigned fid_size = old_snap.fid->df_header.ddh_length;
+
+ edp = kmalloc(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);
+ }
+
if (whiteout) {
/*
* Do this before adding a new entry, so the old entry is sure
* to be still pointing to the valid old entry.
*/
retval = ext4_setent(handle, &old, whiteout->i_ino,
- EXT4_FT_CHRDEV);
+ EXT4_FT_CHRDEV, NULL);
if (retval)
goto end_rename;
retval = ext4_mark_inode_dirty(handle, whiteout);
if (unlikely(retval))
goto end_rename;
-
}
if (!new.bh) {
+ new.dentry->d_fsdata = edp;
retval = ext4_add_entry(handle, new.dentry, old.inode);
+ new.dentry->d_fsdata = NULL;
if (retval)
goto end_rename;
} else {
retval = ext4_setent(handle, &new,
- old.inode->i_ino, old_file_type);
+ old.inode->i_ino, old_file_type, old_snap.fid);
if (retval)
goto end_rename;
+ /* ext4_setent() writes the LUFID in-place without going through
+ * __ext4_add_entry(), so mark ineligible here if needed. */
+ if (old_snap.fid)
+ ext4_fc_mark_ineligible(old.dir->i_sb,
+ EXT4_FC_REASON_DIRDATA, handle);
}
if (force_reread)
force_reread = !ext4_test_inode_flag(new.dir,
@@ -4029,8 +4605,25 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
retval = 0;
end_rename:
+ kfree(edp);
+ ext4_lufid_snap_release(&old_snap);
if (whiteout) {
if (retval) {
+ /* Known limitation: if the old entry carried both LUFID
+ * and CFHASH, ext4_setent_compact_exts() slid CFHASH
+ * into the LUFID slot above, and old_snap was released
+ * before this point, so resetent can only restore the
+ * LUFID flag — not the original byte layout. This
+ * leaves the entry with CFHASH data at the LUFID offset
+ * and garbage at the CFHASH offset until the next
+ * SET_LUFID ioctl re-writes it. The scenario requires
+ * RENAME_WHITEOUT on a casefolded+encrypted dirdata
+ * directory whose entry has a LUFID, followed by a
+ * journal IO error in the single ext4_mark_inode_dirty()
+ * call that follows — an extremely unlikely combination
+ * in practice. (OOM during the LUFID wrapper allocation
+ * cannot trigger this path because that allocation is
+ * hoisted before ext4_setent() modifies the entry.) */
ext4_resetent(handle, &old,
old.inode->i_ino, old_file_type);
drop_nlink(whiteout);
@@ -4065,8 +4658,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,
@@ -4145,12 +4739,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;
@@ -4183,6 +4798,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);
@@ -4219,6 +4836,266 @@ 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 ext4_lufid_snap old_snap = {};
+ struct qstr d_name;
+ /* on-disk name snapshot for non-encrypted casefolded directories: the
+ * on-disk case may differ from what the caller supplied. */
+ char ondisk_name[EXT4_NAME_LEN];
+ struct qstr real_name = {};
+ /* name to pass to ext4_add_entry() for the re-add and rollback */
+ const struct qstr *add_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 calls ext4_delete_entry() on the parent directory, which
+ * bypasses the VFS may_delete() path. inode_permission(MAY_WRITE)
+ * above blocks IS_IMMUTABLE(dir) but does NOT block IS_APPEND(dir):
+ * append-only directories allow new entries but forbid deletion, and
+ * that restriction lives only in may_delete(), not in
+ * inode_permission(). Enforce it explicitly here. */
+ if (IS_APPEND(dir)) {
+ err = -EPERM;
+ goto out_iput;
+ }
+
+ /* The ioctl deletes and re-creates the directory entry, which
+ * effectively modifies the target file's namespace presence.
+ * Refuse if the target itself is immutable or append-only. */
+ if (IS_IMMUTABLE(inode) || IS_APPEND(inode)) {
+ err = -EPERM;
+ goto out_iput;
+ }
+
+ /* 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;
+ }
+
+ /* Choose the name to use for re-adding the entry after deletion.
+ *
+ * For non-encrypted casefolded directories ext4_find_entry() matches
+ * case-insensitively, so de->name may differ in case from the
+ * caller-supplied filename. Re-adding with the caller's case would
+ * silently rename the entry, so preserve the on-disk bytes.
+ *
+ * For encrypted directories (with or without casefold) de->name holds
+ * the raw ciphertext. ext4_fname_setup_filename() deterministically
+ * re-encrypts d_name (the plaintext) to the identical ciphertext, so
+ * using d_name for the re-add is correct. Passing the raw ciphertext
+ * bytes would cause double-encryption and make the entry unreachable. */
+ if (IS_CASEFOLDED(dir) && !IS_ENCRYPTED(dir)) {
+ memcpy(ondisk_name, de->name, de->name_len);
+ real_name.name = ondisk_name;
+ real_name.len = de->name_len;
+ add_name = &real_name;
+ } else {
+ add_name = &d_name;
+ }
+
+ /* Snapshot the old LUFID before deleting so we can restore it if the
+ * re-add fails. Without this the rollback path re-adds the entry
+ * without any LUFID, silently orphaning the old FID-to-path mapping. */
+ err = ext4_lufid_snapshot(de, sb->s_blocksize, &old_snap);
+ if (err)
+ 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 = *add_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. Restore the
+ * original LUFID too: without it the entry is re-added clean
+ * and the old FID-to-path mapping is silently lost. */
+ struct dentry parent_dentry = { .d_inode = dir };
+ struct dentry orig_dentry = {
+ .d_name = *add_name,
+ .d_parent = &parent_dentry,
+ .d_inode = inode,
+ };
+ int rollback_err;
+
+ if (old_snap.fid) {
+ unsigned int fid_size = old_snap.fid->df_header.ddh_length;
+ struct ext4_dentry_param *old_edp =
+ kmalloc(sizeof(*old_edp) + fid_size, GFP_NOFS);
+
+ if (old_edp) {
+ old_edp->edp_magic = EXT4_LUFID_MAGIC;
+ memcpy(old_edp->edp_dfid, old_snap.fid, fid_size);
+ orig_dentry.d_fsdata = old_edp;
+ }
+ }
+
+ rollback_err = ext4_add_entry(handle, &orig_dentry, inode);
+ kfree(orig_dentry.d_fsdata);
+
+ 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,
+ (unsigned long long)inode->i_ino);
+ goto out_journal;
+ }
+
+ inode_set_ctime_current(dir);
+ inode_inc_iversion(dir);
+ ext4_mark_inode_dirty(handle, dir);
+
+out_brelse:
+ brelse(bh);
+out_journal:
+ ext4_lufid_snap_release(&old_snap);
+ 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 923b375e017faa..80074fb15ee9b7 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 9c683991c32f96..9134fe64947a93 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
*/
|
| 17/3 | 2026/10/30 02:41 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 41825 output: 43 thoughts: 2700 Thoughts: **KMSAN Evaluation: Ext4 LUFID Feature Implementation**
Alright, let's break this down. The task is to determine if this patch, introducing the `EXT4_FEATURE_INCOMPAT_DIRDATA` feature for LUFID support, necessitates a KMSAN session. My immediate thought is, "We need to meticulously check for uninitialized memory usage, particularly if anything is exposed to user space." KASAN, LOCKDEP, and the standard debuggers are already on the job for the usual suspects, so KMSAN only comes into play if *uninitialized* memory is a concern.
The patch implements `EXT4_IOC_SET_LUFID`, storing casefolded hashes (`CFHASH`) within `dirdata`. This ioctl is the prime suspect. I've zeroed in on the `ext4_ioctl_set_lufid` function in `fs/ext4/ioctl.c`. The core concern is the `edp` structure:
```c
struct {
__u32 edp_magic;
struct ext4_dirent_data_header df_header;
char df_fid[255];
} edp;
```
This structure receives data from userspace through `copy_from_user` into the `lufid_args` struct and then we have:
```c
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);
```
Here's the rub: `edp.df_fid` is a 255-byte array, but only `lufid_args.esl_data_len` bytes are copied from user space. The *rest* of the array remains uninitialized. That's a textbook KMSAN trigger if those uninitialized bytes are later *read*.
The code then passes a pointer to `edp` to `ext4_dirdata_set_lufid`. I need to track `edp`’s use within that function and beyond. Following the trail through `ext4_dentry_get_fid`, `ext4_add_entry`, and finally `ext4_dirdata_set`, it looks like `dfid`, which is a pointer to the structure `edp`, contains an offset to `df_header`, and `memcpy` is used:
```c
memcpy((char *)de + EXT4_BASE_DIR_LEN + data_offset, dfid, dlen);
```
where `dlen` is calculated from `edp`. The critical part is checking the length of the `memcpy` to verify the amount of bytes being copied and from where. In our case the length is `sizeof(df_header) + esl_data_len`. So, the entire data passed to `memcpy` has been initialized: `sizeof(df_header)` from a value, and then `lufid_args.esl_data_len` bytes copied via `memcpy` from user data. So the `memcpy` is working on initialized data!
I need to confirm if there is data copied from the kernel to the user space: I must check calls to `copy_to_user`, `put_user`, `readdir`, and `getdents`. I must check if these functions can read from uninitialized data in `edp`.
Therefore, based on my analysis of the code path, it looks like there are no uninitialized memory reads that would justify running a KMSAN session, assuming this is *all* the code touching this new feature. I also need to verify that information by checking the other files.
|
| 18/3 | 2026/10/30 02:41 | tool | grepper |
0mArgs:{
"Expression": "dir_emit",
"PathPrefixes": [
"fs/ext4/dir.c",
"fs/ext4/inline.c"
]
}Results: {
"Output": "fs/ext4/dir.c=129=static int ext4_readdir(struct file *file, struct dir_context *ctx)\n--\nfs/ext4/dir.c-290-\t\t\t\tif (!IS_ENCRYPTED(inode)) {\nfs/ext4/dir.c:291:\t\t\t\t\tif (!dir_emit(ctx, de-\u003ename,\nfs/ext4/dir.c-292-\t\t\t\t\t de-\u003ename_len,\n--\nfs/ext4/dir.c-329-\t\t\t\t\t\tgoto errout;\nfs/ext4/dir.c:330:\t\t\t\t\tif (!dir_emit(ctx,\nfs/ext4/dir.c-331-\t\t\t\t\t de_name.name, de_name.len,\n--\nfs/ext4/dir.c=573=static int call_filldir(struct file *file, struct dir_context *ctx,\n--\nfs/ext4/dir.c-587-\twhile (fname) {\nfs/ext4/dir.c:588:\t\tif (!dir_emit(ctx, fname-\u003ename,\nfs/ext4/dir.c-589-\t\t\t\tfname-\u003ename_len,\n--\nfs/ext4/inline.c=1417=int ext4_read_inline_dir(struct file *file,\n--\nfs/ext4/inline.c-1517-\t\tif (ctx-\u003epos == 0) {\nfs/ext4/inline.c:1518:\t\t\tif (!dir_emit(ctx, \".\", 1, inode-\u003ei_ino, DT_DIR))\nfs/ext4/inline.c-1519-\t\t\t\tgoto out;\n--\nfs/ext4/inline.c-1524-\t\tif (ctx-\u003epos == dotdot_offset) {\nfs/ext4/inline.c:1525:\t\t\tif (!dir_emit(ctx, \"..\", 2, parent_ino, DT_DIR))\nfs/ext4/inline.c-1526-\t\t\t\tgoto out;\n--\nfs/ext4/inline.c-1543-\t\tif (le32_to_cpu(de-\u003einode)) {\nfs/ext4/inline.c:1544:\t\t\tif (!dir_emit(ctx, de-\u003ename, de-\u003ename_len,\nfs/ext4/inline.c-1545-\t\t\t\t le32_to_cpu(de-\u003einode),\n"
} |
| 19/3 | 2026/10/30 02:41 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 46011 output: 36 thoughts: 44 Thoughts: Okay, let's dive into this. Here's how I'd approach this task. **Investigating ext4_readdir Implementation** Alright, so I need to refresh my memory on the specifics of how `ext4_readdir` in `fs/ext4/dir.c` actually manages directory entries and filenames. I'm thinking, based on past experience, that lines 280-340 will be a good starting point. This section likely contains the core logic for iterating through the directory blocks, interpreting the directory entry (de) structures, and handling the filename (fname) extraction or copying. I'll need to pay close attention to how these two elements are linked and processed, looking for any potential discrepancies or inefficiencies in this area. Perhaps there's an interesting bitfield manipulation I've been missing in the header or in the documentation. |
| 20/3 | 2026/10/30 02:41 | tool | read-file |
0mArgs:{
"File": "fs/ext4/dir.c",
"FirstLine": 285,
"LineCount": 60
}Results: {
"Contents": " 285:\t\t\t\t\tbreak;\n 286:\t\t\t\t}\n 287:\t\t\t\toffset += ext4_rec_len_from_disk(de-\u003erec_len,\n 288:\t\t\t\t\t\tsb-\u003es_blocksize);\n 289:\t\t\t\tif (le32_to_cpu(de-\u003einode)) {\n 290:\t\t\t\t\tif (!IS_ENCRYPTED(inode)) {\n 291:\t\t\t\t\t\tif (!dir_emit(ctx, de-\u003ename,\n 292:\t\t\t\t\t\t de-\u003ename_len,\n 293:\t\t\t\t\t\t le32_to_cpu(de-\u003einode),\n 294:\t\t\t\t\t\t get_dtype(sb, de-\u003efile_type)))\n 295:\t\t\t\t\t\t\tgoto done;\n 296:\t\t\t\t\t} else {\n 297:\t\t\t\t\t\tint save_len = fstr.len;\n 298:\t\t\t\t\t\tstruct fscrypt_str de_name =\n 299:\t\t\t\t\t\t\t\tFSTR_INIT(de-\u003ename,\n 300:\t\t\t\t\t\t\t\t\tde-\u003ename_len);\n 301:\t\t\t\t\t\tu32 hash;\n 302:\t\t\t\t\t\tu32 minor_hash;\n 303:\t\n 304:\t\t\t\t\t\tif (IS_CASEFOLDED(inode)) {\n 305:\t\t\t\t\t\t\tif (ext4_has_feature_dirdata(sb) \u0026\u0026\n 306:\t\t\t\t\t\t\t (de-\u003efile_type \u0026\n 307:\t\t\t\t\t\t\t EXT4_DIRENT_CFHASH)) {\n 308:\t\t\t\t\t\t\t\tstruct dx_hash_info hi = {};\n 309:\t\t\t\t\t\t\t\text4_dirdata_get(de, inode,\n 310:\t\t\t\t\t\t\t\t\tsb-\u003es_blocksize,\n 311:\t\t\t\t\t\t\t\t\tNULL, \u0026hi);\n 312:\t\t\t\t\t\t\t\thash = hi.hash;\n 313:\t\t\t\t\t\t\t\tminor_hash = hi.minor_hash;\n 314:\t\t\t\t\t\t\t} else {\n 315:\t\t\t\t\t\t\t\thash = EXT4_DIRENT_HASH(de);\n 316:\t\t\t\t\t\t\t\tminor_hash = EXT4_DIRENT_MINOR_HASH(de);\n 317:\t\t\t\t\t\t\t}\n 318:\t\t\t\t\t\t} else {\n 319:\t\t\t\t\t\t\thash = 0;\n 320:\t\t\t\t\t\t\tminor_hash = 0;\n 321:\t\t\t\t\t\t}\n 322:\t\n 323:\t\t\t\t\t\t/* Directory is encrypted */\n 324:\t\t\t\t\t\terr = fscrypt_fname_disk_to_usr(inode,\n 325:\t\t\t\t\t\t\thash, minor_hash, \u0026de_name, \u0026fstr);\n 326:\t\t\t\t\t\tde_name = fstr;\n 327:\t\t\t\t\t\tfstr.len = save_len;\n 328:\t\t\t\t\t\tif (err)\n 329:\t\t\t\t\t\t\tgoto errout;\n 330:\t\t\t\t\t\tif (!dir_emit(ctx,\n 331:\t\t\t\t\t\t de_name.name, de_name.len,\n 332:\t\t\t\t\t\t le32_to_cpu(de-\u003einode),\n 333:\t\t\t\t\t\t get_dtype(sb, de-\u003efile_type)))\n 334:\t\t\t\t\t\t\tgoto done;\n 335:\t\t\t\t\t}\n 336:\t\t\t\t}\n 337:\t\t\t\tctx-\u003epos += ext4_rec_len_from_disk(de-\u003erec_len,\n 338:\t\t\t\t\t\t\tsb-\u003es_blocksize);\n 339:\t\t\t}\n 340:\tnext_block:\n 341:\t\t\tif ((ctx-\u003epos \u003c inode-\u003ei_size) \u0026\u0026 !dir_relax_shared(inode))\n 342:\t\t\t\tgoto done;\n 343:\t\t\tbrelse(bh);\n 344:\t\t\tbh = NULL;\n"
} |
| 21/3 | 2026/10/30 02:41 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 47181 output: 29 thoughts: 1934 Thoughts: **Analyzing Potential Buffer Overflows in ext4 Htree Implementation**
Okay, let's break this down. First, I'm noticing a potential issue in how `ext4_htree_store_dirent` handles extended directory entry data. I see the function masks the filetype in `ext4_readdir`, but I need to focus on where that masked filetype is used.
I'm zeroing in on this section in `ext4_htree_store_dirent`: the condition to check if dirdata is present and the size calculation seems like a potential hotspot:
```c
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->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 */);
```
The key line to investigate here is the `unsafe_memcpy`. The concern arises from copying `extra_data` bytes from `dirent->name + dirent->name_len` to `new_fn->name + ent_name->len`. The comment mentions that the source `extra_data` might be past the `name_len` boundary. That's a red flag!
I'm thinking, "Where does `dirent->name + dirent->name_len` actually point?". The on-disk dirent layout dictates that `de->name` is followed by its length, `dirent->name_len`. Right after that, the additional directory data flags (dirdata) will be appended, as specified by ext4.h. Therefore, `dirent->name + dirent->name_len` should point to the first byte of that extra data. I also see that `extra_data` is calculated by the function `ext4_dirent_get_data_len()`, which now needs to be examined.
Peering into `ext4_dirent_get_data_len`, I see:
```c
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;
...
```
The important observation is that `offset` is calculated as `EXT4_BASE_DIR_LEN + de->name_len + 1`. This is where the file name and the 1 byte null terminator resides. So, that `offset` tells me the starting point of the additional dirdata. Also, `ext4_dirent_get_data_len` adds the ddh lengths, which are part of the dirdata. So I know this calculation includes all those extension headers.
And here I see the critical point. I remember that the `extra_data` includes a byte for the null terminator! I see it stated here: `/* extra_data contains +1 byte for NULL */` and also in the code:
```c
extra_data = ext4_dirent_get_data_len(dirent, rec_len);
```
The next question: Does this memcpy read past the bounds of the dirent?
|
| 22/3 | 2026/10/30 02:42 | tool | grepper |
0mArgs:{
"Expression": "struct fname ",
"PathPrefixes": [
"fs/ext4/"
]
}Results: {
"Output": "fs/ext4/dir.c=423=static loff_t ext4_dir_llseek(struct file *file, loff_t offset, int whence)\n--\nfs/ext4/dir.c-442- */\nfs/ext4/dir.c:443:struct fname {\nfs/ext4/dir.c-444-\t__u32\t\thash;\n--\nfs/ext4/dir.c=458=static void free_rb_tree_fname(struct rb_root *root)\nfs/ext4/dir.c-459-{\nfs/ext4/dir.c:460:\tstruct fname *fname, *next;\nfs/ext4/dir.c-461-\n--\nfs/ext4/dir.c-463-\t\twhile (fname) {\nfs/ext4/dir.c:464:\t\t\tstruct fname *old = fname;\nfs/ext4/dir.c-465-\t\t\tfname = fname-\u003enext;\n--\nfs/ext4/dir.c=496=int ext4_htree_store_dirent(struct file *dir_file, __u32 hash,\n--\nfs/ext4/dir.c-501-\tstruct rb_node **p, *parent = NULL;\nfs/ext4/dir.c:502:\tstruct fname *fname, *new_fn;\nfs/ext4/dir.c-503-\tstruct dir_private_info *info;\n--\nfs/ext4/dir.c=573=static int call_filldir(struct file *file, struct dir_context *ctx,\nfs/ext4/dir.c:574:\t\t\tstruct fname *fname)\nfs/ext4/dir.c-575-{\n--\nfs/ext4/dir.c=600=static int ext4_dx_readdir(struct file *file, struct dir_context *ctx)\n--\nfs/ext4/dir.c-603-\tstruct inode *inode = file_inode(file);\nfs/ext4/dir.c:604:\tstruct fname *fname;\nfs/ext4/dir.c-605-\tint ret = 0;\n"
} |
| 23/3 | 2026/10/30 02:42 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 50721 output: 36 thoughts: 21 Thoughts: Okay, let's dive into this. Here's my take on understanding this code: **Analyzing `call_filldir` in `fs/ext4/dir.c`** Alright, I need to get a handle on how the directory entry filling function, `call_filldir`, works within the ext4 filesystem. Let's start by digging into `fs/ext4/dir.c`. I expect to find the core implementation of how directory entries are constructed and populated in this file. I'm looking for the nitty-gritty details of how filenames, inode numbers, and entry types are packaged and passed to the caller. Understanding this function is crucial to grasping how the file system interacts with user-space directory listing operations, and how it handles potential buffer overflows or security issues during directory traversal. I'll be particularly interested in the parameters it takes and what it does with them. I'll pay close attention to the way the function handles potential errors or edge cases, such as when directory entries are fragmented or corrupted. I suspect there will be interactions with lower-level block management functions and memory allocation, so I will watch for that interaction. |
| 24/3 | 2026/10/30 02:42 | tool | read-file |
0mArgs:{
"File": "fs/ext4/dir.c",
"FirstLine": 570,
"LineCount": 35
}Results: {
"Contents": " 570:\t * for all entries on the fname linked list. (Normally there is only\n 571:\t * one entry on the linked list, unless there are 62 bit hash collisions.)\n 572:\t */\n 573:\tstatic int call_filldir(struct file *file, struct dir_context *ctx,\n 574:\t\t\t\tstruct fname *fname)\n 575:\t{\n 576:\t\tstruct dir_private_info *info = file-\u003eprivate_data;\n 577:\t\tstruct inode *inode = file_inode(file);\n 578:\t\tstruct super_block *sb = inode-\u003ei_sb;\n 579:\t\n 580:\t\tif (!fname) {\n 581:\t\t\text4_msg(sb, KERN_ERR, \"%s:%d: inode #%llu: comm %s: \"\n 582:\t\t\t\t \"called with null fname?!?\", __func__, __LINE__,\n 583:\t\t\t\t inode-\u003ei_ino, current-\u003ecomm);\n 584:\t\t\treturn 0;\n 585:\t\t}\n 586:\t\tctx-\u003epos = hash2pos(file, fname-\u003ehash, fname-\u003eminor_hash);\n 587:\t\twhile (fname) {\n 588:\t\t\tif (!dir_emit(ctx, fname-\u003ename,\n 589:\t\t\t\t\tfname-\u003ename_len,\n 590:\t\t\t\t\tfname-\u003einode,\n 591:\t\t\t\t\tget_dtype(sb, fname-\u003efile_type))) {\n 592:\t\t\t\tinfo-\u003eextra_fname = fname;\n 593:\t\t\t\treturn 1;\n 594:\t\t\t}\n 595:\t\t\tfname = fname-\u003enext;\n 596:\t\t}\n 597:\t\treturn 0;\n 598:\t}\n 599:\t\n 600:\tstatic int ext4_dx_readdir(struct file *file, struct dir_context *ctx)\n 601:\t{\n 602:\t\tstruct dir_private_info *info = file-\u003eprivate_data;\n 603:\t\tstruct inode *inode = file_inode(file);\n 604:\t\tstruct fname *fname;\n"
} |
| 25/3 | 2026/10/30 02:42 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 51576 output: 37 thoughts: 451 Thoughts: **Analyzing the `ext4` Directory Entry Handling**
Okay, let's break this down. In `call_filldir`, the crucial line is `dir_emit(ctx, fname->name, fname->name_len, fname->inode, get_dtype(sb, fname->file_type))`. I see that `fname->name` is being emitted for `fname->name_len` bytes. And, looking closer, `fname->name_len` is directly linked to `ent_name->len`. I'm getting that `fname->name` contains the initialized name, thanks to that `memcpy(new_fn->name, ent_name->name, ent_name->len);` call. The type byte is cleanly extracted using `get_dtype(sb, fname->file_type)`. Straightforward so far.
Now, I need to focus on `ext4_dirdata_get`. I want to understand how this function initializes its output or, more importantly, how its callers set up its inputs. Let's see...
Looking at the callers, starting with `ext4_readdir`, I see that a `struct dx_hash_info hi` is zero-initialized: `struct dx_hash_info hi = {};`. So the values I care about, `hash` and `minor_hash`, will be set correctly. Okay, good.
The second caller is inline.c. In that inline function, I see `ext4_dirdata_get(de, dir, inline_size, NULL, hinfo)`. Hmm, where did `hinfo` come from? I need to check where `hinfo` is defined. This leads me to `ext4_inlinedir_to_tree`. Let me scroll to line 1300 of `fs/ext4/inline.c` to see where `hinfo` is initialized and used.
|
| 26/3 | 2026/10/30 02:42 | tool | read-file |
0mArgs:{
"File": "fs/ext4/inline.c",
"FirstLine": 1290,
"LineCount": 40
}Results: {
"Contents": "1290:\t\tint ret, inline_size = 0;\n1291:\t\tstruct ext4_iloc iloc;\n1292:\t\tvoid *dir_buf = NULL;\n1293:\t\tstruct ext4_dir_entry_2 fake;\n1294:\t\tstruct fscrypt_str tmp_str;\n1295:\t\n1296:\t\tret = ext4_get_inode_loc(inode, \u0026iloc);\n1297:\t\tif (ret)\n1298:\t\t\treturn ret;\n1299:\t\n1300:\t\tdown_read(\u0026EXT4_I(inode)-\u003exattr_sem);\n1301:\t\tif (!ext4_has_inline_data(inode)) {\n1302:\t\t\tup_read(\u0026EXT4_I(inode)-\u003exattr_sem);\n1303:\t\t\t*has_inline_data = 0;\n1304:\t\t\tgoto out;\n1305:\t\t}\n1306:\t\n1307:\t\tinline_size = ext4_get_inline_size(inode);\n1308:\t\tdir_buf = kmalloc(inline_size, GFP_NOFS);\n1309:\t\tif (!dir_buf) {\n1310:\t\t\tret = -ENOMEM;\n1311:\t\t\tup_read(\u0026EXT4_I(inode)-\u003exattr_sem);\n1312:\t\t\tgoto out;\n1313:\t\t}\n1314:\t\n1315:\t\tret = ext4_read_inline_data(inode, dir_buf, inline_size, \u0026iloc);\n1316:\t\tup_read(\u0026EXT4_I(inode)-\u003exattr_sem);\n1317:\t\tif (ret \u003c 0)\n1318:\t\t\tgoto out;\n1319:\t\n1320:\t\tpos = 0;\n1321:\t\tparent_ino = le32_to_cpu(((struct ext4_dir_entry_2 *)dir_buf)-\u003einode);\n1322:\t\twhile (pos \u003c inline_size) {\n1323:\t\t\t/*\n1324:\t\t\t * As inlined dir doesn't store any information about '.' and\n1325:\t\t\t * only the inode number of '..' is stored, we have to handle\n1326:\t\t\t * them differently.\n1327:\t\t\t */\n1328:\t\t\tif (pos == 0) {\n1329:\t\t\t\tfake.inode = cpu_to_le32(inode-\u003ei_ino);\n"
} |
| 27/3 | 2026/10/30 02:42 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 53111 output: 31 thoughts: 24 Thoughts: Here's my summary, as I delve into the code: **Navigating the `ext4_inlinedir_to_tree` Function** Okay, so I need to understand where `hinfo` comes from, specifically within the `ext4_inlinedir_to_tree` function. My usual approach is to trace its origin – find its definition. I'm looking for the scope in which `hinfo` is introduced, and how it gets populated. Is it passed in as an argument? Is it a local variable declared within the function? Or, perhaps, is it a global of some kind (though I *highly* doubt that in well-written kernel code). I need to determine the *context* of `hinfo`. This will tell me how the state that it represents gets set. This function likely manipulates directory entries, but without knowing the specifics of `hinfo`, it is impossible to go further. My next step will be to search the codebase to identify how `hinfo` is defined. |
| 28/3 | 2026/10/30 02:42 | tool | grepper |
0mArgs:{
"Expression": "hinfo",
"PathPrefixes": [
"fs/ext4/inline.c"
]
}Results: {
"Output": "fs/ext4/inline.c=1278=int ext4_inlinedir_to_tree(struct file *dir_file,\nfs/ext4/inline.c-1279-\t\t\t struct inode *dir, ext4_lblk_t block,\nfs/ext4/inline.c:1280:\t\t\t struct dx_hash_info *hinfo,\nfs/ext4/inline.c-1281-\t\t\t __u32 start_hash, __u32 start_minor_hash,\n--\nfs/ext4/inline.c-1368-\nfs/ext4/inline.c:1369:\t\tif (!(ext4_dirdata_get(de, dir, inline_size, NULL, hinfo) \u0026\nfs/ext4/inline.c-1370-\t\t\t\t\t\t\tEXT4_DIRENT_CFHASH)) {\n--\nfs/ext4/inline.c-1373-\t\t\t\t * offset. */\nfs/ext4/inline.c:1374:\t\t\t\thinfo-\u003ehash = EXT4_DIRENT_HASH(de);\nfs/ext4/inline.c:1375:\t\t\t\thinfo-\u003eminor_hash = EXT4_DIRENT_MINOR_HASH(de);\nfs/ext4/inline.c-1376-\t\t\t} else {\nfs/ext4/inline.c-1377-\t\t\t\terr = ext4fs_dirhash(dir, de-\u003ename,\nfs/ext4/inline.c:1378:\t\t\t\t\t\t de-\u003ename_len, hinfo);\nfs/ext4/inline.c-1379-\t\t\t\tif (err) {\n--\nfs/ext4/inline.c-1384-\t\t}\nfs/ext4/inline.c:1385:\t\tif ((hinfo-\u003ehash \u003c start_hash) ||\nfs/ext4/inline.c:1386:\t\t ((hinfo-\u003ehash == start_hash) \u0026\u0026\nfs/ext4/inline.c:1387:\t\t (hinfo-\u003eminor_hash \u003c start_minor_hash)))\nfs/ext4/inline.c-1388-\t\t\tcontinue;\n--\nfs/ext4/inline.c-1392-\t\ttmp_str.len = de-\u003ename_len;\nfs/ext4/inline.c:1393:\t\terr = ext4_htree_store_dirent(dir_file, hinfo-\u003ehash,\nfs/ext4/inline.c:1394:\t\t\t\t\t hinfo-\u003eminor_hash, inline_size,\nfs/ext4/inline.c-1395-\t\t\t\t\t de, \u0026tmp_str);\n"
} |
| 29/3 | 2026/10/30 02:42 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 54032 output: 36 thoughts: 29 Thoughts: Here's a summary of my immediate thought process: **Analyzing the `ext4_inlinedir_to_tree` Function** Okay, so I've been given `hinfo` as a pointer, which I assume is some sort of handle or information structure related to the `ext4` filesystem's inlined directory implementation. My next logical step is to understand how this function, `ext4_inlinedir_to_tree`, is being *used*. Specifically, I need to trace back from it. I need to figure out who's calling it. Knowing the callers will tell me the context in which `ext4_inlinedir_to_tree` is used, the data it's likely operating on, and what the expected inputs and outputs are. This helps me understand its purpose within the larger picture of the `ext4` filesystem. I'll need to figure out how to best analyze the call graph, perhaps using `grep`, `cscope`, `ctags`, or my IDE's functionality, depending on the available tools and the size of the codebase. |
| 30/3 | 2026/10/30 02:42 | tool | grepper |
0mArgs:{
"Expression": "ext4_inlinedir_to_tree",
"PathPrefixes": [
"fs/ext4/"
]
}Results: {
"Output": "fs/ext4/ext4.h=3934=extern int ext4_read_inline_dir(struct file *filp,\n--\nfs/ext4/ext4.h-3936-\t\t\t\tint *has_inline_data);\nfs/ext4/ext4.h:3937:extern int ext4_inlinedir_to_tree(struct file *dir_file,\nfs/ext4/ext4.h-3938-\t\t\t\t struct inode *dir, ext4_lblk_t block,\n--\nfs/ext4/inline.c=1209=int ext4_try_add_inline_entry(handle_t *handle, struct ext4_filename *fname,\n--\nfs/ext4/inline.c-1277- */\nfs/ext4/inline.c:1278:int ext4_inlinedir_to_tree(struct file *dir_file,\nfs/ext4/inline.c-1279-\t\t\t struct inode *dir, ext4_lblk_t block,\n--\nfs/ext4/namei.c=1235=int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,\n--\nfs/ext4/namei.c-1262-\t\t\tint has_inline_data = 1;\nfs/ext4/namei.c:1263:\t\t\tcount = ext4_inlinedir_to_tree(dir_file, dir, 0,\nfs/ext4/namei.c-1264-\t\t\t\t\t\t \u0026hinfo, start_hash,\n"
} |
| 31/3 | 2026/10/30 02:42 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 54731 output: 38 thoughts: 51 Thoughts: Here's my thought process, as if I were analyzing that code snippet: **Diving into the ext4 Htree Implementation** Okay, so I'm looking at `ext4_htree_fill_tree` and I need to figure out how `hinfo` is being used within it. The most likely place to start is the definition itself, so I'll go ahead and open up `fs/ext4/namei.c` and take a look at the function. Time to inspect the code and its context to understand the `hinfo` structure's role in the HTree indexing process. From there, I'll be able to work out exactly what the routine is doing with it. |
| 32/3 | 2026/10/30 02:42 | tool | read-file |
0mArgs:{
"File": "fs/ext4/namei.c",
"FirstLine": 1235,
"LineCount": 40
}Results: {
"Contents": "1235:\tint ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,\n1236:\t\t\t\t __u32 start_minor_hash, __u32 *next_hash)\n1237:\t{\n1238:\t\tstruct dx_hash_info hinfo;\n1239:\t\tstruct ext4_dir_entry_2 *de;\n1240:\t\tstruct dx_frame frames[EXT4_HTREE_LEVEL], *frame;\n1241:\t\tstruct inode *dir;\n1242:\t\text4_lblk_t block;\n1243:\t\tint count = 0;\n1244:\t\tint ret, err;\n1245:\t\t__u32 hashval;\n1246:\t\tstruct fscrypt_str tmp_str;\n1247:\t\n1248:\t\tdxtrace(printk(KERN_DEBUG \"In htree_fill_tree, start hash: %x:%x\\n\",\n1249:\t\t\t start_hash, start_minor_hash));\n1250:\t\tdir = file_inode(dir_file);\n1251:\t\tif (!(ext4_test_inode_flag(dir, EXT4_INODE_INDEX))) {\n1252:\t\t\tif (ext4_hash_in_dirent(dir))\n1253:\t\t\t\thinfo.hash_version = DX_HASH_SIPHASH;\n1254:\t\t\telse\n1255:\t\t\t\thinfo.hash_version =\n1256:\t\t\t\t\t\tEXT4_SB(dir-\u003ei_sb)-\u003es_def_hash_version;\n1257:\t\t\tif (hinfo.hash_version \u003c= DX_HASH_TEA)\n1258:\t\t\t\thinfo.hash_version +=\n1259:\t\t\t\t\tEXT4_SB(dir-\u003ei_sb)-\u003es_hash_unsigned;\n1260:\t\t\thinfo.seed = EXT4_SB(dir-\u003ei_sb)-\u003es_hash_seed;\n1261:\t\t\tif (ext4_has_inline_data(dir)) {\n1262:\t\t\t\tint has_inline_data = 1;\n1263:\t\t\t\tcount = ext4_inlinedir_to_tree(dir_file, dir, 0,\n1264:\t\t\t\t\t\t\t \u0026hinfo, start_hash,\n1265:\t\t\t\t\t\t\t start_minor_hash,\n1266:\t\t\t\t\t\t\t \u0026has_inline_data);\n1267:\t\t\t\tif (has_inline_data) {\n1268:\t\t\t\t\t*next_hash = ~0;\n1269:\t\t\t\t\treturn count;\n1270:\t\t\t\t}\n1271:\t\t\t}\n1272:\t\t\tcount = htree_dirblock_to_tree(dir_file, dir, 0, \u0026hinfo,\n1273:\t\t\t\t\t\t start_hash, start_minor_hash);\n1274:\t\t\t*next_hash = ~0;\n"
} |
| 33/3 | 2026/10/30 02:42 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 55713 output: 38 thoughts: 4127 Thoughts: **Deep Dive into ext4 Directory Entry Handling** Okay, so I've been dissecting the intricacies of how ext4 manages directory entries, particularly focusing on those darned hash values and the `EXT4_DIRENT_CFHASH` flag. It seems that if that flag is set in `ext4_dirdata_get`, then `hinfo->hash` and `hinfo->minor_hash` are populated directly from `dh->dh_hash`. Otherwise, these hash values are derived either from a legacy offset or generated by `ext4fs_dirhash` in various functions like `ext4_inlinedir_to_tree`, `htree_dirblock_to_tree`, and `dx_make_map`. The crucial point here is that `hinfo` is *always* initialized, either directly or indirectly through these hash calculations, ensuring these fields aren't left dangling. The `ext4_match` function, then, uses zero-initialized dirent hash info if there is no `EXT4_DIRENT_CFHASH`. Then, I shifted my focus to the `ext4_lufid_snapshot` function, and how it handles LUFID entries. It's clear that if the `EXT4_DIRENT_LUFID` flag is set, a snapshot of the `ext4_dirent_fid` structure is taken. This snapshot, `snap->fid`, points either to an inline buffer (`snap->buf`) or a `kmemdup` allocated buffer, and this is important for preserving the data in the event of a rename. This data appears to be copied to the new `dentry` via `edp`. The logic in `ext4_rename` copies this data to a structure, which is then copied to disk via the code I've examined. I'm satisfied that this flow is sound. I've also confirmed there's no way for the new ioctl, `EXT4_IOC_SET_LUFID`, to leak information, as it's a write-only ioctl and uses `copy_from_user` (and that the existing, and now absent, `copy_to_user` calls don't pose a vulnerability). Now, the crucial part: I've checked the zero initialization, ensuring that uninitialized memory isn't written to disk, which would have been a KMSAN issue. It appears the blocks are either zeroed on allocation or that any relevant structures are memset-ed to zero. The gap byte issue is correctly handled with a zero write. Finally, I checked the checks inside `ext4_check_dx_root`, and ensured that dirent data are initialized before being used. I examined `ext4_find_dest_de`, and traced the initialization of `dest_de` - everything seems to be correctly initialized. It seems I've covered all potential vulnerabilities related to directory entry manipulation. |
| 34/3 | 2026/10/30 02:43 | tool | read-file |
0mArgs:{
"File": "fs/ext4/namei.c",
"FirstLine": 2360,
"LineCount": 40
}Results: {
"Contents": "2360:\t\t\t struct ext4_dir_entry_2 **dest_de,\n2361:\t\t\t int dlen)\n2362:\t{\n2363:\t\tstruct ext4_dir_entry_2 *de;\n2364:\t\t/* When dirdata extensions are present, a NUL gap byte sits between\n2365:\t\t * the filename and the first extension; account for it so the found\n2366:\t\t * slot is always large enough for ext4_dirdata_set() to write into. */\n2367:\t\tunsigned short reclen = ext4_dirent_rec_len(fname_len(fname) + dlen +\n2368:\t\t\t\t\t\t\t (dlen ? 1 : 0), dir);\n2369:\t\tint nlen, rlen;\n2370:\t\tunsigned int offset = 0;\n2371:\t\tchar *top;\n2372:\t\n2373:\t\tde = buf;\n2374:\t\ttop = buf + buf_size - reclen;\n2375:\t\twhile ((char *) de \u003c= top) {\n2376:\t\t\tif (ext4_check_dir_entry(dir, NULL, de, bh,\n2377:\t\t\t\t\t\t buf, buf_size, offset))\n2378:\t\t\t\treturn -EFSCORRUPTED;\n2379:\t\t\tif (ext4_match(dir, fname, de, buf_size))\n2380:\t\t\t\treturn -EEXIST;\n2381:\t\t\tnlen = ext4_dir_entry_len(de, buf_size, dir);\n2382:\t\t\trlen = ext4_rec_len_from_disk(de-\u003erec_len, buf_size);\n2383:\t\t\tif ((de-\u003einode ? rlen - nlen : rlen) \u003e= reclen)\n2384:\t\t\t\tbreak;\n2385:\t\t\tde = (struct ext4_dir_entry_2 *)((char *)de + rlen);\n2386:\t\t\toffset += rlen;\n2387:\t\t}\n2388:\t\tif ((char *) de \u003e top)\n2389:\t\t\treturn -ENOSPC;\n2390:\t\n2391:\t\t*dest_de = de;\n2392:\t\treturn 0;\n2393:\t}\n2394:\t\n2395:\tint ext4_insert_dentry_data(struct inode *dir, struct inode *inode,\n2396:\t\t\t\t struct ext4_dir_entry_2 *de, int buf_size,\n2397:\t\t\t\t struct ext4_filename *fname, void *data)\n2398:\t{\n2399:\t\tint nlen, rlen;\n"
} |
| 35/3 | 2026/10/30 02:43 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 61182 output: 38 thoughts: 1740 Thoughts: **Analyzing Directory Entry Insertion and Data Management in ext4** Okay, so I'm diving into this `ext4_insert_dentry_data` function. The key here seems to be understanding how new directory entries are inserted and how existing ones are potentially split to make room. First, I see the successful assignment of `dest_de = de;`. Good. But let's verify what's happening internally within `ext4_insert_dentry_data`. It seems like `nlen` is the length of the new entry, and `rlen` is the current total record length for this entry. The crucial `if (de->inode)` check is the splitting case. If `de->inode` is non-zero, that means we're modifying an existing entry to make room for a new one. `de1` is a pointer that will hold the new entry, located at the offset `de + nlen`. We split the record length of the original `de`, setting `de1->rec_len = rlen - nlen`, and `de->rec_len = nlen`. Then we're moving `de` to be `de1`. Now we have the split location, and the new directory entry is ready to be populated. The rest of the function is filling in this new entry with the inode number, type, name length, name itself, and finally, calling `ext4_dirdata_set`. Crucially, before the split, the original entry had a record length of `rlen`. Now, the bytes from `de + nlen` to `de + rlen` was unused space. The new `de1` is written into that space. I'm now very curious about `ext4_dirdata_set`, as this is writing to disk blocks. The function calculates `data_offset` as the base directory length plus the filename length, plus one for a null terminator. This is where we clear a gap byte to avoid leaking stale data to disk. Then, there's the conditional insertion of a File Identifier (FID), if it exists. If the FID fits in the record length, it's copied and the `file_type` flag is updated. Finally, if the directory has a hash, we're inserting a directory hash. There are checks to ensure everything fits within the allocated `rec_len`. That's important to prevent buffer overflows and ensure the metadata is correctly written to disk. Now, what about `ext4_init_new_dir_data`? This one is used for creating new directory entries. It's calling `ext4_append` to get a new block to write to. `data1` and `data2` are unused here, so that's something to note. Finally, it uses `ext4_init_dirblock`. Finally, it's worth taking a look at `ext4_setent`. I need to inspect `ext4_setent` from `fs/ext4/namei.c`. I need to see how the name-inode mapping is actually written on disk. |
| 36/3 | 2026/10/30 02:43 | tool | read-file |
0mArgs:{
"File": "fs/ext4/namei.c",
"FirstLine": 4045,
"LineCount": 80
}Results: {
"Contents": "4045:\t\t\t const struct ext4_dirent_fid *src_fid)\n4046:\t{\n4047:\t\tint retval, retval2;\n4048:\t\n4049:\t\tBUFFER_TRACE(ent-\u003ebh, \"get write access\");\n4050:\t\tretval = ext4_journal_get_write_access(handle, ent-\u003edir-\u003ei_sb, ent-\u003ebh,\n4051:\t\t\t\t\t\t EXT4_JTR_NONE);\n4052:\t\tif (retval)\n4053:\t\t\treturn retval;\n4054:\t\tent-\u003ede-\u003einode = cpu_to_le32(ino);\n4055:\t\tif (ext4_has_feature_filetype(ent-\u003edir-\u003ei_sb)) {\n4056:\t\t\t/* Copy the source LUFID payload into the destination slot when\n4057:\t\t\t * both carry a LUFID of the same on-disk size. LUFID is\n4058:\t\t\t * inode-specific and must travel with the inode through rename.\n4059:\t\t\t * CFHASH bytes are left in-place: CFHASH is a function of the\n4060:\t\t\t * destination filename, and its bytes sit at the correct offset\n4061:\t\t\t * because LUFID always precedes CFHASH in the extension layout.\n4062:\t\t\t * When src_fid is NULL, the LUFID flag is synced to the LUFID\n4063:\t\t\t * bit already present in file_type: callers that clear LUFID\n4064:\t\t\t * (rename of a non-LUFID inode, whiteout) pass file_type with\n4065:\t\t\t * LUFID=0; ext4_resetent passes the original file_type so the\n4066:\t\t\t * flag is restored for error recovery without overwriting the\n4067:\t\t\t * LUFID bytes that are still intact on disk. */\n4068:\t\t\tif (ext4_has_feature_dirdata(ent-\u003edir-\u003ei_sb)) {\n4069:\t\t\t\tif (src_fid \u0026\u0026 (ent-\u003ede-\u003efile_type \u0026 EXT4_DIRENT_LUFID)) {\n4070:\t\t\t\t\tunsigned int rec_len =\n4071:\t\t\t\t\t\text4_rec_len_from_disk(ent-\u003ede-\u003erec_len,\n4072:\t\t\t\t\t\t\tent-\u003edir-\u003ei_sb-\u003es_blocksize);\n4073:\t\t\t\t\tunsigned int ddh_off =\n4074:\t\t\t\t\t\tEXT4_BASE_DIR_LEN + ent-\u003ede-\u003ename_len + 1;\n4075:\t\t\t\t\tstruct ext4_dirent_data_header *ddh =\n4076:\t\t\t\t\t\t(struct ext4_dirent_data_header *)\n4077:\t\t\t\t\t\t((char *)ent-\u003ede + ddh_off);\n4078:\t\t\t\t\tunsigned int copy_len = src_fid-\u003edf_header.ddh_length;\n4079:\t\n4080:\t\t\t\t\tif (ddh_off + sizeof(*ddh) \u003e rec_len ||\n4081:\t\t\t\t\t ddh-\u003eddh_length != copy_len ||\n4082:\t\t\t\t\t ddh_off + copy_len \u003e rec_len) {\n4083:\t\t\t\t\t\t/* Cannot copy in-place (size mismatch).\n4084:\t\t\t\t\t\t * If CFHASH is also present, slide it\n4085:\t\t\t\t\t\t * into the LUFID slot so the parser\n4086:\t\t\t\t\t\t * finds it at the correct offset after\n4087:\t\t\t\t\t\t * the LUFID flag is cleared. */\n4088:\t\t\t\t\t\text4_setent_compact_exts(ent, ddh,\n4089:\t\t\t\t\t\t\t\tddh_off, rec_len);\n4090:\t\t\t\t\t\tent-\u003ede-\u003efile_type \u0026= ~EXT4_DIRENT_LUFID;\n4091:\t\t\t\t\t} else {\n4092:\t\t\t\t\t\tmemcpy(ddh, src_fid, copy_len);\n4093:\t\t\t\t\t}\n4094:\t\t\t\t} else if (src_fid) {\n4095:\t\t\t\t\t/* Destination has no LUFID slot; cannot\n4096:\t\t\t\t\t * propagate in-place. Clear the flag from\n4097:\t\t\t\t\t * the on-disk entry directly — clearing only\n4098:\t\t\t\t\t * the local file_type variable has no effect\n4099:\t\t\t\t\t * because the final assignment takes the high\n4100:\t\t\t\t\t * bits from ent-\u003ede-\u003efile_type, not file_type. */\n4101:\t\t\t\t\tent-\u003ede-\u003efile_type \u0026= ~EXT4_DIRENT_LUFID;\n4102:\t\t\t\t} else if (!src_fid) {\n4103:\t\t\t\t\t/* Sync the LUFID flag with what file_type requests.\n4104:\t\t\t\t\t * For normal rename (source has no LUFID) and for\n4105:\t\t\t\t\t * the whiteout setent, file_type carries LUFID=0,\n4106:\t\t\t\t\t * so we clear the stale flag. For ext4_resetent\n4107:\t\t\t\t\t * (error recovery), file_type is the original\n4108:\t\t\t\t\t * file_type with LUFID=1, so we restore the flag —\n4109:\t\t\t\t\t * the LUFID bytes are still on disk untouched.\n4110:\t\t\t\t\t * When clearing LUFID and CFHASH is present, slide\n4111:\t\t\t\t\t * CFHASH bytes into the LUFID slot first so the\n4112:\t\t\t\t\t * parser finds CFHASH at the expected offset. */\n4113:\t\t\t\t\tif (!(file_type \u0026 EXT4_DIRENT_LUFID) \u0026\u0026\n4114:\t\t\t\t\t (ent-\u003ede-\u003efile_type \u0026 EXT4_DIRENT_LUFID)) {\n4115:\t\t\t\t\t\tunsigned int loff =\n4116:\t\t\t\t\t\t\tEXT4_BASE_DIR_LEN +\n4117:\t\t\t\t\t\t\tent-\u003ede-\u003ename_len + 1;\n4118:\t\t\t\t\t\tunsigned int rlen =\n4119:\t\t\t\t\t\t\text4_rec_len_from_disk(\n4120:\t\t\t\t\t\t\t\tent-\u003ede-\u003erec_len,\n4121:\t\t\t\t\t\t\t\tent-\u003edir-\u003ei_sb-\u003es_blocksize);\n4122:\t\t\t\t\t\text4_setent_compact_exts(ent,\n4123:\t\t\t\t\t\t\t(struct ext4_dirent_data_header *)\n4124:\t\t\t\t\t\t\t((char *)ent-\u003ede + loff),\n"
} |
| 37/3 | 2026/10/30 02:43 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 65180 output: 38 thoughts: 18 Thoughts: Okay, here's my attempt at summarizing the thought process, tailored for an expert audience, and written in the first person: **Analyzing a Specific Code Block** Alright, I'm diving back in. My focus is now on lines 4125 to 4150. It's time to meticulously dissect this section of code. I need to understand its functionality, its impact on the surrounding code, and any potential areas of concern. I'll be looking for edge cases, performance bottlenecks, and adherence to established best practices, or deviations from those practices. I'll also be considering its relationship to the broader system architecture and how this small piece fits into the larger puzzle. Let's see what gems we can find. |
| 38/3 | 2026/10/30 02:43 | tool | read-file |
0mArgs:{
"File": "fs/ext4/namei.c",
"FirstLine": 4125,
"LineCount": 25
}Results: {
"Contents": "4125:\t\t\t\t\t\t\tloff, rlen);\n4126:\t\t\t\t\t}\n4127:\t\t\t\t\tent-\u003ede-\u003efile_type =\n4128:\t\t\t\t\t\t(ent-\u003ede-\u003efile_type \u0026 ~EXT4_DIRENT_LUFID) |\n4129:\t\t\t\t\t\t(file_type \u0026 EXT4_DIRENT_LUFID);\n4130:\t\t\t\t}\n4131:\t\t\t}\n4132:\t\t\tent-\u003ede-\u003efile_type = (file_type \u0026 EXT4_FT_MASK) |\n4133:\t\t\t\t\t (ent-\u003ede-\u003efile_type \u0026 ~EXT4_FT_MASK);\n4134:\t\t}\n4135:\t\tinode_inc_iversion(ent-\u003edir);\n4136:\t\tinode_set_mtime_to_ts(ent-\u003edir, inode_set_ctime_current(ent-\u003edir));\n4137:\t\tretval = ext4_mark_inode_dirty(handle, ent-\u003edir);\n4138:\t\tBUFFER_TRACE(ent-\u003ebh, \"call ext4_handle_dirty_metadata\");\n4139:\t\tif (!ent-\u003einlined) {\n4140:\t\t\tretval2 = ext4_handle_dirty_dirblock(handle, ent-\u003edir, ent-\u003ebh);\n4141:\t\t\tif (unlikely(retval2)) {\n4142:\t\t\t\text4_std_error(ent-\u003edir-\u003ei_sb, retval2);\n4143:\t\t\t\treturn retval2;\n4144:\t\t\t}\n4145:\t\t}\n4146:\t\treturn retval;\n4147:\t}\n4148:\t\n4149:\tstatic void ext4_resetent(handle_t *handle, struct ext4_renament *ent,\n"
} |
| 39/3 | 2026/10/30 02:43 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 65834 output: 33 thoughts: 823 Thoughts: **Analyzing `ext4_dirdata_set_lufid` and Potential Vulnerabilities** Okay, let's dive into `ext4_dirdata_set_lufid`. I need to meticulously trace all the local variables and memory allocations within this function to understand its behavior fully. I see the typical suspects like `sb`, `fname`, `de`, `bh`, `inode`, and `handle` being declared, as well as some interesting structures like `ext4_lufid_snap`, `qstr`, and a local buffer `ondisk_name`. I'm already mentally flagging the `struct qstr d_name` because its initialization and usage are always critical. Aha! Here's an interesting conditional block dealing with case-folded and unencrypted directories. Specifically, the code copies `de->name` to `ondisk_name` and then sets `real_name.name` to `ondisk_name` and `real_name.len` to `de->name_len`. This means only a portion of the `ondisk_name` buffer (specifically, `de->name_len` bytes) is actually being used by `real_name`. This is a potential issue because the code *could* be attempting to read beyond the initialized region if it is not careful. I need to keep an eye on this when looking at where `real_name` is used. Looking ahead, I see `add_name` is set to either `real_name` or `d_name`, depending on the case-folding condition. This `add_name` is then passed to `ext4_add_entry()` through a `struct dentry new_dentry`, where the name is copied with `.d_name = *add_name`. It's passed as a `struct qstr` which contains `name` and `len`. Now, focusing in on `ext4_add_entry()`, I'm particularly interested in how the `d_name` (or `real_name`) fields are used by `__ext4_add_entry()`. It ultimately calls `__ext4_add_entry()`, which then calls `fname_name(fname)` and `fname_len(fname)` copies `fname_len(fname)` bytes from `d_name.name`. But before that, I need to know about `d_name->hash`. I remember `qstr`'s internal structure; it includes a hash, and I need to verify how it is initialized and used. Looking at the code, I see that the `real_name` is initialized. I see that `real_name.hash` will be zero. I need to figure out if there's any code path in `__ext4_add_entry` that expects `d_name->hash` to be something other than zero. If not zero, and if `d_name.hash` *is* used but wasn't initialized, this would be a significant problem. Time to trace the usage of `d_name` further within `__ext4_add_entry`! |
| 40/3 | 2026/10/30 02:44 | tool | grepper |
0mArgs:{
"Expression": "d_name",
"PathPrefixes": [
"fs/ext4/namei.c"
]
}Results: {
"Output": "fs/ext4/namei.c=1990=static struct buffer_head *ext4_find_entry(struct inode *dir,\nfs/ext4/namei.c:1991:\t\t\t\t\t const struct qstr *d_name,\nfs/ext4/namei.c-1992-\t\t\t\t\t struct ext4_dir_entry_2 **res_dir,\n--\nfs/ext4/namei.c-1998-\nfs/ext4/namei.c:1999:\terr = ext4_fname_setup_filename(dir, d_name, 1, \u0026fname);\nfs/ext4/namei.c-2000-\tif (err == -ENOENT)\n--\nfs/ext4/namei.c=2082=static struct dentry *ext4_lookup(struct inode *dir, struct dentry *dentry, unsigned int flags)\n--\nfs/ext4/namei.c-2087-\nfs/ext4/namei.c:2088:\tif (dentry-\u003ed_name.len \u003e EXT4_NAME_LEN)\nfs/ext4/namei.c-2089-\t\treturn ERR_PTR(-ENAMETOOLONG);\n--\nfs/ext4/namei.c=2696=static int __ext4_add_entry(handle_t *handle, struct inode *dir,\nfs/ext4/namei.c:2697:\t\t\t const struct qstr *d_name,\nfs/ext4/namei.c-2698-\t\t\t struct inode *inode,\n--\nfs/ext4/namei.c-2716-\nfs/ext4/namei.c:2717:\tif (!generic_ci_validate_strict_name(dir, d_name))\nfs/ext4/namei.c-2718-\t\treturn -EINVAL;\nfs/ext4/namei.c-2719-\nfs/ext4/namei.c:2720:\tretval = ext4_fname_setup_filename(dir, d_name, 0, \u0026fname);\nfs/ext4/namei.c-2721-\tif (retval)\n--\nfs/ext4/namei.c=2808=static int ext4_add_entry(handle_t *handle, struct dentry *dentry,\n--\nfs/ext4/namei.c-2814-\t\treturn -ENOKEY;\nfs/ext4/namei.c:2815:\treturn __ext4_add_entry(handle, dir, \u0026dentry-\u003ed_name, inode,\nfs/ext4/namei.c-2816-\t\t\t\tdentry-\u003ed_fsdata);\n--\nfs/ext4/namei.c=3168=static int ext4_create(struct mnt_idmap *idmap, struct inode *dir,\n--\nfs/ext4/namei.c-3181-retry:\nfs/ext4/namei.c:3182:\tinode = ext4_new_inode_start_handle(idmap, dir, mode, \u0026dentry-\u003ed_name,\nfs/ext4/namei.c-3183-\t\t\t\t\t 0, NULL, EXT4_HT_DIR, credits);\n--\nfs/ext4/namei.c=3203=static int ext4_mknod(struct mnt_idmap *idmap, struct inode *dir,\n--\nfs/ext4/namei.c-3216-retry:\nfs/ext4/namei.c:3217:\tinode = ext4_new_inode_start_handle(idmap, dir, mode, \u0026dentry-\u003ed_name,\nfs/ext4/namei.c-3218-\t\t\t\t\t 0, NULL, EXT4_HT_DIR, credits);\n--\nfs/ext4/namei.c=3358=static struct dentry *ext4_mkdir(struct mnt_idmap *idmap, struct inode *dir,\n--\nfs/ext4/namei.c-3375-\tinode = ext4_new_inode_start_handle(idmap, dir, mode,\nfs/ext4/namei.c:3376:\t\t\t\t\t \u0026dentry-\u003ed_name,\nfs/ext4/namei.c-3377-\t\t\t\t\t 0, NULL, EXT4_HT_DIR, credits);\n--\nfs/ext4/namei.c=3499=static int ext4_rmdir(struct inode *dir, struct dentry *dentry)\n--\nfs/ext4/namei.c-3520-\tretval = -ENOENT;\nfs/ext4/namei.c:3521:\tbh = ext4_find_entry(dir, \u0026dentry-\u003ed_name, \u0026de, NULL);\nfs/ext4/namei.c-3522-\tif (IS_ERR(bh))\n--\nfs/ext4/namei.c-3553-\t\t\t \"empty directory '%.*s' has too many links (%u)\",\nfs/ext4/namei.c:3554:\t\t\t dentry-\u003ed_name.len, dentry-\u003ed_name.name,\nfs/ext4/namei.c-3555-\t\t\t inode-\u003ei_nlink);\n--\nfs/ext4/namei.c-3589-\nfs/ext4/namei.c:3590:int __ext4_unlink(struct inode *dir, const struct qstr *d_name,\nfs/ext4/namei.c-3591-\t\t struct inode *inode,\n--\nfs/ext4/namei.c-3603-\t */\nfs/ext4/namei.c:3604:\tbh = ext4_find_entry(dir, d_name, \u0026de, NULL);\nfs/ext4/namei.c-3605-\tif (IS_ERR(bh))\n--\nfs/ext4/namei.c-3646-\t\text4_warning_inode(inode, \"Deleting file '%.*s' with no links\",\nfs/ext4/namei.c:3647:\t\t\t\t d_name-\u003elen, d_name-\u003ename);\nfs/ext4/namei.c-3648-\telse\n--\nfs/ext4/namei.c=3663=static int ext4_unlink(struct inode *dir, struct dentry *dentry)\n--\nfs/ext4/namei.c-3682-\nfs/ext4/namei.c:3683:\tretval = __ext4_unlink(dir, \u0026dentry-\u003ed_name, d_inode(dentry), dentry);\nfs/ext4/namei.c-3684-\n--\nfs/ext4/namei.c=3725=static int ext4_symlink(struct mnt_idmap *idmap, struct inode *dir,\n--\nfs/ext4/namei.c-3757-\tinode = ext4_new_inode_start_handle(idmap, dir, S_IFLNK|S_IRWXUGO,\nfs/ext4/namei.c:3758:\t\t\t\t\t \u0026dentry-\u003ed_name, 0, NULL,\nfs/ext4/namei.c-3759-\t\t\t\t\t EXT4_HT_DIR, credits);\n--\nfs/ext4/namei.c=3819=int __ext4_link(struct inode *dir, struct inode *inode,\nfs/ext4/namei.c:3820:\t\tconst struct qstr *d_name, struct dentry *dentry)\nfs/ext4/namei.c-3821-{\n--\nfs/ext4/namei.c-3836-\nfs/ext4/namei.c:3837:\terr = __ext4_add_entry(handle, dir, d_name, inode,\nfs/ext4/namei.c-3838-\t\t\t dentry ? dentry-\u003ed_fsdata : NULL);\n--\nfs/ext4/namei.c=3857=static int ext4_link(struct dentry *old_dentry,\n--\nfs/ext4/namei.c-3877-\t\treturn err;\nfs/ext4/namei.c:3878:\terr = __ext4_link(dir, inode, \u0026dentry-\u003ed_name, dentry);\nfs/ext4/namei.c-3879-\tif (!err) {\n--\nfs/ext4/namei.c=4149=static void ext4_resetent(handle_t *handle, struct ext4_renament *ent,\n--\nfs/ext4/namei.c-4159-\t */\nfs/ext4/namei.c:4160:\told.bh = ext4_find_entry(old.dir, \u0026old.dentry-\u003ed_name, \u0026old.de,\nfs/ext4/namei.c-4161-\t\t\t\t \u0026old.inlined);\n--\nfs/ext4/namei.c=4175=static int ext4_find_delete_entry(handle_t *handle, struct inode *dir,\nfs/ext4/namei.c:4176:\t\t\t\t const struct qstr *d_name)\nfs/ext4/namei.c-4177-{\n--\nfs/ext4/namei.c-4181-\nfs/ext4/namei.c:4182:\tbh = ext4_find_entry(dir, d_name, \u0026de, NULL);\nfs/ext4/namei.c-4183-\tif (IS_ERR(bh))\n--\nfs/ext4/namei.c=4192=static void ext4_rename_delete(handle_t *handle, struct ext4_renament *ent,\n--\nfs/ext4/namei.c-4202-\tif (le32_to_cpu(ent-\u003ede-\u003einode) != ent-\u003einode-\u003ei_ino ||\nfs/ext4/namei.c:4203:\t ent-\u003ede-\u003ename_len != ent-\u003edentry-\u003ed_name.len ||\nfs/ext4/namei.c:4204:\t strncmp(ent-\u003ede-\u003ename, ent-\u003edentry-\u003ed_name.name,\nfs/ext4/namei.c-4205-\t\t ent-\u003ede-\u003ename_len) ||\n--\nfs/ext4/namei.c-4207-\t\tretval = ext4_find_delete_entry(handle, ent-\u003edir,\nfs/ext4/namei.c:4208:\t\t\t\t\t\t\u0026ent-\u003edentry-\u003ed_name);\nfs/ext4/namei.c-4209-\t} else {\n--\nfs/ext4/namei.c-4212-\t\t\tretval = ext4_find_delete_entry(handle, ent-\u003edir,\nfs/ext4/namei.c:4213:\t\t\t\t\t\t\t\u0026ent-\u003edentry-\u003ed_name);\nfs/ext4/namei.c-4214-\t\t}\n--\nfs/ext4/namei.c=4235=static struct inode *ext4_whiteout_for_rename(struct mnt_idmap *idmap,\n--\nfs/ext4/namei.c-4251-\t\t\t\t\t S_IFCHR | WHITEOUT_MODE,\nfs/ext4/namei.c:4252:\t\t\t\t\t \u0026ent-\u003edentry-\u003ed_name, 0, NULL,\nfs/ext4/namei.c-4253-\t\t\t\t\t EXT4_HT_DIR, credits);\n--\nfs/ext4/namei.c=4339=static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,\n--\nfs/ext4/namei.c-4390-\nfs/ext4/namei.c:4391:\told.bh = ext4_find_entry(old.dir, \u0026old.dentry-\u003ed_name, \u0026old.de,\nfs/ext4/namei.c-4392-\t\t\t\t \u0026old.inlined);\n--\nfs/ext4/namei.c-4405-\nfs/ext4/namei.c:4406:\tnew.bh = ext4_find_entry(new.dir, \u0026new.dentry-\u003ed_name,\nfs/ext4/namei.c-4407-\t\t\t\t \u0026new.de, \u0026new.inlined);\n--\nfs/ext4/namei.c=4647=static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry,\n--\nfs/ext4/namei.c-4679-\nfs/ext4/namei.c:4680:\told.bh = ext4_find_entry(old.dir, \u0026old.dentry-\u003ed_name,\nfs/ext4/namei.c-4681-\t\t\t\t \u0026old.de, \u0026old.inlined);\n--\nfs/ext4/namei.c-4693-\nfs/ext4/namei.c:4694:\tnew.bh = ext4_find_entry(new.dir, \u0026new.dentry-\u003ed_name,\nfs/ext4/namei.c-4695-\t\t\t\t \u0026new.de, \u0026new.inlined);\n--\nfs/ext4/namei.c=4851=int ext4_dirdata_set_lufid(struct mnt_idmap *idmap, struct inode *dir,\n--\nfs/ext4/namei.c-4862-\tstruct ext4_lufid_snap old_snap = {};\nfs/ext4/namei.c:4863:\tstruct qstr d_name;\nfs/ext4/namei.c-4864-\t/* on-disk name snapshot for non-encrypted casefolded directories: the\n--\nfs/ext4/namei.c-4868-\t/* name to pass to ext4_add_entry() for the re-add and rollback */\nfs/ext4/namei.c:4869:\tconst struct qstr *add_name;\nfs/ext4/namei.c-4870-\t__u32 ino = 0;\n--\nfs/ext4/namei.c-4881-\nfs/ext4/namei.c:4882:\td_name.name = filename;\nfs/ext4/namei.c:4883:\td_name.len = namelen;\nfs/ext4/namei.c-4884-\nfs/ext4/namei.c:4885:\terr = ext4_fname_setup_filename(dir, \u0026d_name, 0, \u0026fname);\nfs/ext4/namei.c-4886-\tif (err)\n--\nfs/ext4/namei.c-4899-\t * prevents from finishing. */\nfs/ext4/namei.c:4900:\tbh = ext4_find_entry(dir, \u0026d_name, \u0026de, NULL);\nfs/ext4/namei.c-4901-\tif (IS_ERR(bh)) {\n--\nfs/ext4/namei.c-4974-\t * change the inode number). */\nfs/ext4/namei.c:4975:\tbh = ext4_find_entry(dir, \u0026d_name, \u0026de, NULL);\nfs/ext4/namei.c-4976-\tif (IS_ERR(bh)) {\n--\nfs/ext4/namei.c-4998-\t * the raw ciphertext. ext4_fname_setup_filename() deterministically\nfs/ext4/namei.c:4999:\t * re-encrypts d_name (the plaintext) to the identical ciphertext, so\nfs/ext4/namei.c:5000:\t * using d_name for the re-add is correct. Passing the raw ciphertext\nfs/ext4/namei.c-5001-\t * bytes would cause double-encryption and make the entry unreachable. */\n--\nfs/ext4/namei.c-5005-\t\treal_name.len = de-\u003ename_len;\nfs/ext4/namei.c:5006:\t\tadd_name = \u0026real_name;\nfs/ext4/namei.c-5007-\t} else {\nfs/ext4/namei.c:5008:\t\tadd_name = \u0026d_name;\nfs/ext4/namei.c-5009-\t}\n--\nfs/ext4/namei.c-5031-\t\tstruct dentry new_dentry = {\nfs/ext4/namei.c:5032:\t\t\t.d_name = *add_name,\nfs/ext4/namei.c-5033-\t\t\t.d_parent = \u0026parent_dentry,\n--\nfs/ext4/namei.c-5046-\t\tstruct dentry orig_dentry = {\nfs/ext4/namei.c:5047:\t\t\t.d_name = *add_name,\nfs/ext4/namei.c-5048-\t\t\t.d_parent = \u0026parent_dentry,\n"
} |
| 41/3 | 2026/10/30 02:44 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 71668 output: 34 thoughts: 139 Thoughts: **Examining `d_name` Initialization in `ext4_dirdata_set_lufid`** Okay, let's dive into this. I'm taking a look at how `d_name` is initialized within `ext4_dirdata_set_lufid`. I see the core initialization: ```c struct qstr d_name; ... d_name.name = filename; d_name.len = namelen; ``` Ah, right. Standard `qstr` struct; it has `name`, and `len`, but I know it *also* has a `hash` field. I see that `d_name.name` gets set to `filename` and `d_name.len` gets set to `namelen` which is straightforward. But I'm now curious about what happens to that `hash` field. The crucial question is: How is the `hash` field of the `qstr` struct, `d_name`, being handled further down the call chain? Let's trace it and find out. The next logical step is to analyze `ext4_fname_setup_filename` and see what it does with `d_name`. I need to understand the complete picture. Let's get that function up. |
| 42/3 | 2026/10/30 02:44 | tool | grepper |
0mArgs:{
"Expression": "ext4_fname_setup_filename",
"PathPrefixes": [
"fs/ext4/"
]
}Results: {
"Output": "fs/ext4/crypto.c=10=static void ext4_fname_from_fscrypt_name(struct ext4_filename *dst,\n--\nfs/ext4/crypto.c-21-\nfs/ext4/crypto.c:22:int ext4_fname_setup_filename(struct inode *dir, const struct qstr *iname,\nfs/ext4/crypto.c-23-\t\t\t int lookup, struct ext4_filename *fname)\n--\nfs/ext4/ext4.h=3031=extern const struct fscrypt_operations ext4_cryptops;\nfs/ext4/ext4.h-3032-\nfs/ext4/ext4.h:3033:int ext4_fname_setup_filename(struct inode *dir, const struct qstr *iname,\nfs/ext4/ext4.h-3034-\t\t\t int lookup, struct ext4_filename *fname);\n--\nfs/ext4/ext4.h=3041=int ext4_ioctl_get_encryption_pwsalt(struct file *filp, void __user *arg);\n--\nfs/ext4/ext4.h-3043-#else /* !CONFIG_FS_ENCRYPTION */\nfs/ext4/ext4.h:3044:static inline int ext4_fname_setup_filename(struct inode *dir,\nfs/ext4/ext4.h-3045-\t\t\t\t\t const struct qstr *iname,\n--\nfs/ext4/ext4.h=3056=static inline int ext4_fname_prepare_lookup(struct inode *dir,\n--\nfs/ext4/ext4.h-3059-{\nfs/ext4/ext4.h:3060:\treturn ext4_fname_setup_filename(dir, \u0026dentry-\u003ed_name, 1, fname);\nfs/ext4/ext4.h-3061-}\n--\nfs/ext4/namei.c=1990=static struct buffer_head *ext4_find_entry(struct inode *dir,\n--\nfs/ext4/namei.c-1998-\nfs/ext4/namei.c:1999:\terr = ext4_fname_setup_filename(dir, d_name, 1, \u0026fname);\nfs/ext4/namei.c-2000-\tif (err == -ENOENT)\n--\nfs/ext4/namei.c=2696=static int __ext4_add_entry(handle_t *handle, struct inode *dir,\n--\nfs/ext4/namei.c-2719-\nfs/ext4/namei.c:2720:\tretval = ext4_fname_setup_filename(dir, d_name, 0, \u0026fname);\nfs/ext4/namei.c-2721-\tif (retval)\n--\nfs/ext4/namei.c=4851=int ext4_dirdata_set_lufid(struct mnt_idmap *idmap, struct inode *dir,\n--\nfs/ext4/namei.c-4884-\nfs/ext4/namei.c:4885:\terr = ext4_fname_setup_filename(dir, \u0026d_name, 0, \u0026fname);\nfs/ext4/namei.c-4886-\tif (err)\n--\nfs/ext4/namei.c-4997-\t * For encrypted directories (with or without casefold) de-\u003ename holds\nfs/ext4/namei.c:4998:\t * the raw ciphertext. ext4_fname_setup_filename() deterministically\nfs/ext4/namei.c-4999-\t * re-encrypts d_name (the plaintext) to the identical ciphertext, so\n"
} |
| 43/3 | 2026/10/30 02:44 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 73068 output: 35 thoughts: 36 Thoughts: Okay, let's dive into this. Here's how I'd approach this task, as if these were my internal thoughts: **Analyzing ext4_fname_setup_filename for Crypto Applications** Alright, I need to understand how filenames are handled within the ext4 filesystem, specifically in relation to encryption. The key function I need to examine is `ext4_fname_setup_filename`. I know it's located in `fs/ext4/crypto.c`, but I'll also double-check the inline version declared within `ext4.h`. This likely means there's either a regular function *and* an optimized inline variant, or potentially a macro definition. Understanding both will be crucial to fully grasp the filename setup process, and how that interacts with any encryption mechanisms. I'll need to pay close attention to the arguments passed to this function, the internal data structures it uses, and, of course, any potential hooks or points of interaction with crypto-related code or structures within the ext4 system. My expert knowledge demands a complete understanding, so no stone is left unturned. |
| 44/3 | 2026/10/30 02:44 | tool | read-file |
0mArgs:{
"File": "fs/ext4/crypto.c",
"FirstLine": 22,
"LineCount": 50
}Results: {
"Contents": " 22:\tint ext4_fname_setup_filename(struct inode *dir, const struct qstr *iname,\n 23:\t\t\t\t int lookup, struct ext4_filename *fname)\n 24:\t{\n 25:\t\tstruct fscrypt_name name;\n 26:\t\tint err;\n 27:\t\n 28:\t\terr = fscrypt_setup_filename(dir, iname, lookup, \u0026name);\n 29:\t\tif (err)\n 30:\t\t\treturn err;\n 31:\t\n 32:\t\text4_fname_from_fscrypt_name(fname, \u0026name);\n 33:\t\n 34:\t\terr = ext4_fname_setup_ci_filename(dir, iname, fname);\n 35:\t\tif (err)\n 36:\t\t\text4_fname_free_filename(fname);\n 37:\t\n 38:\t\treturn err;\n 39:\t}\n 40:\t\n 41:\tint ext4_fname_prepare_lookup(struct inode *dir, struct dentry *dentry,\n 42:\t\t\t\t struct ext4_filename *fname)\n 43:\t{\n 44:\t\tstruct fscrypt_name name;\n 45:\t\tint err;\n 46:\t\n 47:\t\terr = fscrypt_prepare_lookup(dir, dentry, \u0026name);\n 48:\t\tif (err)\n 49:\t\t\treturn err;\n 50:\t\n 51:\t\text4_fname_from_fscrypt_name(fname, \u0026name);\n 52:\t\n 53:\t\terr = ext4_fname_setup_ci_filename(dir, \u0026dentry-\u003ed_name, fname);\n 54:\t\tif (err)\n 55:\t\t\text4_fname_free_filename(fname);\n 56:\t\treturn err;\n 57:\t}\n 58:\t\n 59:\tvoid ext4_fname_free_filename(struct ext4_filename *fname)\n 60:\t{\n 61:\t\tstruct fscrypt_name name;\n 62:\t\n 63:\t\tname.crypto_buf = fname-\u003ecrypto_buf;\n 64:\t\tfscrypt_free_filename(\u0026name);\n 65:\t\n 66:\t\tfname-\u003ecrypto_buf.name = NULL;\n 67:\t\tfname-\u003eusr_fname = NULL;\n 68:\t\tfname-\u003edisk_name.name = NULL;\n 69:\t\n 70:\t\text4_fname_free_ci_filename(fname);\n 71:\t}\n"
} |
| 45/3 | 2026/10/30 02:44 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 74034 output: 44 thoughts: 32 Thoughts: Okay, let's dive into this. Here's how I'd approach this task, as if these were my internal thoughts: **Investigating `fscrypt_setup_filename`** Alright, my first instinct is to drill down into how `fscrypt_setup_filename` functions. Specifically, I need to confirm something crucial: does this function actually utilize `iname->hash` in its process? This is important because it is an important key to solving the current issue. The most efficient way to investigate this is to use `grepper`. I'll immediately use this tool to determine whether or not the specified data structure is used. This should give me a rapid answer, saving me the trouble of manual code-reading. Let's see what `grepper` uncovers. |
| 46/3 | 2026/10/30 02:44 | tool | grepper |
0mArgs:{
"Expression": "fscrypt_setup_filename",
"PathPrefixes": [
"fs/crypto/",
"include/linux/fscrypt.h"
]
}Results: {
"Output": "fs/crypto/fname.c=317=EXPORT_SYMBOL(fscrypt_fname_disk_to_usr);\n--\nfs/crypto/fname.c-319-/**\nfs/crypto/fname.c:320: * fscrypt_setup_filename() - prepare to search a possibly encrypted directory\nfs/crypto/fname.c-321- * @dir: the directory that will be searched\n--\nfs/crypto/fname.c-341- */\nfs/crypto/fname.c:342:int fscrypt_setup_filename(struct inode *dir, const struct qstr *iname,\nfs/crypto/fname.c-343-\t\t\t int lookup, struct fscrypt_name *fname)\n--\nfs/crypto/fname.c-417-}\nfs/crypto/fname.c:418:EXPORT_SYMBOL(fscrypt_setup_filename);\nfs/crypto/fname.c-419-\n--\nfs/crypto/hooks.c=119=int __fscrypt_prepare_lookup(struct inode *dir, struct dentry *dentry,\n--\nfs/crypto/hooks.c-121-{\nfs/crypto/hooks.c:122:\tint err = fscrypt_setup_filename(dir, \u0026dentry-\u003ed_name, 1, fname);\nfs/crypto/hooks.c-123-\n--\ninclude/linux/fscrypt.h=430=bool fscrypt_fname_encrypted_size(const struct inode *inode, u32 orig_len,\ninclude/linux/fscrypt.h-431-\t\t\t\t u32 max_len, u32 *encrypted_len_ret);\ninclude/linux/fscrypt.h:432:int fscrypt_setup_filename(struct inode *inode, const struct qstr *iname,\ninclude/linux/fscrypt.h-433-\t\t\t int lookup, struct fscrypt_name *fname);\n--\ninclude/linux/fscrypt.h=672=static inline int fscrypt_drop_inode(struct inode *inode)\n--\ninclude/linux/fscrypt.h-677- /* fname.c */\ninclude/linux/fscrypt.h:678:static inline int fscrypt_setup_filename(struct inode *dir,\ninclude/linux/fscrypt.h-679-\t\t\t\t\t const struct qstr *iname,\n"
} |
| 47/3 | 2026/10/30 02:44 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 74872 output: 35 thoughts: 19 Thoughts: Okay, here's my interpretation of what's going through my mind, delivered as a first-person summary, keeping in mind the expert audience: **Diving into `fs/crypto/fname.c` Around Line 342** Alright, time to dig into the filesystem crypto code. Specifically, let's zoom in on `fs/crypto/fname.c` and take a look around line 342. I'm probably looking for something specific, maybe a potential vulnerability I've been thinking about, or perhaps trying to understand the control flow of a recent patch. The context around line 342 usually involves the implementation of a feature or bug fix. I anticipate the code deals with things related to filename encryption or decryption, possibly key handling, or access control. I'll be looking to understand the algorithms being used, error handling, and whether there are any obvious opportunities for misuse. Let's see what's actually going on in that section... I'm particularly interested in seeing how they're handling things like race conditions or input validation. This may be related to a problem or question I have about how the system handles the encryption and decryption processes in regards to security. |
| 48/3 | 2026/10/30 02:44 | tool | read-file |
0mArgs:{
"File": "fs/crypto/fname.c",
"FirstLine": 342,
"LineCount": 45
}Results: {
"Contents": " 342:\tint fscrypt_setup_filename(struct inode *dir, const struct qstr *iname,\n 343:\t\t\t\t int lookup, struct fscrypt_name *fname)\n 344:\t{\n 345:\t\tstruct fscrypt_nokey_name *nokey_name;\n 346:\t\tint ret;\n 347:\t\n 348:\t\tmemset(fname, 0, sizeof(struct fscrypt_name));\n 349:\t\tfname-\u003eusr_fname = iname;\n 350:\t\n 351:\t\tif (!IS_ENCRYPTED(dir) || fscrypt_is_dot_dotdot(iname)) {\n 352:\t\t\tfname-\u003edisk_name.name = (unsigned char *)iname-\u003ename;\n 353:\t\t\tfname-\u003edisk_name.len = iname-\u003elen;\n 354:\t\t\treturn 0;\n 355:\t\t}\n 356:\t\tret = fscrypt_get_encryption_info(dir, lookup);\n 357:\t\tif (ret)\n 358:\t\t\treturn ret;\n 359:\t\n 360:\t\tif (fscrypt_has_encryption_key(dir)) {\n 361:\t\t\tif (!fscrypt_fname_encrypted_size(dir, iname-\u003elen, NAME_MAX,\n 362:\t\t\t\t\t\t\t \u0026fname-\u003ecrypto_buf.len))\n 363:\t\t\t\treturn -ENAMETOOLONG;\n 364:\t\t\tfname-\u003ecrypto_buf.name = kmalloc(fname-\u003ecrypto_buf.len,\n 365:\t\t\t\t\t\t\t GFP_NOFS);\n 366:\t\t\tif (!fname-\u003ecrypto_buf.name)\n 367:\t\t\t\treturn -ENOMEM;\n 368:\t\n 369:\t\t\tret = fscrypt_fname_encrypt(dir, iname, fname-\u003ecrypto_buf.name,\n 370:\t\t\t\t\t\t fname-\u003ecrypto_buf.len);\n 371:\t\t\tif (ret)\n 372:\t\t\t\tgoto errout;\n 373:\t\t\tfname-\u003edisk_name.name = fname-\u003ecrypto_buf.name;\n 374:\t\t\tfname-\u003edisk_name.len = fname-\u003ecrypto_buf.len;\n 375:\t\t\treturn 0;\n 376:\t\t}\n 377:\t\tif (!lookup)\n 378:\t\t\treturn -ENOKEY;\n 379:\t\tfname-\u003eis_nokey_name = true;\n 380:\t\n 381:\t\t/*\n 382:\t\t * We don't have the key and we are doing a lookup; decode the\n 383:\t\t * user-supplied name\n 384:\t\t */\n 385:\t\n 386:\t\tif (iname-\u003elen \u003e FSCRYPT_NOKEY_NAME_MAX_ENCODED)\n"
} |
| 49/3 | 2026/10/30 02:44 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 75904 output: 36 thoughts: 65 Thoughts: Okay, let's break this down. **Investigating Potential File Naming Weaknesses** Right, so I'm focusing on file naming and encryption within the file system code. I've been looking at how filenames are handled and I'm trying to identify potential vulnerabilities or areas for improvement. I see that the `iname->hash` value isn't read in `fscrypt_setup_filename`, which is interesting. It's a key observation because that function is responsible for setting up the encrypted filename. Hmm... Now, I need to expand my search. What about `ext4_fname_setup_ci_filename`? Is that function interacting with that `iname->hash` value? Let's check the code for that function immediately! I'll look it up in `fs/ext4/namei.c` and take a deep dive. I'll need to see how that function works and figure out if it interacts with that `iname->hash` information at all. |
| 50/3 | 2026/10/30 02:44 | tool | grepper |
0mArgs:{
"Expression": "ext4_fname_setup_ci_filename",
"PathPrefixes": [
"fs/ext4/"
]
}Results: {
"Output": "fs/ext4/crypto.c=22=int ext4_fname_setup_filename(struct inode *dir, const struct qstr *iname,\n--\nfs/ext4/crypto.c-33-\nfs/ext4/crypto.c:34:\terr = ext4_fname_setup_ci_filename(dir, iname, fname);\nfs/ext4/crypto.c-35-\tif (err)\n--\nfs/ext4/crypto.c=41=int ext4_fname_prepare_lookup(struct inode *dir, struct dentry *dentry,\n--\nfs/ext4/crypto.c-52-\nfs/ext4/crypto.c:53:\terr = ext4_fname_setup_ci_filename(dir, \u0026dentry-\u003ed_name, fname);\nfs/ext4/crypto.c-54-\tif (err)\n--\nfs/ext4/ext4.h=3004=ext4_fsblk_t ext4_inode_to_goal_block(struct inode *);\n--\nfs/ext4/ext4.h-3006-#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/ext4.h:3007:extern int ext4_fname_setup_ci_filename(struct inode *dir,\nfs/ext4/ext4.h-3008-\t\t\t\t\tconst struct qstr *iname,\n--\nfs/ext4/ext4.h=3011=static inline void ext4_fname_free_ci_filename(struct ext4_filename *fname)\n--\nfs/ext4/ext4.h-3016-#else\nfs/ext4/ext4.h:3017:static inline int ext4_fname_setup_ci_filename(struct inode *dir,\nfs/ext4/ext4.h-3018-\t\t\t\t\t const struct qstr *iname,\n--\nfs/ext4/ext4.h=3044=static inline int ext4_fname_setup_filename(struct inode *dir,\n--\nfs/ext4/ext4.h-3052-\nfs/ext4/ext4.h:3053:\treturn ext4_fname_setup_ci_filename(dir, iname, fname);\nfs/ext4/ext4.h-3054-}\n--\nfs/ext4/namei.c=1645=static void dx_insert_block(struct dx_frame *frame, u32 hash, ext4_lblk_t block)\n--\nfs/ext4/namei.c-1659-#if IS_ENABLED(CONFIG_UNICODE)\nfs/ext4/namei.c:1660:int ext4_fname_setup_ci_filename(struct inode *dir, const struct qstr *iname,\nfs/ext4/namei.c-1661-\t\t\t\t struct ext4_filename *name)\n--\nfs/ext4/namei.c=1703=static bool ext4_match(struct inode *parent,\n--\nfs/ext4/namei.c-1726-\t\t * have been added right after\nfs/ext4/namei.c:1727:\t\t * ext4_fname_setup_ci_filename(). In this case, a hash\nfs/ext4/namei.c-1728-\t\t * mismatch will be a false negative. Therefore, make\n"
} |
| 51/3 | 2026/10/30 02:44 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 77050 output: 38 thoughts: 28 Thoughts: Okay, here's my interpretation of that thought process, assuming I'm already deep in kernel development and understand the context: **Analyzing `ext4_fname_setup_ci_filename` in `fs/ext4/namei.c`** Alright, time to dig into the filename creation process within the ext4 filesystem. I need to refresh my understanding of how `ext4_fname_setup_ci_filename` functions, and specifically, where it fits in the larger picture of name lookup and inode interaction. `fs/ext4/namei.c` is the right place to start; it's the core of the namei operations for ext4. I'm focusing on filename setup. This suggests a need to understand how we set up a file or directory name, likely within the context of a new file creation, link setup, or similar operation. I'm probably looking at how character strings are constructed to be inserted into the directory entries. It would be helpful if it handled case-insensitive files too. Time to start the deep dive into the code... |
| 52/3 | 2026/10/30 02:44 | tool | read-file |
0mArgs:{
"File": "fs/ext4/namei.c",
"FirstLine": 1660,
"LineCount": 40
}Results: {
"Contents": "1660:\tint ext4_fname_setup_ci_filename(struct inode *dir, const struct qstr *iname,\n1661:\t\t\t\t\t struct ext4_filename *name)\n1662:\t{\n1663:\t\tstruct qstr *cf_name = \u0026name-\u003ecf_name;\n1664:\t\tunsigned char *buf;\n1665:\t\tstruct dx_hash_info *hinfo = \u0026name-\u003ehinfo;\n1666:\t\tint len;\n1667:\t\n1668:\t\tif (!IS_CASEFOLDED(dir) ||\n1669:\t\t (IS_ENCRYPTED(dir) \u0026\u0026 !fscrypt_has_encryption_key(dir))) {\n1670:\t\t\tcf_name-\u003ename = NULL;\n1671:\t\t\treturn 0;\n1672:\t\t}\n1673:\t\n1674:\t\tbuf = kmalloc(EXT4_NAME_LEN, GFP_NOFS);\n1675:\t\tif (!buf)\n1676:\t\t\treturn -ENOMEM;\n1677:\t\n1678:\t\tlen = utf8_casefold(dir-\u003ei_sb-\u003es_encoding, iname, buf, EXT4_NAME_LEN);\n1679:\t\tif (len \u003c= 0) {\n1680:\t\t\tkfree(buf);\n1681:\t\t\tbuf = NULL;\n1682:\t\t}\n1683:\t\tcf_name-\u003ename = buf;\n1684:\t\tcf_name-\u003elen = (unsigned) len;\n1685:\t\n1686:\t\tif (!IS_ENCRYPTED(dir))\n1687:\t\t\treturn 0;\n1688:\t\n1689:\t\thinfo-\u003ehash_version = DX_HASH_SIPHASH;\n1690:\t\thinfo-\u003eseed = NULL;\n1691:\t\tif (cf_name-\u003ename)\n1692:\t\t\treturn ext4fs_dirhash(dir, cf_name-\u003ename, cf_name-\u003elen, hinfo);\n1693:\t\telse\n1694:\t\t\treturn ext4fs_dirhash(dir, iname-\u003ename, iname-\u003elen, hinfo);\n1695:\t}\n1696:\t#endif\n1697:\t\n1698:\t/*\n1699:\t * Test whether a directory entry matches the filename being searched for.\n"
} |
| 53/3 | 2026/10/30 02:44 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 77960 output: 585 thoughts: 3931 Thoughts: **KMSAN Analysis: Determining `NeedsKMSAN` for EXT4 Directory Data Patch** Okay, let's break this down. The core question is whether these EXT4 changes *require* KMSAN or if KASAN and other standard bug detectors are sufficient. The goal, as I understand it, is to avoid the significant overhead of KMSAN if at all possible. First, I need to make absolutely sure no information leaks to user space. The `EXT4_IOC_SET_LUFID` ioctl is `_IOW` which *only* takes data, it doesn't return anything. No `copy_to_user`, no shenanigans with sockets or netlink. Readdir uses `dir_emit`, but the data copied includes name, length, inode number, and file type. The name and length are from validated and allocated buffers; the file type is masked to exclude any potential uninitialized flags. Zero worries. Next, I have to meticulously examine all conditional logic and branching. I'm going through every variable used in `if` statements and any other conditional expressions. Any potential for uninitialized variables here is a red flag. I see zero-initialized stack structures, like the `dx_hash_info` struct, used for hash calculations. I am constantly checking variables initialized to zero, or given default values such as `0` or `NULL`, and passed to functions. I'm drilling down into functions like `ext4_dirdata_get` to be sure. I have to verify that every pointer is either validated before use or initialized on allocation to zero. Third, the patch introduces memory allocation. I'm looking closely at how memory is allocated (`kzalloc_flex`, `kmemdup`, `kmalloc`) and initialized. `kzalloc` is the gold standard for zeroing. `kmemdup` is copying from a valid, initialized buffer. In every case, if `kmalloc` is used, the allocated memory is *immediately* populated. There are no allocations where fields are left untouched. Finally, I have to carefully consider the potential for buffer overruns or uninitialized bytes being read due to incorrect bounds checks. The data that comes from the disk or inline data is the major concern. KMSAN typically does not track page cache / disk data as uninitialized unless it comes from kernel memory allocations without initialization. I have to confirm there is proper boundary and size checking for the code reading directory data. I am seeing lots of boundary checks in the existing code, and new checks in the patch itself. Now, which bugs are the most likely? If there are buffer overruns, or out-of-bounds reads/writes because of an incorrect offset calculation, KASAN catches them. Use-after-free bugs? KASAN. Locking errors? LOCKDEP. NULL pointer dereferences? The kernel's standard tools get them. Does the code introduce uninitialized memory? The ioctl doesn't leak anything to user space. New stack variables? They are explicitly zero-initialized. Heap allocations? Zeroed with kzalloc. Therefore, there are *no* scenarios that expose uninitialized memory that *only* KMSAN could detect. The risks here - mostly around data integrity, bounds checks, and memory management - are comprehensively handled by KASAN, FORTIFY_SOURCE, and LOCKDEP. `NeedsKMSAN` is unequivocally `false`. |
| 54/3 | 2026/10/30 02:45 | tool | set-results |
0mArgs:{
"KMSANReasoning": "The patch series implements support for the EXT4_FEATURE_INCOMPAT_DIRDATA feature (primarily Lustre Unique File Identifiers - LUFID - and casefolded hashes CFHASH), adding the EXT4_IOC_SET_LUFID ioctl and directory entry manipulation logic.\n\nKMSAN vs KASAN Applicability Analysis:\n1. Kernel-to-User Leaks / Outputs:\n - The newly introduced ioctl EXT4_IOC_SET_LUFID is defined with _IOW, meaning it only reads data from userspace via copy_from_user. It does not return any struct or memory back to userspace via copy_to_user or put_user.\n - In readdir/filldir (ext4_readdir, call_filldir, ext4_read_inline_dir), dirent names are emitted strictly up to their validated name length (de-\u003ename_len / ent_name-\u003elen) and file types are masked with EXT4_FT_MASK. No uninitialized padding or dirdata bytes are leaked to userspace.\n\n2. Structure Initialization and Memory Allocations:\n - All stack variables introduced in the patch (e.g. struct dx_hash_info hi = {}, dirent_hinfo = {}, struct ext4_lufid_snap old_snap = {}, struct ext4_filename fname = {}, struct qstr real_name = {}) are explicitly zero-initialized.\n - Dynamic allocations either use kzalloc_flex (guaranteeing zero-initialization before data copy, such as struct fname in ext4_htree_store_dirent), kmemdup, or kmalloc where all fields and flexible payload bytes are immediately fully populated (such as edp and old_edp in ext4_rename and ext4_dirdata_set_lufid).\n\n3. Logic, Offsets, and Bounds:\n - The modifications primarily adjust directory entry offset calculations, record length bounds checks, and in-place extension compaction (ext4_dirent_get_data_len, ext4_dir_entry_len, ext4_setent_compact_exts).\n - Any errors or edge cases in pointer arithmetic, offset calculation, or buffer bounds risk out-of-bounds (OOB) memory reads/writes or corrupted metadata, which are detected by KASAN and FORTIFY_SOURCE. Locking patterns in ext4_dirdata_set_lufid are monitored by LOCKDEP.\n\nBecause the changes do not introduce uninitialized memory reads or information leak risks that would be caught uniquely by KMSAN, a dedicated KMSAN fuzzing session is not warranted.",
"NeedsKMSAN": false
}Results: {
"KMSANReasoning": "The patch series implements support for the EXT4_FEATURE_INCOMPAT_DIRDATA feature (primarily Lustre Unique File Identifiers - LUFID - and casefolded hashes CFHASH), adding the EXT4_IOC_SET_LUFID ioctl and directory entry manipulation logic.\n\nKMSAN vs KASAN Applicability Analysis:\n1. Kernel-to-User Leaks / Outputs:\n - The newly introduced ioctl EXT4_IOC_SET_LUFID is defined with _IOW, meaning it only reads data from userspace via copy_from_user. It does not return any struct or memory back to userspace via copy_to_user or put_user.\n - In readdir/filldir (ext4_readdir, call_filldir, ext4_read_inline_dir), dirent names are emitted strictly up to their validated name length (de-\u003ename_len / ent_name-\u003elen) and file types are masked with EXT4_FT_MASK. No uninitialized padding or dirdata bytes are leaked to userspace.\n\n2. Structure Initialization and Memory Allocations:\n - All stack variables introduced in the patch (e.g. struct dx_hash_info hi = {}, dirent_hinfo = {}, struct ext4_lufid_snap old_snap = {}, struct ext4_filename fname = {}, struct qstr real_name = {}) are explicitly zero-initialized.\n - Dynamic allocations either use kzalloc_flex (guaranteeing zero-initialization before data copy, such as struct fname in ext4_htree_store_dirent), kmemdup, or kmalloc where all fields and flexible payload bytes are immediately fully populated (such as edp and old_edp in ext4_rename and ext4_dirdata_set_lufid).\n\n3. Logic, Offsets, and Bounds:\n - The modifications primarily adjust directory entry offset calculations, record length bounds checks, and in-place extension compaction (ext4_dirent_get_data_len, ext4_dir_entry_len, ext4_setent_compact_exts).\n - Any errors or edge cases in pointer arithmetic, offset calculation, or buffer bounds risk out-of-bounds (OOB) memory reads/writes or corrupted metadata, which are detected by KASAN and FORTIFY_SOURCE. Locking patterns in ext4_dirdata_set_lufid are monitored by LOCKDEP.\n\nBecause the changes do not introduce uninitialized memory reads or information leak risks that would be caught uniquely by KMSAN, a dedicated KMSAN fuzzing session is not warranted.",
"NeedsKMSAN": false
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|