| #! /bin/bash |
| # SPDX-License-Identifier: GPL-2.0 |
| # Copyright (C) 2015 SUSE Linux Products GmbH. All Rights Reserved. |
| # |
| # FSQA Test No. 113 |
| # |
| # Test that truncating a file that consists of a compressed and inlined extent |
| # to a smaller size and then cloning it into another file is not possible and |
| # does not result in leaking stale data (data past the truncation offset) nor |
| # losing data in the clone operation's destination file. |
| # |
| . ./common/preamble |
| _begin_fstest auto quick compress clone |
| |
| # Import common functions. |
| . ./common/filter |
| . ./common/filter.btrfs |
| |
| # real QA test starts here |
| _supported_fs btrfs |
| _require_scratch |
| _require_cloner |
| |
| _scratch_mkfs >>$seqres.full 2>&1 |
| _scratch_mount "-o compress" |
| |
| # Create our test files. File foo is going to be the source of a clone operation |
| # and consists of a single inline extent with an uncompressed size of 512 bytes, |
| # while file bar consists of a single inline extent with an uncompressed size of |
| # 256 bytes. For our test's purpose, it's important that file bar has an inline |
| # extent with a size smaller than foo's inline extent. |
| $XFS_IO_PROG -f -c "pwrite -S 0xa1 0 128" \ |
| -c "pwrite -S 0x2a 128 384" \ |
| $SCRATCH_MNT/foo | _filter_xfs_io |
| $XFS_IO_PROG -f -c "pwrite -S 0xbb 0 256" $SCRATCH_MNT/bar | _filter_xfs_io |
| |
| # Now durably persist all metadata and data. We do this to make sure that we get |
| # on disk an inline extent with a size of 512 bytes for file foo. |
| sync |
| |
| # Now truncate our file foo to a smaller size. Because it consists of a |
| # compressed and inline extent, btrfs did not shrink the inline extent to the |
| # new size (if the extent was not compressed, btrfs would shrink it to 128 |
| # bytes), it only updates the inode's i_size to 128 bytes. |
| $XFS_IO_PROG -c "truncate 128" $SCRATCH_MNT/foo |
| |
| # Now clone foo's inline extent into bar. |
| # This clone operation should fail with errno EOPNOTSUPP because the source |
| # file consists only of an inline extent and the file's size is smaller than |
| # the inline extent of the destination (128 bytes < 256 bytes). However the |
| # clone ioctl was not prepared to deal with a file that has a size smaller |
| # than the size of its inline extent (something that happens only for compressed |
| # inline extents), resulting in copying the full inline extent from the source |
| # file into the destination file. |
| # |
| # Note that btrfs' clone operation for inline extents consists of removing the |
| # inline extent from the destination inode and copy the inline extent from the |
| # source inode into the destination inode, meaning that if the destination |
| # inode's inline extent is larger (N bytes) than the source inode's inline |
| # extent (M bytes), some bytes (N - M bytes) will be lost from the destination |
| # file. Btrfs could copy the source inline extent's data into the destination's |
| # inline extent so that we would not lose any data, but that's currently not |
| # done due to the complexity that would be needed to deal with such cases |
| # (specially when one or both extents are compressed), returning EOPNOTSUPP, as |
| # it's normally not a very common case to clone very small files (only case |
| # where we get inline extents) and copying inline extents does not save any |
| # space (unlike for normal, non-inlined extents). |
| $CLONER_PROG -s 0 -d 0 -l 0 $SCRATCH_MNT/foo $SCRATCH_MNT/bar \ |
| | _filter_btrfs_cloner_error |
| |
| # Now because the above clone operation used to succeed, and due to foo's inline |
| # extent not being shinked by the truncate operation, our file bar got the whole |
| # inline extent copied from foo, making us lose the last 128 bytes from bar |
| # which got replaced by the bytes in range [128, 256[ from foo before foo was |
| # truncated - in other words, data loss from bar and being able to read old and |
| # stale data from foo that should not be possible to read anymore through normal |
| # filesystem operations. Contrast with the case where we truncate a file from a |
| # size N to a smaller size M, truncate it back to size N and then read the range |
| # [M, N[, we should always get the value 0x00 for all the bytes in that range. |
| |
| # We expected the clone operation to fail with errno EOPNOTSUPP and therefore |
| # not modify our file's bar data/metadata. So its content should be 256 bytes |
| # long with all bytes having the value 0xbb. |
| # |
| # Without the btrfs bug fix, the clone operation succeeded and resulted in |
| # leaking truncated data from foo, the bytes that belonged to its range |
| # [128, 256[, and losing data from bar in that same range. So reading the |
| # file gave us the following content: |
| # |
| # 0000000 a1 a1 a1 a1 a1 a1 a1 a1 a1 a1 a1 a1 a1 a1 a1 a1 |
| # * |
| # 0000200 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a |
| # * |
| # 0000400 |
| echo "File bar's content after the clone operation:" |
| od -t x1 $SCRATCH_MNT/bar |
| |
| # Also because the foo's inline extent was not shrunk by the truncate |
| # operation, btrfs' fsck, which is run by the fstests framework everytime a |
| # test completes, failed reporting the following error: |
| # |
| # root 5 inode 257 errors 400, nbytes wrong |
| |
| status=0 |
| exit |