| From d80d448c6c5bdd32605b78a60fe8081d82d4da0f Mon Sep 17 00:00:00 2001 |
| From: "Darrick J. Wong" <darrick.wong@oracle.com> |
| Date: Wed, 27 Aug 2014 18:40:09 -0400 |
| Subject: ext4: fix same-dir rename when inline data directory overflows |
| |
| From: "Darrick J. Wong" <darrick.wong@oracle.com> |
| |
| commit d80d448c6c5bdd32605b78a60fe8081d82d4da0f upstream. |
| |
| When performing a same-directory rename, it's possible that adding or |
| setting the new directory entry will cause the directory to overflow |
| the inline data area, which causes the directory to be converted to an |
| extent-based directory. Under this circumstance it is necessary to |
| re-read the directory when deleting the old dirent because the "old |
| directory" context still points to i_block in the inode table, which |
| is now an extent tree root! The delete fails with an FS error, and |
| the subsequent fsck complains about incorrect link counts and |
| hardlinked directories. |
| |
| Test case (originally found with flat_dir_test in the metadata_csum |
| test program): |
| |
| # mkfs.ext4 -O inline_data /dev/sda |
| # mount /dev/sda /mnt |
| # mkdir /mnt/x |
| # touch /mnt/x/changelog.gz /mnt/x/copyright /mnt/x/README.Debian |
| # sync |
| # for i in /mnt/x/*; do mv $i $i.longer; done |
| # ls -la /mnt/x/ |
| total 0 |
| -rw-r--r-- 1 root root 0 Aug 25 12:03 changelog.gz.longer |
| -rw-r--r-- 1 root root 0 Aug 25 12:03 copyright |
| -rw-r--r-- 1 root root 0 Aug 25 12:03 copyright.longer |
| -rw-r--r-- 1 root root 0 Aug 25 12:03 README.Debian.longer |
| |
| (Hey! Why are there four files now??) |
| |
| Signed-off-by: Darrick J. Wong <darrick.wong@oracle.com> |
| Signed-off-by: Theodore Ts'o <tytso@mit.edu> |
| Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org> |
| |
| --- |
| fs/ext4/namei.c | 21 ++++++++++++++++++--- |
| 1 file changed, 18 insertions(+), 3 deletions(-) |
| |
| --- a/fs/ext4/namei.c |
| +++ b/fs/ext4/namei.c |
| @@ -3128,7 +3128,8 @@ static int ext4_find_delete_entry(handle |
| return retval; |
| } |
| |
| -static void ext4_rename_delete(handle_t *handle, struct ext4_renament *ent) |
| +static void ext4_rename_delete(handle_t *handle, struct ext4_renament *ent, |
| + int force_reread) |
| { |
| int retval; |
| /* |
| @@ -3140,7 +3141,8 @@ static void ext4_rename_delete(handle_t |
| if (le32_to_cpu(ent->de->inode) != ent->inode->i_ino || |
| ent->de->name_len != ent->dentry->d_name.len || |
| strncmp(ent->de->name, ent->dentry->d_name.name, |
| - ent->de->name_len)) { |
| + ent->de->name_len) || |
| + force_reread) { |
| retval = ext4_find_delete_entry(handle, ent->dir, |
| &ent->dentry->d_name); |
| } else { |
| @@ -3191,6 +3193,7 @@ static int ext4_rename(struct inode *old |
| .dentry = new_dentry, |
| .inode = new_dentry->d_inode, |
| }; |
| + int force_reread; |
| int retval; |
| |
| dquot_initialize(old.dir); |
| @@ -3246,6 +3249,15 @@ static int ext4_rename(struct inode *old |
| if (retval) |
| goto end_rename; |
| } |
| + /* |
| + * If we're renaming a file within an inline_data dir and adding or |
| + * setting the new dirent causes a conversion from inline_data to |
| + * extents/blockmap, we need to force the dirent delete code to |
| + * re-read the directory, or else we end up trying to delete a dirent |
| + * from what is now the extent tree root (or a block map). |
| + */ |
| + force_reread = (new.dir->i_ino == old.dir->i_ino && |
| + ext4_test_inode_flag(new.dir, EXT4_INODE_INLINE_DATA)); |
| if (!new.bh) { |
| retval = ext4_add_entry(handle, new.dentry, old.inode); |
| if (retval) |
| @@ -3256,6 +3268,9 @@ static int ext4_rename(struct inode *old |
| if (retval) |
| goto end_rename; |
| } |
| + if (force_reread) |
| + force_reread = !ext4_test_inode_flag(new.dir, |
| + EXT4_INODE_INLINE_DATA); |
| |
| /* |
| * Like most other Unix systems, set the ctime for inodes on a |
| @@ -3267,7 +3282,7 @@ static int ext4_rename(struct inode *old |
| /* |
| * ok, that's it |
| */ |
| - ext4_rename_delete(handle, &old); |
| + ext4_rename_delete(handle, &old, force_reread); |
| |
| if (new.inode) { |
| ext4_dec_count(handle, new.inode); |