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 <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski
2026-01-26 14:49:18 +01:00
committed by Rob Bradford
parent 111f37f926
commit 771ab1d5a3

View File

@@ -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>),
#[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<F: Seek + Write + FileSync>(&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(|_| "<unknown>".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)