Btrfs: send, fix file corruption due to incorrect cloning operations
authorFilipe Manana <fdmanana@suse.com>
Fri, 2 Oct 2015 09:47:34 +0000 (10:47 +0100)
committerFilipe Manana <fdmanana@suse.com>
Tue, 13 Oct 2015 00:05:27 +0000 (01:05 +0100)
If we have a file that shares an extent with other files, when processing
the extent item relative to a shared extent, we blindly issue a clone
operation that will target a length matching the length in the extent item
and uses as a source some other file the receiver already has and points
to the same extent. However that range in the other file might not
exclusively point only to the shared extent, and so using that length
will result in the receiver getting a file with different data from the
one in the send snapshot. This issue happened both for incremental and
full send operations.

So fix this by issuing clone operations with lengths that don't cover
regions of the source file that point to different extents (or have holes).

The following test case for fstests reproduces the problem.

  seq=`basename $0`
  seqres=$RESULT_DIR/$seq
  echo "QA output created by $seq"

  tmp=/tmp/$$
  status=1 # failure is the default!
  trap "_cleanup; exit \$status" 0 1 2 3 15

  _cleanup()
  {
      rm -fr $send_files_dir
      rm -f $tmp.*
  }

  # get standard environment, filters and checks
  . ./common/rc
  . ./common/filter

  # real QA test starts here
  _supported_fs btrfs
  _supported_os Linux
  _require_scratch
  _need_to_be_root
  _require_cp_reflink
  _require_xfs_io_command "fpunch"

  send_files_dir=$TEST_DIR/btrfs-test-$seq

  rm -f $seqres.full
  rm -fr $send_files_dir
  mkdir $send_files_dir

  _scratch_mkfs >>$seqres.full 2>&1
  _scratch_mount

  # Create our test file with a single 100K extent.
  $XFS_IO_PROG -f -c "pwrite -S 0xaa 0K 100K" \
     $SCRATCH_MNT/foo | _filter_xfs_io

  # Clone our file into a new file named bar.
  cp --reflink=always $SCRATCH_MNT/foo $SCRATCH_MNT/bar

  # Now overwrite parts of our foo file.
  $XFS_IO_PROG -c "pwrite -S 0xbb 50K 10K" \
     -c "pwrite -S 0xcc 90K 10K" \
     -c "fpunch 70K 10k" \
     $SCRATCH_MNT/foo | _filter_xfs_io

  _run_btrfs_util_prog subvolume snapshot -r $SCRATCH_MNT \
     $SCRATCH_MNT/snap

  echo "File digests in the original filesystem:"
  md5sum $SCRATCH_MNT/snap/foo | _filter_scratch
  md5sum $SCRATCH_MNT/snap/bar | _filter_scratch

  _run_btrfs_util_prog send $SCRATCH_MNT/snap -f $send_files_dir/1.snap

  # Now recreate the filesystem by receiving the send stream and verify
  # we get the same file contents that the original filesystem had.
  _scratch_unmount
  _scratch_mkfs >>$seqres.full 2>&1
  _scratch_mount

  _run_btrfs_util_prog receive $SCRATCH_MNT -f $send_files_dir/1.snap

  # We expect the destination filesystem to have exactly the same file
  # data as the original filesystem.
  # The btrfs send implementation had a bug where it sent a clone
  # operation from file foo into file bar covering the whole [0, 100K[
  # range after creating and writing the file foo. This was incorrect
  # because the file bar now included the updates done to file foo after
  # we cloned foo to bar, breaking the COW nature of reflink copies
  # (cloned extents).
  echo "File digests in the new filesystem:"
  md5sum $SCRATCH_MNT/snap/foo | _filter_scratch
  md5sum $SCRATCH_MNT/snap/bar | _filter_scratch

  status=0
  exit

