use alloc::{
format,
string::{String, ToString},
vec,
vec::Vec,
};
use ax_errno::{AxResult, ax_err_type};
use axvmconfig::{
AxVMCrateConfig, PassThroughDeviceConfig, ReservedAddressConfig, VmMemConfig, VmMemMappingType,
};
use fdt_edit::{Fdt, Node, NodeType, PciRange, PciSpace};
#[cfg(target_arch = "aarch64")]
use crate::boot::fdt::create::update_cpu_node;
use crate::{MappingFlags, config::AxVMConfig};
const PAGE_SIZE_4K: usize = 0x1000;
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
pub fn try_get_host_fdt() -> Option<&'static [u8]> {
let bootarg: usize = crate::host_fdt_bootarg();
if bootarg == 0 {
warn!("Boot argument does not contain a host FDT pointer");
return None;
}
let fdt_vaddr = crate::host_phys_to_virt(bootarg.into());
super::tree::host_fdt_bytes_from_ptr(fdt_vaddr.as_ptr()).inspect(|bytes| {
trace!("Host FDT size: 0x{:x}", bytes.len());
})
}
pub fn setup_guest_fdt_from_vmm(
fdt_bytes: &[u8],
vm_cfg: &mut AxVMConfig,
crate_config: &AxVMCrateConfig,
) -> AxResult<Vec<u8>> {
let fdt = Fdt::from_bytes(fdt_bytes)
.map_err(|e| ax_err_type!(InvalidData, format!("Failed to parse host FDT: {e:#?}")))?;
reserve_excluded_device_ranges(vm_cfg, crate_config, fdt_bytes)?;
let passthrough_device_names = super::device::find_all_passthrough_devices(vm_cfg, &fdt);
super::create::create_guest_fdt(&fdt, &passthrough_device_names, crate_config)
}
fn is_reserved_memory_path(node_path: &str) -> bool {
node_path == "/reserved-memory" || node_path.starts_with("/reserved-memory/")
}
fn overlaps_memory_region(lhs_gpa: usize, lhs_size: usize, rhs: &VmMemConfig) -> bool {
let lhs_end = lhs_gpa.saturating_add(lhs_size);
let rhs_end = rhs.gpa.saturating_add(rhs.size);
lhs_gpa < rhs_end && rhs.gpa < lhs_end
}
fn align_down_4k(value: usize) -> usize {
value & !(PAGE_SIZE_4K - 1)
}
fn align_up_4k(value: usize) -> usize {
value
.saturating_add(PAGE_SIZE_4K - 1)
.checked_div(PAGE_SIZE_4K)
.unwrap_or(usize::MAX / PAGE_SIZE_4K)
.saturating_mul(PAGE_SIZE_4K)
}
fn align_reserved_region_4k(gpa: usize, size: usize) -> Option<(usize, usize)> {
if size == 0 {
return None;
}
let aligned_gpa = align_down_4k(gpa);
let end = gpa.saturating_add(size);
let aligned_end = align_up_4k(end);
let aligned_size = aligned_end.saturating_sub(aligned_gpa);
(aligned_size > 0).then_some((aligned_gpa, aligned_size))
}
fn subtract_memory_region_overlap(
start: usize,
size: usize,
existing_regions: &[VmMemConfig],
) -> Vec<(usize, usize)> {
let mut remaining = vec![(start, start.saturating_add(size))];
let mut overlaps = existing_regions.to_vec();
overlaps.sort_by_key(|region| region.gpa);
for region in overlaps {
let overlap_start = region.gpa;
let overlap_end = region.gpa.saturating_add(region.size);
let mut next_remaining = Vec::new();
for (seg_start, seg_end) in remaining {
if overlap_end <= seg_start || overlap_start >= seg_end {
next_remaining.push((seg_start, seg_end));
continue;
}
if seg_start < overlap_start {
next_remaining.push((seg_start, overlap_start.min(seg_end)));
}
if overlap_end < seg_end {
next_remaining.push((overlap_end.max(seg_start), seg_end));
}
}
remaining = next_remaining;
if remaining.is_empty() {
break;
}
}
remaining
.into_iter()
.filter_map(|(seg_start, seg_end)| {
let seg_size = seg_end.saturating_sub(seg_start);
(seg_size > 0).then_some((seg_start, seg_size))
})
.collect()
}
fn reserved_memory_regions(crate_cfg: &AxVMCrateConfig) -> impl Iterator<Item = &VmMemConfig> {
crate_cfg
.kernel
.memory_regions
.iter()
.filter(|region| region.map_type == VmMemMappingType::MapReserved)
}
fn excluded_device_paths(crate_cfg: &AxVMCrateConfig) -> Vec<String> {
crate_cfg
.devices
.excluded_devices
.iter()
.flatten()
.cloned()
.collect()
}
fn is_excluded_node_path(node_path: &str, excluded_paths: &[String]) -> bool {
excluded_paths.iter().any(|excluded| {
node_path == excluded
|| node_path
.strip_prefix(excluded)
.is_some_and(|suffix| suffix.starts_with('/'))
})
}
fn push_reserved_address_range(
ranges: &mut Vec<ReservedAddressConfig>,
node_path: &str,
base: usize,
size: usize,
) {
let Some((base_gpa, length)) = align_reserved_region_4k(base, size) else {
return;
};
let mut merged = ReservedAddressConfig { base_gpa, length };
let mut index = 0;
while index < ranges.len() {
let existing = &ranges[index];
let merged_end = merged.base_gpa.saturating_add(merged.length);
let existing_end = existing.base_gpa.saturating_add(existing.length);
if merged.base_gpa <= existing_end && existing.base_gpa <= merged_end {
let merged_base = merged.base_gpa.min(existing.base_gpa);
let merged_end = merged_end.max(existing_end);
merged = ReservedAddressConfig {
base_gpa: merged_base,
length: merged_end.saturating_sub(merged_base),
};
ranges.remove(index);
} else {
index += 1;
}
}
debug!(
"Reserving excluded device {} range [{:#x}~{:#x}] from passthrough mapping",
node_path,
merged.base_gpa,
merged.base_gpa.saturating_add(merged.length)
);
ranges.push(merged);
}
fn node_regs(fdt: &Fdt, node_id: usize) -> Vec<fdt_edit::RegFixed> {
fdt.view_typed(node_id)
.map(|node| node.regs())
.unwrap_or_default()
}
fn node_pci_ranges(fdt: &Fdt, node_id: usize) -> Vec<PciRange> {
match fdt.view_typed(node_id) {
Some(NodeType::Pci(pci)) => pci.ranges().unwrap_or_default(),
_ => Vec::new(),
}
}
pub fn reserve_excluded_device_ranges(
vm_cfg: &mut AxVMConfig,
crate_cfg: &AxVMCrateConfig,
dtb: &[u8],
) -> AxResult {
let excluded_paths = excluded_device_paths(crate_cfg);
if excluded_paths.is_empty() {
return Ok(());
}
let fdt = Fdt::from_bytes(dtb).map_err(|e| {
ax_err_type!(
InvalidData,
format!("Failed to parse DTB image while reading excluded devices: {e:#?}")
)
})?;
let mut reserved_ranges = Vec::new();
for node_id in fdt.iter_node_ids() {
let node_path = fdt.path_of(node_id);
if !is_excluded_node_path(&node_path, &excluded_paths) {
continue;
}
for reg in node_regs(&fdt, node_id) {
push_reserved_address_range(
&mut reserved_ranges,
&node_path,
reg.address as usize,
reg.size.unwrap_or(0) as usize,
);
}
for range in node_pci_ranges(&fdt, node_id) {
push_reserved_address_range(
&mut reserved_ranges,
&node_path,
range.cpu_address as usize,
range.size as usize,
);
}
}
reserved_ranges.sort_by_key(|range| range.base_gpa);
for range in reserved_ranges {
vm_cfg.add_reserved_address_range(range);
}
Ok(())
}
fn is_memory_like_compatible(node: &Node) -> bool {
node.compatibles().any(|compat| {
compat == "mmio-sram"
|| compat.contains("shared-memory")
|| compat.contains("shmem")
|| compat.contains("sram")
})
}
fn is_partition_like_node(node: &Node, node_path: &str) -> bool {
node.compatibles()
.any(|compat| compat == "fixed-partitions")
|| node_path.contains("/partitions/")
}
fn should_skip_passthrough_node(
fdt: &Fdt,
node_id: usize,
node: &Node,
node_path: &str,
reserved_regions: &[VmMemConfig],
) -> bool {
if !is_memory_like_compatible(node) {
return false;
}
for reg in node_regs(fdt, node_id) {
let gpa = reg.address as usize;
let size = reg.size.unwrap_or(0) as usize;
if size == 0 {
continue;
}
if let Some(region) = reserved_regions
.iter()
.find(|region| overlaps_memory_region(gpa, size, region))
{
debug!(
"Skipping passthrough node {} [{:#x}~{:#x}] because memory-like compatible \
overlaps reserved region [{:#x}~{:#x}]",
node_path,
gpa,
gpa + size,
region.gpa,
region.gpa + region.size
);
return true;
}
}
false
}
pub fn parse_reserved_memory_regions(crate_cfg: &mut AxVMCrateConfig, dtb: &[u8]) -> AxResult {
let fdt = Fdt::from_bytes(dtb).map_err(|e| {
ax_err_type!(
InvalidData,
format!("Failed to parse DTB image while reading reserved memory: {e:#?}")
)
})?;
let default_flags = (MappingFlags::READ | MappingFlags::WRITE | MappingFlags::EXECUTE).bits();
let mut added_count = 0usize;
for node_id in fdt.iter_node_ids() {
let node_path = fdt.path_of(node_id);
if !is_reserved_memory_path(&node_path) {
continue;
}
for reg in node_regs(&fdt, node_id) {
let original_gpa = reg.address as usize;
let original_size = reg.size.unwrap_or(0) as usize;
let Some((gpa, size)) = align_reserved_region_4k(original_gpa, original_size) else {
continue;
};
let remaining_segments =
subtract_memory_region_overlap(gpa, size, &crate_cfg.kernel.memory_regions);
for (seg_gpa, seg_size) in remaining_segments {
crate_cfg.kernel.memory_regions.push(VmMemConfig {
gpa: seg_gpa,
size: seg_size,
flags: default_flags,
map_type: VmMemMappingType::MapReserved,
});
added_count += 1;
}
}
}
if added_count > 0 {
debug!(
"Added {} reserved-memory region(s) from DTB into VM kernel memory_regions",
added_count
);
}
Ok(())
}
pub fn set_phys_cpu_sets(
vm_cfg: &mut AxVMConfig,
fdt: &Fdt,
crate_config: &AxVMCrateConfig,
) -> AxResult {
let phys_cpu_ids = crate_config
.base
.phys_cpu_ids
.as_ref()
.ok_or_else(|| ax_err_type!(InvalidInput, "phys_cpu_ids is missing"))?;
let cpu_nodes_info: Vec<_> = fdt
.iter_node_ids()
.filter_map(|node_id| {
let path = fdt.path_of(node_id);
let node_id_from_path = path
.strip_prefix("/cpus/cpu@")
.and_then(|id| id.split('/').next())
.and_then(|id| usize::from_str_radix(id, 16).ok())?;
let guest_cpu_id = node_regs(fdt, node_id).first()?.address as usize;
info!(
"CPU node: {}, node_id: 0x{:x}, guest_cpu_id: 0x{:x}",
path, node_id_from_path, guest_cpu_id
);
Some((node_id_from_path, guest_cpu_id))
})
.collect();
info!("Found {} host CPU nodes", cpu_nodes_info.len());
let mut new_phys_cpu_sets = Vec::new();
let mut guest_phys_cpu_ids = Vec::new();
for phys_cpu_id in phys_cpu_ids {
if let Some((cpu_index, (_, guest_cpu_id))) = cpu_nodes_info
.iter()
.enumerate()
.find(|(_, (node_id, _))| node_id == phys_cpu_id)
{
let cpu_mask = 1usize << cpu_index;
new_phys_cpu_sets.push(cpu_mask);
guest_phys_cpu_ids.push(*guest_cpu_id);
} else {
error!(
"vCPU {} with phys_cpu_id 0x{:x} not found in device tree!",
vm_cfg.id(),
phys_cpu_id
);
}
}
let phys_cpu_ls = vm_cfg.phys_cpu_ls_mut();
phys_cpu_ls.set_guest_cpu_sets(new_phys_cpu_sets);
phys_cpu_ls.set_guest_phys_cpu_ids(guest_phys_cpu_ids);
Ok(())
}
fn add_device_address_config(
vm_cfg: &mut AxVMConfig,
node_name: &str,
base_address: usize,
size: usize,
index: usize,
prefix: Option<&str>,
) {
if size == 0 {
return;
}
let addr_end = base_address.saturating_add(size);
if let Some(emu_dev) = vm_cfg.emu_devices().iter().find(|emu_dev| {
let emu_start = emu_dev.base_gpa;
let emu_end = emu_dev.base_gpa.saturating_add(emu_dev.length);
base_address < emu_end && emu_start < addr_end
}) {
debug!(
"Skipping passthrough mapping for node {} [{:#x}~{:#x}] because it overlaps emulated \
device {} [{:#x}~{:#x}]",
node_name,
base_address,
addr_end,
emu_dev.name,
emu_dev.base_gpa,
emu_dev.base_gpa.saturating_add(emu_dev.length),
);
return;
}
let device_name = if index == 0 {
match prefix {
Some(p) => format!("{node_name}-{p}"),
None => node_name.to_string(),
}
} else {
match prefix {
Some(p) => format!("{node_name}-{p}-region{index}"),
None => format!("{node_name}-region{index}"),
}
};
vm_cfg.add_pass_through_device(PassThroughDeviceConfig {
name: device_name,
base_gpa: base_address,
base_hpa: base_address,
length: size,
irq_id: 0,
});
}
fn add_pci_ranges_config(vm_cfg: &mut AxVMConfig, node_name: &str, range: &PciRange, index: usize) {
let base_address = range.cpu_address as usize;
let size = range.size as usize;
if size == 0 {
return;
}
let prefix = match range.space {
PciSpace::IO => "io",
PciSpace::Memory32 => "mem32",
PciSpace::Memory64 => "mem64",
};
let device_name = if index == 0 {
format!("{node_name}-{prefix}")
} else {
format!("{node_name}-{prefix}-region{index}")
};
vm_cfg.add_pass_through_device(PassThroughDeviceConfig {
name: device_name,
base_gpa: base_address,
base_hpa: base_address,
length: size,
irq_id: 0,
});
}
pub fn parse_passthrough_devices_address(
vm_cfg: &mut AxVMConfig,
crate_cfg: &AxVMCrateConfig,
dtb: &[u8],
) -> AxResult {
let devices = vm_cfg.pass_through_devices().to_vec();
if !devices.is_empty() && devices[0].length != 0 {
for (index, device) in devices.iter().enumerate() {
add_device_address_config(
vm_cfg,
&device.name,
device.base_gpa,
device.length,
index,
None,
);
}
return Ok(());
}
let fdt = Fdt::from_bytes(dtb).map_err(|e| {
ax_err_type!(
InvalidData,
format!("Failed to parse DTB image while reading passthrough devices: {e:#?}")
)
})?;
vm_cfg.clear_pass_through_devices();
let reserved_regions: Vec<VmMemConfig> = reserved_memory_regions(crate_cfg).cloned().collect();
for node_id in fdt.iter_node_ids() {
let Some(node) = fdt.node(node_id) else {
continue;
};
let node_path = fdt.path_of(node_id);
if node_path == "/"
|| node.name().starts_with("memory")
|| is_reserved_memory_path(&node_path)
{
continue;
}
if is_partition_like_node(node, &node_path)
|| should_skip_passthrough_node(&fdt, node_id, node, &node_path, &reserved_regions)
{
continue;
}
let node_name = node.name().to_string();
if node_name.starts_with("pcie@") || node_name.contains("pci") {
for (index, range) in node_pci_ranges(&fdt, node_id).iter().enumerate() {
add_pci_ranges_config(vm_cfg, &node_name, range, index);
}
for (index, reg) in node_regs(&fdt, node_id).iter().enumerate() {
add_device_address_config(
vm_cfg,
&node_name,
reg.address as usize,
reg.size.unwrap_or(0) as usize,
index,
Some("ecam"),
);
}
} else {
for (index, reg) in node_regs(&fdt, node_id).iter().enumerate() {
add_device_address_config(
vm_cfg,
&node_name,
reg.address as usize,
reg.size.unwrap_or(0) as usize,
index,
None,
);
}
}
}
Ok(())
}
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
pub fn parse_vm_interrupt(vm_cfg: &mut AxVMConfig, dtb: &[u8]) -> AxResult {
let fdt = Fdt::from_bytes(dtb).map_err(|e| {
ax_err_type!(
InvalidData,
format!("Failed to parse DTB image while reading interrupts: {e:#?}")
)
})?;
for node_id in fdt.iter_node_ids() {
let Some(node) = fdt.node(node_id) else {
continue;
};
let name = node.name();
if name.starts_with("memory")
|| name.starts_with("interrupt-controller")
|| name.starts_with("intc")
|| name.starts_with("its")
{
continue;
}
let Some(view) = fdt.view_typed(node_id) else {
continue;
};
for interrupt in view.interrupts() {
if let Some(irq) = passthrough_irq_from_interrupt_specifier(&interrupt.specifier) {
trace!("node: {name}, passthrough interrupt id: 0x{irq:x}");
vm_cfg.add_pass_through_irq(irq);
}
}
}
Ok(())
}
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
fn passthrough_irq_from_interrupt_specifier(specifier: &[u32]) -> Option<u32> {
#[cfg(target_arch = "aarch64")]
{
aarch64_gic_spi_from_interrupt_specifier(specifier)
}
#[cfg(target_arch = "riscv64")]
{
riscv_plic_source_from_interrupt_specifier(specifier)
}
}
#[cfg(target_arch = "aarch64")]
fn aarch64_gic_spi_from_interrupt_specifier(specifier: &[u32]) -> Option<u32> {
(specifier.first().copied() == Some(0))
.then(|| specifier.get(1).copied())
.flatten()
}
#[cfg(any(target_arch = "riscv64", test))]
fn riscv_plic_source_from_interrupt_specifier(specifier: &[u32]) -> Option<u32> {
specifier.first().copied().filter(|source| *source != 0)
}
#[cfg(target_arch = "riscv64")]
pub fn update_provided_fdt(
provided_dtb: &[u8],
_host_dtb: Option<&[u8]>,
_crate_config: &AxVMCrateConfig,
) -> AxResult<Vec<u8>> {
Ok(provided_dtb.to_vec())
}
#[cfg(target_arch = "aarch64")]
pub fn update_provided_fdt(
provided_dtb: &[u8],
host_dtb: Option<&[u8]>,
crate_config: &AxVMCrateConfig,
) -> AxResult<Vec<u8>> {
let provided_fdt = Fdt::from_bytes(provided_dtb).map_err(|e| {
ax_err_type!(
InvalidData,
format!("Failed to parse provided DTB image: {e:#?}")
)
})?;
let host_fdt = host_dtb.map(Fdt::from_bytes).transpose().map_err(|e| {
ax_err_type!(
InvalidData,
format!("Failed to parse host DTB image: {e:#?}")
)
})?;
update_cpu_node(&provided_fdt, host_fdt.as_ref(), crate_config)
}
#[cfg(test)]
mod tests {
use alloc::{string::ToString, vec, vec::Vec};
use axvm_types::{AddressSpacePolicy, VmMemConfig, VmMemMappingType};
use axvmconfig::{AxVMCrateConfig, VMDevicesConfig};
use fdt_edit::{Fdt, Node};
use fdt_raw::RegInfo;
use super::{align_reserved_region_4k, reserve_excluded_device_ranges};
use crate::config::{AxVMConfig, AxVMConfigParams, PhysCpuList};
fn prop_u32(name: &str, value: u32) -> fdt_edit::Property {
let mut prop = fdt_edit::Property::new(name, alloc::vec![]);
prop.set_u32_ls(&[value]);
prop
}
fn fdt_with_excluded_devices() -> Vec<u8> {
let mut fdt = Fdt::new();
let root = fdt.root_id();
fdt.node_mut(root)
.unwrap()
.set_property(prop_u32("#address-cells", 2));
fdt.node_mut(root)
.unwrap()
.set_property(prop_u32("#size-cells", 2));
for (name, base, size) in [
("serial@10001234", 0x1000_1234, 0x100),
("gpio@10002000", 0x1000_2000, 0x1000),
] {
let node = fdt.add_node(root, Node::new(name));
fdt.view_typed_mut(node)
.unwrap()
.set_regs(&[RegInfo::new(base, Some(size))]);
}
fdt.encode().as_ref().to_vec()
}
#[test]
fn align_reserved_region_keeps_aligned_range() {
assert_eq!(
align_reserved_region_4k(0x1000, 0x2000),
Some((0x1000, 0x2000))
);
}
#[test]
fn align_reserved_region_expands_to_cover_unaligned_bounds() {
assert_eq!(
align_reserved_region_4k(0x1100, 0x2500),
Some((0x1000, 0x3000))
);
}
#[test]
fn align_reserved_region_rejects_zero_sized_range() {
assert_eq!(align_reserved_region_4k(0x1000, 0), None);
}
#[test]
fn subtract_memory_region_overlap_keeps_non_overlapping_range() {
let existing = vec![VmMemConfig {
gpa: 0x4000,
size: 0x1000,
flags: 0,
map_type: VmMemMappingType::MapReserved,
}];
assert_eq!(
super::subtract_memory_region_overlap(0x1000, 0x1000, &existing),
vec![(0x1000, 0x1000)]
);
}
#[test]
fn subtract_memory_region_overlap_splits_range_around_overlap() {
let existing = vec![VmMemConfig {
gpa: 0x3000,
size: 0x2000,
flags: 0,
map_type: VmMemMappingType::MapReserved,
}];
assert_eq!(
super::subtract_memory_region_overlap(0x1000, 0x6000, &existing),
vec![(0x1000, 0x2000), (0x5000, 0x2000)]
);
}
#[test]
fn subtract_memory_region_overlap_drops_fully_covered_range() {
let existing = vec![VmMemConfig {
gpa: 0x1000,
size: 0x4000,
flags: 0,
map_type: VmMemMappingType::MapReserved,
}];
assert!(super::subtract_memory_region_overlap(0x2000, 0x1000, &existing).is_empty());
}
#[test]
fn excluded_device_ranges_become_reserved_vm_ranges() {
let dtb = fdt_with_excluded_devices();
let mut vm_cfg = AxVMConfig::new(AxVMConfigParams {
id: 0,
name: "test".to_string(),
phys_cpu_ls: PhysCpuList::new(1, None, None),
..Default::default()
});
let crate_cfg = AxVMCrateConfig {
devices: VMDevicesConfig {
address_space_policy: AddressSpacePolicy::Passthrough,
excluded_devices: vec![vec!["/serial@10001234".to_string()]],
..Default::default()
},
..Default::default()
};
reserve_excluded_device_ranges(&mut vm_cfg, &crate_cfg, &dtb).unwrap();
let ranges = vm_cfg.reserved_address_ranges();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].base_gpa, 0x1000_1000);
assert_eq!(ranges[0].length, 0x1000);
}
#[test]
fn riscv_plic_interrupt_uses_first_fdt_cell() {
assert_eq!(
super::riscv_plic_source_from_interrupt_specifier(&[8]),
Some(8)
);
}
#[test]
fn riscv_plic_interrupt_rejects_reserved_source_zero() {
assert_eq!(
super::riscv_plic_source_from_interrupt_specifier(&[0]),
None
);
assert_eq!(super::riscv_plic_source_from_interrupt_specifier(&[]), None);
}
}