feat: kvm and qemu patches grouped by the detection each clears

This commit is contained in:
2026-09-06 00:14:56 +02:00
commit 2e5269995a
17 changed files with 1912 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
Subject: [PATCH 0/5] KVM: x86: match the architecture in four guest-observable exception cases
To: Sean Christopherson <seanjc@google.com>, Paolo Bonzini <pbonzini@redhat.com>
Cc: Thomas Gleixner <tglx@kernel.org>, Ingo Molnar <mingo@redhat.com>,
Borislav Petkov <bp@alien8.de>, Dave Hansen <dave.hansen@linux.intel.com>,
"H. Peter Anvin" <hpa@zytor.com>, x86@kernel.org,
kvm@vger.kernel.org, linux-kernel@vger.kernel.org
Four cases where a guest observes a different exception, or a different
RIP in the exception frame, than the same instruction produces on bare
metal. All four are reachable from unprivileged guest code, none needs
a debugger attached, and each is a one-hunk fix on a path KVM already
owns. The series does not add any new behaviour; it makes the existing
behaviour match the AMD64 architecture (and, for 2/5 and 3/5, what the
VMX side of KVM already does).
1/5 KVM: SVM: intercept #GP when guest EFER.SVME is clear
svm_set_efer() forces EFER_SVME=1 in the VMCB, so a CPL>0
VMLOAD/VMSAVE/STGI/CLGI passes the hardware SVME check and #GPs
before the instruction intercept can fire. Bare metal, where
SVME really is clear, #UDs. svm_recalc_instruction_intercepts()
already carries a FIXME for this. Intercept #GP while the
guest's SVME is clear so gp_interception() can decode the
instruction and route it to nested_svm_check_permissions(),
which injects the #UD. SEV guests are excluded as KVM cannot
decode their instructions, mirroring the erratum workaround.
2/5 KVM: x86: emulator: #UD, not #GP, for VMCALL at CPL > 0
group7_rm0[] declares VMCALL as Priv without PrivUD, so the CPL
check in x86_emulate_insn() injects #GP(0). 0F 01 C1 is not an
AMD instruction and is invalid outside VMX operation on Intel;
hardware #UDs at CPL > 0 on both vendors. This path is reached
on AMD through the #UD intercept (a guest built for Intel running
on AMD) and on Intel only under forced emulation.
3/5 KVM: x86: #UD for KVM hypercalls issued at CPL > 0
kvm_emulate_hypercall() returns -KVM_EPERM and skips the
instruction for a CPL > 0 caller, so the guest observes no
exception at all. VMMCALL/VMCALL is only a legal instruction
because the hypervisor intercepts it; bare metal #UDs. Inject
#UD instead. This is a guest-observable ABI change, argued
below.
4/5 KVM: SVM: intercept ICEBP and skip it before injecting its #DB
ICEBP's #DB is trap-like: the exception frame RIP is the next
instruction. SVM does not route ICEBP's #DB through the #DB
exception intercept, so the guest normally takes it correctly,
but when the delivery is interrupted by a VM-exit (an NPT fault
on a cold IDT/handler page) EXITINTINFO reports the pending #DB
with RIP saved on the ICEBP and svm_complete_interrupts()
re-injects it as-is, so the guest handler sees the ICEBP's own
address. VMX tags ICEBP (is_icebp()) and skips the instruction;
SVM has the dedicated ICEBP intercept, which fires before the #DB
exists. Enable it and skip.
5/5 KVM: selftests: verify ICEBP #DB reports RIP past the ICEBP
On the 3/5 ABI change: every KVM_HC_* already fails for a CPL > 0
caller, so only the shape of the failure changes, from a silent skip to
#UD. The Xen and Hyper-V dispatch sit before the check and keep their
own CPL semantics; kvm_hv_hypercall() already injects #UD for CPL > 0
(hyperv.c), so this makes the KVM PV path consistent with it rather
than inventing new behaviour. No in-tree guest issues a KVM hypercall
from userspace: Linux paravirt patches VMMCALL/VMCALL at CPL0 only, and
svm_patch_hypercall() / vmx_patch_hypercall() likewise. Windows guests
issue neither.
Vendor coverage: 1/5 and 4/5 are SVM-only. 2/5 and 3/5 live in
emulate.c and x86.c and affect Intel as well. On VMX a CPL > 0 VMCALL
unconditionally VM-exits to handle_vmcall() -> kvm_emulate_hypercall(),
so 3/5 changes its outcome from "skipped silently" to #UD there too,
which is what VMCALL outside VMX operation does on bare metal. The
series has been tested on AMD only (Zen 4, 7950X); the Intel reasoning
is from the code and has not been run on Intel hardware.
Nested: for 1/5, gp_interception() already routes decoded SVM
instructions from L2 through nested_svm_check_permissions(); when L1
intercepts #GP itself the exit is reflected to L1 as before. For 4/5
the #DB is reflected to L1 with L2's RIP past the ICEBP, matching what
the VMX path does today.
SEV: 1/5 does not arm the #GP intercept for SEV guests, and SEV-ES
guests additionally clear it in sev_es_init_vmcb(). 4/5 uses
svm_skip_emulated_instruction(), which already special-cases SEV-ES.
Testing: a Windows 11 guest, a Linux guest and a Hyper-V-enlightened
Windows guest boot and run normally on the patched modules. The
userspace probes that first exposed the divergences (CPL3 VMLOAD, CPL3
VMMCALL, ICEBP under SEH) now observe #UD / #UD / RIP+1, as on bare
metal, and the ICEBP one no longer regresses when the delivery hits a
cold page. 5/5 fails on unpatched SVM (Zen 4, kvm_exit trace shows the
NPT fault with the #DB pending in EXITINTINFO) and passes with 4/5. A
selftest for the CPL > 0 cases would need user-mode guest code, which
the x86 selftest library does not currently provide.
arch/x86/kvm/emulate.c | 7 ++-
arch/x86/kvm/svm/svm.c | 44 ++++++++++++---
arch/x86/kvm/x86.c | 17 +++++-
tools/testing/selftests/kvm/Makefile.kvm | 1 +
tools/testing/selftests/kvm/x86/icebp_test.c | 58 ++++++++++++++++++++
5 files changed, 117 insertions(+), 10 deletions(-)

View File

@@ -0,0 +1,94 @@
From 0a6b8d86c62bd4caebf27872019c7f66b753d61b Mon Sep 17 00:00:00 2001
From: Sandwich <sandwich@archworks.co>
Date: Sat, 5 Sep 2026 12:02:50 +0200
Subject: [PATCH 1/5] KVM: SVM: intercept #GP when guest EFER.SVME is clear
svm_set_efer() unconditionally sets EFER_SVME in the VMCB, because
hardware requires it to run the guest at all. A guest that has not
itself enabled SVM therefore still runs with SVME set from the CPU's
point of view.
When such a guest executes an SVM instruction at CPL > 0, hardware
passes the SVME check, observes CPL != 0, and injects #GP before the
instruction intercept can fire. Bare metal, where SVME really is clear,
raises #UD instead. The divergence is directly observable from
unprivileged guest code.
svm_recalc_instruction_intercepts() already carries a FIXME describing
exactly this. Close it by intercepting #GP while the guest's EFER.SVME
is clear, so gp_interception() can decode the instruction and route it
to its real handler, which injects the #UD the architecture specifies.
The intercept is kept across a guest clearing SVME, and dropped again on
SVME=1 where the erratum workaround does not need it.
SEV guests are excluded for the same reason the erratum workaround
excludes them: KVM can't decode their instructions, so the intercept
would only reinject the same #GP. SEV-ES guests additionally clear
the #GP intercept in sev_es_init_vmcb().
For nested guests, gp_interception() already routes a decoded SVM
instruction through nested_svm_check_permissions(), which injects #UD
when L2's SVME is clear. When L1 intercepts #GP itself, the exit is
reflected to L1 as before.
Signed-off-by: Sandwich <sandwich@archworks.co>
---
arch/x86/kvm/svm/svm.c | 22 +++++++++++++++-------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/arch/x86/kvm/svm/svm.c b/arch/x86/kvm/svm/svm.c
index 1158f3e28..b0db02615 100644
--- a/arch/x86/kvm/svm/svm.c
+++ b/arch/x86/kvm/svm/svm.c
@@ -237,9 +237,11 @@ int svm_set_efer(struct kvm_vcpu *vcpu, u64 efer)
kvm_make_request(KVM_REQ_TRIPLE_FAULT, vcpu);
svm_leave_nested(vcpu);
- /* #GP intercept is still needed for vmware backdoor */
- if (!enable_vmware_backdoor)
- clr_exception_intercept(svm, GP_VECTOR);
+ /*
+ * #GP stays intercepted: with SVME=0 it routes CPL>0
+ * SVM instructions to the #UD the architecture requires,
+ * see svm_recalc_instruction_intercepts().
+ */
/*
* Free the nested guest state, unless we are in SMM.
@@ -263,6 +265,8 @@ int svm_set_efer(struct kvm_vcpu *vcpu, u64 efer)
*/
if (svm_gp_erratum_intercept && !is_sev_guest(vcpu))
set_exception_intercept(svm, GP_VECTOR);
+ else if (!enable_vmware_backdoor)
+ clr_exception_intercept(svm, GP_VECTOR);
}
svm_pmu_handle_nested_transition(svm);
@@ -1073,16 +1077,20 @@ static void svm_recalc_instruction_intercepts(struct kvm_vcpu *vcpu)
* are set when the VMCB is initialized and never cleared (if the
* relevant intercepts are set, the enablements are meaningless anyway).
*
- * FIXME: When #GP is not intercepted, a #GP on these instructions (e.g.
- * due to CPL > 0) could be injected by hardware before the instruction
- * is intercepted, leading to #GP taking precedence over #UD from the
- * guest's perspective.
+ * Because hardware sees SVME=1, a #GP on these instructions (e.g. due
+ * to CPL > 0) is injected before the instruction intercept fires, and
+ * would take precedence over the #UD the guest should observe. Also
+ * intercept #GP so that gp_interception() can decode the instruction
+ * and route it to its handler, which injects the #UD. SEV guests are
+ * excluded as KVM can't decode their instructions.
*/
if (!(vcpu->arch.efer & EFER_SVME)) {
svm_set_intercept(svm, INTERCEPT_VMLOAD);
svm_set_intercept(svm, INTERCEPT_VMSAVE);
svm_set_intercept(svm, INTERCEPT_CLGI);
svm_set_intercept(svm, INTERCEPT_STGI);
+ if (!is_sev_guest(vcpu))
+ set_exception_intercept(svm, GP_VECTOR);
} else {
/*
* If hardware supports Virtual VMLOAD VMSAVE then enable it
--
2.55.0

View File

@@ -0,0 +1,47 @@
From 9826e3e9edc355aea6b2f2f386e538aa0f199542 Mon Sep 17 00:00:00 2001
From: Sandwich <sandwich@archworks.co>
Date: Sat, 5 Sep 2026 11:58:18 +0200
Subject: [PATCH 2/5] KVM: x86: emulator: #UD, not #GP, for VMCALL at CPL > 0
Targets VMAware check: KVM_INTERCEPTION
0F 01 C1 is not an AMD instruction, and on Intel VMCALL is invalid
outside VMX operation. Hardware raises #UD for it at CPL > 0 on both
vendors.
The emulator declares VMCALL as Priv without PrivUD, so the generic CPL
check injects #GP(0) instead. Windows surfaces that to userspace as an
access violation rather than an illegal instruction, which is guest-
observable and differs from every physical machine.
Add PrivUD so the existing branch injects #UD. RIP is not advanced
either way, and CPL0 VMCALL is unaffected, so a guest built for Intel
and migrated onto an AMD host keeps working through
emulator_fix_hypercall().
Signed-off-by: Sandwich <sandwich@archworks.co>
---
arch/x86/kvm/emulate.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/arch/x86/kvm/emulate.c b/arch/x86/kvm/emulate.c
index b566ab5c7..a723d05d9 100644
--- a/arch/x86/kvm/emulate.c
+++ b/arch/x86/kvm/emulate.c
@@ -3969,7 +3969,12 @@ static const struct opcode ud = I(SrcNone, emulate_ud);
static const struct opcode group7_rm0[] = {
N,
- I(SrcNone | Priv | EmulateOnUD, em_hypercall),
+ /*
+ * 0F 01 C1 is not an AMD instruction, and on Intel VMCALL is invalid
+ * outside VMX operation, i.e. hardware raises #UD at CPL > 0. Without
+ * PrivUD the CPL check below injects #GP, which is guest-observable.
+ */
+ I(SrcNone | Priv | PrivUD | EmulateOnUD, em_hypercall),
N, N, N, N, N, N,
};
--
2.55.0

