Files
cloud-hypervisor/net_util/src/ctrl_queue.rs
Dylan Reid a6d3901f3e misc: return errors from IOMMU address translation instead of panicking
The address that is passed from the guest should be treated as
untrusted. Currently an invalid address will panic the VMM. This only
allows the guest to hurt itself, but we shouldn't have the VMM crashing.
Instead let's return an error if possible or invalidate the queue if it
happen during setup.

The data flow from guest to translate_gva/translate_gpa is:

  1. Guest writes a raw u64 address into a virtio descriptor in the
     shared descriptor table (guest memory).
  2. The virtio-queue crate reads this descriptor via read_obj() and
     returns the addr field as-is in a GuestAddress — no validation.
  3. Device code calls .translate_gva(access_platform, len) on the
     GuestAddress.
  4. With IOMMU (access_platform is Some): the address is an IOVA that
     must be translated to a GPA via the IOMMU mapping table. If the
     guest provides an unmapped IOVA, translation returns Err.
     Previously, .unwrap() here panicked the VMM.
  5. Without IOMMU (access_platform is None): translate_gva is a no-op
     (returns self). The raw address flows to GuestMemory::read_obj()
     which validates it — out-of-range addresses return
     Err(InvalidGuestAddress), so no host memory corruption is possible.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-04-14 23:25:03 +00:00

196 lines
7.4 KiB
Rust

