| From 0f375eed92b5a407657532637ed9652611a682f5 Mon Sep 17 00:00:00 2001 |
| From: Filipe Manana <fdmanana@suse.com> |
| Date: Tue, 9 Oct 2018 15:05:29 +0100 |
| Subject: Btrfs: fix wrong dentries after fsync of file that got its parent replaced |
| |
| From: Filipe Manana <fdmanana@suse.com> |
| |
| commit 0f375eed92b5a407657532637ed9652611a682f5 upstream. |
| |
| In a scenario like the following: |
| |
| mkdir /mnt/A # inode 258 |
| mkdir /mnt/B # inode 259 |
| touch /mnt/B/bar # inode 260 |
| |
| sync |
| |
| mv /mnt/B/bar /mnt/A/bar |
| mv -T /mnt/A /mnt/B |
| fsync /mnt/B/bar |
| |
| <power fail> |
| |
| After replaying the log we end up with file bar having 2 hard links, both |
| with the name 'bar' and one in the directory with inode number 258 and the |
| other in the directory with inode number 259. Also, we end up with the |
| directory inode 259 still existing and with the directory inode 258 still |
| named as 'A', instead of 'B'. In this scenario, file 'bar' should only |
| have one hard link, located at directory inode 258, the directory inode |
| 259 should not exist anymore and the name for directory inode 258 should |
| be 'B'. |
| |
| This incorrect behaviour happens because when attempting to log the old |
| parents of an inode, we skip any parents that no longer exist. Fix this |
| by forcing a full commit if an old parent no longer exists. |
| |
| A test case for fstests follows soon. |
| |
| CC: stable@vger.kernel.org # 4.4+ |
| Signed-off-by: Filipe Manana <fdmanana@suse.com> |
| Signed-off-by: David Sterba <dsterba@suse.com> |
| Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org> |
| |
| --- |
| fs/btrfs/tree-log.c | 30 +++++++++++++++++++++++++++--- |
| 1 file changed, 27 insertions(+), 3 deletions(-) |
| |
| --- a/fs/btrfs/tree-log.c |
| +++ b/fs/btrfs/tree-log.c |
| @@ -5373,9 +5373,33 @@ static int btrfs_log_all_parents(struct |
| |
| dir_inode = btrfs_iget(root->fs_info->sb, &inode_key, |
| root, NULL); |
| - /* If parent inode was deleted, skip it. */ |
| - if (IS_ERR(dir_inode)) |
| - continue; |
| + /* |
| + * If the parent inode was deleted, return an error to |
| + * fallback to a transaction commit. This is to prevent |
| + * getting an inode that was moved from one parent A to |
| + * a parent B, got its former parent A deleted and then |
| + * it got fsync'ed, from existing at both parents after |
| + * a log replay (and the old parent still existing). |
| + * Example: |
| + * |
| + * mkdir /mnt/A |
| + * mkdir /mnt/B |
| + * touch /mnt/B/bar |
| + * sync |
| + * mv /mnt/B/bar /mnt/A/bar |
| + * mv -T /mnt/A /mnt/B |
| + * fsync /mnt/B/bar |
| + * <power fail> |
| + * |
| + * If we ignore the old parent B which got deleted, |
| + * after a log replay we would have file bar linked |
| + * at both parents and the old parent B would still |
| + * exist. |
| + */ |
| + if (IS_ERR(dir_inode)) { |
| + ret = PTR_ERR(dir_inode); |
| + goto out; |
| + } |
| |
| if (ctx) |
| ctx->log_new_dentries = false; |