kstack: 3736 of 4096 bytes, measured, and the AP got a page it did not own
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>
This commit is contained in:
parent
370605452c
commit
d58b1cda17
4 changed files with 84 additions and 18 deletions
|
|
@ -160,9 +160,31 @@ trap(struct trapframe *tf)
|
|||
// 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.
|
||||
if (tf->trapno == T_GPFLT) {
|
||||
// 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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue