KVM_SET_MEMORY_ATTRIBUTES validates the user-provided range only for representability (zero size, 64-bit wrap, alignment, supported attributes) and not for magnitude. kvm_vm_set_mem_attributes() then walks every GFN in the range with xa_reserve(), which materializes the full root-to-leaf path of xa_nodes (~9.3 bytes of kernel memory per GFN) even though the GFNs need no backing memory and no memslots need to exist. On ENOMEM the loop bails out without undoing prior reservations, and the only release path is kvm_destroy_vm(), so an unprivileged user with access to /dev/kvm on a VM with private memory support (sw protected, TDX or SNP) accumulates kernel memory across calls until the host runs out of memory: measured outcomes include the OOM killer selecting unrelated third-party processes while the attacker's own memcg stays uncharged, and a kernel panic ("System is deadlocked on memory") when all surviving processes are OOM-disabled. A single ioctl also holds kvm->slots_lock for the whole loop, blocking KVM_GET_DIRTY_LOG and memslot updates of that VM for its duration. Bound the total number of materialized attribute entries per VM to KVM_MEM_ATTR_MAX_GFNS (2^25 GFNs, i.e. 128 GiB of GPA, ~300 MiB of xa_nodes), enforced under kvm->slots_lock after the idempotency early-out. A per-VM counter tracks the population; it is resynchronized from the xarray only when an ioctl actually mutates the array (ENOMEM partial reservations, successful sets and clears), so idempotent requests and rejected ones stay O(1). The counter never under-counts: the pre-check charges the full range, so overlapping requests are rejected conservatively near the bound. Build-tested warning-free on v7.2-rc5. Runtime-verified on v7.2-rc5 (KASAN build, isolated VM): the bound cuts growth with a clean -ENOSPC at 2^25 GFNs; a 4 KiB idempotent re-set on an exhausted VM returns in ~4us and consumes no budget; and with the bound in place, the previously reproduced outcomes of the unbounded growth (OOM killing unrelated processes, kernel panic on OOM-disabled systems) no longer occur with adequate headroom. Found by an AI-assisted security audit. Fixes: 5a475554db1e ("KVM: Introduce per-page memory attributes") Assisted-by: Claude-Code:GLM-5.3-flash KASAN KCSAN Signed-off-by: David Ballesteros --- --- a/virt/kvm/kvm_main.c 2026-07-26 16:45:48.000000000 -0500 +++ b/virt/kvm/kvm_main.c 2026-09-10 14:55:47.774696165 -0500 @@ -1117,6 +1117,7 @@ xa_init(&kvm->vcpu_array); #ifdef CONFIG_KVM_GENERIC_MEMORY_ATTRIBUTES xa_init(&kvm->mem_attr_array); + kvm->mem_attr_gfn_count = 0; #endif INIT_LIST_HEAD(&kvm->gpc_list); @@ -2533,6 +2534,33 @@ return kvm_arch_pre_set_memory_attributes(kvm, range); } +/* + * Return the number of GFNs with a materialized entry in mem_attr_array. + * Serialization against modifications is provided by kvm->slots_lock. + */ +static unsigned long kvm_count_mem_attr_entries(struct kvm *kvm) +{ + XA_STATE(xas, &kvm->mem_attr_array, 0); + unsigned long count = 0; + void *entry; + + xas_for_each(&xas, entry, ULONG_MAX) { + if (xa_is_retry(entry)) { + xas_pause(&xas); + continue; + } + if (entry) + count++; + + if (need_resched()) { + xas_pause(&xas); + cond_resched(); + } + } + + return count; +} + /* Set @attributes for the gfn range [@start, @end). */ static int kvm_vm_set_mem_attributes(struct kvm *kvm, gfn_t start, gfn_t end, unsigned long attributes) @@ -2556,6 +2584,7 @@ }; unsigned long i; void *entry; + bool mutated = false; int r = 0; entry = attributes ? xa_mk_value(attributes) : NULL; @@ -2569,6 +2598,23 @@ goto out_unlock; /* + * Bound the number of materialized GFNs per VM. See the comment on + * KVM_MEM_ATTR_MAX_GFNS. + */ + if (attributes && + kvm->mem_attr_gfn_count + (end - start) > KVM_MEM_ATTR_MAX_GFNS) { + r = -ENOSPC; + goto out_unlock; + } + + /* + * From this point on the request mutates the array, so the per-VM + * counter must be resynchronized on the way out. Early-outs above + * skip the (potentially expensive) recount. + */ + mutated = true; + + /* * Reserve memory ahead of time to avoid having to deal with failures * partway through setting the new attributes. */ @@ -2592,6 +2638,15 @@ kvm_handle_gfn_range(kvm, &post_set_range); out_unlock: + /* + * Resynchronize with the actual state only when the array may have + * changed: on ENOMEM partway through the reserve loop, partially + * reserved GFNs are retained and must be counted, and a successful + * set or clear changes the population. Non-mutating exits skip the + * walk so idempotent requests stay O(1). + */ + if (mutated) + kvm->mem_attr_gfn_count = kvm_count_mem_attr_entries(kvm); mutex_unlock(&kvm->slots_lock); return r; --- a/include/linux/kvm_host.h 2026-07-26 16:45:48.000000000 -0500 +++ b/include/linux/kvm_host.h 2026-09-10 14:22:15.505353725 -0500 @@ -573,6 +573,13 @@ * This number must be determined not to exceed such limits. */ #define KVM_MEM_MAX_NR_PAGES ((1UL << 31) - 1) +/* + * Hardening bound: maximum number of GFNs with a materialized entry in + * mem_attr_array per VM (~300 MiB of xa_nodes at 2^25). Without it, + * KVM_SET_MEMORY_ATTRIBUTES grows the array without limit (~9.3 bytes of + * kernel memory per GFN) on GFNs with no backing memory. + */ +#define KVM_MEM_ATTR_MAX_GFNS (1UL << 25) /* * Since at idle each memslot belongs to two memslot sets it has to contain @@ -874,6 +881,7 @@ #ifdef CONFIG_KVM_GENERIC_MEMORY_ATTRIBUTES /* Protected by slots_lock (for writes) and RCU (for reads) */ struct xarray mem_attr_array; + unsigned long mem_attr_gfn_count; #endif char stats_id[KVM_STATS_NAME_SIZE]; };