KVM/arm64: timer fixes for 6.14
Subsystems (3)
Marc Zyngier
maintainercover KVM/arm64: timer fixes for 6.14
It's been recently pointed out that the NV timer code that is (hopefully) on its way upstream is marginally sub-optimal (read:
terminally broken). This short series attempts to fix things:
- Correctly arm a background timer in all situations
- Avoid corrupting the EL2 state by shoving the EL1 state into it, and simplify the handling of emulated timers
- Make the NV init of the timers less awkward and expensive
Thanks to Volodymyr Babchuk and Wei-Lin Chang for their help.
Patches on top of kvmarm-6.14, and merged back on top of my kvm-arm64/nv-next branch.
Marc Zyngier (3):
KVM: arm64: timer: Always evaluate the need for a soft timer
KVM: arm64: timer: Correctly handle EL1 timer emulation when !FEAT_ECV
KVM: arm64: timer: Consolidate NV configuration of virtual timers
arch/arm64/kvm/arch_timer.c | 82 ++++++++++++++---------------------- arch/arm64/kvm/arm.c | 3 ++ include/kvm/arm_arch_timer.h | 1 + 3 files changed, 36 insertions(+), 50 deletions(-)
Signature · 1 line
2.39.2
Marc Zyngier
maintainerPATCH 1/3 KVM: arm64: timer: Always evaluate the need for a soft timer
When updating the interrupt state for an emulated timer, we return early and skip the setup of a soft timer that runs in parallel with the guest.
While this is OK if we have set the interrupt pending, it is pretty wrong if the guest moved CVAL into the future. In that case, no timer is armed and the guest can wait for a very long time (it will take a full put/load cycle for the situation to resolve).
This is specially visible with EDK2 running at EL2, but still using the EL1 virtual timer, which in that case is fully emulated. Any key-press takes ages to be captured, as there is no UART interrupt and EDK2 relies on polling from a timer...
The fix is simply to drop the early return. If the timer interrupt is pending, we will still return early, and otherwise arm the soft timer.
--- arch/arm64/kvm/arch_timer.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-)
arch/arm64/kvm/arch_timer.c +1 -3
@@ -471,10 +471,8 @@ static void timer_emulate(struct arch_timer_context *ctx)trace_kvm_timer_emulate (ctx, should_fire); -if (should_fire != ctx->irq.level) { +if (should_fire != ctx->irq.level)kvm_timer_update_irq (ctx->vcpu, should_fire, ctx); -return ; - }kvm_timer_update_status (ctx, should_fire);
Signature · 1 line
2.39.2
Re: [PATCH 1/3] KVM: arm64: timer: Always evaluate the need for a soft timer
Hello, Marc.
Quoted 14 lines · Marc Zyngier
When updating the interrupt state for an emulated timer, we return early and skip the setup of a soft timer that runs in parallel with the guest.
While this is OK if we have set the interrupt pending, it is pretty wrong if the guest moved CVAL into the future. In that case, no timer is armed and the guest can wait for a very long time (it will take a full put/load cycle for the situation to resolve).
This is specially visible with EDK2 running at EL2, but still using the EL1 virtual timer, which in that case is fully emulated. Any key-press takes ages to be captured, as there is no UART interrupt and EDK2 relies on polling from a timer...
Quoted 7 lines
The fix is simply to drop the early return. If the timer interrupt is pending, we will still return early, and otherwise arm the soft timer.
Fixes 4d74ecfa6458b ("KVM: arm64: Don't arm a hrtimer for an already pending timer")Signed-off-by Marc Zyngier <[email protected]>
Quoted 20 lines
--- arch/arm64/kvm/arch_timer.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-)arch/arm64/kvm/arch_timer.c +1
-3@@ -471,10 +471,8 @@ static void timer_emulate(struct arch_timer_context *ctx)trace_kvm_timer_emulate (ctx, should_fire); -if (should_fire != ctx->irq.level) { +if (should_fire != ctx->irq.level)kvm_timer_update_irq (ctx->vcpu, should_fire, ctx); -return ; - }kvm_timer_update_status (ctx, should_fire);
Marc Zyngier
maintainerPATCH 2/3 KVM: arm64: timer: Correctly handle EL1 timer emulation when !FEAT_ECV
In reply to Marc ZyngierBoth Wei-Lin Chang and Volodymyr Babchuk report that the way we handle the emulation of EL1 timers with NV is completely wrong, specially in the case of HCR_EL2.E2H==0.
There are three problems in about as many lines of code:
- With E2H==0, the EL1 timers are overwritten with the EL1 state, while they should actually contain the EL2 state (as per the timer map)
- With E2H==1, we run the full EL1 timer emulation even when ECV is present, hiding a bug in timer_emulate() (see previous patch)
- The comments are actively misleading, and say all the wrong things.
This is only attributable to the code having been initially written for FEAT_NV, hacked up to handle FEAT_NV2 *in parallel*, and vaguely hacked again to be FEAT_NV2 only. Oh, and yours truly being a gold plated idiot.
The fix is obvious: just delete most of the E2H==0 code, have a unified handling of the timers (because they really are E2H agnostic), and make sure we don't execute any of that when FEAT_ECV is present.
--- arch/arm64/kvm/arch_timer.c | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-)
arch/arm64/kvm/arch_timer.c +10 -20
@@ -974,31 +974,21 @@ void kvm_timer_sync_nested(struct kvm_vcpu *vcpu)* which allows trapping of the timer registers even with NV2. * Still, this is still worse than FEAT_NV on its own. Meh. */ -if (!vcpu_el2_e2h_is_set (vcpu)) { -if (cpus_have_final_cap (ARM64_HAS_ECV )) -return ; - -/* -* A non-VHE guest hypervisor doesn't have any direct access -* to its timers: the EL2 registers trap (and the HW is -* fully emulated), while the EL0 registers access memory -* despite the access being notionally direct. Boo. -* -* We update the hardware timer registers with the -* latest value written by the guest to the VNCR page -* and let the hardware take care of the rest. -*/ -write_sysreg_el0 (__vcpu_sys_reg (vcpu,CNTV_CTL_EL0 ),SYS_CNTV_CTL ); -write_sysreg_el0 (__vcpu_sys_reg (vcpu,CNTV_CVAL_EL0 ),SYS_CNTV_CVAL ); -write_sysreg_el0 (__vcpu_sys_reg (vcpu,CNTP_CTL_EL0 ),SYS_CNTP_CTL ); -write_sysreg_el0 (__vcpu_sys_reg (vcpu,CNTP_CVAL_EL0 ),SYS_CNTP_CVAL ); - }else { +if (!cpus_have_final_cap (ARM64_HAS_ECV )) {/* * For a VHE guest hypervisor, the EL2 state is directly -* stored in the host EL1 timers, while the emulated EL0 +* stored in the host EL1 timers, while the emulated EL1 * state is stored in the VNCR page. The latter could have * been updated behind our back, and we must reset the * emulation of the timers. +* +* A non-VHE guest hypervisor doesn't have any direct access +* to its timers: the EL2 registers trap despite being +* notionally direct (we use the EL1 HW, as for VHE), while +* the EL1 registers access memory. +* +* In both cases, process the emulated timers on each guest +* exit. Boo. */ struct timer_map map;get_timer_map (vcpu, &map);
Signature · 1 line
2.39.2
Re: [PATCH 2/3] KVM: arm64: timer: Correctly handle EL1 timer emulation when !FEAT_ECV
Hello, Marc.
Quoted 24 lines · Marc Zyngier
Both Wei-Lin Chang and Volodymyr Babchuk report that the way we handle the emulation of EL1 timers with NV is completely wrong, specially in the case of HCR_EL2.E2H==0.
There are three problems in about as many lines of code:
- With E2H==0, the EL1 timers are overwritten with the EL1 state, while they should actually contain the EL2 state (as per the timer map)
- With E2H==1, we run the full EL1 timer emulation even when ECV is present, hiding a bug in
timer_emulate()(see previous patch)- The comments are actively misleading, and say all the wrong things.
This is only attributable to the code having been initially written for
FEAT_NV, hacked up to handleFEAT_NV2*in parallel*, and vaguely hacked again to beFEAT_NV2only. Oh, and yours truly being a gold plated idiot.The fix is obvious: just delete most of the E2H==0 code, have a unified handling of the timers (because they really are E2H agnostic), and make sure we don't execute any of that when
FEAT_ECVis present.
Quoted 6 lines
Fixes 4bad3068cfa9f ("KVM: arm64: nv: Sync nested timer state with FEAT_NV2")Reported-by Wei-Lin Chang <[email protected]>Reported-by Volodymyr Babchuk <[email protected]>Signed-off-by Marc Zyngier <[email protected]>
Quoted 50 lines
--- arch/arm64/kvm/arch_timer.c | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-)arch/arm64/kvm/arch_timer.c +10
-20@@ -974,31 +974,21 @@ void kvm_timer_sync_nested(struct kvm_vcpu *vcpu)* which allows trapping of the timer registers even with NV2. * Still, this is still worse than FEAT_NV on its own. Meh. */ -if (!vcpu_el2_e2h_is_set (vcpu)) { -if (cpus_have_final_cap (ARM64_HAS_ECV )) -return ; - -/* -* A non-VHE guest hypervisor doesn't have any direct access -* to its timers: the EL2 registers trap (and the HW is -* fully emulated), while the EL0 registers access memory -* despite the access being notionally direct. Boo. -* -* We update the hardware timer registers with the -* latest value written by the guest to the VNCR page -* and let the hardware take care of the rest. -*/ -write_sysreg_el0 (__vcpu_sys_reg (vcpu,CNTV_CTL_EL0 ),SYS_CNTV_CTL ); -write_sysreg_el0 (__vcpu_sys_reg (vcpu,CNTV_CVAL_EL0 ),SYS_CNTV_CVAL ); -write_sysreg_el0 (__vcpu_sys_reg (vcpu,CNTP_CTL_EL0 ),SYS_CNTP_CTL ); -write_sysreg_el0 (__vcpu_sys_reg (vcpu,CNTP_CVAL_EL0 ),SYS_CNTP_CVAL ); - }else { +if (!cpus_have_final_cap (ARM64_HAS_ECV )) {/* * For a VHE guest hypervisor, the EL2 state is directly -* stored in the host EL1 timers, while the emulated EL0 +* stored in the host EL1 timers, while the emulated EL1 * state is stored in the VNCR page. The latter could have * been updated behind our back, and we must reset the * emulation of the timers. +* +* A non-VHE guest hypervisor doesn't have any direct access +* to its timers: the EL2 registers trap despite being +* notionally direct (we use the EL1 HW, as for VHE), while +* the EL1 registers access memory. +* +* In both cases, process the emulated timers on each guest +* exit. Boo. */ struct timer_map map;get_timer_map (vcpu, &map);
Marc Zyngier
maintainerPATCH 3/3 KVM: arm64: timer: Consolidate NV configuration of virtual timers
In reply to Marc ZyngierThe way we configure the virtual timers with NV is rather odd:
- the EL1 virtual timer gets setup in kvm_timer_vcpu_reset(). Why not?
- the EL2 virtual timer gets setup at vcpu_load time, which is really bizarre, because this really should be a one-off.
The reason for the second point is that this setup is conditionned on HCR_EL2.E2H, as it decides whether CNTVOFF_EL2 applies to the EL2 virtual counter or not. And of course, this is not known at the point where we reset the timer. Huh.
Solve this by introducing a NV-specific init for the timers, matching what we do for the other subsystems, that gets called once we know for sure that the configuration is final (on first vcpu run, effectively).
This makes kvm_timer_vcpu_load_nested_switch() slightly simpler.
--- arch/arm64/kvm/arch_timer.c | 48 ++++++++++++++++-------------------- arch/arm64/kvm/arm.c | 3 +++ include/kvm/arm_arch_timer.h | 1 + 3 files changed, 25 insertions(+), 27 deletions(-)
arch/arm64/kvm/arch_timer.c +21 -27
@@ -759,21 +759,6 @@ static void kvm_timer_vcpu_load_nested_switch(struct kvm_vcpu *vcpu,timer_irq (map->direct_ptimer), &arch_timer_irq_ops);WARN_ON_ONCE (ret); - -/* -* The virtual offset behaviour is "interesting", as it -* always applies when HCR_EL2.E2H==0, but only when -* accessed from EL1 when HCR_EL2.E2H==1. So make sure we -* track E2H when putting the HV timer in "direct" mode. -*/ -if (map->direct_vtimer ==vcpu_hvtimer (vcpu)) { -struct arch_timer_offset *offs = &map->direct_vtimer->offset; - -if (vcpu_el2_e2h_is_set (vcpu)) - offs->vcpu_offset =NULL ; -else - offs->vcpu_offset = &__vcpu_sys_reg (vcpu,CNTVOFF_EL2 ); - } } }
@@ -1045,18 +1030,6 @@ void kvm_timer_vcpu_reset(struct kvm_vcpu *vcpu)for (int i =0 ; i <nr_timers (vcpu); i++)timer_set_ctl (vcpu_get_timer (vcpu, i),0 ); -/* -* A vcpu running at EL2 is in charge of the offset applied to -* the virtual timer, so use the physical VM offset, and point -* the vcpu offset to CNTVOFF_EL2. -*/ -if (vcpu_has_nv (vcpu)) { -struct arch_timer_offset *offs = &vcpu_vtimer (vcpu)->offset; - - offs->vcpu_offset = &__vcpu_sys_reg (vcpu,CNTVOFF_EL2 ); - offs->vm_offset = &vcpu->kvm->arch.timer_data.poffset; - } -if (timer->enabled) {for (int i =0 ; i <nr_timers (vcpu); i++) kvm_timer_update_irq(vcpu, false,
@@ -1102,6 +1075,27 @@ static void timer_context_init(struct kvm_vcpu *vcpu, int timerid) } } +void kvm_timer_vcpu_nv_init (struct kvm_vcpu *vcpu) +{ +/* +* A vcpu running at EL2 is in charge of the offset applied to +* the virtual timer, so use the physical VM offset, and point +* the vcpu offset to CNTVOFF_EL2. +* +* The virtual offset behaviour is "interesting", as it always +* applies when HCR_EL2.E2H==0, but only when accessed from EL1 when +* HCR_EL2.E2H==1. Apply it to the HV timer when E2H==0. +*/ +struct arch_timer_offset *offs = &vcpu_vtimer (vcpu)->offset; +u64 *voff =__ctxt_sys_reg (&vcpu->arch.ctxt,CNTVOFF_EL2 ); + + offs->vcpu_offset = voff; + offs->vm_offset = &vcpu->kvm->arch.timer_data.poffset; + +if (!vcpu_el2_e2h_is_set (vcpu)) +vcpu_hvtimer (vcpu)->offset.vcpu_offset = voff; +} +void kvm_timer_vcpu_init (struct kvm_vcpu *vcpu) {struct arch_timer_cpu *timer =vcpu_timer (vcpu);
arch/arm64/kvm/arm.c +3 -0
@@ -815,6 +815,9 @@ int kvm_arch_vcpu_run_pid_change(struct kvm_vcpu *vcpu)if (ret)return ret; +if (vcpu_has_nv (vcpu)) +kvm_timer_vcpu_nv_init (vcpu); +/* * This needs to happen after any restriction has been applied * to the feature set.
include/kvm/arm_arch_timer.h +1 -0
@@ -98,6 +98,7 @@ int __init kvm_timer_hyp_init(bool has_gic);int kvm_timer_enable (struct kvm_vcpu *vcpu);void kvm_timer_vcpu_reset (struct kvm_vcpu *vcpu);void kvm_timer_vcpu_init (struct kvm_vcpu *vcpu); +void kvm_timer_vcpu_nv_init (struct kvm_vcpu *vcpu);void kvm_timer_sync_nested (struct kvm_vcpu *vcpu);void kvm_timer_sync_user (struct kvm_vcpu *vcpu);bool kvm_timer_should_notify_user (struct kvm_vcpu *vcpu);
Signature · 1 line
2.39.2
Re: [PATCH 3/3] KVM: arm64: timer: Consolidate NV configuration of virtual timers
arch/arm64/kvm/arch_timer.c static void timer_context_init(struct kvm_vcpu *vcpu, int timerid) · line 1088
Quoted 11 lines
arch/arm64/kvm/arch_timer.c +11
-0+void kvm_timer_vcpu_nv_init (struct kvm_vcpu *vcpu) +{ +/* +* A vcpu running at EL2 is in charge of the offset applied to +* the virtual timer, so use the physical VM offset, and point +* the vcpu offset to CNTVOFF_EL2. +* +* The virtual offset behaviour is "interesting", as it always +* applies when HCR_EL2.E2H==0, but only when accessed from EL1 when +* HCR_EL2.E2H==1. Apply it to the HV timer when E2H==0. +*/
I'm definitely being pedantic, but all the talk of an HV timer when E2H==0 isn't sitting well with me. Since a programmable E2H has gone out the window there isn't such thing as an HV timer when E2H==0, as FEAT_VHE isn't implemented for the VM.
And along those lines, accesses to CNTHV_*_EL2 registers should undef when FEAT_VHE isn't implemented for the VM but I don't think we have any enforcement of that.
Signature · 2 lines
Thanks, Oliver
Marc Zyngier
maintainerRe: [PATCH 3/3] KVM: arm64: timer: Consolidate NV configuration of virtual timers
Quoted 17 lines · Oliver Upton
arch/arm64/kvm/arch_timer.c static void timer_context_init(struct kvm_vcpu *vcpu, int timerid) · line 1088
Quoted 12 lines
arch/arm64/kvm/arch_timer.c +11
-0+void kvm_timer_vcpu_nv_init (struct kvm_vcpu *vcpu) +{ +/* +* A vcpu running at EL2 is in charge of the offset applied to +* the virtual timer, so use the physical VM offset, and point +* the vcpu offset to CNTVOFF_EL2. +* +* The virtual offset behaviour is "interesting", as it always +* applies when HCR_EL2.E2H==0, but only when accessed from EL1 when +* HCR_EL2.E2H==1. Apply it to the HV timer when E2H==0. +*/ I'm definitely being pedantic, but all the talk of an HV timer when E2H==0 isn't sitting well with me. Since a programmable E2H has gone out the window there isn't such thing as an HV timer when E2H==0, as
FEAT_VHEisn't implemented for the VM.
Ah, that's a very good point!
Quoted 3 lines
And along those lines, accesses to CNTHV_*_EL2 registers should undef when
FEAT_VHEisn't implemented for the VM but I don't think we have any enforcement of that.
Indeed. Let me fix this and add the required UNDEF behaviour.
Thanks,
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Re: [PATCH 3/3] KVM: arm64: timer: Consolidate NV configuration of virtual timers
In reply to Marc ZyngierHello, Marc.
Quoted 12 lines · Marc Zyngier
The way we configure the virtual timers with NV is rather odd:
- the EL1 virtual timer gets setup in
kvm_timer_vcpu_reset(). Why not?- the EL2 virtual timer gets setup at
vcpu_loadtime, which is really bizarre, because this really should be a one-off.The reason for the second point is that this setup is conditionned on HCR_EL2.E2H, as it decides whether
CNTVOFF_EL2applies to the EL2 virtual counter or not. And of course, this is not known at the point where we reset the timer. Huh.
Quoted 8 lines
Solve this by introducing a NV-specific init for the timers, matching what we do for the other subsystems, that gets called once we know for sure that the configuration is final (on first vcpu run, effectively).
This makes
kvm_timer_vcpu_load_nested_switch()slightly simpler.Signed-off-by Marc Zyngier <[email protected]>
Quoted 105 lines
--- arch/arm64/kvm/arch_timer.c | 48 ++++++++++++++++-------------------- arch/arm64/kvm/arm.c | 3 +++ include/kvm/arm_arch_timer.h | 1 + 3 files changed, 25 insertions(+), 27 deletions(-)arch/arm64/kvm/arch_timer.c +21
-27@@ -759,21 +759,6 @@ static void kvm_timer_vcpu_load_nested_switch(struct kvm_vcpu *vcpu,timer_irq (map->direct_ptimer), &arch_timer_irq_ops);WARN_ON_ONCE (ret); - -/* -* The virtual offset behaviour is "interesting", as it -* always applies when HCR_EL2.E2H==0, but only when -* accessed from EL1 when HCR_EL2.E2H==1. So make sure we -* track E2H when putting the HV timer in "direct" mode. -*/ -if (map->direct_vtimer ==vcpu_hvtimer (vcpu)) { -struct arch_timer_offset *offs = &map->direct_vtimer->offset; - -if (vcpu_el2_e2h_is_set (vcpu)) - offs->vcpu_offset =NULL ; -else - offs->vcpu_offset = &__vcpu_sys_reg (vcpu,CNTVOFF_EL2 ); - } } }@@ -1045,18 +1030,6 @@ void kvm_timer_vcpu_reset(struct kvm_vcpu *vcpu)for (int i =0 ; i <nr_timers (vcpu); i++)timer_set_ctl (vcpu_get_timer (vcpu, i),0 ); -/* -* A vcpu running at EL2 is in charge of the offset applied to -* the virtual timer, so use the physical VM offset, and point -* the vcpu offset to CNTVOFF_EL2. -*/ -if (vcpu_has_nv (vcpu)) { -struct arch_timer_offset *offs = &vcpu_vtimer (vcpu)->offset; - - offs->vcpu_offset = &__vcpu_sys_reg (vcpu,CNTVOFF_EL2 ); - offs->vm_offset = &vcpu->kvm->arch.timer_data.poffset; - } -if (timer->enabled) {for (int i =0 ; i <nr_timers (vcpu); i++) kvm_timer_update_irq(vcpu, false,@@ -1102,6 +1075,27 @@ static void timer_context_init(struct kvm_vcpu *vcpu, int timerid) } } +void kvm_timer_vcpu_nv_init (struct kvm_vcpu *vcpu) +{ +/* +* A vcpu running at EL2 is in charge of the offset applied to +* the virtual timer, so use the physical VM offset, and point +* the vcpu offset to CNTVOFF_EL2. +* +* The virtual offset behaviour is "interesting", as it always +* applies when HCR_EL2.E2H==0, but only when accessed from EL1 when +* HCR_EL2.E2H==1. Apply it to the HV timer when E2H==0. +*/ +struct arch_timer_offset *offs = &vcpu_vtimer (vcpu)->offset; +u64 *voff =__ctxt_sys_reg (&vcpu->arch.ctxt,CNTVOFF_EL2 ); + + offs->vcpu_offset = voff; + offs->vm_offset = &vcpu->kvm->arch.timer_data.poffset; + +if (!vcpu_el2_e2h_is_set (vcpu)) +vcpu_hvtimer (vcpu)->offset.vcpu_offset = voff; +} +void kvm_timer_vcpu_init (struct kvm_vcpu *vcpu) {struct arch_timer_cpu *timer =vcpu_timer (vcpu);arch/arm64/kvm/arm.c +3
-0@@ -815,6 +815,9 @@ int kvm_arch_vcpu_run_pid_change(struct kvm_vcpu *vcpu)if (ret)return ret; +if (vcpu_has_nv (vcpu)) +kvm_timer_vcpu_nv_init (vcpu); +/* * This needs to happen after any restriction has been applied * to the feature set. include/kvm/arm_arch_timer.h +1
-0@@ -98,6 +98,7 @@ int __init kvm_timer_hyp_init(bool has_gic);int kvm_timer_enable (struct kvm_vcpu *vcpu);void kvm_timer_vcpu_reset (struct kvm_vcpu *vcpu);void kvm_timer_vcpu_init (struct kvm_vcpu *vcpu); +void kvm_timer_vcpu_nv_init (struct kvm_vcpu *vcpu);void kvm_timer_sync_nested (struct kvm_vcpu *vcpu);void kvm_timer_sync_user (struct kvm_vcpu *vcpu);bool kvm_timer_should_notify_user (struct kvm_vcpu *vcpu);