MIPS: KVM: Fix timer IRQ race when writing CP0_Compare
authorJames Hogan <james.hogan@imgtec.com>
Fri, 22 Apr 2016 09:38:46 +0000 (10:38 +0100)
committerPaolo Bonzini <pbonzini@redhat.com>
Tue, 10 May 2016 13:56:50 +0000 (15:56 +0200)
Writing CP0_Compare clears the timer interrupt pending bit
(CP0_Cause.TI), but this wasn't being done atomically. If a timer
interrupt raced with the write of the guest CP0_Compare, the timer
interrupt could end up being pending even though the new CP0_Compare is
nowhere near CP0_Count.

We were already updating the hrtimer expiry with
kvm_mips_update_hrtimer(), which used both kvm_mips_freeze_hrtimer() and
kvm_mips_resume_hrtimer(). Close the race window by expanding out
kvm_mips_update_hrtimer(), and clearing CP0_Cause.TI and setting
CP0_Compare between the freeze and resume. Since the pending timer
interrupt should not be cleared when CP0_Compare is written via the KVM
user API, an ack argument is added to distinguish the source of the
write.

Fixes: e30492bbe95a ("MIPS: KVM: Rewrite count/compare timer emulation")
Signed-off-by: James Hogan <james.hogan@imgtec.com>
Cc: Paolo Bonzini <pbonzini@redhat.com>
Cc: "Radim KrÄ\8dmář" <rkrcmar@redhat.com>
Cc: Ralf Baechle <ralf@linux-mips.org>
Cc: linux-mips@linux-mips.org
Cc: kvm@vger.kernel.org
Cc: <stable@vger.kernel.org> # 3.16.x-
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
arch/mips/include/asm/kvm_host.h
arch/mips/kvm/emulate.c
arch/mips/kvm/trap_emul.c