Another test case that reproduces the problem when we have compressed
extents:

  seq=`basename $0`
  seqres=$RESULT_DIR/$seq
  echo "QA output created by $seq"

  tmp=/tmp/$$
  status=1 # failure is the default!
  trap "_cleanup; exit \$status" 0 1 2 3 15

  _cleanup()
  {
      rm -fr $send_files_dir
      rm -f $tmp.*
  }

  # get standard environment, filters and checks
  . ./common/rc
  . ./common/filter

  # real QA test starts here
  _supported_fs btrfs
  _supported_os Linux
  _require_scratch
  _need_to_be_root
  _require_cp_reflink

  send_files_dir=$TEST_DIR/btrfs-test-$seq

  rm -f $seqres.full
  rm -fr $send_files_dir
  mkdir $send_files_dir

  _scratch_mkfs >>$seqres.full 2>&1
  _scratch_mount "-o compress"

  # Create our file with an extent of 100K starting at file offset 0K.
  $XFS_IO_PROG -f -c "pwrite -S 0xaa 0K 100K"       \
                  -c "fsync"                        \
                  $SCRATCH_MNT/foo | _filter_xfs_io

  # Rewrite part of the previous extent (its first 40K) and write a new
  # 100K extent starting at file offset 100K.
  $XFS_IO_PROG -c "pwrite -S 0xbb 0K 40K"    \
          -c "pwrite -S 0xcc 100K 100K"      \
          $SCRATCH_MNT/foo | _filter_xfs_io

  # Our file foo now has 3 file extent items in its metadata:
  #
  # 1) One covering the file range 0 to 40K;
  # 2) One covering the file range 40K to 100K, which points to the first
  #    extent we wrote to the file and has a data offset field with value
  #    40K (our file no longer uses the first 40K of data from that
  #    extent);
  # 3) One covering the file range 100K to 200K.

  # Now clone our file foo into file bar.
  cp --reflink=always $SCRATCH_MNT/foo $SCRATCH_MNT/bar

  # Create our snapshot for the send operation.
  _run_btrfs_util_prog subvolume snapshot -r $SCRATCH_MNT \
          $SCRATCH_MNT/snap

  echo "File digests in the original filesystem:"
  md5sum $SCRATCH_MNT/snap/foo | _filter_scratch
  md5sum $SCRATCH_MNT/snap/bar | _filter_scratch

  _run_btrfs_util_prog send $SCRATCH_MNT/snap -f $send_files_dir/1.snap

  # Now recreate the filesystem by receiving the send stream and verify we
  # get the same file contents that the original filesystem had.
  # Btrfs send used to issue a clone operation from foo's range
  # [80K, 140K[ to bar's range [40K, 100K[ when cloning the extent pointed
  # to by foo's second file extent item, this was incorrect because of bad
  # accounting of the file extent item's data offset field. The correct
  # range to clone from should have been [40K, 100K[.
  _scratch_unmount
  _scratch_mkfs >>$seqres.full 2>&1
  _scratch_mount "-o compress"

  _run_btrfs_util_prog receive $SCRATCH_MNT -f $send_files_dir/1.snap

  echo "File digests in the new filesystem:"
  # Must match the digests we got in the original filesystem.
  md5sum $SCRATCH_MNT/snap/foo | _filter_scratch
  md5sum $SCRATCH_MNT/snap/bar | _filter_scratch

  status=0
  exit

Signed-off-by: Filipe Manana <fdmanana@suse.com>
fs/btrfs/send.c

index b5d47b9400ba5a9e9da868a9751881e14baff315..ced935bf5f957a89b621dcedd2cacb0fe85136e7 100644 (file)
@@ -1434,16 +1434,6 @@ verbose_printk(KERN_DEBUG "btrfs: find_extent_clone: data_offset=%llu, "
        }
 
        if (cur_clone_root) {
-               if (compressed != BTRFS_COMPRESS_NONE) {
-                       /*
-                        * Offsets given by iterate_extent_inodes() are relative
-                        * to the start of the extent, we need to add logical
-                        * offset from the file extent item.
-                        * (See why at backref.c:check_extent_in_eb())
-                        */
-                       cur_clone_root->offset += btrfs_file_extent_offset(eb,
-                                                                          fi);
-               }
                *found = cur_clone_root;
                ret = 0;
        } else {
@@ -4687,6 +4677,171 @@ tlv_put_failure:
        return ret;
 }
 
