[PATCH v4 2/3] KVM: Make kvm_range_has_memory_attributes() consistent about reservations

From: David Ballesteros

Date: Tue Sep 15 2026 - 13:59:00 EST


Make the !attrs fast path of kvm_range_has_memory_attributes() skip bare
reservations, so that all three of the function's query paths agree on
what an XA_ZERO_ENTRY means.

A reservation carries no attributes, and two of the three paths already
treat it as absent: the end == start + 1 path reads it through
kvm_get_memory_attributes(), which maps it to NULL via xa_load(), and the
general loop skips it via xas_retry(). Only the !attrs fast path calls
xas_find() directly, which returns the reservation as a present entry, so
it reports a range that is in fact all-shared as not-all-shared. Through
hugepage_has_attrs(), that marks a straddling hugepage mixed for a range
whose attributes are uniform.

This is a consistency fix rather than a fix for a reachable bug, and is
not tagged for stable. Every caller of kvm_range_has_memory_attributes()
holds kvm->slots_lock -- the idempotency check in
kvm_vm_set_mem_attributes(), hugepage_has_attrs() from both
kvm_arch_post_set_memory_attributes() and
kvm_mmu_init_memslot_memory_attributes(), and __kvm_gmem_populate() --
and slots_lock excludes the only writer, so with patch 1/3 applied no
caller can observe a reservation. The function should not have to depend
on that to answer consistently.

Note this patch depends on 1/3 and must not be applied without it. Today a
clear over a range that holds only reservations does not take the
idempotency early-out in kvm_vm_set_mem_attributes(), because the fast path
reports the range as not-all-clear; the clear therefore proceeds and its
xa_store(NULL) loop erases the reservations as a side effect. Teaching the
fast path to skip reservations makes that early-out fire and removes the
accidental cleanup, so the patch that stops the reservations from being
abandoned in the first place has to come first.

Found by an AI-assisted security audit.

Fixes: 5a475554db1e ("KVM: Introduce per-page memory attributes")
Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: David Ballesteros <davimaba.v@xxxxxxxxx>
---
virt/kvm/kvm_main.c | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c
index 45e7844..4e5e497 100644
--- a/virt/kvm/kvm_main.c
+++ b/virt/kvm/kvm_main.c
@@ -2446,8 +2446,17 @@ bool kvm_range_has_memory_attributes(struct kvm *kvm, gfn_t start, gfn_t end,
return (kvm_get_memory_attributes(kvm, start) & mask) == attrs;

guard(rcu)();
- if (!attrs)
- return !xas_find(&xas, end - 1);
+ if (!attrs) {
+ /*
+ * Skip reservations: a bare XA_ZERO_ENTRY carries no
+ * attributes, but xas_find() returns it raw.
+ */
+ do {
+ entry = xas_find(&xas, end - 1);
+ } while (xas_retry(&xas, entry));
+
+ return !entry;
+ }

for (index = start; index < end; index++) {
do {