Compare commits

...

14 Commits

Author SHA1 Message Date
Bo Chen
aa208c147e build: Release v51.2
Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-05-14 14:47:42 -07:00
Bo Chen
3453eb6e86 misc: Fix various clippy issues
Assisted-by: Claude:Opus-4.7
Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-05-14 14:47:42 -07:00
Bo Chen
e2cd6ff8c9 block, hypervisor: Fix cargo +nightly fmt issues
Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-05-14 14:47:42 -07:00
Dylan Reid
3ba8e92c6a block: raw_async: reject batch atomically when SQ lacks capacity
submit_batch_requests pushed each BatchRequest into the io_uring SQ in
turn and used `?` to bail on the first push failure.
Leaving the initial SQEs visible to the kernel — but submitter.submit()
was never called, and every other call site in this file gates submit()
behind a preceding sq.push() that now also fails on the full ring.

This could allow a guest to DoS it's own queue or worse if the buffer is
freed early.

Signed-off-by: Dylan Reid <dgreid@fb.com>
(cherry picked from commit ee315d2e7c)
2026-05-14 14:47:42 -07:00
Dylan Reid
7796c49afd block: AlignedOperation owns its bounce buffer via Drop
The bounce buffer for an unaligned descriptor was allocated in
execute_async and leaked on error paths, even though, for the sync
case the kernel already had a pointer to the buffer.

Clean this up by moving ownership of the buffer to the
AlignedOperation type. To make it actually safe, stop stashing a
guest memory pointer for the duration of the op. Instead, save the
guest address and pass guest memory back to the complete function.

Signed-off-by: Dylan Reid <dgreid@fb.com>
(cherry picked from commit 1b8c92dd5e3c0c58316826486ce5ee30eeb71407))
[backport: adapted to stable/v51.x; v51.x has no block/src/request.rs
 split, so the new aligned_operation module is added next to
 block/src/lib.rs and the in tree struct, alloc, free path is
 replaced in place.]
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-14 14:47:42 -07:00
Dylan Reid
68feea7cbe virtio-devices: block: track non-batch inflight reqs immediately
For non-batch backends execute_async submits the kernel I/O inline
before returning. An early return while processing before inserting in
inflight_requests, meant the request went untracked, the local batch
list was never appended to inflight_requests, even though the request is
pending in the kernel.

To track it, insert into self.inflight_requests as soon as execute_async
returns Ok. The completion path's find_inflight_request now matches the
orphan and the bounce buffer is freed only after the kernel signals it
is done.

Signed-off-by: Dylan Reid <dgreid@fb.com>
(cherry picked from commit fa8acbd712)
2026-05-14 14:47:42 -07:00
Dylan Reid
3bf94535d5 virtio-devices: block: reject duplicate in-flight head_index
A malicious or buggy guest can violate virtio by making the same
descriptor head available twice before the first chain has been placed
on the used ring. The submit path pushed both chains onto the
VecDeque-backed inflight_requests keyed by head_index, and on completion
find_inflight_request() returned the first linear match. That Request's
complete_async() freed its bounce buffer while the other chain's
io_uring op was still targeting it, producing a use-after-free the
kernel could then scribble into.

Signed-off-by: Dylan Reid <dgreid@fb.com>
(cherry picked from commit 544fa4aa76)
2026-05-14 14:47:42 -07:00
Peter Oskolkov
2e7ff20a11 virtio-devices: block: handle corrupted requests with NEEDS_RESET
Signed-off-by: Peter Oskolkov <posk@google.com>
(cherry picked from commit 8b60b38281)
2026-05-14 14:47:42 -07:00
Peter Oskolkov
7696bcc71f virtio-devices: net: handle corrupted requests with NEEDS_RESET
A buggy or malicious guest may write an inappropriate value into
virtqueue's next_avail field. This will result in an error
when iterating over the queue:

863837ef86/virtio-queue/src/queue.rs (L708)

but this error is (logged and) ignored if pop_descriptor_chain()
is used:

863837ef86/virtio-queue/src/queue.rs (L583)

A reasonable approach, implemented here, is to mark the device as
NEEDS_RESET and ignore further queue events until the guest
reinitializes the device.

How this patch was tested:

Linux kernel was patched to trigger a bad next_avail when the
virtqueue queue counter reaches 5000:

--------------- START OF LINUX KERNEL PATCH ----------
$ git diff
diff --git a/drivers/virtio/virtio_ring.c b/drivers/virtio/virtio_ring.c
index b784aab668670..989f2a0c64a77 100644
--- a/drivers/virtio/virtio_ring.c
+++ b/drivers/virtio/virtio_ring.c
@@ -15,6 +15,9 @@
 #include <linux/spinlock.h>
 #include <xen/xen.h>

+
+void virtqueue_kick_always(struct virtqueue *vq);
+
 #ifdef DEBUG
 /* For development, we want to crash whenever the ring is screwed. */
 #define BAD_RING(_vq, fmt, args...)                            \
