diff --git a/block/src/qcow/mod.rs b/block/src/qcow/mod.rs index 5f91aa3bd..99c808309 100644 --- a/block/src/qcow/mod.rs +++ b/block/src/qcow/mod.rs @@ -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(&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::(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( + &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); + } } } diff --git a/block/src/qcow/qcow_raw_file.rs b/block/src/qcow/qcow_raw_file.rs index 5b8cf7356..af9823306 100644 --- a/block/src/qcow/qcow_raw_file.rs +++ b/block/src/qcow/qcow_raw_file.rs @@ -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 diff --git a/block/src/qcow/raw_file.rs b/block/src/qcow/raw_file.rs index b80a30adc..eda7751c3 100644 --- a/block/src/qcow/raw_file.rs +++ b/block/src/qcow/raw_file.rs @@ -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> { match self.file.seek_hole(offset) {