diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c index 65c53c08213a..e6f54dba735e 100644 --- a/fs/ext4/namei.c +++ b/fs/ext4/namei.c @@ -412,7 +412,7 @@ static struct dx_countlimit *get_dx_countlimit(struct inode *inode, if (le16_to_cpu(de->rec_len) != (blocksize - rlen)) return NULL; /* de->rec_len covers whole dx_root block, calculate actual length */ - dotdot_rec_len = ext4_dir_entry_len(de, NULL); + dotdot_rec_len = ext4_dir_entry_len(de, inode); root = (struct dx_root_info *)(((char *)de + dotdot_rec_len)); if (root->reserved_zero || root->info_length != sizeof(struct dx_root_info)) @@ -520,13 +520,20 @@ 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(void *de_buf) +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; + /* get dotdot first */ - de_buf += ext4_dir_entry_len(de_buf, NULL); + de_buf += ext4_dir_entry_len(de_buf, dir); /* dx root info is after dotdot entry */ - de_buf += ext4_dir_entry_len(de_buf, NULL); + de_buf += ext4_dir_entry_len(de_buf, dir); + + if (de_buf < base || (char *)de_buf - (char *)base + + sizeof(struct dx_root_info) > blocksize) + return ERR_PTR(-EFSCORRUPTED); return (struct dx_root_info *)de_buf; } @@ -577,7 +584,9 @@ static inline unsigned dx_root_limit(struct inode *dir, struct dx_root_info *info; unsigned int entry_space; - info = dx_get_dx_info(dot_de); + info = dx_get_dx_info(dir, dot_de); + if (IS_ERR(info)) + return 0; entry_space = dir->i_sb->s_blocksize - ((char *)info - (char *)dot_de) - info->info_length; @@ -793,7 +802,9 @@ dx_probe(struct ext4_filename *fname, struct inode *dir, if (IS_ERR(frame->bh)) return (struct dx_frame *) frame->bh; - info = dx_get_dx_info((struct ext4_dir_entry_2 *)frame->bh->b_data); + info = dx_get_dx_info(dir, (struct ext4_dir_entry_2 *)frame->bh->b_data); + if (IS_ERR(info)) + goto fail; if (info->hash_version != DX_HASH_TEA && info->hash_version != DX_HASH_HALF_MD4 && info->hash_version != DX_HASH_LEGACY && @@ -938,7 +949,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; @@ -947,7 +958,9 @@ static void dx_release(struct dx_frame *frames) if (frames[0].bh == NULL) return; - info = dx_get_dx_info((struct ext4_dir_entry_2 *)frames[0].bh->b_data); + info = dx_get_dx_info(dir, (struct ext4_dir_entry_2 *)frames[0].bh->b_data); + if (IS_ERR(info)) + return; /* save local copy, "info" may be freed after brelse() */ indirect_levels = info->indirect_levels; for (i = 0; i <= indirect_levels; i++) { @@ -1253,12 +1266,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); } @@ -1296,8 +1309,10 @@ unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de, struct inode *dir, { unsigned char ret = 0; 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); - if (data_offset > de->rec_len) + if (data_offset > rec_len) return ret; /* compatibility: hash stored inline after filename (no dirdata) */ @@ -1312,19 +1327,20 @@ unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de, struct inode *dir, /* EXT4_DIRENT_* are not expected without flag in i_sb */ if (de->file_type & EXT4_DIRENT_LUFID) { - struct ext4_dirent_fid *dfid = + struct ext4_dirent_fid *disk_fid = (struct ext4_dirent_fid *)(de->name + data_offset); unsigned int dlen; - if (data_offset + sizeof(dfid->df_header) > de->rec_len) + if (data_offset + sizeof(disk_fid->df_header) > rec_len) return ret; - dlen = dfid->df_header.ddh_length; - if (dlen < sizeof(*dfid) || data_offset + dlen > de->rec_len) + dlen = disk_fid->df_header.ddh_length; + if (dlen < sizeof(*disk_fid) || data_offset + dlen > rec_len) return ret; if (dfid) { - memcpy(dfid, dfid->df_fid, dfid->df_header.ddh_length); + memcpy(dfid, disk_fid->df_fid, + disk_fid->df_header.ddh_length); ret |= EXT4_DIRENT_LUFID; } data_offset += dlen; @@ -1336,11 +1352,11 @@ unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de, struct inode *dir, (struct ext4_dirent_data_header *)(de->name + data_offset); unsigned int dlen; - if (data_offset + sizeof(*ddh) > de->rec_len) + if (data_offset + sizeof(*ddh) > rec_len) return ret; dlen = ddh->ddh_length; - if (dlen < sizeof(*ddh) || data_offset + dlen > de->rec_len) + if (dlen < sizeof(*ddh) || data_offset + dlen > rec_len) return ret; data_offset += dlen; @@ -1355,7 +1371,7 @@ unsigned char ext4_dirdata_get(struct ext4_dir_entry_2 *de, struct inode *dir, unsigned int dlen; dlen = dh->dh_header.ddh_length; - if (dlen < sizeof(*dh) || data_offset + dlen > de->rec_len) + if (dlen < sizeof(*dh) || data_offset + dlen > rec_len) return ret; hinfo->hash = le32_to_cpu(dh->dh_hash.hash); @@ -1383,12 +1399,14 @@ static void ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir, { 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); if (dfid) { unsigned int dlen = dfid->df_header.ddh_length; - if (data_offset + dlen > de->rec_len) { + if (data_offset + dlen > rec_len) { EXT4_ERROR_INODE(dir, "Can not insert FID"); return; } @@ -1406,7 +1424,7 @@ static void ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir, struct ext4_dirent_hash *dh = (struct ext4_dirent_hash *)(de->name + data_offset); - if (data_offset + sizeof(*dh) > de->rec_len) { + if (data_offset + sizeof(*dh) > rec_len) { EXT4_ERROR_INODE(dir, "Can not insert dhash dirdata"); return; } @@ -1418,7 +1436,7 @@ static void ext4_dirdata_set(struct ext4_dir_entry_2 *de, struct inode *dir, } else { /* Compatibility: store hash inline after filename */ if (data_offset + sizeof(struct ext4_dir_entry_hash) > - de-> rec_len) { + rec_len) { EXT4_ERROR_INODE(dir, "Can not insert dhash"); return; } @@ -1906,7 +1924,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; } @@ -2425,7 +2443,12 @@ static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname, blocksize); /* initialize hashing info */ - dx_info = dx_get_dx_info(dot_de); + 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)) @@ -2483,7 +2506,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; } @@ -2759,8 +2782,13 @@ 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); - info = dx_get_dx_info((struct ext4_dir_entry_2 *) + 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", @@ -2788,7 +2816,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 */ @@ -4463,6 +4491,29 @@ int ext4_dirdata_set_lufid(struct inode *dir, const char *filename, } EXT4_I(inode)->i_dirdata = old_dirdata; + if (err) { + /* + * The original entry was already removed above and the + * re-add with the new LUFID failed; try to restore the + * original entry so the inode isn't left without any + * directory entry pointing at it. + */ + struct dentry parent_dentry = { .d_inode = dir }; + struct dentry orig_dentry = { + .d_name = d_name, + .d_parent = &parent_dentry, + .d_inode = inode, + }; + int rollback_err = ext4_add_entry(handle, &orig_dentry, inode); + + if (rollback_err) + EXT4_ERROR_INODE(dir, + "Failed to set LUFID on '%.*s' (err=%d) and failed to restore the original directory entry (err=%d); inode %llu may be orphaned", + namelen, filename, err, rollback_err, + inode->i_ino); + goto out_unlock; + } + /* Update inode times */ inode_set_ctime_current(dir); inode_inc_iversion(dir);