All Virtuozzo development lists (kernel + QEMU)
 help / color / mirror / Atom feed
* [PATCH hci-8.0 0/5] qcow2: silent corruption when a dirty image becomes writable
@ 2026-09-16 15:24 Denis V. Lunev
  2026-09-16 15:24 ` [PATCH hci-8.0 1/5] qcow2: do not clear the dirty bit when reopening a read-only node #VSTOR-140073 Denis V. Lunev
                   ` (4 more replies)
  0 siblings, 5 replies; 8+ messages in thread
From: Denis V. Lunev @ 2026-09-16 15:24 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

A customer volume was cross-mapped onto itself by this bug, so hci-8.0
wants the fix even though the upstream series has not been merged yet.

The problem
-----------

qcow2_do_open() repairs an image carrying QCOW2_INCOMPAT_DIRTY, but only
for a node that is writable from the start:

    if (!(flags & BDRV_O_CHECK) && bdrv_is_writable(bs) &&
        (s->incompatible_features & QCOW2_INCOMPAT_DIRTY)) {

A node opened read-only skips it, correctly, since it resolves nothing
and writes nothing. Nothing revisits the question when that same node
later becomes writable, and bdrv_reopen() is not an exotic way to get
there: commit_active_start() and commit_start() both reopen the base
read-write for the duration of the job, so an ordinary block-commit onto
a read-only backing file is enough.

With lazy refcounts the refcount blocks are not written again once the
dirty bit is set, so an image left behind by a killed QEMU has an
on-disk refcount block that accounts for the metadata clusters and
nothing else. Every data cluster reads as free. s->free_cluster_index
starts at 0, so the first allocation after such a reopen starts at the
front of the image and hands out clusters that L2 entries still point
at.

The result is aliasing: two guest offsets mapped onto one host cluster,
so the guest reads back data belonging to some other offset. Nothing
about this fails. No I/O error is reported, the corrupt bit stays clear,
and a clean close clears the dirty bit as well, so no later open will
repair the image either. The only runtime witness, if something
eventually frees one of those clusters, is a bare

    qcow2_free_clusters failed: Invalid argument

on stderr, with the guest none the wiser.

How it looked in production
---------------------------

A VM was killed while its storage was unavailable, leaving a 100 GiB
volume dirty. It came back with a snapshot-revert overlay on top, so the
volume itself was now the read-only backing file, and the overlay was
committed into it two and a half hours later. 8082 host clusters ended
up referenced by two L2 entries each, roughly 8 GiB of guest data
cross-mapped. The guest filesystem began failing metadata verification
on buffers holding fragments of unrelated files.

qemu-img check -r all makes the metadata self-consistent again, and then
honestly reports no errors, but it cannot un-alias anything. The guest
data stays wrong.

Reproducer
----------

Under a second, no guest and no block job required:

  qemu-img create -f qcow2 -o compat=1.1,lazy_refcounts=on base.qcow2 1G
  qemu-io -f qcow2 -c "write -P 0xaa 0 100M" -c flush \
          -c "sigraise 9" base.qcow2

  qemu-io -r -f qcow2 base.qcow2 \
      <<< $'reopen -w\nwrite -P 0xbb 900M 100M\nquit'

  qemu-io -r -f qcow2 -c "read -v 0 16" base.qcow2
  # 00000000:  bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb

The flush is load-bearing: it writes out the L2 cache but not the
refcount blocks, which is exactly the asymmetry the bug needs. Guest
fsyncs supply it in production, so a long-running VM is the ideal
victim. qemu-img commit of an overlay reaches the same state through
commit_active_start().

The fix
-------

Patch 5 repairs the image in qcow2_reopen_commit_post(), the earliest
point at which the node is writable. That callback runs after the reopen
transaction has been committed and cannot reject it, so patch 3 gives it
a return value to report a node it has had to give up on, and patch 4
records the flags the reopen started from so the driver can tell a node
that has just become writable from one that was writable all along.
Patch 2 stops a reopen of a driverless node from crashing, which is how
a failed repair comes back through commit_clean(). Patch 1 is the
read-only header write that fails or asserts on a dirty image.

The repair runs under bdrv_drain_all(), so a block-commit onto a large
dirty base stalls guest I/O for the length of a full metadata scan. That
is the cost of the alternative being silent data loss; rejecting the
reopen would be cheap, but block-commit depends on it succeeding.

Port notes
----------

This is the hci-8.0 port of the v4 series posted upstream on 2026-08-24:
https://lore.kernel.org/qemu-devel/20260824133729.1141990-1-den@openvz.org/

It is still under review there, so the code may yet change. All five
patches cherry-pick onto hci-8.0 with no prerequisites and no conflicts,
and the diff is identical to the upstream one.

One thing differs, in patch 5: hci-8.0 creates qcow2 images with zstd
compression by default, so a fresh v3 image already carries
QCOW2_INCOMPAT_COMPRESSION. The iotests 040 helper therefore masks the
dirty bit out of the incompatible features rather than comparing the
whole word, which upstream can afford to do.

Built and tested on dev-cl04: iotests 039, 040 and 060 pass, and the
qcow2 auto group is otherwise unchanged. vz-mt-vendor fails there only
because that build was configured --disable-spice and so has no
qxl-vga device for the test to instantiate.

Signed-off-by: Denis V. Lunev <den@openvz.org>

Denis V. Lunev (5):
  qcow2: do not clear the dirty bit when reopening a read-only node
    #VSTOR-140073
  block: reject a reopen of an unusable node instead of crashing
    #VSTOR-140073
  block: let bdrv_reopen_commit_post() report a failure #VSTOR-140073
  block: remember the flags a reopen starts from #VSTOR-140073
  qcow2: repair a dirty image when it becomes writable #VSTOR-140073

 block.c                          |  55 ++++++++++++--
 block/qcow2.c                    |  33 ++++++++-
 include/block/block-common.h     |   1 +
 include/block/block_int-common.h |  11 ++-
 qapi/block-core.json             |  19 +++++
 tests/qemu-iotests/039           | 110 +++++++++++++++++++++++++++
 tests/qemu-iotests/039.out       |  56 ++++++++++++++
 tests/qemu-iotests/040           | 123 +++++++++++++++++++++++++++++++
 tests/qemu-iotests/040.out       |   4 +-
 tests/qemu-iotests/060           |  30 ++++++++
 tests/qemu-iotests/060.out       |  13 ++++
 11 files changed, 440 insertions(+), 15 deletions(-)

--
2.53.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-09-16 16:28 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-16 15:24 [PATCH hci-8.0 0/5] qcow2: silent corruption when a dirty image becomes writable Denis V. Lunev
2026-09-16 15:24 ` [PATCH hci-8.0 1/5] qcow2: do not clear the dirty bit when reopening a read-only node #VSTOR-140073 Denis V. Lunev
2026-09-16 15:24 ` [PATCH hci-8.0 2/5] block: reject a reopen of an unusable node instead of crashing #VSTOR-140073 Denis V. Lunev
2026-09-16 15:24 ` [PATCH hci-8.0 3/5] block: let bdrv_reopen_commit_post() report a failure #VSTOR-140073 Denis V. Lunev
2026-09-16 15:24 ` [PATCH hci-8.0 4/5] block: remember the flags a reopen starts from #VSTOR-140073 Denis V. Lunev
2026-09-16 15:24 ` [PATCH hci-8.0 5/5] qcow2: repair a dirty image when it becomes writable #VSTOR-140073 Denis V. Lunev
2026-09-16 16:26   ` Andrey Drobyshev
2026-09-16 16:28     ` Denis V. Lunev

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.