block: qcow: Set corrupt bit on known inconsistencies

Set the QCOW2 corrupt bit when internal inconsistencies are detected
that indicate image metadata may be corrupted:

- Decompression decode failure, meaning compressed cluster data is
  invalid
- Decompression size mismatch, where decompressed data doesn't match
  expected cluster size
- Partial write after decompression, where L2 table was updated but
  data cluster not fully written, leaving metadata inconsistent
- Invalid refcount index, where cluster address is outside valid
  refcount table range, indicating a corrupted L2 entry
- Dirty L2 with zero L1 address, where L2 table is marked dirty but
  L1 has no address for it

Note: Marking decompression failures as corrupt is more conservative
than QEMU, which returns EIO without setting the corrupt bit. This is
debatable since corrupted compressed data doesn't necessarily indicate
metadata corruption, but it provides a stronger safety guarantee by
preventing further writes to potentially damaged images.

Once set, the image can only be opened read-only until repaired with
qemu-img check -r.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski 2026-01-26 15:00:18 +01:00 committed by Rob Bradford
parent c2fcb9bac9
commit 2d86fc8422

View file

@ -1529,6 +1529,19 @@ impl QcowFile {
(address / self.raw_file.cluster_size()) % self.l2_entries
}
/// Attempts to set the corrupt bit, logging failures without propagating them.
///
/// This is "best effort" because the write may fail due to various reasons like
/// disk full, readonly storage, etc. This method is called just before returning
/// EIO to the caller. The error is not propagated because the original corruption
/// error is more important to return to the call site than a secondary I/O
/// failure from marking the image.
fn set_corrupt_bit_best_effort(&mut self) {
if let Err(e) = self.header.set_corrupt_bit(self.raw_file.file_mut()) {
warn!("Failed to persist corrupt bit: {e}");
}
}
// Decompress the cluster, return EIO on failure
fn decompress_l2_cluster(&mut self, l2_entry: u64) -> std::io::Result<Vec<u8>> {
let (compressed_cluster_addr, compressed_cluster_size) =
@ -1547,8 +1560,12 @@ impl QcowFile {
let mut decompressed_cluster = vec![0; cluster_size];
let decompressed_size = decoder
.decode(&compressed_cluster, &mut decompressed_cluster)
.map_err(|_| std::io::Error::from_raw_os_error(EIO))?;
.map_err(|_| {
self.set_corrupt_bit_best_effort();
io::Error::from_raw_os_error(EIO)
})?;
if decompressed_size as u64 != self.raw_file.cluster_size() {
self.set_corrupt_bit_best_effort();
return Err(std::io::Error::from_raw_os_error(EIO));
}
Ok(decompressed_cluster)
@ -1645,6 +1662,7 @@ impl QcowFile {
.seek(SeekFrom::Start(cluster_addr))?;
let nwritten = self.raw_file.file_mut().write(&decompressed_cluster)?;
if nwritten != decompressed_cluster.len() {
self.set_corrupt_bit_best_effort();
return Err(std::io::Error::from_raw_os_error(EIO));
}
@ -1985,6 +2003,7 @@ impl QcowFile {
return Err(e);
}
Err(refcount::Error::InvalidIndex) => {
self.set_corrupt_bit_best_effort();
return Err(std::io::Error::from_raw_os_error(EINVAL));
}
Err(refcount::Error::NeedCluster(addr)) => {
@ -2027,6 +2046,7 @@ impl QcowFile {
self.raw_file
.write_pointer_table_direct(addr, l2_table.iter())?;
} else {
self.set_corrupt_bit_best_effort();
return Err(std::io::Error::from_raw_os_error(EINVAL));
}
l2_table.mark_clean();