KVM: Don't treat reserved xarray entries as having memory attributes

kvm_vm_set_mem_attributes() reserves an xarray entry for every gfn in
the range before storing the new attributes, so that the store loop
can't fail partway through.  If one of the reservations fails, e.g. with
-ENOMEM, the entries that were already reserved are left in the array.
That is harmless as far as xa_reserve() is concerned, as the reserved
entries read back as NULL via xa_load(), but it confuses the "does this
range have no attributes at all" check:

	if (!attrs)
		return !xas_find(&xas, end - 1);

A reserved entry is XA_ZERO_ENTRY, not NULL, and xas_find() returns it
as present.  So a leftover reservation makes KVM report that a fully
shared range has attributes even though kvm_get_memory_attributes()
returns none for every gfn in the range.  On x86, the next time
mixed-attribute tracking is recomputed for the range (memslot creation,
or a later attribute change that straddles the 2MiB page),
hugepage_has_attrs() treats a fully shared 2MiB range as mixed and
refuses to map it with a hugepage, until userspace happens to set
attributes on the range again.

Drop the shortcut and handle the !attrs case in the per-index loop,
using xas_next_entry() to find the next non-NULL entry.  xas_next_entry()
is essentially an optimized xas_find(), so the effective change is that
the !attrs lookup now goes through xas_retry() like the attrs != 0 case,
i.e. reserved entries are skipped and retry entries restart the walk.
Don't check the index when no entry is found, as the xarray leaves the
xas index in a bogus state in that case; no entry simply means the rest
of the range has no attributes.

KVM never stores a non-NULL entry with a value of zero (clearing stores
NULL), but such an entry would be returned by xas_next_entry() and trip
the index check, so WARN if one is ever seen.

Fixes: 5a475554db ("KVM: Introduce per-page memory attributes")
Cc: stable@vger.kernel.org
Suggested-by: Sean Christopherson <seanjc@google.com>
Cc: David Ballesteros <davimaba.v@proton.me>
Signed-off-by: Zeng Chi <zengchi@kylinos.cn>
Link: https://patch.msgid.link/20260921102442.1232375-1-zeng_chi911@163.com
[sean: expand comment to elaborate on xarray APIs, split optimization out]
Signed-off-by: Sean Christopherson <seanjc@google.com>
This commit is contained in:
Zeng Chi 2026-09-21 18:24:42 +08:00 committed by Sean Christopherson
parent c1214f293d
commit 277d3623d9

View File

@ -2447,14 +2447,36 @@ 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);
/*
* Lookup the entry for each index instead of iterating over the xarray
* as KVM deletes/nullifies entries to represent "no attributes", and
* the xas index is effectively invalid when no entry is found. I.e.
* matching non-zero attributes for *every* entry effectively requires
* a manually lookup for each index.
*
* Skip pre-allocated, reserved entries, or restart the lookup if the
* xarray was concurrently modified, via xas_retry() ("retry" means the
* entry holds an internal xarray value, i.e. is either invalid or NULL
* from the caller's perspective).
*
* Use xas_next() when looking for non-zero attributes to optimize for
* the case where the start of the range (or the entire range) doesn't
* have any attributes, as xas_next() returns literally the next entry,
* whereas xas_next_entry() returns the next non-NULL entry (bounded by
* a maximum index).
*/
for (index = start; index < end; index++) {
do {
entry = xas_next(&xas);
entry = attrs ? xas_next(&xas) :
xas_next_entry(&xas, end - 1);
} while (xas_retry(&xas, entry));
if (!entry)
return !attrs;
WARN_ON_ONCE(!xa_to_value(entry));
if (xas.xa_index != index ||
(xa_to_value(entry) & mask) != attrs)
return false;