x86: Prevent NMIs from nesting
authorBarret Rhoden <brho@cs.berkeley.edu>
Tue, 19 Jul 2016 19:38:38 +0000 (15:38 -0400)
committerBarret Rhoden <brho@cs.berkeley.edu>
Fri, 29 Jul 2016 21:43:07 +0000 (17:43 -0400)
If an NMI faults for any reason and then does an iret, the iret will clear
whatever protection hardware offers that prevents another NMI from nesting.
Basically, hardware will delay future NMIs until an iret.  The fault
handler's iret counts.

We get around this by having two stacks for NMIs.  One is for the real NMI
entry.  The second (the one in pcpui) is for the work done in NMI context.
This is the work that could get interrupted.  In that case, the NMI handler
makes sure the worker will do_nmi_work() again.

The main NMI handler and the bottom half work together such that a
concurrent NMI triggers a repeat of do_nmi_work().  The trickiness comes
when the bottom half tries to exit (by popping back to the original TF) and
an NMI happens at the same time.

The most error-prone and least-used part of this is how we handle that
scenario: the NMI handler notices the bottom half was trying to exit and
moves it to an alternate section of code that will return instead of
popping.  We can get away with this because there is a single instruction
that commits the "pop" of the TF and leaves the worker stack: iret.  It's
sort of like a restartable sequence, but instead of restarting, we undo the
operation.  That was relatively simple.  The harder part is writing the
nmi_try_to_pop, which is basically another NMI entry and exit under
slightly different circumstances.

I tested this a little.  I put a few direct jumps from within the OK case
to within the FAIL case.  I also put a "1: jmp 1b" loop in the OK case and
two nops in the FAIL case, added a breakpoint to do_nmi_work, and sent
another NMI to trigger the TF-hacking code.  Seems to work, for now.

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
kern/include/smp.h

index cf22011..0a18695 100644 (file)
@@ -279,6 +279,10 @@ static void pcpu_init_nmi(struct per_cpu_info *pcpui)
        nmi_entry_stacktop -= 16;
        *(uintptr_t*)nmi_entry_stacktop = (uintptr_t)pcpui;
        pcpui->tss->ts_ist1 = nmi_entry_stacktop;
+       /* Our actual NMI work is done on yet another stack, to avoid the "iret
+        * cancelling NMI protections" problem.  All problems can be solved with
+        * another layer of indirection! */
+       pcpui->nmi_worker_stacktop = get_kstack();
 }
 
 /* Perform any initialization needed by per_cpu_info.  Make sure every core
index 69b8404..3b44f95 100644 (file)
 #include <arch/mptables.h>
 #include <ros/procinfo.h>
 
+enum {
+       NMI_NORMAL_OPN = 0,
+       NMI_IN_PROGRESS,
+       NMI_HANDLE_ANOTHER,
+};
+
 taskstate_t ts;
 
 /* Interrupt descriptor table.  64 bit needs 16 byte alignment (i think). */
@@ -324,6 +330,112 @@ static bool __handle_page_fault(struct hw_trapframe *hw_tf, unsigned long *aux)
                return __handler_user_page_fault(hw_tf, fault_va, prot);
 }
 
