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:
parent
c2fcb9bac9
commit
2d86fc8422
1 changed files with 21 additions and 1 deletions
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue