Skip to contentSkip to conversation

KVM: irqchip: Reduce KVM_SET_GSI_ROUTING allocation overhead

PATCH In review Yanfei Xu 3 participants 5 messages May 25

Subsystems (7)
x86 KVM PowerPC KVM x86 +3 more subsystems
lore
Share & tools
lore

Yanfei Xu

author
#0 lore raw

v3 cover KVM: irqchip: Reduce KVM_SET_GSI_ROUTING allocation overhead

KVM_SET_GSI_ROUTING can become expensive for guests with many VFIO devices and MSI-X vectors, Each update rebuilds the routing table, and the repeated allocation/free of routing entries can dominate the cost of kvm_set_irq_routing().

This series reduces that overhead in two steps.

Patch 1 allocates all routing entries in one contiguous allocation, so the entries can be freed together instead of allocating and freeing each entry independently.

Patch 2 adds a per-VM cache for one freed entries[] allocation and reuses it when the next routing update needs the same allocation size. This avoids repeated allocation/free churn for frequent routing updates while preserving the existing SRCU lifetime rules.

I wrote a test that continuously calls KVM_SET_GSI_ROUTING 50000 times and count the elapsed time:
Test machine:
Architecture: x86_64
CPU: Intel Xeon Platinum 8336C @ 2.30GHz

With 512 routes:
Before:

    irq_routing_perf_test --routes=512 --iterations=50000
      total: 1557023.484 us
      average: 31.140 us/update
After:
    irq_routing_perf_test --routes=512 --iterations=50000
      total: 873104.338 us
      average: 17.462 us/update

With 4096 routes:
Before:

    irq_routing_perf_test --routes=4096 --iterations=50000
      total: 15192698.026 us
      average: 303.854 us/update 
After:
    irq_routing_perf_test --routes=4096 --iterations=50000
      total: 3295068.326 us
      average: 65.901 us/update

The result shows that the time spent per call is reduced by approximately 78.3% when the number of routing increase to 4096. It can significantly improves routing update efficiency.

