virtio-devices: iommu: Cap number of domains per device

The per-domain mapping cap bounds memory inside one domain, but a
guest can still grow the domains map indefinitely with ATTACH
requests for distinct domain IDs. Reject ATTACH with
VIRTIO_IOMMU_S_NOMEM at 64K domains.

Signed-off-by: Rob Bradford <rbradford@meta.com>
Assisted-by: Claude:claude-opus-4-7
This commit is contained in:
Rob Bradford
2026-04-26 22:15:54 +01:00
parent 63a523b3f3
commit eb8b10add0

View File

@@ -77,6 +77,10 @@ const VIRTIO_IOMMU_PAGE_GRANULE: u64 = 1u64 << VIRTIO_IOMMU_PAGE_SIZE_MASK.trail
// ~64 MiB at ~64 bytes/entry, well above any legitimate workload.
const MAX_MAPPINGS_PER_DOMAIN: usize = 1 << 20;
// Bound the per-device domain count so a guest cannot grow the
// domain map indefinitely with ATTACH-only requests.
const MAX_DOMAINS: usize = 1 << 16;
#[derive(Copy, Clone, Debug, Default)]
#[repr(C, packed)]
#[allow(dead_code)]
@@ -321,6 +325,8 @@ enum Error {
InvalidUnmapRequestPartialOverlap,
#[error("Per-domain mapping count cap exceeded")]
MappingCountExceeded,
#[error("Per-device domain count cap exceeded: current:{0}, max:{MAX_DOMAINS}")]
DomainCountExceeded(usize),
#[error("Guest sent us invalid PROBE request")]
InvalidProbeRequest,
#[error("Failed to performing external mapping")]
@@ -419,6 +425,17 @@ impl Request {
return Err(Error::InvalidAttachRequest);
}
// Refuse before mutating any state so a failed ATTACH
// does not leave a phantom endpoint pointing at a
// domain that was never created.
{
let domains = mapping.domains.read().unwrap();
if !domains.contains_key(&domain_id) && domains.len() >= MAX_DOMAINS {
status = VIRTIO_IOMMU_S_NOMEM;
return Err(Error::DomainCountExceeded(domains.len()));
}
}
let mut old_domain_id = domain_id;
if let Some(&id) = mapping.endpoints.read().unwrap().get(&endpoint) {
old_domain_id = id;
@@ -449,6 +466,18 @@ impl Request {
// Add new domain with no mapping if the entry didn't exist yet
let mut domains = mapping.domains.write().unwrap();
if !domains.contains_key(&domain_id) && domains.len() >= MAX_DOMAINS {
// Defensive re-check under write lock. Single-threaded
// today, but keeps the invariant if processing ever
// becomes concurrent. Drop the domains write lock
// before taking the endpoints write lock so we never
// hold both simultaneously.
let count = domains.len();
drop(domains);
mapping.endpoints.write().unwrap().remove(&endpoint);
status = VIRTIO_IOMMU_S_NOMEM;
return Err(Error::DomainCountExceeded(count));
}
let domain = Domain {
mappings: BTreeMap::new(),
bypass,