Two QCOW2 images returned No errors were found on the image. in the reproduced QEMU 10.0.11 lab. Only one contained a clean ext4 filesystem. The other held a deliberately damaged block bitmap, and read-only e2fsck exited 4 with bitmap and checksum errors.
That result is not a contradiction. qemu-img check examines the disk-image container; it does not replace the guest filesystem’s checker. Virtualization operators need both verdicts before calling an offline image healthy. This article builds a secret-free 64 MiB control, corrupts guest bytes without corrupting QCOW2 metadata, and proves the boundary without touching a running VM or repairing either test image.
A virtual disk has at least two relevant integrity layers. QCOW2 supplies mappings, reference counts, allocation metadata and optional backing relationships. Inside the guest-visible byte stream, a partition table, LVM, encryption and filesystems may add their own structures. Each layer has a different checker and repair owner.
QEMU’s current qemu-img reference describes the utility as an offline image tool and warns against modifying images used by a running VM or another process. Debian publishes the same in-use image warning. A forced shared open can produce inconsistent results; it is not a shortcut around quiescence.
Operationally, freeze the image identity first. Confirm the VM is shut down, confirm no storage job or hypervisor process holds the file, and record its path plus backing chain. The procedure for checking which files QCOW2 rebase and commit can mutate belongs before any lineage change, not after a clean format result. Work from a protected copy or snapshot whose consistency contract you understand; test snapshot consistency through Proxmox guest-agent snapshot consistency checks because “snapshot exists” and “guest writes were quiesced” are separate facts. Do not paste the lab’s fixed /tmp paths over a production pathname.
This is the decision receipt the lab must reproduce. It appears before the commands so operators know which facts are acceptance criteria and which numbers are only environment detail. Absolute host allocation counts are not needed for the claim; tool versions, exit codes, hash parity, corruption evidence and cleanup state are.
qemu_img=10.0.11 e2fsck=1.47.2 source_fsck=0 virtual_bytes=67108864
corruption=group0_block_bitmap block_size=1024 bitmap_block=259 source_changed=yes
qemu_check clean_rc=0 corrupt_rc=0 clean="No errors were found on the image." corrupt="No errors were found on the image."
guest_fsck clean_rc=0 corrupt_rc=4 clean_hash_match=yes corrupt_hash_match=yes detected="block_bitmap_and_checksum"
acceptance=qemu_metadata_clean_for_both guest_filesystem_clean_only_for_control
cleanup=complete path_absent=yes
Accept the boundary only when both QCOW2 checks return zero with the clean QEMU sentence, both extracted byte streams match their respective raw sources, the clean ext4 control exits zero, and the corrupted ext4 control returns the uncorrected-error bit with block-bitmap plus checksum evidence. Also require a deliberately failed assertion to retain the marker-owned evidence and require the successful path to remove that same directory. A clean qemu-img check, a successful conversion or a VM that reaches a bootloader cannot substitute for those independent outcomes.
Run all tested blocks in one fresh Bash session. The workspace block refuses an existing path, requires the exact tool classes, creates mode-0700 ownership and installs a cleanup trap. It creates no block device, loads no kernel module and never needs a mount.
set -euo pipefail
lab_root=/tmp/voxfor-qemu-fsck-165
owner_token=voxfor-qemu-fsck-165
command -v qemu-img mkfs.ext4 e2fsck dumpe2fs sha256sum >/dev/null
[[ ! -e "$lab_root" ]] || { printf 'Refusing existing path: %s\n' "$lab_root" >&2; exit 1; }
install -d -m 0700 "$lab_root"
printf '%s\n' "$owner_token" > "$lab_root/OWNER"
verify_owner() {
[[ -d "$lab_root" && -O "$lab_root" ]]
[[ "$(<"$lab_root/OWNER")" == "$owner_token" ]]
}
cleanup_owned() {
verify_owner || return 1
find "$lab_root" -depth -delete
}
cleanup_on_exit() {
local rc=$?
if (( rc == 0 )); then
cleanup_owned
else
printf 'failure_evidence_retained=%s\n' "$lab_root" >&2
fi
return "$rc"
}
trap cleanup_on_exit EXIT
This source control is a filesystem directly inside a raw file, with no partition table. That deliberate simplification isolates the claim: QCOW2 metadata can be clean while its guest-visible filesystem metadata is not. Real VM disks often need partition, LVM, RAID or encryption discovery before the correct checker can even see a filesystem.
A 64 MiB virtual size is a reproducible fixture dimension, not a host-capacity prediction. Use the separate QCOW2 discard and sparse-copy measurement when the reader decision is how many host bytes an image consumes.
verify_owner
truncate -s 64M "$lab_root/clean.raw"
mkfs.ext4 -F -q -L VOXFOR165 "$lab_root/clean.raw"
set +e
e2fsck -fn "$lab_root/clean.raw" > "$lab_root/fsck-source.txt" 2>&1
source_fsck_rc=$?
set -e
[[ "$source_fsck_rc" == 0 ]]
source_hash=$(sha256sum "$lab_root/clean.raw" | awk '{print $1}')
printf 'qemu_img=%s e2fsck=%s source_fsck=%s virtual_bytes=%s\n' \
"$(qemu-img --version | awk 'NR==1{print $3}')" \
"$(e2fsck -V 2>&1 | awk 'NR==1{print $2}')" \
"$source_fsck_rc" "$(stat -c %s "$lab_root/clean.raw")"
e2fsck -f forces all passes even though the new filesystem is marked clean, while -n answers no to repair prompts. The e2fsck manual warns that results on mounted filesystems are not valid and generally unsafe; this lab supplies an offline file.
The negative control copies the clean raw source, discovers its first ext4 block bitmap and zeros that bitmap in the copy. It then verifies that the source and corrupted byte hashes differ before converting each stream independently to QCOW2.
block_size=$(dumpe2fs -h "$lab_root/clean.raw" 2>/dev/null | awk -F: '$1 ~ /^Block size/ {gsub(/ /,"",$2); print $2}')
bitmap_block=$(dumpe2fs "$lab_root/clean.raw" 2>/dev/null | sed -n 's/.*Block bitmap at \([0-9][0-9]*\).*/\1/p' | head -1)
[[ "$block_size" =~ ^[0-9]+$ && "$bitmap_block" =~ ^[0-9]+$ ]]
cp --reflink=never "$lab_root/clean.raw" "$lab_root/corrupt.raw"
dd if=/dev/zero of="$lab_root/corrupt.raw" bs="$block_size" seek="$bitmap_block" count=1 conv=notrunc status=none
corrupt_hash=$(sha256sum "$lab_root/corrupt.raw" | awk '{print $1}')
[[ "$corrupt_hash" != "$source_hash" ]]
qemu-img convert -f raw -O qcow2 "$lab_root/clean.raw" "$lab_root/clean.qcow2"
qemu-img convert -f raw -O qcow2 "$lab_root/corrupt.raw" "$lab_root/corrupt.qcow2"
printf 'corruption=group0_block_bitmap block_size=%s bitmap_block=%s source_changed=yes\n' "$block_size" "$bitmap_block"
No production workflow should manufacture corruption this way. The mutation exists solely as a deterministic negative control in an owned disposable file. Its location comes from dumpe2fs, not a hard-coded offset, and the copy uses --reflink=never so the two raw paths are independent files.
A QEMU development discussion once described guest data corruption while qemu-img check reported a clean image. That historical report is demand evidence, not proof about current QEMU. The current lab independently reproduces the general layer boundary with QEMU 10.0.11.
Now run the same QCOW2 consistency check against both containers. The commands retain complete outputs, require zero exit status and match the exact clean sentence.
set +e
qemu-img check -f qcow2 "$lab_root/clean.qcow2" > "$lab_root/qemu-clean.txt" 2>&1
qemu_clean_rc=$?
qemu-img check -f qcow2 "$lab_root/corrupt.qcow2" > "$lab_root/qemu-corrupt.txt" 2>&1
qemu_corrupt_rc=$?
set -e
[[ "$qemu_clean_rc" == 0 && "$qemu_corrupt_rc" == 0 ]]
grep -q '^No errors were found on the image\.$' "$lab_root/qemu-clean.txt"
grep -q '^No errors were found on the image\.$' "$lab_root/qemu-corrupt.txt"
printf 'qemu_check clean_rc=%s corrupt_rc=%s clean="%s" corrupt="%s"\n' \
"$qemu_clean_rc" "$qemu_corrupt_rc" \
"$(head -1 "$lab_root/qemu-clean.txt")" "$(head -1 "$lab_root/qemu-corrupt.txt")"
Both passes are valid container-level results. Neither says the byte ranges QEMU maps form a valid ext4 filesystem. Likewise, qemu-img info can correctly report format, virtual size and allocation without parsing a guest directory tree.
Avoid adding -r all during diagnosis merely because an ordinary check found a problem. Repair mutates image metadata and can complicate recovery. Preserve the original, record the non-repair result, establish available backups and free space, and assign the failure to QCOW2 before authorizing a container repair. Filesystem repair belongs to its own owner and tool.
Conversion back to raw serves two purposes. First, hashes prove that QEMU preserved each guest-visible source stream. Second, e2fsck -fn sees the filesystem directly without NBD, mounting or writes. Exit code 4 means filesystem errors were left uncorrected; that is expected for the deliberately bad read-only control.
qemu-img convert -f qcow2 -O raw "$lab_root/clean.qcow2" "$lab_root/clean-extracted.raw"
qemu-img convert -f qcow2 -O raw "$lab_root/corrupt.qcow2" "$lab_root/corrupt-extracted.raw"
[[ "$(sha256sum "$lab_root/clean-extracted.raw" | awk '{print $1}')" == "$source_hash" ]]
[[ "$(sha256sum "$lab_root/corrupt-extracted.raw" | awk '{print $1}')" == "$corrupt_hash" ]]
set +e
e2fsck -fn "$lab_root/clean-extracted.raw" > "$lab_root/fsck-clean.txt" 2>&1
fsck_clean_rc=$?
e2fsck -fn "$lab_root/corrupt-extracted.raw" > "$lab_root/fsck-corrupt.txt" 2>&1
fsck_corrupt_rc=$?
set -e
[[ "$fsck_clean_rc" == 0 ]]
(( fsck_corrupt_rc & 4 ))
grep -q 'Block bitmap differences:' "$lab_root/fsck-corrupt.txt"
grep -q 'block bitmap does not match checksum' "$lab_root/fsck-corrupt.txt"
grep -q 'Filesystem still has errors' "$lab_root/fsck-corrupt.txt"
printf 'guest_fsck clean_rc=%s corrupt_rc=%s clean_hash_match=yes corrupt_hash_match=yes detected="block_bitmap_and_checksum"\n' \
"$fsck_clean_rc" "$fsck_corrupt_rc"
Hash parity matters because it excludes an accidental conversion difference as the reason for the filesystem verdict. Clean raw bytes stayed clean through QCOW2; corrupted raw bytes stayed corrupted through QCOW2. The two container checks still returned the same result.
Real images need deliberate device discovery. A concise QCOW2 filesystem-check walkthrough exposes an offline image through NBD, and Jason Spencer demonstrates read-only ext4 checking through qemu-nbd. Those techniques require root, an unused NBD device, a powered-off VM, the correct partition and the correct filesystem-specific checker. Blindly iterating fsck -y across partitions can write to swap, encrypted data or the wrong filesystem.
Routing comes after both checkers because the table assigns evidence owners rather than predicting a result.
| Verdict layer | What a clean result supports | What it cannot establish |
|---|---|---|
| QCOW2 container | QEMU found no format-consistency errors in supported image metadata | ext4/XFS/NTFS consistency, file contents, database state or application recovery |
| Guest filesystem | The selected filesystem checker accepted the selected offline filesystem | QCOW2 refcounts, backing-chain correctness or application semantics |
| Restored workload | Boot and application probes satisfy a declared recovery contract | That every unused block or unrelated service is healthy |
Route the combinations by evidence owner:
An integrity check is evidence, not a backup. Before repair, retain the original image, its sidecar/backing files, hashes, tool versions, storage error evidence and a copy of every non-mutating result. Repair a disposable copy first. Then rerun the checker for the repaired layer and validate guest-visible files and the application.
For ext4 that remounted itself read-only, follow offline ext4 evidence and repair boundaries to establish why lower-device health and a truly unmounted target come before write authorization.
When recovery starts from backup, perform isolated VM restore drills to add identity, timing and workload acceptance that neither checker supplies.
Do not declare success at the first boot prompt. Confirm partition discovery, mounts, expected file hashes, database checks, service state, logs and a representative user transaction. Record the boundary of the claim: “QCOW2 metadata passed and ext4 passed read-only checks on this offline copy” is useful; “the VM is healthy” requires more evidence.
The durable decision is two-part: QEMU accepts the container metadata, and the correct guest checker independently accepts the intended offline filesystem. Only then should workload-level recovery testing begin.
The final input emits the central acceptance line, deletes only a directory whose owner token matches, disables the trap and proves absence. It never attaches a device, runs a repair flag, modifies a live image or deletes an unfamiliar path.
printf 'acceptance=qemu_metadata_clean_for_both guest_filesystem_clean_only_for_control\n'
cleanup_owned
trap - EXIT
[[ ! -e "$lab_root" ]]
printf 'cleanup=complete path_absent=yes\n'
The exit trap deletes the owned fixture only after a successful run. A forced failing assertion was separately reproduced: it exited nonzero, printed failure_evidence_retained=/tmp/voxfor-qemu-fsck-165, left the owner marker plus diagnostic file in place, and was then removed through the same owner-verified cleanup function. If any real assertion differs, stop before repair, keep the original VM disk and backing files unchanged, retain the marker-owned outputs for diagnosis, and delete only that verified lab directory when evidence collection is complete. For a real maintenance window, restore access to the unchanged source image or approved pre-check snapshot through the same storage owner; do not use qemu-img check -r, fsck -y, an NBD detach, or a backing-file edit as generic rollback.
No. qemu-img check validates consistency supported by the disk-image format. It does not run ext4, XFS, NTFS, database or application checks inside the guest-visible bytes.
Yes. The reproduced corrupted image returned a clean QCOW2 result while e2fsck -fn reported block-bitmap differences, a bitmap checksum mismatch and uncorrected filesystem errors.
Do not treat a live, changing image as authoritative input. QEMU warns that modifying an in-use image may destroy it and that even queries can observe inconsistent state. Quiesce the owner and use a protected copy or a storage snapshot with a known consistency contract.
-n keeps the first filesystem pass non-repairing, while -f forces all checks. This preserves the copied evidence and reveals whether ext4—not QCOW2—is rejecting the guest-visible structure before a repair plan is approved.
Bit 4 means filesystem errors were left uncorrected. In this lab that is the intended negative-control result because -n refuses fixes; other exit bits must still be decoded rather than assuming every nonzero value means the same failure.
Preserve the original first, then work on copies and decide which layer must expose trustworthy bytes to the next. Do not run two mutating repairs blindly; record the failure evidence, backups, backing relationships and storage health, and verify each repaired layer independently.