[PATCH 17/17] KVM: selftests: Add nested transition benchmark

From: David Woodhouse

Date: Sun Sep 20 2026 - 17:25:27 EST


From: David Woodhouse <dwmw@xxxxxxxxxxxx>

Measure L1<->L2 transition costs from L1's viewpoint, in TSC cycles:
a reflected-exit phase (cpuid in L2, forwarded to L1: the full
emulated vmexit->vmentry round trip, exercising the vmcb12/vmcs12
access paths), and on SVM an MSR-intercept phase (rdmsr in L2 with
INTERCEPT_MSR_PROT: exercises the per-intercept lookup in L1's MSR
permissions map). Reports min/avg/p50/p99/max over 100k iterations.

Works on both VMX and SVM.

Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
Assisted-by: Claude:claude-mythos-5
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../kvm/x86/nested_transition_bench.c | 204 ++++++++++++++++++
2 files changed, 205 insertions(+)
create mode 100644 tools/testing/selftests/kvm/x86/nested_transition_bench.c

diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index b30fa994d5e5..2c5cd619f747 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -156,6 +156,7 @@ TEST_GEN_PROGS_x86 += x86/triple_fault_event_test
TEST_GEN_PROGS_x86 += x86/recalc_apic_map_test
TEST_GEN_PROGS_x86 += x86/aperfmperf_test
TEST_GEN_PROGS_x86 += x86/vmx_apic_update_test
+TEST_GEN_PROGS_x86 += x86/nested_transition_bench
TEST_GEN_PROGS_x86 += access_tracking_perf_test
TEST_GEN_PROGS_x86 += coalesced_io_test
TEST_GEN_PROGS_x86 += dirty_log_perf_test
diff --git a/tools/testing/selftests/kvm/x86/nested_transition_bench.c b/tools/testing/selftests/kvm/x86/nested_transition_bench.c
new file mode 100644
index 000000000000..12fc1ac610e9
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/nested_transition_bench.c
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * nested_transition_bench
+ *
+ * Measure nested virtualization transition costs:
+ * 1. L2 exit reflected to L1 (cpuid): full emulated vmexit->vmentry
+ * round trip, exercising the vmcb12/vmcs12 access paths.
+ * 2. L2 MSR access with MSR intercepts enabled: exercises the
+ * per-intercept L1 permissions-bitmap lookup on SVM, and the
+ * bitmap merge path when force_msr_bitmap_recalc is set.
+ *
+ * Reports TSC cycles per iteration (min/avg/p99) from L1's viewpoint.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "test_util.h"
+#include "kvm_util.h"
+#include "processor.h"
+#include "vmx.h"
+#include "svm_util.h"
+
+/*
+ * Keep the iteration count modest: every timed iteration includes a
+ * ucall round trip to userspace to report its sample, which on a
+ * debug (KASAN) kernel costs milliseconds. 10k samples still give a
+ * stable p99.
+ */
+#define N_WARMUP 1000
+#define N_ITERS 10000
+
+
+static uint64_t results[N_ITERS];
+
+/* L2: exit to L1 in a tight loop. */
+static void l2_guest_code(void)
+{
+ u32 eax, ebx, ecx, edx;
+
+ for (;;)
+ cpuid(0, &eax, &ebx, &ecx, &edx);
+}
+
+static void l2_guest_code_msr(void)
+{
+ for (;;)
+ rdmsr(MSR_IA32_TSC_DEADLINE);
+}
+
+/* SVM L1: run L2, timing VMRUN -> #VMEXIT round trips. */
+static void l1_svm_code(struct svm_test_data *svm)
+{
+ struct vmcb *vmcb = svm->vmcb;
+ uint64_t t0, t1;
+ int i;
+
+ generic_svm_setup(svm, l2_guest_code);
+ /*
+ * Unlike VMX, where CPUID unconditionally exits, SVM only
+ * intercepts CPUID if asked; without it L0 emulates L2's cpuid
+ * and resumes L2 directly, and run_guest() never returns.
+ */
+ vmcb->control.intercept |= BIT_ULL(INTERCEPT_CPUID) |
+ BIT_ULL(INTERCEPT_MSR_PROT);
+
+ /*
+ * Set the read-intercept bit for MSR_IA32_TSC_DEADLINE (0x6e0,
+ * MSRPM range 0: bit 2*msr, i.e. byte 0x1b8 bit 0) in L1's MSRPM,
+ * so that L2's rdmsr in phase 2 reflects to L1. L0's per-exit
+ * lookup in this bitmap is the path being measured.
+ */
+ ((u8 *)svm->msr)[(MSR_IA32_TSC_DEADLINE & 0x1fff) * 2 / 8] |=
+ BIT((MSR_IA32_TSC_DEADLINE & 0x1fff) * 2 % 8);
+
+ for (i = 0; i < N_WARMUP; i++) {
+ run_guest(vmcb, svm->vmcb_gpa);
+ GUEST_ASSERT(vmcb->control.exit_code == SVM_EXIT_CPUID);
+ vmcb->save.rip += 2; /* cpuid */
+ }
+
+ for (i = 0; i < N_ITERS; i++) {
+ t0 = rdtsc();
+ run_guest(vmcb, svm->vmcb_gpa);
+ t1 = rdtsc();
+ GUEST_ASSERT(vmcb->control.exit_code == SVM_EXIT_CPUID);
+ vmcb->save.rip += 2;
+ GUEST_SYNC1(t1 - t0);
+ }
+
+ /* Phase 2: MSR intercept path */
+ vmcb->save.rip = (u64)l2_guest_code_msr;
+ for (i = 0; i < N_ITERS; i++) {
+ t0 = rdtsc();
+ run_guest(vmcb, svm->vmcb_gpa);
+ t1 = rdtsc();
+ GUEST_ASSERT(vmcb->control.exit_code == SVM_EXIT_MSR);
+ vmcb->save.rip += 2; /* rdmsr */
+ GUEST_SYNC1(t1 - t0);
+ }
+
+ GUEST_DONE();
+}
+
+/* VMX L1: run L2, timing VMRESUME -> vmexit round trips. */
+static void l1_vmx_code(struct vmx_pages *vmx)
+{
+ uint64_t t0, t1;
+ int i;
+
+ GUEST_ASSERT(prepare_for_vmx_operation(vmx));
+ GUEST_ASSERT(load_vmcs(vmx));
+ prepare_vmcs(vmx, l2_guest_code);
+
+ GUEST_ASSERT(!vmlaunch());
+ /* first exit is here; loop resumes */
+ for (i = 0; i < N_WARMUP; i++) {
+ GUEST_ASSERT(vmreadz(VM_EXIT_REASON) == EXIT_REASON_CPUID);
+ vmwrite(GUEST_RIP, vmreadz(GUEST_RIP) +
+ vmreadz(VM_EXIT_INSTRUCTION_LEN));
+ GUEST_ASSERT(!vmresume());
+ }
+
+ for (i = 0; i < N_ITERS; i++) {
+ GUEST_ASSERT(vmreadz(VM_EXIT_REASON) == EXIT_REASON_CPUID);
+ vmwrite(GUEST_RIP, vmreadz(GUEST_RIP) +
+ vmreadz(VM_EXIT_INSTRUCTION_LEN));
+ t0 = rdtsc();
+ GUEST_ASSERT(!vmresume());
+ t1 = rdtsc();
+ GUEST_SYNC1(t1 - t0);
+ }
+
+ GUEST_DONE();
+}
+
+static int cmp_u64(const void *a, const void *b)
+{
+ uint64_t x = *(const uint64_t *)a, y = *(const uint64_t *)b;
+
+ return x < y ? -1 : x > y;
+}
+
+static void report(const char *phase, int n)
+{
+ uint64_t sum = 0;
+ int i;
+
+ qsort(results, n, sizeof(results[0]), cmp_u64);
+ for (i = 0; i < n; i++)
+ sum += results[i];
+
+ printf("%-24s n=%d min=%" PRIu64 " avg=%" PRIu64 " p50=%" PRIu64
+ " p99=%" PRIu64 " max=%" PRIu64 " cycles\n",
+ phase, n, results[0], sum / n, results[n / 2],
+ results[(int)(n * 0.99)], results[n - 1]);
+}
+
+int main(int argc, char *argv[])
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ struct ucall uc;
+ gva_t nested_gva = 0;
+ bool is_svm = kvm_cpu_has(X86_FEATURE_SVM);
+ int n = 0, phase = 0;
+ bool done = false;
+
+ TEST_REQUIRE(is_svm || kvm_cpu_has(X86_FEATURE_VMX));
+
+ vm = vm_create_with_one_vcpu(&vcpu, is_svm ? (void *)l1_svm_code
+ : (void *)l1_vmx_code);
+ if (is_svm)
+ vcpu_alloc_svm(vm, &nested_gva);
+ else
+ vcpu_alloc_vmx(vm, &nested_gva);
+ vcpu_args_set(vcpu, 1, nested_gva);
+
+ while (!done) {
+ vcpu_run(vcpu);
+ switch (get_ucall(vcpu, &uc)) {
+ case UCALL_SYNC:
+ results[n++] = uc.args[0];
+ if (n == N_ITERS) {
+ report(phase == 0 ? "reflected-exit" :
+ "msr-intercept", n);
+ n = 0;
+ phase++;
+ }
+ break;
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ case UCALL_DONE:
+ if (n)
+ report("partial", n);
+ done = true;
+ break;
+ default:
+ TEST_FAIL("Unknown ucall %lu", uc.cmd);
+ }
+ }
+
+ kvm_vm_free(vm);
+ return 0;
+}
--
2.55.0