From 38c89b1a9ad112e075afb5920d3e41a6df0f0176 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 25 Oct 2026 00:50:50 +0200 Subject: [PATCH] panic stops every other processor A panic used to stop only the processor it happened on. The rest carried on with a dead kernel: on the ASUS laptop the report flashed up and was drawn over, and the fans stayed loud until the power button was held. Now the panicking processor sends every other one an NMI, which arrives even with interrupts off, and trap() halts a processor that takes one while panic_stop is set -- first thing, before the canary check, since that stack may be what broke. NMI gets its own per-CPU stack (IST 1): for a few instructions at the top of syscall_entry the live stack is the user's, at ring 0, and a fault pushing the frame there would be a double fault on the same stack and a reset instead of a report. A second processor that panics while one is already reporting halts instead of drawing over it; the owner's own nested panic still gets through. Checked under QEMU with -smp 4: after a sysrq crash CPUs 1-3 sit in hlt with IF clear and CPU 0 is in the report's blink loop. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4 --- include/core/defs.h | 2 ++ kernel/arch/x86_64/ioperm.c | 9 +++++++++ kernel/arch/x86_64/lapic.c | 15 +++++++++++++++ kernel/arch/x86_64/trap.c | 13 +++++++++++++ kernel/core/panic.c | 18 ++++++++++++++++++ 5 files changed, 57 insertions(+) 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.