index f6b12790716c071db8a3ce17971dd146c30025f5..942b8f6bf35b463d46bc5f5abe292f6e1ec7844a 100644 (file)
@@ -747,7 +747,7 @@ extern enum emulation_result kvm_mips_complete_mmio_load(struct kvm_vcpu *vcpu,
 
 uint32_t kvm_mips_read_count(struct kvm_vcpu *vcpu);
 void kvm_mips_write_count(struct kvm_vcpu *vcpu, uint32_t count);
-void kvm_mips_write_compare(struct kvm_vcpu *vcpu, uint32_t compare);
+void kvm_mips_write_compare(struct kvm_vcpu *vcpu, uint32_t compare, bool ack);
 void kvm_mips_init_count(struct kvm_vcpu *vcpu);
 int kvm_mips_set_count_ctl(struct kvm_vcpu *vcpu, s64 count_ctl);
 int kvm_mips_set_count_resume(struct kvm_vcpu *vcpu, s64 count_resume);
index ab5681de6eceaf1583e6593f64b7d0d21d8da157..b8b7860ec1a8e3fe266ff5de751ff91e7a49294c 100644 (file)
@@ -437,32 +437,6 @@ static void kvm_mips_resume_hrtimer(struct kvm_vcpu *vcpu,
        hrtimer_start(&vcpu->arch.comparecount_timer, expire, HRTIMER_MODE_ABS);
 }
 
-/**
- * kvm_mips_update_hrtimer() - Update next expiry time of hrtimer.
- * @vcpu:      Virtual CPU.
- *
- * Recalculates and updates the expiry time of the hrtimer. This can be used
- * after timer parameters have been altered which do not depend on the time that
- * the change occurs (in those cases kvm_mips_freeze_hrtimer() and
- * kvm_mips_resume_hrtimer() are used directly).
- *
- * It is guaranteed that no timer interrupts will be lost in the process.
- *
- * Assumes !kvm_mips_count_disabled(@vcpu) (guest CP0_Count timer is running).
- */
-static void kvm_mips_update_hrtimer(struct kvm_vcpu *vcpu)
-{
-       ktime_t now;
-       uint32_t count;
-
-       /*
-        * freeze_hrtimer takes care of a timer interrupts <= count, and
-        * resume_hrtimer the hrtimer takes care of a timer interrupts > count.
-        */
-       now = kvm_mips_freeze_hrtimer(vcpu, &count);
-       kvm_mips_resume_hrtimer(vcpu, now, count);
-}
-
 /**
  * kvm_mips_write_count() - Modify the count and update timer.
  * @vcpu:      Virtual CPU.
@@ -558,23 +532,42 @@ int kvm_mips_set_count_hz(struct kvm_vcpu *vcpu, s64 count_hz)
  * kvm_mips_write_compare() - Modify compare and update timer.
  * @vcpu:      Virtual CPU.
  * @compare:   New CP0_Compare value.
+ * @ack:       Whether to acknowledge timer interrupt.
  *
  * Update CP0_Compare to a new value and update the timeout.
+ * If @ack, atomically acknowledge any pending timer interrupt, otherwise ensure
+ * any pending timer interrupt is preserved.
  */
-void kvm_mips_write_compare(struct kvm_vcpu *vcpu, uint32_t compare)
+void kvm_mips_write_compare(struct kvm_vcpu *vcpu, uint32_t compare, bool ack)
 {
        struct mips_coproc *cop0 = vcpu->arch.cop0;
+       int dc;
+       u32 old_compare = kvm_read_c0_guest_compare(cop0);
+       ktime_t now;
+       uint32_t count;
 
        /* if unchanged, must just be an ack */
-       if (kvm_read_c0_guest_compare(cop0) == compare)
+       if (old_compare == compare) {
+               if (!ack)
+                       return;
+               kvm_mips_callbacks->dequeue_timer_int(vcpu);
+               kvm_write_c0_guest_compare(cop0, compare);
                return;
+       }
+
+       /* freeze_hrtimer() takes care of timer interrupts <= count */
+       dc = kvm_mips_count_disabled(vcpu);
+       if (!dc)
+               now = kvm_mips_freeze_hrtimer(vcpu, &count);
+
+       if (ack)
+               kvm_mips_callbacks->dequeue_timer_int(vcpu);
 
-       /* Update compare */
        kvm_write_c0_guest_compare(cop0, compare);
 
-       /* Update timeout if count enabled */
-       if (!kvm_mips_count_disabled(vcpu))
-               kvm_mips_update_hrtimer(vcpu);
+       /* resume_hrtimer() takes care of timer interrupts > count */
+       if (!dc)
+               kvm_mips_resume_hrtimer(vcpu, now, count);
 }
 
 /**
@@ -1113,9 +1106,9 @@ enum emulation_result kvm_mips_emulate_CP0(uint32_t inst, uint32_t *opc,
 
                                /* If we are writing to COMPARE */
                                /* Clear pending timer interrupt, if any */
-                               kvm_mips_callbacks->dequeue_timer_int(vcpu);
                                kvm_mips_write_compare(vcpu,
-                                                      vcpu->arch.gprs[rt]);
+                                                      vcpu->arch.gprs[rt],
+                                                      true);
                        } else if ((rd == MIPS_CP0_STATUS) && (sel == 0)) {
                                unsigned int old_val, val, change;
 
index c4038d2a724c0df9746a75c67c358deb3d620071..caa5ea1038a08059b2ae92a804a4201cc73b9a04 100644 (file)
@@ -546,7 +546,7 @@ static int kvm_trap_emul_set_one_reg(struct kvm_vcpu *vcpu,
                kvm_mips_write_count(vcpu, v);
                break;
        case KVM_REG_MIPS_CP0_COMPARE:
-               kvm_mips_write_compare(vcpu, v);
+               kvm_mips_write_compare(vcpu, v, false);
                break;
        case KVM_REG_MIPS_CP0_CAUSE:
                /*