+/* Actual body of work done when an NMI arrives */
+static void do_nmi_work(struct hw_trapframe *hw_tf)
+{
+       /* TODO: this is all racy and needs to go */
+       struct per_cpu_info *pcpui = &per_cpu_info[core_id()];
+       char *fn_name;
+       /* This is a bit hacky, but we don't have a decent API yet */
+       extern bool mon_verbose_trace;
+
+       /* Temporarily disable deadlock detection when we print.  We could
+        * deadlock if we were printing when we NMIed. */
+       pcpui->__lock_checking_enabled--;
+       if (mon_verbose_trace) {
+               print_trapframe(hw_tf);
+               backtrace_hwtf(hw_tf);
+       }
+       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++;
+}
+
+/* NMI HW_TF hacking involves four symbols:
+ *
+ * [__nmi_pop_ok_start, __nmi_pop_ok_end) mark the beginning and end of the
+ * code for an nmi popping routine that will actually pop at the end.
+ *
+ * [__nmi_pop_fail_start, __nmi_pop_fail_end) mark the beginning and end of the
+ * shadow code for an nmi popping routine that will fail at the end.
+ *
+ * If we see a TF in the OK section, we'll move it to the FAIL section.  If it's
+ * already in the FAIL section, we'll report that as a success. */
+extern char __nmi_pop_ok_start[], __nmi_pop_ok_end[];
+extern char __nmi_pop_fail_start[], __nmi_pop_fail_end[];
+
+static bool nmi_hw_tf_needs_hacked(struct hw_trapframe *hw_tf)
+{
+       return ((uintptr_t)__nmi_pop_ok_start <= hw_tf->tf_rip) &&
+              (hw_tf->tf_rip < (uintptr_t)__nmi_pop_ok_end);
+}
+
+static bool nmi_hw_tf_was_hacked(struct hw_trapframe *hw_tf)
+{
+       return ((uintptr_t)__nmi_pop_fail_start <= hw_tf->tf_rip) &&
+              (hw_tf->tf_rip < (uintptr_t)__nmi_pop_fail_end);
+}
+
+/* Helper.  Hacks the TF if it was in the OK section so that it is at the same
+ * spot in the FAIL section.  Returns TRUE if the TF is hacked, meaning the NMI
+ * handler can just return. */
+static bool nmi_check_and_hack_tf(struct hw_trapframe *hw_tf)
+{
+       uintptr_t offset;
+
+       if (!nmi_hw_tf_needs_hacked(hw_tf))
+               return FALSE;
+       if (nmi_hw_tf_was_hacked(hw_tf))
+               return TRUE;
+       offset = hw_tf->tf_rip - (uintptr_t)__nmi_pop_ok_start;
+       hw_tf->tf_rip = (uintptr_t)__nmi_pop_fail_start + offset;
+       return TRUE;
+}
+
+/* Bottom half of the NMI handler.  This can be interrupted under some
+ * circumstances by NMIs.  It exits by popping the hw_tf in assembly. */
+void __attribute__((noinline, noreturn))
+__nmi_bottom_half(struct hw_trapframe *hw_tf)
+{
+       struct per_cpu_info *pcpui = &per_cpu_info[core_id()];
+
+       while (1) {
+               /* Signal that we're doing work.  A concurrent NMI will set this to
+                * NMI_HANDLE_ANOTHER if we should continue, which we'll catch later. */
+               pcpui->nmi_status = NMI_IN_PROGRESS;
+               do_nmi_work(hw_tf);
+               /* We need to check nmi_status to see if it is NMI_HANDLE_ANOTHER (if
+                * so, run again), write NMI_NORMAL_OPN, leave this stack, and return to
+                * the original context.  We need to do that in such a manner that an
+                * NMI can come in at any time.  There are two concerns.
+                *
+                * First, we need to not "miss the signal" telling us to re-run the NMI
+                * handler.  To do that, we'll do the actual checking in asm.  Being in
+                * the asm code block is a signal to the real NMI handler that we need
+                * to abort and do_nmi_work() again.
+                *
+                * Second, we need to atomically leave the stack and return.  By being
+                * in asm, the NMI handler knows to just hack our PC to make us return,
+                * instead of starting up a fresh __nmi_bottom_half().
+                *
+                * The NMI handler works together with the following function such that
+                * if that race occurs while we're in the function, it'll fail and
+                * return.  Then we'll just do_nmi_work() and try again. */
+               extern void nmi_try_to_pop(struct hw_trapframe *tf, int *status,
+                                          int old_val, int new_val);
+
+               nmi_try_to_pop(hw_tf, &pcpui->nmi_status, NMI_IN_PROGRESS,
+                              NMI_NORMAL_OPN);
+               /* Either we returned on our own, since we lost a race with nmi_status
+                * and didn't write (status = ANOTHER), or we won the race, but an NMI
+                * handler set the status to ANOTHER and restarted us. */
+               assert(pcpui->nmi_status != NMI_NORMAL_OPN);
+       }
+}
+
 /* 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.
@@ -341,30 +453,92 @@ static bool __handle_page_fault(struct hw_trapframe *hw_tf, unsigned long *aux)
  * - 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. */
