Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/libs/kata-types/src/config/hypervisor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,7 @@ impl TopologyConfigInfo {
HYPERVISOR_NAME_CH,
HYPERVISOR_NAME_DRAGONBALL,
HYPERVISOR_NAME_FIRECRACKER,
HYPERVISOR_NAME_OPENVMM,
HYPERVISOR_NAME_REMOTE,
];
let hypervisor_name = toml_config.runtime.hypervisor_name.as_str();
Expand Down
13 changes: 12 additions & 1 deletion src/runtime-rs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,18 @@ ifeq ($(USE_OPENVMM),true)
KERNELTYPE_OPENVMM = uncompressed
KERNEL_NAME_OPENVMM = $(call MAKE_KERNEL_NAME,$(KERNELTYPE_OPENVMM))
KERNELPATH_OPENVMM = $(KERNELDIR)/$(KERNEL_NAME_OPENVMM)
DEFSANDBOXCGROUPONLY_OPENVMM := true
# OpenVMM runs in-process inside the runtime-rs shim, so guest RAM is
# shmem owned by the shim PID. When sandbox_cgroup_only=true the shim
# lives in the pod cgroup, capped by container.limits.memory, and the
# kernel OOM-killer fires during VM construction for any non-trivial
# guest RAM size ("misc mshv: Failed to populate memory region: -12",
# surfaced as "ttrpc: closed" / FailedCreatePodSandBox). Default to
# false so the shim+VMM land in the unconstrained kata-overhead
# cgroup; container processes still get sized correctly because they
# are placed in the pod cgroup at the usual time. Operators that
# want the legacy single-cgroup behaviour can still flip this back
# in configuration-openvmm.toml.
DEFSANDBOXCGROUPONLY_OPENVMM := false
DEFSTATICRESOURCEMGMT_OPENVMM := false
DEFSHAREDFS_OPENVMM := virtio-fs
RUNTIMENAME := virt_container
Expand Down
2 changes: 1 addition & 1 deletion src/runtime-rs/crates/hypervisor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ protocols = { workspace = true, features = ["async"] }
persist = { workspace = true }
ch-config = { workspace = true, optional = true }
tests_utils = { workspace = true }
tracing-subscriber = { version = "0.3", optional = true }
tracing-subscriber = { version = "0.3", optional = true, features = ["env-filter"] }

# Dependencies that only exist to support hypervisors which upstream only
# build on x86_64 and aarch64 (dragonball, firecracker). Keeping them in a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
mod core;
mod device;

pub use core::{discover_vfio_group_device, VfioDevice};
pub use core::{discover_vfio_group_device, DeviceAddress, VfioDevice};
pub use device::VfioDeviceBase;
pub use device::VfioDeviceModern;
pub use device::VfioDeviceModernHandle;
Expand Down
198 changes: 169 additions & 29 deletions src/runtime-rs/crates/hypervisor/src/device/pci_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,130 @@ use anyhow::{anyhow, Context, Result};
const PCI_SLOT_BITS: u32 = 5;
const MAX_PCI_SLOTS: u32 = (1 << PCI_SLOT_BITS) - 1;

// A PciSlot describes where a PCI device sits on a single bus
// The PCI spec reserves 3 bits for the function number (0..7), allowing
// up to 8 logical functions per PCI device slot. Most devices kata
// talks to are single-function (function 0), and the on-the-wire format
// omits the function in that case to stay backward-compatible (see the
// Display impl below).
const PCI_FUNCTION_BITS: u32 = 3;
const MAX_PCI_FUNCTIONS: u32 = (1 << PCI_FUNCTION_BITS) - 1;

// A PciSlot describes where one logical PCI function sits on a single
// bus: a device number (5 bits, 0..0x1f) plus a function number
// (3 bits, 0..7).
//
// This encapsulates the PCI slot number a.k.a device number, which is
// limited to a 5 bit value [0x00..0x1f] by the PCI specification
// Wire format: a single-function slot serialises as "ss" (e.g. "1f")
// to keep the historical slot-only form intact; a multi-function slot
// serialises as "ss.f" (e.g. "03.5"). Kata-agent's
// `pci::Path::from_str` accepts both ("00" is parsed as function 0).
//
// To support multifunction device's, It's needed to extend
// this to include the PCI 3-bit function number as well.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PciSlot(pub u8);
// Multi-function packing is needed by the OpenVMM backend, where the
// PCIe root complex packs up to 8 root ports per device slot
// (see `GenericPcieRootComplex::new` in microsoft/openvmm:
// `vm/devices/pci/pcie/src/root.rs`).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PciSlot {
pub device: u8,
pub function: u8,
}

