Kernel KVM virtualization development
 help / color / mirror / Atom feed
* [PATCH 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES
@ 2026-09-11 18:48 David Ballesteros
  2026-09-11 18:48 ` [PATCH 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
                   ` (5 more replies)
  0 siblings, 6 replies; 16+ messages in thread
From: David Ballesteros @ 2026-09-11 18:48 UTC (permalink / raw)
  To: pbonzini; +Cc: kvm, security, David Ballesteros


Affected version/commit: v7.2-rc5 (commit 699594e8888d is not needed here;
verified on v7.2-rc5 as of 2026-09-09 and on v6.18.48; the code paths are
unchanged between them and present in mainline. The ioctl was introduced
in v6.8 with the per-page memory attributes series).

Problem
-------

kvm_vm_ioctl_set_mem_attributes() (virt/kvm/kvm_main.c) validates the
user-provided range only for representability (zero size, 64-bit wrap,
alignment, supported attributes) and not for magnitude.  With attributes
!= 0, kvm_vm_set_mem_attributes() then walks every GFN in the range:

        for (i = start; i < end; i++) {
                r = xa_reserve(&kvm->mem_attr_array, i, GFP_KERNEL_ACCOUNT);
                if (r)
                        goto out_unlock;
                cond_resched();
        }

xa_reserve() materializes the full root-to-leaf path of xa_nodes for each
GFN even though the GFNs need no backing memory and no memslots need to
exist.  Each xa_node is 576 bytes from radix_tree_node_cachep; densely
filled, the tree costs ~9.3 bytes per GFN.  On ENOMEM the loop bails out
without undoing prior reservations, and the only release path is
kvm_destroy_vm(); the memory is therefore retained across calls and lives
for as long as the VM fd.

Additionally, the intended memcg accounting does not take effect: the
nodes are allocated with GFP_NOWAIT in xas_alloc(), gaining __GFP_ACCOUNT
only when the xarray carries XA_FLAGS_ACCOUNT, which kvm does not set
(kvm_create_vm() uses plain xa_init()).  Measured: a process inside a
cgroup limited to 256 MiB grew host SReclaimable by ~450 MiB while its
memory.current stayed at ~488 KiB.

Triggering conditions
---------------------

- An unprivileged user with write access to /dev/kvm (sysfs default and
  the common udev default are 0666).
- A VM whose type sets private memory support: sw protected VM
  (CONFIG_KVM_SW_PROTECTED_VM, depends on EXPERT), TDX or SNP.
- No memslots, no guest memory and no guest CPUs are required.
- Verified outcomes on v6.18.48 (KASAN builds, isolated VMs):
  * ~9.29 bytes/GFN and ~75 MiB/s of sustained growth per VM; linear
    scaling with parallel VMs (4 VMs: 3.56x).
  * global OOM killing unrelated third-party processes while the
    attacker's own memcg stays uncharged.
  * when all surviving processes are OOM-disabled, select_bad_process()
    finds no candidate and the kernel panics ("System is deadlocked on
    memory", mm/oom_kill.c).  Reproduced twice.
  * one ioctl holds kvm->slots_lock for the whole reservation loop, so a
    large range blocks KVM_GET_DIRTY_LOG and memslot updates of that VM
    for the duration (measured GET_DIRTY_LOG latency 1us -> 7.7ms with a
    single concurrent call); KVM_RUN is unaffected.
  * replicated through a real container runtime (podman/crun) with
    --memory 512m: the container grew host slab by ~450 MiB while its
    memory.current stayed at ~488 KiB.

Reproducer status
-----------------

A reproducer is available on request (KVM_CREATE_VM + the ioctl in a
loop, plus slab/ accounting measurement), but following security-bugs.rst it is
not attached to this report.

Test environment
----------------

All dynamic results were collected in isolated inspection VMs on a
single AMD host with nested virtualization enabled
(kvm_amd nested=1): QEMU -cpu host, Debian 13 guests, guest kernels
built from the audited tree (v6.18.48) and from v7.2-rc5 with
CONFIG_KVM_SW_PROTECTED_VM=y / CONFIG_KVM_AMD=y / CONFIG_EXPERT=y and
KASAN (with CONFIG_KASAN_VMALLOC=y where noted); the PoC runs as root
inside the guest, host networking restricted (user-mode net,
restrict=on).  Nothing in the host kernels themselves was modified or
attacked.  The container replication used podman 5.4.2/crun inside the
guest with the host kernel having the gate enabled (a stock host kernel
without KVM_SW_PROTECTED_VM rejects KVM_CREATE_VM and the surface is
absent, as expected).

Proposed fix
------------

Attached as 0001-v3.patch (checkpatch clean, build warning-free on
v7.2-rc5, runtime-verified on v7.2-rc5): it bounds the total number of
materialized attribute entries per VM to KVM_MEM_ATTR_MAX_GFNS (2^25
GFNs = 128 GiB of GPA, ~300 MiB of xa_nodes), enforced under
kvm->slots_lock after the idempotency early-out.  A per-VM counter is
resynchronized from the xarray only when an ioctl mutates the array
(ENOMEM partials, successful sets and clears), so idempotent re-sets
and rejected requests stay O(1) — runtime-verified: a 4 KiB idempotent
re-set on an exhausted VM returns in ~4us; a fresh range on an
exhausted VM is rejected with -ENOSPC in ~17us; the bound cuts growth
at 2^25 GFNs.

Patch 2/2 restores the intended accounting with a one-liner; it
attributes the memory but does not bound it (see the analysis below):

-       xa_init(&kvm->mem_attr_array);
+       xa_init_flags(&kvm->mem_attr_array, XA_FLAGS_ACCOUNT);

Design notes for reviewers: the budget value (2^25 GFNs) is a trade-off
— it is well above any current consumer's usage but small next to the
GPA space TDX/SNP guests can have; a tunable or a symmetric decrement on
clear are alternatives if maintainers prefer.  The -ENOSPC errno
distinguishes the bound from the malformed-input -EINVAL paths.

Why a constant and not a derived bound?

We considered bounding materialization by what the VM has actually
declared instead of a constant, and rejected it because the declaration
itself is not proportional.  Two candidate signals:

- Memslot coverage ("reject ranges not covered by any memslot"):
  semantically attractive — attributes on unbacked GFNs are inert state
  with no consumer (kvm_handle_gfn_range only walks real memslots), and
  the only in-tree consumer (tools/testing/selftests/kvm, private_mem_
  conversions_test.c) sets attributes strictly inside its gmem memslot.
  But it is not a bound: a memslot requires only a MAP_NORESERVE userspace
  VMA (no committed pages); its kernel cost is the lpage_info metadata
  (4 bytes/entry, __vcalloc proportional to npages: ~16 MiB for a
  maximum 8 TiB memslot) while the same memslot's GPA span materializes
  ~19.9 GiB of xa_nodes — a ~1200:1 amplification of declared metadata.
  Three or four maximum-size NORESERVE memslots suffice to exhaust a
  64 GiB host.  As a semantic follow-up that eliminates the zero-setup
  variant of the attack it is worth considering on top of the bound.

- Sum of memslot pages as a dynamic budget: same flaw — the attacker
  raises his own budget by declaring free memslots; without a constant
  the ceiling becomes the physical address space.

Per-VM accounting to the caller's memcg (patch 2/2) attributes the
memory but bounds only cgroup-limited tenants; with the common
/dev/kvm 0666 default and unlimited cgroups it does not bound the host.
Hence the layering we propose: accounting (2/2) + hard per-VM bound
(1/2), with memslot-coverage rejection as an optional semantic layer
the maintainers may prefer on top.

Mitigations
-----------

- Restrict /dev/kvm (mode 0660 root:kvm) where the default 0666 is in
  place.
- Monitor SReclaimable and /sys/kernel/slab/radix_tree_node/objects, not
  SUnreclaim: the growth is reclaimable-accounted and never reclaimed.
- Per-tenant memcg limits do NOT mitigate the common path today (see
  above), which is why the counter bound is the effective fix.

Notes
-----

- Not verified: the TDX/SNP hardware paths end-to-end (no hardware);
  the software-protected path was verified end-to-end on both versions.

--
2.47.0



^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES
  2026-09-11 18:48 [PATCH 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
@ 2026-09-11 18:48 ` David Ballesteros
  2026-09-11 19:05   ` sashiko-bot
  2026-09-11 18:48 ` [PATCH 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
                   ` (4 subsequent siblings)
  5 siblings, 1 reply; 16+ messages in thread
From: David Ballesteros @ 2026-09-11 18:48 UTC (permalink / raw)
  To: pbonzini; +Cc: kvm, security, David Ballesteros

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 <davimaba.v@proton.me>
---
--- 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];
 };


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 2/2] KVM: Account mem_attr_array nodes to the caller's memcg
  2026-09-11 18:48 [PATCH 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
  2026-09-11 18:48 ` [PATCH 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
@ 2026-09-11 18:48 ` David Ballesteros
  2026-09-11 19:02   ` sashiko-bot
  2026-09-11 20:32 ` [PATCH v2 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
                   ` (3 subsequent siblings)
  5 siblings, 1 reply; 16+ messages in thread
From: David Ballesteros @ 2026-09-11 18:48 UTC (permalink / raw)
  To: pbonzini; +Cc: kvm, security, David Ballesteros

kvm_vm_set_mem_attributes() passes GFP_KERNEL_ACCOUNT when reserving
xarray entries, but the nodes are allocated by xas_alloc() with
GFP_NOWAIT, which only adds __GFP_ACCOUNT when the xarray carries
XA_FLAGS_ACCOUNT.  mem_attr_array is initialized with plain xa_init(),
so the flag is never set and the nodes are never charged to the caller:
measured on v6.18.48 and v7.2-rc5, a process inside a cgroup limited to
256 MiB grew host SReclaimable by ~450 MiB while its memory.current
stayed at ~488 KiB.  Per-tenant memcg limits therefore do not contain
the growth described in the previous patch.

Set XA_FLAGS_ACCOUNT so that the intended accounting takes effect.
This is a mitigation, not a bound (it does not limit growth); both
patches are needed to bound and attribute the memory.

Runtime-verified on v6.18.48 (KASAN build, isolated VM): with this flag,
a process in a cgroup with memory.max=256M is contained by the cgroup
limit (allocation fails cleanly and, at the limit, the memcg OOM killer
selects the attacker itself) instead of exhausting global memory, and
cgroup memory.current tracks the materialized nodes one to one.

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 <davimaba.v@proton.me>
---
--- a/virt/kvm/kvm_main.c	2026-07-26 16:45:48.000000000 -0500
+++ b/virt/kvm/kvm_main.c	2026-09-10 14:20:00.000000000 -0500
@@ -1117,7 +1117,7 @@
 	xa_init(&kvm->vcpu_array);
 #ifdef CONFIG_KVM_GENERIC_MEMORY_ATTRIBUTES
-	xa_init(&kvm->mem_attr_array);
+	xa_init_flags(&kvm->mem_attr_array, XA_FLAGS_ACCOUNT);
 	kvm->mem_attr_gfn_count = 0;
 #endif
 
 	INIT_LIST_HEAD(&kvm->gpc_list);


^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [PATCH 2/2] KVM: Account mem_attr_array nodes to the caller's memcg
  2026-09-11 18:48 ` [PATCH 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
@ 2026-09-11 19:02   ` sashiko-bot
  0 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-11 19:02 UTC (permalink / raw)
  To: David Ballesteros; +Cc: kvm

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

Pre-existing issues:
- [High] Bypass of `KVM_MEM_ATTR_MAX_GFNS` limit and missing error cleanup when clearing memory attributes leads to memcg exhaustion and stranded XArray nodes.
- [High] The patch fixes a memcg accounting bypass for `mem_attr_array` by adding `XA_FLAGS_ACCOUNT`, but misses other KVM xarrays that use `GFP_KERNEL_ACCOUNT` for insertion and share the exact same vulnerability.
--

commit 32c232444c0ef12bbcc23f63ec3c6ef9046e4c0d
Author: David Ballesteros <davimaba.v@proton.me>

KVM: Account mem_attr_array nodes to the caller's memcg

kvm_vm_set_mem_attributes() passes GFP_KERNEL_ACCOUNT when reserving
xarray entries, but the nodes are allocated by xas_alloc() with
GFP_NOWAIT, which only adds __GFP_ACCOUNT when the xarray carries
XA_FLAGS_ACCOUNT.  mem_attr_array is initialized with plain xa_init(),
so the flag is never set and the nodes are never charged to the caller.

> diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c
> index f07d2ae9a4ad0..25c1ff0c2a93e 100644
> --- a/virt/kvm/kvm_main.c
> +++ b/virt/kvm/kvm_main.c
> @@ -1127,7 +1127,7 @@ static struct kvm *kvm_create_vm(unsigned long type, const char *fdname)
>  	rcuwait_init(&kvm->mn_memslots_update_rcuwait);
>  	xa_init(&kvm->vcpu_array);
        ^^^^^^^^^^^^^^^^^^^^^^^^^

[Severity: High]
This isn't a bug introduced by this patch, but does vcpu_array have the
exact same memcg accounting bypass?

Looking at vcpu_array, it uses GFP_KERNEL_ACCOUNT for node insertion but
lacks XA_FLAGS_ACCOUNT during initialization. Since the xas_alloc fast-path
uses GFP_NOWAIT, it relies on XA_FLAGS_ACCOUNT to charge the memcg.

Are there other KVM xarrays that share this vulnerability? For example, in
ARM64:
- its->translation_cache in arch/arm64/kvm/vgic/vgic-its.c:vgic_its_create()
- dist->lpi_xa in arch/arm64/kvm/vgic/vgic-init.c:kvm_vgic_dist_init()

>  #ifdef CONFIG_KVM_GENERIC_MEMORY_ATTRIBUTES
> -	xa_init(&kvm->mem_attr_array);
> +	xa_init_flags(&kvm->mem_attr_array, XA_FLAGS_ACCOUNT);

[Severity: High]
This is a pre-existing issue, but does kvm_vm_set_mem_attributes() bypass
the KVM_MEM_ATTR_MAX_GFNS limit and leak xarray nodes on error?

When KVM_SET_MEMORY_ATTRIBUTES is called with attributes == 0 and a massive
address range, the KVM_MEM_ATTR_MAX_GFNS bounds check is bypassed:

virt/kvm/kvm_main.c:kvm_vm_set_mem_attributes() {
	...
	if (attributes &&
	    kvm->mem_attr_gfn_count + (end - start) > KVM_MEM_ATTR_MAX_GFNS) {
		r = -ENOSPC;
		goto out_unlock;
	}
	...

Then xa_reserve() unconditionally allocates nodes for the entire range. If
an allocation fails partway through, the error path bails out without
cleaning up the successfully reserved entries:

	for (i = start; i < end; i++) {
		r = xa_reserve(&kvm->mem_attr_array, i, GFP_KERNEL_ACCOUNT);
		if (r)
			goto out_unlock;

		cond_resched();
	}
	...
out_unlock:
	mutex_unlock(&kvm->slots_lock);
	return r;
}

Can an unprivileged userspace process repeatedly call this with
attributes == 0 to exhaust the memcg limit, leaving permanently stranded
memory allocations?

>  	kvm->mem_attr_gfn_count = 0;
>  #endif

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260911184819.101123-1-davimaba.v@proton.me?part=2

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [PATCH 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES
  2026-09-11 18:48 ` [PATCH 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
@ 2026-09-11 19:05   ` sashiko-bot
  0 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-11 19:05 UTC (permalink / raw)
  To: David Ballesteros; +Cc: kvm

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:

New issues:
- [High] Recounting materialized entries on every mutation is an O(N) operation that causes severe CPU exhaustion.
- [High] The arbitrary 128 GiB limit (KVM_MEM_ATTR_MAX_GFNS) introduces a severe functional regression for large confidential VMs.
- [Medium] Iterating the XArray using xas_for_each() without holding rcu_read_lock() triggers lockdep warnings.

Pre-existing issues:
- [High] The KVM_MEM_ATTR_MAX_GFNS limit check is bypassed when clearing attributes, failing to fix the memory exhaustion DoS.
--

commit 200db6833ee43dd9cdcad914e3c4e7942e9f7e5a
Author: David Ballesteros <davimaba.v@proton.me>
KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES

This commit attempts to fix a memory exhaustion denial of service by
capping the maximum number of materialized memory attribute GFNs per VM.
It introduces a counter for materialized entries and restricts updates
exceeding a hardcoded limit.

> diff --git a/include/linux/kvm_host.h b/include/linux/kvm_host.h
> --- a/include/linux/kvm_host.h
> +++ b/include/linux/kvm_host.h
> @@ -573,6 +573,13 @@ static inline int kvm_vcpu_exiting_guest_mode(struct kvm_vcpu *vcpu)
>  #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)

[Severity: High]
Does this limit cause legitimate large enterprise confidential VMs to fail?

A limit of 1UL << 25 pages equates to exactly 128 GiB of memory on systems
using a 4 KiB page size. Since AMD SEV-SNP and Intel TDX VMs routinely require
hundreds of gigabytes or even terabytes of private memory, capping the
allowed private memory to 128 GiB per VM seems like a functional regression
that will break existing large setups with -ENOSPC.

> diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c
> --- a/virt/kvm/kvm_main.c
> +++ b/virt/kvm/kvm_main.c
> @@ -2544,6 +2545,33 @@ static bool kvm_pre_set_memory_attributes(struct kvm *kvm,
> [ ... ]
> +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) {

[Severity: Medium]
Will this xas_for_each() loop trigger lockdep warnings when PROVE_RCU is
enabled? 

The caller holds the slots_lock, but iterating the XArray using this
macro implicitly dereferences node pointers. XArray requires either
rcu_read_lock() or the internal xa_lock to satisfy its lockdep invariants
during traversal, neither of which appear to be held here.

> @@ -2579,6 +2608,23 @@ static int kvm_vm_set_mem_attributes(struct kvm *kvm, gfn_t start, gfn_t end,
> [ ... ]
> +	if (attributes &&
> +	    kvm->mem_attr_gfn_count + (end - start) > KVM_MEM_ATTR_MAX_GFNS) {
> +		r = -ENOSPC;
> +		goto out_unlock;
> +	}

[Severity: High]
This is a pre-existing issue, but does bypassing the limit check when clearing
attributes re-expose the vulnerability?

Because this check requires attributes to be non-zero, a caller can set a
single attribute at index 0, and then call KVM_SET_MEMORY_ATTRIBUTES with
attributes set to 0 for an enormous range. The kvm_range_has_memory_attributes()
function will evaluate to false due to the index 0 entry, and this limit check
will be bypassed.

> [ ... ]
>  	for (i = start; i < end; i++) {
>  		r = xa_reserve(&kvm->mem_attr_array, i, GFP_KERNEL_ACCOUNT);
>  		if (r)
>  			goto out_unlock;

...which causes this subsequent loop to unconditionally reserve XArray nodes for
billions of GFNs until the host hits ENOMEM, exiting without freeing the leaked
reservations.

> [ ... ]
>  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);

[Severity: High]
Does this trigger a severe CPU denial of service during standard memory updates?

When a guest issues a series of 1-page memory attribute updates, this mutated
flag will be true for each successful update. Calling kvm_count_mem_attr_entries()
then iterates over up to 33.5 million populated entries in the XArray while holding
the slots_lock.

Performing an O(N) iteration that takes tens of milliseconds on every successful
mutation yields an O(N^2) complexity that ties up the CPU and monopolizes the
slots_lock for long periods, blocking memslot and dirty logging operations.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260911184819.101123-1-davimaba.v@proton.me?part=1

^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH v2 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES
  2026-09-11 18:48 [PATCH 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
  2026-09-11 18:48 ` [PATCH 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
  2026-09-11 18:48 ` [PATCH 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
@ 2026-09-11 20:32 ` David Ballesteros
  2026-09-11 21:19   ` David Ballesteros
  2026-09-11 22:13   ` [PATCH v3 " David Ballesteros
  2026-09-11 20:32 ` [PATCH v2 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
                   ` (2 subsequent siblings)
  5 siblings, 2 replies; 16+ messages in thread
From: David Ballesteros @ 2026-09-11 20:32 UTC (permalink / raw)
  To: pbonzini; +Cc: kvm, security, David Ballesteros

Fair bot, you win.  The clear-path bound bypass was real, and v2 of
this series exists because the automated review caught it before a
human did.

Changes in v2 (thanks to the sashiko review of v1):

- Reworked the clear path to erase-only iteration over present
  entries.  v1 could bypass the bound: a clear of a partially
  populated range went through xa_reserve() for every GFN in the
  range with no bound check, so alternating set/clear of huge
  ranges materialized kernel memory without limit.  Erasing cannot
  allocate, so clears are now failure-free by construction and
  bounded by the population they actually remove.  Runtime-verified:
  eight consecutive clears of a 2^52-GFN range containing a
  populated GFN leave Slab unchanged (v1 grew it to ENOMEM).
- The per-VM counter is now maintained incrementally (sets charge the
  full range, conservatively; clears subtract the exact number of
  entries erased) and a full rescan runs only on the ENOMEM path.
  This removes the O(array population) cost per mutating ioctl from
  v1, which was significant for legitimate confidential-VM workloads
  converting small ranges on a large array.  Runtime-verified: 50
  alternating 4 KiB set/clear conversions at a near-full array take
  0.01 ms each.
- Added guard(rcu)() to the rescan helper for consistency with
  kvm_range_has_memory_attributes() and to satisfy lockdep.
- No change to patch 2.

On the other review points: the 128 GiB budget value remains a
deliberate trade-off, discussed under "Why a constant and not a
derived bound" below; and the other KVM xarrays do not share this
exposure because their indices are bounded by construction
(vcpu_array by the KVM_MAX_VCPUS check in KVM_CREATE_VCPU, gmem
bindings by KVM_MEM_SLOTS_NUM), unlike mem_attr_array whose index
range is the user-chosen GFN range itself.


Affected version/commit: v7.2-rc5 (commit 699594e8888d is not needed here;
verified on v7.2-rc5 as of 2026-09-09 and on v6.18.48; the code paths are
unchanged between them and present in mainline. The ioctl was introduced
in v6.8 with the per-page memory attributes series).

Problem
-------

kvm_vm_ioctl_set_mem_attributes() (virt/kvm/kvm_main.c) validates the
user-provided range only for representability (zero size, 64-bit wrap,
alignment, supported attributes) and not for magnitude.  With attributes
!= 0, kvm_vm_set_mem_attributes() then walks every GFN in the range:

        for (i = start; i < end; i++) {
                r = xa_reserve(&kvm->mem_attr_array, i, GFP_KERNEL_ACCOUNT);
                if (r)
                        goto out_unlock;
                cond_resched();
        }

xa_reserve() materializes the full root-to-leaf path of xa_nodes for each
GFN even though the GFNs need no backing memory and no memslots need to
exist.  Each xa_node is 576 bytes from radix_tree_node_cachep; densely
filled, the tree costs ~9.3 bytes per GFN.  On ENOMEM the loop bails out
without undoing prior reservations, and the only release path is
kvm_destroy_vm(); the memory is therefore retained across calls and lives
for as long as the VM fd.

Additionally, the intended memcg accounting does not take effect: the
nodes are allocated with GFP_NOWAIT in xas_alloc(), gaining __GFP_ACCOUNT
only when the xarray carries XA_FLAGS_ACCOUNT, which kvm does not set
(kvm_create_vm() uses plain xa_init()).  Measured: a process inside a
cgroup limited to 256 MiB grew host SReclaimable by ~450 MiB while its
memory.current stayed at ~488 KiB.

Triggering conditions
---------------------

- An unprivileged user with write access to /dev/kvm (sysfs default and
  the common udev default are 0666).
- A VM whose type sets private memory support: sw protected VM
  (CONFIG_KVM_SW_PROTECTED_VM, depends on EXPERT), TDX or SNP.
- No memslots, no guest memory and no guest CPUs are required.
- Verified outcomes on v6.18.48 (KASAN builds, isolated VMs):
  * ~9.29 bytes/GFN and ~75 MiB/s of sustained growth per VM; linear
    scaling with parallel VMs (4 VMs: 3.56x).
  * global OOM killing unrelated third-party processes while the
    attacker's own memcg stays uncharged.
  * when all surviving processes are OOM-disabled, select_bad_process()
    finds no candidate and the kernel panics ("System is deadlocked on
    memory", mm/oom_kill.c).  Reproduced twice.
  * one ioctl holds kvm->slots_lock for the whole reservation loop, so a
    large range blocks KVM_GET_DIRTY_LOG and memslot updates of that VM
    for the duration (measured GET_DIRTY_LOG latency 1us -> 7.7ms with a
    single concurrent call); KVM_RUN is unaffected.
  * replicated through a real container runtime (podman/crun) with
    --memory 512m: the container grew host slab by ~450 MiB while its
    memory.current stayed at ~488 KiB.

Reproducer status
-----------------

A reproducer is available on request (KVM_CREATE_VM + the ioctl in a
loop, plus slab/ accounting measurement), but following security-bugs.rst it is
not attached to this report.

Test environment
----------------

All dynamic results were collected in isolated inspection VMs on a
single AMD host with nested virtualization enabled
(kvm_amd nested=1): QEMU -cpu host, Debian 13 guests, guest kernels
built from the audited tree (v6.18.48) and from v7.2-rc5 with
CONFIG_KVM_SW_PROTECTED_VM=y / CONFIG_KVM_AMD=y / CONFIG_EXPERT=y and
KASAN (with CONFIG_KASAN_VMALLOC=y where noted); the PoC runs as root
inside the guest, host networking restricted (user-mode net,
restrict=on).  Nothing in the host kernels themselves was modified or
attacked.  The container replication used podman 5.4.2/crun inside the
guest with the host kernel having the gate enabled (a stock host kernel
without KVM_SW_PROTECTED_VM rejects KVM_CREATE_VM and the surface is
absent, as expected).

Proposed fix
------------

Attached as 0001-v3.patch (checkpatch clean, build warning-free on
v7.2-rc5, runtime-verified on v7.2-rc5): it bounds the total number of
materialized attribute entries per VM to KVM_MEM_ATTR_MAX_GFNS (2^25
GFNs = 128 GiB of GPA, ~300 MiB of xa_nodes), enforced under
kvm->slots_lock after the idempotency early-out.  A per-VM counter is
resynchronized from the xarray only when an ioctl mutates the array
(ENOMEM partials, successful sets and clears), so idempotent re-sets
and rejected requests stay O(1) — runtime-verified: a 4 KiB idempotent
re-set on an exhausted VM returns in ~4us; a fresh range on an
exhausted VM is rejected with -ENOSPC in ~17us; the bound cuts growth
at 2^25 GFNs.

Patch 2/2 restores the intended accounting with a one-liner; it
attributes the memory but does not bound it (see the analysis below):

-       xa_init(&kvm->mem_attr_array);
+       xa_init_flags(&kvm->mem_attr_array, XA_FLAGS_ACCOUNT);

Design notes for reviewers: the budget value (2^25 GFNs) is a trade-off
— it is well above any current consumer's usage but small next to the
GPA space TDX/SNP guests can have; a tunable or a symmetric decrement on
clear are alternatives if maintainers prefer.  The -ENOSPC errno
distinguishes the bound from the malformed-input -EINVAL paths.

Why a constant and not a derived bound?

We considered bounding materialization by what the VM has actually
declared instead of a constant, and rejected it because the declaration
itself is not proportional.  Two candidate signals:

- Memslot coverage ("reject ranges not covered by any memslot"):
  semantically attractive — attributes on unbacked GFNs are inert state
  with no consumer (kvm_handle_gfn_range only walks real memslots), and
  the only in-tree consumer (tools/testing/selftests/kvm, private_mem_
  conversions_test.c) sets attributes strictly inside its gmem memslot.
  But it is not a bound: a memslot requires only a MAP_NORESERVE userspace
  VMA (no committed pages); its kernel cost is the lpage_info metadata
  (4 bytes/entry, __vcalloc proportional to npages: ~16 MiB for a
  maximum 8 TiB memslot) while the same memslot's GPA span materializes
  ~19.9 GiB of xa_nodes — a ~1200:1 amplification of declared metadata.
  Three or four maximum-size NORESERVE memslots suffice to exhaust a
  64 GiB host.  As a semantic follow-up that eliminates the zero-setup
  variant of the attack it is worth considering on top of the bound.

- Sum of memslot pages as a dynamic budget: same flaw — the attacker
  raises his own budget by declaring free memslots; without a constant
  the ceiling becomes the physical address space.

Per-VM accounting to the caller's memcg (patch 2/2) attributes the
memory but bounds only cgroup-limited tenants; with the common
/dev/kvm 0666 default and unlimited cgroups it does not bound the host.
Hence the layering we propose: accounting (2/2) + hard per-VM bound
(1/2), with memslot-coverage rejection as an optional semantic layer
the maintainers may prefer on top.

Mitigations
-----------

- Restrict /dev/kvm (mode 0660 root:kvm) where the default 0666 is in
  place.
- Monitor SReclaimable and /sys/kernel/slab/radix_tree_node/objects, not
  SUnreclaim: the growth is reclaimable-accounted and never reclaimed.
- Per-tenant memcg limits do NOT mitigate the common path today (see
  above), which is why the counter bound is the effective fix.

Notes
-----

- Not verified: the TDX/SNP hardware paths end-to-end (no hardware);
  the software-protected path was verified end-to-end on both versions.

--
2.55.0



^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH v2 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES
  2026-09-11 18:48 [PATCH 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
                   ` (2 preceding siblings ...)
  2026-09-11 20:32 ` [PATCH v2 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
@ 2026-09-11 20:32 ` David Ballesteros
  2026-09-11 20:45   ` sashiko-bot
  2026-09-11 20:32 ` [PATCH v2 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
  2026-09-12  8:48 ` [syzbot ci] Re: KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES syzbot ci
  5 siblings, 1 reply; 16+ messages in thread
From: David Ballesteros @ 2026-09-11 20:32 UTC (permalink / raw)
  To: pbonzini; +Cc: kvm, security, David Ballesteros

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 number of attribute entries materialized 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.

Clearing requests are reworked to never touch the reservation path:
erasing an entry cannot allocate, so a clear iterates only the present
entries in the range and erases them.  This closes a bypass of the
bound found in review of v1 (thanks to the sashiko reviewer): a clear
of a partially populated range used to reserve every GFN in the range
without any bound check, so alternating set/clear of huge ranges could
materialize kernel memory without limit.  It also makes clears
allocation-failure-free by construction.

The per-VM counter is maintained incrementally: sets charge the full
range (overcounting overlapping requests, which is conservative for a
bound), clears subtract the exact number of entries erased, and a full
rescan runs only on the ENOMEM path, where partial reservations are
retained and the exact count is otherwise unknown.  This keeps every
non-failing ioctl O(present entries touched) instead of O(array
population), which matters for confidential-VM workloads converting
small ranges when the array is large.

Build-tested warning-free on v7.2-rc5.  Runtime-verified on v7.2-rc5
(KASAN build, isolated VM): the bound stops growth with a clean
-ENOSPC at 2^25 GFNs; repeated clears of huge (up to 2^52-GFN) ranges
that include a populated GFN leave Slab unchanged instead of growing
it; 50 alternating 4 KiB set/clear conversions at a near-full array
complete in 0.01 ms each; clearing a range returns budget exactly (a
fresh range of the freed size is accepted afterwards).

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 <davimaba.v@proton.me>
---
--- a/virt/kvm/kvm_main.c	2026-07-26 16:45:48.000000000 -0500
+++ b/virt/kvm/kvm_main.c	2026-09-11 15:10:52.234761758 -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,34 @@
 	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;
+
+	guard(rcu)();
+	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)
@@ -2555,6 +2584,7 @@
 		.may_block = true,
 	};
 	unsigned long i;
+	unsigned long freed = 0;
 	void *entry;
 	int r = 0;
 
@@ -2569,13 +2599,56 @@
 		goto out_unlock;
 
 	/*
+	 * Clearing never materializes entries: erasing cannot allocate, so
+	 * there is no reservation phase, no bound to enforce and no way to
+	 * fail partway through.  Iterate the present entries only (absent
+	 * spans are skipped by the xarray walk) and erase them; xa_erase()
+	 * takes the xa_lock per entry and node frees are RCU-deferred, so
+	 * the iterator is safe under the RCU lock while kvm->slots_lock
+	 * excludes other writers.
+	 */
+	if (!attributes) {
+		XA_STATE(xas, &kvm->mem_attr_array, start);
+
+		kvm_handle_gfn_range(kvm, &pre_set_range);
+
+		guard(rcu)();
+		xas_for_each(&xas, entry, end - 1) {
+			if (xa_is_retry(entry)) {
+				xas_pause(&xas);
+				continue;
+			}
+			xa_erase(&kvm->mem_attr_array, xas.xa_index);
+			freed++;
+			cond_resched();
+		}
+
+		kvm_handle_gfn_range(kvm, &post_set_range);
+
+		/* Exactly @freed entries left the array; no resync needed. */
+		kvm->mem_attr_gfn_count -= freed;
+		mutex_unlock(&kvm->slots_lock);
+
+		return 0;
+	}
+
+	/*
+	 * Bound the number of materialized GFNs per VM.  See the comment on
+	 * KVM_MEM_ATTR_MAX_GFNS.
+	 */
+	if (kvm->mem_attr_gfn_count + (end - start) > KVM_MEM_ATTR_MAX_GFNS) {
+		r = -ENOSPC;
+		goto out_unlock;
+	}
+
+	/*
 	 * Reserve memory ahead of time to avoid having to deal with failures
 	 * partway through setting the new attributes.
 	 */
 	for (i = start; i < end; i++) {
 		r = xa_reserve(&kvm->mem_attr_array, i, GFP_KERNEL_ACCOUNT);
 		if (r)
-			goto out_unlock;
+			goto enomem_resync;
 
 		cond_resched();
 	}
@@ -2591,6 +2664,24 @@
 
 	kvm_handle_gfn_range(kvm, &post_set_range);
 
+	/*
+	 * Charge the full range: overlapping requests may overcount, which
+	 * is conservative for a bound and keeps this O(1) instead of a
+	 * rescan of the array.
+	 */
+	kvm->mem_attr_gfn_count += end - start;
+	mutex_unlock(&kvm->slots_lock);
+
+	return 0;
+
+enomem_resync:
+	/*
+	 * Reservations made so far are retained; recount to restore the
+	 * invariant that the counter matches the array population.  This is
+	 * the only path that can leave the counter stale, and it is rare by
+	 * construction (it requires ENOMEM under the bound).
+	 */
+	kvm->mem_attr_gfn_count = kvm_count_mem_attr_entries(kvm);
 out_unlock:
 	mutex_unlock(&kvm->slots_lock);
 
--- 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];
 };


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH v2 2/2] KVM: Account mem_attr_array nodes to the caller's memcg
  2026-09-11 18:48 [PATCH 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
                   ` (3 preceding siblings ...)
  2026-09-11 20:32 ` [PATCH v2 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
@ 2026-09-11 20:32 ` David Ballesteros
  2026-09-11 20:44   ` sashiko-bot
  2026-09-12  8:48 ` [syzbot ci] Re: KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES syzbot ci
  5 siblings, 1 reply; 16+ messages in thread
From: David Ballesteros @ 2026-09-11 20:32 UTC (permalink / raw)
  To: pbonzini; +Cc: kvm, security, David Ballesteros

kvm_vm_set_mem_attributes() passes GFP_KERNEL_ACCOUNT when reserving
xarray entries, but the nodes are allocated by xas_alloc() with
GFP_NOWAIT, which only adds __GFP_ACCOUNT when the xarray carries
XA_FLAGS_ACCOUNT.  mem_attr_array is initialized with plain xa_init(),
so the flag is never set and the nodes are never charged to the caller:
measured on v6.18.48 and v7.2-rc5, a process inside a cgroup limited to
256 MiB grew host SReclaimable by ~450 MiB while its memory.current
stayed at ~488 KiB.  Per-tenant memcg limits therefore do not contain
the growth described in the previous patch.

Set XA_FLAGS_ACCOUNT so that the intended accounting takes effect.
This is a mitigation, not a bound (it does not limit growth); both
patches are needed to bound and attribute the memory.

Runtime-verified on v6.18.48 (KASAN build, isolated VM): with this flag,
a process in a cgroup with memory.max=256M is contained by the cgroup
limit (allocation fails cleanly and, at the limit, the memcg OOM killer
selects the attacker itself) instead of exhausting global memory, and
cgroup memory.current tracks the materialized nodes one to one.

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 <davimaba.v@proton.me>
---
--- a/virt/kvm/kvm_main.c	2026-07-26 16:45:48.000000000 -0500
+++ b/virt/kvm/kvm_main.c	2026-09-10 14:20:00.000000000 -0500
@@ -1117,7 +1117,7 @@
 	xa_init(&kvm->vcpu_array);
 #ifdef CONFIG_KVM_GENERIC_MEMORY_ATTRIBUTES
-	xa_init(&kvm->mem_attr_array);
+	xa_init_flags(&kvm->mem_attr_array, XA_FLAGS_ACCOUNT);
 	kvm->mem_attr_gfn_count = 0;
 #endif

 	INIT_LIST_HEAD(&kvm->gpc_list);


^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [PATCH v2 2/2] KVM: Account mem_attr_array nodes to the caller's memcg
  2026-09-11 20:32 ` [PATCH v2 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
@ 2026-09-11 20:44   ` sashiko-bot
  0 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-11 20:44 UTC (permalink / raw)
  To: David Ballesteros; +Cc: kvm

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [High] Unconditional cond_resched() and blocking MMU invalidations are called inside an RCU read-side critical section in KVM memory attribute paths.
--

commit c6abdd494ec28371324dab717f8c1bf7e27a654d
Author: David Ballesteros <davimaba.v@proton.me>

KVM: Account mem_attr_array nodes to the caller's memcg

This patch sets XA_FLAGS_ACCOUNT for mem_attr_array so that nodes allocated
by xas_alloc() with GFP_NOWAIT are properly charged to the caller's memcg.
This ensures per-tenant memory limits are respected and bounds the growth
of host memory allocations.

> diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c
> index f623c5f07344e..14ca73aedbaf9 100644
> --- a/virt/kvm/kvm_main.c
> +++ b/virt/kvm/kvm_main.c
> @@ -1127,7 +1127,7 @@ static struct kvm *kvm_create_vm(unsigned long type, const char *fdname)
>  	rcuwait_init(&kvm->mn_memslots_update_rcuwait);
>  	xa_init(&kvm->vcpu_array);
>  #ifdef CONFIG_KVM_GENERIC_MEMORY_ATTRIBUTES
> -	xa_init(&kvm->mem_attr_array);
> +	xa_init_flags(&kvm->mem_attr_array, XA_FLAGS_ACCOUNT);
>  	kvm->mem_attr_gfn_count = 0;
>  #endif

While reviewing this, I noticed some pre-existing regressions in the same file
related to the memory attribute paths:

virt/kvm/kvm_main.c:kvm_count_mem_attr_entries() {
    ...
    guard(rcu)();
    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();

[Severity: High]
This isn't a bug introduced by this patch, but does this call cond_resched()
inside the block-scoped RCU read-side section established by guard(rcu)()?

        }
    }
    ...
}


virt/kvm/kvm_main.c:kvm_vm_set_mem_attributes() {
    ...
    if (!attributes) {
        XA_STATE(xas, &kvm->mem_attr_array, start);

        kvm_handle_gfn_range(kvm, &pre_set_range);

        guard(rcu)();
        xas_for_each(&xas, entry, end - 1) {
            if (xa_is_retry(entry)) {
                xas_pause(&xas);
                continue;
            }
            xa_erase(&kvm->mem_attr_array, xas.xa_index);
            freed++;
            cond_resched();

[Severity: High]
This is a pre-existing issue, but would this unconditional cond_resched()
also execute within the RCU read lock scope established by guard(rcu)()?

        }

        kvm_handle_gfn_range(kvm, &post_set_range);

[Severity: High]
This is a pre-existing issue, but is a potentially blocking operation executed
via kvm_handle_gfn_range() (where may_block = true) before the block-scoped
guard(rcu)() ends?

        /* Exactly @freed entries left the array; no resync needed. */
        ...
}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260911184819.101123-1-davimaba.v@proton.me?part=2

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [PATCH v2 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES
  2026-09-11 20:32 ` [PATCH v2 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
@ 2026-09-11 20:45   ` sashiko-bot
  0 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-11 20:45 UTC (permalink / raw)
  To: David Ballesteros; +Cc: kvm

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Sleepable functions called inside an RCU read-side critical section established by `guard(rcu)()`.
- [High] Asymmetric accounting of `mem_attr_gfn_count` allows permanent Denial of Service via budget exhaustion.
- [High] XArray iterator corruption by modifying the array with `xa_erase()` during lockless `xas_for_each()` iteration.
--

commit 3f5796ac8ce025c5b7da5cd6adbb4331967a81b4
Author: David Ballesteros <davimaba.v@proton.me>

KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES

This commit bounds the number of GFNs materialized per VM in
KVM_SET_MEMORY_ATTRIBUTES to prevent kernel memory exhaustion. It enforces a
limit under kvm->slots_lock and reworks clearing requests to avoid allocations
by erasing entries directly. The per-VM counter is maintained incrementally to
improve performance on non-failing paths.

> diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c
> index 108d42c5c1d6a..f623c5f07344e 100644
> --- a/virt/kvm/kvm_main.c
> +++ b/virt/kvm/kvm_main.c
[ ... ]
> @@ -2544,6 +2545,34 @@ static bool kvm_pre_set_memory_attributes(struct kvm *kvm,
>  	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;
> +
> +	guard(rcu)();
> +	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();

[Severity: High]
Does this code call cond_resched() while holding the RCU read lock?

Since guard(rcu)() establishes a function-scoped RCU read-side critical section,
the lock is held until the function returns. Calling cond_resched() inside
this loop could lead to scheduling while atomic panics or RCU grace period
stalls.

> +		}
> +	}
> +
> +	return count;
> +}
> +
[ ... ]
> @@ -2579,6 +2609,49 @@ static int kvm_vm_set_mem_attributes(struct kvm *kvm, gfn_t start, gfn_t end,
>  	if (kvm_range_has_memory_attributes(kvm, start, end, ~0, attributes))
>  		goto out_unlock;
>  
> +	/*
> +	 * Clearing never materializes entries: erasing cannot allocate, so
> +	 * there is no reservation phase, no bound to enforce and no way to
> +	 * fail partway through.  Iterate the present entries only (absent
> +	 * spans are skipped by the xarray walk) and erase them; xa_erase()
> +	 * takes the xa_lock per entry and node frees are RCU-deferred, so
> +	 * the iterator is safe under the RCU lock while kvm->slots_lock
> +	 * excludes other writers.
> +	 */
> +	if (!attributes) {
> +		XA_STATE(xas, &kvm->mem_attr_array, start);
> +
> +		kvm_handle_gfn_range(kvm, &pre_set_range);
> +
> +		guard(rcu)();
> +		xas_for_each(&xas, entry, end - 1) {
> +			if (xa_is_retry(entry)) {
> +				xas_pause(&xas);
> +				continue;
> +			}
> +			xa_erase(&kvm->mem_attr_array, xas.xa_index);

[Severity: High]
Can modifying the array with xa_erase() during a lockless xas_for_each()
iteration corrupt the iterator state?

xa_erase() acquires its own lock and mutates the tree structure, but it does
not update the local xas iterator state. Modifying the tree without updating
the iterator might lead to skipped entries or infinite loops.

Would using xas_store(&xas, NULL) under xas_lock(&xas) be safer here?

> +			freed++;
> +			cond_resched();

[Severity: High]
Does this sleep while the RCU read lock is held?

The guard(rcu)() macro above establishes a block-scoped RCU critical section
for this if-block. cond_resched() is called unconditionally on every iteration
while the RCU read lock is actively held.

> +		}
> +
> +		kvm_handle_gfn_range(kvm, &post_set_range);

[Severity: High]
Is it safe to call kvm_handle_gfn_range() before the block-scoped guard(rcu)()
closes?

kvm_handle_gfn_range() acquires MMU locks and may block, but the RCU read lock
acquired by guard(rcu)() is still held at this point, since the block hasn't
ended yet.

> +
> +		/* Exactly @freed entries left the array; no resync needed. */
> +		kvm->mem_attr_gfn_count -= freed;
> +		mutex_unlock(&kvm->slots_lock);
> +
> +		return 0;
> +	}
> +
> +	/*
> +	 * Bound the number of materialized GFNs per VM.  See the comment on
> +	 * KVM_MEM_ATTR_MAX_GFNS.
> +	 */
> +	if (kvm->mem_attr_gfn_count + (end - start) > KVM_MEM_ATTR_MAX_GFNS) {
> +		r = -ENOSPC;
> +		goto out_unlock;

[Severity: High]
Can asymmetric accounting of mem_attr_gfn_count allow permanent denial of
service via budget exhaustion?

During a SET operation, the counter is incremented by the full logical range
size (end - start). This overcharges the count if entries are already populated.

When clearing, the counter is only decremented by the exact number of entries
physically erased (freed). This means alternating overlapping requests could
cause the counter to drift upward monotonically.

Once it hits KVM_MEM_ATTR_MAX_GFNS, future requests return -ENOSPC. Since the
-ENOSPC path jumps directly to out_unlock without recalculating the true count
(unlike the -ENOMEM path), the budget appears to be permanently exhausted.

> +	}
> +
>  	/*
>  	 * Reserve memory ahead of time to avoid having to deal with failures
>  	 * partway through setting the new attributes.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260911184819.101123-1-davimaba.v@proton.me?part=1

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [PATCH v2 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES
  2026-09-11 20:32 ` [PATCH v2 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
@ 2026-09-11 21:19   ` David Ballesteros
  2026-09-11 22:13   ` [PATCH v3 " David Ballesteros
  1 sibling, 0 replies; 16+ messages in thread
From: David Ballesteros @ 2026-09-11 21:19 UTC (permalink / raw)
  To: pbonzini; +Cc: kvm

Status note on this thread:

The report and this series were intentionally posted publicly from the
start (v1, 18:48 UTC today); the finding is AI-assisted, which per
Documentation/process/security-bugs.rst is handled as public.
security@kernel.org was copied on both.
security@kernel.org: dropping from Cc from here on, since the thread
is public and archived; your private copy of the report is unchanged.

Errata for the v2 cover letter: everything under "Proposed fix" below
the "Changes in v2" block is stale v1 text describing the previous
design (per-mutating-ioctl rescan, "0001-v3.patch").  The accurate
description of this series is the "Changes in v2" section and the
commit messages of 1/2 and 2/2.  "No change to patch 2" means no
functional change: 2/2 is rebased on 1/2.

One precision fix for the 1/2 commit message: "keeps every non-failing
ioctl O(present entries touched)" should be read as applying to the
counter maintenance and to clears; a set request still performs the
upstream O(range) reserve/store loops.

A range-diff v1..v2 is available on request.  The instrumented
reproducer used for the measurements (multi-VM scaling,
GET_DIRTY_LOG latency, clear-path bypass) is available on request.

David Ballesteros


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH v3 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES
  2026-09-11 20:32 ` [PATCH v2 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
  2026-09-11 21:19   ` David Ballesteros
@ 2026-09-11 22:13   ` David Ballesteros
  2026-09-11 22:13     ` [PATCH v3 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
  2026-09-11 22:13     ` [PATCH v2 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
  1 sibling, 2 replies; 16+ messages in thread
From: David Ballesteros @ 2026-09-11 22:13 UTC (permalink / raw)
  To: pbonzini; +Cc: kvm, security, David Ballesteros

The report and series are intentionally public (AI-assisted finding, cf.
Documentation/process/security-bugs.rst).

Changes in v3 (from review of v2):

- The clear path now iterates with xa_for_each_range(), which takes
  and releases the RCU lock on every step.  v2 held a single RCU
  read-side critical section across the whole loop, which made
  cond_resched() illegal on non-PREEMPT_RCU configurations and
  wrongly spanned the post-set MMU invalidation handler.  The
  per-step re-find also makes erasing the current entry safe with
  plain xa_erase(); v2 mixed xa_erase() into a lockless xas_for_each()
  iteration whose state could point at nodes the erase unlinked.
- The set path charges the budget exactly, using xa_cmpxchg() (i.e.
  xa_reserve() with the old entry returned) so only GFNs that were
  actually absent are counted.  v2 charged the full range of every
  request, so overlapping sets overcounted permanently and a caller
  could exhaust its own budget with an empty array.

Changes in v2 (from review of v1):

- Clear path reworked from reserve-then-store-NULL to erase-only,
  closing a bound bypass (a clear of a partially populated range went
  through xa_reserve() for the whole range with no bound check).
- Counter maintained incrementally instead of a per-mutating-ioctl
  rescan of the whole array.


Affected version/commit: v7.2-rc5 (commit 699594e8888d is not needed here;
verified on v7.2-rc5 as of 2026-09-09 and on v6.18.48; the code paths are
unchanged between them and present in mainline. The ioctl was introduced
in v6.8 with the per-page memory attributes series).

Problem
-------

kvm_vm_ioctl_set_mem_attributes() (virt/kvm/kvm_main.c) validates the
user-provided range only for representability (zero size, 64-bit wrap,
alignment, supported attributes) and not for magnitude.  With attributes
!= 0, kvm_vm_set_mem_attributes() then walks every GFN in the range:

        for (i = start; i < end; i++) {
                r = xa_reserve(&kvm->mem_attr_array, i, GFP_KERNEL_ACCOUNT);
                if (r)
                        goto out_unlock;
                cond_resched();
        }

xa_reserve() materializes the full root-to-leaf path of xa_nodes for each
GFN even though the GFNs need no backing memory and no memslots need to
exist.  Each xa_node is 576 bytes from radix_tree_node_cachep; densely
filled, the tree costs ~9.3 bytes per GFN.  On ENOMEM the loop bails out
without undoing prior reservations, and the only release path is
kvm_destroy_vm(); the memory is therefore retained across calls and lives
for as long as the VM fd.

Additionally, the intended memcg accounting does not take effect: the
nodes are allocated with GFP_NOWAIT in xas_alloc(), gaining __GFP_ACCOUNT
only when the xarray carries XA_FLAGS_ACCOUNT, which kvm does not set
(kvm_create_vm() uses plain xa_init()).  Measured: a process inside a
cgroup limited to 256 MiB grew host SReclaimable by ~450 MiB while its
memory.current stayed at ~488 KiB.

Triggering conditions
---------------------

- An unprivileged user with write access to /dev/kvm (sysfs default and
  the common udev default are 0666).
- A VM whose type sets private memory support: sw protected VM
  (CONFIG_KVM_SW_PROTECTED_VM, depends on EXPERT), TDX or SNP.
- No memslots, no guest memory and no guest CPUs are required.
- Verified outcomes on v6.18.48 (KASAN builds, isolated VMs):
  * ~9.29 bytes/GFN and ~75 MiB/s of sustained growth per VM; linear
    scaling with parallel VMs (4 VMs: 3.56x).
  * global OOM killing unrelated third-party processes while the
    attacker's own memcg stays uncharged.
  * when all surviving processes are OOM-disabled, select_bad_process()
    finds no candidate and the kernel panics ("System is deadlocked on
    memory", mm/oom_kill.c).  Reproduced twice.
  * one ioctl holds kvm->slots_lock for the whole reservation loop, so a
    large range blocks KVM_GET_DIRTY_LOG and memslot updates of that VM
    for the duration (measured GET_DIRTY_LOG latency 1us -> 7.7ms with a
    single concurrent call); KVM_RUN is unaffected.
  * replicated through a real container runtime (podman/crun) with
    --memory 512m: the container grew host slab by ~450 MiB while its
    memory.current stayed at ~488 KiB.

Reproducer status
-----------------

A reproducer is available on request (KVM_CREATE_VM + the ioctl in a
loop, plus slab/ accounting measurement), but following security-bugs.rst it is
not attached to this report.

Test environment
----------------

All dynamic results were collected in isolated inspection VMs on a
single AMD host with nested virtualization enabled
(kvm_amd nested=1): QEMU -cpu host, Debian 13 guests, guest kernels
built from the audited tree (v6.18.48) and from v7.2-rc5 with
CONFIG_KVM_SW_PROTECTED_VM=y / CONFIG_KVM_AMD=y / CONFIG_EXPERT=y and
KASAN (with CONFIG_KASAN_VMALLOC=y where noted); the PoC runs as root
inside the guest, host networking restricted (user-mode net,
restrict=on).  Nothing in the host kernels themselves was modified or
attacked.  The container replication used podman 5.4.2/crun inside the
guest with the host kernel having the gate enabled (a stock host kernel
without KVM_SW_PROTECTED_VM rejects KVM_CREATE_VM and the surface is
absent, as expected).

Proposed fix
------------

Patch 1/2 bounds the number of attribute entries materialized per VM
to KVM_MEM_ATTR_MAX_GFNS (2^25 GFNs = 128 GiB of GPA, ~300 MiB of
xa_nodes), enforced under kvm->slots_lock after the idempotency
early-out.  Clear requests are reworked to erase-only iteration over
the present entries in the range: erasing cannot allocate, so a clear
cannot fail partway and cannot materialize new entries (this closes a
bound bypass present in the first revision, where a clear of a
partially populated range still went through xa_reserve() unchecked).
The per-VM counter is maintained incrementally: sets charge the full
range, clears subtract the exact number of entries erased, and a full
rescan runs only on the ENOMEM path.  Runtime-verified on v7.2-rc5
(KASAN build, isolated VM): growth stops with a clean -ENOSPC at 2^25
GFNs; eight consecutive clears of 2^52-GFN ranges containing a
populated GFN leave Slab unchanged; 50 alternating 4 KiB set/clear
conversions at a near-full array take 0.01 ms each; clearing a range
returns budget exactly.

Patch 2/2 restores the intended memcg accounting with a one-liner:

-       xa_init(&kvm->mem_attr_array);
+       xa_init_flags(&kvm->mem_attr_array, XA_FLAGS_ACCOUNT);

Design notes for reviewers: the budget value (2^25 GFNs) is a trade-off
— it is well above any current consumer's usage but small next to the
GPA space TDX/SNP guests can have; a tunable or a symmetric decrement on
clear are alternatives if maintainers prefer.  The -ENOSPC errno
distinguishes the bound from the malformed-input -EINVAL paths.

Why a constant and not a derived bound?

We considered bounding materialization by what the VM has actually
declared instead of a constant, and rejected it because the declaration
itself is not proportional.  Two candidate signals:

- Memslot coverage ("reject ranges not covered by any memslot"):
  semantically attractive — attributes on unbacked GFNs are inert state
  with no consumer (kvm_handle_gfn_range only walks real memslots), and
  the only in-tree consumer (tools/testing/selftests/kvm, private_mem_
  conversions_test.c) sets attributes strictly inside its gmem memslot.
  But it is not a bound: a memslot requires only a MAP_NORESERVE userspace
  VMA (no committed pages); its kernel cost is the lpage_info metadata
  (4 bytes/entry, __vcalloc proportional to npages: ~16 MiB for a
  maximum 8 TiB memslot) while the same memslot's GPA span materializes
  ~19.9 GiB of xa_nodes — a ~1200:1 amplification of declared metadata.
  Three or four maximum-size NORESERVE memslots suffice to exhaust a
  64 GiB host.  As a semantic follow-up that eliminates the zero-setup
  variant of the attack it is worth considering on top of the bound.

- Sum of memslot pages as a dynamic budget: same flaw — the attacker
  raises his own budget by declaring free memslots; without a constant
  the ceiling becomes the physical address space.

Per-VM accounting to the caller's memcg (patch 2/2) attributes the
memory but bounds only cgroup-limited tenants; with the common
/dev/kvm 0666 default and unlimited cgroups it does not bound the host.
Hence the layering we propose: accounting (2/2) + hard per-VM bound
(1/2), with memslot-coverage rejection as an optional semantic layer
the maintainers may prefer on top.

Mitigations
-----------

- Restrict /dev/kvm (mode 0660 root:kvm) where the default 0666 is in
  place.
- Monitor SReclaimable and /sys/kernel/slab/radix_tree_node/objects, not
  SUnreclaim: the growth is reclaimable-accounted and never reclaimed.
- Per-tenant memcg limits do NOT mitigate the common path today (see
  above), which is why the counter bound is the effective fix.

Notes
-----

- Not verified: the TDX/SNP hardware paths end-to-end (no hardware);
  the software-protected path was verified end-to-end on both versions.

--
2.55.0



^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH v3 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES
  2026-09-11 22:13   ` [PATCH v3 " David Ballesteros
@ 2026-09-11 22:13     ` David Ballesteros
  2026-09-11 22:32       ` sashiko-bot
  2026-09-11 22:13     ` [PATCH v2 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
  1 sibling, 1 reply; 16+ messages in thread
From: David Ballesteros @ 2026-09-11 22:13 UTC (permalink / raw)
  To: pbonzini; +Cc: kvm, security, David Ballesteros

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.  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 number of attribute entries materialized 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.

Clear requests never touch the reservation path: erasing an entry
cannot allocate, so a clear iterates the present entries in the range
and erases them.  This closes a bound bypass present in v1 (a clear of
a partially populated range used to reserve every GFN in the range
with no bound check) and makes clears failure-free by construction.
The iteration uses xa_for_each_range(), which takes and releases the
RCU lock on every step, so cond_resched() between entries is legal on
all configurations, each step re-finds from the previous index (which
makes erasing the current entry safe), and no RCU read-side critical
section spans the MMU invalidation handlers.

The per-VM counter is maintained exactly: the set path uses
xa_cmpxchg() (xa_reserve() with the old entry returned) so it charges
only the GFNs that were actually absent before the request, clears
subtract the exact number of entries erased, and a full rescan runs
only on the ENOMEM path where partial reservations are retained.
Exact charging also avoids a phantom-budget exhaustion found in review
of v2, where overlapping set requests were charged their full range
and the overcount never returned after clearing.

Build-tested warning-free on v7.2-rc5.  Runtime-verified on v7.2-rc5
(KASAN build, isolated VM): growth stops with a clean -ENOSPC at 2^25
GFNs; eight consecutive clears of 2^52-GFN ranges containing a
populated GFN leave Slab unchanged; 50 alternating 4 KiB set/clear
conversions at a near-full array complete in 0.01 ms each; after an
overlap-set cycle followed by clearing everything, the full budget is
available again (four chunks re-set succeed, the fifth returns
-ENOSPC).

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 <davimaba.v@proton.me>
---
--- a/virt/kvm/kvm_main.c	2026-07-26 16:45:48.000000000 -0500
+++ b/virt/kvm/kvm_main.c	2026-09-11 16:27:45.921813177 -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,25 @@
 	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)
+{
+	unsigned long count = 0;
+	unsigned long index;
+	void *entry;
+
+	xa_for_each(&kvm->mem_attr_array, index, entry) {
+		count++;
+		if (need_resched())
+			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)
@@ -2555,7 +2575,10 @@
 		.may_block = true,
 	};
 	unsigned long i;
+	unsigned long freed = 0;
+	unsigned long newly = 0;
 	void *entry;
+	void *old;
 	int r = 0;
 
 	entry = attributes ? xa_mk_value(attributes) : NULL;
@@ -2569,13 +2592,66 @@
 		goto out_unlock;
 
 	/*
+	 * Clearing never materializes entries: erasing cannot allocate, so
+	 * there is no reservation phase, no bound to enforce and no way to
+	 * fail partway through.  Iterate the present entries only (absent
+	 * spans are skipped by the xarray walk) and erase them; xa_erase()
+	 * takes the xa_lock per entry and node frees are RCU-deferred, so
+	 * the iterator is safe under the RCU lock while kvm->slots_lock
+	 * excludes other writers.
+	 */
+	if (!attributes) {
+		unsigned long index;
+
+		kvm_handle_gfn_range(kvm, &pre_set_range);
+
+		/*
+		 * xa_for_each_range() takes and releases the RCU lock on
+		 * every iteration, so cond_resched() below is legal in all
+		 * configurations and each step re-finds from the previous
+		 * index, which makes erasing the current entry safe.
+		 */
+		xa_for_each_range(&kvm->mem_attr_array, index, entry,
+				  start, end - 1) {
+			xa_erase(&kvm->mem_attr_array, index);
+			freed++;
+			cond_resched();
+		}
+
+		kvm_handle_gfn_range(kvm, &post_set_range);
+
+		/* Exactly @freed entries left the array; no resync needed. */
+		kvm->mem_attr_gfn_count -= freed;
+		mutex_unlock(&kvm->slots_lock);
+
+		return 0;
+	}
+
+	/*
+	 * Bound the number of materialized GFNs per VM.  See the comment on
+	 * KVM_MEM_ATTR_MAX_GFNS.
+	 */
+	if (kvm->mem_attr_gfn_count + (end - start) > KVM_MEM_ATTR_MAX_GFNS) {
+		r = -ENOSPC;
+		goto out_unlock;
+	}
+
+	/*
 	 * Reserve memory ahead of time to avoid having to deal with failures
-	 * partway through setting the new attributes.
+	 * partway through setting the new attributes.  xa_cmpxchg() is
+	 * xa_reserve() with the old entry returned: a NULL old value marks a
+	 * GFN that was absent before this request, so @newly is the exact
+	 * number of entries this ioctl adds to the array.
 	 */
 	for (i = start; i < end; i++) {
-		r = xa_reserve(&kvm->mem_attr_array, i, GFP_KERNEL_ACCOUNT);
-		if (r)
-			goto out_unlock;
+		old = xa_cmpxchg(&kvm->mem_attr_array, i, NULL,
+				 XA_ZERO_ENTRY, GFP_KERNEL_ACCOUNT);
+		if (xa_is_err(old)) {
+			r = xa_err(old);
+			goto enomem_resync;
+		}
+		if (!old)
+			newly++;
 
 		cond_resched();
 	}
@@ -2591,6 +2667,20 @@
 
 	kvm_handle_gfn_range(kvm, &post_set_range);
 
+	/* Charge exactly the entries this request added. */
+	kvm->mem_attr_gfn_count += newly;
+	mutex_unlock(&kvm->slots_lock);
+
+	return 0;
+
+enomem_resync:
+	/*
+	 * Reservations made so far are retained; recount to restore the
+	 * invariant that the counter matches the array population.  This is
+	 * the only path that can leave the counter stale, and it is rare by
+	 * construction (it requires ENOMEM under the bound).
+	 */
+	kvm->mem_attr_gfn_count = kvm_count_mem_attr_entries(kvm);
 out_unlock:
 	mutex_unlock(&kvm->slots_lock);
 
--- 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];
 };


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH v2 2/2] KVM: Account mem_attr_array nodes to the caller's memcg
  2026-09-11 22:13   ` [PATCH v3 " David Ballesteros
  2026-09-11 22:13     ` [PATCH v3 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
@ 2026-09-11 22:13     ` David Ballesteros
  1 sibling, 0 replies; 16+ messages in thread
From: David Ballesteros @ 2026-09-11 22:13 UTC (permalink / raw)
  To: pbonzini; +Cc: kvm, security, David Ballesteros

kvm_vm_set_mem_attributes() passes GFP_KERNEL_ACCOUNT when reserving
xarray entries, but the nodes are allocated by xas_alloc() with
GFP_NOWAIT, which only adds __GFP_ACCOUNT when the xarray carries
XA_FLAGS_ACCOUNT.  mem_attr_array is initialized with plain xa_init(),
so the flag is never set and the nodes are never charged to the caller:
measured on v6.18.48 and v7.2-rc5, a process inside a cgroup limited to
256 MiB grew host SReclaimable by ~450 MiB while its memory.current
stayed at ~488 KiB.  Per-tenant memcg limits therefore do not contain
the growth described in the previous patch.

Set XA_FLAGS_ACCOUNT so that the intended accounting takes effect.
This is a mitigation, not a bound (it does not limit growth); both
patches are needed to bound and attribute the memory.

Runtime-verified on v6.18.48 (KASAN build, isolated VM): with this flag,
a process in a cgroup with memory.max=256M is contained by the cgroup
limit (allocation fails cleanly and, at the limit, the memcg OOM killer
selects the attacker itself) instead of exhausting global memory, and
cgroup memory.current tracks the materialized nodes one to one.

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 <davimaba.v@proton.me>
---
--- a/virt/kvm/kvm_main.c	2026-07-26 16:45:48.000000000 -0500
+++ b/virt/kvm/kvm_main.c	2026-09-10 14:20:00.000000000 -0500
@@ -1117,7 +1117,7 @@
 	xa_init(&kvm->vcpu_array);
 #ifdef CONFIG_KVM_GENERIC_MEMORY_ATTRIBUTES
-	xa_init(&kvm->mem_attr_array);
+	xa_init_flags(&kvm->mem_attr_array, XA_FLAGS_ACCOUNT);
 	kvm->mem_attr_gfn_count = 0;
 #endif

 	INIT_LIST_HEAD(&kvm->gpc_list);


^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [PATCH v3 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES
  2026-09-11 22:13     ` [PATCH v3 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
@ 2026-09-11 22:32       ` sashiko-bot
  0 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-11 22:32 UTC (permalink / raw)
  To: David Ballesteros; +Cc: kvm

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Conservative quota bound check over-accounts overlapping requests and restricts maximum VM memory to 128 GiB.
--

commit 458d355532cc6dd4b9524cc1a387ddd8644cc6f6
Author: David Ballesteros <davimaba.v@proton.me>

KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES

This commit adds a bound to the number of GFNs that can be materialized
in the memory attributes xarray. This ensures unprivileged users cannot
exhaust host memory when creating private memory VMs.

> diff --git a/include/linux/kvm_host.h b/include/linux/kvm_host.h
> index 3dd04605f2e55..bb03dc20d3f98 100644
> --- a/include/linux/kvm_host.h
> +++ b/include/linux/kvm_host.h
[ ... ]
> @@ -573,6 +573,13 @@ static inline int kvm_vcpu_exiting_guest_mode(struct kvm_vcpu *vcpu)
>   * 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)

[ ... ]
> diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c
> index 108d42c5c1d6a..c0696bd88461a 100644
> --- a/virt/kvm/kvm_main.c
> +++ b/virt/kvm/kvm_main.c
[ ... ]
> @@ -2579,14 +2602,67 @@ static int kvm_vm_set_mem_attributes(struct kvm *kvm, gfn_t start, gfn_t end,
[ ... ]
> +	/*
> +	 * Bound the number of materialized GFNs per VM.  See the comment on
> +	 * KVM_MEM_ATTR_MAX_GFNS.
> +	 */
> +	if (kvm->mem_attr_gfn_count + (end - start) > KVM_MEM_ATTR_MAX_GFNS) {
> +		r = -ENOSPC;
> +		goto out_unlock;
> +	}

[Severity: High]
Does this boundary check over-account for overlapping requests?

If a VM issues a request over a large range that overlaps with mostly 
populated entries, this check assumes every GFN in the range is a new entry 
by adding end - start to the current count.

Could this cause valid requests on large VMs (e.g. bulk memory attribute
updates during a reboot) to fail with a spurious -ENOSPC, effectively 
preventing operations even when the actual number of new entries being 
added would remain well under the limit?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260911221302.53013-1-davimaba.v@proton.me?part=1

^ permalink raw reply	[flat|nested] 16+ messages in thread

* [syzbot ci] Re: KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES
  2026-09-11 18:48 [PATCH 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
                   ` (4 preceding siblings ...)
  2026-09-11 20:32 ` [PATCH v2 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
@ 2026-09-12  8:48 ` syzbot ci
  5 siblings, 0 replies; 16+ messages in thread
From: syzbot ci @ 2026-09-12  8:48 UTC (permalink / raw)
  To: davimaba.v, kvm, pbonzini, security; +Cc: syzbot, syzkaller-bugs

syzbot ci has tested the following series

[v1] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES
https://lore.kernel.org/all/20260911184819.101123-1-davimaba.v@proton.me
* [PATCH 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES
* [PATCH 2/2] KVM: Account mem_attr_array nodes to the caller's memcg

and found the following issue:
WARNING: suspicious RCU usage in kvm_vm_ioctl_set_mem_attributes

Full report is available here:
https://ci.syzbot.org/series/734bda86-476d-49cf-9a5e-335cc5b006c0

***

WARNING: suspicious RCU usage in kvm_vm_ioctl_set_mem_attributes

tree:      kvm-next
URL:       https://kernel.googlesource.com/pub/scm/virt/kvm/kvm/
base:      d4b7fb647204f0c81dfeae2d1a708e4d858e0c94
arch:      amd64
compiler:  Debian clang version 22.1.8 (++20260613092233+e80beda6e255-1~exp1~20260613092250.77), Debian LLD 22.1.8
config:    https://ci.syzbot.org/builds/a54b5b46-809d-4f22-a473-c317ec575648/config
syz repro: https://ci.syzbot.org/findings/c345c736-c3eb-46c9-9d9d-cc78ce4bf677/syz_repro

=============================
WARNING: suspicious RCU usage
syzkaller #0 Not tainted
-----------------------------
./include/linux/xarray.h:1211 suspicious rcu_dereference_check() usage!

other info that might help us debug this:


rcu_scheduler_active = 2, debug_locks = 1
locks held by syz.2.19/5790: 1, last CPU#0:
 #0: ffff88810b9900a0 (&kvm->slots_lock){+.+.}-{4:4}, at: kvm_vm_set_mem_attributes virt/kvm/kvm_main.c:2605 [inline]
 #0: ffff88810b9900a0 (&kvm->slots_lock){+.+.}-{4:4}, at: kvm_vm_ioctl_set_mem_attributes+0x365/0x1af0 virt/kvm/kvm_main.c:2690

stack backtrace:
CPU: 0 UID: 0 PID: 5790 Comm: syz.2.19 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
Call Trace:
 <TASK>
 dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
 lockdep_rcu_suspicious+0x140/0x1d0 kernel/locking/lockdep.c:6938
 xa_head include/linux/xarray.h:1210 [inline]
 xas_start+0x618/0x770 lib/xarray.c:191
 xas_load+0x2c/0x5a0 lib/xarray.c:239
 xas_find+0x157/0x980 lib/xarray.c:1409
 kvm_count_mem_attr_entries virt/kvm/kvm_main.c:2558 [inline]
 kvm_vm_set_mem_attributes virt/kvm/kvm_main.c:2660 [inline]
 kvm_vm_ioctl_set_mem_attributes+0x1578/0x1af0 virt/kvm/kvm_main.c:2690
 kvm_vm_ioctl+0xb33/0xd30 virt/kvm/kvm_main.c:5421
 vfs_ioctl fs/ioctl.c:51 [inline]
 __do_sys_ioctl fs/ioctl.c:597 [inline]
 __se_sys_ioctl+0xfc/0x170 fs/ioctl.c:583
 do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
 do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f4321f9e159
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f4322e33028 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
RAX: ffffffffffffffda RBX: 00007f4322225fa0 RCX: 00007f4321f9e159
RDX: 0000200000002200 RSI: 000000004020aed2 RDI: 0000000000000004
RBP: 00007f432203503b R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f4322226038 R14: 00007f4322225fa0 R15: 00007ffea2b6d9c8
 </TASK>

=============================
WARNING: suspicious RCU usage
syzkaller #0 Not tainted
-----------------------------
./include/linux/xarray.h:1227 suspicious rcu_dereference_check() usage!

other info that might help us debug this:


rcu_scheduler_active = 2, debug_locks = 1
locks held by syz.2.19/5790: 1, last CPU#0:
 #0: ffff88810b9900a0 (&kvm->slots_lock){+.+.}-{4:4}, at: kvm_vm_set_mem_attributes virt/kvm/kvm_main.c:2605 [inline]
 #0: ffff88810b9900a0 (&kvm->slots_lock){+.+.}-{4:4}, at: kvm_vm_ioctl_set_mem_attributes+0x365/0x1af0 virt/kvm/kvm_main.c:2690

stack backtrace:
CPU: 0 UID: 0 PID: 5790 Comm: syz.2.19 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
Call Trace:
 <TASK>
 dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
 lockdep_rcu_suspicious+0x140/0x1d0 kernel/locking/lockdep.c:6938
 xa_entry include/linux/xarray.h:1226 [inline]
 xas_descend lib/xarray.c:208 [inline]
 xas_load+0x4dc/0x5a0 lib/xarray.c:246
 xas_find+0x157/0x980 lib/xarray.c:1409
 kvm_count_mem_attr_entries virt/kvm/kvm_main.c:2558 [inline]
 kvm_vm_set_mem_attributes virt/kvm/kvm_main.c:2660 [inline]
 kvm_vm_ioctl_set_mem_attributes+0x1578/0x1af0 virt/kvm/kvm_main.c:2690
 kvm_vm_ioctl+0xb33/0xd30 virt/kvm/kvm_main.c:5421
 vfs_ioctl fs/ioctl.c:51 [inline]
 __do_sys_ioctl fs/ioctl.c:597 [inline]
 __se_sys_ioctl+0xfc/0x170 fs/ioctl.c:583
 do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
 do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f4321f9e159
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f4322e33028 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
RAX: ffffffffffffffda RBX: 00007f4322225fa0 RCX: 00007f4321f9e159
RDX: 0000200000002200 RSI: 000000004020aed2 RDI: 0000000000000004
RBP: 00007f432203503b R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f4322226038 R14: 00007f4322225fa0 R15: 00007ffea2b6d9c8
 </TASK>

=============================
WARNING: suspicious RCU usage
syzkaller #0 Not tainted
-----------------------------
./include/linux/xarray.h:1227 suspicious rcu_dereference_check() usage!

other info that might help us debug this:


rcu_scheduler_active = 2, debug_locks = 1
locks held by syz.2.19/5790: 1, last CPU#0:
 #0: ffff88810b9900a0 (&kvm->slots_lock){+.+.}-{4:4}, at: kvm_vm_set_mem_attributes virt/kvm/kvm_main.c:2605 [inline]
 #0: ffff88810b9900a0 (&kvm->slots_lock){+.+.}-{4:4}, at: kvm_vm_ioctl_set_mem_attributes+0x365/0x1af0 virt/kvm/kvm_main.c:2690

stack backtrace:
CPU: 0 UID: 0 PID: 5790 Comm: syz.2.19 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
Call Trace:
 <TASK>
 dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
 lockdep_rcu_suspicious+0x140/0x1d0 kernel/locking/lockdep.c:6938
 xa_entry include/linux/xarray.h:1226 [inline]
 xas_next_entry include/linux/xarray.h:1731 [inline]
 kvm_count_mem_attr_entries virt/kvm/kvm_main.c:2558 [inline]
 kvm_vm_set_mem_attributes virt/kvm/kvm_main.c:2660 [inline]
 kvm_vm_ioctl_set_mem_attributes+0x1834/0x1af0 virt/kvm/kvm_main.c:2690
 kvm_vm_ioctl+0xb33/0xd30 virt/kvm/kvm_main.c:5421
 vfs_ioctl fs/ioctl.c:51 [inline]
 __do_sys_ioctl fs/ioctl.c:597 [inline]
 __se_sys_ioctl+0xfc/0x170 fs/ioctl.c:583
 do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
 do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f4321f9e159
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f4322e33028 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
RAX: ffffffffffffffda RBX: 00007f4322225fa0 RCX: 00007f4321f9e159
RDX: 0000200000002200 RSI: 000000004020aed2 RDI: 0000000000000004
RBP: 00007f432203503b R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f4322226038 R14: 00007f4322225fa0 R15: 00007ffea2b6d9c8
 </TASK>

=============================
WARNING: suspicious RCU usage
syzkaller #0 Not tainted
-----------------------------
./include/linux/xarray.h:1244 suspicious rcu_dereference_check() usage!

other info that might help us debug this:


rcu_scheduler_active = 2, debug_locks = 1
locks held by syz.2.19/5790: 1, last CPU#0:
 #0: ffff88810b9900a0 (&kvm->slots_lock){+.+.}-{4:4}, at: kvm_vm_set_mem_attributes virt/kvm/kvm_main.c:2605 [inline]
 #0: ffff88810b9900a0 (&kvm->slots_lock){+.+.}-{4:4}, at: kvm_vm_ioctl_set_mem_attributes+0x365/0x1af0 virt/kvm/kvm_main.c:2690

stack backtrace:
CPU: 0 UID: 0 PID: 5790 Comm: syz.2.19 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
Call Trace:
 <TASK>
 dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
 lockdep_rcu_suspicious+0x140/0x1d0 kernel/locking/lockdep.c:6938
 xa_parent+0xec/0xf0 include/linux/xarray.h:1243
 xas_find+0x6f2/0x980 lib/xarray.c:1422
 kvm_count_mem_attr_entries virt/kvm/kvm_main.c:2558 [inline]
 kvm_vm_set_mem_attributes virt/kvm/kvm_main.c:2660 [inline]
 kvm_vm_ioctl_set_mem_attributes+0x1578/0x1af0 virt/kvm/kvm_main.c:2690
 kvm_vm_ioctl+0xb33/0xd30 virt/kvm/kvm_main.c:5421
 vfs_ioctl fs/ioctl.c:51 [inline]
 __do_sys_ioctl fs/ioctl.c:597 [inline]
 __se_sys_ioctl+0xfc/0x170 fs/ioctl.c:583
 do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
 do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f4321f9e159
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f4322e33028 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
RAX: ffffffffffffffda RBX: 00007f4322225fa0 RCX: 00007f4321f9e159
RDX: 0000200000002200 RSI: 000000004020aed2 RDI: 0000000000000004
RBP: 00007f432203503b R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f4322226038 R14: 00007f4322225fa0 R15: 00007ffea2b6d9c8
 </TASK>


***

If these findings have caused you to resend the series or submit a
separate fix, please add the following tag to your commit message:
  Tested-by: syzbot@syzkaller.appspotmail.com

---
This report is generated by a bot. It may contain errors.
syzbot ci engineers can be reached at syzkaller@googlegroups.com.

To test a fix for this bug, please reply with `#syz test`
(on a separate line) and attach the patch to the email.

Notes:
- The patch will be applied on top of the tested series (as an
  incremental fix).
- To test a new version of the whole series, please send it directly
  to syzbot@lists.linux.dev.
- Arguments like custom git repos and branches are not supported.

^ permalink raw reply	[flat|nested] 16+ messages in thread

end of thread, other threads:[~2026-09-12  8:48 UTC | newest]

Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-11 18:48 [PATCH 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
2026-09-11 18:48 ` [PATCH 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
2026-09-11 19:05   ` sashiko-bot
2026-09-11 18:48 ` [PATCH 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
2026-09-11 19:02   ` sashiko-bot
2026-09-11 20:32 ` [PATCH v2 0/2] KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
2026-09-11 21:19   ` David Ballesteros
2026-09-11 22:13   ` [PATCH v3 " David Ballesteros
2026-09-11 22:13     ` [PATCH v3 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
2026-09-11 22:32       ` sashiko-bot
2026-09-11 22:13     ` [PATCH v2 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
2026-09-11 20:32 ` [PATCH v2 1/2] KVM: Bound per-VM GFN materialization in KVM_SET_MEMORY_ATTRIBUTES David Ballesteros
2026-09-11 20:45   ` sashiko-bot
2026-09-11 20:32 ` [PATCH v2 2/2] KVM: Account mem_attr_array nodes to the caller's memcg David Ballesteros
2026-09-11 20:44   ` sashiko-bot
2026-09-12  8:48 ` [syzbot ci] Re: KVM: unbounded per-VM kernel memory growth via KVM_SET_MEMORY_ATTRIBUTES syzbot ci

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox