diff --git a/include/core/defs.h b/include/core/defs.h index c7cbd0c..e05d5fc 100644 --- a/include/core/defs.h +++ b/include/core/defs.h @@ -457,6 +457,8 @@ extern uint64 boot_epoch; int cpunum(void); extern volatile uint* lapic; void lapiceoi(void); +extern volatile int panic_stop; // kernel/core/panic.c +void lapic_nmi_others(void); void lapicinit(void); void lapicstartap(uchar, uint); void microdelay(int); diff --git a/kernel/arch/x86_64/ioperm.c b/kernel/arch/x86_64/ioperm.c index cf6d238..b27bcb4 100644 --- a/kernel/arch/x86_64/ioperm.c +++ b/kernel/arch/x86_64/ioperm.c @@ -56,6 +56,14 @@ struct tssblk { static struct tssblk tssblk[NCPU]; +// The stack an NMI arrives on: IST slot 1, one per CPU. Without it an NMI runs +// on whatever stack was live, and for a few instructions at the top of +// syscall_entry that is the user's -- at ring 0, so the CPU would push the +// trap frame there, and a fault on that push is a double fault on the same +// stack. Panic stops the other processors with an NMI; it must not be the +// thing that resets the machine instead of the report. +static uint8 nmistack[NCPU][4096] __attribute__((aligned(16))); + // What each CPU currently has open. Kept per CPU rather than read back from the // process, because switching *away* has to close what was opened and the // process that opened it may be part-way through exit() by then. @@ -89,6 +97,7 @@ ioperm_tss(int cpu, uint64 *limit) memset(b->map, 0xFF, sizeof(b->map)); b->tail = 0xFF; b->ts.iomb = (uint16)__builtin_offsetof(struct tssblk, map); + b->ts.ist[0] = (uint64)(nmistack[cpu] + sizeof(nmistack[cpu])); installed[cpu].n = 0; // The limit is the offset of the last valid byte, and it has to include the diff --git a/kernel/arch/x86_64/lapic.c b/kernel/arch/x86_64/lapic.c index 5830d05..2c06bd0 100644 --- a/kernel/arch/x86_64/lapic.c +++ b/kernel/arch/x86_64/lapic.c @@ -402,6 +402,21 @@ cpunum(void) panic("unknown apicid\n"); } +// An NMI to every processor but this one: how a panic stops the others. An +// NMI is delivered with interrupts off too, so it reaches a processor spinning +// on a lock with cli as surely as one running a program; trap() halts any that +// takes one while panic_stop is set. Bounded, because the caller is dying. +void +lapic_nmi_others(void) +{ + if (!lapic) + return; + lapicw(ICRHI, 0); + lapicw(ICRLO, 0x000C0000 | 0x00000400 | ASSERT); // all but self, NMI + for (int i = 0; i < 100000 && (lapic[ICRLO] & DELIVS); i++) + microdelay(1); +} + // Acknowledge interrupt. void lapiceoi(void) diff --git a/kernel/arch/x86_64/trap.c b/kernel/arch/x86_64/trap.c index 6c90fa4..46ecde5 100644 --- a/kernel/arch/x86_64/trap.c +++ b/kernel/arch/x86_64/trap.c @@ -48,6 +48,8 @@ void tvinit(void) for (n = 0; n < 256; n++) mkgate(idt, n, vectors[n], 0); mkgate(idt, T_SYSCALL, vectors[T_SYSCALL], 3); + // NMI on its own stack, IST slot 1; see nmistack in ioperm.c. + idt[T_NMI * 4 + 1] |= 1; } uint @@ -81,6 +83,17 @@ trapsig(uint64 trapno) void trap(struct trapframe *tf) { + // Another processor has panicked and wants this one stopped: a processor + // left running carries on with a kernel that is already dead, and on a + // laptop that showed as the report flashing up and being drawn over, and + // as fans that stayed loud until somebody held the power button. First, and + // before the canary check, because this stack may be what is broken. + if(tf->trapno == T_NMI && panic_stop) + for(;;){ + cli(); + hlt(); + } + kstack_check_canary("trap entry"); switch(tf->trapno){ case T_IRQ0 + IRQ_TIMER: diff --git a/kernel/core/panic.c b/kernel/core/panic.c index 1bf094b..cedd853 100644 --- a/kernel/core/panic.c +++ b/kernel/core/panic.c @@ -559,6 +559,9 @@ report_serial(char *reason, addr_t *pcs, int npcs) static void panic_blink(void) __attribute__((noreturn)); +volatile int panic_stop; +static volatile int panic_owner = -1; + __attribute__((noreturn)) void panic(char *s) @@ -566,6 +569,21 @@ panic(char *s) addr_t pcs[10]; cli(); + + // One processor reports; any other that panics meanwhile -- the same broken + // state tends to reach several at once -- stops where it is. The owner may + // panic again while drawing, and that nested report is let through. + int me = cpunum(); + int owner = __sync_val_compare_and_swap(&panic_owner, -1, me); + if (owner != -1 && owner != me) + for (;;) { + cli(); + hlt(); + } + // And the rest are stopped outright, see trap(). + panic_stop = 1; + lapic_nmi_others(); + // Stop every other path from writing to the console. consputc checks this // and spins, which is what keeps another CPU from painting over the report // while it is being drawn.