qemu-img Check Can Pass a Corrupt Guest Filesystem
Last edited on August 13, 2026

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.

Freeze the image identity and define both layers

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.

Read the two-layer receipt before reproducing it

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.

Recreate the pair in a marker-owned workspace

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.

Add a guest-only corruption control

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.

Run two independent checkers

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.

Ask e2fsck about the same bytes

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.

Route each verdict to the correct owner

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:

  • QCOW2 fails, filesystem not yet tested: preserve the source and investigate image metadata, storage I/O and backing relationships before exposing guest bytes.
  • QCOW2 passes, filesystem fails: the container can map bytes consistently, but the selected guest filesystem needs its own offline recovery decision.
  • Both pass, workload fails: move upward to partitions, LVM, encryption, boot configuration, database consistency and application acceptance.
  • Both fail: keep the original immutable, plan space for recovery copies and avoid letting one repair overwrite evidence needed by the other layer.

Carry the result into recovery

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.

Preserve failure evidence and clean successful fixtures

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.

Guest integrity questions

Does qemu-img check validate files inside a VM?

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.

Can a QCOW2 image be clean while ext4 is corrupt?

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.

May I run qemu-img check while the VM is running?

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.

Why use e2fsck -n before repair?

-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.

What does e2fsck exit code 4 mean here?

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.

Which layer should be repaired first if both fail?

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.

Share this Post

Leave a Reply

Your email address will not be published. Required fields are marked *