From: Artem Blagodarenko Introduce ext4_dir_entry_len() helper to compute the required rec_len for a directory entry, taking into account dirdata and casefold+fscrypt hash space. Convert ext4_dirent_get_data_len() to take the decoded rec_len as an argument and add bounds checking when walking dirdata extensions to avoid overruns on malformed entries. Update dx_root_limit() to use ext4_dir_entry_len() instead of open-coded ext4_dir_rec_len() for '.' and '..' entries. Signed-off-by: Artem Blagodarenko Reviewed-by: Andreas Dilger --- fs/ext4/ext4.h | 45 ++++++++++++++++++++++++++++++++++++++++++--- fs/ext4/namei.c | 23 +++++++++++++++-------- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index f833f6ef0040..45e90b8be9e8 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3988,6 +3988,7 @@ static inline bool ext4_dir_entry_is_tail(struct ext4_dir_entry_2 *de) /* * 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 @@ -3996,22 +3997,41 @@ static inline bool ext4_dir_entry_is_tail(struct ext4_dir_entry_2 *de) * * 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) +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; - ddh = (struct ext4_dirent_data_header *)(de->name + de->name_len + - 1 /* NUL terminator */); + /* 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 dlen; + + /* validate ddh_length is reasonable */ + if (ddh->ddh_length == 0 || ddh->ddh_length > + rec_len - offset) + return dlen; + dlen += ddh->ddh_length + (dlen == 0); + offset += ddh->ddh_length; ddh = ext4_dirdata_next(ddh); } extra_data_flags >>= 1; @@ -4019,6 +4039,25 @@ static inline int ext4_dirent_get_data_len(struct ext4_dir_entry_2 *de) return dlen; } +/* + * ext4_dir_entry_len() - Compute the required rec_len for a directory entry. + * @de: directory entry (used to read name_len and any dirdata length) + * @dir: directory inode (may be NULL for '.' and '..' entries) + * + * 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, + const struct inode *dir) +{ + unsigned int blocksize = (dir && dir->i_sb) ? dir->i_sb->s_blocksize : 4096; + 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_dir_rec_len(de->name_len + dirdata, dir); +} + extern const struct iomap_ops ext4_iomap_ops; extern const struct iomap_ops ext4_iomap_report_ops; diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c index 87d8cd2c6377..0635eac2de8d 100644 --- a/fs/ext4/namei.c +++ b/fs/ext4/namei.c @@ -570,11 +570,15 @@ 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(dot_de); + 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); @@ -850,10 +854,13 @@ dx_probe(struct ext4_filename *fname, struct inode *dir, entries = (struct dx_entry *)(((char *)info) + info->info_length); - if (dx_get_limit(entries) != dx_root_limit(dir, info->info_length)) { + if (dx_get_limit(entries) != + dx_root_limit(dir, (struct ext4_dir_entry_2 *)frame->bh->b_data)) { ext4_warning_inode(dir, "dx entry: limit %u != root limit %u", dx_get_limit(entries), - dx_root_limit(dir, info->info_length)); + dx_root_limit(dir, + (struct ext4_dir_entry_2 *)frame->bh->b_data + )); goto fail; } @@ -2278,10 +2285,10 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname, dx_info->hash_version = EXT4_SB(dir->i_sb)->s_def_hash_version; - entries = (void *)dx_info + sizeof(*dx_info); + entries = (void *)dx_info + dx_info->info_length; dx_set_block(entries, 1); dx_set_count(entries, 1); - dx_set_limit(entries, dx_root_limit(dir, sizeof(*dx_info))); + dx_set_limit(entries, dx_root_limit(dir, dot_de)); /* Initialize as for dx_probe */ fname->hinfo.hash_version = dx_info->hash_version; -- 2.43.7