From: David Woodhouse <[email protected]> A higher VTL arms register intercepts on IDTR/GDTR/LDTR/TR writes (KVM_CAP_HYPERV_REG_INTERCEPT), and the emulator diverts an intercepted load into a KVM_EXIT_HYPERV_REG_INTERCEPT exit instead of applying it (hv_divert_dt_write()). Only VMX armed the hardware exits (SECONDARY_EXEC_DESC), so on AMD a VTL0 lidt/lgdt/lldt/ltr executed unintercepted and the higher VTL never received its register intercept messages: some twenty hyperv_vsm KUT register-intercept tests fail, and a VTL1 that relies on the intercepts for kernel patch protection silently loses them.
Arm the four VMCB descriptor-table load intercepts whenever reg_intercept_control is non-zero, mirroring the all-or-nothing VMX control, and route the exits into kvm_emulate_instruction(): a load the divert check declines is applied by the emulator as before. Store intercepts stay untrapped; the control has no bits for them. The (dis)arming gets a dedicated kvm_x86_ops.update_hv_reg_intercepts() op touching only the L1 controls, instead of riding on hooks with lifecycles of their own: vmx_set_cr4() owned SECONDARY_EXEC_DESC for UMIP emulation and a VTL0 set-then-clear of CR4.UMIP disarmed the very lidt/lgdt trap hv_divert_dt_write() exists to catch; enabling the cap through vcpu_after_set_cpuid() after an exit from L2 rewrote vmcs02 state that the next nested VM-exit threw away. On VMX the op defers while vmcs02 is loaded and nested_vmx_vmexit() refreshes vmcs01 after switching back; on SVM it arms vmcb01, and nested_vmcb02_recalc_intercepts() strips the LOAD_* bits from vmcb02 unless vmcb12 wants them, matching the VMX DESC strip. The CR4 path preserves the control while the intercepts are armed, and hv_divert_dt_write() bails in guest mode so an L2 descriptor exit which still reaches the emulator (UMIP emulation, for example) applies the load normally. Arming is refused with -EINVAL on SEV guests, failing KVM_ENABLE_CAP before anything is armed (disarming still succeeds): the diverted load needs full emulation, which encrypted guests cannot have. On SEV-ES desc_interception() hits svm_check_emulate_instruction() returning X86EMUL_RETRY_INSTR, resuming at the same RIP with the intercept still armed, so one descriptor-table load spins a vCPU (and a host CPU) forever; on plain SEV the emulator has no instruction bytes and injects #UD on an architecturally valid lgdt. Signed-off-by: David Woodhouse <[email protected]> Assisted-by: LLM --- arch/x86/include/asm/kvm-x86-ops.h | 1 + arch/x86/include/asm/kvm_host.h | 7 ++++ arch/x86/kvm/hyperv.h | 16 ++++++++ arch/x86/kvm/svm/nested.c | 16 ++++++++ arch/x86/kvm/svm/svm.c | 66 ++++++++++++++++++++++++++++++ arch/x86/kvm/vmx/main.c | 10 +++++ arch/x86/kvm/vmx/nested.c | 1 + arch/x86/kvm/vmx/vmx.c | 65 ++++++++++++++++++++++------- arch/x86/kvm/vmx/x86_ops.h | 1 + arch/x86/kvm/x86.c | 19 ++++++--- 10 files changed, 183 insertions(+), 19 deletions(-) diff --git a/arch/x86/include/asm/kvm-x86-ops.h b/arch/x86/include/asm/kvm-x86-ops.h index 4f96090c04c9..428daf81f258 100644 --- a/arch/x86/include/asm/kvm-x86-ops.h +++ b/arch/x86/include/asm/kvm-x86-ops.h @@ -140,6 +140,7 @@ KVM_X86_OP(get_feature_msr) KVM_X86_OP(check_emulate_instruction) KVM_X86_OP(apic_init_signal_blocked) KVM_X86_OP_OPTIONAL(enable_l2_tlb_flush) +KVM_X86_OP(update_hv_reg_intercepts) KVM_X86_OP_OPTIONAL(migrate_timers) KVM_X86_OP(recalc_intercepts) KVM_X86_OP(complete_emulated_msr) diff --git a/arch/x86/include/asm/kvm_host.h b/arch/x86/include/asm/kvm_host.h index e03ae6493641..cf36baceaabc 100644 --- a/arch/x86/include/asm/kvm_host.h +++ b/arch/x86/include/asm/kvm_host.h @@ -2109,6 +2109,13 @@ struct kvm_x86_ops { bool (*apic_init_signal_blocked)(struct kvm_vcpu *vcpu); int (*enable_l2_tlb_flush)(struct kvm_vcpu *vcpu); + /* + * Bring the L1 (vmcs01/vmcb01) descriptor-table intercepts in line + * with hv_vcpu->reg_intercept_control. Never touches L2 controls; + * safe to call at any point, including with a vCPU in guest mode. + */ + int (*update_hv_reg_intercepts)(struct kvm_vcpu *vcpu); + void (*migrate_timers)(struct kvm_vcpu *vcpu); void (*recalc_intercepts)(struct kvm_vcpu *vcpu); int (*complete_emulated_msr)(struct kvm_vcpu *vcpu, int err); diff --git a/arch/x86/kvm/hyperv.h b/arch/x86/kvm/hyperv.h index 5ba2bb2e649b..810055c71301 100644 --- a/arch/x86/kvm/hyperv.h +++ b/arch/x86/kvm/hyperv.h @@ -237,6 +237,18 @@ static inline bool kvm_hv_vcpu_suspended(struct kvm_vcpu *vcpu) READ_ONCE(vcpu->arch.hyperv->suspended); } +/* + * True when a higher VTL armed descriptor-table write intercepts + * (HvRegisterCrInterceptControl) on this vCPU, i.e. when the vendor code + * must trap the L1 guest's descriptor-table loads for hv_divert_dt_write(). + */ +static inline bool kvm_hv_dt_intercepts_armed(struct kvm_vcpu *vcpu) +{ + struct kvm_vcpu_hv *hv_vcpu = to_hv_vcpu(vcpu); + + return hv_vcpu && hv_vcpu->reg_intercept_control; +} + static inline bool kvm_hv_is_tlb_flush_hcall(struct kvm_vcpu *vcpu) { struct kvm_vcpu_hv *hv_vcpu = to_hv_vcpu(vcpu); @@ -328,6 +340,10 @@ static inline int kvm_hv_verify_vp_assist(struct kvm_vcpu *vcpu) { return 0; } +static inline bool kvm_hv_dt_intercepts_armed(struct kvm_vcpu *vcpu) +{ + return false; +} static inline u32 kvm_hv_get_vpindex(struct kvm_vcpu *vcpu) { return vcpu->vcpu_idx; diff --git a/arch/x86/kvm/svm/nested.c b/arch/x86/kvm/svm/nested.c index b340dc9991ad..09b57ea115b0 100644 --- a/arch/x86/kvm/svm/nested.c +++ b/arch/x86/kvm/svm/nested.c @@ -156,6 +156,22 @@ void nested_vmcb02_recalc_intercepts(struct vcpu_svm *svm) for (i = 0; i < MAX_INTERCEPT; i++) vmcb02->control.intercepts[i] |= vmcb12_ctrl->intercepts[i]; + /* + * KVM arms the descriptor-table load intercepts in vmcb01 to divert + * L1's loads into Hyper-V register-intercept exits (see + * hv_divert_dt_write()); they do not apply to L2. Drop them from + * vmcb02 unless L1 wants the intercept itself, matching the + * SECONDARY_EXEC_DESC strip in VMX's prepare_vmcs02(). + */ + if (!vmcb12_is_intercept(vmcb12_ctrl, INTERCEPT_LOAD_IDTR)) + vmcb_clr_intercept(&vmcb02->control, INTERCEPT_LOAD_IDTR); + if (!vmcb12_is_intercept(vmcb12_ctrl, INTERCEPT_LOAD_GDTR)) + vmcb_clr_intercept(&vmcb02->control, INTERCEPT_LOAD_GDTR); + if (!vmcb12_is_intercept(vmcb12_ctrl, INTERCEPT_LOAD_LDTR)) + vmcb_clr_intercept(&vmcb02->control, INTERCEPT_LOAD_LDTR); + if (!vmcb12_is_intercept(vmcb12_ctrl, INTERCEPT_LOAD_TR)) + vmcb_clr_intercept(&vmcb02->control, INTERCEPT_LOAD_TR); + /* If SMI is not intercepted, ignore guest SMI intercept as well */ if (!intercept_smi) vmcb_clr_intercept(&vmcb02->control, INTERCEPT_SMI); diff --git a/arch/x86/kvm/svm/svm.c b/arch/x86/kvm/svm/svm.c index a30f6f888b51..42a12f2ad612 100644 --- a/arch/x86/kvm/svm/svm.c +++ b/arch/x86/kvm/svm/svm.c @@ -2084,6 +2084,17 @@ static int npf_interception(struct kvm_vcpu *vcpu) return rc; } +static int desc_interception(struct kvm_vcpu *vcpu) +{ + /* + * Descriptor-table write exits are armed only while a higher VTL + * has register intercepts on them (reg_intercept_control); the + * emulator diverts the write into a register-intercept exit via + * hv_divert_dt_write() instead of applying it. + */ + return kvm_emulate_instruction(vcpu, 0); +} + static int db_interception(struct kvm_vcpu *vcpu) { struct kvm_run *kvm_run = vcpu->run; @@ -3365,6 +3376,10 @@ static int (*const svm_exit_handlers[])(struct kvm_vcpu *vcpu) = { [SVM_EXIT_WRITE_CR3] = cr_interception, [SVM_EXIT_WRITE_CR4] = cr_interception, [SVM_EXIT_WRITE_CR8] = cr8_write_interception, + [SVM_EXIT_IDTR_WRITE] = desc_interception, + [SVM_EXIT_GDTR_WRITE] = desc_interception, + [SVM_EXIT_LDTR_WRITE] = desc_interception, + [SVM_EXIT_TR_WRITE] = desc_interception, [SVM_EXIT_READ_DR0] = dr_interception, [SVM_EXIT_READ_DR1] = dr_interception, [SVM_EXIT_READ_DR2] = dr_interception, @@ -4758,6 +4773,49 @@ static bool svm_has_emulated_msr(struct kvm *kvm, u32 index) return true; } +static int svm_update_hv_reg_intercepts(struct kvm_vcpu *vcpu) +{ + struct vcpu_svm *svm = to_svm(vcpu); + + /* + * Trap descriptor-table loads while a higher VTL has register + * intercepts armed on them (reg_intercept_control), and stop + * trapping when they are disarmed; the emulator diverts a trapped + * load into a register-intercept exit (hv_divert_dt_write()). + * VMX arms the all-or-nothing SECONDARY_EXEC_DESC; the SVM + * intercepts are per-register but armed as a block to match, and + * an exit the emulator does not divert applies the load normally. + * + * The intercepts are armed in vmcb01 only: an L2 descriptor load + * is never diverted, and nested_vmcb02_recalc_intercepts() keeps + * these bits out of vmcb02 unless L1 asked for them itself. + */ + if (kvm_hv_dt_intercepts_armed(vcpu)) { + /* + * The diverted load needs full emulation, which encrypted + * guests cannot have: on SEV-ES and SNP the emulator cannot + * read the registers, so svm_check_emulate_instruction() + * retries at the same RIP with the intercept still armed and + * every descriptor-table load livelocks the vCPU; on plain + * SEV there are no instruction bytes to decode and a valid + * lgdt takes a spurious #UD. Refuse to arm. + */ + if (is_sev_guest(vcpu)) + return -EINVAL; + + svm_set_intercept(svm, INTERCEPT_LOAD_IDTR); + svm_set_intercept(svm, INTERCEPT_LOAD_GDTR); + svm_set_intercept(svm, INTERCEPT_LOAD_LDTR); + svm_set_intercept(svm, INTERCEPT_LOAD_TR); + } else { + svm_clr_intercept(svm, INTERCEPT_LOAD_IDTR); + svm_clr_intercept(svm, INTERCEPT_LOAD_GDTR); + svm_clr_intercept(svm, INTERCEPT_LOAD_LDTR); + svm_clr_intercept(svm, INTERCEPT_LOAD_TR); + } + return 0; +} + static void svm_vcpu_after_set_cpuid(struct kvm_vcpu *vcpu) { struct vcpu_svm *svm = to_svm(vcpu); @@ -4784,6 +4842,13 @@ static void svm_vcpu_after_set_cpuid(struct kvm_vcpu *vcpu) if (guest_cpuid_is_intel_compatible(vcpu)) guest_cpu_cap_clear(vcpu->common, X86_FEATURE_V_VMSAVE_VMLOAD); + /* + * Re-arm the descriptor-table intercepts; setting guest CPUID does + * not change reg_intercept_control, but keep the controls in sync + * the same way vmx_vcpu_after_set_cpuid() does. + */ + svm_update_hv_reg_intercepts(vcpu); + if (is_sev_guest(vcpu)) sev_vcpu_after_set_cpuid(svm); } @@ -5538,6 +5603,7 @@ struct kvm_x86_ops svm_x86_ops __initdata = { .apic_init_signal_blocked = svm_apic_init_signal_blocked, .recalc_intercepts = svm_recalc_intercepts, + .update_hv_reg_intercepts = svm_update_hv_reg_intercepts, .complete_emulated_msr = svm_complete_emulated_msr, .vcpu_deliver_sipi_vector = svm_vcpu_deliver_sipi_vector, diff --git a/arch/x86/kvm/vmx/main.c b/arch/x86/kvm/vmx/main.c index 81a8d398068b..24663bfc14b8 100644 --- a/arch/x86/kvm/vmx/main.c +++ b/arch/x86/kvm/vmx/main.c @@ -207,6 +207,15 @@ static void vt_recalc_intercepts(struct kvm_vcpu *vcpu) vmx_recalc_intercepts(vcpu); } +static int vt_update_hv_reg_intercepts(struct kvm_vcpu *vcpu) +{ + /* TDX doesn't allow descriptor-table loads to be intercepted. */ + if (is_td_vcpu(vcpu)) + return -EINVAL; + + return vmx_update_hv_reg_intercepts(vcpu); +} + static int vt_complete_emulated_msr(struct kvm_vcpu *vcpu, int err) { if (is_td_vcpu(vcpu)) @@ -1018,6 +1027,7 @@ struct kvm_x86_ops vt_x86_ops __initdata = { .check_emulate_instruction = vt_op(check_emulate_instruction), .apic_init_signal_blocked = vt_op(apic_init_signal_blocked), + .update_hv_reg_intercepts = vt_op(update_hv_reg_intercepts), .migrate_timers = vmx_migrate_timers, .recalc_intercepts = vt_op(recalc_intercepts), diff --git a/arch/x86/kvm/vmx/nested.c b/arch/x86/kvm/vmx/nested.c index 3fe88f29be7a..de5d72502e70 100644 --- a/arch/x86/kvm/vmx/nested.c +++ b/arch/x86/kvm/vmx/nested.c @@ -5142,6 +5142,7 @@ void __nested_vmx_vmexit(struct kvm_vcpu *vcpu, u32 vm_exit_reason, * Update any VMCS fields that might have changed while vmcs02 was the * active VMCS. The tracking is per-vCPU, not per-VMCS. */ + vmx_update_hv_reg_intercepts(vcpu); vmcs_write32(VM_EXIT_MSR_STORE_COUNT, vmx->msr_autostore.nr); vmcs_write32(VM_EXIT_MSR_LOAD_COUNT, vmx->msr_autoload.host.nr); vmcs_write32(VM_ENTRY_MSR_LOAD_COUNT, vmx->msr_autoload.guest.nr); diff --git a/arch/x86/kvm/vmx/vmx.c b/arch/x86/kvm/vmx/vmx.c index ec50e46404af..bc5b08ebaefb 100644 --- a/arch/x86/kvm/vmx/vmx.c +++ b/arch/x86/kvm/vmx/vmx.c @@ -3535,6 +3535,42 @@ bool vmx_is_valid_cr4(struct kvm_vcpu *vcpu, unsigned long cr4) return true; } +/* + * SECONDARY_EXEC_DESC has two consumers: UMIP emulation (descriptor-table + * reads must #GP when CR4.UMIP is set and the CPU cannot do it) and the + * Hyper-V VTL register intercepts (descriptor-table writes divert into + * KVM_EXIT_HYPERV_REG_INTERCEPT, see hv_divert_dt_write()). Every arm or + * disarm of the control for one consumer must preserve the other's claim. + */ +static bool vmx_need_desc_exiting(struct kvm_vcpu *vcpu) +{ + return (vmx_umip_emulated() && + kvm_is_cr4_bit_set(vcpu, X86_CR4_UMIP)) || + kvm_hv_dt_intercepts_armed(vcpu); +} + +int vmx_update_hv_reg_intercepts(struct kvm_vcpu *vcpu) +{ + struct vcpu_vmx *vmx = to_vmx(vcpu); + + if (!(vmcs_config.cpu_based_2nd_exec_ctrl & SECONDARY_EXEC_DESC)) + return 0; + + /* + * Only vmcs01 carries the register intercepts: prepare_vmcs02() + * strips SECONDARY_EXEC_DESC unless vmcs12 or L2's UMIP emulation + * wants it, so an L2 descriptor load is never diverted. While + * vmcs02 is loaded, leave it alone; nested_vmx_vmexit() refreshes + * vmcs01 after switching back. + */ + if (vmx->loaded_vmcs != &vmx->vmcs01) + return 0; + + secondary_exec_controls_changebit(vmx, SECONDARY_EXEC_DESC, + vmx_need_desc_exiting(vcpu)); + return 0; +} + void vmx_set_cr4(struct kvm_vcpu *vcpu, unsigned long cr4) { unsigned long old_cr4 = kvm_read_cr4(vcpu); @@ -3558,8 +3594,16 @@ void vmx_set_cr4(struct kvm_vcpu *vcpu, unsigned long cr4) if (cr4 & X86_CR4_UMIP) { secondary_exec_controls_setbit(vmx, SECONDARY_EXEC_DESC); hw_cr4 &= ~X86_CR4_UMIP; - } else if (!is_guest_mode(vcpu) || - !nested_cpu_has2(get_vmcs12(vcpu), SECONDARY_EXEC_DESC)) { + } else if (is_guest_mode(vcpu)) { + if (!nested_cpu_has2(get_vmcs12(vcpu), SECONDARY_EXEC_DESC)) + secondary_exec_controls_clearbit(vmx, + SECONDARY_EXEC_DESC); + } else if (!kvm_hv_dt_intercepts_armed(vcpu)) { + /* + * Clearing CR4.UMIP must not disarm the control + * while a higher VTL holds register intercepts on + * the descriptor tables. + */ secondary_exec_controls_clearbit(vmx, SECONDARY_EXEC_DESC); } } @@ -5648,7 +5692,7 @@ static int handle_desc(struct kvm_vcpu *vcpu) * SECONDARY_EXEC_DESC control. */ WARN_ON_ONCE(!kvm_is_cr4_bit_set(vcpu, X86_CR4_UMIP) && - !(to_hv_vcpu(vcpu) && to_hv_vcpu(vcpu)->reg_intercept_control)); + !kvm_hv_dt_intercepts_armed(vcpu)); return kvm_emulate_instruction(vcpu, 0); } @@ -8105,18 +8149,11 @@ void vmx_vcpu_after_set_cpuid(struct kvm_vcpu *vcpu) vmx_update_exception_bitmap(vcpu); /* - * Trap descriptor-table instructions when a higher VTL armed - * register intercepts on them, and stop trapping when the - * intercepts are disarmed. Preserve the control when UMIP - * emulation needs it (CR4.UMIP owns it then). + * Setting the secondary exec controls above recomputed them from + * scratch; re-arm descriptor-table exiting for the VTL register + * intercepts and UMIP emulation. */ - if (vmcs_config.cpu_based_2nd_exec_ctrl & SECONDARY_EXEC_DESC) { - if (to_hv_vcpu(vcpu) && to_hv_vcpu(vcpu)->reg_intercept_control) - secondary_exec_controls_setbit(vmx, SECONDARY_EXEC_DESC); - else if (!(vmx_umip_emulated() && - kvm_is_cr4_bit_set(vcpu, X86_CR4_UMIP))) - secondary_exec_controls_clearbit(vmx, SECONDARY_EXEC_DESC); - } + vmx_update_hv_reg_intercepts(vcpu); } static __init u64 vmx_get_perf_capabilities(void) diff --git a/arch/x86/kvm/vmx/x86_ops.h b/arch/x86/kvm/vmx/x86_ops.h index b75dfe7f039d..10db312faa8e 100644 --- a/arch/x86/kvm/vmx/x86_ops.h +++ b/arch/x86/kvm/vmx/x86_ops.h @@ -53,6 +53,7 @@ void vmx_deliver_interrupt(struct kvm_lapic *apic, int delivery_mode, void vmx_vcpu_after_set_cpuid(struct kvm_vcpu *vcpu); bool vmx_has_emulated_msr(struct kvm *kvm, u32 index); void vmx_recalc_intercepts(struct kvm_vcpu *vcpu); +int vmx_update_hv_reg_intercepts(struct kvm_vcpu *vcpu); void vmx_prepare_switch_to_guest(struct kvm_vcpu *vcpu); void vmx_update_exception_bitmap(struct kvm_vcpu *vcpu); int vmx_get_feature_msr(u32 msr, u64 *data); diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c index e2e54f06b092..5190aa7025e3 100644 --- a/arch/x86/kvm/x86.c +++ b/arch/x86/kvm/x86.c @@ -6222,9 +6222,16 @@ static int kvm_vcpu_ioctl_enable_cap(struct kvm_vcpu *vcpu, return r; hv_vcpu = to_hv_vcpu(vcpu); hv_vcpu->reg_intercept_control = cap->args[0]; - /* Refresh the descriptor-table exiting controls. */ - kvm_x86_call(vcpu_after_set_cpuid)(vcpu); - return 0; + /* + * Refresh the descriptor-table exiting controls. The vendor + * op touches only the L1 controls, so it is safe here even + * when the vCPU last exited from a nested guest and + * vcpu_load() restored vmcs02/vmcb02. + */ + r = kvm_x86_call(update_hv_reg_intercepts)(vcpu); + if (r) + hv_vcpu->reg_intercept_control = 0; + return r; } case KVM_CAP_HYPERV_DIRECT_TLBFLUSH: if (!kvm_x86_ops.enable_l2_tlb_flush) @@ -8900,7 +8907,9 @@ static void emulator_get_idt(struct x86_emulate_ctxt *ctxt, struct desc_ptr *dt) /* * Divert a descriptor-table write into a register-intercept exit when the * higher VTL armed one: the write is not applied, and userspace delivers - * the TLFS register-intercept message to the higher VTL. + * the TLFS register-intercept message to the higher VTL. The intercept + * contract covers only the L1 guest; a descriptor load emulated on behalf + * of a nested (L2) guest, however its exit arrived, is applied normally. */ static bool hv_divert_dt_write(struct x86_emulate_ctxt *ctxt, u32 hv_reg, const struct desc_ptr *dt, u64 ctl_bit) @@ -8909,7 +8918,7 @@ static bool hv_divert_dt_write(struct x86_emulate_ctxt *ctxt, u32 hv_reg, struct kvm_vcpu *vcpu = emul_to_vcpu(ctxt); struct kvm_vcpu_hv *hv_vcpu = to_hv_vcpu(vcpu); - if (!hv_vcpu || vcpu->plane_level || + if (!hv_vcpu || vcpu->plane_level || is_guest_mode(vcpu) || !(hv_vcpu->reg_intercept_control & ctl_bit)) return false; -- 2.50.1

