Painting every kernel stack at allocation and scanning it on each trap puts the
high-water mark at **3736 of 4096 bytes** -- 91%, in netd, inside lwIP's input
path. 360 bytes of headroom on a kernel that takes interrupts on the current
stack is not headroom: a timer landing at that depth pushes a trapframe and
runs trap() straight off the bottom of the page and into whatever kalloc handed
out next, which is very often another process's kernel stack.
The canary at kstack[0] did not catch this and could not have. It is checked on
trap entry and exit, and a frame allocated in one `sub $N, %rsp` steps clean
over the bottom eight bytes and writes below them without ever touching it. It
catches a slow walk past the end, not one deep call.
8192 now, and the stacks are static -- one per ptable slot, NPROC * KSTACKSIZE
= 1 MB of BSS. Static rather than a bigger kalloc because kalloc() hands out
exactly one page and there is no contiguous multi-page allocator to ask. A
process's slot is fixed for its lifetime, so the stack belonging to that slot is
the natural answer: no allocation to fail, no free to forget, and the kalloc
failure path in allocproc() goes away with it. The page was being paid for out
of kalloc before, one at a time, and undercounted.
**And the AP scheduler stack was already wrong.** startothers() did
`stack = kalloc()` for one page and then handed the AP `stack + KSTACKSIZE` as
its stack top. Exactly right for as long as KSTACKSIZE happened to equal
PGSIZE, and a whole page past the allocation the moment it did not -- which
this commit would have made true. That is not a throwaway boot stack either:
the AP runs mpmain() and then scheduler() on it for the rest of the machine's
life, and takes interrupts on it whenever no process is current. Static too,
NCPU of them.
**This does not fix the intermittent zsh crash**, and the measurement above was
taken while looking for it. It is worth saying plainly: 1-3 shells per boot
still die, so the hypothesis was wrong and the stack was a separate latent bug
that the search turned up.
What the search did establish is worth more than the guess was:
- **It is SMP-only.** Three boots at -smp 1: zero faults. Three at -smp 2:
one, three, one.
- **It is not only zsh.** schinitd took one too.
- **The faults name their cause if you print enough.** `rip 0x0` is a process
handed a zero and following it, not a process that branched anywhere. And
schinitd's `addr 0xffffffffffffffea` is -22, which is -EINVAL: a call
returned an error and the caller used it as a pointer.
So trap.c dumps registers, segments, and the top of the user stack for a fault
at rip below a page or a faulting address near zero, the way it already did for
a GP fault. Neither of the two facts above was visible from the one-line
report, and both came out of the first dump that included them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
204 lines
6.9 KiB
C
204 lines
6.9 KiB
C
#include "types.h"
|
|
#include "defs.h"
|
|
#include "irq.h"
|
|
#include "param.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "proc.h"
|
|
#include "x86.h"
|
|
#include "traps.h"
|
|
#include "spinlock.h"
|
|
#include "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);
|
|
}
|
|
|
|
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)
|
|
{
|
|
kstack_check_canary("trap entry");
|
|
switch(tf->trapno){
|
|
case T_IRQ0 + IRQ_TIMER:
|
|
if(cpunum() == 0){
|
|
acquire(&tickslock);
|
|
ticks++;
|
|
wakeup(&ticks);
|
|
release(&tickslock);
|
|
}
|
|
// 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/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){
|
|
// 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));
|
|
|
|
if(myproc() && myproc()->state == RUNNING && tf->trapno == T_IRQ0+IRQ_TIMER)
|
|
yield();
|
|
|
|
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
|
|
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
|
|
|
|
kstack_check_canary("trap exit");
|
|
}
|