+ *   can't lock easily.
+ *
+ * Normally, once you're in NMI, other NMIs are blocked until we return.
+ * However, if our NMI handler faults (PF, GPF, breakpoint) due to something
+ * like tracing, the iret from that fault will cancel our NMI protections.  Thus
+ * we need another layer of code to make sure we don't run the NMI handler
+ * concurrently on the same core.  See https://lwn.net/Articles/484932/ for more
+ * info.
+ *
+ * We'll get around the problem by running on yet another NMI stack.  All NMIs
+ * come in on the nmi entry stack (tss->ist1).  While we're on that stack, we
+ * will not be interrupted.  We jump to another stack to do_nmi_work.  That code
+ * can be interrupted, but we are careful to only have one 'thread' running on
+ * that stack at a time.  We do this by carefully hopping off the stack in
+ * assembly, similar to popping user TFs. */
 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);
+       struct per_cpu_info *pcpui = &per_cpu_info[core_id()];
+       struct hw_trapframe *hw_tf_copy;
+       uintptr_t worker_stacktop;
+
+       /* At this point, we're an NMI and other NMIs are blocked.  Only once we
+        * hop to the bottom half could that be no longer true.  NMI with NMIs fully
+        * blocked will run without interruption.  For that reason, we don't have to
+        * be careful about any memory accesses or compiler tricks. */
+       if (pcpui->nmi_status == NMI_HANDLE_ANOTHER)
+               return;
+       if (pcpui->nmi_status == NMI_IN_PROGRESS) {
+               /* Force the handler to run again.  We don't need to worry about
+                * concurrent access here.  We're running, they are not.  We cannot
+                * 'PAUSE' since NMIs are fully blocked.
+                *
+                * The asm routine, for its part, does a compare-and-swap, so if we
+                * happened to interrupt it before it wrote NMI_NORMAL_OPN, it'll
+                * notice, abort, and not write the status. */
+               pcpui->nmi_status = NMI_HANDLE_ANOTHER;
+               return;
        }
