diff mbox series

[v17,092/116] KVM: TDX: Handle TDX PV HLT hypercall

Message ID 7ca4b7af33646e3f5693472b4394ba0179b550e1.1699368322.git.isaku.yamahata@intel.com (mailing list archive)
State New, archived
Headers show
Series KVM TDX basic feature support | expand

Commit Message

Isaku Yamahata Nov. 7, 2023, 2:56 p.m. UTC
From: Isaku Yamahata <isaku.yamahata@intel.com>

Wire up TDX PV HLT hypercall to the KVM backend function.

Signed-off-by: Isaku Yamahata <isaku.yamahata@intel.com>
---
 arch/x86/kvm/vmx/tdx.c | 42 +++++++++++++++++++++++++++++++++++++++++-
 arch/x86/kvm/vmx/tdx.h |  3 +++
 2 files changed, 44 insertions(+), 1 deletion(-)

Comments

Sean Christopherson Jan. 5, 2024, 11:05 p.m. UTC | #1
On Tue, Nov 07, 2023, isaku.yamahata@intel.com wrote:
> From: Isaku Yamahata <isaku.yamahata@intel.com>
> 
> Wire up TDX PV HLT hypercall to the KVM backend function.
> 
> Signed-off-by: Isaku Yamahata <isaku.yamahata@intel.com>
> ---
>  arch/x86/kvm/vmx/tdx.c | 42 +++++++++++++++++++++++++++++++++++++++++-
>  arch/x86/kvm/vmx/tdx.h |  3 +++
>  2 files changed, 44 insertions(+), 1 deletion(-)
> 
> diff --git a/arch/x86/kvm/vmx/tdx.c b/arch/x86/kvm/vmx/tdx.c
> index 3a1fe74b95c3..4e48989d364f 100644
> --- a/arch/x86/kvm/vmx/tdx.c
> +++ b/arch/x86/kvm/vmx/tdx.c
> @@ -662,7 +662,32 @@ void tdx_vcpu_load(struct kvm_vcpu *vcpu, int cpu)
>  
>  bool tdx_protected_apic_has_interrupt(struct kvm_vcpu *vcpu)
>  {
> -	return pi_has_pending_interrupt(vcpu);
> +	bool ret = pi_has_pending_interrupt(vcpu);
> +	struct vcpu_tdx *tdx = to_tdx(vcpu);
> +
> +	if (ret || vcpu->arch.mp_state != KVM_MP_STATE_HALTED)
> +		return true;
> +
> +	if (tdx->interrupt_disabled_hlt)
> +		return false;
> +
> +	/*
> +	 * This is for the case where the virtual interrupt is recognized,
> +	 * i.e. set in vmcs.RVI, between the STI and "HLT".  KVM doesn't have
> +	 * access to RVI and the interrupt is no longer in the PID (because it
> +	 * was "recognized".  It doesn't get delivered in the guest because the
> +	 * TDCALL completes before interrupts are enabled.
> +	 *
> +	 * TDX modules sets RVI while in an STI interrupt shadow.
> +	 * - TDExit(typically TDG.VP.VMCALL<HLT>) from the guest to TDX module.
> +	 *   The interrupt shadow at this point is gone.
> +	 * - It knows that there is an interrupt that can be delivered
> +	 *   (RVI > PPR && EFLAGS.IF=1, the other conditions of 29.2.2 don't
> +	 *    matter)
> +	 * - It forwards the TDExit nevertheless, to a clueless hypervisor that
> +	 *   has no way to glean either RVI or PPR.

WTF.  Seriously, what in the absolute hell is going on.  I reported this internally
four ***YEARS*** ago.  This is not some obscure theoretical edge case, this is core
functionality and it's completely broken garbage.

NAK.  Hard NAK.  Fix the TDX module, full stop.

Even worse, TDX 1.5 apparently _already_ has the necessary logic for dealing with
interrupts that are pending in RVI when handling NESTED VM-Enter.  Really!?!?!
Y'all went and added nested virtualization support of some kind, but can't find
the time to get the basics right?
Chao Gao Jan. 8, 2024, 5:09 a.m. UTC | #2
On Fri, Jan 05, 2024 at 03:05:12PM -0800, Sean Christopherson wrote:
>On Tue, Nov 07, 2023, isaku.yamahata@intel.com wrote:
>> From: Isaku Yamahata <isaku.yamahata@intel.com>
>> 
>> Wire up TDX PV HLT hypercall to the KVM backend function.
>> 
>> Signed-off-by: Isaku Yamahata <isaku.yamahata@intel.com>
>> ---
>>  arch/x86/kvm/vmx/tdx.c | 42 +++++++++++++++++++++++++++++++++++++++++-
>>  arch/x86/kvm/vmx/tdx.h |  3 +++
>>  2 files changed, 44 insertions(+), 1 deletion(-)
>> 
>> diff --git a/arch/x86/kvm/vmx/tdx.c b/arch/x86/kvm/vmx/tdx.c
>> index 3a1fe74b95c3..4e48989d364f 100644
>> --- a/arch/x86/kvm/vmx/tdx.c
>> +++ b/arch/x86/kvm/vmx/tdx.c
>> @@ -662,7 +662,32 @@ void tdx_vcpu_load(struct kvm_vcpu *vcpu, int cpu)
>>  
>>  bool tdx_protected_apic_has_interrupt(struct kvm_vcpu *vcpu)
>>  {
>> -	return pi_has_pending_interrupt(vcpu);
>> +	bool ret = pi_has_pending_interrupt(vcpu);
>> +	struct vcpu_tdx *tdx = to_tdx(vcpu);
>> +
>> +	if (ret || vcpu->arch.mp_state != KVM_MP_STATE_HALTED)
>> +		return true;
>> +
>> +	if (tdx->interrupt_disabled_hlt)
>> +		return false;
>> +
>> +	/*
>> +	 * This is for the case where the virtual interrupt is recognized,
>> +	 * i.e. set in vmcs.RVI, between the STI and "HLT".  KVM doesn't have
>> +	 * access to RVI and the interrupt is no longer in the PID (because it
>> +	 * was "recognized".  It doesn't get delivered in the guest because the
>> +	 * TDCALL completes before interrupts are enabled.
>> +	 *
>> +	 * TDX modules sets RVI while in an STI interrupt shadow.
>> +	 * - TDExit(typically TDG.VP.VMCALL<HLT>) from the guest to TDX module.
>> +	 *   The interrupt shadow at this point is gone.
>> +	 * - It knows that there is an interrupt that can be delivered
>> +	 *   (RVI > PPR && EFLAGS.IF=1, the other conditions of 29.2.2 don't
>> +	 *    matter)
>> +	 * - It forwards the TDExit nevertheless, to a clueless hypervisor that
>> +	 *   has no way to glean either RVI or PPR.
>
>WTF.  Seriously, what in the absolute hell is going on.  I reported this internally
>four ***YEARS*** ago.  This is not some obscure theoretical edge case, this is core
>functionality and it's completely broken garbage.
>
>NAK.  Hard NAK.  Fix the TDX module, full stop.
>
>Even worse, TDX 1.5 apparently _already_ has the necessary logic for dealing with
>interrupts that are pending in RVI when handling NESTED VM-Enter.  Really!?!?!
>Y'all went and added nested virtualization support of some kind, but can't find
>the time to get the basics right?

We actually fixed the TDX module. See 11.9.5. Pending Virtual Interrupt
Delivery Indication in TDX module 1.5 spec [1]

  The host VMM can detect whether a virtual interrupt is pending delivery to a
  VCPU in the Virtual APIC page, using TDH.VP.RD to read the VCPU_STATE_DETAILS
  TDVPS field.
  
  The typical use case is when the guest TD VCPU indicates to the host VMM, using
  TDG.VP.VMCALL, that it has no work to do and can be halted. The guest TD is
  expected to pass an “interrupt blocked” flag. The guest TD is expected to set
  this flag to 0 if and only if RFLAGS.IF is 1 or the TDCALL instruction that
  invokes TDG.VP.VMCALL immediately follows an STI instruction. If the “interrupt
  blocked” flag is 0, the host VMM can determine whether to re-schedule the guest
  TD VCPU based on VCPU_STATE_DETAILS.

Isaku, this patch didn't read VCPU_STATE_DETAILS. Maybe you missed something
during rebase? Regarding buggy_hlt_workaround, do you aim to avoid reading
VCPU_STATE_DETAILS as much as possible (because reading it via SEAMCALL is
costly, ~3-4K cycles)? if so, please make it clear in the changelog/comment.

[1]: https://cdrdv2.intel.com/v1/dl/getContent/733575

>
Sean Christopherson Jan. 9, 2024, 4:21 p.m. UTC | #3
On Mon, Jan 08, 2024, Chao Gao wrote:
> On Fri, Jan 05, 2024 at 03:05:12PM -0800, Sean Christopherson wrote:
> >On Tue, Nov 07, 2023, isaku.yamahata@intel.com wrote:
> >> From: Isaku Yamahata <isaku.yamahata@intel.com>
> >> 
> >> Wire up TDX PV HLT hypercall to the KVM backend function.
> >> 
> >> Signed-off-by: Isaku Yamahata <isaku.yamahata@intel.com>
> >> ---
> >>  arch/x86/kvm/vmx/tdx.c | 42 +++++++++++++++++++++++++++++++++++++++++-
> >>  arch/x86/kvm/vmx/tdx.h |  3 +++
> >>  2 files changed, 44 insertions(+), 1 deletion(-)
> >> 
> >> diff --git a/arch/x86/kvm/vmx/tdx.c b/arch/x86/kvm/vmx/tdx.c
> >> index 3a1fe74b95c3..4e48989d364f 100644
> >> --- a/arch/x86/kvm/vmx/tdx.c
> >> +++ b/arch/x86/kvm/vmx/tdx.c
> >> @@ -662,7 +662,32 @@ void tdx_vcpu_load(struct kvm_vcpu *vcpu, int cpu)
> >>  
> >>  bool tdx_protected_apic_has_interrupt(struct kvm_vcpu *vcpu)
> >>  {
> >> -	return pi_has_pending_interrupt(vcpu);
> >> +	bool ret = pi_has_pending_interrupt(vcpu);
> >> +	struct vcpu_tdx *tdx = to_tdx(vcpu);
> >> +
> >> +	if (ret || vcpu->arch.mp_state != KVM_MP_STATE_HALTED)
> >> +		return true;
> >> +
> >> +	if (tdx->interrupt_disabled_hlt)
> >> +		return false;
> >> +
> >> +	/*
> >> +	 * This is for the case where the virtual interrupt is recognized,
> >> +	 * i.e. set in vmcs.RVI, between the STI and "HLT".  KVM doesn't have
> >> +	 * access to RVI and the interrupt is no longer in the PID (because it
> >> +	 * was "recognized".  It doesn't get delivered in the guest because the
> >> +	 * TDCALL completes before interrupts are enabled.
> >> +	 *
> >> +	 * TDX modules sets RVI while in an STI interrupt shadow.
> >> +	 * - TDExit(typically TDG.VP.VMCALL<HLT>) from the guest to TDX module.
> >> +	 *   The interrupt shadow at this point is gone.
> >> +	 * - It knows that there is an interrupt that can be delivered
> >> +	 *   (RVI > PPR && EFLAGS.IF=1, the other conditions of 29.2.2 don't
> >> +	 *    matter)
> >> +	 * - It forwards the TDExit nevertheless, to a clueless hypervisor that
> >> +	 *   has no way to glean either RVI or PPR.
> >
> >WTF.  Seriously, what in the absolute hell is going on.  I reported this internally
> >four ***YEARS*** ago.  This is not some obscure theoretical edge case, this is core
> >functionality and it's completely broken garbage.
> >
> >NAK.  Hard NAK.  Fix the TDX module, full stop.
> >
> >Even worse, TDX 1.5 apparently _already_ has the necessary logic for dealing with
> >interrupts that are pending in RVI when handling NESTED VM-Enter.  Really!?!?!
> >Y'all went and added nested virtualization support of some kind, but can't find
> >the time to get the basics right?
> 
> We actually fixed the TDX module. See 11.9.5. Pending Virtual Interrupt
> Delivery Indication in TDX module 1.5 spec [1]
> 
>   The host VMM can detect whether a virtual interrupt is pending delivery to a
>   VCPU in the Virtual APIC page, using TDH.VP.RD to read the VCPU_STATE_DETAILS
>   TDVPS field.
>   
>   The typical use case is when the guest TD VCPU indicates to the host VMM, using
>   TDG.VP.VMCALL, that it has no work to do and can be halted. The guest TD is
>   expected to pass an “interrupt blocked” flag. The guest TD is expected to set
>   this flag to 0 if and only if RFLAGS.IF is 1 or the TDCALL instruction that
>   invokes TDG.VP.VMCALL immediately follows an STI instruction. If the “interrupt
>   blocked” flag is 0, the host VMM can determine whether to re-schedule the guest
>   TD VCPU based on VCPU_STATE_DETAILS.
> 
> Isaku, this patch didn't read VCPU_STATE_DETAILS. Maybe you missed something
> during rebase? Regarding buggy_hlt_workaround, do you aim to avoid reading
> VCPU_STATE_DETAILS as much as possible (because reading it via SEAMCALL is
> costly, ~3-4K cycles)? 

*sigh*  Why only earth doesn't the TDX module simply compute VMXIP on TDVMCALL?
It's literally one bit and one extra VMREAD.  There are plenty of register bits
available, and I highly doubt ~20 cycles in the TDVMCALL path will be noticeable,
let alone problematic.  Such functionality could even be added on top in a TDX
module update, and Intel could even bill it as a performance optimization.

Eating 4k cycles in the HLT path isn't the end of the world, but it's far from
optimal and it's just so incredibly wasteful.  I wouldn't be surprised if the
latency is measurable for certain workloads, which will lead to guests using
idle=poll and/or other games being played in the guest.

And AFAICT, the TDX module doesn't support HLT passthrough, so fully dedicated
CPUs can't even mitigate the pain that way.

Anyways, regarding the "workaround", my NAK stands.  It has bad tradeoffs of its
own, e.g. will result in spurious wakeups, and can't possibly work for VMs with
passthrough devices.  Not to mention that the implementation has several races
and false positives.
Isaku Yamahata Jan. 9, 2024, 5:36 p.m. UTC | #4
On Tue, Jan 09, 2024 at 08:21:13AM -0800,
Sean Christopherson <seanjc@google.com> wrote:

> On Mon, Jan 08, 2024, Chao Gao wrote:
> > On Fri, Jan 05, 2024 at 03:05:12PM -0800, Sean Christopherson wrote:
> > >On Tue, Nov 07, 2023, isaku.yamahata@intel.com wrote:
> > >> From: Isaku Yamahata <isaku.yamahata@intel.com>
> > >> 
> > >> Wire up TDX PV HLT hypercall to the KVM backend function.
> > >> 
> > >> Signed-off-by: Isaku Yamahata <isaku.yamahata@intel.com>
> > >> ---
> > >>  arch/x86/kvm/vmx/tdx.c | 42 +++++++++++++++++++++++++++++++++++++++++-
> > >>  arch/x86/kvm/vmx/tdx.h |  3 +++
> > >>  2 files changed, 44 insertions(+), 1 deletion(-)
> > >> 
> > >> diff --git a/arch/x86/kvm/vmx/tdx.c b/arch/x86/kvm/vmx/tdx.c
> > >> index 3a1fe74b95c3..4e48989d364f 100644
> > >> --- a/arch/x86/kvm/vmx/tdx.c
> > >> +++ b/arch/x86/kvm/vmx/tdx.c
> > >> @@ -662,7 +662,32 @@ void tdx_vcpu_load(struct kvm_vcpu *vcpu, int cpu)
> > >>  
> > >>  bool tdx_protected_apic_has_interrupt(struct kvm_vcpu *vcpu)
> > >>  {
> > >> -	return pi_has_pending_interrupt(vcpu);
> > >> +	bool ret = pi_has_pending_interrupt(vcpu);
> > >> +	struct vcpu_tdx *tdx = to_tdx(vcpu);
> > >> +
> > >> +	if (ret || vcpu->arch.mp_state != KVM_MP_STATE_HALTED)
> > >> +		return true;
> > >> +
> > >> +	if (tdx->interrupt_disabled_hlt)
> > >> +		return false;
> > >> +
> > >> +	/*
> > >> +	 * This is for the case where the virtual interrupt is recognized,
> > >> +	 * i.e. set in vmcs.RVI, between the STI and "HLT".  KVM doesn't have
> > >> +	 * access to RVI and the interrupt is no longer in the PID (because it
> > >> +	 * was "recognized".  It doesn't get delivered in the guest because the
> > >> +	 * TDCALL completes before interrupts are enabled.
> > >> +	 *
> > >> +	 * TDX modules sets RVI while in an STI interrupt shadow.
> > >> +	 * - TDExit(typically TDG.VP.VMCALL<HLT>) from the guest to TDX module.
> > >> +	 *   The interrupt shadow at this point is gone.
> > >> +	 * - It knows that there is an interrupt that can be delivered
> > >> +	 *   (RVI > PPR && EFLAGS.IF=1, the other conditions of 29.2.2 don't
> > >> +	 *    matter)
> > >> +	 * - It forwards the TDExit nevertheless, to a clueless hypervisor that
> > >> +	 *   has no way to glean either RVI or PPR.
> > >
> > >WTF.  Seriously, what in the absolute hell is going on.  I reported this internally
> > >four ***YEARS*** ago.  This is not some obscure theoretical edge case, this is core
> > >functionality and it's completely broken garbage.
> > >
> > >NAK.  Hard NAK.  Fix the TDX module, full stop.
> > >
> > >Even worse, TDX 1.5 apparently _already_ has the necessary logic for dealing with
> > >interrupts that are pending in RVI when handling NESTED VM-Enter.  Really!?!?!
> > >Y'all went and added nested virtualization support of some kind, but can't find
> > >the time to get the basics right?
> > 
> > We actually fixed the TDX module. See 11.9.5. Pending Virtual Interrupt
> > Delivery Indication in TDX module 1.5 spec [1]
> > 
> >   The host VMM can detect whether a virtual interrupt is pending delivery to a
> >   VCPU in the Virtual APIC page, using TDH.VP.RD to read the VCPU_STATE_DETAILS
> >   TDVPS field.
> >   
> >   The typical use case is when the guest TD VCPU indicates to the host VMM, using
> >   TDG.VP.VMCALL, that it has no work to do and can be halted. The guest TD is
> >   expected to pass an “interrupt blocked” flag. The guest TD is expected to set
> >   this flag to 0 if and only if RFLAGS.IF is 1 or the TDCALL instruction that
> >   invokes TDG.VP.VMCALL immediately follows an STI instruction. If the “interrupt
> >   blocked” flag is 0, the host VMM can determine whether to re-schedule the guest
> >   TD VCPU based on VCPU_STATE_DETAILS.
> > 
> > Isaku, this patch didn't read VCPU_STATE_DETAILS. Maybe you missed something
> > during rebase? Regarding buggy_hlt_workaround, do you aim to avoid reading
> > VCPU_STATE_DETAILS as much as possible (because reading it via SEAMCALL is
> > costly, ~3-4K cycles)? 
> 
> *sigh*  Why only earth doesn't the TDX module simply compute VMXIP on TDVMCALL?
> It's literally one bit and one extra VMREAD.  There are plenty of register bits
> available, and I highly doubt ~20 cycles in the TDVMCALL path will be noticeable,
> let alone problematic.  Such functionality could even be added on top in a TDX
> module update, and Intel could even bill it as a performance optimization.
> 
> Eating 4k cycles in the HLT path isn't the end of the world, but it's far from
> optimal and it's just so incredibly wasteful.  I wouldn't be surprised if the
> latency is measurable for certain workloads, which will lead to guests using
> idle=poll and/or other games being played in the guest.
> 
> And AFAICT, the TDX module doesn't support HLT passthrough, so fully dedicated
> CPUs can't even mitigate the pain that way.
> 
> Anyways, regarding the "workaround", my NAK stands.  It has bad tradeoffs of its
> own, e.g. will result in spurious wakeups, and can't possibly work for VMs with
> passthrough devices.  Not to mention that the implementation has several races
> and false positives.

I'll drop the last part and use TDH.VP.RD(VCPU_STATE_DETAILS) with TDX 1.5.
diff mbox series

Patch

diff --git a/arch/x86/kvm/vmx/tdx.c b/arch/x86/kvm/vmx/tdx.c
index 3a1fe74b95c3..4e48989d364f 100644
--- a/arch/x86/kvm/vmx/tdx.c
+++ b/arch/x86/kvm/vmx/tdx.c
@@ -662,7 +662,32 @@  void tdx_vcpu_load(struct kvm_vcpu *vcpu, int cpu)
 
 bool tdx_protected_apic_has_interrupt(struct kvm_vcpu *vcpu)
 {
-	return pi_has_pending_interrupt(vcpu);
+	bool ret = pi_has_pending_interrupt(vcpu);
+	struct vcpu_tdx *tdx = to_tdx(vcpu);
+
+	if (ret || vcpu->arch.mp_state != KVM_MP_STATE_HALTED)
+		return true;
+
+	if (tdx->interrupt_disabled_hlt)
+		return false;
+
+	/*
+	 * This is for the case where the virtual interrupt is recognized,
+	 * i.e. set in vmcs.RVI, between the STI and "HLT".  KVM doesn't have
+	 * access to RVI and the interrupt is no longer in the PID (because it
+	 * was "recognized".  It doesn't get delivered in the guest because the
+	 * TDCALL completes before interrupts are enabled.
+	 *
+	 * TDX modules sets RVI while in an STI interrupt shadow.
+	 * - TDExit(typically TDG.VP.VMCALL<HLT>) from the guest to TDX module.
+	 *   The interrupt shadow at this point is gone.
+	 * - It knows that there is an interrupt that can be delivered
+	 *   (RVI > PPR && EFLAGS.IF=1, the other conditions of 29.2.2 don't
+	 *    matter)
+	 * - It forwards the TDExit nevertheless, to a clueless hypervisor that
+	 *   has no way to glean either RVI or PPR.
+	 */
+	return !!xchg(&tdx->buggy_hlt_workaround, 0);
 }
 
 void tdx_prepare_switch_to_guest(struct kvm_vcpu *vcpu)