View File

@@ -0,0 +1,64 @@
From 122f8478087bd13892672a6feb3e3417e882c316 Mon Sep 17 00:00:00 2001
From: Sandwich <sandwich@archworks.co>
Date: Sat, 5 Sep 2026 11:58:18 +0200
Subject: [PATCH 3/5] KVM: x86: #UD for KVM hypercalls issued at CPL > 0
Targets VMAware check: KVM_INTERCEPTION
VMMCALL is only a legal instruction because the hypervisor intercepts
it; bare metal raises #UD. kvm_emulate_hypercall() instead returns
-KVM_EPERM for a CPL > 0 caller and skips the instruction, so the guest
observes no exception at all - which is directly observable and differs
from every physical machine.
Inject #UD for CPL > 0 instead. Every KVM_HC_* already failed for such
callers, so only the shape of the failure changes, and no legitimate
hypercall is issued from userspace.
The check is placed after the Xen and Hyper-V dispatch, so enlightened
guests keep their own CPL semantics. kvm_hv_hypercall() already does
the identical thing for the Hyper-V ABI, so this makes the KVM PV path
consistent with it.
Signed-off-by: Sandwich <sandwich@archworks.co>
---
arch/x86/kvm/x86.c | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 47cb9eba1..0dd7ebcc0 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -10503,14 +10503,27 @@ EXPORT_SYMBOL_FOR_KVM_INTERNAL(____kvm_emulate_hypercall);
int kvm_emulate_hypercall(struct kvm_vcpu *vcpu)
{
+ int cpl;
+
if (kvm_xen_hypercall_enabled(vcpu->kvm))
return kvm_xen_hypercall(vcpu);
if (kvm_hv_hypercall_enabled(vcpu))
return kvm_hv_hypercall(vcpu);
- return __kvm_emulate_hypercall(vcpu, kvm_x86_call(get_cpl)(vcpu),
- complete_hypercall_exit);
+ /*
+ * Bare metal #UDs on VMMCALL from CPL > 0 (the intercept is what
+ * makes it legal at all); returning -KVM_EPERM and skipping the
+ * insn instead is guest-observable. Xen and Hyper-V guests are
+ * dispatched above and keep their own CPL semantics.
+ */
+ cpl = kvm_x86_call(get_cpl)(vcpu);
+ if (cpl) {
+ kvm_queue_exception(vcpu, UD_VECTOR);
+ return 1;
+ }
+
+ return __kvm_emulate_hypercall(vcpu, cpl, complete_hypercall_exit);
}
EXPORT_SYMBOL_FOR_KVM_INTERNAL(kvm_emulate_hypercall);
--
2.55.0

