Skip to contentSkip to conversation

KVM: arm64: pvtime: Don't lose stolen time on failed updates

PATCH In review Hao Zhang 2 participants 3 messages Sep 21 – Sep 24

Subsystems (4)
lore
Share & tools
lore

Hao Zhang

author
#0 lore raw

PATCH 1/2 KVM: arm64: pvtime: Don't lose stolen time on failed updates

From: Hao Zhang <[email protected]>

kvm_update_stolen_time() advances last_steal before checking whether kvm_put_guest() successfully writes the updated value to guest memory.

If the write fails, the updated stolen time is not visible to the guest, but the corresponding run delay has already been consumed from KVM's accounting state. A later successful update therefore starts from the advanced last_steal value and permanently loses that interval.

Stolen-time updates are best-effort, but a failed update must not consume accounting state. Otherwise, a transient write failure turns into a permanent loss even if a later update succeeds.

Read the current run delay into a local variable and update last_steal only after kvm_put_guest() succeeds. This leaves the unreported delay pending so that a later update can account for it.

Fixes 53f985584e3c ("KVM: arm64: pvtime: Fix stolen time accounting across migration")
---
 arch/arm64/kvm/pvtime.c | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)
arch/arm64/kvm/pvtime.c +5 -3
index 4ceabaa4c30b..9b4f0645d92b 100644
@@ -17,6 +17,7 @@ void kvm_update_stolen_time(struct kvm_vcpu *vcpu)
 	u64 last_steal = vcpu->arch.steal.last_steal;
 	u64 offset = offsetof(struct pvclock_vcpu_stolen_time, stolen_time);
 	u64 steal = 0;
+	u64 run_delay;
 	int idx;
 
 	if (base == INVALID_GPA)
@@ -25,9 +26,10 @@ void kvm_update_stolen_time(struct kvm_vcpu *vcpu)
 	idx = srcu_read_lock(&kvm->srcu);
 	if (!kvm_get_guest(kvm, base + offset, steal)) {
 		steal = le64_to_cpu(steal);
-		vcpu->arch.steal.last_steal = READ_ONCE(current->sched_info.run_delay);
-		steal += vcpu->arch.steal.last_steal - last_steal;
-		kvm_put_guest(kvm, base + offset, cpu_to_le64(steal));
+		run_delay = READ_ONCE(current->sched_info.run_delay);
+		steal += run_delay - last_steal;
+		if (!kvm_put_guest(kvm, base + offset, cpu_to_le64(steal)))
+			vcpu->arch.steal.last_steal = run_delay;
 	}
 	srcu_read_unlock(&kvm->srcu, idx);
 }

base-commit: 93f51579e7df248780214094418f205253383cc5

Signature · 1 line
2.15.0
author
#1 lore raw

PATCH 2/2 KVM: selftests: Test arm64 stolen time after failed updates

From: Hao Zhang <[email protected]>

Use mprotect() to keep the stolen-time page readable while removing write access. This allows kvm_get_guest() to succeed and forces the subsequent kvm_put_guest() to fail.

Accumulate vCPU run delay, trigger the failed update, restore write access, and run the vCPU again. Verify that the successful retry includes the delay that could not be reported by the failed update.

Without the fix, KVM advances last_steal on the failed write and the test observes that the accumulated delay is lost.

---
 tools/testing/selftests/kvm/steal_time.c | 58 +++++++++++++++++++++++++++++++-
 1 file changed, 57 insertions(+), 1 deletion(-)
tools/testing/selftests/kvm/steal_time.c +57 -1
index bc3c62b72c58..fbdd55fa5ab6 100644
@@ -8,6 +8,8 @@
 #include <time.h>
 #include <sched.h>
 #include <pthread.h>
+#include <sys/mman.h>
+#include <unistd.h>
 #include <linux/kernel.h>
 #include <asm/kvm.h>
 #ifdef __riscv
@@ -162,6 +164,15 @@ static void guest_code(int cpu)
 
 	check_status(st);
 	WRITE_ONCE(guest_stolen_time[cpu], st->st_time);
