use kvm_bindings::kvm_cpuid_entry2;
use crate::vmm::topology::Topology;
const PMU_ARCH_PERFMON_VERSION: u32 = 2;
const PMU_NUM_GP_COUNTERS: u32 = 4;
const PMU_GP_COUNTER_WIDTH: u32 = 48;
const PMU_EVENT_MASK_LENGTH: u32 = 7;
const PMU_NUM_FIXED_COUNTERS: u32 = 3;
const PMU_FIXED_COUNTER_WIDTH: u32 = 48;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CpuVendor {
Intel,
Amd,
Unknown,
}
fn detect_vendor(entries: &[kvm_cpuid_entry2]) -> CpuVendor {
let leaf0 = entries.iter().find(|e| e.function == 0 && e.index == 0);
match leaf0 {
Some(e) => {
match (e.ebx, e.edx, e.ecx) {
(0x756e_6547, 0x4965_6e69, 0x6c65_746e) => CpuVendor::Intel,
(0x6874_7541, 0x6974_6e65, 0x444d_4163) => CpuVendor::Amd,
_ => CpuVendor::Unknown,
}
}
None => CpuVendor::Unknown,
}
}
fn bits_needed(n: u32) -> u32 {
if n <= 1 {
return 0;
}
32 - (n - 1).leading_zeros()
}
pub fn apic_id(topo: &Topology, cpu_id: u32) -> u32 {
let (llc_id, core_id, thread_id) = topo.decompose(cpu_id);
let thread_bits = bits_needed(topo.threads_per_core);
let core_bits = bits_needed(topo.cores_per_llc);
(llc_id << (core_bits + thread_bits)) | (core_id << thread_bits) | thread_id
}
pub fn max_apic_id(topo: &Topology) -> u32 {
let total = topo.total_cpus();
if total == 0 {
return 0;
}
apic_id(topo, total - 1)
}
pub fn smt_shift(topo: &Topology) -> u32 {
bits_needed(topo.threads_per_core)
}
pub fn core_shift(topo: &Topology) -> u32 {
bits_needed(topo.threads_per_core) + bits_needed(topo.cores_per_llc)
}
fn patch_cache_topology_eax(entry: &mut kvm_cpuid_entry2, smt: u32, core: u32, cores_per_llc: u32) {
let cache_level = (entry.eax >> 5) & 0x7;
let max_sharing = match cache_level {
1 | 2 => (1u32 << smt).saturating_sub(1),
3 => (1u32 << core).saturating_sub(1),
_ => 0,
};
entry.eax = (entry.eax & 0xfc003fff) | ((max_sharing & 0xfff) << 14);
let core_bits = bits_needed(cores_per_llc);
let max_core_ids = (1u32 << core_bits).saturating_sub(1);
entry.eax = (entry.eax & 0x03ffffff) | ((max_core_ids & 0x3f) << 26);
}
pub fn generate_cpuid(
base_cpuid: &[kvm_cpuid_entry2],
topo: &Topology,
cpu_id: u32,
performance_mode: bool,
) -> Vec<kvm_cpuid_entry2> {
let mut entries: Vec<kvm_cpuid_entry2> = base_cpuid.to_vec();
let vendor = detect_vendor(&entries);
let apic = apic_id(topo, cpu_id);
let smt = smt_shift(topo);
let core = core_shift(topo);
let threads_per_llc = topo.cores_per_llc * topo.threads_per_core;
for entry in entries.iter_mut() {
match entry.function {
0x1 => {
entry.ebx = (entry.ebx & 0x00ffffff) | ((apic & 0xff) << 24);
entry.ebx = (entry.ebx & 0xff00ffff) | ((threads_per_llc.min(255)) << 16);
if threads_per_llc > 1 {
entry.edx |= 1 << 28;
}
}
0x4 if vendor == CpuVendor::Intel => {
patch_cache_topology_eax(entry, smt, core, topo.cores_per_llc);
}
0xb | 0x1f => match entry.index {
0 => {
entry.eax = smt;
entry.ebx = topo.threads_per_core & 0xffff;
entry.ecx = (1 << 8) | (entry.index & 0xff); entry.edx = apic;
}
1 => {
entry.eax = core;
entry.ebx = threads_per_llc & 0xffff;
entry.ecx = (2 << 8) | (entry.index & 0xff); entry.edx = apic;
}
_ => {
entry.eax = 0;
entry.ebx = 0;
entry.ecx = entry.index & 0xff; entry.edx = apic;
}
},
0x8000_001d if vendor == CpuVendor::Amd => {
patch_cache_topology_eax(entry, smt, core, topo.cores_per_llc);
}
0xa => {
if entry.eax & 0xff != 0 {
entry.eax = PMU_ARCH_PERFMON_VERSION
| (PMU_NUM_GP_COUNTERS << 8)
| (PMU_GP_COUNTER_WIDTH << 16)
| (PMU_EVENT_MASK_LENGTH << 24);
entry.ebx = 0;
entry.ecx = 0;
entry.edx = PMU_NUM_FIXED_COUNTERS | (PMU_FIXED_COUNTER_WIDTH << 5);
}
}
0x8000_0001 if vendor == CpuVendor::Amd && threads_per_llc > 1 => {
entry.ecx |= (1 << 1) | (1 << 22);
}
0x8000_0008 => {
if threads_per_llc > 1 {
let apic_id_size = core;
entry.ecx = (apic_id_size << 12) | (threads_per_llc - 1);
} else {
entry.ecx = 0;
}
}
0x8000_001e if vendor == CpuVendor::Amd => {
entry.eax = apic;
let (llc_id, core_id, _) = topo.decompose(cpu_id);
entry.ebx = ((topo.threads_per_core - 1) << 8) | (core_id & 0xff);
let node_id = topo.numa_node_of(llc_id);
entry.ecx = node_id | ((topo.numa_nodes - 1) << 8);
entry.edx = 0;
}
_ => {}
}
}
if !entries.iter().any(|e| e.function == 0x4000_0000) {
entries.push(kvm_cpuid_entry2 {
function: 0x4000_0000,
index: 0,
flags: 0,
eax: 0x4000_0001, ebx: 0x4b56_4d4b, ecx: 0x564b_4d56, edx: 0x0000_004d, ..Default::default()
});
}
if performance_mode {
if let Some(entry) = entries.iter_mut().find(|e| e.function == 0x4000_0001) {
entry.edx |= 1;
}
if let Some(entry) = entries.iter_mut().find(|e| e.function == 0x4000_0000) {
entry.eax = entry.eax.max(0x4000_0001);
}
}
entries
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bits_needed_values() {
assert_eq!(bits_needed(1), 0);
assert_eq!(bits_needed(2), 1);
assert_eq!(bits_needed(3), 2);
assert_eq!(bits_needed(4), 2);
assert_eq!(bits_needed(5), 3);
assert_eq!(bits_needed(8), 3);
assert_eq!(bits_needed(9), 4);
assert_eq!(bits_needed(16), 4);
}
fn make_cache_entry(level: u32) -> kvm_cpuid_entry2 {
kvm_cpuid_entry2 {
function: 0x4, index: 0,
flags: 0,
eax: (level << 5) | 2 | (0xfff << 14) | (0x3f << 26),
ebx: 0,
ecx: 0,
edx: 0,
..Default::default()
}
}
#[test]
fn patch_cache_l1_smt_scoped() {
let mut entry = make_cache_entry(1);
patch_cache_topology_eax(&mut entry, 1, 3, 4);
let sharing = (entry.eax >> 14) & 0xfff;
assert_eq!(sharing, 1, "L1 sharing should be SMT-scoped");
}
#[test]
fn patch_cache_l2_smt_scoped() {
let mut entry = make_cache_entry(2);
patch_cache_topology_eax(&mut entry, 1, 3, 4);
let sharing = (entry.eax >> 14) & 0xfff;
assert_eq!(sharing, 1, "L2 sharing should be SMT-scoped");
}
#[test]
fn patch_cache_l3_llc_scoped() {
let mut entry = make_cache_entry(3);
patch_cache_topology_eax(&mut entry, 1, 3, 4);
let sharing = (entry.eax >> 14) & 0xfff;
assert_eq!(sharing, 7, "L3 sharing should be LLC-scoped");
}
#[test]
fn patch_cache_unknown_level_zero() {
let mut entry0 = make_cache_entry(0);
patch_cache_topology_eax(&mut entry0, 1, 3, 4);
assert_eq!((entry0.eax >> 14) & 0xfff, 0, "level 0 sharing should be 0");
let mut entry4 = make_cache_entry(4);
patch_cache_topology_eax(&mut entry4, 1, 3, 4);
assert_eq!((entry4.eax >> 14) & 0xfff, 0, "level 4 sharing should be 0");
let mut entry7 = make_cache_entry(7);
patch_cache_topology_eax(&mut entry7, 1, 3, 4);
assert_eq!((entry7.eax >> 14) & 0xfff, 0, "level 7 sharing should be 0");
}
#[test]
fn patch_cache_single_core_single_thread() {
let mut entry_l1 = make_cache_entry(1);
patch_cache_topology_eax(&mut entry_l1, 0, 0, 1);
assert_eq!(
(entry_l1.eax >> 14) & 0xfff,
0,
"1c/1t L1 sharing should be 0"
);
assert_eq!((entry_l1.eax >> 26) & 0x3f, 0, "1c/1t core IDs should be 0");
let mut entry_l3 = make_cache_entry(3);
patch_cache_topology_eax(&mut entry_l3, 0, 0, 1);
assert_eq!(
(entry_l3.eax >> 14) & 0xfff,
0,
"1c/1t L3 sharing should be 0"
);
}
#[test]
fn patch_cache_large_topology() {
let smt = bits_needed(2); let core = smt + bits_needed(16); assert_eq!(smt, 1);
assert_eq!(core, 5);
let mut entry_l3 = make_cache_entry(3);
patch_cache_topology_eax(&mut entry_l3, smt, core, 16);
let sharing = (entry_l3.eax >> 14) & 0xfff;
assert_eq!(sharing, 31, "16c/2t L3 sharing");
let core_ids = (entry_l3.eax >> 26) & 0x3f;
assert_eq!(core_ids, 15, "16c/2t core IDs");
assert!(sharing < (1 << 12));
assert!(core_ids < (1 << 6));
let mut entry_l1 = make_cache_entry(1);
patch_cache_topology_eax(&mut entry_l1, smt, core, 16);
assert_eq!(
(entry_l1.eax >> 14) & 0xfff,
1,
"16c/2t L1 sharing (SMT-scoped)"
);
}
#[test]
fn patch_cache_preserves_lower_bits() {
let mut entry = kvm_cpuid_entry2 {
eax: (3 << 5) | 0b10101 | (0x2a << 8), ..Default::default()
};
patch_cache_topology_eax(&mut entry, 1, 3, 4);
assert_eq!(entry.eax & 0x1f, 0b10101, "type bits [4:0] preserved");
assert_eq!((entry.eax >> 5) & 0x7, 3, "level bits [7:5] preserved");
assert_eq!((entry.eax >> 8) & 0x3f, 0x2a, "bits [13:8] preserved");
}
#[test]
fn patch_cache_leaf4_and_8000001d_identical() {
let topos = [(1, 1, 1), (2, 4, 1), (2, 4, 2), (4, 8, 2), (8, 16, 2)];
for (llcs, cores, threads) in topos {
let smt = bits_needed(threads);
let core = smt + bits_needed(cores);
for level in 1..=3 {
let mut leaf4 = make_cache_entry(level);
leaf4.function = 0x4;
let mut leaf_amd = make_cache_entry(level);
leaf_amd.function = 0x8000_001d;
patch_cache_topology_eax(&mut leaf4, smt, core, cores);
patch_cache_topology_eax(&mut leaf_amd, smt, core, cores);
assert_eq!(
leaf4.eax, leaf_amd.eax,
"{llcs}l/{cores}c/{threads}t L{level}: leaf 0x4 and 0x8000001D \
EAX should be identical"
);
}
}
}
#[test]
fn apic_ids_unique() {
let t = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let ids: Vec<u32> = (0..t.total_cpus()).map(|i| apic_id(&t, i)).collect();
let unique: std::collections::HashSet<u32> = ids.iter().copied().collect();
assert_eq!(ids.len(), unique.len(), "APIC IDs must be unique: {ids:?}");
}
#[test]
fn apic_ids_smt_siblings_adjacent() {
let t = Topology {
llcs: 2,
cores_per_llc: 2,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let smt_mask = (1u32 << smt_shift(&t)) - 1;
for core_start in (0..t.total_cpus()).step_by(t.threads_per_core as usize) {
let base = apic_id(&t, core_start) & !smt_mask;
for thread in 0..t.threads_per_core {
let apic = apic_id(&t, core_start + thread);
assert_eq!(
apic & !smt_mask,
base,
"SMT siblings should share upper bits: cpu {}, apic {apic:#x}",
core_start + thread
);
}
}
}
#[test]
fn apic_ids_same_llc_share_upper_bits() {
let t = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let pkg_mask = !((1u32 << core_shift(&t)) - 1);
let cpus_per_llc = t.cores_per_llc * t.threads_per_core;
for llc in 0..t.llcs {
let start = llc * cpus_per_llc;
let llc_bits = apic_id(&t, start) & pkg_mask;
for cpu in start..start + cpus_per_llc {
assert_eq!(
apic_id(&t, cpu) & pkg_mask,
llc_bits,
"CPU {cpu} should be in LLC {llc}"
);
}
}
let s0 = apic_id(&t, 0) & pkg_mask;
let s1 = apic_id(&t, cpus_per_llc) & pkg_mask;
assert_ne!(s0, s1, "different LLCs should have different package IDs");
}
#[test]
fn smt_shift_values() {
assert_eq!(
smt_shift(&Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
}),
0
);
assert_eq!(
smt_shift(&Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
}),
1
);
assert_eq!(
smt_shift(&Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 4,
numa_nodes: 1,
nodes: None,
distances: None,
}),
2
);
}
#[test]
fn core_shift_values() {
assert_eq!(
core_shift(&Topology {
llcs: 1,
cores_per_llc: 4,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
}),
2
);
assert_eq!(
core_shift(&Topology {
llcs: 1,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
}),
3
);
}
#[test]
fn generate_cpuid_produces_entries() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return, };
let topo = Topology {
llcs: 2,
cores_per_llc: 2,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
assert!(!cpuid.is_empty());
let leaf1 = cpuid.iter().find(|e| e.function == 1);
if let Some(entry) = leaf1 {
let apic_from_cpuid = (entry.ebx >> 24) & 0xff;
assert_eq!(apic_from_cpuid, apic_id(&topo, 0));
}
}
#[test]
fn generate_cpuid_different_per_vcpu() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 2,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid0 = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let cpuid1 = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
1,
false,
);
let leaf_b_0 = cpuid0.iter().find(|e| e.function == 0xb && e.index == 0);
let leaf_b_1 = cpuid1.iter().find(|e| e.function == 0xb && e.index == 0);
if let (Some(e0), Some(e1)) = (leaf_b_0, leaf_b_1) {
assert_ne!(
e0.edx, e1.edx,
"different vCPUs should have different x2APIC IDs"
);
}
}
#[test]
fn topology_odd_counts() {
let t = Topology {
llcs: 3,
cores_per_llc: 3,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
assert_eq!(t.total_cpus(), 9);
let ids: Vec<u32> = (0..9).map(|i| apic_id(&t, i)).collect();
let unique: std::collections::HashSet<u32> = ids.iter().copied().collect();
assert_eq!(
unique.len(),
9,
"odd topology APIC IDs must be unique: {ids:?}"
);
}
#[test]
fn leaf1_threads_per_llc_not_total() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 4,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
assert_eq!(topo.total_cpus(), 32);
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf1 = cpuid.iter().find(|e| e.function == 1);
if let Some(entry) = leaf1 {
let threads_per_llc = (entry.ebx >> 16) & 0xff;
assert_eq!(
threads_per_llc,
8, "EBX[23:16] should be threads per LLC (8), not total CPUs (32)"
);
}
}
#[test]
fn apic_ids_unique_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), ];
for (llcs, cores, threads) in topos {
let t = Topology {
llcs,
cores_per_llc: cores,
threads_per_core: threads,
numa_nodes: 1,
nodes: None,
distances: None,
};
let ids: Vec<u32> = (0..t.total_cpus()).map(|i| apic_id(&t, i)).collect();
let unique: std::collections::HashSet<u32> = ids.iter().copied().collect();
assert_eq!(
ids.len(),
unique.len(),
"topology {llcs}l/{cores}c/{threads}t: APIC IDs not unique"
);
}
}
#[test]
fn leaf0b_subleaf0_ebx_is_threads_per_core() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf_b_0 = cpuid.iter().find(|e| e.function == 0xb && e.index == 0);
if let Some(entry) = leaf_b_0 {
assert_eq!(
entry.ebx & 0xffff,
2, "leaf 0xB subleaf 0 EBX should be threads per core"
);
assert_eq!(
entry.eax,
smt_shift(&topo),
"leaf 0xB subleaf 0 EAX should be smt_shift"
);
}
}
#[test]
fn leaf0b_subleaf1_ebx_is_threads_per_llc() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf_b_1 = cpuid.iter().find(|e| e.function == 0xb && e.index == 1);
if let Some(entry) = leaf_b_1 {
assert_eq!(
entry.ebx & 0xffff,
8, "leaf 0xB subleaf 1 EBX should be threads per LLC"
);
assert_eq!(
entry.eax,
core_shift(&topo),
"leaf 0xB subleaf 1 EAX should be core_shift"
);
}
}
#[test]
fn leaf0b_ecx_includes_subleaf_index() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 2,
cores_per_llc: 2,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
if let Some(entry) = cpuid.iter().find(|e| e.function == 0xb && e.index == 0) {
assert_eq!(entry.ecx & 0xff, 0, "subleaf 0 ECX[7:0] should be 0");
assert_eq!(
(entry.ecx >> 8) & 0xff,
1,
"subleaf 0 ECX[15:8] should be 1 (SMT)"
);
}
if let Some(entry) = cpuid.iter().find(|e| e.function == 0xb && e.index == 1) {
assert_eq!(entry.ecx & 0xff, 1, "subleaf 1 ECX[7:0] should be 1");
assert_eq!(
(entry.ecx >> 8) & 0xff,
2,
"subleaf 1 ECX[15:8] should be 2 (Core)"
);
}
}
#[test]
fn leaf4_l3_shared_within_llc() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let l3 = cpuid
.iter()
.find(|e| e.function == 0x4 && ((e.eax >> 5) & 0x7) == 3);
if let Some(entry) = l3 {
let max_sharing = ((entry.eax >> 14) & 0xfff) + 1;
let expected = 1u32 << core_shift(&topo); assert_eq!(max_sharing, expected, "L3 max sharing: APIC-ID-space value");
}
}
#[test]
fn leaf4_core_ids_apic_space() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 1,
cores_per_llc: 3,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf4 = cpuid
.iter()
.find(|e| e.function == 0x4 && ((e.eax >> 5) & 0x7) > 0);
if let Some(entry) = leaf4 {
let max_core_ids = ((entry.eax >> 26) & 0x3f) + 1;
let core_bits = bits_needed(topo.cores_per_llc);
assert_eq!(
max_core_ids,
1 << core_bits,
"leaf 0x4 EAX[31:26]+1 should be power-of-2 from APIC ID space"
);
}
}
#[test]
fn leaf1_hypervisor_bit_set() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf1 = cpuid.iter().find(|e| e.function == 1);
if let Some(entry) = leaf1 {
assert_ne!(
entry.ecx & (1 << 31),
0,
"hypervisor bit (ECX.31) should be set"
);
}
}
#[test]
fn leaf1_clflush_set() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf1 = cpuid.iter().find(|e| e.function == 1);
if let Some(entry) = leaf1 {
let clflush = (entry.ebx >> 8) & 0xff;
assert_eq!(clflush, 8, "CLFLUSH should be 8 (64-byte cache lines)");
}
}
#[test]
fn leaf_0xa_pmu_v2_synthesized() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf_a = cpuid.iter().find(|e| e.function == 0xa);
if let Some(entry) = leaf_a {
let version = entry.eax & 0xff;
if version == 0 {
return;
}
let num_gp = (entry.eax >> 8) & 0xff;
let gp_width = (entry.eax >> 16) & 0xff;
let mask_len = (entry.eax >> 24) & 0xff;
assert_eq!(version, PMU_ARCH_PERFMON_VERSION, "PMU v2 version");
assert_eq!(num_gp, PMU_NUM_GP_COUNTERS, "PMU v2 GP counter count");
assert_eq!(gp_width, PMU_GP_COUNTER_WIDTH, "PMU v2 GP counter width");
assert_eq!(
mask_len, PMU_EVENT_MASK_LENGTH,
"mask_length must equal ARCH_PERFMON_EVENTS_COUNT"
);
assert_eq!(entry.ebx, 0, "no architectural events disabled");
assert_eq!(entry.ecx, 0, "ECX reserved for PMU v2");
let num_fixed = entry.edx & 0x1f;
let fixed_width = (entry.edx >> 5) & 0xff;
assert_eq!(
num_fixed, PMU_NUM_FIXED_COUNTERS,
"PMU v2 fixed counter count"
);
assert_eq!(
fixed_width, PMU_FIXED_COUNTER_WIDTH,
"PMU v2 fixed counter width"
);
assert_eq!(
entry.edx & !0x1fff,
0,
"EDX bits[31:13] must be zero for PMU v2"
);
}
}
#[test]
fn leaf_0xa_absent_from_base_stays_absent() {
let base = vec![kvm_cpuid_entry2 {
function: 0,
index: 0,
flags: 0,
eax: 0,
ebx: 0x756e_6547,
edx: 0x4965_6e69,
ecx: 0x6c65_746e,
..Default::default()
}];
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(&base, &topo, 0, false);
assert!(
cpuid.iter().all(|e| e.function != 0xa),
"leaf 0xA must not be fabricated when absent from base"
);
}
#[test]
fn leaf_0xa_zero_version_preserved() {
let base = vec![
kvm_cpuid_entry2 {
function: 0,
index: 0,
flags: 0,
eax: 0,
ebx: 0x756e_6547,
edx: 0x4965_6e69,
ecx: 0x6c65_746e,
..Default::default()
},
kvm_cpuid_entry2 {
function: 0xa,
index: 0,
flags: 0,
eax: 0,
ebx: 0,
ecx: 0,
edx: 0,
..Default::default()
},
];
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(&base, &topo, 0, false);
let leaf_a = cpuid
.iter()
.find(|e| e.function == 0xa)
.expect("leaf 0xA preserved from base");
assert_eq!(leaf_a.eax, 0, "EAX must stay zero (host PMU disabled)");
assert_eq!(leaf_a.ebx, 0, "EBX must stay zero");
assert_eq!(leaf_a.ecx, 0, "ECX must stay zero");
assert_eq!(leaf_a.edx, 0, "EDX must stay zero");
}
#[test]
fn leaf_0xa_nonzero_version_synthesized_to_v2() {
let base = vec![
kvm_cpuid_entry2 {
function: 0,
index: 0,
flags: 0,
eax: 0,
ebx: 0x756e_6547,
edx: 0x4965_6e69,
ecx: 0x6c65_746e,
..Default::default()
},
kvm_cpuid_entry2 {
function: 0xa,
index: 0,
flags: 0,
eax: 5 | (8 << 8) | (56 << 16) | (10 << 24),
ebx: 0xdead_beef,
ecx: 0xcafe_f00d,
edx: 0xface_d00d,
..Default::default()
},
];
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(&base, &topo, 0, false);
let leaf_a = cpuid
.iter()
.find(|e| e.function == 0xa)
.expect("leaf 0xA present from base");
let expected_eax = PMU_ARCH_PERFMON_VERSION
| (PMU_NUM_GP_COUNTERS << 8)
| (PMU_GP_COUNTER_WIDTH << 16)
| (PMU_EVENT_MASK_LENGTH << 24);
let expected_edx = PMU_NUM_FIXED_COUNTERS | (PMU_FIXED_COUNTER_WIDTH << 5);
assert_eq!(leaf_a.eax, expected_eax, "EAX synthesized to PMU v2");
assert_eq!(leaf_a.ebx, 0, "EBX cleared (no events disabled)");
assert_eq!(leaf_a.ecx, 0, "ECX cleared (reserved)");
assert_eq!(leaf_a.edx, expected_edx, "EDX synthesized to PMU v2");
}
#[test]
fn leaf_0xa_amd_vendor_same_synthesis() {
let base = vec![
kvm_cpuid_entry2 {
function: 0,
index: 0,
flags: 0,
eax: 0,
ebx: 0x6874_7541, edx: 0x6974_6e65, ecx: 0x444d_4163, ..Default::default()
},
kvm_cpuid_entry2 {
function: 0xa,
index: 0,
flags: 0,
eax: 1 | (2 << 8) | (32 << 16) | (4 << 24),
ebx: 0x1111_1111,
ecx: 0x2222_2222,
edx: 0x3333_3333,
..Default::default()
},
];
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(&base, &topo, 0, false);
let leaf_a = cpuid
.iter()
.find(|e| e.function == 0xa)
.expect("leaf 0xA present from base");
let expected_eax = PMU_ARCH_PERFMON_VERSION
| (PMU_NUM_GP_COUNTERS << 8)
| (PMU_GP_COUNTER_WIDTH << 16)
| (PMU_EVENT_MASK_LENGTH << 24);
let expected_edx = PMU_NUM_FIXED_COUNTERS | (PMU_FIXED_COUNTER_WIDTH << 5);
assert_eq!(
leaf_a.eax, expected_eax,
"AMD synthesized identically to Intel"
);
assert_eq!(leaf_a.ebx, 0, "EBX cleared on AMD");
assert_eq!(leaf_a.ecx, 0, "ECX cleared on AMD");
assert_eq!(
leaf_a.edx, expected_edx,
"AMD synthesized identically to Intel"
);
}
#[test]
fn max_hypervisor_leaf_not_lowered_in_performance_mode() {
let base = vec![
kvm_cpuid_entry2 {
function: 0,
index: 0,
flags: 0,
eax: 0,
ebx: 0x756e_6547,
edx: 0x4965_6e69,
ecx: 0x6c65_746e,
..Default::default()
},
kvm_cpuid_entry2 {
function: 0x4000_0000,
index: 0,
flags: 0,
eax: 0x4000_0010, ebx: 0,
ecx: 0,
edx: 0,
..Default::default()
},
kvm_cpuid_entry2 {
function: 0x4000_0001,
index: 0,
flags: 0,
eax: 0,
ebx: 0,
ecx: 0,
edx: 0,
..Default::default()
},
];
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(&base, &topo, 0, true);
let leaf40 = cpuid
.iter()
.find(|e| e.function == 0x4000_0000)
.expect("leaf 0x40000000 present");
assert_eq!(
leaf40.eax, 0x4000_0010,
"max hypervisor leaf must not regress when already > 0x40000001"
);
}
#[test]
fn hypervisor_leaf_present() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf_40 = cpuid.iter().find(|e| e.function == 0x4000_0000);
assert!(leaf_40.is_some(), "hypervisor leaf 0x40000000 should exist");
}
#[test]
fn decompose_roundtrip_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), ];
for (llcs, cores, threads) in topos {
let t = Topology {
llcs,
cores_per_llc: cores,
threads_per_core: threads,
numa_nodes: 1,
nodes: None,
distances: None,
};
for cpu in 0..t.total_cpus() {
let (s, c, th) = t.decompose(cpu);
assert!(s < llcs, "cpu {cpu}: llc {s} >= {llcs}");
assert!(c < cores, "cpu {cpu}: core {c} >= {cores}");
assert!(th < threads, "cpu {cpu}: thread {th} >= {threads}");
let recomposed = s * cores * threads + c * threads + th;
assert_eq!(
recomposed, cpu,
"decompose roundtrip failed for {llcs}l/{cores}c/{threads}t cpu {cpu}"
);
}
}
}
#[test]
fn leaf_80000008_amd_topology() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf = cpuid.iter().find(|e| e.function == 0x8000_0008);
if let Some(entry) = leaf {
let nc = entry.ecx & 0xff;
assert_eq!(nc, 7, "NC should be threads_per_package - 1");
let apic_id_size = (entry.ecx >> 12) & 0xf;
assert_eq!(apic_id_size, core_shift(&topo), "APIC ID size = core_shift");
}
}
#[test]
fn leaf_8000001e_amd_extended_apic() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid0 = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf0 = cpuid0.iter().find(|e| e.function == 0x8000_001e);
if let Some(entry) = leaf0 {
assert_eq!(entry.eax, apic_id(&topo, 0), "EAX = extended APIC ID");
assert_eq!(entry.ebx & 0xff, 0, "core ID for cpu 0 should be 0");
assert_eq!(
(entry.ebx >> 8) & 0xff,
1,
"threads per core - 1 should be 1"
);
assert_eq!(entry.ecx & 0xff, 0, "single node: node ID = 0");
assert_eq!(
(entry.ecx >> 8) & 0x7,
0,
"single node: nodes per proc - 1 = 0"
);
assert_eq!(entry.edx, 0, "EDX reserved");
}
let cpuid3 = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
3,
false,
);
let leaf3 = cpuid3.iter().find(|e| e.function == 0x8000_001e);
if let Some(entry) = leaf3 {
assert_eq!(entry.eax, apic_id(&topo, 3), "EAX = extended APIC ID");
assert_eq!(entry.ebx & 0xff, 1, "core ID for cpu 3 should be 1");
}
}
#[test]
fn leaf_8000001e_multi_numa_node_id() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 4,
cores_per_llc: 2,
threads_per_core: 2,
numa_nodes: 2,
nodes: None,
distances: None,
};
let base = kvm
.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap();
let vendor = detect_vendor(base.as_slice());
if vendor != CpuVendor::Amd {
return;
}
let cpus_per_llc = topo.cores_per_llc * topo.threads_per_core;
let cpuid0 = generate_cpuid(base.as_slice(), &topo, 0, false);
let leaf0 = cpuid0.iter().find(|e| e.function == 0x8000_001e);
if let Some(entry) = leaf0 {
assert_eq!(entry.ecx & 0xff, 0, "cpu 0: node ID = 0");
assert_eq!((entry.ecx >> 8) & 0x7, 1, "nodes per processor - 1 = 1");
}
let cpu_in_node1 = 2 * cpus_per_llc;
let cpuid1 = generate_cpuid(base.as_slice(), &topo, cpu_in_node1, false);
let leaf1 = cpuid1.iter().find(|e| e.function == 0x8000_001e);
if let Some(entry) = leaf1 {
assert_eq!(entry.ecx & 0xff, 1, "cpu {cpu_in_node1}: node ID = 1");
assert_eq!((entry.ecx >> 8) & 0x7, 1, "nodes per processor - 1 = 1");
}
}
#[test]
fn leaf_80000008_single_cpu() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf = cpuid.iter().find(|e| e.function == 0x8000_0008);
if let Some(entry) = leaf {
assert_eq!(entry.ecx & 0xff, 0, "single CPU: NC = 0");
assert_eq!((entry.ecx >> 12) & 0xf, 0, "single CPU: ApicIdSize = 0");
}
}
#[test]
fn leaf1f_matches_leaf0b() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
for sub in 0..2 {
let leaf_b = cpuid.iter().find(|e| e.function == 0xb && e.index == sub);
let leaf_1f = cpuid.iter().find(|e| e.function == 0x1f && e.index == sub);
if let (Some(b), Some(f)) = (leaf_b, leaf_1f) {
assert_eq!(b.eax, f.eax, "subleaf {sub}: EAX should match");
assert_eq!(b.ebx, f.ebx, "subleaf {sub}: EBX should match");
assert_eq!(b.edx, f.edx, "subleaf {sub}: EDX should match");
assert_eq!(b.ecx, f.ecx, "subleaf {sub}: ECX should match");
}
}
}
#[test]
fn leaf1_htt_not_set_for_single_cpu() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf1 = cpuid.iter().find(|e| e.function == 1);
if let Some(entry) = leaf1 {
let threads_per_pkg = (entry.ebx >> 16) & 0xff;
assert_eq!(threads_per_pkg, 1, "single CPU: threads per pkg = 1");
}
}
#[test]
fn leaf_80000001_cmplegacy_and_topoext_multi_cpu() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf = cpuid.iter().find(|e| e.function == 0x8000_0001);
if let Some(entry) = leaf {
assert_ne!(entry.ecx & (1 << 1), 0, "CmpLegacy (bit 1) should be set");
assert_ne!(
entry.ecx & (1 << 22),
0,
"TopologyExtensions (bit 22) should be set"
);
}
}
#[test]
fn leaf_80000001_not_set_for_single_cpu() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf = cpuid.iter().find(|e| e.function == 0x8000_0001);
if let Some(entry) = leaf {
let our_bits = (1u32 << 1) | (1u32 << 22);
let host_cpuid = kvm
.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.expect("get_supported_cpuid");
let host_leaf = host_cpuid
.as_slice()
.iter()
.find(|e| e.function == 0x8000_0001);
if let Some(host_entry) = host_leaf {
let added = entry.ecx & !host_entry.ecx;
assert_eq!(
added & our_bits,
0,
"single CPU: should not add CmpLegacy or TopologyExtensions"
);
}
}
}
#[test]
fn leaf_80000008_apic_id_size_representative() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
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), (1, 64, 1), (1, 18, 1), ];
for (llcs, cores, threads) in topos {
let topo = Topology {
llcs,
cores_per_llc: cores,
threads_per_core: threads,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf = cpuid.iter().find(|e| e.function == 0x8000_0008);
if let Some(entry) = leaf {
let threads_per_llc = cores * threads;
let apic_id_size = (entry.ecx >> 12) & 0xf;
let nc = entry.ecx & 0xff;
if threads_per_llc > 1 {
assert!(
(1u32 << apic_id_size) >= threads_per_llc,
"{llcs}l/{cores}c/{threads}t: ApicIdSize {apic_id_size} too small \
for {threads_per_llc} threads (2^{apic_id_size} = {})",
1u32 << apic_id_size
);
assert_eq!(
nc,
threads_per_llc - 1,
"{llcs}l/{cores}c/{threads}t: NC should be threads_per_llc - 1"
);
} else {
assert_eq!(
entry.ecx & 0xf0ff,
0,
"{llcs}l/{cores}c/{threads}t: single CPU ECX should be 0"
);
}
}
}
}
#[test]
fn detect_vendor_intel() {
let entries = [kvm_cpuid_entry2 {
function: 0,
index: 0,
flags: 0,
eax: 0,
ebx: 0x756e_6547, edx: 0x4965_6e69, ecx: 0x6c65_746e, ..Default::default()
}];
assert_eq!(detect_vendor(&entries), CpuVendor::Intel);
}
#[test]
fn detect_vendor_amd() {
let entries = [kvm_cpuid_entry2 {
function: 0,
index: 0,
flags: 0,
eax: 0,
ebx: 0x6874_7541, edx: 0x6974_6e65, ecx: 0x444d_4163, ..Default::default()
}];
assert_eq!(detect_vendor(&entries), CpuVendor::Amd);
}
#[test]
fn detect_vendor_unknown() {
let entries = [kvm_cpuid_entry2 {
function: 0,
index: 0,
flags: 0,
eax: 0,
ebx: 0,
edx: 0,
ecx: 0,
..Default::default()
}];
assert_eq!(detect_vendor(&entries), CpuVendor::Unknown);
}
#[test]
fn detect_vendor_missing_leaf0() {
let entries = [kvm_cpuid_entry2 {
function: 1,
index: 0,
..Default::default()
}];
assert_eq!(detect_vendor(&entries), CpuVendor::Unknown);
}
#[test]
fn detect_vendor_from_kvm() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let cpuid = kvm
.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.expect("get_supported_cpuid");
let vendor = detect_vendor(cpuid.as_slice());
assert_ne!(vendor, CpuVendor::Unknown, "host should be Intel or AMD");
}
#[test]
fn brand_string_not_clobbered() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let host_cpuid = kvm
.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.expect("get_supported_cpuid");
let topo = Topology {
llcs: 2,
cores_per_llc: 2,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
for leaf_fn in [0x8000_0002u32, 0x8000_0003, 0x8000_0004] {
let host_leaf = host_cpuid.as_slice().iter().find(|e| e.function == leaf_fn);
let guest_leaf = cpuid.iter().find(|e| e.function == leaf_fn);
match (host_leaf, guest_leaf) {
(Some(h), Some(g)) => {
assert_eq!(
(h.eax, h.ebx, h.ecx, h.edx),
(g.eax, g.ebx, g.ecx, g.edx),
"brand string leaf {leaf_fn:#x} should pass through from host"
);
}
(None, None) => {}
_ => panic!(
"leaf {leaf_fn:#x}: host has it = {}, guest has it = {}",
host_leaf.is_some(),
guest_leaf.is_some()
),
}
}
}
#[test]
fn vendor_conditional_leaf4_on_intel() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let host_cpuid = kvm
.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.expect("get_supported_cpuid");
let vendor = detect_vendor(host_cpuid.as_slice());
if vendor != CpuVendor::Intel {
return; }
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let l3 = cpuid
.iter()
.find(|e| e.function == 0x4 && ((e.eax >> 5) & 0x7) == 3);
if let Some(entry) = l3 {
let max_sharing = (entry.eax >> 14) & 0xfff;
assert_eq!(
max_sharing,
(1u32 << core_shift(&topo)) - 1,
"Intel leaf 0x4 L3 sharing should be patched"
);
}
}
#[test]
fn vendor_conditional_leaf8000001e_on_amd() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let host_cpuid = kvm
.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.expect("get_supported_cpuid");
let vendor = detect_vendor(host_cpuid.as_slice());
if vendor != CpuVendor::Amd {
return; }
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let leaf = cpuid.iter().find(|e| e.function == 0x8000_001e);
if let Some(entry) = leaf {
assert_eq!(
entry.eax,
apic_id(&topo, 0),
"AMD leaf 0x8000001E EAX should be patched"
);
}
}
#[test]
fn vendor_conditional_leaf8000001d_on_amd() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let host_cpuid = kvm
.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.expect("get_supported_cpuid");
let vendor = detect_vendor(host_cpuid.as_slice());
if vendor != CpuVendor::Amd {
return;
}
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let l3 = cpuid
.iter()
.find(|e| e.function == 0x8000_001d && ((e.eax >> 5) & 0x7) == 3);
if let Some(entry) = l3 {
let max_sharing = (entry.eax >> 14) & 0xfff;
assert_eq!(
max_sharing,
(1u32 << core_shift(&topo)) - 1,
"AMD leaf 0x8000001D L3 sharing should be patched to LLC scope"
);
let max_core_ids = (entry.eax >> 26) & 0x3f;
let core_bits = bits_needed(topo.cores_per_llc);
assert_eq!(
max_core_ids,
(1u32 << core_bits) - 1,
"AMD leaf 0x8000001D core IDs should match topology"
);
}
}
#[test]
fn leaf8000001d_l1_l2_sharing_per_core() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let host_cpuid = kvm
.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.expect("get_supported_cpuid");
let vendor = detect_vendor(host_cpuid.as_slice());
if vendor != CpuVendor::Amd {
return;
}
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
for level in [1u32, 2] {
let leaf = cpuid
.iter()
.find(|e| e.function == 0x8000_001d && ((e.eax >> 5) & 0x7) == level);
if let Some(entry) = leaf {
let max_sharing = (entry.eax >> 14) & 0xfff;
assert_eq!(
max_sharing,
(1u32 << smt_shift(&topo)) - 1,
"AMD leaf 0x8000001D L{level} sharing should be per-core (SMT level)"
);
}
}
}
#[test]
fn leaf8000001d_cache_ids_differ_across_llcs() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let host_cpuid = kvm
.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.expect("get_supported_cpuid");
let vendor = detect_vendor(host_cpuid.as_slice());
if vendor != CpuVendor::Amd {
return;
}
let topo = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid0 = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
let cpuid4 = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
4,
false,
);
let l3_0 = cpuid0
.iter()
.find(|e| e.function == 0x8000_001d && ((e.eax >> 5) & 0x7) == 3);
let l3_4 = cpuid4
.iter()
.find(|e| e.function == 0x8000_001d && ((e.eax >> 5) & 0x7) == 3);
if let (Some(e0), Some(e4)) = (l3_0, l3_4) {
let sharing0 = (e0.eax >> 14) & 0xfff;
let sharing4 = (e4.eax >> 14) & 0xfff;
assert_eq!(sharing0, sharing4, "L3 sharing field should be identical");
let num_threads_sharing = sharing0 + 1;
let index_msb = 32 - (num_threads_sharing - 1).leading_zeros();
let cache_id_0 = apic_id(&topo, 0) >> index_msb;
let cache_id_4 = apic_id(&topo, 4) >> index_msb;
assert_ne!(
cache_id_0,
cache_id_4,
"CPUs in different LLCs should have different L3 cache IDs \
(apic0={}, apic4={}, shift={index_msb})",
apic_id(&topo, 0),
apic_id(&topo, 4),
);
}
}
#[test]
fn cache_ids_distinct_per_llc_representative() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let host_cpuid = kvm
.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.expect("get_supported_cpuid");
let vendor = detect_vendor(host_cpuid.as_slice());
let cache_leaf: u32 = match vendor {
CpuVendor::Intel => 0x4,
CpuVendor::Amd => 0x8000_001d,
CpuVendor::Unknown => return,
};
let topos = [
(2, 1, 1), (3, 3, 1), (2, 4, 2), (7, 5, 3), (5, 3, 2), (15, 16, 1), (14, 9, 2), ];
for (llcs, cores, threads) in topos {
let topo = Topology {
llcs,
cores_per_llc: cores,
threads_per_core: threads,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpus_per_llc = cores * threads;
let cpuid0 = generate_cpuid(host_cpuid.as_slice(), &topo, 0, false);
let l3 = cpuid0
.iter()
.find(|e| e.function == cache_leaf && ((e.eax >> 5) & 0x7) == 3);
let Some(l3_entry) = l3 else { continue };
let sharing = (l3_entry.eax >> 14) & 0xfff;
let num_threads_sharing = sharing + 1;
let index_msb = 32 - (num_threads_sharing - 1).leading_zeros();
let mut cache_ids: std::collections::HashSet<u32> = std::collections::HashSet::new();
for l in 0..llcs {
let cpu = l * cpus_per_llc;
let apic = apic_id(&topo, cpu);
let cache_id = apic >> index_msb;
cache_ids.insert(cache_id);
}
assert_eq!(
cache_ids.len(),
llcs as usize,
"{llcs}l/{cores}c/{threads}t: expected {llcs} distinct L3 cache IDs, \
got {} (shift={index_msb})",
cache_ids.len(),
);
}
}
#[test]
fn max_apic_id_single_cpu() {
let t = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
assert_eq!(max_apic_id(&t), 0);
}
#[test]
fn max_apic_id_equals_last_cpu() {
let t = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
assert_eq!(max_apic_id(&t), apic_id(&t, t.total_cpus() - 1));
}
#[test]
fn max_apic_id_large_topology() {
let t = Topology {
llcs: 14,
cores_per_llc: 9,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
};
assert_eq!(max_apic_id(&t), 433);
assert!(max_apic_id(&t) > 254);
}
#[test]
fn topology_single_thread_per_core() {
let t = Topology {
llcs: 4,
cores_per_llc: 4,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
assert_eq!(smt_shift(&t), 0);
let ids: Vec<u32> = (0..t.total_cpus()).map(|i| apic_id(&t, i)).collect();
let unique: std::collections::HashSet<u32> = ids.iter().cloned().collect();
assert_eq!(ids.len(), unique.len());
}
#[test]
fn topology_1x1x1() {
let t = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
assert_eq!(t.total_cpus(), 1);
assert_eq!(apic_id(&t, 0), 0);
assert_eq!(max_apic_id(&t), 0);
}
#[test]
fn kvm_hints_realtime_set_in_performance_mode() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 1,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
true,
);
let leaf = cpuid.iter().find(|e| e.function == 0x4000_0001);
assert!(
leaf.is_some(),
"leaf 0x40000001 should exist in performance_mode"
);
let entry = leaf.unwrap();
assert_ne!(
entry.edx & 1,
0,
"KVM_HINTS_REALTIME (EDX bit 0) should be set"
);
let leaf40 = cpuid
.iter()
.find(|e| e.function == 0x4000_0000)
.expect("leaf 0x40000000 should exist");
assert!(
leaf40.eax >= 0x4000_0001,
"0x40000000.EAX should be >= 0x40000001, got {:#x}",
leaf40.eax,
);
}
#[test]
fn kvm_hints_realtime_not_set_without_performance_mode() {
let kvm = match kvm_ioctls::Kvm::new() {
Ok(k) => k,
Err(_) => return,
};
let topo = Topology {
llcs: 1,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(
kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)
.unwrap()
.as_slice(),
&topo,
0,
false,
);
if let Some(entry) = cpuid.iter().find(|e| e.function == 0x4000_0001) {
assert_eq!(
entry.edx & 1,
0,
"KVM_HINTS_REALTIME should not be set without performance_mode"
);
}
}
#[test]
fn kvm_hints_realtime_preserves_other_edx_bits() {
let base = vec![
kvm_cpuid_entry2 {
function: 0,
index: 0,
flags: 0,
eax: 0,
ebx: 0x756e_6547,
edx: 0x4965_6e69,
ecx: 0x6c65_746e,
..Default::default()
},
kvm_cpuid_entry2 {
function: 0x4000_0000,
index: 0,
flags: 0,
eax: 0x4000_0000, ebx: 0,
ecx: 0,
edx: 0,
..Default::default()
},
kvm_cpuid_entry2 {
function: 0x4000_0001,
index: 0,
flags: 0,
eax: 0,
ebx: 0,
ecx: 0,
edx: 0xdead_0000, ..Default::default()
},
];
let topo = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let cpuid = generate_cpuid(&base, &topo, 0, true);
let entry = cpuid
.iter()
.find(|e| e.function == 0x4000_0001)
.expect("leaf 0x40000001 should exist");
assert_eq!(
entry.edx, 0xdead_0001,
"should OR bit 0 into existing EDX, not replace"
);
let leaf40 = cpuid
.iter()
.find(|e| e.function == 0x4000_0000)
.expect("leaf 0x40000000 should exist");
assert_eq!(
leaf40.eax, 0x4000_0001,
"0x40000000.EAX should be bumped to 0x40000001"
);
let cpuid_no_perf = generate_cpuid(&base, &topo, 0, false);
let entry_no_perf = cpuid_no_perf
.iter()
.find(|e| e.function == 0x4000_0001)
.expect("leaf should still exist");
assert_eq!(
entry_no_perf.edx, 0xdead_0000,
"without performance_mode, EDX should be unchanged"
);
}
}