// Copyright (c) 2021 Intel Corporation. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use log::{debug, error, info, warn};
use thiserror::Error;
use virtio_bindings::virtio_net::{
VIRTIO_NET_CTRL_ANNOUNCE, VIRTIO_NET_CTRL_ANNOUNCE_ACK, VIRTIO_NET_CTRL_GUEST_OFFLOADS,
VIRTIO_NET_CTRL_GUEST_OFFLOADS_SET, VIRTIO_NET_CTRL_MQ, VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MAX,
VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MIN, VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET, VIRTIO_NET_CTRL_RX,
VIRTIO_NET_CTRL_RX_ALLMULTI, VIRTIO_NET_CTRL_RX_ALLUNI, VIRTIO_NET_CTRL_RX_NOBCAST,
VIRTIO_NET_CTRL_RX_NOMULTI, VIRTIO_NET_CTRL_RX_NOUNI, VIRTIO_NET_CTRL_RX_PROMISC,
VIRTIO_NET_CTRL_VLAN, VIRTIO_NET_CTRL_VLAN_ADD, VIRTIO_NET_CTRL_VLAN_DEL, VIRTIO_NET_ERR,
VIRTIO_NET_OK,
};
use virtio_queue::{Queue, QueueT};
use vm_memory::{ByteValued, Bytes, GuestMemoryError};
use vm_virtio::{AccessPlatform, Translatable};
use super::virtio_features_to_tap_offload;
use crate::{GuestMemoryMmap, Tap};
#[derive(Error, Debug)]
pub enum Error {
/// Read queue failed.
#[error("Read queue failed")]
GuestMemory(#[source] GuestMemoryError),
/// No control header descriptor
#[error("No control header descriptor")]
NoControlHeaderDescriptor,
/// Missing the data descriptor in the chain.
#[error("Missing the data descriptor in the chain")]
NoDataDescriptor,
/// No status descriptor
#[error("No status descriptor")]
NoStatusDescriptor,
/// Failed adding used index
#[error("Failed adding used index")]
QueueAddUsed(#[source] virtio_queue::Error),
/// Failed creating an iterator over the queue
#[error("Failed creating an iterator over the queue")]
QueueIterator(#[source] virtio_queue::Error),
/// Failed enabling notification for the queue
#[error("Failed enabling notification for the queue")]
QueueEnableNotification(#[source] virtio_queue::Error),
}
type Result<T> = std::result::Result<T, Error>;
#[repr(C, packed)]
#[derive(Debug, Clone, Copy, Default)]
pub struct ControlHeader {
pub class: u8,
pub cmd: u8,
}
// SAFETY: ControlHeader only contains a series of integers
unsafe impl ByteValued for ControlHeader {}
fn is_tolerated_ctrl_command(ctrl_hdr: ControlHeader) -> bool {
match u32::from(ctrl_hdr.class) {
VIRTIO_NET_CTRL_RX => matches!(
u32::from(ctrl_hdr.cmd),
VIRTIO_NET_CTRL_RX_PROMISC
| VIRTIO_NET_CTRL_RX_ALLMULTI
| VIRTIO_NET_CTRL_RX_ALLUNI
| VIRTIO_NET_CTRL_RX_NOMULTI
| VIRTIO_NET_CTRL_RX_NOUNI
| VIRTIO_NET_CTRL_RX_NOBCAST
),
VIRTIO_NET_CTRL_VLAN => matches!(
u32::from(ctrl_hdr.cmd),
VIRTIO_NET_CTRL_VLAN_ADD | VIRTIO_NET_CTRL_VLAN_DEL
),
VIRTIO_NET_CTRL_ANNOUNCE => u32::from(ctrl_hdr.cmd) == VIRTIO_NET_CTRL_ANNOUNCE_ACK,
_ => false,
}
}
pub struct CtrlQueue {
pub taps: Vec<Tap>,
}
impl CtrlQueue {
pub fn new(taps: Vec<Tap>) -> Self {
CtrlQueue { taps }
}
pub fn process(
&mut self,
mem: &GuestMemoryMmap,
queue: &mut Queue,
access_platform: Option<&dyn AccessPlatform>,
) -> Result<()> {
while let Some(mut desc_chain) = queue.pop_descriptor_chain(mem) {
let ctrl_desc = desc_chain.next().ok_or(Error::NoControlHeaderDescriptor)?;
let ctrl_hdr: ControlHeader = desc_chain
.memory()
.read_obj(
ctrl_desc
.addr()
.translate_gva(access_platform, ctrl_desc.len() as usize)
.map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?,
)
.map_err(Error::GuestMemory)?;
let data_desc = desc_chain.next().ok_or(Error::NoDataDescriptor)?;
let data_desc_addr = data_desc
.addr()
.translate_gva(access_platform, data_desc.len() as usize)
.map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?;
let status_desc = desc_chain.next().ok_or(Error::NoStatusDescriptor)?;
let ok = match u32::from(ctrl_hdr.class) {
VIRTIO_NET_CTRL_MQ => {
let queue_pairs = desc_chain
.memory()
.read_obj::<u16>(data_desc_addr)
.map_err(Error::GuestMemory)?;
if u32::from(ctrl_hdr.cmd) != VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET {
warn!("Unsupported command: {}", ctrl_hdr.cmd);
false
} else if (queue_pairs < VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MIN as u16)
|| (queue_pairs > VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MAX as u16)
{
warn!("Number of MQ pairs out of range: {queue_pairs}");
false
} else {
info!("Number of MQ pairs requested: {queue_pairs}");
true
}
}
VIRTIO_NET_CTRL_GUEST_OFFLOADS => {
let features = desc_chain
.memory()
.read_obj::<u64>(data_desc_addr)
.map_err(Error::GuestMemory)?;
if u32::from(ctrl_hdr.cmd) == VIRTIO_NET_CTRL_GUEST_OFFLOADS_SET {
let mut ok = true;
for tap in self.taps.iter_mut() {
info!("Reprogramming tap offload with features: {features}");
tap.set_offload(virtio_features_to_tap_offload(features))
.map_err(|e| {
error!("Error programming tap offload: {e:?}");
ok = false;
})
.ok();
}
ok
} else {
warn!("Unsupported command: {}", ctrl_hdr.cmd);
false
}
}
_ if is_tolerated_ctrl_command(ctrl_hdr) => {
debug!("Ignoring unsupported but tolerated control command {ctrl_hdr:?}");
true
}
_ => {
warn!("Unsupported command {ctrl_hdr:?}");
false
}
};
desc_chain
.memory()
.write_obj(
if ok { VIRTIO_NET_OK } else { VIRTIO_NET_ERR } as u8,
status_desc
.addr()
.translate_gva(access_platform, status_desc.len() as usize)
.map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?,
)
.map_err(Error::GuestMemory)?;
// Per virtio spec 2.6.8, used_len is the number of bytes written
// to device-writable descriptors. Only the status byte is written.
let len = status_desc.len();
queue
.add_used(desc_chain.memory(), desc_chain.head_index(), len)
.map_err(Error::QueueAddUsed)?;
if !queue
.enable_notification(mem)
.map_err(Error::QueueEnableNotification)?
{
break;
}
}
Ok(())
}
}