+static int send_extent_data(struct send_ctx *sctx,
+                           const u64 offset,
+                           const u64 len)
+{
+       u64 sent = 0;
+
+       if (sctx->flags & BTRFS_SEND_FLAG_NO_FILE_DATA)
+               return send_update_extent(sctx, offset, len);
+
+       while (sent < len) {
+               u64 size = len - sent;
+               int ret;
+
+               if (size > BTRFS_SEND_READ_SIZE)
+                       size = BTRFS_SEND_READ_SIZE;
+               ret = send_write(sctx, offset + sent, size);
+               if (ret < 0)
+                       return ret;
+               if (!ret)
+                       break;
+               sent += ret;
+       }
+       return 0;
+}
+
+static int clone_range(struct send_ctx *sctx,
+                      struct clone_root *clone_root,
+                      const u64 disk_byte,
+                      u64 data_offset,
+                      u64 offset,
+                      u64 len)
+{
+       struct btrfs_path *path;
+       struct btrfs_key key;
+       int ret;
+
+       path = alloc_path_for_send();
+       if (!path)
+               return -ENOMEM;
+
+       /*
+        * We can't send a clone operation for the entire range if we find
+        * extent items in the respective range in the source file that
+        * refer to different extents or if we find holes.
+        * So check for that and do a mix of clone and regular write/copy
+        * operations if needed.
+        *
+        * Example:
+        *
+        * mkfs.btrfs -f /dev/sda
+        * mount /dev/sda /mnt
+        * xfs_io -f -c "pwrite -S 0xaa 0K 100K" /mnt/foo
+        * cp --reflink=always /mnt/foo /mnt/bar
+        * xfs_io -c "pwrite -S 0xbb 50K 50K" /mnt/foo
+        * btrfs subvolume snapshot -r /mnt /mnt/snap
+        *
+        * If when we send the snapshot and we are processing file bar (which
+        * has a higher inode number than foo) we blindly send a clone operation
+        * for the [0, 100K[ range from foo to bar, the receiver ends up getting
+        * a file bar that matches the content of file foo - iow, doesn't match
+        * the content from bar in the original filesystem.
+        */
+       key.objectid = clone_root->ino;
+       key.type = BTRFS_EXTENT_DATA_KEY;
+       key.offset = clone_root->offset;
+       ret = btrfs_search_slot(NULL, clone_root->root, &key, path, 0, 0);
+       if (ret < 0)
+               goto out;
+       if (ret > 0 && path->slots[0] > 0) {
+               btrfs_item_key_to_cpu(path->nodes[0], &key, path->slots[0] - 1);
+               if (key.objectid == clone_root->ino &&
+                   key.type == BTRFS_EXTENT_DATA_KEY)
+                       path->slots[0]--;
+       }
+
+       while (true) {
+               struct extent_buffer *leaf = path->nodes[0];
+               int slot = path->slots[0];
+               struct btrfs_file_extent_item *ei;
+               u8 type;
+               u64 ext_len;
+               u64 clone_len;
+
+               if (slot >= btrfs_header_nritems(leaf)) {
+                       ret = btrfs_next_leaf(clone_root->root, path);
+                       if (ret < 0)
+                               goto out;
+                       else if (ret > 0)
+                               break;
+                       continue;
+               }
+
+               btrfs_item_key_to_cpu(leaf, &key, slot);
+
+               /*
+                * We might have an implicit trailing hole (NO_HOLES feature
+                * enabled). We deal with it after leaving this loop.
+                */
+               if (key.objectid != clone_root->ino ||
+                   key.type != BTRFS_EXTENT_DATA_KEY)
+                       break;
+
+               ei = btrfs_item_ptr(leaf, slot, struct btrfs_file_extent_item);
+               type = btrfs_file_extent_type(leaf, ei);
+               if (type == BTRFS_FILE_EXTENT_INLINE) {
+                       ext_len = btrfs_file_extent_inline_len(leaf, slot, ei);
+                       ext_len = PAGE_CACHE_ALIGN(ext_len);
+               } else {
+                       ext_len = btrfs_file_extent_num_bytes(leaf, ei);
+               }
+
+               if (key.offset + ext_len <= clone_root->offset)
+                       goto next;
+
+               if (key.offset > clone_root->offset) {
+                       /* Implicit hole, NO_HOLES feature enabled. */
+                       u64 hole_len = key.offset - clone_root->offset;
+
+                       if (hole_len > len)
+                               hole_len = len;
+                       ret = send_extent_data(sctx, offset, hole_len);
+                       if (ret < 0)
+                               goto out;
+
+                       len -= hole_len;
+                       if (len == 0)
+                               break;
+                       offset += hole_len;
+                       clone_root->offset += hole_len;
+                       data_offset += hole_len;
+               }
+
+               if (key.offset >= clone_root->offset + len)
+                       break;
+
+               clone_len = min_t(u64, ext_len, len);
+
+               if (btrfs_file_extent_disk_bytenr(leaf, ei) == disk_byte &&
+                   btrfs_file_extent_offset(leaf, ei) == data_offset)
+                       ret = send_clone(sctx, offset, clone_len, clone_root);
+               else
+                       ret = send_extent_data(sctx, offset, clone_len);
+
+               if (ret < 0)
+                       goto out;
+
+               len -= clone_len;
+               if (len == 0)
+                       break;
+               offset += clone_len;
+               clone_root->offset += clone_len;
+               data_offset += clone_len;
+next:
+               path->slots[0]++;
+       }
+
+       if (len > 0)
+               ret = send_extent_data(sctx, offset, len);
+       else
+               ret = 0;
+out:
+       btrfs_free_path(path);
+       return ret;
+}
+
 static int send_write_or_clone(struct send_ctx *sctx,
                               struct btrfs_path *path,
                               struct btrfs_key *key,
@@ -4695,9 +4850,7 @@ static int send_write_or_clone(struct send_ctx *sctx,
        int ret = 0;
        struct btrfs_file_extent_item *ei;
        u64 offset = key->offset;
-       u64 pos = 0;
        u64 len;
-       u32 l;
        u8 type;
        u64 bs = sctx->send_root->fs_info->sb->s_blocksize;
 
@@ -4725,22 +4878,15 @@ static int send_write_or_clone(struct send_ctx *sctx,
        }
 
        if (clone_root && IS_ALIGNED(offset + len, bs)) {
-               ret = send_clone(sctx, offset, len, clone_root);
-       } else if (sctx->flags & BTRFS_SEND_FLAG_NO_FILE_DATA) {
-               ret = send_update_extent(sctx, offset, len);
+               u64 disk_byte;
+               u64 data_offset;
+
+               disk_byte = btrfs_file_extent_disk_bytenr(path->nodes[0], ei);
+               data_offset = btrfs_file_extent_offset(path->nodes[0], ei);
+               ret = clone_range(sctx, clone_root, disk_byte, data_offset,
+                                 offset, len);
        } else {
-               while (pos < len) {
-                       l = len - pos;
-                       if (l > BTRFS_SEND_READ_SIZE)
-                               l = BTRFS_SEND_READ_SIZE;
-                       ret = send_write(sctx, pos + offset, l);
-                       if (ret < 0)
-                               goto out;
-                       if (!ret)
-                               break;
-                       pos += ret;
-               }
-               ret = 0;
+               ret = send_extent_data(sctx, offset, len);
        }
 out:
        return ret;