diff mbox series

[v2,3/7] qcow2: Prevent allocating compressed clusters at offset 0

Message ID fb53467cf48e95ff3330def1cf1003a5b862b7d9.1509718618.git.berto@igalia.com
State New
Headers show
Series Misc qcow2 corruption checks | expand

Commit Message

Alberto Garcia Nov. 3, 2017, 2:18 p.m. UTC
If the refcount data is corrupted then we can end up trying to
allocate a new compressed cluster at offset 0 in the image, triggering
an assertion in qcow2_alloc_bytes() that would crash QEMU:

  qcow2_alloc_bytes: Assertion `offset' failed.

This patch adds an explicit check for this scenario and a new test
case.

Signed-off-by: Alberto Garcia <berto@igalia.com>
---
 block/qcow2-refcount.c     |  8 +++++++-
 tests/qemu-iotests/060     | 10 ++++++++++
 tests/qemu-iotests/060.out |  8 ++++++++
 3 files changed, 25 insertions(+), 1 deletion(-)

Comments

Max Reitz Nov. 3, 2017, 4:27 p.m. UTC | #1
On 2017-11-03 15:18, Alberto Garcia wrote:
> If the refcount data is corrupted then we can end up trying to
> allocate a new compressed cluster at offset 0 in the image, triggering
> an assertion in qcow2_alloc_bytes() that would crash QEMU:
> 
>   qcow2_alloc_bytes: Assertion `offset' failed.
> 
> This patch adds an explicit check for this scenario and a new test
> case.
> 
> Signed-off-by: Alberto Garcia <berto@igalia.com>
> ---
>  block/qcow2-refcount.c     |  8 +++++++-
>  tests/qemu-iotests/060     | 10 ++++++++++
>  tests/qemu-iotests/060.out |  8 ++++++++
>  3 files changed, 25 insertions(+), 1 deletion(-)
> 
> diff --git a/block/qcow2-refcount.c b/block/qcow2-refcount.c
> index 9059996c4b..7eaac11429 100644
> --- a/block/qcow2-refcount.c
> +++ b/block/qcow2-refcount.c
> @@ -1082,6 +1082,13 @@ int64_t qcow2_alloc_bytes(BlockDriverState *bs, int size)
>                  return new_cluster;
>              }
>  
> +            if (new_cluster == 0) {
> +                qcow2_signal_corruption(bs, true, -1, -1, "Preventing invalid "
> +                                        "allocation of compressed cluster "
> +                                        "at offset 0");
> +                return -EIO;
> +            }
> +
>              if (!offset || ROUND_UP(offset, s->cluster_size) != new_cluster) {
>                  offset = new_cluster;
>                  free_in_cluster = s->cluster_size;
> @@ -1090,7 +1097,6 @@ int64_t qcow2_alloc_bytes(BlockDriverState *bs, int size)
>              }
>          }
>  
> -        assert(offset);

I don't think this assert() was meant as a protection against offset
being 0. :-)

Reviewed-by: Max Reitz <mreitz@redhat.com>

if the assert() stays.

