use super::*;
fn test_mem(mib: u32) -> GuestMemoryMmap {
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), (mib as usize) << 20)]).unwrap()
}
fn test_layout(topo: &Topology, mib: u32) -> NumaMemoryLayout {
NumaMemoryLayout::compute(topo, mib, 0, None).unwrap()
}
fn test_setup(mem: &GuestMemoryMmap, topo: &Topology, mib: u32) -> AcpiLayout {
let layout = test_layout(topo, mib);
setup_acpi(mem, topo, &layout).unwrap()
}
fn read_table(mem: &GuestMemoryMmap, addr: u64) -> Vec<u8> {
let mut len_bytes = [0u8; 4];
mem.read_slice(&mut len_bytes, GuestAddress(addr + 4))
.unwrap();
let len = u32::from_le_bytes(len_bytes) as usize;
let mut buf = vec![0u8; len];
mem.read_slice(&mut buf, GuestAddress(addr)).unwrap();
buf
}
fn read_madt(mem: &GuestMemoryMmap, layout: &AcpiLayout) -> Vec<u8> {
read_table(mem, layout.madt_addr)
}
fn walk_madt_entries(madt: &[u8]) -> Vec<(u8, u8, &[u8])> {
let hdr_size = std::mem::size_of::<MadtHeader>();
let mut entries = Vec::new();
let mut offset = hdr_size;
while offset < madt.len() {
let entry_type = madt[offset];
let entry_len = madt[offset + 1];
entries.push((
entry_type,
entry_len,
&madt[offset..offset + entry_len as usize],
));
offset += entry_len as usize;
}
entries
}
const _: () = assert!(std::mem::size_of::<SdtHeader>() == 36);
const _: () = assert!(std::mem::size_of::<Rsdp>() == 36);
const _: () = assert!(std::mem::size_of::<MadtHeader>() == 44);
const _: () = assert!(std::mem::size_of::<MadtLocalApic>() == 8);
const _: () = assert!(std::mem::size_of::<MadtX2Apic>() == 16);
const _: () = assert!(std::mem::size_of::<MadtIoApic>() == 12);
const _: () = assert!(std::mem::size_of::<MadtIso>() == 10);
const _: () = assert!(std::mem::size_of::<MadtLapicNmi>() == 6);
const _: () = assert!(std::mem::size_of::<MadtX2ApicNmi>() == 12);
const _: () = assert!(std::mem::size_of::<SratCpuAffinity>() == 24);
const _: () = assert!(std::mem::size_of::<SratMemAffinity>() == 40);
#[test]
fn acpi_rejects_slit_dominated_overflow_at_extreme_numa() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 512,
nodes: None,
distances: None,
};
let layout = test_layout(&topo, 512);
let err =
setup_acpi(&mem, &topo, &layout).expect_err("512-NUMA SLIT must overflow the ISA hole");
let msg = format!("{err:#}");
assert!(
msg.contains("SLIT alone") && msg.contains("ISA hole"),
"error must name the SLIT-dominated ISA-hole overflow, got: {msg}"
);
}
#[test]
fn acpi_rejects_cpu_table_overflow_at_extreme_vcpu_count() {
let mem = test_mem(16);
let topo = Topology {
llcs: 4096,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let layout = test_layout(&topo, 256);
let err = setup_acpi(&mem, &topo, &layout)
.expect_err("4096-vCPU ACPI CPU tables must overflow the ISA hole");
let msg = format!("{err:#}");
assert!(
msg.contains("CPU tables") && msg.contains("ISA hole"),
"error must name the CPU-table ISA-hole overflow, got: {msg}"
);
}
#[test]
fn acpi_rejects_total_overflow_when_slit_alone_fits() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 361,
nodes: None,
distances: None,
};
let layout = test_layout(&topo, 361);
let err = setup_acpi(&mem, &topo, &layout)
.expect_err("361-NUMA total tables must overflow the ISA hole");
let msg = format!("{err:#}");
assert!(
msg.contains("tables overflow") && msg.contains("ISA hole"),
"error must name the total-pack ISA-hole overflow, got: {msg}"
);
}
#[test]
fn acpi_tables_fit_at_wide_smp_topology() {
let mem = test_mem(16);
let topo = Topology {
llcs: 16,
cores_per_llc: 16,
threads_per_core: 1,
numa_nodes: 16,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 4096);
let end = l.xsdt_addr + l.xsdt_size;
assert!(
end <= HIMEM_START,
"wide-SMP ACPI tables must fit the ISA hole: end={end:#x} \
HIMEM_START={HIMEM_START:#x}"
);
}
#[test]
fn rsdp_signature_and_checksum() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut rsdp = [0u8; 20];
mem.read_slice(&mut rsdp, GuestAddress(l.rsdp_addr))
.unwrap();
assert_eq!(&rsdp[..8], b"RSD PTR ");
let sum: u8 = rsdp.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(sum, 0, "RSDP checksum must be zero");
}
#[test]
fn rsdt_signature_and_checksum() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let rsdt = read_table(&mem, l.rsdt_addr);
assert_eq!(&rsdt[..4], b"RSDT");
let sum: u8 = rsdt.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(sum, 0, "RSDT checksum must be zero");
}
#[test]
fn madt_signature_and_checksum() {
let mem = test_mem(16);
let topo = Topology {
llcs: 2,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
assert_eq!(&madt[..4], b"APIC");
let sum: u8 = madt.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(sum, 0, "MADT checksum must be zero");
}
#[test]
fn madt_has_correct_cpu_count() {
let mem = test_mem(16);
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
let cpu_count = entries
.iter()
.filter(|(t, _, _)| *t == 0 || *t == 9)
.count();
assert_eq!(cpu_count, 16);
}
#[test]
fn madt_apic_ids_match_topology() {
let mem = test_mem(16);
let topo = Topology {
llcs: 2,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
let mut cpu_idx = 0u32;
for (entry_type, _, data) in &entries {
match *entry_type {
0 => {
assert_eq!(data[3] as u32, apic_id(&topo, cpu_idx));
cpu_idx += 1;
}
9 => {
assert_eq!(
u32::from_le_bytes(data[4..8].try_into().unwrap()),
apic_id(&topo, cpu_idx)
);
cpu_idx += 1;
}
_ => {}
}
}
}
#[test]
fn madt_has_ioapic() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
let ioapic = entries.iter().find(|(t, _, _)| *t == 1);
assert!(ioapic.is_some());
let (_, _, data) = ioapic.unwrap();
assert_eq!(
u32::from_le_bytes(data[4..8].try_into().unwrap()),
IOAPIC_ADDR
);
}
#[test]
fn rsdp_points_to_rsdt() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut rsdp = [0u8; 20];
mem.read_slice(&mut rsdp, GuestAddress(l.rsdp_addr))
.unwrap();
assert_eq!(
u32::from_le_bytes(rsdp[16..20].try_into().unwrap()),
l.rsdt_addr as u32
);
}
#[test]
fn rsdt_table_pointers() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut entry = [0u8; 4];
mem.read_slice(&mut entry, GuestAddress(l.rsdt_addr + 36))
.unwrap();
assert_eq!(u32::from_le_bytes(entry), l.fadt_addr as u32);
mem.read_slice(&mut entry, GuestAddress(l.rsdt_addr + 40))
.unwrap();
assert_eq!(u32::from_le_bytes(entry), l.madt_addr as u32);
mem.read_slice(&mut entry, GuestAddress(l.rsdt_addr + 44))
.unwrap();
assert_eq!(u32::from_le_bytes(entry), l.srat_addr as u32);
mem.read_slice(&mut entry, GuestAddress(l.rsdt_addr + 48))
.unwrap();
assert_eq!(u32::from_le_bytes(entry), l.slit_addr as u32);
}
#[test]
fn madt_has_iso_irq0_gsi2() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
let iso = entries.iter().find(|(t, _, _)| *t == 2).unwrap();
assert_eq!(iso.2[3], 0);
assert_eq!(u32::from_le_bytes(iso.2[4..8].try_into().unwrap()), 2);
}
#[test]
fn madt_has_nmi() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
assert!(entries.iter().any(|(t, _, _)| *t == 4 || *t == 0x0A));
}
#[test]
fn small_topology_uses_lapic_entries() {
let mem = test_mem(16);
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
assert_eq!(entries.iter().filter(|(t, _, _)| *t == 9).count(), 0);
assert_eq!(entries.iter().filter(|(t, _, _)| *t == 0).count(), 16);
assert!(entries.iter().any(|(t, _, _)| *t == 4));
assert!(!entries.iter().any(|(t, _, _)| *t == 0x0A));
}
#[test]
fn large_topology_uses_mixed_entries() {
let mem = test_mem(16);
let topo = Topology {
llcs: 14,
cores_per_llc: 9,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let mut has_low = false;
let mut has_high = false;
for cpu_id in 0..topo.total_cpus() {
let id = apic_id(&topo, cpu_id);
if id < 255 {
has_low = true;
} else {
has_high = true;
}
}
assert!(has_low && has_high);
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
let lapic_count = entries.iter().filter(|(t, _, _)| *t == 0).count();
let x2apic_count = entries.iter().filter(|(t, _, _)| *t == 9).count();
assert!(lapic_count > 0);
assert!(x2apic_count > 0);
assert_eq!(lapic_count + x2apic_count, topo.total_cpus() as usize);
assert!(entries.iter().any(|(t, _, _)| *t == 4));
assert!(entries.iter().any(|(t, _, _)| *t == 0x0A));
}
#[test]
fn x2apic_nmi_fields_correct() {
let mem = test_mem(16);
let topo = Topology {
llcs: 14,
cores_per_llc: 9,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
let (_, len, data) = entries.iter().find(|(t, _, _)| *t == 0x0A).unwrap();
assert_eq!(*len, 12);
assert_eq!(u16::from_le_bytes(data[2..4].try_into().unwrap()), 0);
assert_eq!(
u32::from_le_bytes(data[4..8].try_into().unwrap()),
0xFFFF_FFFF
);
assert_eq!(data[8], 1);
}
#[test]
fn lapic_nmi_fields_correct() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
let (_, len, data) = entries.iter().find(|(t, _, _)| *t == 4).unwrap();
assert_eq!(*len, 6);
assert_eq!(data[2], 0xFF);
assert_eq!(u16::from_le_bytes(data[3..5].try_into().unwrap()), 0);
assert_eq!(data[5], 1);
}
#[test]
fn madt_checksum_representative_topologies() {
let topos = [
(1, 1, 1), (2, 1, 1), (3, 3, 1), (1, 1, 2), (2, 4, 2), (7, 5, 3), (15, 16, 1), (14, 9, 2), (2, 128, 1), (14, 18, 1), ];
for (llcs, cores, threads) in topos {
let mem = test_mem(16);
let topo = Topology {
llcs,
cores_per_llc: cores,
threads_per_core: threads,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let sum: u8 = madt.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(
sum, 0,
"MADT checksum failed for {llcs}l/{cores}c/{threads}t"
);
let entries = walk_madt_entries(&madt);
let cpu_count = entries
.iter()
.filter(|(t, _, _)| *t == 0 || *t == 9)
.count();
assert_eq!(cpu_count, topo.total_cpus() as usize);
assert!(entries.iter().any(|(t, _, _)| *t == 4 || *t == 0x0A));
}
}
#[test]
fn cpu_entry_type_matches_apic_id() {
for (llcs, cores, threads) in [(1, 4, 1), (2, 2, 2), (15, 8, 2), (14, 9, 2)] {
let mem = test_mem(16);
let topo = Topology {
llcs,
cores_per_llc: cores,
threads_per_core: threads,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
let mut cpu_idx = 0u32;
for (entry_type, _, data) in &entries {
match *entry_type {
0 => {
let id = data[3] as u32;
assert!(id < 255);
assert_eq!(id, apic_id(&topo, cpu_idx));
cpu_idx += 1;
}
9 => {
let id = u32::from_le_bytes(data[4..8].try_into().unwrap());
assert!(id >= 255);
assert_eq!(id, apic_id(&topo, cpu_idx));
cpu_idx += 1;
}
_ => {}
}
}
}
}
#[test]
fn madt_entry_lengths_valid() {
let mem = test_mem(16);
let topo = Topology {
llcs: 14,
cores_per_llc: 9,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
for (entry_type, entry_len, _) in &entries {
let expected = match *entry_type {
0 => 8,
1 => 12,
2 => 10,
4 => 6,
9 => 16,
0x0A => 12,
t => panic!("unexpected MADT entry type {t}"),
};
assert_eq!(*entry_len, expected);
}
}
#[test]
fn madt_total_length_matches_entries() {
let mem = test_mem(16);
let topo = Topology {
llcs: 14,
cores_per_llc: 9,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let declared_len = u32::from_le_bytes(madt[4..8].try_into().unwrap()) as usize;
assert_eq!(declared_len, madt.len());
let entries = walk_madt_entries(&madt);
let entries_size: usize = entries.iter().map(|(_, l, _)| *l as usize).sum();
assert_eq!(
std::mem::size_of::<MadtHeader>() + entries_size,
declared_len
);
}
#[test]
fn cpu_flags_enabled() {
let mem = test_mem(16);
let topo = Topology {
llcs: 2,
cores_per_llc: 2,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let madt = read_madt(&mem, &l);
let entries = walk_madt_entries(&madt);
for (entry_type, _, data) in &entries {
match *entry_type {
0 => assert_eq!(u32::from_le_bytes(data[4..8].try_into().unwrap()) & 1, 1),
9 => assert_eq!(u32::from_le_bytes(data[8..12].try_into().unwrap()) & 1, 1),
_ => {}
}
}
}
#[test]
fn rsdp_rev2_structure() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut rsdp = [0u8; 36];
mem.read_slice(&mut rsdp, GuestAddress(l.rsdp_addr))
.unwrap();
assert_eq!(&rsdp[..8], b"RSD PTR ");
assert_eq!(rsdp[15], 2);
assert_eq!(
u32::from_le_bytes(rsdp[16..20].try_into().unwrap()),
l.rsdt_addr as u32
);
assert_eq!(u32::from_le_bytes(rsdp[20..24].try_into().unwrap()), 36);
assert_eq!(
u64::from_le_bytes(rsdp[24..32].try_into().unwrap()),
l.xsdt_addr
);
let sum20: u8 = rsdp[..20].iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(sum20, 0);
let sum36: u8 = rsdp.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(sum36, 0);
}
#[test]
fn xsdt_signature_and_checksum() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let xsdt = read_table(&mem, l.xsdt_addr);
assert_eq!(&xsdt[..4], b"XSDT");
assert_eq!(xsdt.len(), 68);
let sum: u8 = xsdt.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(sum, 0);
}
#[test]
fn xsdt_table_pointers() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut entry = [0u8; 8];
mem.read_slice(&mut entry, GuestAddress(l.xsdt_addr + 36))
.unwrap();
assert_eq!(u64::from_le_bytes(entry), l.fadt_addr);
mem.read_slice(&mut entry, GuestAddress(l.xsdt_addr + 44))
.unwrap();
assert_eq!(u64::from_le_bytes(entry), l.madt_addr);
mem.read_slice(&mut entry, GuestAddress(l.xsdt_addr + 52))
.unwrap();
assert_eq!(u64::from_le_bytes(entry), l.srat_addr);
mem.read_slice(&mut entry, GuestAddress(l.xsdt_addr + 60))
.unwrap();
assert_eq!(u64::from_le_bytes(entry), l.slit_addr);
}
#[test]
fn fadt_signature_and_checksum() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut fadt = [0u8; 276];
mem.read_slice(&mut fadt, GuestAddress(l.fadt_addr))
.unwrap();
assert_eq!(&fadt[..4], b"FACP");
assert_eq!(u32::from_le_bytes(fadt[4..8].try_into().unwrap()), 276);
assert_eq!(fadt[8], 6);
let sum: u8 = fadt.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(sum, 0);
}
#[test]
fn fadt_hw_reduced_flags() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut fadt = [0u8; 276];
mem.read_slice(&mut fadt, GuestAddress(l.fadt_addr))
.unwrap();
let flags = u32::from_le_bytes(fadt[112..116].try_into().unwrap());
assert_eq!(flags & (1 << 20), 0, "HW_REDUCED_ACPI must not be set");
assert_ne!(flags & FADT_F_PWR_BUTTON, 0);
assert_ne!(flags & FADT_F_SLP_BUTTON, 0);
}
#[test]
fn fadt_minor_version() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut fadt = [0u8; 276];
mem.read_slice(&mut fadt, GuestAddress(l.fadt_addr))
.unwrap();
assert_eq!(fadt[131], 5);
}
#[test]
fn fadt_dsdt_pointers() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut fadt = [0u8; 276];
mem.read_slice(&mut fadt, GuestAddress(l.fadt_addr))
.unwrap();
assert_eq!(
u32::from_le_bytes(fadt[40..44].try_into().unwrap()),
l.dsdt_addr as u32
);
assert_eq!(
u64::from_le_bytes(fadt[140..148].try_into().unwrap()),
l.dsdt_addr
);
}
#[test]
fn dsdt_signature_and_checksum() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut dsdt = [0u8; 36];
mem.read_slice(&mut dsdt, GuestAddress(l.dsdt_addr))
.unwrap();
assert_eq!(&dsdt[..4], b"DSDT");
assert_eq!(u32::from_le_bytes(dsdt[4..8].try_into().unwrap()), 36);
let sum: u8 = dsdt.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(sum, 0, "DSDT checksum must be zero");
}
#[test]
fn rsdp_points_to_xsdt() {
let mem = test_mem(16);
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let mut rsdp = [0u8; 36];
mem.read_slice(&mut rsdp, GuestAddress(l.rsdp_addr))
.unwrap();
assert_eq!(
u64::from_le_bytes(rsdp[24..32].try_into().unwrap()),
l.xsdt_addr
);
}
fn walk_srat_entries(srat: &[u8]) -> Vec<(u8, u8, &[u8])> {
let hdr_size = 48; let mut entries = Vec::new();
let mut offset = hdr_size;
while offset < srat.len() {
let entry_type = srat[offset];
let entry_len = srat[offset + 1] as usize;
entries.push((
entry_type,
entry_len as u8,
&srat[offset..offset + entry_len],
));
offset += entry_len;
}
entries
}
#[test]
fn srat_cpu_affinity_multi_numa() {
for (numa_nodes, llcs, cores, threads) in
[(2, 4, 2, 1), (2, 4, 2, 2), (4, 8, 1, 1), (3, 6, 2, 2)]
{
let mem = test_mem(16);
let topo = Topology {
llcs,
cores_per_llc: cores,
threads_per_core: threads,
numa_nodes,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let srat = read_table(&mem, l.srat_addr);
let entries = walk_srat_entries(&srat);
let mut cpu_idx = 0u32;
for (entry_type, _, data) in &entries {
if *entry_type == 2 {
let prox_domain = u32::from_le_bytes(data[4..8].try_into().unwrap());
let (llc_id, _, _) = topo.decompose(cpu_idx);
let expected_node = topo.numa_node_of(llc_id);
assert_eq!(
prox_domain, expected_node,
"cpu {cpu_idx}: proximity_domain {prox_domain} != expected {expected_node} \
(topo: {numa_nodes}n/{llcs}l/{cores}c/{threads}t)"
);
let x2apic = u32::from_le_bytes(data[8..12].try_into().unwrap());
assert_eq!(
x2apic,
apic_id(&topo, cpu_idx),
"cpu {cpu_idx}: x2apic_id mismatch"
);
cpu_idx += 1;
}
}
assert_eq!(cpu_idx, topo.total_cpus());
}
}
#[test]
fn srat_memory_split_multi_numa() {
for (numa_nodes, llcs) in [(2, 4), (3, 6), (4, 8)] {
let mem = test_mem(16);
let topo = Topology {
llcs,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes,
nodes: None,
distances: None,
};
let mem_bytes = 256u64 << 20;
let l = test_setup(&mem, &topo, 256);
let srat = read_table(&mem, l.srat_addr);
let entries = walk_srat_entries(&srat);
let mem_entries: Vec<_> = entries.iter().filter(|(t, _, _)| *t == 1).collect();
assert_eq!(mem_entries.len(), numa_nodes as usize);
let mut prev_end: u64 = 0;
let mut total: u64 = 0;
for (i, (_, _, data)) in mem_entries.iter().enumerate() {
let prox_domain = u32::from_le_bytes(data[2..6].try_into().unwrap());
assert_eq!(
prox_domain, i as u32,
"node {i}: proximity_domain {prox_domain} != {i} \
(topo: {numa_nodes}n/{llcs}l)"
);
let base = u64::from_le_bytes(data[8..16].try_into().unwrap());
let length = u64::from_le_bytes(data[16..24].try_into().unwrap());
assert_eq!(
base, prev_end,
"node {i}: base {base:#x} != prev_end {prev_end:#x} \
(topo: {numa_nodes}n/{llcs}l)"
);
assert!(length > 0, "node {i}: zero-length memory region");
prev_end = base + length;
total += length;
}
assert_eq!(
total, mem_bytes,
"total memory mismatch for {numa_nodes}n/{llcs}l"
);
}
}
#[test]
fn slit_distance_matrix_multi_numa() {
for (numa_nodes, llcs) in [(2, 2), (3, 3), (4, 4), (2, 4), (2, 6), (3, 9)] {
let mem = test_mem(16);
let topo = Topology {
llcs,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let slit = read_table(&mem, l.slit_addr);
assert_eq!(&slit[..4], b"SLIT", "SLIT signature mismatch");
let n = u64::from_le_bytes(slit[36..44].try_into().unwrap());
assert_eq!(n, numa_nodes as u64);
let matrix_start = 44;
for i in 0..n {
for j in 0..n {
let dist = slit[matrix_start + (i * n + j) as usize];
if i == j {
assert_eq!(dist, 10, "diagonal ({i},{j}) != 10");
} else {
assert_eq!(dist, 20, "off-diagonal ({i},{j}) != 20");
}
}
}
}
}
#[test]
fn srat_slit_checksum_multi_numa() {
for (numa_nodes, llcs, cores, threads) in
[(2, 2, 2, 1), (2, 4, 2, 2), (3, 3, 1, 1), (4, 8, 4, 2)]
{
let mem = test_mem(16);
let topo = Topology {
llcs,
cores_per_llc: cores,
threads_per_core: threads,
numa_nodes,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let srat = read_table(&mem, l.srat_addr);
let srat_sum: u8 = srat.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(
srat_sum, 0,
"SRAT checksum failed for {numa_nodes}n/{llcs}l/{cores}c/{threads}t"
);
let slit = read_table(&mem, l.slit_addr);
let slit_sum: u8 = slit.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(
slit_sum, 0,
"SLIT checksum failed for {numa_nodes}n/{llcs}l/{cores}c/{threads}t"
);
}
}
#[test]
fn srat_memory_split_remainder() {
let memory_mib = 257u32;
let mem = test_mem(memory_mib);
let topo = Topology {
llcs: 3,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 3,
nodes: None,
distances: None,
};
let mem_bytes = (memory_mib as u64) << 20;
let first_two = 86u64 << 20; let last_bytes = 85u64 << 20; let l = test_setup(&mem, &topo, memory_mib);
let srat = read_table(&mem, l.srat_addr);
let entries = walk_srat_entries(&srat);
let mem_entries: Vec<_> = entries.iter().filter(|(t, _, _)| *t == 1).collect();
assert_eq!(mem_entries.len(), 3);
let mut total: u64 = 0;
for (i, (_, _, data)) in mem_entries.iter().enumerate() {
let length = u64::from_le_bytes(data[16..24].try_into().unwrap());
if i < 2 {
assert_eq!(
length, first_two,
"node {i}: expected {first_two}, got {length}"
);
} else {
assert_eq!(
length, last_bytes,
"last node: expected {last_bytes}, got {length}"
);
assert!(
length < first_two,
"last node should be smaller — it absorbs the \
sub-2 MiB tail while the first nodes get the extra \
hugepages"
);
}
total += length;
}
assert_eq!(total, mem_bytes);
}
#[test]
fn srat_total_memory() {
let mem = test_mem(16);
let topo = Topology {
llcs: 2,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
let srat = read_table(&mem, l.srat_addr);
let entries = walk_srat_entries(&srat);
let total_mem: u64 = entries
.iter()
.filter(|(t, _, _)| *t == 1)
.map(|(_, _, data)| u64::from_le_bytes(data[16..24].try_into().unwrap()))
.sum();
let expected = 256u64 << 20;
assert_eq!(total_mem, expected);
}
use crate::vmm::topology::NumaNode;
static CXL_NODES: [NumaNode; 3] = [
NumaNode::new(2, 256),
NumaNode::new(2, 256),
NumaNode::new(0, 128),
];
#[test]
fn hmat_not_emitted_single_node() {
let mem = test_mem(16);
let topo = Topology {
llcs: 2,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
assert_eq!(l.hmat_size, 0, "HMAT must not be emitted for single-node");
}
#[test]
fn hmat_emitted_multi_numa_without_cxl() {
let mem = test_mem(16);
let topo = Topology {
llcs: 4,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 2,
nodes: None,
distances: None,
};
let l = test_setup(&mem, &topo, 256);
assert!(l.hmat_size > 0, "HMAT must be emitted for multi-NUMA");
}
#[test]
fn hmat_emitted_with_cxl() {
let topo = Topology::with_nodes(4, 1, &CXL_NODES);
let mem = test_mem(16);
let layout = NumaMemoryLayout::compute(&topo, 640, 0, None).unwrap();
let l = setup_acpi(&mem, &topo, &layout).unwrap();
assert!(l.hmat_size > 0, "HMAT must be emitted with CXL nodes");
}
#[test]
fn hmat_checksum() {
let topo = Topology::with_nodes(4, 1, &CXL_NODES);
let mem = test_mem(16);
let layout = NumaMemoryLayout::compute(&topo, 640, 0, None).unwrap();
let l = setup_acpi(&mem, &topo, &layout).unwrap();
let hmat = read_table(&mem, l.hmat_addr);
let sum: u8 = hmat.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(sum, 0, "HMAT checksum must be zero");
}
#[test]
fn hmat_header_fields() {
let topo = Topology::with_nodes(4, 1, &CXL_NODES);
let mem = test_mem(16);
let layout = NumaMemoryLayout::compute(&topo, 640, 0, None).unwrap();
let l = setup_acpi(&mem, &topo, &layout).unwrap();
let hmat = read_table(&mem, l.hmat_addr);
assert_eq!(&hmat[..4], b"HMAT");
assert_eq!(hmat[8], 2, "HMAT revision must be 2");
assert_eq!(
&hmat[36..40],
&[0, 0, 0, 0],
"4 reserved bytes after SDT header"
);
}
#[test]
fn hmat_mpda_count_and_flags() {
let topo = Topology::with_nodes(4, 1, &CXL_NODES);
let mem = test_mem(16);
let layout = NumaMemoryLayout::compute(&topo, 640, 0, None).unwrap();
let l = setup_acpi(&mem, &topo, &layout).unwrap();
let hmat = read_table(&mem, l.hmat_addr);
let num_targets = layout.regions().len();
let mut offset = 40;
let mut mpda_count = 0;
while offset < hmat.len() {
let hmat_type = u16::from_le_bytes(hmat[offset..offset + 2].try_into().unwrap());
if hmat_type != 0 {
break;
}
let length = u32::from_le_bytes(hmat[offset + 4..offset + 8].try_into().unwrap());
assert_eq!(length, 40, "MPDA length must be 40");
let flags = u16::from_le_bytes(hmat[offset + 8..offset + 10].try_into().unwrap());
assert_eq!(
flags, 3,
"MPDA flags must be 3 (PROCESSOR_PD_VALID | MEMORY_PD_VALID)"
);
mpda_count += 1;
offset += length as usize;
}
assert_eq!(
mpda_count, num_targets,
"one MPDA per memory target (layout region)"
);
}
#[test]
fn hmat_mpda_cxl_initiator() {
let topo = Topology::with_nodes(4, 1, &CXL_NODES);
let mem = test_mem(16);
let layout = NumaMemoryLayout::compute(&topo, 640, 0, None).unwrap();
let l = setup_acpi(&mem, &topo, &layout).unwrap();
let hmat = read_table(&mem, l.hmat_addr);
let mut offset = 40;
for region in layout.regions() {
let hmat_type = u16::from_le_bytes(hmat[offset..offset + 2].try_into().unwrap());
assert_eq!(hmat_type, 0);
let initiator = u32::from_le_bytes(hmat[offset + 12..offset + 16].try_into().unwrap());
let memory_pd = u32::from_le_bytes(hmat[offset + 16..offset + 20].try_into().unwrap());
assert_eq!(memory_pd, region.node_id);
if topo.llcs_in_node(region.node_id) > 0 {
assert_eq!(
initiator, region.node_id,
"CPU-bearing node {}: initiator must be self",
region.node_id
);
} else {
assert_ne!(
topo.llcs_in_node(initiator),
0,
"CXL node {}: initiator {} must be CPU-bearing",
region.node_id,
initiator
);
}
offset += 40;
}
}
#[test]
fn hmat_sllbi_latency_and_bandwidth() {
let topo = Topology::with_nodes(4, 1, &CXL_NODES);
let mem = test_mem(16);
let layout = NumaMemoryLayout::compute(&topo, 640, 0, None).unwrap();
let l = setup_acpi(&mem, &topo, &layout).unwrap();
let hmat = read_table(&mem, l.hmat_addr);
let num_targets = layout.regions().len();
let mut offset = 40 + 40 * num_targets;
for expected_data_type in [0u8, 3u8] {
let hmat_type = u16::from_le_bytes(hmat[offset..offset + 2].try_into().unwrap());
assert_eq!(hmat_type, 1, "SLLBI type must be 1");
let length = u32::from_le_bytes(hmat[offset + 4..offset + 8].try_into().unwrap());
let data_type = hmat[offset + 9];
assert_eq!(data_type, expected_data_type);
let ni = u32::from_le_bytes(hmat[offset + 12..offset + 16].try_into().unwrap());
let nt = u32::from_le_bytes(hmat[offset + 16..offset + 20].try_into().unwrap());
assert_eq!(ni, topo.cpu_bearing_nodes());
assert_eq!(nt, num_targets as u32);
let base_unit = u64::from_le_bytes(hmat[offset + 24..offset + 32].try_into().unwrap());
if data_type == 0 {
assert_eq!(base_unit, 100_000, "latency base must be 100000 ps");
} else {
assert_eq!(base_unit, 10_240, "bandwidth base must be 10240 MB/s");
}
offset += length as usize;
}
}
#[test]
fn hmat_sllbi_cxl_entries_differ() {
let topo = Topology::with_nodes(4, 1, &CXL_NODES);
let mem = test_mem(16);
let layout = NumaMemoryLayout::compute(&topo, 640, 0, None).unwrap();
let l = setup_acpi(&mem, &topo, &layout).unwrap();
let hmat = read_table(&mem, l.hmat_addr);
let num_targets = layout.regions().len();
let ni = topo.cpu_bearing_nodes() as usize;
let nt = num_targets;
let sllbi_offset = 40 + 40 * nt;
let matrix_offset = sllbi_offset + 32 + 4 * ni + 4 * nt;
let dram_entry = u16::from_le_bytes(hmat[matrix_offset..matrix_offset + 2].try_into().unwrap());
let cxl_entry = u16::from_le_bytes(
hmat[matrix_offset + 2 * (nt - 1)..matrix_offset + 2 * nt]
.try_into()
.unwrap(),
);
assert_eq!(dram_entry, 1, "local DRAM latency entry must be 1");
assert_eq!(
cxl_entry, 6,
"remote CXL latency entry must be 6 (distance-scaled)"
);
}
#[test]
fn hmat_rsdt_xsdt_include_pointer() {
let topo = Topology::with_nodes(4, 1, &CXL_NODES);
let mem = test_mem(16);
let layout = NumaMemoryLayout::compute(&topo, 640, 0, None).unwrap();
let l = setup_acpi(&mem, &topo, &layout).unwrap();
let rsdt = read_table(&mem, l.rsdt_addr);
let rsdt_entries = (rsdt.len() - 36) / 4;
assert_eq!(rsdt_entries, 5, "RSDT must have 5 table pointers with HMAT");
let hmat_ptr = u32::from_le_bytes(rsdt[36 + 16..36 + 20].try_into().unwrap());
assert_eq!(hmat_ptr, l.hmat_addr as u32);
let xsdt = read_table(&mem, l.xsdt_addr);
let xsdt_entries = (xsdt.len() - 36) / 8;
assert_eq!(xsdt_entries, 5, "XSDT must have 5 table pointers with HMAT");
let hmat_ptr64 = u64::from_le_bytes(xsdt[36 + 32..36 + 40].try_into().unwrap());
assert_eq!(hmat_ptr64, l.hmat_addr);
}
#[test]
fn no_hmat_rsdt_has_4_entries() {
let mem = test_mem(16);
let topo = Topology::new(1, 2, 2, 1);
let l = test_setup(&mem, &topo, 256);
let rsdt = read_table(&mem, l.rsdt_addr);
let rsdt_entries = (rsdt.len() - 36) / 4;
assert_eq!(
rsdt_entries, 4,
"RSDT must have 4 table pointers without HMAT"
);
}