use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, PoisonError, mpsc};
use arcbox_hv::reg::{HV_REG_X0 as X0, HV_REG_X1 as X1, HV_REG_X2 as X2, HV_REG_X3 as X3};
const PSCI_VERSION: u64 = 0x8400_0000;
const PSCI_CPU_OFF: u64 = 0x8400_0002;
const PSCI_CPU_ON_64: u64 = 0xC400_0003;
const PSCI_AFFINITY_INFO_64: u64 = 0xC400_0004;
const PSCI_AFFINITY_INFO_32: u64 = 0x8400_0004;
const PSCI_MIGRATE_INFO_TYPE: u64 = 0x8400_0006;
const PSCI_SYSTEM_OFF: u64 = 0x8400_0008;
const PSCI_SYSTEM_RESET: u64 = 0x8400_0009;
const PSCI_FEATURES: u64 = 0x8400_000A;
const PSCI_SUCCESS: u64 = 0;
const PSCI_NOT_SUPPORTED: u64 = (-1_i64) as u64;
const PSCI_INVALID_PARAMS: u64 = (-2_i64) as u64;
const PSCI_DENIED: u64 = (-3_i64) as u64;
const PSCI_ALREADY_ON: u64 = (-4_i64) as u64;
const AFFINITY_INFO_ON: u64 = 0;
const AFFINITY_INFO_OFF: u64 = 1;
const MIGRATE_TYPE_NOT_REQUIRED: u64 = 2;
const PSCI_VERSION_1_0: u64 = 1 << 16;
pub struct CpuOnRequest {
pub _target_cpu: u64,
pub entry_point: u64,
pub context_id: u64,
}
struct CpuSlot {
on: bool,
tx: Option<mpsc::Sender<CpuOnRequest>>,
}
pub struct CpuPowerRegistry {
slots: Mutex<Vec<CpuSlot>>,
}
pub type CpuPower = Arc<CpuPowerRegistry>;
impl CpuPowerRegistry {
pub fn from_senders(senders: Vec<Option<mpsc::Sender<CpuOnRequest>>>) -> CpuPower {
Arc::new(Self {
slots: Mutex::new(
senders
.into_iter()
.map(|tx| CpuSlot {
on: tx.is_none(),
tx,
})
.collect(),
),
})
}
fn lock(&self) -> std::sync::MutexGuard<'_, Vec<CpuSlot>> {
self.slots.lock().unwrap_or_else(PoisonError::into_inner)
}
fn snapshot(&self) -> Vec<bool> {
self.lock().iter().map(|slot| slot.on).collect()
}
fn cpu_on(&self, target: usize, request: CpuOnRequest) -> u64 {
let mut slots = self.lock();
let Some(slot) = slots.get_mut(target) else {
return PSCI_INVALID_PARAMS;
};
if slot.on {
return PSCI_ALREADY_ON;
}
let Some(tx) = slot.tx.as_ref() else {
return PSCI_ALREADY_ON;
};
match tx.send(request) {
Ok(()) => {
slot.on = true;
PSCI_SUCCESS
}
Err(_) => PSCI_ALREADY_ON,
}
}
fn mark_off(&self, vcpu_id: usize) {
if let Some(slot) = self.lock().get_mut(vcpu_id) {
slot.on = false;
}
}
pub fn close(&self) {
for slot in self.lock().iter_mut() {
slot.tx = None;
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum PsciEffect {
None,
SystemOff,
SystemReset,
CpuOn {
target: usize,
entry_point: u64,
context_id: u64,
},
CpuOff,
}
#[derive(Debug, PartialEq, Eq)]
pub(in crate::vmm) enum PsciExit {
Continue,
CpuOff,
}
fn psci_feature_supported(func_id: u64) -> bool {
matches!(
func_id,
PSCI_VERSION
| PSCI_FEATURES
| PSCI_CPU_OFF
| PSCI_CPU_ON_64
| PSCI_AFFINITY_INFO_64
| PSCI_AFFINITY_INFO_32
| PSCI_MIGRATE_INFO_TYPE
| PSCI_SYSTEM_OFF
| PSCI_SYSTEM_RESET
)
}
fn resolve_psci(
caller: usize,
func_id: u64,
x1: u64,
x2: u64,
x3: u64,
cpu_on: &[bool],
) -> (u64, PsciEffect) {
match func_id {
PSCI_VERSION => (PSCI_VERSION_1_0, PsciEffect::None),
PSCI_FEATURES => {
let ret = if psci_feature_supported(x1) {
PSCI_SUCCESS
} else {
PSCI_NOT_SUPPORTED
};
(ret, PsciEffect::None)
}
PSCI_SYSTEM_OFF => (PSCI_SUCCESS, PsciEffect::SystemOff),
PSCI_SYSTEM_RESET => (PSCI_SUCCESS, PsciEffect::SystemReset),
PSCI_MIGRATE_INFO_TYPE => (MIGRATE_TYPE_NOT_REQUIRED, PsciEffect::None),
PSCI_CPU_OFF => {
if caller == 0 {
(PSCI_DENIED, PsciEffect::None)
} else {
(PSCI_SUCCESS, PsciEffect::CpuOff)
}
}
PSCI_CPU_ON_64 => {
let target = (x1 & 0xFF) as usize;
match cpu_on.get(target).copied() {
None => (PSCI_INVALID_PARAMS, PsciEffect::None),
Some(true) => (PSCI_ALREADY_ON, PsciEffect::None),
Some(false) => (
PSCI_SUCCESS,
PsciEffect::CpuOn {
target,
entry_point: x2,
context_id: x3,
},
),
}
}
PSCI_AFFINITY_INFO_64 | PSCI_AFFINITY_INFO_32 => {
let target = (x1 & 0xFF) as usize;
match cpu_on.get(target).copied() {
None => (PSCI_INVALID_PARAMS, PsciEffect::None),
Some(true) => (AFFINITY_INFO_ON, PsciEffect::None),
Some(false) => (AFFINITY_INFO_OFF, PsciEffect::None),
}
}
_ => (PSCI_NOT_SUPPORTED, PsciEffect::None),
}
}
fn cpu_on_snapshot(cpu_power: Option<&CpuPower>) -> Vec<bool> {
match cpu_power {
Some(registry) => registry.snapshot(),
None => vec![true],
}
}
pub(in crate::vmm) fn handle_psci(
vcpu_id: u32,
func_id: u64,
vcpu: &arcbox_hv::HvVcpu,
running: &Arc<AtomicBool>,
reset_requested: &Arc<AtomicBool>,
cpu_power: Option<&CpuPower>,
) -> PsciExit {
let x1 = vcpu.get_reg(X1).unwrap_or(0);
let x2 = vcpu.get_reg(X2).unwrap_or(0);
let x3 = vcpu.get_reg(X3).unwrap_or(0);
let cpu_on = cpu_on_snapshot(cpu_power);
let (mut ret, effect) = resolve_psci(vcpu_id as usize, func_id, x1, x2, x3, &cpu_on);
let mut exit = PsciExit::Continue;
match effect {
PsciEffect::None => {
if ret == PSCI_NOT_SUPPORTED {
tracing::debug!("vCPU {vcpu_id}: unhandled PSCI func {func_id:#x}");
}
}
PsciEffect::SystemOff => {
tracing::info!("vCPU {vcpu_id}: PSCI SYSTEM_OFF");
running.store(false, Ordering::SeqCst);
}
PsciEffect::SystemReset => {
tracing::info!("vCPU {vcpu_id}: PSCI SYSTEM_RESET");
reset_requested.store(true, Ordering::SeqCst);
running.store(false, Ordering::SeqCst);
}
PsciEffect::CpuOn {
target,
entry_point,
context_id,
} => {
ret = match cpu_power {
Some(registry) => registry.cpu_on(
target,
CpuOnRequest {
_target_cpu: x1,
entry_point,
context_id,
},
),
None => PSCI_INVALID_PARAMS,
};
if ret == PSCI_SUCCESS {
tracing::info!(
"vCPU {vcpu_id}: PSCI CPU_ON target={target} \
entry={entry_point:#x} ctx={context_id:#x}"
);
} else {
tracing::debug!("vCPU {vcpu_id}: PSCI CPU_ON target={target} ret={:#x}", ret);
}
}
PsciEffect::CpuOff => {
if let Some(registry) = cpu_power {
registry.mark_off(vcpu_id as usize);
}
tracing::info!("vCPU {vcpu_id}: PSCI CPU_OFF");
exit = PsciExit::CpuOff;
}
}
let _ = vcpu.set_reg(X0, ret);
exit
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_reports_1_0() {
let (ret, eff) = resolve_psci(0, PSCI_VERSION, 0, 0, 0, &[true]);
assert_eq!(ret, PSCI_VERSION_1_0);
assert_eq!(eff, PsciEffect::None);
}
#[test]
fn features_reports_supported_and_unsupported() {
for f in [
PSCI_CPU_OFF,
PSCI_CPU_ON_64,
PSCI_AFFINITY_INFO_64,
PSCI_SYSTEM_OFF,
PSCI_SYSTEM_RESET,
PSCI_MIGRATE_INFO_TYPE,
PSCI_VERSION,
PSCI_FEATURES,
] {
assert_eq!(
resolve_psci(0, PSCI_FEATURES, f, 0, 0, &[true]).0,
PSCI_SUCCESS
);
}
assert_eq!(
resolve_psci(0, PSCI_FEATURES, 0xC400_0001, 0, 0, &[true]).0,
PSCI_NOT_SUPPORTED
);
}
#[test]
fn system_off_and_reset_effects() {
assert_eq!(
resolve_psci(0, PSCI_SYSTEM_OFF, 0, 0, 0, &[true]).1,
PsciEffect::SystemOff
);
assert_eq!(
resolve_psci(0, PSCI_SYSTEM_RESET, 0, 0, 0, &[true]).1,
PsciEffect::SystemReset
);
}
#[test]
fn migrate_info_type_not_required() {
assert_eq!(
resolve_psci(0, PSCI_MIGRATE_INFO_TYPE, 0, 0, 0, &[true]),
(MIGRATE_TYPE_NOT_REQUIRED, PsciEffect::None)
);
}
#[test]
fn cpu_on_off_target_yields_dispatch() {
let (ret, eff) = resolve_psci(0, PSCI_CPU_ON_64, 1, 0x4020_0000, 0x1234, &[true, false]);
assert_eq!(ret, PSCI_SUCCESS);
assert_eq!(
eff,
PsciEffect::CpuOn {
target: 1,
entry_point: 0x4020_0000,
context_id: 0x1234,
}
);
}
#[test]
fn cpu_on_already_on_and_invalid() {
assert_eq!(
resolve_psci(0, PSCI_CPU_ON_64, 0, 0, 0, &[true, false]).0,
PSCI_ALREADY_ON
);
assert_eq!(
resolve_psci(0, PSCI_CPU_ON_64, 5, 0, 0, &[true, false]).0,
PSCI_INVALID_PARAMS
);
}
#[test]
fn affinity_info_reports_state() {
assert_eq!(
resolve_psci(0, PSCI_AFFINITY_INFO_64, 0, 0, 0, &[true, false]).0,
AFFINITY_INFO_ON
);
assert_eq!(
resolve_psci(0, PSCI_AFFINITY_INFO_64, 1, 0, 0, &[true, false]).0,
AFFINITY_INFO_OFF
);
assert_eq!(
resolve_psci(0, PSCI_AFFINITY_INFO_32, 9, 0, 0, &[true, false]).0,
PSCI_INVALID_PARAMS
);
}
#[test]
fn cpu_off_denied_for_bsp_allowed_for_secondary() {
assert_eq!(
resolve_psci(0, PSCI_CPU_OFF, 0, 0, 0, &[true, true]),
(PSCI_DENIED, PsciEffect::None)
);
assert_eq!(
resolve_psci(1, PSCI_CPU_OFF, 0, 0, 0, &[true, true]),
(PSCI_SUCCESS, PsciEffect::CpuOff)
);
}
#[test]
fn unknown_function_not_supported() {
assert_eq!(
resolve_psci(0, 0xC400_0012, 0, 0, 0, &[true]),
(PSCI_NOT_SUPPORTED, PsciEffect::None)
);
}
#[test]
fn registry_online_offline_reonline_roundtrip() {
let (tx, rx) = mpsc::channel::<CpuOnRequest>();
let registry = CpuPowerRegistry::from_senders(vec![None, Some(tx)]);
assert_eq!(registry.snapshot(), vec![true, false]);
let req = |entry| CpuOnRequest {
_target_cpu: 1,
entry_point: entry,
context_id: 0,
};
assert_eq!(registry.cpu_on(1, req(0x1000)), PSCI_SUCCESS);
assert_eq!(rx.try_recv().unwrap().entry_point, 0x1000);
assert_eq!(registry.snapshot(), vec![true, true]);
assert_eq!(registry.cpu_on(1, req(0x1000)), PSCI_ALREADY_ON);
registry.mark_off(1);
assert_eq!(registry.snapshot(), vec![true, false]);
assert_eq!(registry.cpu_on(1, req(0x2000)), PSCI_SUCCESS);
assert_eq!(rx.try_recv().unwrap().entry_point, 0x2000);
assert_eq!(registry.snapshot(), vec![true, true]);
assert_eq!(registry.cpu_on(9, req(0)), PSCI_INVALID_PARAMS);
registry.mark_off(0); assert_eq!(registry.cpu_on(0, req(0)), PSCI_ALREADY_ON);
}
#[test]
fn registry_close_disconnects_parked_receivers() {
let (tx, rx) = mpsc::channel::<CpuOnRequest>();
let registry = CpuPowerRegistry::from_senders(vec![None, Some(tx)]);
let thread_handle = registry.clone(); registry.close();
assert!(rx.recv().is_err());
drop(thread_handle);
}
}