v2->v3:
1.switch kcalloc to kv flavor of allocation. (Sean's suggestion) 2.introduce patch2 to cache freed entries allocation and reuse it at the
next routing update. (Sean's suggestion)

v1->v2:
1. fix variable 'r' is used uninitialized when a 'if' condition is true
2. simplified free_irq_routing_table() by removing the iteration over the entries and the hlist cleanup.

[v2]: https://lore.kernel.org/kvm/[email protected]/

Yanfei Xu (2):
KVM: irqchip: KVM: Reduce allocation overhead in kvm_set_irq_routing()
KVM: irqchip: Cache IRQ routing entries allocation

 arch/loongarch/kvm/irqfd.c |  2 +
 arch/powerpc/kvm/mpic.c    |  2 +
 arch/x86/kvm/irq.c         |  2 +
 include/linux/kvm_host.h   |  7 +++
 virt/kvm/irqchip.c         | 88 +++++++++++++++++++++++++++-----------
 5 files changed, 76 insertions(+), 25 deletions(-)
Signature · 1 line
2.20.1
author
#1 lore raw

PATCH v3 1/2 KVM: irqchip: KVM: Reduce allocation overhead in kvm_set_irq_routing()

In guests with many VFIO devices and MSI-X vectors, kvm_set_irq_routing() becomes a high-overhead operation. Each invocation walks the entire IRQ routing table and reallocates/frees every routing entry.

As the routing table grows on each call, entry allocation and freeing dominate the execution time of this function. In scenarios such as VM live migration or live upgrade, this behavior can introduce unnecessary downtime.

Allocate memory for all routing entries in one shot using kvzalloc_objs(), allowing them to be freed together with a single kfree() call.

Example: On a VM with 120 vCPUs and 15 VFIO devices (virtio-net), the total number of calls to kzalloc and kfree is over 20000. With this change, it is reduced to around 30.

---
 include/linux/kvm_host.h |  1 +
 virt/kvm/irqchip.c       | 37 ++++++++++++-------------------------
 2 files changed, 13 insertions(+), 25 deletions(-)
include/linux/kvm_host.h +1 -0
index 4c14aee1fb06..a8d74b25c5cf 100644
@@ -693,6 +693,7 @@ struct kvm_kernel_irq_routing_entry {
 struct kvm_irq_routing_table {
 	int chip[KVM_NR_IRQCHIPS][KVM_IRQCHIP_NUM_PINS];
 	u32 nr_rt_entries;
+	struct kvm_kernel_irq_routing_entry *entries;
 	/*
 	 * Array indexed by gsi. Each entry contains list of irq chips
 	 * the gsi is connected to.
virt/kvm/irqchip.c +12 -25
index 462c70621247..b78f113af4ea 100644
@@ -98,21 +98,10 @@ int kvm_set_irq(struct kvm *kvm, int irq_source_id, u32 irq, int level,
 
 static void free_irq_routing_table(struct kvm_irq_routing_table *rt)
 {
-	int i;
-
 	if (!rt)
 		return;
 
-	for (i = 0; i < rt->nr_rt_entries; ++i) {
-		struct kvm_kernel_irq_routing_entry *e;
-		struct hlist_node *n;
-
-		hlist_for_each_entry_safe(e, n, &rt->map[i], link) {
-			hlist_del(&e->link);
-			kfree(e);
-		}
-	}
-
+	kvfree(rt->entries);
 	kfree(rt);
 }
 
@@ -187,31 +176,33 @@ int kvm_set_irq_routing(struct kvm *kvm,
 	if (!new)
 		return -ENOMEM;
 
+	e = kvzalloc_objs(*e, nr, GFP_KERNEL_ACCOUNT);
+	if (!e) {
+		r = -ENOMEM;
+		goto out;
+	}
+	new->entries = e;
+
 	new->nr_rt_entries = nr_rt_entries;
 	for (i = 0; i < KVM_NR_IRQCHIPS; i++)
 		for (j = 0; j < KVM_IRQCHIP_NUM_PINS; j++)
 			new->chip[i][j] = -1;
 
 	for (i = 0; i < nr; ++i) {
-		r = -ENOMEM;
-		e = kzalloc_obj(*e, GFP_KERNEL_ACCOUNT);
-		if (!e)
-			goto out;
-
 		r = -EINVAL;
 		switch (ue->type) {
 		case KVM_IRQ_ROUTING_MSI:
 			if (ue->flags & ~KVM_MSI_VALID_DEVID)
-				goto free_entry;
+				goto out;
 			break;
 		default:
 			if (ue->flags)
-				goto free_entry;
+				goto out;
 			break;
 		}
-		r = setup_routing_entry(kvm, new, e, ue);
+		r = setup_routing_entry(kvm, new, e + i, ue);
 		if (r)
-			goto free_entry;
+			goto out;
 		++ue;
 	}
 
@@ -226,10 +217,6 @@ int kvm_set_irq_routing(struct kvm *kvm,
 
 	new = old;
 	r = 0;
-	goto out;
-
-free_entry:
-	kfree(e);
 out:
 	free_irq_routing_table(new);
 
Signature · 1 line
2.20.1
author
#2 lore raw

PATCH v3 2/2 KVM: irqchip: Cache IRQ routing entries allocation

In reply to Yanfei Xu

Cache one VM-local entries[] allocation from a freed IRQ routing table and reuse it when the next routing update needs the same allocation size.

This reduces repeated allocation/free churn for frequent KVM_SET_GSI_ROUTING updates without changing the existing SRCU lifetime rules. Because cached entries are reused without being zeroed, initialize the remaining MSI fields on x86, LoongArch and PowerPC.

---
 arch/loongarch/kvm/irqfd.c |  2 ++
 arch/powerpc/kvm/mpic.c    |  2 ++
 arch/x86/kvm/irq.c         |  2 ++
 include/linux/kvm_host.h   |  6 ++++
 virt/kvm/irqchip.c         | 61 ++++++++++++++++++++++++++++++++++----
 5 files changed, 68 insertions(+), 5 deletions(-)
arch/loongarch/kvm/irqfd.c +2 -0
index f4f953b22419..15cc80cde071 100644
@@ -60,6 +60,8 @@ int kvm_set_routing_entry(struct kvm *kvm,
 		e->msi.address_lo = ue->u.msi.address_lo;
 		e->msi.address_hi = ue->u.msi.address_hi;
 		e->msi.data = ue->u.msi.data;
+		e->msi.flags = ue->flags;
+		e->msi.devid = ue->u.msi.devid;
 		return 0;
 	default:
 		return -EINVAL;
arch/powerpc/kvm/mpic.c +2 -0
index 3070f36d9fb8..2785e434fc9d 100644
@@ -1841,6 +1841,8 @@ int kvm_set_routing_entry(struct kvm *kvm,
 		e->msi.address_lo = ue->u.msi.address_lo;
 		e->msi.address_hi = ue->u.msi.address_hi;
 		e->msi.data = ue->u.msi.data;
+		e->msi.flags = ue->flags;
+		e->msi.devid = ue->u.msi.devid;
 		break;
 	default:
 		goto out;
arch/x86/kvm/irq.c +2 -0
index 9519fec09ee6..6774e4964861 100644
@@ -332,6 +332,8 @@ int kvm_set_routing_entry(struct kvm *kvm,
 		e->msi.address_lo = ue->u.msi.address_lo;
 		e->msi.address_hi = ue->u.msi.address_hi;
 		e->msi.data = ue->u.msi.data;
+		e->msi.flags = ue->flags;
+		e->msi.devid = ue->u.msi.devid;
 
 		if (kvm_msi_route_invalid(kvm, e))
 			return -EINVAL;
include/linux/kvm_host.h +6 -0
index a8d74b25c5cf..54d9641e2b52 100644
@@ -694,6 +694,7 @@ struct kvm_irq_routing_table {
 	int chip[KVM_NR_IRQCHIPS][KVM_IRQCHIP_NUM_PINS];
 	u32 nr_rt_entries;
 	struct kvm_kernel_irq_routing_entry *entries;
+	size_t entries_size;
 	/*
 	 * Array indexed by gsi. Each entry contains list of irq chips
 	 * the gsi is connected to.
@@ -845,6 +846,11 @@ struct kvm {
 	 * Update side is protected by irq_lock.
 	 */
 	struct kvm_irq_routing_table __rcu *irq_routing;
+	/*
+	 * Cache for the entries[] allocation of a freed IRQ routing table.
+	 */
+	struct kvm_kernel_irq_routing_entry *irq_routing_entries_cache;
+	size_t irq_routing_entries_cache_size;
 
 	struct hlist_head irq_ack_notifier_list;
 #endif
virt/kvm/irqchip.c +56 -5

100 lines not on this page · raw message

Signature · 1 line
2.20.1
#3 lore raw

Re: [v3 2/2] KVM: irqchip: Cache IRQ routing entries allocation

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

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

Pre-existing issues:
- [Critical] Missing bounds check on user-supplied ue->u.irqchip.irqchip allows an out-of-bounds array write on LoongArch and PowerPC. --

commit 8ac8d5f6cab84711afabacca6347f1ad673dc605
Author: Yanfei Xu <[email protected]>
Subject: KVM: irqchip: Cache IRQ routing entries allocation

This patch caches a VM-local entries array allocation from a freed IRQ routing table to reduce allocation and free churn during frequent routing updates. It also initializes the remaining MSI fields on x86, LoongArch, and PowerPC since cached entries are reused without being zeroed.

arch/loongarch/kvm/irqfd.c int kvm_set_routing_entry(struct kvm *kvm, · line 64

Quoted 10 lines
arch/loongarch/kvm/irqfd.c +2 -0
index f4f953b224192..15cc80cde071b 100644
@@ -60,6 +60,8 @@ int kvm_set_routing_entry(struct kvm *kvm,
 		e->msi.address_lo = ue->u.msi.address_lo;
 		e->msi.address_hi = ue->u.msi.address_hi;
 		e->msi.data = ue->u.msi.data;
+		e->msi.flags = ue->flags;
+		e->msi.devid = ue->u.msi.devid;

[Severity: Critical]
This isn't a bug introduced by this patch, but does the KVM_IRQ_ROUTING_IRQCHIP case in this function lack bounds checking on the irqchip index?

Looking at kvm_set_routing_entry() on LoongArch (and similarly on PowerPC), the irqchip.pin is validated against KVM_IRQCHIP_NUM_PINS, but there doesn't appear to be a check for e->irqchip.irqchip against KVM_NR_IRQCHIPS.

When kvm_set_routing_entry() returns success, setup_routing_entry() executes this array write:

virt/kvm/irqchip.c:setup_routing_entry() {
    ...
    if (e->type == KVM_IRQ_ROUTING_IRQCHIP)
        rt->chip[e->irqchip.irqchip][e->irqchip.pin] = e->gsi;
    ...
}

Could this allow a user-supplied routing entry to provide an arbitrarily large irqchip index, causing an out-of-bounds write of e->gsi into the heap memory following the kvm_irq_routing_table?

[ ... ]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
#4 lore raw

Re: [v3 2/2] KVM: irqchip: Cache IRQ routing entries allocation

Quoted 5 lines · On 2026/5/25 13:17, [email protected] wrote:

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

Pre-existing issues:
- [Critical] Missing bounds check on user-supplied ue->u.irqchip.irqchip allows an out-of-bounds array write on LoongArch and PowerPC. --

Posted a separate fix for this issue.
https://lore.kernel.org/kvm/[email protected]/

Thanks,
Yanfei

Quoted 43 lines

commit 8ac8d5f6cab84711afabacca6347f1ad673dc605
Author: Yanfei Xu <[email protected]>
Subject: KVM: irqchip: Cache IRQ routing entries allocation

This patch caches a VM-local entries array allocation from a freed IRQ routing table to reduce allocation and free churn during frequent routing updates. It also initializes the remaining MSI fields on x86, LoongArch, and PowerPC since cached entries are reused without being zeroed.

arch/loongarch/kvm/irqfd.c int kvm_set_routing_entry(struct kvm *kvm, · line 64

Quoted 10 lines
arch/loongarch/kvm/irqfd.c +2 -0
index f4f953b224192..15cc80cde071b 100644
@@ -60,6 +60,8 @@ int kvm_set_routing_entry(struct kvm *kvm,
  		e->msi.address_lo = ue->u.msi.address_lo;
  		e->msi.address_hi = ue->u.msi.address_hi;
  		e->msi.data = ue->u.msi.data;
+		e->msi.flags = ue->flags;
+		e->msi.devid = ue->u.msi.devid;

[Severity: Critical]
This isn't a bug introduced by this patch, but does the KVM_IRQ_ROUTING_IRQCHIP case in this function lack bounds checking on the irqchip index?

Looking at kvm_set_routing_entry() on LoongArch (and similarly on PowerPC), the irqchip.pin is validated against KVM_IRQCHIP_NUM_PINS, but there doesn't appear to be a check for e->irqchip.irqchip against KVM_NR_IRQCHIPS.

When kvm_set_routing_entry() returns success, setup_routing_entry() executes this array write:

virt/kvm/irqchip.c:setup_routing_entry() {
     ...
     if (e->type == KVM_IRQ_ROUTING_IRQCHIP)
         rt->chip[e->irqchip.irqchip][e->irqchip.pin] = e->gsi;
     ...
}

Could this allow a user-supplied routing entry to provide an arbitrarily large irqchip index, causing an out-of-bounds write of e->gsi into the heap memory following the kvm_irq_routing_table?

[ ... ]