[linux] 03/03: Btrfs: fix truncation of compressed and inlined extents (CVE-2015-8374)
debian-kernel at lists.debian.org
debian-kernel at lists.debian.org
Wed Dec 2 22:51:28 UTC 2015
This is an automated email from the git hooks/post-receive script.
benh pushed a commit to branch jessie-security
in repository linux.
commit ddf23d0c6ae4739987f6459ea46cf58373627aa3
Author: Ben Hutchings <ben at decadent.org.uk>
Date: Wed Dec 2 22:47:56 2015 +0000
Btrfs: fix truncation of compressed and inlined extents (CVE-2015-8374)
---
debian/changelog | 1 +
...runcation-of-compressed-and-inlined-exten.patch | 283 +++++++++++++++++++++
debian/patches/series | 1 +
3 files changed, 285 insertions(+)
diff --git a/debian/changelog b/debian/changelog
index 5deab2d..430d8c4 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -8,6 +8,7 @@ linux (3.16.7-ckt20-1+deb8u1) UNRELEASED; urgency=medium
(CVE-2015-7833, partly fixed in 3.16.7-ckt11-1+deb8u6)
* splice: sendfile() at once fails for big files (Closes: #785189)
* unix: avoid use-after-free in ep_remove_wait_queue (CVE-2013-7446)
+ * Btrfs: fix truncation of compressed and inlined extents (CVE-2015-8374)
-- Salvatore Bonaccorso <carnil at debian.org> Sun, 15 Nov 2015 10:01:10 +0100
diff --git a/debian/patches/bugfix/all/btrfs-fix-truncation-of-compressed-and-inlined-exten.patch b/debian/patches/bugfix/all/btrfs-fix-truncation-of-compressed-and-inlined-exten.patch
new file mode 100644
index 0000000..fa73700
--- /dev/null
+++ b/debian/patches/bugfix/all/btrfs-fix-truncation-of-compressed-and-inlined-exten.patch
@@ -0,0 +1,283 @@
+From: Filipe Manana <fdmanana at suse.com>
+Date: Fri, 16 Oct 2015 12:34:25 +0100
+Subject: Btrfs: fix truncation of compressed and inlined extents
+Origin: https://git.kernel.org/linus/0305cd5f7fca85dae392b9ba85b116896eb7c1c7
+
+When truncating a file to a smaller size which consists of an inline
+extent that is compressed, we did not discard (or made unusable) the
+data between the new file size and the old file size, wasting metadata
+space and allowing for the truncated data to be leaked and the data
+corruption/loss mentioned below.
+We were also not correctly decrementing the number of bytes used by the
+inode, we were setting it to zero, giving a wrong report for callers of
+the stat(2) syscall. The fsck tool also reported an error about a mismatch
+between the nbytes of the file versus the real space used by the file.
+
+Now because we weren't discarding the truncated region of the file, it
+was possible for a caller of the clone ioctl to actually read the data
+that was truncated, allowing for a security breach without requiring root
+access to the system, using only standard filesystem operations. The
+scenario is the following:
+
+ 1) User A creates a file which consists of an inline and compressed
+ extent with a size of 2000 bytes - the file is not accessible to
+ any other users (no read, write or execution permission for anyone
+ else);
+
+ 2) The user truncates the file to a size of 1000 bytes;
+
+ 3) User A makes the file world readable;
+
+ 4) User B creates a file consisting of an inline extent of 2000 bytes;
+
+ 5) User B issues a clone operation from user A's file into its own
+ file (using a length argument of 0, clone the whole range);
+
+ 6) User B now gets to see the 1000 bytes that user A truncated from
+ its file before it made its file world readbale. User B also lost
+ the bytes in the range [1000, 2000[ bytes from its own file, but
+ that might be ok if his/her intention was reading stale data from
+ user A that was never supposed to be public.
+
+Note that this contrasts with the case where we truncate a file from 2000
+bytes to 1000 bytes and then truncate it back from 1000 to 2000 bytes. In
+this case reading any byte from the range [1000, 2000[ will return a value
+of 0x00, instead of the original data.
+
+This problem exists since the clone ioctl was added and happens both with
+and without my recent data loss and file corruption fixes for the clone
+ioctl (patch "Btrfs: fix file corruption and data loss after cloning
+inline extents").
+
+So fix this by truncating the compressed inline extents as we do for the
+non-compressed case, which involves decompressing, if the data isn't already
+in the page cache, compressing the truncated version of the extent, writing
+the compressed content into the inline extent and then truncate it.
+
+The following test case for fstests reproduces the problem. In order for
+the test to pass both this fix and my previous fix for the clone ioctl
+that forbids cloning a smaller inline extent into a larger one,
+which is titled "Btrfs: fix file corruption and data loss after cloning
+inline extents", are needed. Without that other fix the test fails in a
+different way that does not leak the truncated data, instead part of
+destination file gets replaced with zeroes (because the destination file
+has a larger inline extent than the source).
+
+ 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
+
+ rm -f $seqres.full
+
+ _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
+
+ # 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
+
+Cc: stable at vger.kernel.org
+Signed-off-by: Filipe Manana <fdmanana at suse.com>
+---
+ fs/btrfs/inode.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++----------
+ 1 file changed, 68 insertions(+), 14 deletions(-)
+
+--- a/fs/btrfs/inode.c
++++ b/fs/btrfs/inode.c
+@@ -3993,6 +3993,47 @@ out:
+ return err;
+ }
+
++static int truncate_inline_extent(struct inode *inode,
++ struct btrfs_path *path,
++ struct btrfs_key *found_key,
++ const u64 item_end,
++ const u64 new_size)
++{
++ struct extent_buffer *leaf = path->nodes[0];
++ int slot = path->slots[0];
++ struct btrfs_file_extent_item *fi;
++ u32 size = (u32)(new_size - found_key->offset);
++ struct btrfs_root *root = BTRFS_I(inode)->root;
++
++ fi = btrfs_item_ptr(leaf, slot, struct btrfs_file_extent_item);
++
++ if (btrfs_file_extent_compression(leaf, fi) != BTRFS_COMPRESS_NONE) {
++ loff_t offset = new_size;
++ loff_t page_end = ALIGN(offset, PAGE_CACHE_SIZE);
++
++ /*
++ * Zero out the remaining of the last page of our inline extent,
++ * instead of directly truncating our inline extent here - that
++ * would be much more complex (decompressing all the data, then
++ * compressing the truncated data, which might be bigger than
++ * the size of the inline extent, resize the extent, etc).
++ * We release the path because to get the page we might need to
++ * read the extent item from disk (data not in the page cache).
++ */
++ btrfs_release_path(path);
++ return btrfs_truncate_page(inode, offset, page_end - offset, 0);
++ }
++
++ btrfs_set_file_extent_ram_bytes(leaf, fi, size);
++ size = btrfs_file_extent_calc_inline_size(size);
++ btrfs_truncate_item(root, path, size, 1);
++
++ if (test_bit(BTRFS_ROOT_REF_COWS, &root->state))
++ inode_sub_bytes(inode, item_end + 1 - new_size);
++
++ return 0;
++}
++
+ /*
+ * this can truncate away extent items, csum items and directory items.
+ * It starts at a high offset and removes keys until it can't find
+@@ -4162,27 +4203,40 @@ search_again:
+ * special encodings
+ */
+ if (!del_item &&
+- btrfs_file_extent_compression(leaf, fi) == 0 &&
+ btrfs_file_extent_encryption(leaf, fi) == 0 &&
+ btrfs_file_extent_other_encoding(leaf, fi) == 0) {
+- u32 size = new_size - found_key.offset;
+-
+- if (test_bit(BTRFS_ROOT_REF_COWS, &root->state))
+- inode_sub_bytes(inode, item_end + 1 -
+- new_size);
+
+ /*
+- * update the ram bytes to properly reflect
+- * the new size of our item
++ * Need to release path in order to truncate a
++ * compressed extent. So delete any accumulated
++ * extent items so far.
+ */
+- btrfs_set_file_extent_ram_bytes(leaf, fi, size);
+- size =
+- btrfs_file_extent_calc_inline_size(size);
+- btrfs_truncate_item(root, path, size, 1);
++ if (btrfs_file_extent_compression(leaf, fi) !=
++ BTRFS_COMPRESS_NONE && pending_del_nr) {
++ err = btrfs_del_items(trans, root, path,
++ pending_del_slot,
++ pending_del_nr);
++ if (err) {
++ btrfs_abort_transaction(trans,
++ root,
++ err);
++ goto error;
++ }
++ pending_del_nr = 0;
++ }
++
++ err = truncate_inline_extent(inode, path,
++ &found_key,
++ item_end,
++ new_size);
++ if (err) {
++ btrfs_abort_transaction(trans,
++ root, err);
++ goto error;
++ }
+ } else if (test_bit(BTRFS_ROOT_REF_COWS,
+ &root->state)) {
+- inode_sub_bytes(inode, item_end + 1 -
+- found_key.offset);
++ inode_sub_bytes(inode, item_end + 1 - new_size);
+ }
+ }
+ delete:
diff --git a/debian/patches/series b/debian/patches/series
index 245b83b..967e126 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -647,3 +647,4 @@ bugfix/all/usbvision-fix-overflow-of-interfaces-array.patch
bugfix/all/media-usbvision-fix-crash-on-detecting-device-with-i.patch
bugfix/all/unix-avoid-use-after-free-in-ep_remove_wait_queue.patch
debian/af_unix-avoid-abi-changes.patch
+bugfix/all/btrfs-fix-truncation-of-compressed-and-inlined-exten.patch
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/kernel/linux.git
More information about the Kernel-svn-changes
mailing list