Compare commits

...

9 Commits
main ... v50.1

Author SHA1 Message Date
Bo Chen
bf94d3dad9 build: Release v50.1
This release contains security fixes. Details can be found in the
release notes.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-02-19 17:14:43 -08:00
Demi Marie Obenour
02e3570bdd hypervisor: Suppress unused_unsafe warning
x86::__cpuid is safe on Rust ≥1.94 but unsafe on older versions.  This
causes unused_unsafe warnings when compiling with Rust ≥1.94.  However,
on earlier Rust versions, the code won’t compile if the unsafe blocks
are absent.

Work around this by adding #[allow(unused_unsafe)] where needed to
suppress the warnings.

See #7588 for more discussion.

Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com>
2026-02-19 17:14:43 -08:00
Rob Bradford
dae66ce493 vhost_user_block: Disable use of backing files in test implementation
Remove the use of QCOW2 backing files in the test implementation used
for CI.

Signed-off-by: Rob Bradford <rbradford@meta.com>
(cherry picked from commit 76e233504b)
2026-02-19 17:14:43 -08:00
Rob Bradford
90cee24f98 vmm, virtio-devices: Deny zero sector writes for autodetected raw images
If the disk image was autodetected to raw (not specified with image_type
= 0) then in the virtio-block subsystem generate errors for writes to
block 0 (treat as if read-only). This gives an immediate error vs using
the image implementations in the block subsystem.

Signed-off-by: Rob Bradford <rbradford@meta.com>
(cherry picked from commit b3e8e2abc5)
2026-02-19 17:14:43 -08:00
Rob Bradford
5a0b6f2d06 vmm: Improve resiliency of image type handling
Add an image_type to DiskConfig to specify the image type. If none is
specified autodetect the image type but disable potentially unsafe
behaviour in the QCOW2 backend by disabling the backing file support.

If the image type is autodetected then fix it in the config so that it
will be persistant across reboots and migrations/snapshot & restores.
This also handles the case where the image type was not specified as
part of the disk configuration.

Signed-off-by: Rob Bradford <rbradford@meta.com>
(cherry picked from commit 6f2357c14e)
2026-02-19 17:14:43 -08:00
Anatol Belski
30166a4ea5 vmm: openapi: Sync DiskConfig OpenAPI spec
Add backing_files field to the REST API.

Note: This backport does not include the 'sparse' field as the
corresponding feature support is not backported.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
(cherry picked from commit e36096db3e)
2026-02-19 17:14:43 -08:00
Rob Bradford
f93340d337 vmm: Add option to control backing files
Backing files (e.g. for QCOW2) interact badly with landlock since they
are not obvious from the initial VM configuration. Only enable their use
with an explicit option.

Signed-off-by: Rob Bradford <rbradford@meta.com>
Signed-off-by: Bo Chen <bchen@crusoe.ai>
(cherry picked from commit 509832298b)
2026-02-19 17:14:43 -08:00
Anatol Belski
4c1f854ee9 block: qcow: Use Arc<Mutex<>> for thread safe multiqueue access
Wrap QcowFile in Arc<Mutex<>> to ensure thread safety when multiple
virtio queues access the same QCOW2 image concurrently.

Previously, each queue received its own QcowSync instance via
new_async_io() that shared the underlying QcowFile through Clone.
However, cloned QcowFile instances share internal mutable state
(L2 cache, reference counts, file seek position) without
synchronization, leading to data corruption under concurrent I/O.

This change serializes all QCOW2 operations through a mutex, which
ensures correctness at the cost of parallelism. A more performant
solution would require separating metadata locking from actual I/O
operations, tracked in #7560.

Related: #7560

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
(cherry picked from commit 9bc367a27b)
2026-02-19 17:14:43 -08:00
Wei Liu
ecab9f1b96 vmm: api: Expose the nested option in API description
Signed-off-by: Wei Liu <liuwe@microsoft.com>
(cherry picked from commit 0a5e79afce)
2026-02-19 17:14:43 -08:00
16 changed files with 297 additions and 50 deletions

