]> git.hungrycats.org Git - linux/commitdiff
Btrfs: fix stale directory entries after fsync log replay
authorJeff Mahoney <jeffm@suse.com>
Mon, 24 Aug 2015 16:19:43 +0000 (12:19 -0400)
committerZygo Blaxell <zblaxell@thirteen.furryterror.org>
Mon, 31 Aug 2015 22:08:50 +0000 (18:08 -0400)
(bsc#942925).

suse-commit: 8152b37d7e8d49cf32f532d70e9be8a87e3fb92d
(cherry picked from commit 4de4f38569bdc4f8c0edb1c2fca08b63052fd7a2)

fs/btrfs/tree-log.c

index 040d2b0693d86edf02b216fa91bafe483a19ecbe..146cd1557e63b8151e8ac6e4e12e9ce15222aada 100644 (file)
@@ -1612,6 +1612,9 @@ static bool name_in_log_ref(struct btrfs_root *log_root,
  * not exist in the FS, it is skipped.  fsyncs on directories
  * do not force down inodes inside that directory, just changes to the
  * names or unlinks in a directory.
+ *
+ * Returns < 0 on error, 0 if the name wasn't replayed (dentry points to a
+ * non-existing inode) and 1 if the name was replayed.
  */
 static noinline int replay_one_name(struct btrfs_trans_handle *trans,
                                    struct btrfs_root *root,
@@ -1630,6 +1633,7 @@ static noinline int replay_one_name(struct btrfs_trans_handle *trans,
        int exists;
        int ret = 0;
        bool update_size = (key->type == BTRFS_DIR_INDEX_KEY);
+       bool name_added = false;
 
        dir = read_one_inode(root, key->objectid);
        if (!dir)
@@ -1707,6 +1711,8 @@ out:
        }
        kfree(name);
        iput(dir);
+       if (!ret && name_added)
+               ret = 1;
        return ret;
 
 insert:
@@ -1722,6 +1728,8 @@ insert:
                              name, name_len, &log_key);
        if (ret && ret != -ENOENT && ret != -EEXIST)
                goto out;
+       if (!ret)
+               name_added = true;
        update_size = false;
        ret = 0;
        goto out;
@@ -1739,12 +1747,13 @@ static noinline int replay_one_dir_item(struct btrfs_trans_handle *trans,
                                        struct extent_buffer *eb, int slot,
                                        struct btrfs_key *key)
 {
-       int ret;
+       int ret = 0;
        u32 item_size = btrfs_item_size_nr(eb, slot);
        struct btrfs_dir_item *di;
        int name_len;
        unsigned long ptr;
        unsigned long ptr_end;
+       struct btrfs_path *fixup_path = NULL;
 
        ptr = btrfs_item_ptr_offset(eb, slot);
        ptr_end = ptr + item_size;
@@ -1754,12 +1763,59 @@ static noinline int replay_one_dir_item(struct btrfs_trans_handle *trans,
                        return -EIEIO;
                name_len = btrfs_dir_name_len(eb, di);
                ret = replay_one_name(trans, root, path, eb, di, key);
-               if (ret)
-                       return ret;
+               if (ret < 0)
+                       break;
                ptr = (unsigned long)(di + 1);
                ptr += name_len;
+
+               /*
+                * If this entry refers to a non-directory (directories can not
+                * have a link count > 1) and it was added in the transaction
+                * that was not committed, make sure we fixup the link count of
+                * the inode it the entry points to. Otherwise something like
+                * the following would result in a directory pointing to an
+                * inode with a wrong link that does not account for this dir
+                * entry:
+                *
+                * mkdir testdir
+                * touch testdir/foo
+                * touch testdir/bar
+                * sync
+                *
+                * ln testdir/bar testdir/bar_link
+                * ln testdir/foo testdir/foo_link
+                * xfs_io -c "fsync" testdir/bar
+                *
+                * <power failure>
+                *
+                * mount fs, log replay happens
+                *
+                * File foo would remain with a link count of 1 when it has two
+                * entries pointing to it in the directory testdir. This would
+                * make it impossible to ever delete the parent directory has
+                * it would result in stale dentries that can never be deleted.
+                */
+               if (ret == 1 && btrfs_dir_type(eb, di) != BTRFS_FT_DIR) {
+                       struct btrfs_key di_key;
+
+                       if (!fixup_path) {
+                               fixup_path = btrfs_alloc_path();
+                               if (!fixup_path) {
+                                       ret = -ENOMEM;
+                                       break;
+                               }
+                       }
+
+                       btrfs_dir_item_key_to_cpu(eb, di, &di_key);
+                       ret = link_to_fixup_dir(trans, root, fixup_path,
+                                               di_key.objectid);
+                       if (ret)
+                               break;
+               }
+               ret = 0;
        }
-       return 0;
+       btrfs_free_path(fixup_path);
+       return ret;
 }
 
 /*