@@ -1104,6 +1129,17 @@  static int tdx_emulate_cpuid(struct kvm_vcpu *vcpu)
 	return 1;
 }
 
+static int tdx_emulate_hlt(struct kvm_vcpu *vcpu)
+{
+	struct vcpu_tdx *tdx = to_tdx(vcpu);
+
+	/* See tdx_protected_apic_has_interrupt() to avoid heavy seamcall */
+	tdx->interrupt_disabled_hlt = tdvmcall_a0_read(vcpu);
+
+	tdvmcall_set_return_code(vcpu, TDG_VP_VMCALL_SUCCESS);
+	return kvm_emulate_halt_noskip(vcpu);
+}
+
 static int handle_tdvmcall(struct kvm_vcpu *vcpu)
 {
 	if (tdvmcall_exit_type(vcpu))
@@ -1112,6 +1148,8 @@  static int handle_tdvmcall(struct kvm_vcpu *vcpu)
 	switch (tdvmcall_leaf(vcpu)) {
 	case EXIT_REASON_CPUID:
 		return tdx_emulate_cpuid(vcpu);
+	case EXIT_REASON_HLT:
+		return tdx_emulate_hlt(vcpu);
 	default:
 		break;
 	}
@@ -1486,6 +1524,8 @@  void tdx_deliver_interrupt(struct kvm_lapic *apic, int delivery_mode,
 	struct kvm_vcpu *vcpu = apic->vcpu;
 	struct vcpu_tdx *tdx = to_tdx(vcpu);
 
+	/* See comment in tdx_protected_apic_has_interrupt(). */
+	tdx->buggy_hlt_workaround = 1;
 	/* TDX supports only posted interrupt.  No lapic emulation. */
 	__vmx_deliver_posted_interrupt(vcpu, &tdx->pi_desc, vector);
 }
diff --git a/arch/x86/kvm/vmx/tdx.h b/arch/x86/kvm/vmx/tdx.h
index 870a0d15e073..4ddcf804c0a4 100644
--- a/arch/x86/kvm/vmx/tdx.h
+++ b/arch/x86/kvm/vmx/tdx.h
@@ -101,6 +101,9 @@  struct vcpu_tdx {
 	bool host_state_need_restore;
 	u64 msr_host_kernel_gs_base;
 
+	bool interrupt_disabled_hlt;
+	unsigned int buggy_hlt_workaround;
+
 	/*
 	 * Dummy to make pmu_intel not corrupt memory.
 	 * TODO: Support PMU for TDX.  Future work.