>          ret = update_refcount(bs, offset, size, 1, false, QCOW2_DISCARD_NEVER);
>          if (ret < 0) {
>              offset = 0;
> diff --git a/tests/qemu-iotests/060 b/tests/qemu-iotests/060
> index 40f85cc216..c3bce27b33 100755
> --- a/tests/qemu-iotests/060
> +++ b/tests/qemu-iotests/060
> @@ -260,6 +260,16 @@ _make_test_img 64M
>  poke_file "$TEST_IMG" "$rb_offset"        "\x00\x00\x00\x00\x00\x00\x00\x00"
>  $QEMU_IO -c "write 0 64k" "$TEST_IMG" | _filter_qemu_io
>  
> +echo
> +echo "=== Testing empty refcount block with compressed write ==="
> +echo
> +_make_test_img 64M
> +$QEMU_IO -c "write 64k 64k" "$TEST_IMG" | _filter_qemu_io
> +poke_file "$TEST_IMG" "$rb_offset"        "\x00\x00\x00\x00\x00\x00\x00\x00"
> +# The previous write already allocated an L2 table, so now this new
> +# write will try to allocate a compressed data cluster at offset 0.
> +$QEMU_IO -c "write -c 0k 64k" "$TEST_IMG" | _filter_qemu_io
> +
>  # success, all done
>  echo "*** done"
>  rm -f $seq.full
> diff --git a/tests/qemu-iotests/060.out b/tests/qemu-iotests/060.out
> index 5b8b518486..cf8790ff57 100644
> --- a/tests/qemu-iotests/060.out
> +++ b/tests/qemu-iotests/060.out
> @@ -195,4 +195,12 @@ write failed: Input/output error
>  Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=67108864
>  qcow2: Marking image as corrupt: Preventing invalid allocation of L2 table at offset 0; further corruption events will be suppressed
>  write failed: Input/output error
> +
> +=== Testing empty refcount block with compressed write ===
> +
> +Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=67108864
> +wrote 65536/65536 bytes at offset 65536
> +64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
> +qcow2: Marking image as corrupt: Preventing invalid allocation of compressed cluster at offset 0; further corruption events will be suppressed
> +write failed: Input/output error
>  *** done
>
Alberto Garcia Nov. 3, 2017, 8:22 p.m. UTC | #2
On Fri 03 Nov 2017 05:27:59 PM CET, Max Reitz wrote:
>> +            if (new_cluster == 0) {
>> +                qcow2_signal_corruption(bs, true, -1, -1, "Preventing invalid "
>> +                                        "allocation of compressed cluster "
>> +                                        "at offset 0");
>> +                return -EIO;
>> +            }
>> +
>>              if (!offset || ROUND_UP(offset, s->cluster_size) != new_cluster) {
>>                  offset = new_cluster;
>>                  free_in_cluster = s->cluster_size;
>> @@ -1090,7 +1097,6 @@ int64_t qcow2_alloc_bytes(BlockDriverState *bs, int size)
>>              }
>>          }
>>  
>> -        assert(offset);
>
> I don't think this assert() was meant as a protection against offset
> being 0. :-)

After the new check offset is now guaranteed to be 0, so what's the
point of keeping the assert() ?

Berto
Alberto Garcia Nov. 3, 2017, 8:32 p.m. UTC | #3
On Fri 03 Nov 2017 09:22:39 PM CET, Alberto Garcia wrote:
>>> -        assert(offset);
>>
>> I don't think this assert() was meant as a protection against offset
>> being 0. :-)
>
> After the new check offset is now guaranteed to be 0, so what's the
> point of keeping the assert() ?

I meant "guaranteed _not_ to be 0" :-)

Berto
Max Reitz Nov. 6, 2017, 12:36 p.m. UTC | #4
On 2017-11-03 21:32, Alberto Garcia wrote:
> On Fri 03 Nov 2017 09:22:39 PM CET, Alberto Garcia wrote:
>>>> -        assert(offset);
>>>
>>> I don't think this assert() was meant as a protection against offset
>>> being 0. :-)
>>
>> After the new check offset is now guaranteed to be 0, so what's the
>> point of keeping the assert() ?
> 
> I meant "guaranteed _not_ to be 0" :-)

That is the point of an assert.

An assert should not guard against something that can occur.  It should
express that something will always be true (in this case that the offset
is guaranteed not to be 0).  Then, someone who reads the code does not
have to read all code paths to check whether that condition is true.

If an assert checks a condition that can be true, it's wrong.  Then
either the code is buggy (like it was before this patch) or the error
should be handled gracefully instead of aborting the program.

In a perfect world, all assert()s would be checked at compile time.

