x86: Use a separate stack and handler for NMIs
authorBarret Rhoden <brho@cs.berkeley.edu>
Mon, 25 Jul 2016 16:23:50 +0000 (12:23 -0400)
committerBarret Rhoden <brho@cs.berkeley.edu>
Wed, 27 Jul 2016 16:52:43 +0000 (12:52 -0400)
It turns out that on x86_64, you *must* use a separate stack for
NMIs.  On i386, this was an option.  Why, you ask?  Because SYSCALL (64
bit) differs from SYSENTER (32 bit) in that SYSCALL does *not* set the
stack pointer for the kernel on entry.  It's up to the SYSCALL/SYSENTER
entry point software to figure out and set the stack pointer.

The problem is that if you receive an NMI before the kernel can set the
stack pointer.  If your NMI handler doesn't use a separate, pre-determined
stack, the interrupt hardware pushes the basic interrupt info onto the
*current* stack, which happens to be a user-controlled pointer at this
point.  Good times.

For more fun times, we also need to return to user TFs directly, and not
call proc_restartcore().  See the notes on handle_nmi() for more info.

Signed-off-by: Barret Rhoden <brho@cs.berkeley.edu>
kern/arch/x86/smp_boot.c
kern/arch/x86/trap.c
kern/arch/x86/trapentry64.S

index e1f1f15..cf22011 100644 (file)
@@ -268,6 +268,19 @@ uintptr_t smp_main(void)
        return my_stack_top; // will be loaded in smp_entry.S
 }
 
+static void pcpu_init_nmi(struct per_cpu_info *pcpui)
+{
+       uintptr_t nmi_entry_stacktop = get_kstack();
+
+       /* NMI handlers can't use swapgs for kernel TFs, so we need to bootstrap a
+        * bit.  We'll use a little bit of space above the actual NMI stacktop for
+        * storage for the pcpui pointer.  But we need to be careful: the HW will
+        * align RSP to 16 bytes on entry. */
+       nmi_entry_stacktop -= 16;
+       *(uintptr_t*)nmi_entry_stacktop = (uintptr_t)pcpui;
+       pcpui->tss->ts_ist1 = nmi_entry_stacktop;
+}
+
 /* Perform any initialization needed by per_cpu_info.  Make sure every core
  * calls this at some point in the smp_boot process.  If you don't smp_boot, you
  * must still call this for core 0.  This must NOT be called from smp_main,
@@ -320,6 +333,7 @@ void __arch_pcpu_init(uint32_t coreid)
        assert(read_msr(MSR_KERN_GS_BASE) == (uint64_t)pcpui);
        /* Don't try setting up til after setting GS */
        x86_sysenter_init(x86_get_stacktop_tss(pcpui->tss));
+       pcpu_init_nmi(pcpui);
        /* need to init perfctr before potentially using it in timer handler */
        perfmon_pcpu_init();
        vmm_pcpu_init();
index e23b9b0..69b8404 100644 (file)
@@ -148,6 +148,8 @@ void idt_init(void)
         * DPL 3 means this can be triggered by the int instruction */
        idt[T_SYSCALL].gd_dpl = 3;
        idt[T_BRKPT].gd_dpl = 3;
+       /* Send NMIs to their own stack (IST1 in every core's TSS) */
+       idt[T_NMI].gd_ist = 1;
 
        /* Set up our kernel stack when changing rings */
        /* Note: we want 16 byte aligned kernel stack frames (AMD 2:8.9.3) */
@@ -322,6 +324,49 @@ static bool __handle_page_fault(struct hw_trapframe *hw_tf, unsigned long *aux)
                return __handler_user_page_fault(hw_tf, fault_va, prot);
 }
 
