* [PATCH 1/3] KVM: arm64: timers: Compute an offset-applied CVAL from the current count
2026-09-21 14:04 [PATCH 0/3] KVM: arm64: timers: Program CVAL from the current count when the hardware won't apply the offset Fuad Tabba
@ 2026-09-21 14:04 ` Fuad Tabba
2026-09-21 14:04 ` [PATCH 2/3] KVM: arm64: nv: Read a guest hypervisor's CNTV_CVAL_EL0 from memory on x1e Fuad Tabba
2026-09-21 14:04 ` [PATCH 3/3] KVM: arm64: selftests: Test a timer set past the counter's wrap Fuad Tabba
2 siblings, 0 replies; 4+ messages in thread
From: Fuad Tabba @ 2026-09-21 14:04 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Joey Gouly, Suzuki K Poulose, Zenghui Yu, Zenghui Yu,
Ganapatrao Kulkarni, Will Deacon, Fuad Tabba, kvmarm, kvm,
linux-arm-kernel, linux-kernel, linux-kselftest
When the hardware won't apply a timer's offset, KVM programs CVAL +
offset and lets it compare that against the raw counter. The compare is
unsigned (IsTimerConditionMet, DDI0487M_c J1.4.3.29):
condition_met = (UInt(PhysicalCountInt() - offset) -
UInt(compare_value)) >= 0;
Adding the offset to both sides preserves it only when both sums wrap
or neither does: a guest whose counter is ahead of the host's by more
than CVAL has an expired timer that CVAL + offset puts in the far
future, and one behind it has a far-future timer that CVAL + offset
wraps into the past.
On a CNTPOFF_EL2 host the first case livelocks: __activate_traps()
reloads the guest's CVAL and the timer fires, __deactivate_traps()
rewrites it as CVAL + offset on the way out and the line drops before
the host can take the interrupt, and the vCPU only makes progress once
an unrelated vcpu_put() and vcpu_load() inject the interrupt from the
memory copy. arch_timer_edge_cases stops in its physical past-timer
cases. Without CNTPOFF_EL2 the same cases hang until the vCPU is next
loaded, which is what Zenghui reported on a Kunpeng920.
Derive the programmed value from the current count instead: CVAL +
offset while the deadline is ahead, 0 for a timer that has already
expired for the guest, ~0 for one past the counter's wrap. That value
can't be inverted, so timer_save_state() keeps the memory copy whenever
an offset is in play; it's current there, written by the trap handler
when the accesses are trapped and by __deactivate_traps() on the
CNTPOFF_EL2 path. kvm_hyp_handle_timer() subtracted the offset back out
for a guest hypervisor's read of its own physical CVAL, so return the
memory copy there too.
Fixes: c605ee245097 ("KVM: arm64: timers: Allow physical offset without CNTPOFF_EL2")
Fixes: 9404673293b0 ("KVM: arm64: timers: Correctly handle TGE flip with CNTPOFF_EL2")
Fixes: 0bc9a9e85fcf ("KVM: arm64: Work around x1e's CNTVOFF_EL2 bogosity")
Reported-by: Zenghui Yu <yuzenghui@huawei.com>
Closes: https://lore.kernel.org/r/460258be-0102-e922-c342-4e87cd94b9e5@huawei.com
Cc: stable@vger.kernel.org
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/arch_timer.c | 27 ++++++++++++---------------
arch/arm64/kvm/hyp/vhe/switch.c | 11 ++++++-----
include/kvm/arm_arch_timer.h | 15 +++++++++++++++
3 files changed, 33 insertions(+), 20 deletions(-)
diff --git a/arch/arm64/kvm/arch_timer.c b/arch/arm64/kvm/arch_timer.c
index 6ac3321f4c575..9278b5383c044 100644
--- a/arch/arm64/kvm/arch_timer.c
+++ b/arch/arm64/kvm/arch_timer.c
@@ -528,17 +528,11 @@ static void timer_save_state(struct arch_timer_context *ctx)
goto out;
switch (index) {
- u64 cval;
-
case TIMER_VTIMER:
case TIMER_HVTIMER:
timer_set_ctl(ctx, read_sysreg_el0(SYS_CNTV_CTL));
- cval = read_sysreg_el0(SYS_CNTV_CVAL);
-
- if (has_broken_cntvoff())
- cval -= timer_get_offset(ctx);
-
- timer_set_cval(ctx, cval);
+ if (!has_broken_cntvoff() || !timer_get_offset(ctx))
+ timer_set_cval(ctx, read_sysreg_el0(SYS_CNTV_CVAL));
/* Disable the timer */
write_sysreg_el0(0, SYS_CNTV_CTL);
@@ -564,11 +558,12 @@ static void timer_save_state(struct arch_timer_context *ctx)
case TIMER_PTIMER:
case TIMER_HPTIMER:
timer_set_ctl(ctx, read_sysreg_el0(SYS_CNTP_CTL));
- cval = read_sysreg_el0(SYS_CNTP_CVAL);
-
- cval -= timer_get_offset(ctx);
-
- timer_set_cval(ctx, cval);
+ /*
+ * With an offset, memory already holds the guest's CVAL (the
+ * trap handler or __deactivate_traps() wrote it).
+ */
+ if (!timer_get_offset(ctx))
+ timer_set_cval(ctx, read_sysreg_el0(SYS_CNTP_CVAL));
/* Disable the timer */
write_sysreg_el0(0, SYS_CNTP_CTL);
@@ -647,7 +642,8 @@ static void timer_restore_state(struct arch_timer_context *ctx)
offset = timer_get_offset(ctx);
if (has_broken_cntvoff()) {
set_cntvoff(0);
- cval += offset;
+ if (offset)
+ cval = timer_apply_offset(cval, offset, kvm_phys_timer_read());
} else {
set_cntvoff(offset);
}
@@ -660,7 +656,8 @@ static void timer_restore_state(struct arch_timer_context *ctx)
cval = timer_get_cval(ctx);
offset = timer_get_offset(ctx);
set_cntpoff(offset);
- cval += offset;
+ if (offset)
+ cval = timer_apply_offset(cval, offset, kvm_phys_timer_read());
write_sysreg_el0(cval, SYS_CNTP_CVAL);
isb();
write_sysreg_el0(timer_get_ctl(ctx), SYS_CNTP_CTL);
diff --git a/arch/arm64/kvm/hyp/vhe/switch.c b/arch/arm64/kvm/hyp/vhe/switch.c
index 7875911c05063..14aada311bac4 100644
--- a/arch/arm64/kvm/hyp/vhe/switch.c
+++ b/arch/arm64/kvm/hyp/vhe/switch.c
@@ -175,7 +175,8 @@ static void __deactivate_traps(struct kvm_vcpu *vcpu)
offset = read_sysreg_s(SYS_CNTPOFF_EL2);
if (map.direct_ptimer && offset) {
- write_sysreg_el0(val + offset, SYS_CNTP_CVAL);
+ val = timer_apply_offset(val, offset, arch_timer_read_cntpct_el0());
+ write_sysreg_el0(val, SYS_CNTP_CVAL);
isb();
}
}
@@ -296,10 +297,10 @@ static bool kvm_hyp_handle_timer(struct kvm_vcpu *vcpu, u64 *exit_code)
break;
case SYS_CNTP_CVAL_EL0:
if (vcpu_el2_e2h_is_set(vcpu)) {
- val = read_sysreg_el0(SYS_CNTP_CVAL);
-
- if (!has_cntpoff())
- val -= timer_get_offset(vcpu_hptimer(vcpu));
+ if (!has_cntpoff() && timer_get_offset(vcpu_hptimer(vcpu)))
+ val = __vcpu_sys_reg(vcpu, CNTHP_CVAL_EL2);
+ else
+ val = read_sysreg_el0(SYS_CNTP_CVAL);
} else {
val = __vcpu_sys_reg(vcpu, CNTP_CVAL_EL0);
}
diff --git a/include/kvm/arm_arch_timer.h b/include/kvm/arm_arch_timer.h
index bc6f2fdd7ad33..80f96ea59f1ae 100644
--- a/include/kvm/arm_arch_timer.h
+++ b/include/kvm/arm_arch_timer.h
@@ -195,4 +195,19 @@ static inline void timer_set_offset(struct arch_timer_context *ctxt, u64 offset)
WRITE_ONCE(*ctxt->offset.vm_offset, offset);
}
+/*
+ * CVAL to program when the hardware won't apply the timer's offset, so that
+ * its compare against the raw counter matches the guest's at 'now'. An
+ * expired timer stays expired; one past the wrap never fires.
+ */
+static inline u64 timer_apply_offset(u64 cval, u64 offset, u64 now)
+{
+ u64 hw = cval + offset;
+
+ if (now - offset >= cval)
+ return 0;
+
+ return hw > now ? hw : U64_MAX;
+}
+
#endif
--
2.39.5
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH 2/3] KVM: arm64: nv: Read a guest hypervisor's CNTV_CVAL_EL0 from memory on x1e
2026-09-21 14:04 [PATCH 0/3] KVM: arm64: timers: Program CVAL from the current count when the hardware won't apply the offset Fuad Tabba
2026-09-21 14:04 ` [PATCH 1/3] KVM: arm64: timers: Compute an offset-applied CVAL from the current count Fuad Tabba
@ 2026-09-21 14:04 ` Fuad Tabba
2026-09-21 14:04 ` [PATCH 3/3] KVM: arm64: selftests: Test a timer set past the counter's wrap Fuad Tabba
2 siblings, 0 replies; 4+ messages in thread
From: Fuad Tabba @ 2026-09-21 14:04 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Joey Gouly, Suzuki K Poulose, Zenghui Yu, Zenghui Yu,
Ganapatrao Kulkarni, Will Deacon, Fuad Tabba, kvmarm, kvm,
linux-arm-kernel, linux-kernel, linux-kselftest
On x1e KVM keeps CNTVOFF_EL2 at zero, traps the virtual timer whenever
it has an offset and programs the hardware with the offset applied.
kvm_hyp_handle_timer() returns that hardware value for a VHE guest
hypervisor's read of its own CNTV_CVAL_EL0, instead of the CVAL it
wrote. Its writes trap to the kernel and land in CNTHV_CVAL_EL2, so
return that, as the physical timer's read does without CNTPOFF_EL2.
Fixes: 0bc9a9e85fcf ("KVM: arm64: Work around x1e's CNTVOFF_EL2 bogosity")
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/hyp/vhe/switch.c | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/arch/arm64/kvm/hyp/vhe/switch.c b/arch/arm64/kvm/hyp/vhe/switch.c
index 14aada311bac4..4c965f9b1e1de 100644
--- a/arch/arm64/kvm/hyp/vhe/switch.c
+++ b/arch/arm64/kvm/hyp/vhe/switch.c
@@ -322,10 +322,14 @@ static bool kvm_hyp_handle_timer(struct kvm_vcpu *vcpu, u64 *exit_code)
val = __vcpu_sys_reg(vcpu, CNTV_CVAL_EL0);
break;
case SYS_CNTV_CVAL_EL0:
- if (vcpu_el2_e2h_is_set(vcpu))
- val = read_sysreg_el0(SYS_CNTV_CVAL);
- else
+ if (vcpu_el2_e2h_is_set(vcpu)) {
+ if (has_broken_cntvoff() && timer_get_offset(vcpu_hvtimer(vcpu)))
+ val = __vcpu_sys_reg(vcpu, CNTHV_CVAL_EL2);
+ else
+ val = read_sysreg_el0(SYS_CNTV_CVAL);
+ } else {
val = __vcpu_sys_reg(vcpu, CNTV_CVAL_EL0);
+ }
break;
case SYS_CNTVCT_EL0:
case SYS_CNTVCTSS_EL0:
--
2.39.5
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH 3/3] KVM: arm64: selftests: Test a timer set past the counter's wrap
2026-09-21 14:04 [PATCH 0/3] KVM: arm64: timers: Program CVAL from the current count when the hardware won't apply the offset Fuad Tabba
2026-09-21 14:04 ` [PATCH 1/3] KVM: arm64: timers: Compute an offset-applied CVAL from the current count Fuad Tabba
2026-09-21 14:04 ` [PATCH 2/3] KVM: arm64: nv: Read a guest hypervisor's CNTV_CVAL_EL0 from memory on x1e Fuad Tabba
@ 2026-09-21 14:04 ` Fuad Tabba
2 siblings, 0 replies; 4+ messages in thread
From: Fuad Tabba @ 2026-09-21 14:04 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Joey Gouly, Suzuki K Poulose, Zenghui Yu, Zenghui Yu,
Ganapatrao Kulkarni, Will Deacon, Fuad Tabba, kvmarm, kvm,
linux-arm-kernel, linux-kernel, linux-kselftest
Program an unmasked timer whose cval sits within long_wait_ms of the
counter's wrap while the guest's counter is at wait_ms, and expect no
interrupt. The guest's counter can't reach it, but a host that applies
the timer's offset by adding it to cval wraps the sum into the past and
fires at once. The one existing case with a cval past the wrap, in the
sanity checks, runs with the timer masked and can't see that.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
.../kvm/arm64/arch_timer_edge_cases.c | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c b/tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c
index d9c9377a63256..69986a7f0d37d 100644
--- a/tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c
+++ b/tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c
@@ -787,6 +787,30 @@ static void test_timers_in_the_past(enum arch_timer timer)
}
}
+/*
+ * With the counter at wait_ms, a tval of -(wait_ms + long_wait_ms) puts cval
+ * within long_wait_ms of the counter's wrap: a deadline further away than the
+ * counter can reach. Unmasked, so an early ISTATUS shows up as an IRQ.
+ */
+static void test_timers_past_counter_wrap(enum arch_timer timer)
+{
+ s32 tval = -(s32)msec_to_cycles(test_args.wait_ms + test_args.long_wait_ms);
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(sleep_method); i++) {
+ sleep_method_t sm = sleep_method[i];
+
+ local_irq_disable();
+ set_counter(timer, msec_to_cycles(test_args.wait_ms));
+ set_tval_irq(timer, tval, CTL_ENABLE);
+ sm(timer, msecs_to_usecs(test_args.wait_ms) + TIMEOUT_NO_IRQ_US);
+ local_irq_enable();
+ isb();
+ assert_irqs_handled(0);
+ timer_set_ctl(timer, CTL_IMASK);
+ }
+}
+
static void test_long_timer_delays(enum arch_timer timer)
{
s32 tval = (s32)msec_to_cycles(test_args.long_wait_ms);
@@ -806,6 +830,7 @@ static void guest_run_iteration(enum arch_timer timer)
test_basic_functionality(timer);
test_timers_sanity_checks(timer);
+ test_timers_past_counter_wrap(timer);
test_timers_above_tval_max(timer);
test_timers_in_the_past(timer);
--
2.39.5
^ permalink raw reply [flat|nested] 4+ messages in thread