From 771ab1d5a35a6074994813d816a6e19b0d4cdd0f Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Mon, 26 Jan 2026 14:49:18 +0100 Subject: [PATCH] block: qcow: Add corrupt bit support for QCOW2 v3 images Implement proper handling of the QCOW2 corrupt bit (incompatible feature bit 1) according to the specification: - Add Error::CorruptImage for rejecting writable opens of corrupt images - Add CORRUPT to SUPPORTED features (handled specially, not rejected) - Add QcowHeader::set_corrupt_bit() to mark images as corrupt - Add QcowHeader::is_corrupt() helper method - Reject writable opens of corrupt images with Error::CorruptImage - Allow readonly opens of corrupt images with a warning The corrupt bit indicates that image metadata may be inconsistent. Per spec, such images must not be written to until repaired by external tools like qemu-img. Read-only access is permitted to allow data recovery. Users can open corrupt images read-only using: --disk path=/path/to/image.qcow2,readonly=on Signed-off-by: Anatol Belski --- block/src/qcow/mod.rs | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/block/src/qcow/mod.rs b/block/src/qcow/mod.rs index 308014c1a..a2f81cec1 100644 --- a/block/src/qcow/mod.rs +++ b/block/src/qcow/mod.rs @@ -12,7 +12,7 @@ mod vec_cache; use std::cmp::{max, min}; use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; -use std::fs::OpenOptions; +use std::fs::{OpenOptions, read_link}; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::mem::size_of; use std::os::fd::{AsRawFd, RawFd}; @@ -20,7 +20,7 @@ use std::str::{self, FromStr}; use bitflags::bitflags; use libc::{EINVAL, EIO, ENOSPC}; -use log::error; +use log::{error, warn}; use remain::sorted; use thiserror::Error; use vmm_sys_util::file_traits::{FileSetLen, FileSync}; @@ -46,6 +46,8 @@ pub enum Error { BackingFileOpen(#[source] Box), #[error("Backing file name is too long: {0} bytes over")] BackingFileTooLong(usize), + #[error("Image is marked corrupt and cannot be opened for writing")] + CorruptImage, #[error("Failed to evict cache")] EvictingCache(#[source] io::Error), #[error("File larger than max of {MAX_QCOW_FILE_SIZE}: {0}")] @@ -230,8 +232,9 @@ bitflags! { impl IncompatFeatures { /// Features supported by this implementation. - const SUPPORTED: IncompatFeatures = - IncompatFeatures::DIRTY.union(IncompatFeatures::COMPRESSION); + const SUPPORTED: IncompatFeatures = IncompatFeatures::DIRTY + .union(IncompatFeatures::CORRUPT) + .union(IncompatFeatures::COMPRESSION); /// Get the fallback name for a known feature bit. fn flag_name(bit: u8) -> Option<&'static str> { @@ -698,6 +701,24 @@ impl QcowHeader { } Ok(()) } + + /// Set the corrupt bit for QCOW2 v3 images. + /// + /// This marks the image as corrupted. Once set, the image can only be + /// opened read-only until repaired. + pub fn set_corrupt_bit(&mut self, file: &mut F) -> Result<()> { + if self.version == 3 { + self.incompatible_features |= IncompatFeatures::CORRUPT.bits(); + self.write_incompatible_features(file)?; + file.fsync().map_err(Error::WritingHeader)?; + } + Ok(()) + } + + pub fn is_corrupt(&self) -> bool { + IncompatFeatures::from_bits_truncate(self.incompatible_features) + .contains(IncompatFeatures::CORRUPT) + } } fn max_refcount_clusters(refcount_order: u32, cluster_size: u32, num_clusters: u32) -> u64 { @@ -911,6 +932,15 @@ impl QcowFile { .ok_or(Error::InvalidClusterSize)?; let is_writable = raw_file.file().is_writable(); + if header.is_corrupt() { + if is_writable { + return Err(Error::CorruptImage); + } + let path = read_link(format!("/proc/self/fd/{}", raw_file.file().as_raw_fd())) + .map_or_else(|_| "".to_string(), |p| p.display().to_string()); + warn!("QCOW2 image is marked corrupt, opening read-only: {path}"); + } + // Image already has dirty bit set. Refcounts may be invalid. if IncompatFeatures::from_bits_truncate(header.incompatible_features) .contains(IncompatFeatures::DIRTY)