+/* Separate handler from traps, since there's too many rules for NMI ctx.
+ *
+ * The general rule is that any writes from NMI context must be very careful.
+ * When talking about reads and writes to per-core data:
+ * - If NMIs write things written by normal kernel contexts, including IRQs and
+ *   traps with IRQs disabled, then you must use atomics on both sides.
+ * - If NMIs write things read by normal contexts, then readers must be careful,
+ *   since the data can change at will.
+ * - If NMIs read things written by normal contexts, don't worry: you're running
+ *   uninterrupted (given x86 NMI caveats).
+ * - We cannot block.  The current kthread thinks its stacktop is different than
+ *   the one we're on.  Just get in and get out.
+ * - If we interrupted a user TF, then we don't need to worry any more than for
+ *   normal traps/IRQs.
+ * - However, we cannot call proc_restartcore.  That could trigger all sorts of
+ *   things, like kthreads blocking.
+ * - Parallel accesses (from other cores) are the same as always.  You just
+ *   can't lock easily. */
+void handle_nmi(struct hw_trapframe *hw_tf)
+{
+       struct per_cpu_info *pcpui;
+
+       /* TODO: this is all racy and needs to go */
+
+       /* Temporarily disable deadlock detection when we print.  We could
+        * deadlock if we were printing when we NMIed. */
+       pcpui = &per_cpu_info[core_id()];
+       pcpui->__lock_checking_enabled--;
+       /* This is a bit hacky, but we don't have a decent API yet */
+       extern bool mon_verbose_trace;
+       if (mon_verbose_trace) {
+               print_trapframe(hw_tf);
+               backtrace_hwtf(hw_tf);
+       }
+       char *fn_name = get_fn_name(get_hwtf_pc(hw_tf));
+
+       printk("Core %d is at %p (%s)\n", core_id(), get_hwtf_pc(hw_tf),
+                  fn_name);
+       kfree(fn_name);
+       print_kmsgs(core_id());
+       pcpui->__lock_checking_enabled++;
+}
+
 /* Certain traps want IRQs enabled, such as the syscall.  Others can't handle
  * it, like the page fault handler.  Turn them on on a case-by-case basis. */
 static void trap_dispatch(struct hw_trapframe *hw_tf)