@@ -677,6 +680,12 @@ static inline int virtqueue_add_split(
                   struct virtqueue *_vq,
         * new available array entries. */
        virtio_wmb(vq->weak_barriers);
        vq->split.avail_idx_shadow++;
+       {
+        if ((vq->split.avail_idx_shadow % 100) == 0)
+            printk(KERN_ERR "avail idx: %d",
+                  (int)vq->split.avail_idx_shadow);
+               if (vq->split.avail_idx_shadow == 5000)
+               vq->split.avail_idx_shadow = 0;
+       }
        vq->split.vring.avail->idx = cpu_to_virtio16(_vq->vdev,
                                      vq->split.avail_idx_shadow);
        vq->num_added++;
@@ -689,6 +698,11 @@ static inline int virtqueue_add_split(
                  struct virtqueue *_vq,
        if (unlikely(vq->num_added == (1 << 16) - 1))
                virtqueue_kick(_vq);

+       {
+               if (unlikely(vq->split.avail_idx_shadow == 0))
+                       virtqueue_kick_always(_vq);
+       }
+
        return 0;

 unmap_release:
@@ -2515,6 +2529,11 @@ bool virtqueue_kick(struct virtqueue *vq)
 }
 EXPORT_SYMBOL_GPL(virtqueue_kick);

+void virtqueue_kick_always(struct virtqueue *vq)
+{
+       virtqueue_kick_prepare(vq);
+       virtqueue_notify(vq);
+}
 /**
  * virtqueue_get_buf_ctx - get the next used buffer
  * @_vq: the struct virtqueue we're talking about.
--------------- END OF LINUX KERNEL PATCH ----------

Then the kernel was booted, and the host pinged until the
nic became unresponsive:

ping -i 0.002 192.168.4.1

Device status was confirmed using

cat /sys/class/net/eth0/device/status

(it was 0x4f).

Then the device was re-initialized:

DEV_NAME=$(basename $(readlink -f /sys/class/net/eth0/device))
echo $DEV_NAME | tee /sys/bus/virtio/drivers/virtio_net/unbind
echo $DEV_NAME | tee /sys/bus/virtio/drivers/virtio_net/bind
ip link set eth0 up

At this point networking became healthly again.

Signed-off-by: Peter Oskolkov <posk@google.com>
(cherry picked from commit 563303b50a)
2026-05-14 14:47:42 -07:00
Peter Oskolkov
e97524ecf6 virtio-devices: wire driver_status to EpollHandler
Signed-off-by: Peter Oskolkov <posk@google.com>
(cherry picked from commit b5053ae4de)
2026-05-14 14:47:42 -07:00
Peter Oskolkov
38dbcc2f44 virtio-devices: switch driver_status to Arc<AtomicU8>
Signed-off-by: Peter Oskolkov <posk@google.com>
(cherry picked from commit 21bd3ae916)
2026-05-14 14:47:42 -07:00
Peter Oskolkov
fdd682c0b0 virtio-devices: introduce ActivationContext for device activation
Signed-off-by: Peter Oskolkov <posk@google.com>
(cherry picked from commit f77c6ef78b)
2026-05-14 14:47:42 -07:00
Bo Chen
9503e1ade9 build: Release v51.1
Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-02-22 21:12:53 +00:00
Wei Liu
52b2ebb2b8 vmm: api: Fix image_type in OpenAPI definition
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-02-21 08:50:31 +00:00
45 changed files with 588 additions and 354 deletions

2
Cargo.lock generated
View File

@@ -427,7 +427,7 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "cloud-hypervisor"
version = "51.0.0"
version = "51.2.0"
dependencies = [
"anyhow",
"api_client",

View File

@@ -641,17 +641,16 @@ pub fn generate_common_cpuid(
// Update some existing CPUID
for entry in cpuid.as_mut_slice().iter_mut() {
#[allow(unused_unsafe)]
match entry.function {
// Clear AMX related bits if the AMX feature is not enabled
0x7 => {
if !config.amx {
if entry.index == 0 {
entry.edx &= !((1 << AMX_BF16) | (1 << AMX_TILE) | (1 << AMX_INT8));
}
if entry.index == 1 {
entry.eax &= !(1 << AMX_FP16);
entry.edx &= !(1 << AMX_COMPLEX);
}
0x7 if !config.amx => {
if entry.index == 0 {
entry.edx &= !((1 << AMX_BF16) | (1 << AMX_TILE) | (1 << AMX_INT8));
}
if entry.index == 1 {
entry.eax &= !(1 << AMX_FP16);
entry.edx &= !(1 << AMX_COMPLEX);
}
}
0xd =>
@@ -673,55 +672,46 @@ pub fn generate_common_cpuid(
}
}
}
0x1d => {
// Tile Information (purely AMX related).
if !config.amx {
entry.eax = 0;
entry.ebx = 0;
entry.ecx = 0;
entry.edx = 0;
}
// Tile Information (purely AMX related).
0x1d if !config.amx => {
entry.eax = 0;
entry.ebx = 0;
entry.ecx = 0;
entry.edx = 0;
}
0x1e => {
// TMUL information (purely AMX related)
if !config.amx {
entry.eax = 0;
entry.ebx = 0;
entry.ecx = 0;
entry.edx = 0;
}
// TMUL information (purely AMX related)
0x1e if !config.amx => {
entry.eax = 0;
entry.ebx = 0;
entry.ecx = 0;
entry.edx = 0;
}
// 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)]
0x8000_0005
if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0
// SAFETY: cpuid called with valid leaves
if unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 {
// SAFETY: cpuid called with valid leaves
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0005) };
entry.eax = leaf.eax;
entry.ebx = leaf.ebx;
entry.ecx = leaf.ecx;
entry.edx = leaf.edx;
}
}
&& unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 =>
{
// SAFETY: cpuid called with valid leaves
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0005) };
entry.eax = leaf.eax;
entry.ebx = leaf.ebx;
entry.ecx = leaf.ecx;
entry.edx = leaf.edx;
}
// 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)]
0x8000_0006
if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0
// 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;
entry.ebx = leaf.ebx;
entry.ecx = leaf.ecx;
entry.edx = leaf.edx;
}
}
&& unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 =>
{
// SAFETY: cpuid called with valid leaves
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0006) };
entry.eax = leaf.eax;
entry.ebx = leaf.ebx;
entry.ecx = leaf.ecx;
entry.edx = leaf.edx;
}
// Set CPU physical bits
0x8000_0008 => {

View File

@@ -0,0 +1,90 @@
// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::io;
use vm_memory::GuestAddress;
/// Owns an aligned bounce buffer used when a guest descriptor's host VA
/// does not meet the disk backend's alignment requirement.
#[derive(Debug)]
pub struct AlignedOperation {
data_addr: GuestAddress,
aligned_ptr: *mut u8,
size: usize,
layout: Layout,
}
impl AlignedOperation {
/// Allocate a zero-initialized buffer of `size` bytes aligned to
/// `alignment`. Returns `InvalidInput` if `size` is zero;
/// `alignment` must be a power of two and not exceed `isize::MAX`
/// after rounding up.
pub fn new(data_addr: GuestAddress, size: usize, alignment: usize) -> io::Result<Self> {
if size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"AlignedOperation requires a non-zero size",
));
}
let layout = Layout::from_size_align(size, alignment)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
// SAFETY: size is non-zero (checked above) and Layout::from_size_align
// rejects alignments that are not a power of two or that overflow.
let aligned_ptr = unsafe { alloc_zeroed(layout) };
if aligned_ptr.is_null() {
return Err(io::Error::last_os_error());
}
Ok(Self {
data_addr,
aligned_ptr,
size,
layout,
})
}
/// Gets the raw pointer to the aligned buffer.
pub fn as_mut_ptr(&mut self) -> *mut u8 {
self.aligned_ptr
}
/// Returns the aligned buffer as a slice.
pub fn as_bytes(&self) -> &[u8] {
// SAFETY: `new` allocates `size` bytes via alloc_zeroed (so they
// are initialized) and AlignedOperation owns the buffer
// exclusively.
unsafe { std::slice::from_raw_parts(self.aligned_ptr, self.size) }
}
/// Returns the aligned buffer as a mutable slice.
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
// SAFETY: same invariant as as_bytes; &mut self rules out other
// simultaneous borrows.
unsafe { std::slice::from_raw_parts_mut(self.aligned_ptr, self.size) }
}
/// Returns the guest address for this op.
pub fn data_addr(&self) -> GuestAddress {
self.data_addr
}
}
impl Drop for AlignedOperation {
fn drop(&mut self) {
// SAFETY: `new` is the only constructor, and it stores a pointer
// returned by `alloc_zeroed` paired with the exact `layout` used
// for that allocation. Ownership has not escaped (the type is
// neither `Clone` nor `Copy`).
unsafe {
dealloc(self.aligned_ptr, self.layout);
}
}
}
// SAFETY: AlignedOperation owns its heap allocation exclusively (no Clone/
// Copy, no shared aliases) and the allocation's lifetime is tied to the
// value's. Moving an AlignedOperation between threads transfers that
// ownership; the same rationale Box<T> uses for its Send impl.
unsafe impl Send for AlignedOperation {}

View File

@@ -8,6 +8,7 @@
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
mod aligned_operation;
pub mod async_io;
pub mod fcntl;
pub mod fixed_vhd;
@@ -28,7 +29,7 @@ pub mod vhd;
pub mod vhdx;
pub mod vhdx_sync;
use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::alloc::{Layout, alloc_zeroed};
use std::collections::VecDeque;
use std::fmt::{self, Debug};
use std::fs::File;
@@ -40,6 +41,7 @@ use std::str::FromStr;
use std::time::Instant;
use std::{cmp, result};
pub use aligned_operation::AlignedOperation;
#[cfg(feature = "io_uring")]
use io_uring::{IoUring, Probe, opcode};
use libc::{S_IFBLK, S_IFMT, ioctl};
@@ -232,14 +234,6 @@ fn sector<B: Bitmap + 'static>(
const DEFAULT_DESCRIPTOR_VEC_SIZE: usize = 32;
#[derive(Debug)]
pub struct AlignedOperation {
origin_ptr: u64,
aligned_ptr: u64,
size: usize,
layout: Layout,
}
pub struct BatchRequest {
pub offset: libc::off_t,
pub iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]>,
@@ -473,31 +467,19 @@ impl Request {
let iov_base = if (origin_ptr.as_ptr() as u64).is_multiple_of(SECTOR_SIZE) {
origin_ptr.as_ptr() as *mut libc::c_void
} else {
let layout = Layout::from_size_align(data_len, SECTOR_SIZE as usize).unwrap();
// SAFETY: layout has non-zero size
let aligned_ptr = unsafe { alloc_zeroed(layout) };
if aligned_ptr.is_null() {
return Err(ExecuteError::TemporaryBufferAllocation(
io::Error::last_os_error(),
));
}
let mut aligned_op =
AlignedOperation::new(data_addr, data_len, SECTOR_SIZE as usize)
.map_err(ExecuteError::TemporaryBufferAllocation)?;
// We need to perform the copy beforehand in case we're writing
// data out.
if request_type == RequestType::Out {
// SAFETY: destination buffer has been allocated with
// the proper size.
unsafe { std::ptr::copy(origin_ptr.as_ptr(), aligned_ptr, data_len) };
mem.read_slice(aligned_op.as_bytes_mut(), data_addr)
.map_err(ExecuteError::Read)?;
}
// Store both origin and aligned pointers for complete_async()
// to process them.
self.aligned_operations.push(AlignedOperation {
origin_ptr: origin_ptr.as_ptr() as u64,
aligned_ptr: aligned_ptr as u64,
size: data_len,
layout,
});
let aligned_ptr = aligned_op.as_mut_ptr();
self.aligned_operations.push(aligned_op);
aligned_ptr as *mut libc::c_void
};
@@ -639,31 +621,17 @@ impl Request {
Ok(ret)
}
pub fn complete_async(&mut self) -> result::Result<(), Error> {
for aligned_operation in self.aligned_operations.drain(..) {
pub fn complete_async<B: Bitmap + 'static>(
&mut self,
mem: &vm_memory::GuestMemoryMmap<B>,
) -> result::Result<(), Error> {
for aligned_op in self.aligned_operations.drain(..) {
// We need to perform the copy after the data has been read inside
// the aligned buffer in case we're reading data in.
if self.request_type == RequestType::In {
// SAFETY: origin buffer has been allocated with the
// proper size.
unsafe {
std::ptr::copy(
aligned_operation.aligned_ptr as *const u8,
aligned_operation.origin_ptr as *mut u8,
aligned_operation.size,
);
};
mem.write_slice(aligned_op.as_bytes(), aligned_op.data_addr())
.map_err(Error::GuestMemory)?;
}
// Free the temporary aligned buffer.
// SAFETY: aligned_ptr was allocated by alloc_zeroed with the same
// layout
unsafe {
dealloc(
aligned_operation.aligned_ptr as *mut u8,
aligned_operation.layout,
);
};
}
Ok(())

View File

@@ -197,6 +197,14 @@ impl AsyncIo for RawFileAsync {
let (submitter, mut sq, _) = self.io_uring.split();
let mut submitted = false;
// Refuse the whole batch if it can't fit in the SQ to avoid having to unroll a partially
// successful push.
if batch_request.len() > sq.capacity() - sq.len() {
return Err(AsyncIoError::SubmitBatchRequests(Error::other(
"io_uring submission queue is full",
)));
}
for req in batch_request {
match req.request_type {
RequestType::In => {

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 = "51.0.0"
version = "51.2.0"
# Minimum buildable version:
# Keep in sync with version in .github/workflows/build.yaml
# Policy on MSRV (see #4318):

View File

@@ -95,15 +95,16 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
reporting_queue_evt.write(1).unwrap();
balloon
.activate(
guest_memory,
Arc::new(NoopVirtioInterrupt {}),
vec![
.activate(virtio_devices::ActivationContext {
mem: guest_memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![
(0, inflate_q, inflate_evt),
(1, deflate_q, deflate_evt),
(2, reporting_q, reporting_evt),
],
)
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.ok();
// Wait for the events to finish and balloon device worker thread to return

View File

@@ -89,11 +89,12 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
queue_evt.write(1).unwrap();
block
.activate(
guest_memory,
Arc::new(NoopVirtioInterrupt {}),
vec![(0, q, evt)],
)
.activate(virtio_devices::ActivationContext {
mem: guest_memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![(0, q, evt)],
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.ok();
// Wait for the events to finish and block device worker thread to return

View File

@@ -128,11 +128,12 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
pipe_tx.write_all(console_input_bytes).unwrap(); // To use fuzzed data;
console
.activate(
guest_memory,
Arc::new(NoopVirtioInterrupt {}),
vec![(0, input_queue, input_evt), (1, output_queue, output_evt)],
)
.activate(virtio_devices::ActivationContext {
mem: guest_memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![(0, input_queue, input_evt), (1, output_queue, output_evt)],
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.unwrap();
// Wait for the events to finish and console device worker thread to return

View File

@@ -107,14 +107,15 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
request_queue_evt.write(1).unwrap();
iommu
.activate(
guest_memory,
Arc::new(NoopVirtioInterrupt {}),
vec![
.activate(virtio_devices::ActivationContext {
mem: guest_memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![
(0, request_queue, request_evt),
(0, _event_queue, _event_evt),
],
)
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.ok();
// Wait for the events to finish and vIOMMU device worker thread to return

View File

@@ -105,11 +105,12 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
queue_evt.write(1).unwrap();
virtio_mem
.activate(
guest_memory,
Arc::new(NoopVirtioInterrupt {}),
vec![(0, q, evt)],
)
.activate(virtio_devices::ActivationContext {
mem: guest_memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![(0, q, evt)],
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.ok();
// Wait for the events to finish and virtio-mem device worker thread to return

View File

@@ -143,11 +143,12 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
input_queue_evt.write(1).unwrap();
output_queue_evt.write(1).unwrap();
net.activate(
guest_memory,
Arc::new(NoopVirtioInterrupt {}),
vec![(0, input_queue, input_evt), (1, output_queue, output_evt)],
)
net.activate(virtio_devices::ActivationContext {
mem: guest_memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![(0, input_queue, input_evt), (1, output_queue, output_evt)],
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.unwrap();
// Wait for the events to finish and net device worker thread to return

View File

@@ -61,11 +61,12 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
// Kick the 'queue' event before activate the pmem device
queue_evt.write(1).unwrap();
pmem.activate(
guest_memory,
Arc::new(NoopVirtioInterrupt {}),
vec![(0, q, evt)],
)
pmem.activate(virtio_devices::ActivationContext {
mem: guest_memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![(0, q, evt)],
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.ok();
// Wait for the events to finish and pmem device worker thread to return

View File

@@ -99,11 +99,12 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
// Kick the 'queue' event before activate the rng device
queue_evt.write(1).unwrap();
rng.activate(
guest_memory,
Arc::new(NoopVirtioInterrupt {}),
vec![(0, q, evt)],
)
rng.activate(virtio_devices::ActivationContext {
mem: guest_memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![(0, q, evt)],
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.ok();
// Wait for the events to finish and rng device worker thread to return

View File

@@ -108,11 +108,12 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
.unwrap();
vsock
.activate(
guest_memory,
Arc::new(NoopVirtioInterrupt {}),
vec![(0, q, evt)],
)
.activate(virtio_devices::ActivationContext {
mem: guest_memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![(0, q, evt)],
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.ok();
// Wait for the events to finish and vsock device worker thread to return

View File

@@ -64,11 +64,12 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
queue_evt.write(1).unwrap();
watchdog
.activate(
guest_memory,
Arc::new(NoopVirtioInterrupt {}),
vec![(0, q, evt)],
)
.activate(virtio_devices::ActivationContext {
mem: guest_memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![(0, q, evt)],
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.ok();
// Wait for the events to finish and watchdog device worker thread to return

View File

@@ -90,11 +90,11 @@ pub use kvm_bindings::kvm_vcpu_events as VcpuEvents;
#[cfg(target_arch = "x86_64")]
use kvm_bindings::nested::KvmNestedStateBuffer;
pub use kvm_bindings::{
KVM_GUESTDBG_ENABLE, KVM_GUESTDBG_SINGLESTEP, KVM_IRQ_ROUTING_IRQCHIP, KVM_IRQ_ROUTING_MSI,
KVM_MEM_LOG_DIRTY_PAGES, KVM_MEM_READONLY, KVM_MSI_VALID_DEVID, kvm_clock_data,
kvm_create_device, kvm_create_device as CreateDevice, kvm_device_attr as DeviceAttr,
kvm_device_type_KVM_DEV_TYPE_VFIO, kvm_guest_debug, kvm_irq_routing, kvm_irq_routing_entry,
kvm_mp_state, kvm_run, kvm_userspace_memory_region,
self, KVM_GUESTDBG_ENABLE, KVM_GUESTDBG_SINGLESTEP, KVM_IRQ_ROUTING_IRQCHIP,
KVM_IRQ_ROUTING_MSI, KVM_MEM_LOG_DIRTY_PAGES, KVM_MEM_READONLY, KVM_MSI_VALID_DEVID,
kvm_clock_data, kvm_create_device, kvm_create_device as CreateDevice,
kvm_device_attr as DeviceAttr, kvm_device_type_KVM_DEV_TYPE_VFIO, kvm_guest_debug,
kvm_irq_routing, kvm_irq_routing_entry, kvm_mp_state, kvm_run, kvm_userspace_memory_region,
};
#[cfg(target_arch = "aarch64")]
use kvm_bindings::{
@@ -109,14 +109,13 @@ use kvm_bindings::{KVM_REG_RISCV_CORE, kvm_riscv_core};
use kvm_bindings::{KVM_X86_DEFAULT_VM, KVM_X86_SW_PROTECTED_VM, KVMIO, kvm_run__bindgen_ty_1};
#[cfg(target_arch = "x86_64")]
use kvm_bindings::{Xsave as xsave2, kvm_xsave2};
pub use kvm_ioctls::{Cap, Kvm, VcpuExit};
pub use kvm_ioctls::{self, Cap, Kvm, VcpuExit};
use thiserror::Error;
use vfio_ioctls::VfioDeviceFd;
#[cfg(target_arch = "x86_64")]
use vmm_sys_util::{fam::FamStruct, ioctl_io_nr};
#[cfg(feature = "tdx")]
use vmm_sys_util::{ioctl::ioctl_with_val, ioctl_iowr_nr};
pub use {kvm_bindings, kvm_ioctls};
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
use crate::RegList;

View File

@@ -51,7 +51,13 @@ impl TxVirtio {
let mut retry_write = false;
let mut rate_limit_reached = false;
while let Some(mut desc_chain) = queue.pop_descriptor_chain(mem) {
loop {
let mut iter = queue
.iter(mem)
.map_err(NetQueuePairError::QueueIteratorFailed)?;
let Some(mut desc_chain) = iter.next() else {
break;
};
if rate_limit_reached {
queue.go_to_previous_position();
break;
@@ -180,7 +186,13 @@ impl RxVirtio {
let mut exhausted_descs = true;
let mut rate_limit_reached = false;
while let Some(mut desc_chain) = queue.pop_descriptor_chain(mem) {
loop {
let mut iter = queue
.iter(mem)
.map_err(NetQueuePairError::QueueIteratorFailed)?;
let Some(mut desc_chain) = iter.next() else {
break;
};
if rate_limit_reached {
exhausted_descs = false;
queue.go_to_previous_position();

View File

@@ -1,3 +1,5 @@
- [v51.2](#v512)
- [v51.1](#v511)
- [v51.0](#v510)
- [Security Fixes](#security-fixes)
- [Significant QCOW2 v3 Improvements](#significant-qcow2-v3-improvements)
@@ -418,6 +420,18 @@
- [Unit testing](#unit-testing)
- [Integration tests parallelization](#integration-tests-parallelization)
# v51.2
This is a point release containing security fixes to a use-after-free
vulnerability in the `virtio-block` async I/O completion path
(#8220). Details can be found in GHSA-f47p-p25q-83rh (CVE-2026-45782).
# v51.1
This is a bug fix release. The following issues have been addressed:
* Fix image_type in OpenAPI definition (#7734)
# v51.0
This release has been tracked in [v51.0

View File

@@ -590,12 +590,13 @@ impl VirtioDevice for Balloon {
}
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
mut queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
mut queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
let (kill_evt, pause_evt) = self.common.dup_eventfds();

View File

@@ -13,7 +13,7 @@ use std::num::Wrapping;
use std::ops::Deref;
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
use std::sync::{Arc, Barrier};
use std::{io, result};
@@ -161,6 +161,7 @@ struct BlockEpollHandler {
host_cpus: Option<Vec<usize>>,
acked_features: u64,
disable_sector0_writes: bool,
device_status: Arc<AtomicU8>,
}
fn has_feature(features: u64, feature_flag: u64) -> bool {
@@ -168,6 +169,10 @@ fn has_feature(features: u64, feature_flag: u64) -> bool {
}
impl BlockEpollHandler {
fn needs_reset(&self) -> bool {
(self.device_status.load(Ordering::Acquire) & crate::DEVICE_NEEDS_RESET as u8) != 0
}
fn check_request(
features: u64,
request: &Request,
@@ -192,12 +197,71 @@ impl BlockEpollHandler {
Ok(())
}
fn handle_queue_iterator_error(&mut self, err: &virtio_queue::Error) {
// The guest submitted a corrupted VirtQ request, and the error
// was logged during queue processing. We cannot just ignore the
// error, as the guest could continue spamming the VMM with bad
// requests, triggering excessive error logging. So we mark
// the device "NEEDS_RESET", effectively stopping all request
// processing (see self.needs_reset() usage) until the guest
// resets and reactivates the device.
warn!(
"Corrupted request detected (virtqueue error: {err:?}). \
Setting device status to 'NEEDS_RESET' and stopping processing queues until reset."
);
self.set_needs_reset();
}
fn set_needs_reset(&mut self) {
self.device_status
.fetch_or(crate::DEVICE_NEEDS_RESET as u8, Ordering::SeqCst);
// Let the guest know that the device status has changed.
if let Err(e) = self.interrupt_cb.trigger(VirtioInterruptType::Config) {
error!("Failed to signal config interrupt: {e:?}");
}
}
// A spec-compliant driver never reuses a virtqueue head_index while the
// corresponding chain is still available (virtio 1.x §2.7.13.4).
// Double check the guest driver is behaving.
fn is_head_in_flight(
inflight: &VecDeque<(u16, Request)>,
batch: &[(u16, Request)],
head: u16,
) -> bool {
batch.iter().any(|(h, _)| *h == head) || inflight.iter().any(|(h, _)| *h == head)
}
fn process_queue_submit(&mut self) -> Result<()> {
if self.needs_reset() {
return Ok(());
}
let queue = &mut self.queue;
let mut batch_requests = Vec::new();
let mut batch_inflight_requests = Vec::new();
while let Some(mut desc_chain) = queue.pop_descriptor_chain(self.mem.memory()) {
loop {
let mut desc_chain = match queue.iter(self.mem.memory()) {
Ok(mut iter) => match iter.next() {
Some(c) => c,
None => break,
},
Err(err) => {
self.handle_queue_iterator_error(&err);
return Ok(());
}
};
let head = desc_chain.head_index();
if Self::is_head_in_flight(&self.inflight_requests, &batch_inflight_requests, head) {
warn!("Guest reused virtio-blk head_index {head} while the chain was used");
self.set_needs_reset();
return Ok(());
}
let mut request = Request::parse(&mut desc_chain, self.access_platform.as_deref())
.map_err(Error::RequestParsing)?;
@@ -282,8 +346,11 @@ impl BlockEpollHandler {
)
}
}
batch_inflight_requests.push((desc_chain.head_index(), request));
} else {
self.inflight_requests
.push_back((desc_chain.head_index(), request));
}
batch_inflight_requests.push((desc_chain.head_index(), request));
} else {
let status = match result {
Ok(_) => VIRTIO_BLK_S_OK,
@@ -380,6 +447,9 @@ impl BlockEpollHandler {
}
fn process_queue_complete(&mut self) -> Result<()> {
if self.needs_reset() {
return Ok(());
}
let mem = self.mem.memory();
let mut read_bytes = Wrapping(0);
let mut write_bytes = Wrapping(0);
@@ -391,7 +461,9 @@ impl BlockEpollHandler {
let mut request = self.find_inflight_request(desc_index)?;
request.complete_async().map_err(Error::RequestCompleting)?;
request
.complete_async(&mem)
.map_err(Error::RequestCompleting)?;
let latency = request.start.elapsed().as_micros() as u64;
let read_ops_last = self.counters.read_ops.load(Ordering::Relaxed);
@@ -662,6 +734,7 @@ pub struct Block {
serial: Vec<u8>,
queue_affinity: BTreeMap<u16, Vec<usize>>,
disable_sector0_writes: bool,
device_status: Arc<AtomicU8>,
}
#[derive(Serialize, Deserialize)]
@@ -807,6 +880,7 @@ impl Block {
serial,
queue_affinity,
disable_sector0_writes,
device_status: Arc::new(AtomicU8::new(0)),
})
}
@@ -998,12 +1072,14 @@ impl VirtioDevice for Block {
self.update_writeback();
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
mut queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
mut queues,
device_status,
} = context;
self.device_status = device_status;
// See if the guest didn't ack the device being read-only.
// If so, warn and pretend it did.
let original_acked_features = self.common.acked_features;
@@ -1059,6 +1135,7 @@ impl VirtioDevice for Block {
host_cpus: self.queue_affinity.get(&queue_idx).cloned(),
acked_features: self.common.acked_features,
disable_sector0_writes: self.disable_sector0_writes,
device_status: self.device_status.clone(),
};
let paused = self.common.paused.clone();

View File

@@ -703,12 +703,13 @@ impl VirtioDevice for Console {
self.read_config_from_slice(self.config.lock().unwrap().as_slice(), offset, data);
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
mut queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
mut queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
self.resizer
.acked_features

View File

@@ -9,7 +9,7 @@
use std::collections::HashMap;
use std::io::Write;
use std::num::Wrapping;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::{Arc, Barrier};
use std::thread;
@@ -53,6 +53,13 @@ pub struct VirtioSharedMemoryList {
pub region_list: Vec<VirtioSharedMemory>,
}
pub struct ActivationContext {
pub mem: GuestMemoryAtomic<GuestMemoryMmap>,
pub interrupt_cb: Arc<dyn VirtioInterrupt>,
pub queues: Vec<(usize, Queue, EventFd)>,
pub device_status: Arc<AtomicU8>,
}
/// Trait for virtio devices to be driven by a virtio transport.
///
/// The lifecycle of a virtio device is to be moved to a virtio transport, which will then query the
@@ -94,12 +101,7 @@ pub trait VirtioDevice: Send {
}
/// Activates this device for real usage.
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_evt: Arc<dyn VirtioInterrupt>,
queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult;
fn activate(&mut self, context: ActivationContext) -> ActivateResult;
/// Optionally deactivates this device and returns ownership of the guest memory map, interrupt
/// event, and queue events.

View File

@@ -1075,12 +1075,13 @@ impl VirtioDevice for Iommu {
self.update_bypass();
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
mut queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
mut queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
let (kill_evt, pause_evt) = self.common.dup_eventfds();

View File

@@ -42,8 +42,8 @@ pub use self::balloon::Balloon;
pub use self::block::{Block, BlockState};
pub use self::console::{Console, ConsoleResizer, Endpoint};
pub use self::device::{
DmaRemapping, VirtioCommon, VirtioDevice, VirtioInterrupt, VirtioInterruptType,
VirtioSharedMemoryList,
ActivationContext, DmaRemapping, VirtioCommon, VirtioDevice, VirtioInterrupt,
VirtioInterruptType, VirtioSharedMemoryList,
};
pub use self::epoll_helper::{
EPOLL_HELPER_EVENT_LAST, EpollHelper, EpollHelperError, EpollHelperHandler,
@@ -66,6 +66,7 @@ const DEVICE_ACKNOWLEDGE: u32 = 0x01;
const DEVICE_DRIVER: u32 = 0x02;
const DEVICE_DRIVER_OK: u32 = 0x04;
const DEVICE_FEATURES_OK: u32 = 0x08;
const DEVICE_NEEDS_RESET: u32 = 0x40;
const DEVICE_FAILED: u32 = 0x80;
const VIRTIO_F_RING_INDIRECT_DESC: u32 = 28;

View File

@@ -950,12 +950,13 @@ impl VirtioDevice for Mem {
self.read_config_from_slice(self.config.lock().unwrap().as_slice(), offset, data);
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
mut queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
mut queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
let (kill_evt, pause_evt) = self.common.dup_eventfds();

View File

@@ -10,13 +10,13 @@ use std::net::IpAddr;
use std::num::Wrapping;
use std::ops::Deref;
use std::os::unix::io::{AsRawFd, RawFd};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::{Arc, Barrier};
use std::{result, thread};
use anyhow::anyhow;
use event_monitor::event;
use log::{debug, error, info};
use log::{debug, error, info, warn};
#[cfg(not(fuzzing))]
use net_util::virtio_features_to_tap_offload;
use net_util::{
@@ -179,6 +179,7 @@ struct NetEpollHandler {
// a restore as the vCPU thread isn't ready to handle the interrupt. This causes
// issues when combined with VIRTIO_RING_F_EVENT_IDX interrupt suppression.
driver_awake: bool,
device_status: Arc<AtomicU8>,
}
impl NetEpollHandler {
@@ -192,6 +193,9 @@ impl NetEpollHandler {
}
fn handle_rx_event(&mut self) -> result::Result<(), DeviceError> {
if self.needs_reset() {
return Ok(());
}
let queue_evt = &self.queue_evt_pair.0;
if let Err(e) = queue_evt.read() {
error!("Failed to get rx queue event: {e:?}");
@@ -220,13 +224,43 @@ impl NetEpollHandler {
Ok(())
}
fn handle_queue_iterator_error(&mut self, err: &virtio_queue::Error) {
// The guest submitted a corrupted VirtQ request, and the error
// was logged during queue processing. We cannot just ignore the
// error, as the guest could continue spamming the VMM with bad
// requests, triggering excessive error logging. So we mark
// the device "NEEDS_RESET", effectively stopping all request
// processing (see self.needs_reset() usage) until the guest
// resets and reactivates the device.
warn!(
"Corrupted request detected (virtqueue error: {err:?}). \
Setting device status to 'NEEDS_RESET' and stopping processing queues until reset."
);
self.device_status
.fetch_or(crate::DEVICE_NEEDS_RESET as u8, Ordering::SeqCst);
// Let the guest know that the device status has changed.
if let Err(e) = self.interrupt_cb.trigger(VirtioInterruptType::Config) {
error!("Failed to signal config interrupt: {e:?}");
}
}
fn process_tx(&mut self) -> result::Result<(), DeviceError> {
if self
if self.needs_reset() {
return Ok(());
}
let res = self
.net
.process_tx(&self.mem.memory(), &mut self.queue_pair.1)
.map_err(DeviceError::NetQueuePair)?
|| !self.driver_awake
{
.process_tx(&self.mem.memory(), &mut self.queue_pair.1);
if let Err(net_util::NetQueuePairError::QueueIteratorFailed(err)) = res {
self.handle_queue_iterator_error(&err);
return Ok(());
}
if res.map_err(DeviceError::NetQueuePair)? || !self.driver_awake {
self.signal_used_queue(self.queue_index_base + 1)?;
debug!("Signalling TX queue");
} else {
@@ -250,12 +284,19 @@ impl NetEpollHandler {
}
fn handle_rx_tap_event(&mut self) -> result::Result<(), DeviceError> {
if self
if self.needs_reset() {
return Ok(());
}
let res = self
.net
.process_rx(&self.mem.memory(), &mut self.queue_pair.0)
.map_err(DeviceError::NetQueuePair)?
|| !self.driver_awake
{
.process_rx(&self.mem.memory(), &mut self.queue_pair.0);
if let Err(net_util::NetQueuePairError::QueueIteratorFailed(err)) = res {
self.handle_queue_iterator_error(&err);
return Ok(());
}
if res.map_err(DeviceError::NetQueuePair)? || !self.driver_awake {
self.signal_used_queue(self.queue_index_base)?;
debug!("Signalling RX queue");
} else {
@@ -305,6 +346,10 @@ impl NetEpollHandler {
Ok(())
}
fn needs_reset(&self) -> bool {
(self.device_status.load(Ordering::Acquire) & crate::DEVICE_NEEDS_RESET as u8) != 0
}
}
impl EpollHelperHandler for NetEpollHandler {
@@ -414,6 +459,7 @@ pub struct Net {
seccomp_action: SeccompAction,
rate_limiter_config: Option<RateLimiterConfig>,
exit_evt: EventFd,
device_status: Arc<AtomicU8>,
}
#[derive(Serialize, Deserialize)]
@@ -535,6 +581,7 @@ impl Net {
seccomp_action,
rate_limiter_config,
exit_evt,
device_status: Arc::new(AtomicU8::new(0)),
})
}
@@ -693,12 +740,14 @@ impl VirtioDevice for Net {
self.read_config_from_slice(self.config.as_slice(), offset, data);
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
mut queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
mut queues,
device_status,
} = context;
self.device_status = device_status;
self.common.activate(&queues, interrupt_cb.clone())?;
let num_queues = queues.len();
@@ -803,6 +852,7 @@ impl VirtioDevice for Net {
kill_evt,
pause_evt,
driver_awake: false,
device_status: self.device_status.clone(),
};
let paused = self.common.paused.clone();

View File

@@ -377,12 +377,13 @@ impl VirtioDevice for Pmem {
self.read_config_from_slice(self.config.as_slice(), offset, data);
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
mut queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
mut queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
let (kill_evt, pause_evt) = self.common.dup_eventfds();
if let Some(disk) = self.disk.as_ref() {

View File

@@ -244,12 +244,13 @@ impl VirtioDevice for Rng {
self.common.ack_features(value);
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
mut queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
mut queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
let (kill_evt, pause_evt) = self.common.dup_eventfds();

View File

@@ -6,7 +6,7 @@
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::sync::atomic::{AtomicU16, Ordering};
use std::sync::atomic::{AtomicU8, AtomicU16, Ordering};
use std::sync::{Arc, Mutex};
use byteorder::{ByteOrder, LittleEndian};
@@ -125,7 +125,7 @@ pub fn get_vring_size(t: VringType, queue_size: u16) -> u64 {
/// le64 queue_used; // 0x30 // read-write
pub struct VirtioPciCommonConfig {
pub access_platform: Option<Arc<dyn AccessPlatform>>,
pub driver_status: u8,
pub driver_status: Arc<AtomicU8>,
pub config_generation: u8,
pub device_feature_select: u32,
pub driver_feature_select: u32,
@@ -141,7 +141,7 @@ impl VirtioPciCommonConfig {
) -> Self {
VirtioPciCommonConfig {
access_platform,
driver_status: state.driver_status,
driver_status: Arc::new(AtomicU8::new(state.driver_status)),
config_generation: state.config_generation,
device_feature_select: state.device_feature_select,
driver_feature_select: state.driver_feature_select,
@@ -153,7 +153,7 @@ impl VirtioPciCommonConfig {
fn state(&self) -> VirtioPciCommonConfigState {
VirtioPciCommonConfigState {
driver_status: self.driver_status,
driver_status: self.driver_status.load(Ordering::Acquire),
config_generation: self.config_generation,
device_feature_select: self.device_feature_select,
driver_feature_select: self.driver_feature_select,
@@ -223,7 +223,7 @@ impl VirtioPciCommonConfig {
debug!("read_common_config_byte: offset 0x{offset:x}");
// The driver is only allowed to do aligned, properly sized access.
match offset {
0x14 => self.driver_status,
0x14 => self.driver_status.load(Ordering::Acquire),
0x15 => self.config_generation,
_ => {
warn!("invalid virtio config byte read: 0x{offset:x}");
@@ -235,7 +235,7 @@ impl VirtioPciCommonConfig {
fn write_common_config_byte(&mut self, offset: u64, value: u8) {
debug!("write_common_config_byte: offset 0x{offset:x}");
match offset {
0x14 => self.driver_status = value,
0x14 => self.driver_status.store(value, Ordering::Release),
_ => {
warn!("invalid virtio config byte write: 0x{offset:x}");
}
@@ -404,11 +404,8 @@ impl Snapshottable for VirtioPciCommonConfig {
#[cfg(test)]
mod unit_tests {
use vm_memory::GuestMemoryAtomic;
use vmm_sys_util::eventfd::EventFd;
use super::*;
use crate::{ActivateResult, GuestMemoryMmap, VirtioInterrupt};
use crate::{ActivateResult, ActivationContext};
struct DummyDevice(u32);
const QUEUE_SIZE: u16 = 256;
@@ -421,12 +418,7 @@ mod unit_tests {
fn queue_max_sizes(&self) -> &[u16] {
QUEUE_SIZES
}
fn activate(
&mut self,
_mem: GuestMemoryAtomic<GuestMemoryMmap>,
_interrupt_evt: Arc<dyn VirtioInterrupt>,
_queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, _context: ActivationContext) -> ActivateResult {
Ok(())
}
@@ -445,7 +437,7 @@ mod unit_tests {
fn write_base_regs() {
let mut regs = VirtioPciCommonConfig {
access_platform: None,
driver_status: 0xaa,
driver_status: Arc::new(AtomicU8::new(0xaa)),
config_generation: 0x55,
device_feature_select: 0x0,
driver_feature_select: 0x0,

View File

@@ -10,7 +10,7 @@ use std::any::Any;
use std::cmp;
use std::io::Write;
use std::ops::Deref;
use std::sync::atomic::{AtomicBool, AtomicU16, AtomicUsize, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU16, AtomicUsize, Ordering};
use std::sync::{Arc, Barrier, Mutex};
use anyhow::anyhow;
@@ -288,15 +288,18 @@ pub struct VirtioPciDeviceActivator {
queues: Option<Vec<(usize, Queue, EventFd)>>,
barrier: Option<Arc<Barrier>>,
id: String,
status: Arc<AtomicU8>,
}
impl VirtioPciDeviceActivator {
pub fn activate(&mut self) -> ActivateResult {
self.device.lock().unwrap().activate(
self.memory.take().unwrap(),
self.interrupt.take().unwrap(),
self.queues.take().unwrap(),
)?;
pub fn activate(mut self) -> ActivateResult {
let mut locked_device = self.device.lock().unwrap();
locked_device.activate(crate::device::ActivationContext {
mem: self.memory.take().unwrap(),
interrupt_cb: self.interrupt.take().unwrap(),
queues: self.queues.take().unwrap(),
device_status: self.status,
})?;
self.device_activated.store(true, Ordering::SeqCst);
if let Some(barrier) = self.barrier.take() {
@@ -641,13 +644,13 @@ impl VirtioPciDevice {
fn is_driver_ready(&self) -> bool {
let ready_bits =
(DEVICE_ACKNOWLEDGE | DEVICE_DRIVER | DEVICE_DRIVER_OK | DEVICE_FEATURES_OK) as u8;
self.common_config.driver_status == ready_bits
&& self.common_config.driver_status & DEVICE_FAILED as u8 == 0
let driver_status = self.common_config.driver_status.load(Ordering::SeqCst);
driver_status == ready_bits && (driver_status & DEVICE_FAILED as u8) == 0
}
/// Determines if the driver has requested the device (re)init / reset itself
fn is_driver_init(&self) -> bool {
self.common_config.driver_status == DEVICE_INIT as u8
self.common_config.driver_status.load(Ordering::SeqCst) == DEVICE_INIT as u8
}
pub fn config_bar_addr(&self) -> u64 {
@@ -801,6 +804,7 @@ impl VirtioPciDevice {
device_activated: self.device_activated.clone(),
barrier,
id: self.id.clone(),
status: self.common_config.driver_status.clone(),
}
}
@@ -1219,7 +1223,9 @@ impl PciDevice for VirtioPciDevice {
self.common_config.queue_select = 0;
} else {
error!("Attempt to reset device when not implemented in underlying device");
self.common_config.driver_status = crate::DEVICE_FAILED as u8;
self.common_config
.driver_status
.store(crate::DEVICE_FAILED as u8, Ordering::SeqCst);
}
}

View File

@@ -428,12 +428,13 @@ impl VirtioDevice for Vdpa {
}
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
virtio_interrupt: Arc<dyn VirtioInterrupt>,
queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb: virtio_interrupt,
queues,
..
} = context;
self.activate_vdpa(&mem.memory(), virtio_interrupt.as_ref(), &queues)
.map_err(ActivateError::ActivateVdpa)?;

View File

@@ -19,7 +19,6 @@ use virtio_bindings::virtio_blk::{
VIRTIO_BLK_F_GEOMETRY, VIRTIO_BLK_F_MQ, VIRTIO_BLK_F_RO, VIRTIO_BLK_F_SEG_MAX,
VIRTIO_BLK_F_SIZE_MAX, VIRTIO_BLK_F_TOPOLOGY, VIRTIO_BLK_F_WRITE_ZEROES,
};
use virtio_queue::Queue;
use vm_memory::{ByteValued, GuestMemoryAtomic};
use vm_migration::protocol::MemoryRangeTable;
use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable};
@@ -279,12 +278,13 @@ impl VirtioDevice for Blk {
}
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
self.guest_memory = Some(mem.clone());

View File

@@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize};
use serde_with::{Bytes, serde_as};
use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures};
use vhost::vhost_user::{FrontendReqHandler, VhostUserFrontend, VhostUserFrontendReqHandler};
use virtio_queue::Queue;
use vm_device::UserspaceMapping;
use vm_memory::{ByteValued, GuestMemoryAtomic};
use vm_migration::protocol::MemoryRangeTable;
@@ -261,12 +260,13 @@ impl VirtioDevice for Fs {
self.read_config_from_slice(self.config.as_slice(), offset, data);
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
self.guest_memory = Some(mem.clone());

View File

@@ -19,7 +19,7 @@ use virtio_bindings::virtio_net::{
VIRTIO_NET_F_MAC, VIRTIO_NET_F_MRG_RXBUF, VIRTIO_NET_F_MTU,
};
use virtio_bindings::virtio_ring::VIRTIO_RING_F_EVENT_IDX;
use virtio_queue::{Queue, QueueT};
use virtio_queue::QueueT;
use vm_memory::{ByteValued, GuestMemoryAtomic};
use vm_migration::protocol::MemoryRangeTable;
use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable};
@@ -288,12 +288,13 @@ impl VirtioDevice for Net {
self.read_config_from_slice(self.config.as_slice(), offset, data);
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
mut queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
mut queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
self.guest_memory = Some(mem.clone());

View File

@@ -435,12 +435,13 @@ where
}
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
let (kill_evt, pause_evt) = self.common.dup_eventfds();
@@ -593,9 +594,12 @@ mod unit_tests {
let memory = GuestMemoryAtomic::new(ctx.mem.clone());
// Test a bad activation.
let bad_activate =
ctx.device
.activate(memory.clone(), Arc::new(NoopVirtioInterrupt {}), Vec::new());
let bad_activate = ctx.device.activate(crate::device::ActivationContext {
mem: memory.clone(),
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: Vec::new(),
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
});
match bad_activate {
Err(ActivateError::BadActivate) => (),
other => panic!("{other:?}"),
@@ -603,10 +607,10 @@ mod unit_tests {
// Test a correct activation.
ctx.device
.activate(
memory,
Arc::new(NoopVirtioInterrupt {}),
vec![
.activate(crate::device::ActivationContext {
mem: memory,
interrupt_cb: Arc::new(NoopVirtioInterrupt {}),
queues: vec![
(
0,
Queue::new(256).unwrap(),
@@ -623,7 +627,8 @@ mod unit_tests {
EventFd::new(EFD_NONBLOCK).unwrap(),
),
],
)
device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)),
})
.unwrap();
}

View File

@@ -407,9 +407,11 @@ impl VsockMuxer {
Some(EpollListener::HostSock) => {
if self.conn_map.len() == defs::MAX_CONNECTIONS {
// If we're already maxed-out on connections, we'll just accept and
// immediately discard this potentially new one.
// immediately discard this potentially new one. Dropping the returned
// `UnixStream` closes the new connection; we don't care if `accept()`
// itself failed.
warn!("vsock: connection limit reached; refusing new host connection");
self.host_sock.accept().map(|_| 0).unwrap_or(0);
let _ = self.host_sock.accept();
return;
}
self.host_sock

View File

@@ -110,6 +110,10 @@ impl MuxerKillQ {
/// This will succeed and return a connection key, only if the connection at the front of
/// the queue has expired. Otherwise, `None` is returned.
///
// `VecDeque::pop_front_if` is unstable on the project MSRV; allow the
// beta clippy lint that asks for it. `unknown_lints` is needed because
// the lint does not exist on stable clippy.
#[allow(unknown_lints, clippy::manual_pop_if)]
pub fn pop(&mut self) -> Option<ConnMapKey> {
if let Some(item) = self.q.front()
&& Instant::now() > item.kill_time

View File

@@ -326,12 +326,13 @@ impl VirtioDevice for Watchdog {
self.common.ack_features(value);
}
fn activate(
&mut self,
mem: GuestMemoryAtomic<GuestMemoryMmap>,
interrupt_cb: Arc<dyn VirtioInterrupt>,
mut queues: Vec<(usize, Queue, EventFd)>,
) -> ActivateResult {
fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult {
let crate::device::ActivationContext {
mem,
interrupt_cb,
mut queues,
..
} = context;
self.common.activate(&queues, interrupt_cb.clone())?;
let (kill_evt, pause_evt) = self.common.dup_eventfds();

View File

@@ -948,7 +948,8 @@ components:
type: boolean
default: true
image_type:
type: enum ["FixedVhd", "Qcow2", "Raw", "Vhdx"]
type: string
enum: [FixedVhd, Qcow2, Raw, Vhdx, Unknown]
NetConfig:

View File

@@ -4477,7 +4477,7 @@ impl DeviceManager {
}
pub fn activate_virtio_devices(&self) -> DeviceManagerResult<()> {
for mut activator in self.pending_activations.lock().unwrap().drain(..) {
for activator in self.pending_activations.lock().unwrap().drain(..) {
activator
.activate()
.map_err(DeviceManagerError::VirtioActivate)?;

View File

@@ -484,7 +484,7 @@ impl run_blocking::BlockingEventLoop for GdbEventLoop {
}
}
if conn.peek().map(|b| b.is_some()).unwrap_or(true) {
if conn.peek().map_or(true, |b| b.is_some()) {
let byte = conn
.read()
.map_err(run_blocking::WaitForStopReasonError::Connection)?;

View File

@@ -433,7 +433,7 @@ pub fn load_igvm(
let mut now = Instant::now();
// Sort the gpas to group them by the page type
gpas.sort_by(|a, b| a.gpa.cmp(&b.gpa));
gpas.sort_by_key(|a| a.gpa);
let gpas_grouped = gpas
.iter()

View File

@@ -735,22 +735,20 @@ impl Vmm {
for signal in signals.forever() {
match signal {
SIGTERM | SIGINT => {
if exit_evt.write(1).is_err() {
// Resetting the terminal is usually done as the VMM exits
if let Ok(lock) = original_termios_opt.lock() {
if let Some(termios) = *lock {
// SAFETY: FFI call
let _ = unsafe {
tcsetattr(stdout().lock().as_raw_fd(), TCSANOW, &termios)
};
}
} else {
warn!("Failed to lock original termios");
SIGTERM | SIGINT if exit_evt.write(1).is_err() => {
// Resetting the terminal is usually done as the VMM exits
if let Ok(lock) = original_termios_opt.lock() {
if let Some(termios) = *lock {
// SAFETY: FFI call
let _ = unsafe {
tcsetattr(stdout().lock().as_raw_fd(), TCSANOW, &termios)
};
}
std::process::exit(1);
} else {
warn!("Failed to lock original termios");
}
std::process::exit(1);
}
_ => (),
}

View File

@@ -132,33 +132,29 @@ impl SerialManager {
let in_fd = match output {
ConsoleOutput::Pty(ref fd) => fd.as_raw_fd(),
ConsoleOutput::Tty(_) => {
// If running on an interactive TTY then accept input
// SAFETY: trivially safe
if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } {
// SAFETY: STDIN_FILENO is a valid fd
let fd = unsafe { libc::dup(libc::STDIN_FILENO) };
if fd == -1 {
return Err(Error::DupFd(std::io::Error::last_os_error()));
}
// SAFETY: fd is valid and owned by us
let stdin_clone = unsafe { File::from_raw_fd(fd) };
// SAFETY: FFI calls with correct arguments
let ret = unsafe {
let mut flags = libc::fcntl(stdin_clone.as_raw_fd(), libc::F_GETFL);
flags |= libc::O_NONBLOCK;
libc::fcntl(stdin_clone.as_raw_fd(), libc::F_SETFL, flags)
};
if ret < 0 {
return Err(Error::SetNonBlocking(std::io::Error::last_os_error()));
}
output = ConsoleOutput::Tty(Arc::new(stdin_clone));
fd
} else {
return Ok(None);
// If running on an interactive TTY then accept input.
// SAFETY: trivially safe
ConsoleOutput::Tty(_) if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } => {
// SAFETY: STDIN_FILENO is a valid fd
let fd = unsafe { libc::dup(libc::STDIN_FILENO) };
if fd == -1 {
return Err(Error::DupFd(std::io::Error::last_os_error()));
}
// SAFETY: fd is valid and owned by us
let stdin_clone = unsafe { File::from_raw_fd(fd) };
// SAFETY: FFI calls with correct arguments
let ret = unsafe {
let mut flags = libc::fcntl(stdin_clone.as_raw_fd(), libc::F_GETFL);
flags |= libc::O_NONBLOCK;
libc::fcntl(stdin_clone.as_raw_fd(), libc::F_SETFL, flags)
};
if ret < 0 {
return Err(Error::SetNonBlocking(std::io::Error::last_os_error()));
}
output = ConsoleOutput::Tty(Arc::new(stdin_clone));
fd
}
ConsoleOutput::Socket(ref fd) => {
if let Some(path_in_socket) = socket {