View File

@@ -0,0 +1,93 @@
From 93bb902ad3516b0c5a31482e27a5ecfd93f6799d Mon Sep 17 00:00:00 2001
From: Sandwich <sandwich@archworks.co>
Date: Sat, 5 Sep 2026 12:31:16 +0200
Subject: [PATCH 4/5] KVM: SVM: intercept ICEBP and skip it before injecting
its #DB
ICEBP (INT1, opcode F1) generates a trap-like #DB: the return RIP pushed
for the exception is that of the instruction following the ICEBP.
SVM does not report ICEBP's #DB through the #DB exception intercept, so
the guest normally takes it directly and correctly. But when delivery
of that #DB is interrupted by a VM-exit - a nested page fault on a cold
IDT, handler or stack page is the common case - the exit reports the
pending #DB in EXITINTINFO with the saved RIP still on the ICEBP, and
svm_complete_interrupts() re-queues it as a plain hardware exception.
The injected #DB then pushes that RIP, so the guest's handler sees the
ICEBP's own address and an IRET re-executes it. The divergence is
observable from unprivileged guest code (an SEH/signal handler reading
the context RIP) and is sporadic, since it needs the delivery to touch
a page KVM has not mapped.
VMX already handles ICEBP: the #DB VM-exit is tagged as a privileged
software exception (is_icebp()) and handle_exception_nmi() skips the
instruction before queueing the #DB. SVM has the equivalent signal in
the dedicated ICEBP instruction intercept, which fires before the #DB
exists.
Enable the ICEBP intercept alongside the #DB exception intercept, skip
the instruction and queue the #DB with the same DR6 payload the
exception path uses. Once RIP has been advanced, an injection that is
itself interrupted is re-injected with the advanced RIP. The vendor
skip helper is used deliberately, as on VMX: a pending single-step #DB
on the ICEBP itself is superseded by the ICEBP's #DB. The emulator
decodes 0xF1, so the non-NRIPS fallback works.
For nested guests the #DB is reflected to L1 with L2's RIP past the
ICEBP, matching what the VMX path does today.
Signed-off-by: Sandwich <sandwich@archworks.co>
---
arch/x86/kvm/svm/svm.c | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/arch/x86/kvm/svm/svm.c b/arch/x86/kvm/svm/svm.c
index b0db02615..5b37855ee 100644
--- a/arch/x86/kvm/svm/svm.c
+++ b/arch/x86/kvm/svm/svm.c
@@ -1153,6 +1153,7 @@ static void init_vmcb(struct kvm_vcpu *vcpu, bool init_event)
set_exception_intercept(svm, MC_VECTOR);
set_exception_intercept(svm, AC_VECTOR);
set_exception_intercept(svm, DB_VECTOR);
+ svm_set_intercept(svm, INTERCEPT_ICEBP);
/*
* Guest access to VMware backdoor ports could legitimately
* trigger #GP because of TSS I/O permission bitmap.
@@ -2078,6 +2079,26 @@ static int db_interception(struct kvm_vcpu *vcpu)
return 1;
}
+static int icebp_interception(struct kvm_vcpu *vcpu)
+{
+ struct vcpu_svm *svm = to_svm(vcpu);
+ u32 payload = svm->vmcb->save.dr6 ^ DR6_ACTIVE_LOW;
+
+ /*
+ * ICEBP generates a trap-like #DB, but is intercepted as an
+ * instruction, i.e. RIP still points at the ICEBP itself. Skip it
+ * before injecting the #DB so the guest observes the RIP of the next
+ * instruction, as it does on bare metal and under VMX (see is_icebp()).
+ * Use the vendor skip helper: a single-step #DB on the ICEBP is
+ * superseded by the ICEBP's own #DB.
+ */
+ if (!svm_skip_emulated_instruction(vcpu))
+ return 0;
+
+ kvm_queue_exception_p(vcpu, DB_VECTOR, payload);
+ return 1;
+}
+
static int bp_interception(struct kvm_vcpu *vcpu)
{
struct vcpu_svm *svm = to_svm(vcpu);
@@ -3369,6 +3390,7 @@ static int (*const svm_exit_handlers[])(struct kvm_vcpu *vcpu) = {
[SVM_EXIT_WRITE_DR6] = dr_interception,
[SVM_EXIT_WRITE_DR7] = dr_interception,
[SVM_EXIT_EXCP_BASE + DB_VECTOR] = db_interception,
+ [SVM_EXIT_ICEBP] = icebp_interception,
[SVM_EXIT_EXCP_BASE + BP_VECTOR] = bp_interception,
[SVM_EXIT_EXCP_BASE + UD_VECTOR] = ud_interception,
[SVM_EXIT_EXCP_BASE + PF_VECTOR] = pf_interception,
--
2.55.0

View File

@@ -0,0 +1,127 @@
From 1cc0b5d1d2f61b76c2b9124646b827abda525828 Mon Sep 17 00:00:00 2001
From: Sandwich <sandwich@archworks.co>
Date: Sat, 5 Sep 2026 12:31:16 +0200
Subject: [PATCH 5/5] KVM: selftests: verify ICEBP #DB reports RIP past the
ICEBP
Execute ICEBP (INT1) in the guest under a #DB handler that records the
exception frame's RIP, and assert it is the address of the following
instruction, as on bare metal. The handler advances RIP itself if it
finds it still on the ICEBP so a failing KVM does not loop forever.
The first ICEBP in a fresh VM faults on the unmapped IDT and handler
pages during delivery, which is the interrupted-delivery case where SVM
re-injected the #DB with the wrong RIP. Fails on SVM without the
preceding patch, passes with it, and passes on VMX, which already skips
the instruction.
Signed-off-by: Sandwich <sandwich@archworks.co>
---
arch/x86/kvm/svm/svm.c | 7 ++-
tools/testing/selftests/kvm/Makefile.kvm | 1 +
tools/testing/selftests/kvm/x86/icebp_test.c | 63 ++++++++++++++++++++
3 files changed, 68 insertions(+), 3 deletions(-)
create mode 100644 tools/testing/selftests/kvm/x86/icebp_test.c
diff --git a/arch/x86/kvm/svm/svm.c b/arch/x86/kvm/svm/svm.c
index 5b37855ee..34e025cba 100644
--- a/arch/x86/kvm/svm/svm.c
+++ b/arch/x86/kvm/svm/svm.c
@@ -2087,9 +2087,10 @@ static int icebp_interception(struct kvm_vcpu *vcpu)
/*
* ICEBP generates a trap-like #DB, but is intercepted as an
* instruction, i.e. RIP still points at the ICEBP itself. Skip it
- * before injecting the #DB so the guest observes the RIP of the next
- * instruction, as it does on bare metal and under VMX (see is_icebp()).
- * Use the vendor skip helper: a single-step #DB on the ICEBP is
+ * before injecting the #DB so that the guest, and any re-injection
+ * after an exit interrupts the delivery, sees the RIP of the next
+ * instruction, as on bare metal and under VMX (see is_icebp()). Use
+ * the vendor skip helper: a single-step #DB on the ICEBP is
* superseded by the ICEBP's own #DB.
*/
if (!svm_skip_emulated_instruction(vcpu))
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 6fc34e9bf..b45fe0a13 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -138,6 +138,7 @@ TEST_GEN_PROGS_x86 += x86/xapic_tpr_test
TEST_GEN_PROGS_x86 += x86/xcr0_cpuid_test
TEST_GEN_PROGS_x86 += x86/xss_msr_test
TEST_GEN_PROGS_x86 += x86/debug_regs
+TEST_GEN_PROGS_x86 += x86/icebp_test
TEST_GEN_PROGS_x86 += x86/tsc_msrs_test
TEST_GEN_PROGS_x86 += x86/vmx_pmu_caps_test
TEST_GEN_PROGS_x86 += x86/xen_shinfo_test
diff --git a/tools/testing/selftests/kvm/x86/icebp_test.c b/tools/testing/selftests/kvm/x86/icebp_test.c
new file mode 100644
index 000000000..7890fa949
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/icebp_test.c
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * icebp_test
+ *
+ * Verify that the #DB generated by ICEBP (INT1) is delivered with RIP
+ * pointing at the instruction following the ICEBP, as on bare metal.
+ *
+ * The first ICEBP in a fresh VM faults on the not-yet-mapped IDT and
+ * handler pages while the #DB is being delivered, so the delivery is
+ * interrupted by a VM-exit and re-injected by KVM. That is exactly the
+ * case where SVM used to re-inject with RIP still on the ICEBP.
+ */
+#include "test_util.h"
+#include "kvm_util.h"
+#include "processor.h"
+
+static u64 db_rip;
+
+static void db_handler(struct ex_regs *regs)
+{
+ db_rip = regs->rip;
+
+ /* Don't loop forever if RIP was left on the ICEBP itself. */
+ if (*(u8 *)regs->rip == 0xf1)
+ regs->rip++;
+}
+
+static void guest_code(void)
+{
+ u64 next_rip;
+
+ asm volatile(".byte 0xf1\n\t" /* icebp */
+ "1: lea 1b(%%rip), %0"
+ : "=r"(next_rip) : : "memory");
+
+ GUEST_ASSERT_EQ(db_rip, next_rip);
+ GUEST_DONE();
+}
+
+int main(int argc, char *argv[])
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ struct ucall uc;
+
+ vm = vm_create_with_one_vcpu(&vcpu, guest_code);
+ vm_install_exception_handler(vm, DB_VECTOR, db_handler);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO);
+
+ switch (get_ucall(vcpu, &uc)) {
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ case UCALL_DONE:
+ break;
+ default:
+ TEST_FAIL("Unknown ucall 0x%lx.", uc.cmd);
+ }
+
+ kvm_vm_free(vm);
+ return 0;
+}
--
2.55.0

