use anyhow::{Context, Result};
use kvm_bindings::{
KVM_REG_ARM64, KVM_REG_ARM64_SYSREG, KVM_REG_ARM64_SYSREG_CRM_MASK,
KVM_REG_ARM64_SYSREG_CRM_SHIFT, KVM_REG_ARM64_SYSREG_CRN_MASK, KVM_REG_ARM64_SYSREG_CRN_SHIFT,
KVM_REG_ARM64_SYSREG_OP0_MASK, KVM_REG_ARM64_SYSREG_OP0_SHIFT, KVM_REG_ARM64_SYSREG_OP1_MASK,
KVM_REG_ARM64_SYSREG_OP1_SHIFT, KVM_REG_ARM64_SYSREG_OP2_MASK, KVM_REG_ARM64_SYSREG_OP2_SHIFT,
KVM_REG_SIZE_U64,
};
use kvm_ioctls::VcpuFd;
use crate::vmm::topology::Topology;
const MAX_CACHE_LEVEL: u32 = 7;
pub const MPIDR_EL1: u64 = KVM_REG_ARM64
| KVM_REG_SIZE_U64
| KVM_REG_ARM64_SYSREG as u64
| ((3u64 << KVM_REG_ARM64_SYSREG_OP0_SHIFT) & KVM_REG_ARM64_SYSREG_OP0_MASK as u64)
| ((0u64 << KVM_REG_ARM64_SYSREG_OP1_SHIFT) & KVM_REG_ARM64_SYSREG_OP1_MASK as u64)
| ((0u64 << KVM_REG_ARM64_SYSREG_CRN_SHIFT) & KVM_REG_ARM64_SYSREG_CRN_MASK as u64)
| ((0u64 << KVM_REG_ARM64_SYSREG_CRM_SHIFT) & KVM_REG_ARM64_SYSREG_CRM_MASK as u64)
| ((5u64 << KVM_REG_ARM64_SYSREG_OP2_SHIFT) & KVM_REG_ARM64_SYSREG_OP2_MASK as u64);
const CLIDR_EL1: u64 = KVM_REG_ARM64
| KVM_REG_SIZE_U64
| KVM_REG_ARM64_SYSREG as u64
| ((3u64 << KVM_REG_ARM64_SYSREG_OP0_SHIFT) & KVM_REG_ARM64_SYSREG_OP0_MASK as u64)
| ((1u64 << KVM_REG_ARM64_SYSREG_OP1_SHIFT) & KVM_REG_ARM64_SYSREG_OP1_MASK as u64)
| ((0u64 << KVM_REG_ARM64_SYSREG_CRN_SHIFT) & KVM_REG_ARM64_SYSREG_CRN_MASK as u64)
| ((0u64 << KVM_REG_ARM64_SYSREG_CRM_SHIFT) & KVM_REG_ARM64_SYSREG_CRM_MASK as u64)
| ((1u64 << KVM_REG_ARM64_SYSREG_OP2_SHIFT) & KVM_REG_ARM64_SYSREG_OP2_MASK as u64);
const CLIDR_CTYPE_NO_CACHE: u64 = 0;
const CLIDR_CTYPE_INSTRUCTION: u64 = 1;
const CLIDR_CTYPE_DATA: u64 = 2;
const CLIDR_CTYPE_SEPARATE: u64 = 3;
const CLIDR_CTYPE_UNIFIED: u64 = 4;
const CLIDR_CTYPE_BITS: u32 = 3;
const CLIDR_LOC_SHIFT: u32 = 24;
pub fn host_l1_is_unified() -> bool {
let cache_dir = "/sys/devices/system/cpu/cpu0/cache";
let entries = match std::fs::read_dir(cache_dir) {
Ok(e) => e,
Err(_) => return false,
};
let mut has_data = false;
let mut has_instruction = false;
let mut has_unified = false;
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.starts_with("index") {
continue;
}
let level_path = entry.path().join("level");
let type_path = entry.path().join("type");
let Ok(level_str) = std::fs::read_to_string(&level_path) else {
continue;
};
let Ok(level) = level_str.trim().parse::<u32>() else {
continue;
};
if level != 1 {
continue;
}
let Ok(type_str) = std::fs::read_to_string(&type_path) else {
continue;
};
match type_str.trim() {
"Data" => has_data = true,
"Instruction" => has_instruction = true,
"Unified" => has_unified = true,
_ => {}
}
}
has_unified && !has_data && !has_instruction
}
fn build_clidr_from_sysfs() -> u64 {
let cache_dir = "/sys/devices/system/cpu/cpu0/cache";
let entries = match std::fs::read_dir(cache_dir) {
Ok(e) => e,
Err(_) => return 0,
};
let mut level_types: [u8; MAX_CACHE_LEVEL as usize + 1] = [0; MAX_CACHE_LEVEL as usize + 1];
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.starts_with("index") {
continue;
}
let level_path = entry.path().join("level");
let type_path = entry.path().join("type");
let Ok(level_str) = std::fs::read_to_string(&level_path) else {
continue;
};
let Ok(level) = level_str.trim().parse::<u32>() else {
continue;
};
if level == 0 || level > MAX_CACHE_LEVEL {
continue;
}
let Ok(type_str) = std::fs::read_to_string(&type_path) else {
continue;
};
match type_str.trim() {
"Data" => level_types[level as usize] |= 1,
"Instruction" => level_types[level as usize] |= 2,
"Unified" => level_types[level as usize] |= 4,
_ => {}
}
}
let mut clidr: u64 = 0;
let mut max_level: u32 = 0;
for level in 1..=MAX_CACHE_LEVEL {
let flags = level_types[level as usize];
if flags == 0 {
break;
}
let ctype = if flags & 4 != 0 {
CLIDR_CTYPE_UNIFIED
} else if flags & 1 != 0 && flags & 2 != 0 {
CLIDR_CTYPE_SEPARATE
} else if flags & 2 != 0 {
CLIDR_CTYPE_INSTRUCTION
} else if flags & 1 != 0 {
CLIDR_CTYPE_DATA
} else {
CLIDR_CTYPE_NO_CACHE
};
let shift = CLIDR_CTYPE_BITS * (level - 1);
clidr |= ctype << shift;
max_level = level;
}
clidr |= (max_level as u64) << CLIDR_LOC_SHIFT;
clidr
}
fn merge_clidr(current: u64, sysfs: u64) -> u64 {
const CTYPE_MASK: u64 = 0x001F_FFFF;
const LOC_MASK: u64 = 0x0700_0000;
const REPLACE_MASK: u64 = CTYPE_MASK | LOC_MASK;
(current & !REPLACE_MASK) | (sysfs & REPLACE_MASK)
}
pub fn override_clidr(vcpus: &[VcpuFd]) -> Result<()> {
let sysfs_clidr = build_clidr_from_sysfs();
if sysfs_clidr == 0 {
tracing::warn!("no cache info from sysfs, skipping CLIDR override");
return Ok(());
}
let mut cur_clidr_bytes = [0u8; 8];
if let Err(e) = vcpus[0].get_one_reg(CLIDR_EL1, &mut cur_clidr_bytes) {
tracing::warn!("failed to read CLIDR_EL1, skipping override: {e}");
return Ok(());
}
let cur_clidr = u64::from_le_bytes(cur_clidr_bytes);
let new_clidr = merge_clidr(cur_clidr, sysfs_clidr);
if new_clidr != cur_clidr {
let new_bytes = new_clidr.to_le_bytes();
for (i, vcpu) in vcpus.iter().enumerate() {
vcpu.set_one_reg(CLIDR_EL1, &new_bytes)
.with_context(|| format!("set CLIDR_EL1 on vCPU {i}"))?;
}
tracing::debug!(
cur = format_args!("{cur_clidr:#x}"),
new = format_args!("{new_clidr:#x}"),
"CLIDR_EL1 override applied",
);
}
Ok(())
}
pub fn host_cache_levels() -> u32 {
let mut max_level: u32 = 0;
let cache_dir = "/sys/devices/system/cpu/cpu0/cache";
let entries = match std::fs::read_dir(cache_dir) {
Ok(e) => e,
Err(_) => return 0,
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.starts_with("index") {
continue;
}
let level_path = entry.path().join("level");
if let Ok(s) = std::fs::read_to_string(&level_path)
&& let Ok(level) = s.trim().parse::<u32>()
&& level > max_level
{
max_level = level;
}
}
max_level
}
pub const MPIDR_AFF_MASK: u64 = 0xFF_FFFF;
pub fn read_mpidr(vcpu: &VcpuFd) -> Result<u64> {
let mut buf = [0u8; 8];
vcpu.get_one_reg(MPIDR_EL1, &mut buf)
.context("read MPIDR_EL1")?;
Ok(u64::from_le_bytes(buf))
}
pub fn read_mpidrs(vcpus: &[VcpuFd]) -> Result<Vec<u64>> {
vcpus.iter().map(read_mpidr).collect()
}
pub fn mpidr_to_fdt_reg(mpidr: u64) -> u64 {
mpidr & MPIDR_AFF_MASK
}
pub fn mpidr_from_topology(topo: &Topology, cpu_id: u32) -> u64 {
let (llc, core, thread) = topo.decompose(cpu_id);
let aff0 = thread as u64;
let aff1 = core as u64;
let aff2 = llc as u64;
(1u64 << 31) | (aff2 << 16) | (aff1 << 8) | aff0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mpidr_el1_reg_id() {
assert_eq!(
MPIDR_EL1, 0x6030_0000_0013_C005,
"MPIDR_EL1 register ID encoding"
);
}
#[test]
fn mpidr_from_topology_single() {
let t = Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
};
let mpidr = mpidr_from_topology(&t, 0);
assert_eq!(mpidr & MPIDR_AFF_MASK, 0);
assert_ne!(mpidr & (1 << 31), 0, "bit 31 must be set");
}
#[test]
fn mpidr_from_topology_multi() {
let t = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
};
let m0 = mpidr_from_topology(&t, 0);
assert_eq!(m0 & 0xFF, 0, "aff0 (thread) = 0");
assert_eq!((m0 >> 8) & 0xFF, 0, "aff1 (core) = 0");
assert_eq!((m0 >> 16) & 0xFF, 0, "aff2 (LLC) = 0");
let m1 = mpidr_from_topology(&t, 1);
assert_eq!(m1 & 0xFF, 1, "aff0 (thread) = 1");
assert_eq!((m1 >> 8) & 0xFF, 0, "aff1 (core) = 0");
let m2 = mpidr_from_topology(&t, 2);
assert_eq!(m2 & 0xFF, 0, "aff0 (thread) = 0");
assert_eq!((m2 >> 8) & 0xFF, 1, "aff1 (core) = 1");
let m8 = mpidr_from_topology(&t, 8);
assert_eq!(m8 & 0xFF, 0, "aff0 (thread) = 0");
assert_eq!((m8 >> 8) & 0xFF, 0, "aff1 (core) = 0");
assert_eq!((m8 >> 16) & 0xFF, 1, "aff2 (LLC) = 1");
}
#[test]
fn mpidr_to_fdt_reg_masks() {
let mpidr = (1u64 << 31) | (2 << 16) | (3 << 8) | 1;
let reg = mpidr_to_fdt_reg(mpidr);
assert_eq!(reg, (2 << 16) | (3 << 8) | 1);
assert_eq!(reg & (1 << 31), 0, "bit 31 should be masked out");
}
#[test]
fn mpidr_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), (255, 1, 1), (1, 255, 1), (4, 32, 1), ];
for (llcs, cores, threads) in topos {
let t = Topology {
llcs,
cores_per_llc: cores,
threads_per_core: threads,
numa_nodes: 1,
};
let mpidrs: Vec<u64> = (0..t.total_cpus())
.map(|i| mpidr_from_topology(&t, i))
.collect();
let unique: std::collections::HashSet<u64> = mpidrs.iter().copied().collect();
assert_eq!(
mpidrs.len(),
unique.len(),
"topology {llcs}l/{cores}c/{threads}t: MPIDRs not unique"
);
}
}
#[test]
fn mpidr_bit31_always_set() {
let t = Topology {
llcs: 2,
cores_per_llc: 4,
threads_per_core: 2,
numa_nodes: 1,
};
for cpu in 0..t.total_cpus() {
let mpidr = mpidr_from_topology(&t, cpu);
assert_ne!(mpidr & (1 << 31), 0, "cpu {cpu}: MPIDR bit 31 must be set");
}
}
#[test]
fn mpidr_aff_mask_covers_three_levels() {
assert_eq!(MPIDR_AFF_MASK, 0xFF_FFFF);
assert_eq!(MPIDR_AFF_MASK & 0xFF, 0xFF, "Aff0 fully covered");
assert_eq!((MPIDR_AFF_MASK >> 8) & 0xFF, 0xFF, "Aff1 fully covered");
assert_eq!((MPIDR_AFF_MASK >> 16) & 0xFF, 0xFF, "Aff2 fully covered");
}
#[test]
fn decompose_matches_mpidr_fields() {
let t = Topology {
llcs: 3,
cores_per_llc: 5,
threads_per_core: 2,
numa_nodes: 1,
};
for cpu in 0..t.total_cpus() {
let (llc, core, thread) = t.decompose(cpu);
let mpidr = mpidr_from_topology(&t, cpu);
assert_eq!(mpidr & 0xFF, thread as u64, "cpu {cpu}: aff0 = thread");
assert_eq!((mpidr >> 8) & 0xFF, core as u64, "cpu {cpu}: aff1 = core");
assert_eq!((mpidr >> 16) & 0xFF, llc as u64, "cpu {cpu}: aff2 = LLC");
}
}
#[test]
fn host_cache_levels_reads_sysfs() {
let level = host_cache_levels();
assert!(
level >= 1,
"host_cache_levels should detect at least 1 cache level, got {level}"
);
}
#[test]
fn clidr_el1_reg_id() {
assert_eq!(
CLIDR_EL1, 0x6030_0000_0013_C801,
"CLIDR_EL1 register ID encoding"
);
}
#[test]
fn build_clidr_from_sysfs_nonzero() {
let clidr = build_clidr_from_sysfs();
assert_ne!(clidr, 0, "sysfs should produce a non-zero CLIDR");
let ctype1 = clidr & 0x7;
assert_ne!(ctype1, 0, "L1 Ctype must be non-zero");
let loc = (clidr >> CLIDR_LOC_SHIFT) & 0x7;
assert!(loc >= 1, "LoC must be >= 1, got {loc}");
}
#[test]
fn merge_clidr_replaces_ctype_and_loc() {
let current: u64 = (2 << 27) | (1 << 21) | (1 << 24) | 4;
let sysfs: u64 = (2 << 24) | (4 << 3) | 3;
let merged = merge_clidr(current, sysfs);
assert_eq!(merged & 0x001F_FFFF, sysfs & 0x001F_FFFF);
assert_eq!((merged >> 24) & 0x7, 2, "LoC from sysfs");
assert_eq!((merged >> 21) & 0x7, 1, "LoUIS preserved");
assert_eq!((merged >> 27) & 0x7, 2, "LoUU preserved");
}
#[test]
fn merge_clidr_identity_when_equal() {
let val = 0x0000_0000_0200_0023_u64;
assert_eq!(merge_clidr(val, val), val);
}
}