use std::{ptr::NonNull, string::String, vec::Vec};
use ax_memory_addr::MemoryAddr;
use axvmconfig::GuestConfig;
use fdt_edit::{Fdt, Node, NodeId, Property};
use super::tree::{FdtTree, GuestMemorySpec};
use crate::{
AxVMRef, AxVmResult, GuestPhysAddr, VMMemoryRegion, ax_err_type,
boot::images::load_vm_image_from_memory,
};
pub fn create_guest_fdt(
fdt: &Fdt,
passthrough_device_names: &[String],
crate_config: &GuestConfig,
) -> AxVmResult<Vec<u8>> {
let phys_cpu_ids = crate_config
.base
.phys_cpu_ids
.as_deref()
.ok_or_else(|| ax_err_type!(InvalidInput, "phys_cpu_ids is missing"))?;
let machine_interrupt_providers = fdt
.iter_node_ids()
.filter_map(|node_id| {
let node = fdt.node(node_id)?;
is_machine_interrupt_provider(node).then(|| fdt.path_of(node_id))
})
.collect::<Vec<_>>();
let mut guest_tree = FdtTree::clone_filtered(fdt, |node_id, path, node| {
should_keep_generated_node(
fdt,
node_id,
path,
node,
passthrough_device_names,
phys_cpu_ids,
&machine_interrupt_providers,
)
})?;
prune_dangling_interrupts_extended(fdt, &mut guest_tree)?;
Ok(guest_tree.finish())
}
fn should_keep_generated_node(
fdt: &Fdt,
node_id: NodeId,
node_path: &str,
node: &Node,
passthrough_device_names: &[String],
phys_cpu_ids: &[usize],
machine_interrupt_providers: &[String],
) -> bool {
if node.name().starts_with("memory") {
return false;
}
if node_path == "/cpus" || node_path.starts_with("/cpus/cpu-map") {
return true;
}
if node_path.starts_with("/cpus/cpu@") {
return need_cpu_node(phys_cpu_ids, fdt, node_id, node_path);
}
if machine_interrupt_providers
.iter()
.any(|controller| is_path_or_ancestor(node_path, controller))
{
return true;
}
if node
.compatibles()
.any(|compatible| matches!(compatible, "arm,psci" | "arm,psci-0.2" | "arm,psci-1.0"))
{
return true;
}
passthrough_device_names
.iter()
.any(|device_path| device_path == node_path)
|| is_descendant_of_passthrough_device(node_path, passthrough_device_names)
|| is_ancestor_of_passthrough_device(node_path, passthrough_device_names)
}
fn is_machine_interrupt_provider(node: &Node) -> bool {
node.compatibles().any(|compatible| {
compatible == "arm,gic-v3-its"
|| (node.get_property("interrupt-controller").is_some()
&& matches!(
compatible,
"arm,gic-v3"
| "arm,cortex-a15-gic"
| "arm,gic-400"
| "riscv,plic0"
| "sifive,plic-1.0.0"
))
})
}
fn is_path_or_ancestor(candidate: &str, path: &str) -> bool {
candidate == path
|| path
.strip_prefix(candidate)
.is_some_and(|suffix| candidate == "/" || suffix.starts_with('/'))
}
fn prune_dangling_interrupts_extended(source: &Fdt, guest: &mut FdtTree) -> AxVmResult {
let nodes = guest
.inner()
.iter_node_ids()
.filter_map(|node_id| {
guest
.inner()
.node(node_id)?
.get_property("interrupts-extended")
.map(|_| (node_id, guest.inner().path_of(node_id)))
})
.collect::<Vec<_>>();
for (node_id, path) in nodes {
let property = source
.get_by_path(&path)
.and_then(|node| node.as_node().get_property("interrupts-extended"))
.ok_or_else(|| {
ax_err_type!(
InvalidData,
std::format!("source FDT node {path} lost interrupts-extended")
)
})?;
let cells = property.get_u32_iter().collect::<Vec<_>>();
let mut filtered = Vec::with_capacity(cells.len());
let mut cursor = 0;
while cursor < cells.len() {
let phandle = cells[cursor];
let provider = find_node_by_phandle(source, phandle)
.and_then(|node_id| source.node(node_id))
.ok_or_else(|| {
ax_err_type!(
InvalidData,
std::format!(
"FDT node {path} references missing interrupt provider {phandle:#x}"
)
)
})?;
let interrupt_cells = provider
.get_property("#interrupt-cells")
.and_then(Property::get_u32)
.ok_or_else(|| {
ax_err_type!(
InvalidData,
std::format!(
"interrupt provider {phandle:#x} for {path} has no #interrupt-cells"
)
)
})? as usize;
let end = cursor
.checked_add(interrupt_cells + 1)
.filter(|end| *end <= cells.len())
.ok_or_else(|| {
ax_err_type!(
InvalidData,
std::format!("FDT node {path} has truncated interrupts-extended")
)
})?;
if find_node_by_phandle(guest.inner(), phandle).is_some() {
filtered.extend_from_slice(&cells[cursor..end]);
}
cursor = end;
}
if filtered.len() != cells.len() {
let mut property = Property::new("interrupts-extended", std::vec![]);
property.set_u32_ls(&filtered);
guest.set_property(node_id, property)?;
}
}
Ok(())
}
fn find_node_by_phandle(fdt: &Fdt, phandle: u32) -> Option<NodeId> {
fdt.iter_node_ids().find(|node_id| {
fdt.node(*node_id).is_some_and(|node| {
node.get_property("phandle")
.or_else(|| node.get_property("linux,phandle"))
.and_then(Property::get_u32)
== Some(phandle)
})
})
}
fn is_descendant_of_passthrough_device(
node_path: &str,
passthrough_device_names: &[String],
) -> bool {
passthrough_device_names.iter().any(|passthrough_path| {
node_path
.strip_prefix(passthrough_path)
.is_some_and(|suffix| suffix.starts_with('/'))
})
}
fn is_ancestor_of_passthrough_device(node_path: &str, passthrough_device_names: &[String]) -> bool {
passthrough_device_names.iter().any(|passthrough_path| {
passthrough_path
.strip_prefix(node_path)
.is_some_and(|suffix| suffix.starts_with('/'))
|| node_path == "/"
})
}
fn cpu_node_id(node_path: &str) -> Option<usize> {
node_path
.strip_prefix("/cpus/cpu@")
.and_then(|rest| rest.split('/').next())
.and_then(|id| usize::from_str_radix(id, 16).ok())
}
fn cpu_reg_address(fdt: &Fdt, node_id: NodeId) -> Option<usize> {
fdt.view_typed(node_id)
.and_then(|node| node.regs().first().map(|reg| reg.address as usize))
}
pub(crate) fn need_cpu_node(
phys_cpu_ids: &[usize],
fdt: &Fdt,
node_id: NodeId,
node_path: &str,
) -> bool {
if !node_path.starts_with("/cpus/cpu@") {
return true;
}
if let Some(cpu_id) = cpu_node_id(node_path) {
return phys_cpu_ids.contains(&cpu_id);
}
cpu_reg_address(fdt, node_id).is_some_and(|cpu_address| {
debug!("Checking CPU node {node_path} with address 0x{cpu_address:x}");
phys_cpu_ids.contains(&cpu_address)
})
}
fn guest_memory_specs(
new_memory: &[VMMemoryRegion],
crate_config: &GuestConfig,
) -> Vec<GuestMemorySpec> {
let configured_region_count = if crate_config.kernel.configured_memory_region_count == 0 {
crate_config.kernel.memory_regions.len()
} else {
crate_config
.kernel
.configured_memory_region_count
.min(crate_config.kernel.memory_regions.len())
};
if new_memory.len() != crate_config.kernel.memory_regions.len() {
warn!(
"VM memory region count {} does not match config region count {}; filtering /memory \
by zipped order",
new_memory.len(),
crate_config.kernel.memory_regions.len()
);
}
new_memory
.iter()
.take(configured_region_count)
.zip(
crate_config
.kernel
.memory_regions
.iter()
.take(configured_region_count),
)
.map(|(mem, _cfg)| GuestMemorySpec::new(mem.gpa.as_usize() as u64, mem.size() as u64))
.collect()
}
#[cfg(test)]
fn initrd_range_from_image_config(
ramdisk: Option<&crate::config::RamdiskInfo>,
) -> Option<(u64, u64)> {
let ramdisk = ramdisk?;
let start = ramdisk.load_gpa.as_usize() as u64;
let size = ramdisk.size? as u64;
Some((start, start.saturating_add(size)))
}
pub fn update_fdt(
fdt_src: NonNull<u8>,
dtb_size: usize,
vm: AxVMRef,
crate_config: &GuestConfig,
) -> AxVmResult {
let patch_runtime = super::selected_guest_fdt_policy().patch_runtime;
let fdt_bytes = unsafe { std::slice::from_raw_parts(fdt_src.as_ptr(), dtb_size) };
let new_fdt_bytes = patch_runtime(fdt_bytes, &vm, crate_config)?;
load_patched_fdt(vm, new_fdt_bytes)
}
fn load_patched_fdt(vm: AxVMRef, new_fdt_bytes: Vec<u8>) -> AxVmResult {
let dest_addr = calculate_dtb_load_addr(vm.clone(), new_fdt_bytes.len())?;
debug!(
"New FDT will be loaded at {:x}, size: 0x{:x}",
dest_addr,
new_fdt_bytes.len()
);
load_vm_image_from_memory(&new_fdt_bytes, dest_addr, vm.clone())?;
vm.set_guest_device_tree(dest_addr, new_fdt_bytes)
}
pub(crate) fn patch_guest_fdt_for_runtime(
fdt_bytes: &[u8],
memory_regions: &[VMMemoryRegion],
crate_config: &GuestConfig,
serial_profile: crate::machine::GuestSerialProfile,
serial_identity: Option<&crate::machine::GuestSerialFdtIdentity>,
additional_serials: &[crate::machine::GuestSerialProfile],
gic_profile: Option<&crate::machine::GuestGicProfile>,
plic_profile: Option<&crate::machine::GuestPlicProfile>,
timer_profile: Option<&crate::machine::GuestTimerProfile>,
initrd_start_size: Option<(u64, u64)>,
create_chosen: bool,
) -> AxVmResult<Vec<u8>> {
let mut tree = FdtTree::from_bytes(fdt_bytes)?;
let memory_specs = guest_memory_specs(memory_regions, crate_config);
tree.rebuild_memory_nodes(&memory_specs)?;
if create_chosen
|| initrd_start_size.is_some()
|| crate_config.kernel.cmdline.is_some()
|| tree.inner().get_by_path_id("/chosen").is_some()
{
tree.patch_chosen(initrd_start_size, crate_config.kernel.cmdline.as_deref())?;
}
super::interrupt::install_machine_interrupt_controller(
&mut tree,
crate_config.base.cpu_num,
gic_profile,
plic_profile,
)?;
super::timer::install_machine_timer(&mut tree, timer_profile)?;
super::serial::install_machine_serial(&mut tree, serial_profile, serial_identity)?;
for serial in additional_serials {
super::serial::install_additional_serial(&mut tree, *serial)?;
}
let bytes = tree.finish();
Fdt::from_bytes(&bytes).map_err(|error| {
ax_err_type!(InvalidData, std::format!("invalid patched FDT: {error:?}"))
})?;
Ok(bytes)
}
pub(crate) fn calculate_dtb_load_addr(vm: AxVMRef, fdt_size: usize) -> AxVmResult<GuestPhysAddr> {
const MB: usize = 1024 * 1024;
let main_memory =
vm.memory_regions().first().cloned().ok_or_else(|| {
ax_err_type!(InvalidInput, "VM has no memory region for DTB placement")
})?;
let dtb_addr = vm.with_config(|config| {
let use_configured_dtb_addr =
config.image_config.dtb_load_gpa.is_some() && !main_memory.is_identical();
let dtb_addr = if let Some(configured) = config
.image_config
.dtb_load_gpa
.filter(|_| use_configured_dtb_addr)
{
configured
} else {
let main_memory_size = main_memory.size().min(512 * MB);
let addr = (main_memory.gpa + main_memory_size - fdt_size).align_down(2 * MB);
if fdt_size > main_memory_size {
error!("DTB size is larger than available memory");
}
addr
};
config.image_config.dtb_load_gpa = Some(dtb_addr);
dtb_addr
});
Ok(dtb_addr)
}
#[cfg(test)]
mod tests {
use axvmconfig::GuestConfig;
use fdt_edit::{Fdt, Node, Property};
use fdt_raw::RegInfo;
use super::{
super::tree::sanitize_bootargs, cpu_node_id, initrd_range_from_image_config, need_cpu_node,
};
use crate::{GuestPhysAddr, config::RamdiskInfo};
fn prop_u32(name: &str, value: u32) -> Property {
let mut prop = Property::new(name, std::vec![]);
prop.set_u32_ls(&[value]);
prop
}
fn test_fdt(dts: &str) -> Fdt {
let mut fdt = Fdt::new();
let root = fdt.root_id();
let cpus = fdt.add_node(root, Node::new("cpus"));
fdt.node_mut(cpus)
.unwrap()
.set_property(prop_u32("#address-cells", 2));
fdt.node_mut(cpus)
.unwrap()
.set_property(prop_u32("#size-cells", 0));
for line in dts.lines().map(str::trim).filter(|line| !line.is_empty()) {
let (name, reg) = line.split_once('=').unwrap();
let node = fdt.add_node(cpus, Node::new(name));
let reg = usize::from_str_radix(reg, 16).unwrap();
fdt.view_typed_mut(node)
.unwrap()
.set_regs(&[RegInfo::new(reg as u64, None)]);
}
fdt
}
#[test]
fn cpu_node_selection_uses_node_id_when_reg_differs() {
let fdt = test_fdt("cpu@0=200\ncpu@100=0\ncpu@101=100");
let selected: std::vec::Vec<_> = fdt
.iter_node_ids()
.map(|id| (id, fdt.path_of(id)))
.filter(|(_, path)| path.starts_with("/cpus/cpu@"))
.filter_map(|(id, path)| need_cpu_node(&[0x100], &fdt, id, &path).then_some(path))
.collect();
assert_eq!(selected, ["/cpus/cpu@100"]);
}
#[test]
fn cpu_node_id_parses_hex_unit_address() {
assert_eq!(cpu_node_id("/cpus/cpu@100"), Some(0x100));
}
#[test]
fn initrd_range_requires_both_address_and_size() {
assert_eq!(
initrd_range_from_image_config(Some(&RamdiskInfo {
load_gpa: GuestPhysAddr::from(0xa000_0000usize),
size: None,
})),
None
);
assert_eq!(
initrd_range_from_image_config(Some(&RamdiskInfo {
load_gpa: GuestPhysAddr::from(0xa000_0000usize),
size: Some(0x1234),
})),
Some((0xa000_0000, 0xa000_1234))
);
}
#[test]
fn sanitize_bootargs_enables_auto_repair_for_block_roots() {
let bootargs = "root=/dev/mmcblk0p2 rw console=ttyS2,1500000 rootwait rootfstype=ext4";
assert_eq!(
sanitize_bootargs(bootargs),
"root=/dev/mmcblk0p2 rw console=ttyS2,1500000 rootwait rootfstype=ext4 fsck.repair=yes"
);
}
#[test]
fn sanitize_bootargs_preserves_existing_fsck_policy() {
let bootargs =
"root=/dev/mmcblk0p2 ro rootwait rootfstype=ext4 fsckfix rdinit=/init root=/dev/ram0";
assert_eq!(
sanitize_bootargs(bootargs),
"root=/dev/mmcblk0p2 rw rootwait rootfstype=ext4 fsckfix"
);
}
#[test]
fn runtime_patch_can_leave_missing_chosen_for_host_copy() {
let fdt = Fdt::new();
let dtb = fdt.encode().as_ref().to_vec();
let cfg = GuestConfig::default();
let serial = crate::machine::current_machine_profile(1).serial;
let patched = super::patch_guest_fdt_for_runtime(
&dtb,
&[],
&cfg,
serial,
None,
&[],
None,
None,
None,
None,
false,
)
.unwrap();
let reparsed = Fdt::from_bytes(&patched).unwrap();
assert!(reparsed.get_by_path_id("/chosen").is_none());
let serial = crate::machine::current_machine_profile(1).serial;
let patched = super::patch_guest_fdt_for_runtime(
&dtb,
&[],
&cfg,
serial,
None,
&[],
None,
None,
None,
None,
true,
)
.unwrap();
let reparsed = Fdt::from_bytes(&patched).unwrap();
assert!(reparsed.get_by_path_id("/chosen").is_some());
}
#[test]
fn generated_fdt_filters_cpu_nodes_by_unit_address() {
let fdt = test_fdt("cpu@0=200\ncpu@100=0\ncpu@101=100");
let cfg = GuestConfig {
base: axvmconfig::VMBaseConfig {
phys_cpu_ids: Some(std::vec![0x100]),
..Default::default()
},
..Default::default()
};
let dtb = super::create_guest_fdt(&fdt, &[], &cfg).unwrap();
let reparsed = Fdt::from_bytes(&dtb).unwrap();
assert!(reparsed.get_by_path_id("/cpus/cpu@100").is_some());
assert!(reparsed.get_by_path_id("/cpus/cpu@0").is_none());
assert!(reparsed.get_by_path_id("/cpus/cpu@101").is_none());
}
#[test]
fn generated_fdt_keeps_psci_firmware_node() {
let mut fdt = test_fdt("cpu@0=0");
let psci = fdt.add_node(fdt.root_id(), Node::new("psci"));
let mut compatible = Property::new("compatible", std::vec![]);
compatible.set_string("arm,psci-0.2");
fdt.node_mut(psci).unwrap().set_property(compatible);
let cfg = GuestConfig {
base: axvmconfig::VMBaseConfig {
phys_cpu_ids: Some(std::vec![0]),
..Default::default()
},
..Default::default()
};
let dtb = super::create_guest_fdt(&fdt, &[], &cfg).unwrap();
let reparsed = Fdt::from_bytes(&dtb).unwrap();
assert!(reparsed.get_by_path_id("/psci").is_some());
}
#[test]
fn generated_fdt_keeps_the_host_interrupt_controller_for_a_virtual_machine() {
let mut fdt = test_fdt("cpu@0=0\ncpu@1=1");
for (cpu_path, phandle) in [("/cpus/cpu@0", 8), ("/cpus/cpu@1", 6)] {
let cpu = fdt.get_by_path_id(cpu_path).unwrap();
let intc = fdt.add_node(cpu, Node::new("interrupt-controller"));
fdt.node_mut(intc)
.unwrap()
.set_property(prop_u32("#interrupt-cells", 1));
fdt.node_mut(intc)
.unwrap()
.set_property(Property::new("interrupt-controller", std::vec![]));
fdt.node_mut(intc)
.unwrap()
.set_property(prop_u32("phandle", phandle));
}
let root = fdt.root_id();
let soc = fdt.add_node(root, Node::new("soc"));
let plic = fdt.add_node(soc, Node::new("plic@c000000"));
let mut compatible = Property::new("compatible", std::vec![]);
compatible.set_string("riscv,plic0");
fdt.node_mut(plic).unwrap().set_property(compatible);
fdt.node_mut(plic)
.unwrap()
.set_property(Property::new("interrupt-controller", std::vec![]));
fdt.node_mut(plic)
.unwrap()
.set_property(prop_u32("phandle", 9));
let mut contexts = Property::new("interrupts-extended", std::vec![]);
contexts.set_u32_ls(&[8, 11, 8, 9, 6, 11, 6, 9]);
fdt.node_mut(plic).unwrap().set_property(contexts);
let its = fdt.add_node(root, Node::new("its@8080000"));
let mut compatible = Property::new("compatible", std::vec![]);
compatible.set_string("arm,gic-v3-its");
fdt.node_mut(its).unwrap().set_property(compatible);
fdt.node_mut(its)
.unwrap()
.set_property(Property::new("msi-controller", std::vec![]));
let cfg = GuestConfig {
base: axvmconfig::VMBaseConfig {
phys_cpu_ids: Some(std::vec![0]),
..Default::default()
},
..Default::default()
};
let dtb = super::create_guest_fdt(&fdt, &[], &cfg).unwrap();
let reparsed = Fdt::from_bytes(&dtb).unwrap();
let plic = reparsed.get_by_path("/soc/plic@c000000").unwrap();
assert!(reparsed.get_by_path_id("/its@8080000").is_some());
assert_eq!(
plic.as_node().get_property("phandle").unwrap().get_u32(),
Some(9)
);
assert_eq!(
plic.as_node()
.get_property("interrupts-extended")
.unwrap()
.get_u32_iter()
.collect::<std::vec::Vec<_>>(),
[8, 11, 8, 9]
);
}
}