use std::{string::String, vec::Vec};
use axdevice_base::ItsId;
use super::GuestMmioRegion;
pub(crate) const AARCH64_GIC_REDISTRIBUTOR_FRAME_SIZE: usize = 0x2_0000;
const GICV2_DISTRIBUTOR_SIZE: usize = 0x1_000;
const GICV3_DISTRIBUTOR_MINIMUM_SIZE: usize = 0x1_0000;
const GICC_MINIMUM_SIZE: usize = 0x2_000;
const GICR_ALIGNMENT: usize = 0x1_0000;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GuestGicRedistributorProfile {
pub regions: Vec<GuestMmioRegion>,
pub stride: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GuestGicCpuRegion {
CpuInterface(GuestMmioRegion),
Redistributors(GuestGicRedistributorProfile),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GuestItsProfile {
pub id: ItsId,
pub node_path: String,
pub node_phandle: Option<u32>,
pub registers: GuestMmioRegion,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GuestGicProfile {
pub compatible: String,
pub node_path: String,
pub node_phandle: Option<u32>,
pub distributor: GuestMmioRegion,
pub cpu_region: GuestGicCpuRegion,
pub its: Vec<GuestItsProfile>,
}
impl GuestGicProfile {
pub(crate) fn normalized_for_vcpus(
mut self,
vcpu_count: usize,
) -> Result<Self, GuestGicProfileError> {
if let GuestGicCpuRegion::CpuInterface(region) = &mut self.cpu_region {
if self.distributor.length < GICV2_DISTRIBUTOR_SIZE {
return Err(GuestGicProfileError::DistributorTooSmall {
length: self.distributor.length,
minimum: GICV2_DISTRIBUTOR_SIZE,
});
}
if region.length < GICC_MINIMUM_SIZE {
return Err(GuestGicProfileError::CpuInterfaceTooSmall {
length: region.length,
});
}
self.distributor.length = GICV2_DISTRIBUTOR_SIZE;
region.length = GICC_MINIMUM_SIZE;
}
self.validate_for_vcpus(vcpu_count)?;
Ok(self)
}
pub(crate) fn validate_for_vcpus(&self, vcpu_count: usize) -> Result<(), GuestGicProfileError> {
let distributor_minimum = match &self.cpu_region {
GuestGicCpuRegion::CpuInterface(_) => GICV2_DISTRIBUTOR_SIZE,
GuestGicCpuRegion::Redistributors(_) => GICV3_DISTRIBUTOR_MINIMUM_SIZE,
};
if self.distributor.length < distributor_minimum {
return Err(GuestGicProfileError::DistributorTooSmall {
length: self.distributor.length,
minimum: distributor_minimum,
});
}
let mut windows = Vec::with_capacity(2 + self.its.len());
push_window(&mut windows, "distributor", 0, self.distributor)?;
match &self.cpu_region {
GuestGicCpuRegion::CpuInterface(region) => {
if region.length < GICC_MINIMUM_SIZE {
return Err(GuestGicProfileError::CpuInterfaceTooSmall {
length: region.length,
});
}
if !self.its.is_empty() {
return Err(GuestGicProfileError::ItsRequiresGicV3);
}
push_window(&mut windows, "CPU interface", 0, *region)?;
}
GuestGicCpuRegion::Redistributors(redistributors) => {
redistributors.validate_for_vcpus(vcpu_count)?;
for (index, region) in redistributors.regions.iter().copied().enumerate() {
push_window(&mut windows, "Redistributor", index, region)?;
}
}
}
for (index, its) in self.its.iter().enumerate() {
if its.registers.length == 0 {
return Err(GuestGicProfileError::EmptyRegion {
resource: "ITS",
index,
});
}
if self.its[..index]
.iter()
.any(|existing| existing.id == its.id)
{
return Err(GuestGicProfileError::DuplicateItsId { id: its.id });
}
push_window(&mut windows, "ITS", index, its.registers)?;
}
validate_non_overlapping(&windows)
}
}
impl GuestGicRedistributorProfile {
fn validate_for_vcpus(&self, vcpu_count: usize) -> Result<(), GuestGicProfileError> {
if self.regions.is_empty() {
return Err(GuestGicProfileError::MissingRedistributors);
}
if self.stride < AARCH64_GIC_REDISTRIBUTOR_FRAME_SIZE
|| !self.stride.is_multiple_of(GICR_ALIGNMENT)
{
return Err(GuestGicProfileError::InvalidRedistributorStride {
stride: self.stride,
});
}
let mut frame_count = 0usize;
for (index, region) in self.regions.iter().enumerate() {
if region.length == 0
|| !region.base.is_multiple_of(GICR_ALIGNMENT)
|| !region.length.is_multiple_of(GICR_ALIGNMENT)
{
return Err(GuestGicProfileError::InvalidRedistributorRegion {
index,
base: region.base,
length: region.length,
});
}
frame_count = frame_count
.checked_add(region.length / self.stride)
.ok_or(GuestGicProfileError::RedistributorCapacityOverflow)?;
}
if frame_count < vcpu_count {
return Err(GuestGicProfileError::InsufficientRedistributors {
available: frame_count,
required: vcpu_count,
});
}
Ok(())
}
}
type NamedWindow = (&'static str, usize, GuestMmioRegion);
fn push_window(
windows: &mut Vec<NamedWindow>,
resource: &'static str,
index: usize,
region: GuestMmioRegion,
) -> Result<(), GuestGicProfileError> {
region
.base
.checked_add(region.length)
.ok_or(GuestGicProfileError::RegionEndOverflow {
resource,
index,
base: region.base,
length: region.length,
})?;
windows.push((resource, index, region));
Ok(())
}
fn validate_non_overlapping(windows: &[NamedWindow]) -> Result<(), GuestGicProfileError> {
for (position, (first_resource, first_index, first)) in windows.iter().enumerate() {
let first_end = first.base + first.length;
for (second_resource, second_index, second) in &windows[position + 1..] {
let second_end = second.base + second.length;
if first.base < second_end && second.base < first_end {
return Err(GuestGicProfileError::OverlappingRegions {
first_resource,
first_index: *first_index,
second_resource,
second_index: *second_index,
});
}
}
}
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
pub enum GuestGicProfileError {
#[error("AArch64 GIC distributor window {length:#x} is smaller than {minimum:#x}")]
DistributorTooSmall { length: usize, minimum: usize },
#[error("AArch64 GIC CPU-interface window {length:#x} is smaller than {GICC_MINIMUM_SIZE:#x}")]
CpuInterfaceTooSmall { length: usize },
#[error("AArch64 GICv3 has no Redistributor regions")]
MissingRedistributors,
#[error("AArch64 GIC Redistributor stride {stride:#x} is invalid")]
InvalidRedistributorStride { stride: usize },
#[error(
"AArch64 GIC Redistributor region {index} at {base:#x} with length {length:#x} is not \
64-KiB aligned"
)]
InvalidRedistributorRegion {
index: usize,
base: usize,
length: usize,
},
#[error("AArch64 GIC Redistributor capacity overflows usize")]
RedistributorCapacityOverflow,
#[error(
"AArch64 GIC provides {available} Redistributor frames for {required} configured vCPUs"
)]
InsufficientRedistributors { available: usize, required: usize },
#[error("AArch64 GICv2 firmware profile cannot contain ITS instances")]
ItsRequiresGicV3,
#[error("AArch64 {resource} region {index} is empty")]
EmptyRegion {
resource: &'static str,
index: usize,
},
#[error(
"AArch64 {resource} region {index} at {base:#x} with length {length:#x} overflows usize"
)]
RegionEndOverflow {
resource: &'static str,
index: usize,
base: usize,
length: usize,
},
#[error(
"AArch64 {first_resource} region {first_index} overlaps {second_resource} region \
{second_index}"
)]
OverlappingRegions {
first_resource: &'static str,
first_index: usize,
second_resource: &'static str,
second_index: usize,
},
#[error("AArch64 GIC profile contains duplicate ITS identifier {id:?}")]
DuplicateItsId { id: ItsId },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gicv2_normalization_retains_only_guest_visible_frames() {
let profile = GuestGicProfile {
compatible: "arm,gic-400".into(),
node_path: "/interrupt-controller@2a701000".into(),
node_phandle: Some(1),
distributor: GuestMmioRegion {
base: 0x2a70_1000,
length: 0x1_0000,
},
cpu_region: GuestGicCpuRegion::CpuInterface(GuestMmioRegion {
base: 0x2a70_2000,
length: 0x1_0000,
}),
its: Vec::new(),
};
let normalized = profile.normalized_for_vcpus(1).unwrap();
assert_eq!(normalized.distributor.length, GICV2_DISTRIBUTOR_SIZE);
assert_eq!(
normalized.cpu_region,
GuestGicCpuRegion::CpuInterface(GuestMmioRegion {
base: 0x2a70_2000,
length: GICC_MINIMUM_SIZE,
})
);
}
}