Re: [PATCH v4] KVM: x86/tsc: Don't sync user changes to TSC with KVM-initiated change

From: Sean Christopherson
Date: Fri Aug 11 2023 - 19:00:06 EST


On Tue, Aug 01, 2023, Like Xu wrote:
> diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
> index 278dbd37dab2..eeaf4ad9174d 100644
> --- a/arch/x86/kvm/x86.c
> +++ b/arch/x86/kvm/x86.c
> @@ -2713,7 +2713,7 @@ static void __kvm_synchronize_tsc(struct kvm_vcpu *vcpu, u64 offset, u64 tsc,
> kvm_track_tsc_matching(vcpu);
> }
>
> -static void kvm_synchronize_tsc(struct kvm_vcpu *vcpu, u64 data)
> +static void kvm_synchronize_tsc(struct kvm_vcpu *vcpu, u64 data, bool user_initiated)

Rather than pass two somewhat magic values for the KVM-internal call, what about
making @data a pointer and passing NULL?

> {
> struct kvm *kvm = vcpu->kvm;
> u64 offset, ns, elapsed;
> @@ -2734,20 +2734,29 @@ static void kvm_synchronize_tsc(struct kvm_vcpu *vcpu, u64 data)
> * kvm_clock stable after CPU hotplug
> */
> synchronizing = true;
> - } else {
> + } else if (kvm->arch.user_changed_tsc) {
> u64 tsc_exp = kvm->arch.last_tsc_write +
> nsec_to_cycles(vcpu, elapsed);
> u64 tsc_hz = vcpu->arch.virtual_tsc_khz * 1000LL;
> /*
> - * Special case: TSC write with a small delta (1 second)
> - * of virtual cycle time against real time is
> - * interpreted as an attempt to synchronize the CPU.
> + * Here lies UAPI baggage: user-initiated TSC write with
> + * a small delta (1 second) of virtual cycle time
> + * against real time is interpreted as an attempt to
> + * synchronize the CPU.
> + *
> + * Don't synchronize user changes to the TSC with the
> + * KVM-initiated change in kvm_arch_vcpu_postcreate()
> + * by conditioning this mess on userspace having
> + * written the TSC at least once already.

Ok, this confused me for a good long while. As in, super duper wtf is going on
confused. Calling out kvm_arch_vcpu_postcreate() is a gigantic red-herring,
because this path is *never* reached by the kvm_synchronize_tsc() call from
kvm_arch_vcpu_postcreate(). @data is 0, and so the internal KVM call goes straight
to synchronizing.

And the fact that KVM does synchronization from kvm_arch_vcpu_postcreate() isn't
interesting, as that's just an internal detail. What's important is that
synchronization needs to be forced when creating or hotplugging a vCPU (data == 0),
but when NOT hotplugging should be skipped for the first write from userspace.

And IMO, this blurb from the changelog is flat out wrong:

: Unfortunately the TSC sync code makes no distinction
: between kernel and user-initiated writes, which leads to the target VM
: synchronizing on the TSC offset from creation instead of the
: user-intended value.

The problem isn't that the sync code doesn't differentiate between kernel and
user-initiated writes, because parts of the code *do* differentiate. I think it's
more accurate to say that the problem is that the sync code doesn't differentiate
between userspace initializing the TSC and userspace attempting to synchronize the
TSC.

And the "user_changed_tsc" only adds to the confusion, because in the hotplug
^^^^^^^
case, AFAICT there is no guarantee that the TSC will be changed, i.e. userspace
may set the exact same value.

With a massaged changelog, I think we want this (untested):

---
arch/x86/include/asm/kvm_host.h | 1 +
arch/x86/kvm/x86.c | 25 ++++++++++++++++---------
2 files changed, 17 insertions(+), 9 deletions(-)

diff --git a/arch/x86/include/asm/kvm_host.h b/arch/x86/include/asm/kvm_host.h
index 4c2d659a1269..bf566262ebd8 100644
--- a/arch/x86/include/asm/kvm_host.h
+++ b/arch/x86/include/asm/kvm_host.h
@@ -1331,6 +1331,7 @@ struct kvm_arch {
int nr_vcpus_matched_tsc;

u32 default_tsc_khz;
+ bool user_set_tsc;

seqcount_raw_spinlock_t pvclock_sc;
bool use_master_clock;
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 34945c7dba38..d01991aadf19 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -2698,8 +2698,9 @@ static void __kvm_synchronize_tsc(struct kvm_vcpu *vcpu, u64 offset, u64 tsc,
kvm_track_tsc_matching(vcpu);
}

-static void kvm_synchronize_tsc(struct kvm_vcpu *vcpu, u64 data)
+static void kvm_synchronize_tsc(struct kvm_vcpu *vcpu, u64 *user_value)
{
+ u64 data = user_value ? *user_value : 0;
struct kvm *kvm = vcpu->kvm;
u64 offset, ns, elapsed;
unsigned long flags;
@@ -2712,14 +2713,17 @@ static void kvm_synchronize_tsc(struct kvm_vcpu *vcpu, u64 data)
elapsed = ns - kvm->arch.last_tsc_nsec;

if (vcpu->arch.virtual_tsc_khz) {
+ /*
+ * Force synchronization when creating or hotplugging a vCPU,
+ * i.e. when the TSC value is '0', to help keep clocks stable.
+ * If this is NOT a hotplug/creation case, skip synchronization
+ * on the first write from userspace so as not to misconstrue
+ * state restoration after live migration as an attempt from
+ * userspace to synchronize.
+ */
if (data == 0) {
- /*
- * detection of vcpu initialization -- need to sync
- * with other vCPUs. This particularly helps to keep
- * kvm_clock stable after CPU hotplug
- */
synchronizing = true;
- } else {
+ } else if (kvm->arch.user_set_tsc) {
u64 tsc_exp = kvm->arch.last_tsc_write +
nsec_to_cycles(vcpu, elapsed);
u64 tsc_hz = vcpu->arch.virtual_tsc_khz * 1000LL;
@@ -2733,6 +2737,9 @@ static void kvm_synchronize_tsc(struct kvm_vcpu *vcpu, u64 data)
}
}

+ if (user_value)
+ kvm->arch.user_set_tsc = true;
+
/*
* For a reliable TSC, we can match TSC offsets, and for an unstable
* TSC, we add elapsed time in this computation. We could let the
@@ -3761,7 +3768,7 @@ int kvm_set_msr_common(struct kvm_vcpu *vcpu, struct msr_data *msr_info)
break;
case MSR_IA32_TSC:
if (msr_info->host_initiated) {
- kvm_synchronize_tsc(vcpu, data);
+ kvm_synchronize_tsc(vcpu, &data);
} else {
u64 adj = kvm_compute_l1_tsc_offset(vcpu, data) - vcpu->arch.l1_tsc_offset;
adjust_tsc_offset_guest(vcpu, adj);
@@ -11934,7 +11941,7 @@ void kvm_arch_vcpu_postcreate(struct kvm_vcpu *vcpu)
if (mutex_lock_killable(&vcpu->mutex))
return;
vcpu_load(vcpu);
- kvm_synchronize_tsc(vcpu, 0);
+ kvm_synchronize_tsc(vcpu, NULL);
vcpu_put(vcpu);

/* poll control enabled by default */

base-commit: ba44e03ef5e5d3ece316d1384e43e3d7761c89d4
--