From: "Denis V. Lunev" <den@openvz.org>
To: svt-core@virtuozzo.com
Cc: andrey.drobyshev@virtuozzo.com, den@openvz.org
Subject: [PATCH hci-8.0 0/5] qcow2: silent corruption when a dirty image becomes writable
Date: Wed, 16 Sep 2026 17:24:32 +0200 [thread overview]
Message-ID: <20260916152437.1787889-1-den@openvz.org> (raw)
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
next reply other threads:[~2026-09-16 15:24 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-16 15:24 Denis V. Lunev [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260916152437.1787889-1-den@openvz.org \
--to=den@openvz.org \
--cc=andrey.drobyshev@virtuozzo.com \
--cc=svt-core@virtuozzo.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox