feat: kvm and qemu patches grouped by the detection each clears
This commit is contained in:
105
patches/kvm/0000-cover-letter.txt
Normal file
105
patches/kvm/0000-cover-letter.txt
Normal 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(-)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
195
patches/kvm/EXPERIMENTAL-0006-runtime-cpuid-passthrough.patch
Normal file
195
patches/kvm/EXPERIMENTAL-0006-runtime-cpuid-passthrough.patch
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user