View File

@@ -0,0 +1,195 @@
From ddc09a6009d5016130acfd1aa8eef69f99da5972 Mon Sep 17 00:00:00 2001
From: Sandwich <sandwich@archworks.co>
Date: Sat, 5 Sep 2026 12:23:22 +0200
Subject: [PATCH] KVM: SVM: opt-in runtime CPUID passthrough with brand-string
override
NOT FOR UPSTREAM. Targets VMAware check: TIMER (instruction-latency
detector).
The detector times an intercepted CPUID against LFENCE using a cross-core
cache-line counter, so the #VMEXIT world switch itself is what it
measures and no clock can be adjusted to hide it. The only way to stop
paying it is to not exit.
With cpuid_passthrough=1 the CPUID intercept is cleared on every guest
entry and the guest executes raw CPUID. Raw CPUID reports the host SKU,
which contradicts a smaller declared topology, so the AMD Processor Name
String MSRs (0xC0010030-35) are reprogrammed on the pinned core while the
vCPU runs and restored in svm_vcpu_put(). Raw CPUID also advertises
RDPRU, which KVM masks out and #UDs on; the RDPRU intercept is dropped
together with the CPUID one so the guest gets the real instruction.
Constraints, all deliberate:
- AMD only; the name-string MSRs are probed for writability at load and
the brand override is skipped when the probe fails.
- Requires strict 1:1 vCPU pinning, or the override leaks onto host
cores and the guest reads inconsistent brands. The vCPU thread's
allowed-CPU mask is checked on every entry and passthrough is
withheld from any thread not confined to exactly one CPU.
- Enable only after the guest has booted. Windows enumerates KVM's
synthetic leaf-1 bits (x2apic, tsc-deadline) during boot and hangs if
they vanish mid-enumeration.
- The #DB intercept is untouched. Clearing it breaks KVM's single-step
re-injection and DR6 handling, which is separately detectable.
Signed-off-by: Sandwich <sandwich@archworks.co>
---
arch/x86/kvm/svm/svm.c | 120 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 120 insertions(+)
diff --git a/arch/x86/kvm/svm/svm.c b/arch/x86/kvm/svm/svm.c
index 34e025cba..bfc316505 100644
--- a/arch/x86/kvm/svm/svm.c
+++ b/arch/x86/kvm/svm/svm.c
@@ -179,6 +179,120 @@ module_param(vnmi, bool, 0444);
module_param(enable_mediated_pmu, bool, 0444);
+/*
+ * vfio-native: neutralise the VMAware TIMER instruction-latency detector on a
+ * strictly 1:1-pinned guest. That detector times the #VMEXIT world switch on an
+ * intercepted CPUID with a cross-core cache counter; dropping CPUID interception
+ * removes the exit so the guest measures like bare metal. Raw CPUID would then
+ * report the host SKU and trip VMAware's thread/brand checks, so the AMD
+ * Processor Name String MSRs (0xC0010030-35) are reprogrammed on the pinned core
+ * while the guest runs and restored when the vCPU yields the core.
+ *
+ * Opt-in and runtime-toggled (cpuid_passthrough). It does NOT touch the #DB
+ * intercept: clearing that breaks KVM's single-step re-injection and DR6, which
+ * is separately detectable. Requires 1:1 vCPU pinning - without it the dropped
+ * interception and reprogrammed brand leak onto the wrong cores.
+ */
+static bool cpuid_passthrough;
+module_param(cpuid_passthrough, bool, 0644);
+MODULE_PARM_DESC(cpuid_passthrough,
+ "Drop CPUID/RDPRU interception and override the CPU brand on a 1:1-pinned guest (AMD only)");
+
+static char brand_string[48];
+module_param_string(brand_string, brand_string, sizeof(brand_string), 0644);
+MODULE_PARM_DESC(brand_string,
+ "48-byte CPU brand string exposed via CPUID 0x80000002-4 while cpuid_passthrough is active");
+
+#define MSR_AMD_NAME_STRING 0xc0010030 /* AMD Processor Name String, 6 consecutive MSRs */
+
+static u64 hw_brand[6];
+static bool brand_msr_ok __ro_after_init; /* the name-string MSRs are writable here */
+static DEFINE_PER_CPU(bool, brand_applied); /* this core currently holds the override */
+
+static void svm_brand_probe(void)
+{
+ int i;
+
+ if (boot_cpu_data.x86_vendor != X86_VENDOR_AMD)
+ return;
+ for (i = 0; i < 6; i++)
+ if (rdmsrq_safe(MSR_AMD_NAME_STRING + i, &hw_brand[i]))
+ return;
+ if (wrmsrq_safe(MSR_AMD_NAME_STRING, hw_brand[0])) /* confirm writable */
+ return;
+ brand_msr_ok = true;
+}
+
+/* current cpu, preemption disabled */
+static void svm_brand_write(void)
+{
+ u64 regs[6] = {};
+ int i;
+
+ memcpy(regs, brand_string, sizeof(brand_string));
+ for (i = 0; i < 6; i++)
+ wrmsrq_safe(MSR_AMD_NAME_STRING + i, regs[i]);
+}
+
+/* current cpu, preemption disabled */
+static void svm_brand_restore(void)
+{
+ int i;
+
+ for (i = 0; i < 6; i++)
+ wrmsrq_safe(MSR_AMD_NAME_STRING + i, hw_brand[i]);
+}
+
+/* on guest entry */
+static void svm_passthrough_apply(struct kvm_vcpu *vcpu)
+{
+ struct vcpu_svm *svm = to_svm(vcpu);
+ /*
+ * Only a vCPU thread confined to exactly one CPU may run with raw
+ * CPUID and the brand override: on any other core the override is
+ * missing and the guest reads two different brands. Checking the
+ * thread's own allowed mask here verifies the 1:1 pinning instead of
+ * trusting the operator to have set it.
+ */
+ bool on = cpuid_passthrough && vcpu->guest_debug == 0 &&
+ cpumask_weight(current->cpus_ptr) == 1;
+
+ if (likely(!on)) {
+ if (unlikely(!svm_is_intercept(svm, INTERCEPT_CPUID))) {
+ svm_set_intercept(svm, INTERCEPT_CPUID);
+ svm_set_intercept(svm, INTERCEPT_RDPRU);
+ }
+ if (brand_msr_ok && this_cpu_read(brand_applied)) {
+ svm_brand_restore();
+ this_cpu_write(brand_applied, false);
+ }
+ return;
+ }
+
+ if (svm_is_intercept(svm, INTERCEPT_CPUID)) {
+ svm_clr_intercept(svm, INTERCEPT_CPUID);
+ /*
+ * Raw CPUID advertises RDPRU, which KVM masks out and #UDs on.
+ * A guest that sees the bit and executes the instruction must
+ * get the real one, or the #UD is itself a hypervisor tell.
+ */
+ svm_clr_intercept(svm, INTERCEPT_RDPRU);
+ }
+ if (brand_msr_ok && !this_cpu_read(brand_applied)) {
+ svm_brand_write();
+ this_cpu_write(brand_applied, true);
+ }
+}
+
+/* current cpu, preemption disabled */
+static void svm_passthrough_leave_cpu(void)
+{
+ if (brand_msr_ok && this_cpu_read(brand_applied)) {
+ svm_brand_restore();
+ this_cpu_write(brand_applied, false);
+ }
+}
+
static bool __ro_after_init svm_gp_erratum_intercept = true;
static u8 rsm_ins_bytes[] = "\x0f\xaa";
@@ -1500,6 +1614,8 @@ static void svm_vcpu_load(struct kvm_vcpu *vcpu, int cpu)
static void svm_vcpu_put(struct kvm_vcpu *vcpu)
{
+ svm_passthrough_leave_cpu();
+
if (kvm_vcpu_apicv_active(vcpu))
avic_vcpu_put(vcpu);
@@ -4517,6 +4633,8 @@ static __no_kcsan fastpath_t svm_vcpu_run(struct kvm_vcpu *vcpu, u64 run_flags)
struct vcpu_svm *svm = to_svm(vcpu);
unsigned enter_flags = 0;
+ svm_passthrough_apply(vcpu);
+
if (!msr_write_intercepted(svm, MSR_IA32_SPEC_CTRL))
enter_flags |= KVM_ENTER_SAVE_SPEC_CTRL;
@@ -5652,6 +5770,8 @@ static __init int svm_hardware_setup(void)
void *iopm_va;
int cpu, r;
+ svm_brand_probe();
+
/*
* NX is required for shadow paging and for NPT if the NX huge pages
* mitigation is enabled.
--
2.55.0