diff mbox series

[v2] SVM: add test for nested guest RIP corruption

Message ID 20200623105207.149798-1-mlevitsk@redhat.com (mailing list archive)
State New, archived
Headers show
Series [v2] SVM: add test for nested guest RIP corruption | expand

Commit Message

Maxim Levitsky June 23, 2020, 10:52 a.m. UTC
This adds a unit test for SVM nested register corruption that happened when
L0 was emulating an instruction, but then injecting an interrupt intercept to L1, which
lead it to give L1 vmexit handler stale (pre emulation) values of RAX,RIP and RSP.

This test detects the RIP corruption (situation when RIP is at the start of
the emulated instruction but the instruction, was already executed.

The upstream commit that fixed this bug is b6162e82aef19fee9c32cb3fe9ac30d9116a8c73
  KVM: nSVM: Preserve registers modifications done before nested_svm_vmexit()

Signed-off-by: Maxim Levitsky <mlevitsk@redhat.com>
---
 x86/svm_tests.c | 102 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 102 insertions(+)

Comments

Maxim Levitsky June 23, 2020, 10:57 a.m. UTC | #1
On Tue, 2020-06-23 at 13:52 +0300, Maxim Levitsky wrote:
> This adds a unit test for SVM nested register corruption that happened when
> L0 was emulating an instruction, but then injecting an interrupt intercept to L1, which
> lead it to give L1 vmexit handler stale (pre emulation) values of RAX,RIP and RSP.
> 
> This test detects the RIP corruption (situation when RIP is at the start of
> the emulated instruction but the instruction, was already executed.
> 
> The upstream commit that fixed this bug is b6162e82aef19fee9c32cb3fe9ac30d9116a8c73
>   KVM: nSVM: Preserve registers modifications done before nested_svm_vmexit()
> 
> Signed-off-by: Maxim Levitsky <mlevitsk@redhat.com>
> ---
>  x86/svm_tests.c | 102 ++++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 102 insertions(+)
> 
> diff --git a/x86/svm_tests.c b/x86/svm_tests.c
> index c1abd55..202c829 100644
> --- a/x86/svm_tests.c
> +++ b/x86/svm_tests.c
> @@ -1789,6 +1789,105 @@ static bool virq_inject_check(struct svm_test *test)
>      return get_test_stage(test) == 5;
>  }
>  
> +/*
> + * Detect nested guest RIP corruption as explained in kernel commit
> + * b6162e82aef19fee9c32cb3fe9ac30d9116a8c73
> + *
> + * In the assembly loop below 'ins' is executed while IO instructions
> + * are not intercepted; the instruction is emulated by L0.
> +
> + * At the same time we are getting interrupts from the local APIC timer,
> + * and we do intercept them in L1
> + *
> + * If the interrupt happens on the insb instruction, L0 will VMexit, emulate
> + * the insb instruction and then it will inject the interrupt to L1 through
> + * a nested VMexit.  Due to a bug, it would leave pre-emulation values of RIP,
> + * RAX and RSP in the VMCB.
> + *
> + * In our intercept handler we detect the bug by checking that RIP is that of
> + * the insb instruction, but its memory operand has already been written.
> + * This means that insb was already executed.
> + */
> +
> +static volatile int isr_cnt = 0;
> +static volatile uint8_t io_port_var = 0xAA;
> +extern const char insb_instruction_label[];
> +
> +static void reg_corruption_isr(isr_regs_t *regs)
> +{
> +    isr_cnt++;
> +    apic_write(APIC_EOI, 0);
> +}
> +
> +static void reg_corruption_prepare(struct svm_test *test)
> +{
> +    default_prepare(test);
> +    set_test_stage(test, 0);
> +
> +    vmcb->control.int_ctl = V_INTR_MASKING_MASK;
> +    vmcb->control.intercept |= (1ULL << INTERCEPT_INTR);
> +
> +    handle_irq(TIMER_VECTOR, reg_corruption_isr);
> +
> +    /* set local APIC to inject external interrupts */
> +    apic_write(APIC_TMICT, 0);
> +    apic_write(APIC_TDCR, 0);
> +    apic_write(APIC_LVTT, TIMER_VECTOR | APIC_LVT_TIMER_PERIODIC);
> +    apic_write(APIC_TMICT, 1000);
> +}
> +
> +static void reg_corruption_test(struct svm_test *test)
> +{
> +    /* this is endless loop, which is interrupted by the timer interrupt */
> +    asm volatile (
> +            "1:\n\t"
> +            "movw $0x4d0, %%dx\n\t" // IO port
> +            "lea %[_io_port_var], %%rdi\n\t"
> +            "movb $0xAA, %[_io_port_var]\n\t"
> +            "insb_instruction_label:\n\t"
> +            "insb\n\t"
> +            "jmp 1b\n\t"
> +
> +            : [_io_port_var] "=m" (io_port_var)
> +            : /* no inputs*/
> +            : "rdx", "rdi"
> +    );
> +}
> +
> +static bool reg_corruption_finished(struct svm_test *test)
> +{
> +    if (isr_cnt == 10000) {
> +        report(true,
> +               "No RIP corruption detected after %d timer interrupts",
> +               isr_cnt);
> +        set_test_stage(test, 1);
> +        return true;
> +    }
> +
> +    if (vmcb->control.exit_code == SVM_EXIT_INTR) {
> +
> +        void* guest_rip = (void*)vmcb->save.rip;
> +
> +        irq_enable();
> +        asm volatile ("nop");
> +        irq_disable();
> +
> +        if (guest_rip == insb_instruction_label && io_port_var != 0xAA) {
> +            report(false,
> +                   "RIP corruption detected after %d timer interrupts",
> +                   isr_cnt);
> +            return true;
> +        }
> +
> +    }
> +    return false;
> +}
> +
> +static bool reg_corruption_check(struct svm_test *test)
> +{
> +    return get_test_stage(test) == 1;
> +}
> +
>  #define TEST(name) { #name, .v2 = name }
>  
>  /*
> @@ -1950,6 +2049,9 @@ struct svm_test svm_tests[] = {
>      { "virq_inject", default_supported, virq_inject_prepare,
>        default_prepare_gif_clear, virq_inject_test,
>        virq_inject_finished, virq_inject_check },
> +    { "reg_corruption", default_supported, reg_corruption_prepare,
> +      default_prepare_gif_clear, reg_corruption_test,
> +      reg_corruption_finished, reg_corruption_check },
>      TEST(svm_guest_state_test),
>      { NULL, NULL, NULL, NULL, NULL, NULL, NULL }
>  };

Oops, I see that you already applied the patch with the changes,
so no need for this patch.
Thanks!

Best regards,
	Maxim Levitsky
diff mbox series

Patch

diff --git a/x86/svm_tests.c b/x86/svm_tests.c
index c1abd55..202c829 100644
--- a/x86/svm_tests.c
+++ b/x86/svm_tests.c
@@ -1789,6 +1789,105 @@  static bool virq_inject_check(struct svm_test *test)
     return get_test_stage(test) == 5;
 }
 
+/*
+ * Detect nested guest RIP corruption as explained in kernel commit
+ * b6162e82aef19fee9c32cb3fe9ac30d9116a8c73
+ *
+ * In the assembly loop below 'ins' is executed while IO instructions
+ * are not intercepted; the instruction is emulated by L0.
+
+ * At the same time we are getting interrupts from the local APIC timer,
+ * and we do intercept them in L1
+ *
+ * If the interrupt happens on the insb instruction, L0 will VMexit, emulate
+ * the insb instruction and then it will inject the interrupt to L1 through
+ * a nested VMexit.  Due to a bug, it would leave pre-emulation values of RIP,
+ * RAX and RSP in the VMCB.
+ *
+ * In our intercept handler we detect the bug by checking that RIP is that of
+ * the insb instruction, but its memory operand has already been written.
+ * This means that insb was already executed.
+ */
+
+static volatile int isr_cnt = 0;
+static volatile uint8_t io_port_var = 0xAA;
+extern const char insb_instruction_label[];
+
+static void reg_corruption_isr(isr_regs_t *regs)
+{
+    isr_cnt++;
+    apic_write(APIC_EOI, 0);
+}
+
+static void reg_corruption_prepare(struct svm_test *test)
+{
+    default_prepare(test);
+    set_test_stage(test, 0);
+
+    vmcb->control.int_ctl = V_INTR_MASKING_MASK;
+    vmcb->control.intercept |= (1ULL << INTERCEPT_INTR);
+
+    handle_irq(TIMER_VECTOR, reg_corruption_isr);
+
+    /* set local APIC to inject external interrupts */
+    apic_write(APIC_TMICT, 0);
+    apic_write(APIC_TDCR, 0);
+    apic_write(APIC_LVTT, TIMER_VECTOR | APIC_LVT_TIMER_PERIODIC);
+    apic_write(APIC_TMICT, 1000);
+}
+
+static void reg_corruption_test(struct svm_test *test)
+{
+    /* this is endless loop, which is interrupted by the timer interrupt */
+    asm volatile (
+            "1:\n\t"
+            "movw $0x4d0, %%dx\n\t" // IO port
+            "lea %[_io_port_var], %%rdi\n\t"
+            "movb $0xAA, %[_io_port_var]\n\t"
+            "insb_instruction_label:\n\t"
+            "insb\n\t"
+            "jmp 1b\n\t"
+
+            : [_io_port_var] "=m" (io_port_var)
+            : /* no inputs*/
+            : "rdx", "rdi"
+    );
+}
+
+static bool reg_corruption_finished(struct svm_test *test)
+{
+    if (isr_cnt == 10000) {
+        report(true,
+               "No RIP corruption detected after %d timer interrupts",
+               isr_cnt);
+        set_test_stage(test, 1);
+        return true;
+    }
+
+    if (vmcb->control.exit_code == SVM_EXIT_INTR) {
+
+        void* guest_rip = (void*)vmcb->save.rip;
+
+        irq_enable();
+        asm volatile ("nop");
+        irq_disable();
+
+        if (guest_rip == insb_instruction_label && io_port_var != 0xAA) {
+            report(false,
+                   "RIP corruption detected after %d timer interrupts",
+                   isr_cnt);
+            return true;
+        }
+
+    }
+    return false;
+}
+
+static bool reg_corruption_check(struct svm_test *test)
+{
+    return get_test_stage(test) == 1;
+}
+
 #define TEST(name) { #name, .v2 = name }
 
 /*
@@ -1950,6 +2049,9 @@  struct svm_test svm_tests[] = {
     { "virq_inject", default_supported, virq_inject_prepare,
       default_prepare_gif_clear, virq_inject_test,
       virq_inject_finished, virq_inject_check },
+    { "reg_corruption", default_supported, reg_corruption_prepare,
+      default_prepare_gif_clear, reg_corruption_test,
+      reg_corruption_finished, reg_corruption_check },
     TEST(svm_guest_state_test),
     { NULL, NULL, NULL, NULL, NULL, NULL, NULL }
 };