@@ -333,25 +378,6 @@ static void trap_dispatch(struct hw_trapframe *hw_tf)
 
        // Handle processor exceptions.
        switch(hw_tf->tf_trapno) {
-               case T_NMI:
-                       /* Temporarily disable deadlock detection when we print.  We could
-                        * deadlock if we were printing when we NMIed. */
-                       pcpui = &per_cpu_info[core_id()];
-                       pcpui->__lock_checking_enabled--;
-                       /* This is a bit hacky, but we don't have a decent API yet */
-                       extern bool mon_verbose_trace;
-                       if (mon_verbose_trace) {
-                               print_trapframe(hw_tf);
-                               backtrace_hwtf(hw_tf);
-                       }
-                       char *fn_name = get_fn_name(get_hwtf_pc(hw_tf));
-
-                       printk("Core %d is at %p (%s)\n", core_id(), get_hwtf_pc(hw_tf),
-                              fn_name);
-                       kfree(fn_name);
-                       print_kmsgs(core_id());
-                       pcpui->__lock_checking_enabled++;
-                       break;
                case T_BRKPT:
                        enable_irq();
                        monitor(hw_tf);
index 132f656..cd60889 100644 (file)
        .quad name;                                                     \
        .long num
 
+#define NMI_HANDLER(name, num)                 \
+       .text;                                                          \
+       .globl name;                                            \
+       .type name, @function;                          \
+       .align 2;                                                       \
+       name:                                                           \
+       pushq $0;                                                       \
+       pushq $(num);                                           \
+       jmp _nmi_entry;                                         \
+       .data;                                                          \
+       .quad name;                                                     \
+       .long num
+
 /* Only used in the kernel during SMP boot.  Send a LAPIC_EOI and iret. */
 #define POKE_HANDLER(name, num)                        \
        .text;                                                          \
@@ -89,8 +102,8 @@ trap_tbl:
 
 /* Generate entry points for the different traps.  Note that all of these bounce
  * off the corresponding trap.c function, such as handle_irqs, and that the name
- * e.g. ISR_NMI is soley for the little stup that jumps to something like
- * _alltraps.
+ * e.g. ISR_divide_error is soley for the little stup that jumps to something
+ * like _alltraps.
  *
  * Technically, these HANDLER entries do not need to be in numeric order.
  * trap.c will do a 'foreach (up to last-1), set the IDT for the number to point
@@ -98,7 +111,7 @@ trap_tbl:
  * one wins'. */
 TRAPHANDLER_NOEC(ISR_divide_error, T_DIVIDE)
 TRAPHANDLER_NOEC(ISR_debug_exceptions, T_DEBUG)
-TRAPHANDLER_NOEC(ISR_NMI, T_NMI)
+NMI_HANDLER(ISR_NMI, T_NMI)
 TRAPHANDLER_NOEC(ISR_breakpoint, T_BRKPT)
 TRAPHANDLER_NOEC(ISR_overflow, T_OFLOW)
 TRAPHANDLER_NOEC(ISR_bounds_check, T_BOUND)
@@ -456,6 +469,126 @@ irq_all_tf:
        addq $0x10, %rsp                        # skip trapno and err
        iretq
 
+# Unlike normal trap and IRQ handlers, both user and kernel TFs are handled the
+# similarly.  Both come in here and return from here.  We cannot do anything
+# fancy like proc_restartcore() from NMI context.
+#
+# All NMIs will come in fresh on the same stack (IST1, per-core).  We don't need
+# to find a stackpointer, but we do need to find GS.
+#
+# Regardless of whether or not the interrupted context was in the kernel, we
+# can't trust GS.  Basically we can never tell from looking at GS and KERN_GS
+# whether swapgs occurred, since the user could have put kernel addrs in their
+# GS.  But we can use the stack for storage to bootstrap GS.  %rsp at entry
+# points to pcpui *.
+#
+# We can also tell if GS was set correctly or not and avoid the wrmsr calls.
+# This isn't a huge deal.  But note that we don't know whether or not the kernel
+# actually set the gsbase.  We just know if it was correct or not.  The user
+# could have set it and the kernel hadn't had a chance to swapgs yet.  The NMI
+# handler doesn't care, since all TFs come in and go out via this asm.
+#
+# If we want to be paranoid, we can completely ignore user TFs and just save and
+# restore GS with the same mechanism we use for the kernel.  But since we came
+# in on a user TF, we can use swapgs.  Note that even for nested NMI handlers,
+# we have a user TF for only the first time, and only the last exit will swapgs
+# back to the way it was initially.
+_nmi_entry:
+       cld
+       pushq %r15
+       pushq %r14
+       pushq %r13
+       pushq %r12
+       pushq %r11
+       pushq %r10
+       pushq %r9
+       pushq %r8
+       pushq %rdi
+       pushq %rsi
+       pushq %rbp
+       pushq %rdx
+       pushq %rcx
+       pushq %rbx
+       pushq %rax
+       cmpw $GD_KT, 0x90(%rsp) # 0x90 - diff btw tf_cs and tf_rax
+       je nmi_kern_tf
+       # this is a user TF.  we need to swapgs to get the kernel's gs and mark the
+       # context as partial
+       swapgs                                  # user's GS is now in MSR_KERNEL_GS_BASE
+       movl $0x1, 0xac(%rsp)   # 0xac - diff btw tf_padding0 and tf_rax
+       pushq $0                                # fsbase space
+       pushq $0                                # gsbase space
+       jmp nmi_all_tf
+nmi_kern_tf:
+       # this is a kernel TF.  but we don't know if they set up gs yet, so we'll
+       # save and restore whatever they had loaded and use our own
+       pushq $0                                # fsbase space
+       # Get the current GS base into rax
+       movl $MSR_GS_BASE, %ecx
+       rdmsr
+       shlq $32, %rdx
+       orq %rdx, %rax
+       # Get the real GS base from the top of the stack.  This was set in smp_boot,
+       # and our rsp pointed to it when we entered the kernel.
+       movq 0xb8(%rsp), %rdx   # 0xb8 from fs_base to the top
+       # Compare them.  If they are the same, we can just push 0 for gsbase (which
+       # later will mean "no need to restore GS".
+       cmpq %rdx, %rax
+       je nmi_gs_ok
+       # They weren't the same.  Save the old one and set the new one.
+       pushq %rax                              # gsbase space
+       movq %rdx, %rax
+       shrq $32, %rdx
+       andl $0xffffffff, %eax
+       wrmsr
+       jmp nmi_all_tf
+nmi_gs_ok:
+       pushq $0                                # gsbase space
+nmi_all_tf:
+       # At this point, GS is set correctly, either due to swapgs (user TF), wrmsr
+       # (kern TF with bad GS), or it was already fine (and gsbase in the TF = 0).
+       movq $0, %rbp                   # so we can backtrace to this point
+       movq %rsp, %rdi
+       call handle_nmi
+       # Unlike in normal IRQs/Traps, both user and kernel contexts return via this
+       # path.
+       cmpw $GD_KT, 0xa0(%rsp) # 0xa0 - diff btw tf_cs and tf_gsbase
+       je nmi_kern_restore_gs
+       # User TF.  Restore whatever was there with swapgs.  We don't care what it
+       # was, nor do we care what was in the TF.
+       swapgs                                  # user's GS is now in MSR_GS_BASE
+       addq $0x10, %rsp                # skip gs/fs base
+       jmp nmi_popal
+nmi_kern_restore_gs:
+       popq %rax                               # fetch saved gsbase
+       addq $0x08, %rsp                # skip fs base
+       cmpq $0, %rax
+       je nmi_popal
+       # gsbase in the TF != 0, which means we need to restore that gsbase
+       movl $MSR_GS_BASE, %ecx
+       movq %rax, %rdx
+       shrq $32, %rdx
+       andl $0xffffffff, %eax
+       wrmsr
+nmi_popal:
+       popq %rax
+       popq %rbx
+       popq %rcx
+       popq %rdx
+       popq %rbp
+       popq %rsi
+       popq %rdi
+       popq %r8
+       popq %r9
+       popq %r10
+       popq %r11
+       popq %r12
+       popq %r13
+       popq %r14
+       popq %r15
+       addq $0x10, %rsp                        # skip trapno and err
+       iretq
+
 .globl sysenter_handler;
 .type sysenter_handler, @function;