3
Cargo.lock generated
View File

@@ -417,10 +417,11 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "cloud-hypervisor"
version = "50.0.0"
version = "50.1.0"
dependencies = [
"anyhow",
"api_client",
"block",
"clap",
"dhat",
"dirs",

View File

@@ -559,6 +559,7 @@ pub fn generate_common_cpuid(
hypervisor: &dyn hypervisor::Hypervisor,
config: &CpuidConfig,
) -> super::Result<Vec<CpuIdEntry>> {
#[allow(unused_unsafe)]
// SAFETY: cpuid called with valid leaves
if unsafe { x86_64::__cpuid(1) }.ecx & (1 << HYPERVISOR_ECX_BIT) == 1 << HYPERVISOR_ECX_BIT {
// SAFETY: cpuid called with valid leaves
@@ -694,6 +695,7 @@ pub fn generate_common_cpuid(
// Copy host L1 cache details if not populated by KVM
0x8000_0005 => {
if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 {
#[allow(unused_unsafe)]
// SAFETY: cpuid called with valid leaves
if unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 {
// SAFETY: cpuid called with valid leaves
@@ -708,8 +710,10 @@ pub fn generate_common_cpuid(
// Copy host L2 cache details if not populated by KVM
0x8000_0006 => {
if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 {
#[allow(unused_unsafe)]
// SAFETY: cpuid called with valid leaves
if unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 {
#[allow(unused_unsafe)]
// SAFETY: cpuid called with valid leaves
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0006) };
entry.eax = leaf.eax;
@@ -747,6 +751,7 @@ pub fn generate_common_cpuid(
for i in 0x8000_0002..=0x8000_0004 {
cpuid.retain(|c| c.function != i);
// SAFETY: call cpuid with valid leaves
#[allow(unused_unsafe)]
let leaf = unsafe { std::arch::x86_64::__cpuid(i) };
cpuid.push(CpuIdEntry {
function: i,
@@ -859,6 +864,7 @@ pub fn configure_vcpu(
// The TSC frequency CPUID leaf should not be included when running with HyperV emulation
if !kvm_hyperv && let Some(tsc_khz) = vcpu.tsc_khz().map_err(Error::GetTscFrequency)? {
// Need to check that the TSC doesn't vary with dynamic frequency
#[allow(unused_unsafe)]
// SAFETY: cpuid called with valid leaves
if unsafe { std::arch::x86_64::__cpuid(0x8000_0007) }.edx & (1u32 << INVARIANT_TSC_EDX_BIT)
> 0
@@ -1307,6 +1313,7 @@ pub fn initramfs_load_addr(
pub fn get_host_cpu_phys_bits(hypervisor: &dyn hypervisor::Hypervisor) -> u8 {
// SAFETY: call cpuid with valid leaves
#[allow(unused_unsafe)]
unsafe {
let leaf = x86_64::__cpuid(0x8000_0000);

View File

@@ -30,12 +30,13 @@ pub mod vhdx_sync;
use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::collections::VecDeque;
use std::fmt::Debug;
use std::fmt::{self, Debug};
use std::fs::File;
use std::io::{self, IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write};
use std::os::linux::fs::MetadataExt;
use std::os::unix::io::AsRawFd;
use std::path::Path;
use std::str::FromStr;
use std::time::Instant;
use std::{cmp, result};
@@ -788,11 +789,44 @@ pub trait AsyncAdaptor {
}
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ImageType {
FixedVhd,
Qcow2,
Raw,
Vhdx,
#[default]
Unknown,
}
impl fmt::Display for ImageType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImageType::FixedVhd => write!(f, "vhd"),
ImageType::Qcow2 => write!(f, "qcow2"),
ImageType::Raw => write!(f, "raw"),
ImageType::Vhdx => write!(f, "vhdx"),
ImageType::Unknown => write!(f, "unknown"),
}
}
}
pub enum ImageTypeParseError {
InvalidValue(String),
}
impl FromStr for ImageType {
type Err = ImageTypeParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"vhd" => Ok(ImageType::FixedVhd),
"qcow2" => Ok(ImageType::Qcow2),
"raw" => Ok(ImageType::Raw),
"vhdx" => Ok(ImageType::Vhdx),
_ => Err(ImageTypeParseError::InvalidValue(s.to_string())),
}
}
}
const QCOW_MAGIC: u32 = 0x5146_49fb;

View File

@@ -6,6 +6,7 @@ use std::collections::VecDeque;
use std::fs::File;
use std::io::{Seek, SeekFrom};
use std::os::fd::AsRawFd;
use std::sync::{Arc, Mutex};
use vmm_sys_util::eventfd::EventFd;
@@ -16,26 +17,45 @@ use crate::qcow::{QcowFile, RawFile, Result as QcowResult};
use crate::{AsyncAdaptor, BlockBackend};
pub struct QcowDiskSync {
qcow_file: QcowFile,
// FIXME: The Mutex serializes all QCOW2 I/O operations across queues, which
// is necessary for correctness but eliminates any parallelism benefit from
// multiqueue. QcowFile has internal mutable state (L2 cache, refcounts, file
// position) that is not safe to share across threads via Clone.
//
// A proper fix would require restructuring QcowFile to separate metadata
// operations (which need synchronization) from data I/O (which could be
// parallelized with per queue file descriptors). See #7560 for details.
qcow_file: Arc<Mutex<QcowFile>>,
}
impl QcowDiskSync {
pub fn new(file: File, direct_io: bool) -> QcowResult<Self> {
Ok(QcowDiskSync {
qcow_file: QcowFile::from(RawFile::new(file, direct_io))?,
})
pub fn new(file: File, direct_io: bool, backing_files: bool) -> QcowResult<Self> {
if backing_files {
Ok(QcowDiskSync {
qcow_file: Arc::new(Mutex::new(QcowFile::from(RawFile::new(file, direct_io))?)),
})
} else {
Ok(QcowDiskSync {
qcow_file: Arc::new(Mutex::new(QcowFile::from_with_nesting_depth(
RawFile::new(file, direct_io),
0,
)?)),
})
}
}
}
impl DiskFile for QcowDiskSync {
fn logical_size(&mut self) -> DiskFileResult<u64> {
self.qcow_file
.lock()
.unwrap()
.seek(SeekFrom::End(0))
.map_err(DiskFileError::Size)
}
fn physical_size(&mut self) -> DiskFileResult<u64> {
self.qcow_file.physical_size().map_err(|e| {
self.qcow_file.lock().unwrap().physical_size().map_err(|e| {
let io_inner = match e {
crate::Error::GetFileMetadata(e) => e,
_ => unreachable!(),
@@ -45,22 +65,22 @@ impl DiskFile for QcowDiskSync {
}
fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
Ok(Box::new(QcowSync::new(self.qcow_file.clone())) as Box<dyn AsyncIo>)
Ok(Box::new(QcowSync::new(Arc::clone(&self.qcow_file))) as Box<dyn AsyncIo>)
}
fn fd(&mut self) -> BorrowedDiskFd<'_> {
BorrowedDiskFd::new(self.qcow_file.as_raw_fd())
BorrowedDiskFd::new(self.qcow_file.lock().unwrap().as_raw_fd())
}
}
pub struct QcowSync {
qcow_file: QcowFile,
qcow_file: Arc<Mutex<QcowFile>>,
eventfd: EventFd,
completion_list: VecDeque<(u64, i32)>,
}
impl QcowSync {
pub fn new(qcow_file: QcowFile) -> Self {
pub fn new(qcow_file: Arc<Mutex<QcowFile>>) -> Self {
QcowSync {
qcow_file,
eventfd: EventFd::new(libc::EFD_NONBLOCK)
@@ -83,7 +103,7 @@ impl AsyncIo for QcowSync {
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
self.qcow_file.read_vectored_sync(
self.qcow_file.lock().unwrap().read_vectored_sync(
offset,
iovecs,
user_data,
@@ -98,7 +118,7 @@ impl AsyncIo for QcowSync {
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
self.qcow_file.write_vectored_sync(
self.qcow_file.lock().unwrap().write_vectored_sync(
offset,
iovecs,
user_data,
@@ -108,8 +128,11 @@ impl AsyncIo for QcowSync {
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
self.qcow_file
.fsync_sync(user_data, &self.eventfd, &mut self.completion_list)
self.qcow_file.lock().unwrap().fsync_sync(
user_data,
&self.eventfd,
&mut self.completion_list,
)
}
fn next_completed_request(&mut self) -> Option<(u64, i32)> {

View File

@@ -7,7 +7,7 @@ edition = "2024"
homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor"
license = "Apache-2.0 AND BSD-3-Clause"
name = "cloud-hypervisor"
version = "50.0.0"
version = "50.1.0"
# Minimum buildable version:
# Keep in sync with version in .github/workflows/build.yaml
# Policy on MSRV (see #4318):
@@ -41,6 +41,7 @@ vmm-sys-util = { workspace = true }
zbus = { version = "5.7.1", optional = true }
[dev-dependencies]
block = { path = "../block" }
dirs = { workspace = true }
net_util = { path = "../net_util" }
serde_json = { workspace = true }

View File

@@ -1199,14 +1199,14 @@ mod unit_tests {
"--kernel",
"/path/to/kernel",
"--disk",
"path=/path/to/disk/1",
"path=/path/to/disk/1,image_type=raw",
"path=/path/to/disk/2",
],
r#"{
"payload": {"kernel": "/path/to/kernel"},
"disks": [
{"path": "/path/to/disk/1"},
{"path": "/path/to/disk/2"}
{"path": "/path/to/disk/1", "image_type": "Raw"},
{"path": "/path/to/disk/2", "image_type": "Unknown"}
]
}"#,
true,
@@ -1217,8 +1217,8 @@ mod unit_tests {
"--kernel",
"/path/to/kernel",
"--disk",
"path=/path/to/disk/1",
"path=/path/to/disk/2",
"path=/path/to/disk/1,image_type=raw",
"path=/path/to/disk/2,image_type=qcow2",
],
r#"{
"payload": {"kernel": "/path/to/kernel"},
@@ -1280,8 +1280,8 @@ mod unit_tests {
r#"{
"payload": {"kernel": "/path/to/kernel"},
"disks": [
{"path": "/path/to/disk/1", "rate_limit_group": "group0"},
{"path": "/path/to/disk/2", "rate_limit_group": "group0"}
{"path": "/path/to/disk/1", "rate_limit_group": "group0", "image_type": "Unknown"},
{"path": "/path/to/disk/2", "rate_limit_group": "group0", "image_type": "Unknown"}
],
"rate_limit_groups": [
{"id": "group0", "rate_limiter_config": {"bandwidth": {"size": 1000, "one_time_burst": 0, "refill_time": 100}}}

View File

@@ -2526,6 +2526,8 @@ mod common_parallel {
use std::fs::OpenOptions;
use std::io::SeekFrom;
use block::ImageType;
use crate::*;
#[test]
@@ -3172,7 +3174,7 @@ mod common_parallel {
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
format!("path={test_disk_path},pci_segment=15").as_str(),
format!("path={test_disk_path},pci_segment=15,image_type=raw").as_str(),
])
.capture_output()
.default_net();
@@ -3412,6 +3414,8 @@ mod common_parallel {
disable_io_uring: bool,
disable_aio: bool,
verify_os_disk: bool,
backing_files: bool,
image_type: ImageType,
) {
let disk_config = UbuntuDiskConfig::new(image_name.to_string());
let guest = Guest::new(Box::new(disk_config));
@@ -3432,8 +3436,9 @@ mod common_parallel {
.args([
"--disk",
format!(
"path={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
"path={},backing_files={},image_type={image_type}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap(),
if backing_files { "on"} else {"off"},
)
.as_str(),
format!(
@@ -3503,17 +3508,17 @@ mod common_parallel {
#[test]
fn test_virtio_block_io_uring() {
_test_virtio_block(FOCAL_IMAGE_NAME, false, true, false);
_test_virtio_block(FOCAL_IMAGE_NAME, false, true, false, false, ImageType::Raw);
}
#[test]
fn test_virtio_block_aio() {
_test_virtio_block(FOCAL_IMAGE_NAME, true, false, false);
_test_virtio_block(FOCAL_IMAGE_NAME, true, false, false, false, ImageType::Raw);
}
#[test]
fn test_virtio_block_sync() {
_test_virtio_block(FOCAL_IMAGE_NAME, true, true, false);
_test_virtio_block(FOCAL_IMAGE_NAME, true, true, false, false, ImageType::Raw);
}
/// Uses `qemu-img check` to verify disk image consistency.
@@ -3549,22 +3554,50 @@ mod common_parallel {
#[test]
fn test_virtio_block_qcow2() {
_test_virtio_block(JAMMY_IMAGE_NAME_QCOW2, false, false, true);
_test_virtio_block(
JAMMY_IMAGE_NAME_QCOW2,
false,
false,
true,
false,
ImageType::Qcow2,
);
}
#[test]
fn test_virtio_block_qcow2_zlib() {
_test_virtio_block(JAMMY_IMAGE_NAME_QCOW2_ZLIB, false, false, true);
_test_virtio_block(
JAMMY_IMAGE_NAME_QCOW2_ZLIB,
false,
false,
true,
false,
ImageType::Qcow2,
);
}
#[test]
fn test_virtio_block_qcow2_zstd() {
_test_virtio_block(JAMMY_IMAGE_NAME_QCOW2_ZSTD, false, false, true);
_test_virtio_block(
JAMMY_IMAGE_NAME_QCOW2_ZSTD,
false,
false,
true,
false,
ImageType::Qcow2,
);
}
#[test]
fn test_virtio_block_qcow2_backing_zstd_file() {
_test_virtio_block(JAMMY_IMAGE_NAME_QCOW2_BACKING_ZSTD_FILE, false, false, true);
_test_virtio_block(
JAMMY_IMAGE_NAME_QCOW2_BACKING_ZSTD_FILE,
false,
false,
true,
true,
ImageType::Qcow2,
);
}
#[test]
@@ -3574,6 +3607,8 @@ mod common_parallel {
false,
false,
true,
true,
ImageType::Qcow2,
);
}
@@ -3599,7 +3634,14 @@ mod common_parallel {
.output()
.expect("Expect generating VHD image from RAW image");
_test_virtio_block(FOCAL_IMAGE_NAME_VHD, false, false, false);
_test_virtio_block(
FOCAL_IMAGE_NAME_VHD,
false,
false,
false,
false,
ImageType::FixedVhd,
);
}
#[test]
@@ -3623,7 +3665,14 @@ mod common_parallel {
.output()
.expect("Expect generating dynamic VHDx image from RAW image");
_test_virtio_block(FOCAL_IMAGE_NAME_VHDX, false, false, true);
_test_virtio_block(
FOCAL_IMAGE_NAME_VHDX,
false,
false,
true,
false,
ImageType::Vhdx,
);
}
#[test]
@@ -4688,7 +4737,7 @@ mod common_parallel {
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
format!("path={}", vfio_disk_path.to_str().unwrap()).as_str(),
format!("path={},image_type=raw", vfio_disk_path.to_str().unwrap()).as_str(),
format!("path={},iommu=on,readonly=true", blk_file_path.to_str().unwrap()).as_str(),
])
.args([

View File

@@ -67,6 +67,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
EventFd::new(EFD_NONBLOCK).unwrap(),
None,
queue_affinity,
false,
)
.unwrap();

View File

@@ -155,7 +155,8 @@ pub trait Hypervisor: Send + Sync {
/// Determine CPU vendor
///
fn get_cpu_vendor(&self) -> CpuVendor {
// SAFETY: call cpuid with valid leaves
#[allow(unused_unsafe)]
// SAFETY: not actually unsafe, but considered unsafe by current stable
unsafe {
let leaf = x86_64::__cpuid(0x0);

View File

@@ -1,3 +1,4 @@
- [v50.1](#v501)
- [v50.0](#v500)
- [Configurable Nested Virtualization Option on x86_64](#configurable-nested-virtualization-option-on-x86_64)
- [Compression Support for QCOW2](#compression-support-for-qcow2)
@@ -409,6 +410,31 @@
- [Unit testing](#unit-testing)
- [Integration tests parallelization](#integration-tests-parallelization)
# v50.1
This is a point release containing security fixes and bug fixes.
### Security Fixes
This release fixes a security vulnerability in disk image handling.
Details can be found in
[GHSA-jmr4-g2hv-mjj6](https://github.com/cloud-hypervisor/cloud-hypervisor/security/advisories/GHSA-jmr4-g2hv-mjj6).
* A new `backing_files=on|off` option has been added to `--disk` to
explicitly control whether QCOW2 backing files are permitted. This
defaults to `off` to prevent the loading of backing files entirely.
(#7685)
* Explicit image type specification via the user interface, removing
reliance on format autodetection (#7728).
* Prevent sector-zero writes for autodetected raw images (#7728).
### Bug Fixes
* Fix various inconsistencies in our OpenAPI specification file
(#7716, #7726)
* Fix QCOW2 thread safety for multiple virtio queues
(`num_queues > 1`) (#7661)
# v50.0
This release has been tracked in [v50.0

View File

@@ -224,9 +224,9 @@ impl VhostUserBlkBackend {
let image_type = qcow::detect_image_type(&mut raw_img).unwrap();
let image = match image_type {
ImageType::Raw => Arc::new(Mutex::new(raw_img)) as Arc<Mutex<dyn DiskFile>>,
ImageType::Qcow2 => {
Arc::new(Mutex::new(QcowFile::from(raw_img).unwrap())) as Arc<Mutex<dyn DiskFile>>
}
ImageType::Qcow2 => Arc::new(Mutex::new(
QcowFile::from_with_nesting_depth(raw_img, 0).unwrap(),
)) as Arc<Mutex<dyn DiskFile>>,
};
let nsectors = (image.lock().unwrap().seek(SeekFrom::End(0)).unwrap()) / SECTOR_SIZE;

View File

@@ -160,6 +160,7 @@ struct BlockEpollHandler {
access_platform: Option<Arc<dyn AccessPlatform>>,
host_cpus: Option<Vec<usize>>,
acked_features: u64,
disable_sector0_writes: bool,
}
fn has_feature(features: u64, feature_flag: u64) -> bool {
@@ -167,8 +168,13 @@ fn has_feature(features: u64, feature_flag: u64) -> bool {
}
impl BlockEpollHandler {
fn check_request(features: u64, request_type: RequestType) -> result::Result<(), ExecuteError> {
if has_feature(features, VIRTIO_BLK_F_RO.into())
fn check_request(
features: u64,
request: &Request,
disable_sector0_writes: bool,
) -> result::Result<(), ExecuteError> {
let request_type = request.request_type;
if (has_feature(features, VIRTIO_BLK_F_RO.into()))
&& !(request_type == RequestType::In || request_type == RequestType::GetDeviceId)
{
// For virtio spec compliance
@@ -176,6 +182,11 @@ impl BlockEpollHandler {
// if the VIRTIO_BLK_F_RO feature if offered, and MUST NOT write any data."
return Err(ExecuteError::ReadOnly);
}
if request_type == RequestType::Out && disable_sector0_writes && request.sector == 0 {
return Err(ExecuteError::ReadOnly);
}
Ok(())
}
@@ -191,7 +202,10 @@ impl BlockEpollHandler {
// For virtio spec compliance
// "A device MUST set the status byte to VIRTIO_BLK_S_IOERR for a write request
// if the VIRTIO_BLK_F_RO feature if offered, and MUST NOT write any data."
if let Err(e) = Self::check_request(self.acked_features, request.request_type) {
// Also, if sector 0 writes are disabled, treat writes to sector 0 as read-only as well.
if let Err(e) =
Self::check_request(self.acked_features, &request, self.disable_sector0_writes)
{
warn!("Request check failed: {request:x?} {e:?}");
desc_chain
.memory()
@@ -644,6 +658,7 @@ pub struct Block {
exit_evt: EventFd,
serial: Vec<u8>,
queue_affinity: BTreeMap<u16, Vec<usize>>,
disable_sector0_writes: bool,
}
#[derive(Serialize, Deserialize)]
@@ -672,6 +687,7 @@ impl Block {
exit_evt: EventFd,
state: Option<BlockState>,
queue_affinity: BTreeMap<u16, Vec<usize>>,
disable_sector0_writes: bool,
) -> io::Result<Self> {
let (disk_nsectors, avail_features, acked_features, config, paused) =
if let Some(state) = state {
@@ -772,6 +788,7 @@ impl Block {
exit_evt,
serial,
queue_affinity,
disable_sector0_writes,
})
}
@@ -1016,6 +1033,7 @@ impl VirtioDevice for Block {
access_platform: self.common.access_platform.clone(),
host_cpus: self.queue_affinity.get(&queue_idx).cloned(),
acked_features: self.common.acked_features,
disable_sector0_writes: self.disable_sector0_writes,
};
let paused = self.common.paused.clone();

View File

@@ -703,6 +703,9 @@ components:
default: false
max_phys_bits:
type: integer
nested:
type: boolean
default: true
affinity:
type: array
items:
@@ -938,6 +941,12 @@ components:
type: array
items:
$ref: "#/components/schemas/VirtQueueAffinity"
backing_files:
type: boolean
default: false
image_type:
type: enum ["FixedVhd", "Qcow2", "Raw", "Vhdx"]
NetConfig:
type: object

View File

@@ -10,6 +10,7 @@ use std::path::PathBuf;
use std::result;
use std::str::FromStr;
use block::ImageType;
use clap::ArgMatches;
use log::{debug, warn};
use option_parser::{
@@ -1093,7 +1094,8 @@ impl DiskConfig {
ops_size=<io_ops>,ops_one_time_burst=<io_ops>,ops_refill_time=<ms>,\
id=<device_id>,pci_segment=<segment_id>,rate_limit_group=<group_id>,\
queue_affinity=<list_of_queue_indices_with_their_associated_cpuset>,\
serial=<serial_number>";
serial=<serial_number>,backing_files=on|off,\
image_type=<raw,qcow2,vhd,vhdx>";
pub fn parse(disk: &str) -> Result<Self> {
let mut parser = OptionParser::new();
@@ -1118,7 +1120,10 @@ impl DiskConfig {
.add("pci_segment")
.add("serial")
.add("rate_limit_group")
.add("queue_affinity");
.add("queue_affinity")
.add("backing_files")
.add("image_type");
parser.parse(disk).map_err(Error::ParseDisk)?;
let path = parser.get("path").map(PathBuf::from);
@@ -1203,6 +1208,22 @@ impl DiskConfig {
})
.collect()
});
let backing_files = parser
.convert::<Toggle>("backing_files")
.map_err(Error::ParseDisk)?
.unwrap_or(Toggle(false))
.0;
let image_type = if vhost_socket.is_none() {
parser
.convert::<ImageType>("image_type")
.map_err(Error::ParseDisk)?
.unwrap_or(ImageType::Unknown)
} else {
ImageType::Unknown
};
let bw_tb_config = if bw_size != 0 && bw_refill_time != 0 {
Some(TokenBucketConfig {
size: bw_size,
@@ -1247,6 +1268,8 @@ impl DiskConfig {
pci_segment,
serial,
queue_affinity,
backing_files,
image_type,
})
}
@@ -3414,6 +3437,8 @@ mod unit_tests {
pci_segment: 0,
serial: None,
queue_affinity: None,
backing_files: false,
image_type: ImageType::Unknown,
}
}
@@ -3436,6 +3461,7 @@ mod unit_tests {
path: None,
vhost_socket: Some(String::from("/tmp/sock")),
vhost_user: true,
image_type: ImageType::Unknown,
..disk_fixture()
}
);

View File

@@ -674,6 +674,15 @@ pub enum DeviceManagerError {
/// Disk resizing failed.
#[error("Disk resize error")]
DiskResize(#[source] virtio_devices::block::Error),
/// Disk image type does not match expected type.
#[error(
"Disk image type does not match expected type: specified = {specified}, detected = {detected}"
)]
DiskImageTypeMismatch {
specified: ImageType,
detected: ImageType,
},
}
pub type DeviceManagerResult<T> = result::Result<T, DeviceManagerError>;
@@ -2654,10 +2663,45 @@ impl DeviceManager {
.clone(),
)
.map_err(DeviceManagerError::Disk)?;
let image_type =
detect_image_type(&mut file).map_err(DeviceManagerError::DetectImageType)?;
let image = match image_type {
let detected_image_type =
detect_image_type(&mut file).map_err(DeviceManagerError::DetectImageType)?;
let mut disable_sector0_writes = false;
if disk_cfg.image_type == ImageType::Unknown {
warn!(
"No image_type specified - detected as {detected_image_type}. \
Configuration updated to persist type across reboots and migrations."
);
if detected_image_type == ImageType::Raw {
warn!("Autodetected raw image type. Disabling sector 0 writes.");
disable_sector0_writes = true;
} else {
warn!(
"Non-raw image type detected. In the future it will be necessary \
to specify image_type for non-raw files."
);
}
if detected_image_type == ImageType::Qcow2 && disk_cfg.backing_files {
warn!("QCOW2 image type autodetected. Disabling backing files");
disk_cfg.backing_files = false;
}
disk_cfg.image_type = detected_image_type;
} else if disk_cfg.image_type != detected_image_type {
return Err(DeviceManagerError::DiskImageTypeMismatch {
specified: disk_cfg.image_type,
detected: detected_image_type,
});
}
if disk_cfg.image_type != ImageType::Qcow2 && disk_cfg.backing_files {
warn!("Enabling backing_files option only applies for QCOW2 files");
}
let image = match disk_cfg.image_type {
ImageType::FixedVhd => {
// Use asynchronous backend relying on io_uring if the
// syscalls are supported.
@@ -2710,7 +2754,7 @@ impl DeviceManager {
ImageType::Qcow2 => {
info!("Using synchronous QCOW2 disk file");
Box::new(
QcowDiskSync::new(file, disk_cfg.direct)
QcowDiskSync::new(file, disk_cfg.direct, disk_cfg.backing_files)
.map_err(DeviceManagerError::CreateQcowDiskSync)?,
) as Box<dyn DiskFile>
}
@@ -2721,6 +2765,7 @@ impl DeviceManager {
.map_err(DeviceManagerError::CreateFixedVhdxDiskSync)?,
) as Box<dyn DiskFile>
}
ImageType::Unknown => unreachable!(),
};
let rate_limit_group =
@@ -2785,6 +2830,7 @@ impl DeviceManager {
state_from_id(self.snapshot.as_ref(), id.as_str())
.map_err(DeviceManagerError::RestoreGetState)?,
queue_affinity,
disable_sector0_writes,
)
.map_err(DeviceManagerError::CreateVirtioBlock)?;

View File

@@ -8,6 +8,7 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::{fs, result};
use block::ImageType;
use log::{debug, warn};
use net_util::MacAddr;
use serde::{Deserialize, Serialize};
@@ -284,6 +285,10 @@ pub struct DiskConfig {
pub serial: Option<String>,
#[serde(default)]
pub queue_affinity: Option<Vec<VirtQueueAffinity>>,
#[serde(default)]
pub backing_files: bool,
#[serde(default)]
pub image_type: ImageType,
}
impl ApplyLandlock for DiskConfig {