Research notes / Virtual disk preservation
QCOW2 backups: an unchanged overlay can expose different data
/ Measured tool behavior
A QCOW2 overlay is not necessarily a self-contained disk. In our controlled experiment, replacing its base changed 131,072 guest-visible bytes while the overlay hash stayed identical and qemu-img check still succeeded. Separately, changing one stored payload byte also passed the consistency check.
On this page7 sections
Three meanings of an empty-looking region
QCOW2 maps a virtual disk onto clusters in an image file. An unallocated guest cluster can read from a backing file; without one, it reads as zeros. A standard zero-cluster descriptor explicitly supplies zeros instead of inheriting backing data. The distinction is recorded in metadata, not visible in a plain zero-filled read. [2]
Our fixture is deliberately small: a 256 KiB raw base containing byte 0x11, plus a QCOW2 overlay with 64 KiB clusters. We write 0x22 to the first cluster, explicitly zero the second, and leave the last two inherited. The expected virtual disk is:
| Guest offset | Overlay state | Initial read | After base replacement |
|---|---|---|---|
| 0 KiB | Stored data | 0x22 | 0x22 |
| 64 KiB | Explicit zero | 0x00 | 0x00 |
| 128 KiB | Inherited | 0x11 | 0x33 |
| 192 KiB | Inherited | 0x11 | 0x33 |
Replacing every byte in the base with 0x33 leaves the first two guest clusters unchanged. Only the inherited half changes. This is why a backup inventory needs the backing chain, not just the active overlay's filename and hash.
What the checks actually observed
Each row begins from fresh files. We close every writer before the manipulation, run the tools offline, export the whole virtual disk to raw bytes where possible, and compare it against a separately constructed baseline.
| Case | Overlay hash unchanged? | Check exit | Compare exit | Changed guest bytes |
|---|---|---|---|---|
| Intact chain | Yes | 0 | 0 | 0 |
| Replace base bytes | Yes | 0 | 1 | 131,072 |
| Remove base | Yes | 1 | 2 | Unreadable |
| Move overlay alone | Yes | 1 | 2 | Unreadable |
| Move complete chain | Yes | 0 | 0 | 0 |
| Change one payload byte | No | 0 | 1 | 1 |
| Safe rebase to different base | No | 0 | 0 | 0 |
| Unsafe rebase to different base | No | 0 | 1 | 131,072 |
| Shorten base to 128 KiB | Yes | 0 | 1 | 131,072 |
Missing is not substituted. Removing the base made the check, comparison and full export fail. Moving only the overlay, then deleting the original base, did the same. Moving both files together preserved the virtual disk. We did not observe missing bases silently becoming zeros; that would misdescribe this experiment.
An existing but shortened base behaves differently. In the added case we truncate the raw base from 256 KiB to 128 KiB without touching the overlay. Both inherited clusters now read as zeros: 131,072 guest bytes change, while the overlay hash remains identical and check returns 0. The base still opens; unlike an absent file, its missing tail is covered by the format's zero-fill rule. This is documented behavior, not evidence that a truncated backup is complete. [2]
A valid structure can carry changed data. We located the first stored data extent using qemu-img map --output=json, changed its first byte from 0x22 to 0x23, and left its allocation metadata alone. Check returned 0, but full export and comparison detected exactly one differing guest byte.
The pinned QEMU implementation helps interpret this result: qcow2_check_refcounts reconstructs and compares reference counts and checks copied flags. It is structural evidence, not an expected-content oracle for the guest's files. This code review explains our case; it is not an exhaustive audit of every image checker. [3]
Rebasing changes the dependency. Safe rebase preserved our expected guest bytes by compensating for the different base. Unsafe rebase -u changed the inherited half. Both resulting structures passed check. The documented unsafe mode changes backing metadata without ensuring content equivalence; it is not a shortcut for reconstructing a missing, unknown base. [1]
Even an equal comparison has a policy
The tenth scenario uses raw files to isolate comparison semantics from QCOW2. One contains 65,536 bytes of 0x11; the other contains the same prefix followed by 65,536 zero bytes. Their sizes and SHA-256 hashes differ. Default qemu-img compare returned 0; strict comparison with -s returned 1.
The default accepts a larger image's zero or unallocated tail. Strict mode rejects size and allocation differences as well as different content. Therefore neither mode should be interpreted as universal backup equivalence. If virtual capacity matters but allocation layout may differ, check capacity separately and compare content. [1]
This is not a hash collision. The tools answer different questions: file identity, format consistency, permitted guest-byte equivalence and intended recovery state. A baseline generated from an already substituted chain would also preserve the wrong state faithfully.
Choose the verification boundary
Saltzer, Reed and Clark's End-to-End Arguments in System Design analyzes file transfer across storage, memory and a network. Its receiver reads the destination back and compares a file checksum; reliable transport alone cannot cover the other stages. Lower-layer checks remain useful for reducing retries. [5]
Applied here, our engineering inference is to verify the resolved guest disk against trusted recovery evidence, including its capacity and dependencies. Checking one container file covers a narrower object. Neither the paper nor this byte-pattern experiment establishes application-consistent VM recovery.
A recovery manifest needs dependency identity
Libvirt can obtain chain topology from image metadata or explicit domain configuration. Its documentation emphasizes paths and explicit backing formats, and describes relocation failures. A filename alone is not a content identity; deployment configuration can determine which chain is used. [4]
- Capture the intended state. Record a stable point in time, guest disk capacity, required chain members, their formats and content identities. For multiple disks, record the consistency procedure; our single-disk test does not establish one.
- Preserve and resolve dependencies. Restore away from the originals. Inventory with
qemu-img info --backing-chain --output=jsonand verify the resolved files against the intended manifest. Inventory is not authentication. - Separate structure from contents. Run non-repairing image checks, then verify guest-visible contents against trusted evidence. Keep opening failures distinct from completed checks that report differences.
- Test the required recovery. Check guest filesystem and application state, not merely image readability. An offline raw export can remove this backing dependency, but is not a general export of snapshots, bitmaps, VM configuration or application consistency.
Never modify an image used by a running VM; even concurrent inspection can see inconsistent state. Work on protected copies and keep original evidence before repairs. [1]
This extends backup verification versus restore testing. Compare HDF5 virtual dataset dependencies, where missing sources can yield fill values, and sparse-file allocation, where zero bytes and allocated space are also distinct observations.
Method and reproducibility
Measured with qemu-img version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.18); Python 3.12.3. Nine QCOW2 cases plus one raw-size control, three repetitions each: 30 runs on one host. Each exported payload is checked byte for byte, not sampled. The repetitions establish repeatability of these fixtures, not a population failure rate or confidence interval.
The dataset includes column definitions, CSV, raw command results and hashes. Download the Python harness; with qemu-img and qemu-io available:
python3 qcow2-dependencies.py --repetitions 3 > observations.jsonOptional --qemu-img, --qemu-io and --temp-dir arguments select executables and a temporary parent. The runner accepts no existing image, creates private fixtures, uses explicit formats and bounds subprocess waits. Its checks remain active under optimized Python. The table generator verifies coverage, harness identity, command receipts and expected payload hashes before publishing derived rows. Checksums identify the downloaded bytes; they do not authenticate an untrusted observation log.
Limits: uncompressed, unencrypted standard clusters with one raw backing file. We did not boot a VM, simulate a crash, measure performance, test guest filesystems, use live writers, or exercise internal snapshots, external data files and persistent dirty bitmaps. This is a documented behavior demonstration, not a new QEMU vulnerability or a claim that all corruptions pass check. Upstream v8.2.2 source review is separate from the Ubuntu-patched binary tested; executable hashes and its complete package version are retained.
Primary sources
- QEMU 8.2.2: qemu-img check, compare and rebase
- QEMU 8.2.2: QCOW2 image file format, cluster mapping and zero clusters
- QEMU 8.2.2: qcow2_check_refcounts implementation
- libvirt: disk image chains, backing formats and relocation
- Saltzer, Reed and Clark (1984), End-to-End Arguments in System Design, ACM TOCS 2(4), 277-288; author-hosted text
Checked 18 October 2026. Original code and observations: CC0-1.0; upstream materials retain their licenses.