+	if (!cpu) {
+		/*
+		 * Let userspace trigger a failed stolen-time update, then retry
+		 * the update after restoring write access.
+		 */
+		GUEST_SYNC(2);
+		GUEST_SYNC(3);
+		WRITE_ONCE(guest_stolen_time[cpu], st->st_time);
+	}
 	GUEST_DONE();
 }
 
@@ -469,9 +480,10 @@ static void check_steal_time_uapi(void)
 static void *do_steal_time(void *arg)
 {
 	struct timespec ts, stop;
+	unsigned long duration = arg ? *(unsigned long *)arg : MIN_RUN_DELAY_NS;
 
 	clock_gettime(CLOCK_MONOTONIC, &ts);
-	stop = timespec_add_ns(ts, MIN_RUN_DELAY_NS);
+	stop = timespec_add_ns(ts, duration);
 
 	while (1) {
 		clock_gettime(CLOCK_MONOTONIC, &ts);
@@ -500,6 +512,45 @@ static void run_vcpu(struct kvm_vcpu *vcpu)
 	}
 }
 
+#ifdef __aarch64__
+static void test_failed_stolen_time_update(struct kvm_vcpu *vcpu)
+{
+	struct kvm_vm *vm = vcpu->vm;
+	struct st_time *st = addr_gva2hva(vm, ST_GPA_BASE);
+	unsigned long duration = 100 * 1000 * 1000UL;
+	u64 stolen_time = st->st_time;
+	long run_delay;
+	pthread_t thread;
+
+	/*
+	 * Keep the page readable so that kvm_get_guest() succeeds, but make
+	 * kvm_put_guest() fail when KVM attempts to update stolen time.
+	 */
+	TEST_ASSERT(!mprotect(st, getpagesize(), PROT_READ),
+		    "Failed to make stolen time page read-only");
+
+	run_delay = get_run_delay();
+	kvm_pthread_create(&thread, NULL, do_steal_time, &duration);
+	do
+		sched_yield();
+	while (get_run_delay() - run_delay < duration / 5);
+	kvm_pthread_join(thread, NULL);
+	run_delay = get_run_delay() - run_delay;
+
+	/* The update fails, but the guest must still be able to run. */
+	run_vcpu(vcpu);
+	TEST_ASSERT(st->st_time == stolen_time,
+		    "Stolen time changed on a read-only page");
+
+	TEST_ASSERT(!mprotect(st, getpagesize(), PROT_READ | PROT_WRITE),
+		    "Failed to restore stolen time page write access");
+	run_vcpu(vcpu);
+	TEST_ASSERT(st->st_time - stolen_time >= run_delay,
+		    "Lost stolen time after a failed update: expected >= %ld, got %lu",
+		    run_delay, (unsigned long)(st->st_time - stolen_time));
+}
+#endif
+
 int main(int ac, char **av)
 {
 	struct kvm_vcpu *vcpus[NR_VCPUS];
@@ -569,6 +620,11 @@ int main(int ac, char **av)
 			    "Expected stolen time >= %ld, got %ld",
 			    run_delay, stolen_time);
 
+#ifdef __aarch64__
+		if (!i)
+			test_failed_stolen_time_update(vcpus[i]);
+#endif
+
 		if (verbose) {
 			ksft_print_msg("VCPU%d: total-stolen-time=%ld test-stolen-time=%ld%s\n",
 				       i, guest_stolen_time[i], stolen_time,
Signature · 1 line
2.15.0
maintainer
#2 lore raw
In reply to Hao Zhang
Quoted 10 lines · Hao Zhang

From: Hao Zhang <[email protected]>

kvm_update_stolen_time() advances last_steal before checking whether kvm_put_guest() successfully writes the updated value to guest memory.

If the write fails, the updated stolen time is not visible to the guest, but the corresponding run delay has already been consumed from KVM's accounting state. A later successful update therefore starts from the advanced last_steal value and permanently loses that interval.

I'm sceptical.

A "transient" write failure means that userspace has unmapped the page this should have been stored to. Userspace is in charge of the VM -- it literally is the owner.

Why should we prevent it from shooting itself in the foot? Userspace can equally stop the vcpu from running, and this is "stolen time" that is not accounted for.

	M.
Signature · 1 line
Without deviation from the norm, progress is not possible.