mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf94d3dad9 | ||
|
|
02e3570bdd | ||
|
|
dae66ce493 | ||
|
|
90cee24f98 | ||
|
|
5a0b6f2d06 | ||
|
|
30166a4ea5 | ||
|
|
f93340d337 | ||
|
|
4c1f854ee9 | ||
|
|
ecab9f1b96 |
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)> {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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}}}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -67,6 +67,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
|
||||
EventFd::new(EFD_NONBLOCK).unwrap(),
|
||||
None,
|
||||
queue_affinity,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user