block: qcow: Implement dirty bit support for QCOW2 v3 images

Add support for the dirty bit (bit 0 of incompatible_features) which
indicates the image was not closed cleanly. This improves data
integrity by allowing detection of potentially corrupted images.

On open:
- If dirty bit is already set, log a warning and trigger
  refcount rebuild
- Set the dirty bit and write it to disk immediately
- Sync to ensure persistence before any writes
- Skip dirty bit and refcount rebuild for readonly files

On clean close:
- Clear the dirty bit in the header
- Write it to disk and sync

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski
2026-01-25 17:30:13 +01:00
committed by Rob Bradford
parent a6aecad635
commit cc96fc14b4
3 changed files with 81 additions and 2 deletions

View File

@@ -231,7 +231,8 @@ bitflags! {
impl IncompatFeatures {
/// Features supported by this implementation.
const SUPPORTED: IncompatFeatures = IncompatFeatures::COMPRESSION;
const SUPPORTED: IncompatFeatures =
IncompatFeatures::DIRTY.union(IncompatFeatures::COMPRESSION);
/// Get the fallback name for a known feature bit.
fn flag_name(bit: u8) -> Option<&'static str> {
@@ -680,6 +681,39 @@ impl QcowHeader {
Ok(())
}
/// Write only the incompatible_features field to the file at its fixed offset.
fn write_incompatible_features<F: Seek + Write>(&self, file: &mut F) -> Result<()> {
if self.version != 3 {
return Ok(());
}
file.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64))
.map_err(Error::WritingHeader)?;
file.write_u64::<BigEndian>(self.incompatible_features)
.map_err(Error::WritingHeader)?;
Ok(())
}
/// Set or clear the dirty bit for QCOW2 v3 images.
///
/// When `dirty` is true, sets the bit to indicate the image is in use.
/// When `dirty` is false, clears the bit to indicate a clean shutdown.
pub fn set_dirty_bit<F: Seek + Write + FileSync>(
&mut self,
file: &mut F,
dirty: bool,
) -> Result<()> {
if self.version == 3 {
if dirty {
self.incompatible_features |= IncompatFeatures::DIRTY.bits();
} else {
self.incompatible_features &= !IncompatFeatures::DIRTY.bits();
}
self.write_incompatible_features(file)?;
file.fsync().map_err(Error::WritingHeader)?;
}
Ok(())
}
}
fn max_refcount_clusters(refcount_order: u32, cluster_size: u32, num_clusters: u32) -> u64 {
@@ -892,7 +926,18 @@ impl QcowFile {
let mut raw_file = QcowRawFile::from(file, cluster_size, refcount_bits)
.ok_or(Error::InvalidClusterSize)?;
if refcount_rebuild_required {
let is_writable = raw_file.file().is_writable();
// Image already has dirty bit set. Refcounts may be invalid.
if IncompatFeatures::from_bits_truncate(header.incompatible_features)
.contains(IncompatFeatures::DIRTY)
{
log::warn!("QCOW2 image not cleanly closed, rebuilding refcounts");
refcount_rebuild_required = true;
}
// Skip refcount rebuilding for readonly files.
if refcount_rebuild_required && is_writable {
QcowFile::rebuild_refcounts(&mut raw_file, header.clone())?;
}
@@ -965,6 +1010,13 @@ impl QcowFile {
qcow.find_avail_clusters()?;
if !IncompatFeatures::from_bits_truncate(qcow.header.incompatible_features)
.contains(IncompatFeatures::DIRTY)
&& is_writable
{
qcow.header.set_dirty_bit(qcow.raw_file.file_mut(), true)?;
}
Ok(qcow)
}
@@ -1999,6 +2051,7 @@ impl QcowFile {
if sync_required {
self.raw_file.file_mut().sync_data()?;
}
Ok(())
}
}
@@ -2012,6 +2065,9 @@ impl AsRawFd for QcowFile {
impl Drop for QcowFile {
fn drop(&mut self) {
let _ = self.sync_caches();
if self.raw_file.file().is_writable() {
let _ = self.header.set_dirty_bit(self.raw_file.file_mut(), false);
}
}
}

View File

@@ -276,6 +276,11 @@ impl QcowRawFile {
Ok(Some(new_cluster_address))
}
/// Returns a reference to the underlying file.
pub fn file(&self) -> &RawFile {
&self.file
}
/// Returns a mutable reference to the underlying file.
pub fn file_mut(&mut self) -> &mut RawFile {
&mut self.file

View File

@@ -15,6 +15,7 @@ use std::os::unix::io::{AsRawFd, RawFd};
use std::slice;
use libc::c_void;
use vmm_sys_util::file_traits::FileSync;
use vmm_sys_util::seek_hole::SeekHole;
use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt};
@@ -122,6 +123,17 @@ impl RawFile {
pub fn is_direct(&self) -> bool {
self.direct_io
}
/// Returns true if the file was opened with write access.
pub fn is_writable(&self) -> bool {
// SAFETY: fcntl with F_GETFL is safe and doesn't modify the file descriptor
let flags = unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_GETFL) };
if flags < 0 {
return false;
}
let access_mode = flags & libc::O_ACCMODE;
access_mode == libc::O_WRONLY || access_mode == libc::O_RDWR
}
}
impl Read for RawFile {
@@ -327,6 +339,12 @@ impl PunchHole for RawFile {
}
}
impl FileSync for RawFile {
fn fsync(&mut self) -> std::io::Result<()> {
self.file.fsync()
}
}
impl SeekHole for RawFile {
fn seek_hole(&mut self, offset: u64) -> std::io::Result<Option<u64>> {
match self.file.seek_hole(offset) {