SchOS/kernel/arch/x86_64/trap.c
Nexoniarz 38c89b1a9a 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 00:50:50 +02:00

257 lines
9.8 KiB
C

#include "core/types.h"
#include "core/defs.h"
#include "core/irq.h"
#include "core/param.h"
#include "arch/memlayout.h"
#include "arch/mmu.h"
#include "core/proc.h"
#include "arch/x86.h"
#include "arch/traps.h"
#include "core/spinlock.h"
#include "uapi/posix.h"
uint *idt;
extern addr_t vectors[];
struct spinlock tickslock;
uint ticks;
static void
mkgate(uint *idt, uint n, addr_t kva, uint pl)
{
uint64 addr = (uint64) kva;
n *= 4;
idt[n+0] = (addr & 0xFFFF) | (KERNEL_CS << 16);
idt[n+1] = (addr & 0xFFFF0000) | 0x8E00 | ((pl & 3) << 13);
idt[n+2] = addr >> 32;
idt[n+3] = 0;
}
void idtinit(void)
{
struct {
ushort limit;
uint64 base;
} __attribute__((packed)) idtdesc;
idtdesc.limit = PGSIZE - 1;
idtdesc.base = (uint64)idt;
asm volatile("lidt %0" : : "m" (idtdesc));
}
void tvinit(void)
{
int n;
idt = (uint*) kalloc();
memset(idt, 0, PGSIZE);
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
uptime(void)
{
uint xticks;
acquire(&tickslock);
xticks = ticks;
release(&tickslock);
return xticks;
}
// Which signal a CPU fault corresponds to, so a process killed by a fault is
// reported to wait() as died-by-signal with a sensible cause rather than a
// blanket SIGKILL.
static int
trapsig(uint64 trapno)
{
switch(trapno){
case T_DIVIDE: return SIGFPE;
case T_ILLOP: return SIGILL;
case T_GPFLT: return SIGSEGV;
case T_PGFLT: return SIGSEGV;
case T_BRKPT: return SIGTRAP;
case T_DEBUG: return SIGTRAP;
default: return SIGSEGV;
}
}
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:
if(cpunum() == 0){
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
// Charge the tick to whoever it interrupted. Every CPU does this, not just
// CPU 0 -- the global clock advances once, but the *work* happened on
// whichever processor was running that process.
//
// The split is by the interrupted privilege level, because that is the
// only thing the tick knows about what it interrupted. It is sampling, not
// measurement, which is what Linux does too and why these are ticks rather
// than microseconds.
//
// **stime is therefore almost always zero here, and that is structural.**
// MSR_SFMASK includes FL_IF (see syscallinit), so SYSCALL clears interrupts
// and nothing re-enables them for the duration -- a timer tick cannot land
// while a process is executing a syscall. The only ticks that ever reach
// the `else` are ones that arrive in a nested trap. Charging real system
// time needs TSC deltas closed and reopened around every sched(), which is
// scheduler accounting rather than sampling, and is not written.
if(myproc()){
if((tf->cs & 3) == DPL_USER) myproc()->utime++;
else myproc()->stime++;
}
// Debug registers are per-CPU, and there is no general IPI here, so this
// is how a watchpoint armed on one processor reaches the others: each
// notices the table's generation has moved and reloads. One comparison
// per tick when nothing has changed. See kernel/arch/x86_64/watch.c.
watch_sync();
lapiceoi();
break;
case T_DEBUG:
// A hardware watchpoint fired. It is a *trap*, not a fault -- the store
// has already happened -- so reporting and returning carries on with the
// program none the wiser, which is what lets this be used on a machine
// that is still running.
if(watch_trap(tf))
break;
// Not ours: a single-step, or a #DB from somewhere else. Falls through to
// the unexpected-trap path rather than being swallowed.
goto unexpected;
case T_IRQ0 + IRQ_SPURIOUS:
cprintf("cpu%d: spurious interrupt at %p:%p\n",
cpunum(), tf->cs, tf->rip);
lapiceoi();
break;
default:
// Every other IOAPIC pin goes through the registration table. The device
// interrupts used to be listed here one case at a time, which cannot work
// for anything found on the PCI bus: a driver does not know its line
// until it reads the device's config space at probe time, so there is no
// constant to write a case label for. See include/core/irq.h.
if(tf->trapno >= T_IRQ0 && tf->trapno < T_IRQ0 + NIRQ){
irq_dispatch(tf->trapno - T_IRQ0);
lapiceoi();
break;
}
unexpected:
if(myproc() == 0 || (tf->cs&3) == 0){
// A kernel-mode trap ends in panic() a dozen lines down, so the console
// can be put into its panic mode *now* -- and it has to be, because the
// fault may have happened inside the console itself.
//
// cprintf() takes cons.lock. If the faulting code was already holding
// it -- anything under console output, which on a verbose boot is most
// of the kernel -- then this report deadlocks on a lock its own call
// chain owns, acquire() panics with "acquire", and the trap that
// actually happened is never printed. That is a bad trade at the best
// of times and a worse one here: the whole point of the report is to
// name the fault.
cons_panic_mode();
// The rip is named where it can be. This is a kernel address, so
// ksym_lookup() has something to say about it -- and the whole reason
// docs/architecture/debugging.md puts D0 first is that "rip ffff8000001294c2"
// costs an objdump session and "rip in dma_alloc+0x8c" does not.
char where[72];
ksym_fmt(where, sizeof(where), tf->rip);
cprintf("unexpected trap %d from cpu %d rip %p <%s> (cr2=0x%p) err=0x%p\n",
tf->trapno, cpunum(), tf->rip, where, rcr2(), tf->err);
if (myproc())
cprintf("proc id: %d\n", myproc()->pid);
if (tf->trapno == T_GPFLT) {
uint64 *iretframe = (uint64*)tf->rsp;
cprintf("iretq frame @ %p: rip=%p cs=%p rflags=%p rsp=%p ss=%p\n",
iretframe, iretframe[0], iretframe[1], iretframe[2],
iretframe[3], iretframe[4]);
if (myproc())
cprintf("this tf=%p myproc()->tf=%p myproc()->kstack=%p (tf-kstack=%p)\n",
tf, myproc()->tf, myproc()->kstack, (uint64)tf - (uint64)myproc()->kstack);
}
panic("trap");
}
cprintf("pid %d %s: trap %d err %d on cpu %d "
"rip 0x%p addr 0x%p--kill proc\n",
myproc()->pid, myproc()->name, tf->trapno, tf->err, cpunum(), tf->rip,
rcr2());
// General-purpose register dump. This used to be followed by ~25 lines
// that walked musl's mallocng group/meta structures out of rax/rdi and
// dumped the user stack -- scaffolding for the boot-crash investigation
// recorded in docs/history/kernel.md section 1. That
// investigation is closed and the underlying kernel bugs are fixed, so
// the allocator-specific part is gone; the registers stay, since they are
// useful for any fault.
// The full picture, for the two faults that mean something went wrong
// *elsewhere* rather than here: a general protection fault, and a jump to
// an address no code is ever linked at.
//
// The second case earns its own dump because of what it catches. A process
// executing at rip 0 did not branch there; it was handed a zero and
// followed it, and the registers say which one. The same dump on a
// *faulting address* below a page caught `addr 0xffffffffffffffea` in
// schinitd -- which is -22, which is -EINVAL: a call returned an error and
// the caller used it as a pointer. Neither of those is visible from the
// one-line report.
if (tf->trapno == T_GPFLT || tf->rip < PGSIZE || rcr2() + PGSIZE < PGSIZE * 2) {
cprintf(" rax=%p rbx=%p rcx=%p rdx=%p rsi=%p rdi=%p rbp=%p rsp=%p\n",
tf->rax, tf->rbx, tf->rcx, tf->rdx, tf->rsi, tf->rdi, tf->rbp, tf->rsp);
cprintf(" cs=%p ss=%p rflags=%p r11=%p tf=%p kstack=%p sz=%p\n",
tf->cs, tf->ss, tf->rflags, tf->r11, tf,
myproc()->kstack, myproc()->sz);
// The top of the user stack -- whatever a `ret` was about to use.
cprintf(" ustack:");
for (int i = -2; i < 4; i++) {
addr_t v;
if (fetchaddr(tf->rsp + i * 8, &v) == 0) cprintf(" %p", v);
else cprintf(" ????");
}
cprintf("\n");
}
myproc()->killed = 1;
if(myproc()->killsig == 0)
myproc()->killsig = trapsig(tf->trapno);
}
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
// The timer tick is when preemption is *considered*, not when it happens.
//
// This used to yield unconditionally, so a process alone on a CPU paid a
// full context switch -- switchuvm, a %cr3 reload, a flushed TLB -- a
// hundred times a second only to be handed straight back to itself.
// sched_slice_expired() asks whether it has actually had its share; see the
// scheduler in kernel/proc/proc.c.
if(myproc() && myproc()->state == RUNNING && tf->trapno == T_IRQ0+IRQ_TIMER
&& sched_slice_expired())
yield();
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
kstack_check_canary("trap exit");
}