• Filipe Manana's avatar
    Btrfs: fix file corruption and data loss after cloning inline extents · 8039d87d
    Filipe Manana authored
    Currently the clone ioctl allows to clone an inline extent from one file
    to another that already has other (non-inlined) extents. This is a problem
    because btrfs is not designed to deal with files having inline and regular
    extents, if a file has an inline extent then it must be the only extent
    in the file and must start at file offset 0. Having a file with an inline
    extent followed by regular extents results in EIO errors when doing reads
    or writes against the first 4K of the file.
    
    Also, the clone ioctl allows one to lose data if the source file consists
    of a single inline extent, with a size of N bytes, and the destination
    file consists of a single inline extent with a size of M bytes, where we
    have M > N. In this case the clone operation removes the inline extent
    from the destination file and then copies the inline extent from the
    source file into the destination file - we lose the M - N bytes from the
    destination file, a read operation will get the value 0x00 for any bytes
    in the the range [N, M] (the destination inode's i_size remained as M,
    that's why we can read past N bytes).
    
    So fix this by not allowing such destructive operations to happen and
    return errno EOPNOTSUPP to user space.
    
    Currently the fstest btrfs/035 tests the data loss case but it totally
    ignores this - i.e. expects the operation to succeed and does not check
    the we got data loss.
    
    The following test case for fstests exercises all these cases that result
    in file corruption and data loss:
    
      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 -f $tmp.*
      }
    
      # get standard environment, filters and checks
      . ./common/rc
      . ./common/filter
    
      # real QA test starts here
      _need_to_be_root
      _supported_fs btrfs
      _supported_os Linux
      _require_scratch
      _require_cloner
      _require_btrfs_fs_feature "no_holes"
      _require_btrfs_mkfs_feature "no-holes"
    
      rm -f $seqres.full
    
      test_cloning_inline_extents()
      {
          local mkfs_opts=$1
          local mount_opts=$2
    
          _scratch_mkfs $mkfs_opts >>$seqres.full 2>&1
          _scratch_mount $mount_opts
    
          # File bar, the source for all the following clone operations, consists
          # of a single inline extent (50 bytes).
          $XFS_IO_PROG -f -c "pwrite -S 0xbb 0 50" $SCRATCH_MNT/bar \
              | _filter_xfs_io
    
          # Test cloning into a file with an extent (non-inlined) where the
          # destination offset overlaps that extent. It should not be possible to
          # clone the inline extent from file bar into this file.
          $XFS_IO_PROG -f -c "pwrite -S 0xaa 0K 16K" $SCRATCH_MNT/foo \
              | _filter_xfs_io
          $CLONER_PROG -s 0 -d 0 -l 0 $SCRATCH_MNT/bar $SCRATCH_MNT/foo
    
          # Doing IO against any range in the first 4K of the file should work.
          # Due to a past clone ioctl bug which allowed cloning the inline extent,
          # these operations resulted in EIO errors.
          echo "File foo data after clone operation:"
          # All bytes should have the value 0xaa (clone operation failed and did
          # not modify our file).
          od -t x1 $SCRATCH_MNT/foo
          $XFS_IO_PROG -c "pwrite -S 0xcc 0 100" $SCRATCH_MNT/foo | _filter_xfs_io
    
          # Test cloning the inline extent against a file which has a hole in its
          # first 4K followed by a non-inlined extent. It should not be possible
          # as well to clone the inline extent from file bar into this file.
          $XFS_IO_PROG -f -c "pwrite -S 0xdd 4K 12K" $SCRATCH_MNT/foo2 \
              | _filter_xfs_io
          $CLONER_PROG -s 0 -d 0 -l 0 $SCRATCH_MNT/bar $SCRATCH_MNT/foo2
    
          # Doing IO against any range in the first 4K of the file should work.
          # Due to a past clone ioctl bug which allowed cloning the inline extent,
          # these operations resulted in EIO errors.
          echo "File foo2 data after clone operation:"
          # All bytes should have the value 0x00 (clone operation failed and did
          # not modify our file).
          od -t x1 $SCRATCH_MNT/foo2
          $XFS_IO_PROG -c "pwrite -S 0xee 0 90" $SCRATCH_MNT/foo2 | _filter_xfs_io
    
          # Test cloning the inline extent against a file which has a size of zero
          # but has a prealloc extent. It should not be possible as well to clone
          # the inline extent from file bar into this file.
          $XFS_IO_PROG -f -c "falloc -k 0 1M" $SCRATCH_MNT/foo3 | _filter_xfs_io
          $CLONER_PROG -s 0 -d 0 -l 0 $SCRATCH_MNT/bar $SCRATCH_MNT/foo3
    
          # Doing IO against any range in the first 4K of the file should work.
          # Due to a past clone ioctl bug which allowed cloning the inline extent,
          # these operations resulted in EIO errors.
          echo "First 50 bytes of foo3 after clone operation:"
          # Should not be able to read any bytes, file has 0 bytes i_size (the
          # clone operation failed and did not modify our file).
          od -t x1 $SCRATCH_MNT/foo3
          $XFS_IO_PROG -c "pwrite -S 0xff 0 90" $SCRATCH_MNT/foo3 | _filter_xfs_io
    
          # Test cloning the inline extent against a file which consists of a
          # single inline extent that has a size not greater than the size of
          # bar's inline extent (40 < 50).
          # It should be possible to do the extent cloning from bar to this file.
          $XFS_IO_PROG -f -c "pwrite -S 0x01 0 40" $SCRATCH_MNT/foo4 \
              | _filter_xfs_io
          $CLONER_PROG -s 0 -d 0 -l 0 $SCRATCH_MNT/bar $SCRATCH_MNT/foo4
    
          # Doing IO against any range in the first 4K of the file should work.
          echo "File foo4 data after clone operation:"
          # Must match file bar's content.
          od -t x1 $SCRATCH_MNT/foo4
          $XFS_IO_PROG -c "pwrite -S 0x02 0 90" $SCRATCH_MNT/foo4 | _filter_xfs_io
    
          # Test cloning the inline extent against a file which consists of a
          # single inline extent that has a size greater than the size of bar's
          # inline extent (60 > 50).
          # It should not be possible to clone the inline extent from file bar
          # into this file.
          $XFS_IO_PROG -f -c "pwrite -S 0x03 0 60" $SCRATCH_MNT/foo5 \
              | _filter_xfs_io
          $CLONER_PROG -s 0 -d 0 -l 0 $SCRATCH_MNT/bar $SCRATCH_MNT/foo5
    
          # Reading the file should not fail.
          echo "File foo5 data after clone operation:"
          # Must have a size of 60 bytes, with all bytes having a value of 0x03
          # (the clone operation failed and did not modify our file).
          od -t x1 $SCRATCH_MNT/foo5
    
          # Test cloning the inline extent against a file which has no extents but
          # has a size greater than bar's inline extent (16K > 50).
          # It should not be possible to clone the inline extent from file bar
          # into this file.
          $XFS_IO_PROG -f -c "truncate 16K" $SCRATCH_MNT/foo6 | _filter_xfs_io
          $CLONER_PROG -s 0 -d 0 -l 0 $SCRATCH_MNT/bar $SCRATCH_MNT/foo6
    
          # Reading the file should not fail.
          echo "File foo6 data after clone operation:"
          # Must have a size of 16K, with all bytes having a value of 0x00 (the
          # clone operation failed and did not modify our file).
          od -t x1 $SCRATCH_MNT/foo6
    
          # Test cloning the inline extent against a file which has no extents but
          # has a size not greater than bar's inline extent (30 < 50).
          # It should be possible to clone the inline extent from file bar into
          # this file.
          $XFS_IO_PROG -f -c "truncate 30" $SCRATCH_MNT/foo7 | _filter_xfs_io
          $CLONER_PROG -s 0 -d 0 -l 0 $SCRATCH_MNT/bar $SCRATCH_MNT/foo7
    
          # Reading the file should not fail.
          echo "File foo7 data after clone operation:"
          # Must have a size of 50 bytes, with all bytes having a value of 0xbb.
          od -t x1 $SCRATCH_MNT/foo7
    
          # Test cloning the inline extent against a file which has a size not
          # greater than the size of bar's inline extent (20 < 50) but has
          # a prealloc extent that goes beyond the file's size. It should not be
          # possible to clone the inline extent from bar into this file.
          $XFS_IO_PROG -f -c "falloc -k 0 1M" \
                          -c "pwrite -S 0x88 0 20" \
                          $SCRATCH_MNT/foo8 | _filter_xfs_io
          $CLONER_PROG -s 0 -d 0 -l 0 $SCRATCH_MNT/bar $SCRATCH_MNT/foo8
    
          echo "File foo8 data after clone operation:"
          # Must have a size of 20 bytes, with all bytes having a value of 0x88
          # (the clone operation did not modify our file).
          od -t x1 $SCRATCH_MNT/foo8
    
          _scratch_unmount
      }
    
      echo -e "\nTesting without compression and without the no-holes feature...\n"
      test_cloning_inline_extents
    
      echo -e "\nTesting with compression and without the no-holes feature...\n"
      test_cloning_inline_extents "" "-o compress"
    
      echo -e "\nTesting without compression and with the no-holes feature...\n"
      test_cloning_inline_extents "-O no-holes" ""
    
      echo -e "\nTesting with compression and with the no-holes feature...\n"
      test_cloning_inline_extents "-O no-holes" "-o compress"
    
      status=0
      exit
    
    Cc: stable@vger.kernel.org
    Signed-off-by: default avatarFilipe Manana <fdmanana@suse.com>
    8039d87d
ioctl.c 137 KB