Skip to contentSkip to conversation

KVM: arm64: timers: Program CVAL from the current count when the hardware won't apply the offset

PATCH Superseded Fuad Tabba 2 participants 6 messages Sep 21

Subsystems (4)

v1 of 2 Superseded Latest is v2 →

lore
Share & tools
lore

Fuad Tabba

author reviewer
#0 lore raw

cover KVM: arm64: timers: Program CVAL from the current count when the hardware won't apply the offset

Hi folks,

A guest timer can fail to fire, or fire early, once the guest's counter is far enough from the host's. Where the hardware won't apply the offset itself (the physical timer without CNTPOFF_EL2, a CNTPOFF_EL2 host while TGE is set, the virtual timer on x1e), KVM adds it to CVAL and lets the hardware compare the sum against the raw counter. The timer condition is an unsigned compare, and adding the offset to both sides of it preserves it only while 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 never fires, and one behind it can have a far-future timer fire at once.

Patch 1 computes the programmed value from the current count instead, and returns a guest hypervisor's own physical CVAL from memory where it used to subtract the offset from the hardware value. Patch 2 does the same for the virtual CVAL read on x1e, which has returned CVAL + offset since the workaround landed. Patch 3 adds the second case, a guest behind the host with a far-future timer, to arch_timer_edge_cases.

So far it has only shown up in the selftest, which moves the guest counter half its range away from the host's. On a VHE host without CNTPOFF_EL2 arch_timer_edge_cases hangs in its physical cval = 0 cases until the vCPU is next loaded, which is what Zenghui saw on a Kunpeng920 [1]. On a CNTPOFF_EL2 host (QEMU, here) the same cases livelock instead. Both go away with patch 1. The arithmetic is wrong for any offset once one sum wraps and the other doesn't, and the fix for the selftest's case should be the fix for all of them.

Tested with arch_timer_edge_cases on QEMU with [2] applied, under VHE, nVHE and pKVM, and under VHE with CNTPOFF_EL2 removed by id_aa64mmfr0.ecv=1, the host class Zenghui hit. Under VHE with patch 1 reverted the new case fails. QEMU's TCG has a separate bug with the same offsets, which [2] fixes. Without it the test stalls under nVHE and pKVM as well, where KVM emulates the physical timer and patch 1 changes nothing. Not exercised here: patch 1's x1e paths in timer_save_state() and timer_restore_state() and all of patch 2, since nothing here runs on an x1e, and patch 1's CNTP_CVAL_EL0 read in kvm_hyp_handle_timer(), which only a guest hypervisor with a physical offset on a host without CNTPOFF_EL2 reaches.

Patch 1 carries Cc: stable for the hang and the livelock.

Based on Linux 7.3-rc3 (fd73f4a665989).

Cheers,
/fuad

[1] https://lore.kernel.org/r/[email protected]
[2] https://lore.kernel.org/qemu-devel/[email protected]/

Fuad Tabba (3):
KVM: arm64: timers: Compute an offset-applied CVAL from the current
count
KVM: arm64: nv: Read a guest hypervisor's CNTV_CVAL_EL0 from memory on

    x1e

KVM: arm64: selftests: Test a timer set past the counter's wrap

 arch/arm64/kvm/arch_timer.c                   | 27 +++++++++----------
 arch/arm64/kvm/hyp/vhe/switch.c               | 21 +++++++++------
 include/kvm/arm_arch_timer.h                  | 15 +++++++++++
 .../kvm/arm64/arch_timer_edge_cases.c         | 25 +++++++++++++++++
 4 files changed, 65 insertions(+), 23 deletions(-)
Signature · 1 line
2.39.5
author reviewer
#1 lore raw

PATCH 1/3 KVM: arm64: timers: Compute an offset-applied CVAL from the current count

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")
---
 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(-)
arch/arm64/kvm/arch_timer.c +12 -15
index 6ac3321f4c575..9278b5383c044 100644
@@ -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);
arch/arm64/kvm/hyp/vhe/switch.c +6 -5
index 7875911c05063..14aada311bac4 100644
@@ -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);
 		}