Max
Alberto Garcia Nov. 6, 2017, 12:52 p.m. UTC | #5
On Mon 06 Nov 2017 01:36:01 PM CET, Max Reitz wrote:
>>>>> -        assert(offset);
>>>>
>>>> I don't think this assert() was meant as a protection against offset
>>>> being 0. :-)
>>>
>>> After the new check offset is now guaranteed to be 0, so what's the
>>> point of keeping the assert() ?
>> 
>> I meant "guaranteed _not_ to be 0" :-)
>
> That is the point of an assert.
>
> An assert should not guard against something that can occur. It should
> express that something will always be true (in this case that the
> offset is guaranteed not to be 0).

Right, and they're especially useful when it's not obvious that the
assertion is always true.

The reason why I removed the assertion in this case is that the code
that checks and guarantees that the offset is not null is immediately
before the assert() line.

But I'm not going to argue over something like this, I don't mind if you
prefer to keep it :-)

Berto
diff mbox series

Patch

diff --git a/block/qcow2-refcount.c b/block/qcow2-refcount.c
index 9059996c4b..7eaac11429 100644
--- a/block/qcow2-refcount.c
+++ b/block/qcow2-refcount.c
@@ -1082,6 +1082,13 @@  int64_t qcow2_alloc_bytes(BlockDriverState *bs, int size)
                 return new_cluster;
             }
 
+            if (new_cluster == 0) {
+                qcow2_signal_corruption(bs, true, -1, -1, "Preventing invalid "
+                                        "allocation of compressed cluster "
+                                        "at offset 0");
+                return -EIO;
+            }
+
             if (!offset || ROUND_UP(offset, s->cluster_size) != new_cluster) {
                 offset = new_cluster;
                 free_in_cluster = s->cluster_size;
@@ -1090,7 +1097,6 @@  int64_t qcow2_alloc_bytes(BlockDriverState *bs, int size)
             }
         }
 
-        assert(offset);
         ret = update_refcount(bs, offset, size, 1, false, QCOW2_DISCARD_NEVER);
         if (ret < 0) {
             offset = 0;
diff --git a/tests/qemu-iotests/060 b/tests/qemu-iotests/060
index 40f85cc216..c3bce27b33 100755
--- a/tests/qemu-iotests/060
+++ b/tests/qemu-iotests/060
@@ -260,6 +260,16 @@  _make_test_img 64M
 poke_file "$TEST_IMG" "$rb_offset"        "\x00\x00\x00\x00\x00\x00\x00\x00"
 $QEMU_IO -c "write 0 64k" "$TEST_IMG" | _filter_qemu_io
 
+echo
+echo "=== Testing empty refcount block with compressed write ==="
+echo
+_make_test_img 64M
+$QEMU_IO -c "write 64k 64k" "$TEST_IMG" | _filter_qemu_io
+poke_file "$TEST_IMG" "$rb_offset"        "\x00\x00\x00\x00\x00\x00\x00\x00"
+# The previous write already allocated an L2 table, so now this new
+# write will try to allocate a compressed data cluster at offset 0.
+$QEMU_IO -c "write -c 0k 64k" "$TEST_IMG" | _filter_qemu_io
+
 # success, all done
 echo "*** done"
 rm -f $seq.full
diff --git a/tests/qemu-iotests/060.out b/tests/qemu-iotests/060.out
index 5b8b518486..cf8790ff57 100644
--- a/tests/qemu-iotests/060.out
+++ b/tests/qemu-iotests/060.out
@@ -195,4 +195,12 @@  write failed: Input/output error
 Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=67108864
 qcow2: Marking image as corrupt: Preventing invalid allocation of L2 table at offset 0; further corruption events will be suppressed
 write failed: Input/output error
+
+=== Testing empty refcount block with compressed write ===
+
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=67108864
+wrote 65536/65536 bytes at offset 65536
+64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+qcow2: Marking image as corrupt: Preventing invalid allocation of compressed cluster at offset 0; further corruption events will be suppressed
+write failed: Input/output error
 *** done