-       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++;
+       assert(pcpui->nmi_status == NMI_NORMAL_OPN);
+       pcpui->nmi_status = NMI_HANDLE_ANOTHER;
+       /* We could be interrupting an NMI that is trying to pop back to a normal
+        * context.  We can tell by looking at its PC.  If it is within the popping
+        * routine, then we interrupted it at this bad time.  We'll hack the TF such
+        * that it will return instead of succeeding. */
+       if (nmi_check_and_hack_tf(hw_tf))
+               return;
+       /* OK, so we didn't interrupt an NMI that was trying to return.  So we need
+        * to run the bottom half.  We're going to jump stacks, but we also need to
+        * copy the hw_tf.  The existing one will be clobbered by any interrupting
+        * NMIs.
+        *
+        * We also need to save some space on the top of that stack for a pointer to
+        * pcpui and a scratch register, which nmi_try_to_pop() will use.  The
+        * target stack will look like this:
+        *
+        *               +--------------------------+ Page boundary (e.g. 0x6000)
+        *               |   scratch space (rsp)    |
+        *               |       pcpui pointer      |
+        *               |      tf_ss + padding     | HW_TF end
+        *               |          tf_rsp          |
+        *               |            .             |
+        *               |            .             |
+        * RSP ->        |         tf_gsbase        | HW_TF start, hw_tf_copy
+        *               +--------------------------+
+        *               |            .             |
+        *               |            .             |
+        *               |            .             |
+        *               +--------------------------+ Page boundary (e.g. 0x5000)
+        *
+        * __nmi_bottom_half() just picks up using the stack below tf_gsbase.  It'll
+        * push as needed, growing down.  Basically we're just using the space
+        * 'above' the stack as storage. */
+       worker_stacktop = pcpui->nmi_worker_stacktop - 2 * sizeof(uintptr_t);
+       *(uintptr_t*)worker_stacktop = (uintptr_t)pcpui;
+       worker_stacktop = worker_stacktop - sizeof(struct hw_trapframe);
+       hw_tf_copy = (struct hw_trapframe*)worker_stacktop;
+       *hw_tf_copy = *hw_tf;
+       /* Once we head to the bottom half, consider ourselves interruptible (though
+        * it's not until the first time we do_nmi_work()).  We'll never come back
+        * to this stack.  Doing this in asm so we can easily pass an argument.  We
+        * don't need to call (vs jmp), but it helps keep the stack aligned. */
+       asm volatile("mov $0x0, %%rbp;"
+                    "mov %0, %%rsp;"
+                    "call __nmi_bottom_half;"
+                    : : "r"(worker_stacktop), "D"(hw_tf_copy));
+       assert(0);
 }
 
 /* Certain traps want IRQs enabled, such as the syscall.  Others can't handle
@@ -702,6 +876,7 @@ void send_ipi(uint32_t os_coreid, uint8_t vector)
                panic("Unmapped OS coreid (OS %d)!\n", os_coreid);
                return;
        }
+       assert(vector != T_NMI);
        __send_ipi(hw_coreid, vector);
 }
 
index cd60889..0128331 100644 (file)
@@ -589,6 +589,218 @@ nmi_popal:
        addq $0x10, %rsp                        # skip trapno and err
        iretq
 
+.globl __nmi_pop_ok_start;
+.globl __nmi_pop_ok_end;
+.globl __nmi_pop_fail_start;
+.globl __nmi_pop_fail_end;
+
+# extern void nmi_try_to_pop(struct hw_trapframe *tf, int *status,
+#                            int old_val, int new_val);
+#
+# __nmi_bottom_half calls this to atomically pop a hw_tf (%rdi) and set
+# &pcpui->nmi_status (%rsi) with compare and swap to NMI_NORMAL_OPN (%ecx) given
+# that it was NMI_IN_PROGRESS (%edx)
+#
+# (Careful, nmi_status is an int, not a long.)
+#
+# If the real NMI handler interrupts us, it'll move us to the fail section of
+# the code.  That code is identical to 'ok', up until ok's final statement.
+#
+# In that event, we'll need a little help returning: specifically to bootstrap
+# pcpui and our current stackpointer.  pcpui is already saved near the top of
+# stack.  We'll save rsp ourselves.
+.globl nmi_try_to_pop;
+.type nmi_try_to_pop, @function;
+nmi_try_to_pop:
+__nmi_pop_ok_start:
+       # careful only to use caller-saved or argument registers before saving
+       movl %edx, %eax                 # load old_val into eax for the CAS
+       cmpxchgl %ecx, (%rsi)   # no need for LOCK, since an NMI would serialize
+       jz nmi_ok_cas_worked    # ZF = 1 on successful CAS
+       ret
+nmi_ok_cas_worked:
+       # save callee-saved regs (the pops below clobber them, and we might return)
+       pushq %rbp
+       pushq %rbx
+       pushq %r12
+       pushq %r13
+       pushq %r14
+       pushq %r15
+       # We need to save the current rsp into the scratch space at the top of the
+       # stack.  This assumes we're within the top page of our stack, which should
+       # always be true.  Careful not to use rdi, which still has an argument.
+       movq %rsp, %rbx
+       # Want to round rbx up to PGSIZE, then subtract 8, to get our slot.
+       movq $0xfff, %rax
+       notq %rax                               # rax = 0xfffffffffffff000
+       andq %rax, %rbx                 # round down rbx
+       addq $0x1000, %rbx              # add PGSIZE, assuming rsp was not page aligned
+       subq $0x8, %rbx                 # point to the scratch space
+       movq %rsp, (%rbx)               # save rsp in the scratch space
+       # We jump our rsp to the base of the HW_TF.  This is still on the same
+       # stack, just farther back than where our caller is.  We need to be careful
+       # to not clobber the stack.  Otherwise we'll have chaos.
+       movq %rdi, %rsp
+       # From here down is the same as the normal NMI exit path, but with 'ok' in
+       # the symbol names.
+       cmpw $GD_KT, 0xa0(%rsp) # 0xa0 - diff btw tf_cs and tf_gsbase
+       je nmi_ok_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_ok_popal
+nmi_ok_kern_restore_gs:
+       popq %rax                               # fetch saved gsbase
+       addq $0x08, %rsp                # skip fs base
+       cmpq $0, %rax
+       je nmi_ok_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_ok_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
+__nmi_pop_ok_end:
+
+# This is the 'fail' case.  It is identical to the 'ok' case, up until the
+# iretq, other than 'ok' replaced with 'fail'.  In place of iretq, we undo the
+# entire operation.
+__nmi_pop_fail_start:
+       # careful only to use caller-saved or argument registers before saving
+       movl %edx, %eax                 # load old_val into eax for the CAS
+       cmpxchgl %ecx, (%rsi)   # no need for LOCK, since an NMI would serialize
+       jz nmi_fail_cas_worked  # ZF = 1 on successful CAS
+       ret
+nmi_fail_cas_worked:
+       # save callee-saved regs (the pops below clobber them, and we might return)
+       pushq %rbp
+       pushq %rbx
+       pushq %r12
+       pushq %r13
+       pushq %r14
+       pushq %r15
+       # We need to save the current rsp into the scratch space at the top of the
+       # stack.  This assumes we're within the top page of our stack, which should
+       # always be true.  Careful not to use rdi, which still has an argument.
+       movq %rsp, %rbx
+       # Want to round rbx up to PGSIZE, then subtract 8, to get our slot.
+       movq $0xfff, %rax
+       notq %rax                               # rax = 0xfffffffffffff000
+       andq %rax, %rbx                 # round down rbx
+       addq $0x1000, %rbx              # add PGSIZE, assuming rsp was not page aligned
+       subq $0x8, %rbx                 # point to the scratch space
+       movq %rsp, (%rbx)               # save rsp in the scratch space
+       # We jump our rsp to the base of the HW_TF.  This is still on the same
+       # stack, just farther back than where our caller is.  We need to be careful
+       # to not clobber the stack.  Otherwise we'll have chaos.
+       movq %rdi, %rsp
+       # From here down is the same as the normal NMI exit path and the ok path,
+       # but with 'fail' in the symbol names.
+       cmpw $GD_KT, 0xa0(%rsp) # 0xa0 - diff btw tf_cs and tf_gsbase
+       je nmi_fail_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_fail_popal
+nmi_fail_kern_restore_gs:
+       popq %rax                               # fetch saved gsbase
+       addq $0x08, %rsp                # skip fs base
+       cmpq $0, %rax
+       je nmi_fail_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_fail_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
+       # Here's is where we differ from OK.  Time to undo everything and return
+       # rsp currently is pointing at tf->tf_rip.  Remember that we don't want to
+       # write anything to the stack - everything in the TF is still the way it was
+       # when we started to pop.
+       #
+       # First off, let's get the stack addr of the pcpui pointer loaded
+       movq %rsp, %rbx
+       movq $0xfff, %rax
+       notq %rax                               # rax = 0xfffffffffffff000
+       andq %rax, %rbx                 # round down rbx
+       addq $0x1000, %rbx              # add PGSIZE, assuming rsp was not page aligned
+       subq $0x10, %rbx                # point to the pcpui pointer
+       # Now let's start to unwind
+       subq $0x98, %rsp                # jump from rip to tf_gsbase (top of hw_tf)
+       # Need to restore gs, just like on an NMI entry
+       cmpw $GD_KT, 0xa0(%rsp) # 0xa0 - diff btw tf_cs and tf_gsbase
+       je nmi_pop_fail_kern_tf
+       # This is a user TF.  We need to swapgs to get the kernel's gs
+       # We don't need to mark the context as partial (we never do for NMIs,
+       # actually), and in general, we don't want to write anything on the stack.
+       swapgs                                  # user's GS is now in MSR_KERNEL_GS_BASE
+       jmp nmi_pop_fail_all_tf
+nmi_pop_fail_kern_tf:
+       # Kernel TF.  We basically need to do the same thing on entry, since we
+       # might have restored some weird GS base.  We can tell based on tf_gsbase
+       # 0 for gsbase means we didn't need to change GS
+       cmpq $0, (%rsp)
+       je nmi_pop_fail_gs_fine
+       # rbx points to where pcpui* is stored
+       mov (%rbx), %rdx
+       movl $MSR_GS_BASE, %ecx
+       movq %rdx, %rax
+       shrq $32, %rdx
+       andl $0xffffffff, %eax
+       wrmsr
+nmi_pop_fail_gs_fine:
+nmi_pop_fail_all_tf:
+       addq $0x8, %rbx                 # move to the scratch slot, holding rsp
+       mov (%rbx), %rsp
+       # restore callee-saved regs
+       popq %r15
+       popq %r14
+       popq %r13
+       popq %r12
+       popq %rbx
+       popq %rbp
+       ret
+       # sweet jeebus.
+__nmi_pop_fail_end:
+
+
 .globl sysenter_handler;
 .type sysenter_handler, @function;
 
index 8c78229..ddb4f7c 100644 (file)
@@ -37,6 +37,8 @@ struct per_cpu_info {
 #ifdef CONFIG_X86
        uintptr_t stacktop;                     /* must be first */
        int coreid;                                     /* must be second */
+       int nmi_status;
+       uintptr_t nmi_worker_stacktop;
        int vmx_enabled;
        int guest_pcoreid;
 #endif