impl PciSlot {
pub fn new(v: u8) -> PciSlot {
PciSlot(v)
/// Construct a single-function PciSlot at the given device number.
pub fn new(device: u8) -> PciSlot {
PciSlot {
device,
function: 0,
}
}

/// Construct a multi-function PciSlot. Used by the openvmm backend
/// when packing many root ports into one device slot.
pub fn with_function(device: u8, function: u8) -> Result<PciSlot> {
if device as u32 > MAX_PCI_SLOTS {
return Err(anyhow!(
"PCI device number {} exceeds MAX: {}",
device,
MAX_PCI_SLOTS
));
}
if function as u32 > MAX_PCI_FUNCTIONS {
return Err(anyhow!(
"PCI function number {} exceeds MAX: {}",
function,
MAX_PCI_FUNCTIONS
));
}
Ok(PciSlot { device, function })
}
}

impl std::fmt::Display for PciSlot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:02x}", self.0)
// Function 0 is the historical default; omit it so the wire
// format stays compatible with single-function callers and with
// kata-agent's `SlotFn::from_str("xx")` shorthand.
if self.function == 0 {
write!(f, "{:02x}", self.device)
} else {
write!(f, "{:02x}.{:01x}", self.device, self.function)
}
}
}

impl TryFrom<&str> for PciSlot {
type Error = anyhow::Error;

fn try_from(s: &str) -> Result<PciSlot> {
if s.is_empty() || s.len() > 2 {
if s.is_empty() {
return Err(anyhow!("string given is invalid."));
}

// Accept both "ss" (function defaults to 0) and "ss.f".
let mut tokens = s.splitn(2, '.');
let dev_str = tokens.next().unwrap();
let func_str = tokens.next();

if dev_str.is_empty() || dev_str.len() > 2 {
return Err(anyhow!("string given is invalid."));
}
if let Some(f) = func_str {
if f.is_empty() || f.len() > 1 {
return Err(anyhow!("function token '{}' is invalid", f));
}
}

let base = 16;
let n = u64::from_str_radix(s, base).context(format!(
"convert string to number with base {base:?} failed."
let device = u8::from_str_radix(dev_str, base).context(format!(
"convert device string '{}' to number with base {} failed.",
dev_str, base
))?;
if n >> PCI_SLOT_BITS > 0 {
if (device as u32) > MAX_PCI_SLOTS {
return Err(anyhow!(
"number {:?} exceeds MAX:{:?}, failed.",
n,
"PCI device number {} exceeds MAX: {}",
device,
MAX_PCI_SLOTS
));
}

Ok(PciSlot(n as u8))
let function = match func_str {
Some(f) => {
let n = u8::from_str_radix(f, base).context(format!(
"convert function string '{}' to number with base {} failed.",
f, base
))?;
if (n as u32) > MAX_PCI_FUNCTIONS {
return Err(anyhow!(
"PCI function number {} exceeds MAX: {}",
n,
MAX_PCI_FUNCTIONS
));
}
n
}
None => 0,
};

Ok(PciSlot { device, function })
}
}

Expand All @@ -71,7 +152,10 @@ impl TryFrom<u32> for PciSlot {
return Err(anyhow!("value {:?} exceeds MAX: {:?}", v, MAX_PCI_SLOTS));
}

Ok(PciSlot(v as u8))
Ok(PciSlot {
device: v as u8,
function: 0,
})
}
}

Expand Down Expand Up @@ -117,12 +201,14 @@ impl PciPath {

impl std::fmt::Display for PciPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Delegate to PciSlot::Display so that multi-function slots
// serialise as "ss.f" while single-function slots stay "ss".
write!(
f,
"{}",
self.slots
.iter()
.map(|pci_slot| format!("{:02x}", pci_slot.0))
.map(|pci_slot| pci_slot.to_string())
.collect::<Vec<String>>()
.join("/")
)
Expand Down Expand Up @@ -182,41 +268,95 @@ mod tests {
// valid number
let pci_slot_04 = PciSlot::try_from(1_u32);
assert!(pci_slot_04.is_ok());
assert_eq!(pci_slot_04.as_ref().unwrap().0, 1_u8);
assert_eq!(pci_slot_04.as_ref().unwrap().device, 1_u8);
assert_eq!(pci_slot_04.as_ref().unwrap().function, 0_u8);
let pci_slot_str = pci_slot_04.as_ref().unwrap().to_string();
assert_eq!(pci_slot_str, format!("{:02x}", pci_slot_04.unwrap().0));
assert_eq!(
pci_slot_str,
format!("{:02x}", pci_slot_04.unwrap().device)
);

// max number
let pci_slot_05 = PciSlot::try_from(31_u32);
assert!(pci_slot_05.is_ok());
assert_eq!(pci_slot_05.unwrap().0, 31_u8);
assert_eq!(pci_slot_05.unwrap().device, 31_u8);

// exceed and error
let pci_slot_06 = PciSlot::try_from(32_u32);
assert!(pci_slot_06.is_err());
}

#[test]
fn test_pci_slot_multifunction() {
// Multi-function constructor accepts function 0..7.
let slot = PciSlot::with_function(0x03, 5).unwrap();
assert_eq!(slot.device, 0x03);
assert_eq!(slot.function, 5);

// Wire format: non-zero function emits "ss.f".
assert_eq!(slot.to_string(), "03.5");

// Wire format: function 0 stays in the historical short form.
assert_eq!(PciSlot::new(0x03).to_string(), "03");
assert_eq!(PciSlot::with_function(0x03, 0).unwrap().to_string(), "03");

// Parser accepts both forms and round-trips them.
assert_eq!(PciSlot::try_from("03.5").unwrap(), slot);
assert_eq!(PciSlot::try_from("03").unwrap(), PciSlot::new(0x03));

// Function > 7 is rejected by both the constructor and the parser.
assert!(PciSlot::with_function(0x03, 8).is_err());
assert!(PciSlot::try_from("03.8").is_err());

// Device > 0x1f is rejected by both the constructor and the parser.
assert!(PciSlot::with_function(0x20, 0).is_err());
assert!(PciSlot::try_from("20.0").is_err());

// Malformed forms are rejected.
assert!(PciSlot::try_from("03.").is_err());
assert!(PciSlot::try_from("03.55").is_err());
assert!(PciSlot::try_from(".5").is_err());
}

#[test]
fn test_pci_patch() {
let pci_path_0 = PciPath::try_from("01/0a/05");
assert!(pci_path_0.is_ok());
let pci_path_unwrap = pci_path_0.unwrap();
assert_eq!(pci_path_unwrap.slots[0].0, 1);
assert_eq!(pci_path_unwrap.slots[1].0, 10);
assert_eq!(pci_path_unwrap.slots[2].0, 5);

let pci_path_01 = PciPath::new(vec![PciSlot(1), PciSlot(10), PciSlot(5)]);
assert_eq!(pci_path_unwrap.slots[0].device, 1);
assert_eq!(pci_path_unwrap.slots[1].device, 10);
assert_eq!(pci_path_unwrap.slots[2].device, 5);

let pci_path_01 = PciPath::new(vec![
PciSlot::new(1),
PciSlot::new(10),
PciSlot::new(5),
]);
assert!(pci_path_01.is_some());
let pci_path = pci_path_01.unwrap();
let pci_path_02 = pci_path.to_string();
assert_eq!(pci_path_02, "01/0a/05".to_string());

let dev_slot = pci_path.get_device_slot();
assert!(dev_slot.is_some());
assert_eq!(dev_slot.unwrap().0, 5);
assert_eq!(dev_slot.unwrap().device, 5);

let root_slot = pci_path.get_root_slot();
assert!(root_slot.is_some());
assert_eq!(root_slot.unwrap().0, 1);
assert_eq!(root_slot.unwrap().device, 1);
}

#[test]
fn test_pci_path_multifunction_roundtrip() {
// PciPath with a multi-function root slot (e.g. OpenVMM root
// port packed at device 0x03, function 5) round-trips through
// Display and TryFrom<&str>.
let mf_root = PciSlot::with_function(0x03, 5).unwrap();
let endpoint = PciSlot::new(0);
let path = PciPath::new(vec![mf_root, endpoint]).unwrap();
assert_eq!(path.to_string(), "03.5/00");

let parsed = PciPath::try_from("03.5/00").unwrap();
assert_eq!(parsed, path);
}
}
2 changes: 1 addition & 1 deletion src/runtime-rs/crates/hypervisor/src/device/topology.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ impl PCIeTopology {
pub fn insert_device(&mut self, ep: &mut PCIeEndpoint) -> Option<PciPath> {
let to_pcipath = |v: u32| -> PciPath {
PciPath {
slots: vec![PciSlot(v as u8)],
slots: vec![PciSlot::new(v as u8)],
}
};

Expand Down
14 changes: 9 additions & 5 deletions src/runtime-rs/crates/hypervisor/src/openvmm/inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ use tokio::sync::mpsc;

use super::vmm_instance::VmmInstance;
use super::{
OPENVMM_BLOCK_HOTPLUG_PORT_COUNT, OPENVMM_BLOCK_HOTPLUG_PORT_PREFIX,
openvmm_port_pci_path, OPENVMM_BLOCK_HOTPLUG_PORT_COUNT, OPENVMM_BLOCK_HOTPLUG_PORT_PREFIX,
OPENVMM_STATIC_PCI_PORT_COUNT,
};
use crate::device::pci_path::{PciPath, PciSlot};
use crate::device::pci_path::PciPath;

#[derive(Clone, Debug)]
pub(crate) struct OpenVmmHotplugPort {
Expand All @@ -30,9 +30,13 @@ pub(crate) struct OpenVmmHotplugPort {

impl OpenVmmHotplugPort {
fn new(index: u8) -> Self {
let root_slot = OPENVMM_STATIC_PCI_PORT_COUNT + index;
let pci_path = PciPath::new(vec![PciSlot::new(root_slot), PciSlot::new(0)])
.expect("openvmm hotplug port PCI path must be non-empty");
// OpenVMM packs root ports into multi-function device slots:
// port index `STATIC + index` lands at `device = (..)/8,
// function = (..)%8` of bus 0. See
// `openvmm_port_pci_path` for the rationale.
let port_index = OPENVMM_STATIC_PCI_PORT_COUNT + index;
let pci_path = openvmm_port_pci_path(port_index)
.expect("openvmm hotplug port PCI path must be valid");

Self {
name: format!("{}{}", OPENVMM_BLOCK_HOTPLUG_PORT_PREFIX, index),
Expand Down
Loading
Loading