include/kvm/arm_arch_timer.h +15 -0
index bc6f2fdd7ad33..80f96ea59f1ae 100644
@@ -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
Signature · 1 line
2.39.5
author reviewer
#2 lore raw

PATCH 2/3 KVM: arm64: nv: Read a guest hypervisor's CNTV_CVAL_EL0 from memory on x1e

In reply to Fuad Tabba

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")
---
 arch/arm64/kvm/hyp/vhe/switch.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)
arch/arm64/kvm/hyp/vhe/switch.c +7 -3
index 14aada311bac4..4c965f9b1e1de 100644
@@ -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:
Signature · 1 line
2.39.5
author reviewer
#3 lore raw

PATCH 3/3 KVM: arm64: selftests: Test a timer set past the counter's wrap

In reply to Fuad Tabba

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.

---
 .../kvm/arm64/arch_timer_edge_cases.c         | 25 +++++++++++++++++++
 1 file changed, 25 insertions(+)
tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c +25 -0
index d9c9377a63256..69986a7f0d37d 100644
@@ -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);
 
Signature · 1 line
2.39.5
#4 lore raw

Re: [PATCH 3/3] KVM: arm64: selftests: Test a timer set past the counter's wrap

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] selftests: flaky test due to scheduling delay in test_timers_past_counter_wrap --

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests: flaky test due to scheduling delay in test_timers_past_counter_wrap --

commit 8788b1ebd8d97a89915e8766ef7a7c75ad83b3c9
Author: Fuad Tabba <[email protected]>

KVM: arm64: selftests: Test a timer set past the counter's wrap

This commit adds a KVM selftest to verify timer behavior when programmed with a cval past the counter's wrap point. It programs an unmasked timer and tests that no unexpected interrupts fire since the guest's counter cannot realistically reach the deadline.

Quoted 4 lines
tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c +0 -0
index d9c9377a63256..69986a7f0d37d 100644

[ ... ]

Quoted 6 lines
tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c +6 -0
+/*
+ * 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)

tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c static void test_timers_in_the_past(enum arch_timer timer) · line 805

Quoted 10 lines
tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c +10 -0
+{
+	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);

[Severity: Medium]
Could this sequence lead to a flaky test on a loaded host system?

If a host scheduling delay exceeds long_wait_ms between setting the counter and writing the TVAL, the guest's CNT can advance such that:
CNT > wait_ms + long_wait_ms

When set_tval_irq() then executes, the hardware calculates the absolute deadline as CVAL = CNT + TVAL. Because TVAL is a negative value of -(wait_ms + long_wait_ms), CVAL evaluates to a small positive number instead of wrapping around to near U64_MAX.

Since CVAL is now less than CNT, the timer condition (CNT >= CVAL) is met immediately, and the hardware legitimately fires the unmasked timer. This would cause the subsequent assert_irqs_handled(0) check to fail.

Quoted 7 lines
tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c +7 -0
+		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);
+	}
+}
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
author reviewer
#5 lore raw

Re: [PATCH 3/3] KVM: arm64: selftests: Test a timer set past the counter's wrap

On Mon, 21 Sept 2026 at 15:17, <[email protected]> wrote:
[...]

Quoted 12 lines
Quoted 12 lines
tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c +11 -0
+/*
+ * 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++) {
Quoted 8 lines

tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c static void test_timers_in_the_past(enum arch_timer timer) · line 805

Quoted 5 lines
tools/testing/selftests/kvm/arm64/arch_timer_edge_cases.c +5 -0
+             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);

[Severity: Medium]
Could this sequence lead to a flaky test on a loaded host system?

Yes, a stall of more than long_wait_ms between the counter write and the TVAL write leaves cval below the counter. This should sort it out:

- s32 tval = -(s32)msec_to_cycles(test_args.wait_ms +
test_args.long_wait_ms);
+ u64 cval = -msec_to_cycles(test_args.long_wait_ms);
[...]
- set_tval_irq(timer, tval, CTL_ENABLE);
+ set_cval_irq(timer, cval, CTL_ENABLE);

Cheers,
/fuad