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
174 lines
5.8 KiB
C
174 lines
5.8 KiB
C
// The TSS I/O permission bitmap. See include/arch/ioperm.h for why.
|
|
|
|
#include "core/types.h"
|
|
#include "core/defs.h"
|
|
#include "core/param.h"
|
|
#include "arch/mmu.h"
|
|
#include "core/proc.h"
|
|
#include "arch/ioperm.h"
|
|
|
|
// The 64-bit TSS, written out rather than indexed.
|
|
//
|
|
// The old code reached it as `uint *tss` and set `tss[16]` to put 0x68 in the
|
|
// I/O map base -- but the I/O map base is at offset 102, which is the high half
|
|
// of the dword at index 25, not 16. Index 16 is offset 64, in the middle of the
|
|
// interrupt stack table.
|
|
//
|
|
// That was not harmless. The field it meant to write stayed 0, and a base of 0
|
|
// with a limit of 0x67 does not mean "no bitmap" -- it means the bitmap starts
|
|
// at the top of the TSS, so the CPU checked ports 0-831 against the TSS's own
|
|
// bytes. Those bytes are RSP0: a kernel stack pointer, whose zero bits are
|
|
// *permission*. Some low ports were reachable from user mode and had been
|
|
// since the kernel was 64-bit, which nothing had any reason to notice.
|
|
//
|
|
// Naming the fields is what makes that visible, so they are named.
|
|
struct taskstate64 {
|
|
uint32 res0;
|
|
uint64 rsp0;
|
|
uint64 rsp1;
|
|
uint64 rsp2;
|
|
uint64 res1;
|
|
uint64 ist[7];
|
|
uint64 res2;
|
|
uint16 res3;
|
|
uint16 iomb; // offset from the base of the TSS
|
|
} __attribute__((packed));
|
|
|
|
#define IOMAP_BITS 65536
|
|
#define IOMAP_BYTES (IOMAP_BITS / 8)
|
|
|
|
// The TSS and its bitmap have to be one object: `iomb` is a 16-bit offset from
|
|
// the TSS base, and the TSS descriptor's limit is what cuts the bitmap off.
|
|
// So they are declared together and there is no way for them to drift apart.
|
|
//
|
|
// 8 KB per CPU of permanently resident kernel memory is the cost of being able
|
|
// to say "these ports and no others", and it is charged whether or not any
|
|
// process ever uses it. A bitmap sized to the highest port actually granted
|
|
// would be smaller and would have to be resized at runtime, under a descriptor
|
|
// the CPU is using; this is the version that is obviously correct.
|
|
struct tssblk {
|
|
struct taskstate64 ts;
|
|
uint8 map[IOMAP_BYTES];
|
|
// Intel requires a byte of all-ones past the end, so an access that straddles
|
|
// the final port still reads a deny bit rather than whatever follows.
|
|
uint8 tail;
|
|
} __attribute__((packed, aligned(16)));
|
|
|
|
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.
|
|
static struct {
|
|
struct ioport_range r[NIOPORT];
|
|
int n;
|
|
} installed[NCPU];
|
|
|
|
static void
|
|
map_set(uint8 *map, uint16 base, uint16 len, int allow)
|
|
{
|
|
uint32 p, end = (uint32)base + len;
|
|
|
|
if (end > IOMAP_BITS)
|
|
end = IOMAP_BITS;
|
|
for (p = base; p < end; p++) {
|
|
if (allow)
|
|
map[p >> 3] &= ~(1 << (p & 7));
|
|
else
|
|
map[p >> 3] |= (1 << (p & 7));
|
|
}
|
|
}
|
|
|
|
void *
|
|
ioperm_tss(int cpu, uint64 *limit)
|
|
{
|
|
struct tssblk *b = &tssblk[cpu];
|
|
|
|
// Deny everything to start with. A zero bit is permission, so a zeroed
|
|
// bitmap is an open machine -- this memset is not initialisation tidiness.
|
|
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
|
|
// trailing 0xFF -- stopping one byte short is the difference between a
|
|
// straddling access being denied and it reading the next object.
|
|
*limit = (uint64)__builtin_offsetof(struct tssblk, tail);
|
|
return b;
|
|
}
|
|
|
|
// The same block, without the reset. seginit() calls ioperm_tss() once per CPU
|
|
// to build it; everything after that just wants the pointer.
|
|
void *
|
|
ioperm_tss_of(int cpu)
|
|
{
|
|
return &tssblk[cpu];
|
|
}
|
|
|
|
void
|
|
ioperm_set_rsp0(void *tss, addr_t stktop)
|
|
{
|
|
((struct taskstate64 *)tss)->rsp0 = (uint64)stktop;
|
|
}
|
|
|
|
void
|
|
ioperm_grant(struct proc *p, uint16 base, uint16 len)
|
|
{
|
|
int i;
|
|
|
|
if (len == 0)
|
|
return;
|
|
for (i = 0; i < p->nioport; i++)
|
|
if (p->ioport[i].base == base && p->ioport[i].len == len)
|
|
return;
|
|
if (p->nioport >= NIOPORT)
|
|
return; // refused quietly; the driver will fault loudly
|
|
p->ioport[p->nioport].base = base;
|
|
p->ioport[p->nioport].len = len;
|
|
p->nioport++;
|
|
}
|
|
|
|
void
|
|
ioperm_revoke_all(struct proc *p)
|
|
{
|
|
p->nioport = 0;
|
|
// The bitmap is not touched here. This can run on a CPU that is not the one
|
|
// holding the process's ranges open, and the next switchuvm() on each CPU
|
|
// closes what that CPU opened -- which is the only place the two can be made
|
|
// to agree without an IPI.
|
|
}
|
|
|
|
void
|
|
ioperm_switch(struct proc *p)
|
|
{
|
|
int c = cpunum();
|
|
struct tssblk *b = &tssblk[c];
|
|
int i;
|
|
|
|
// Close whatever was open. Doing this unconditionally rather than diffing
|
|
// against the incoming process costs a few hundred bit clears on a machine
|
|
// where nobody uses ports at all -- which is `installed[c].n == 0`, and then
|
|
// this loop does nothing.
|
|
for (i = 0; i < installed[c].n; i++)
|
|
map_set(b->map, installed[c].r[i].base, installed[c].r[i].len, 0);
|
|
installed[c].n = 0;
|
|
|
|
if (!p || p->nioport == 0)
|
|
return;
|
|
|
|
for (i = 0; i < p->nioport && i < NIOPORT; i++) {
|
|
map_set(b->map, p->ioport[i].base, p->ioport[i].len, 1);
|
|
installed[c].r[i] = p->ioport[i];
|
|
}
|
|
installed[c].n = i;
|
|
}
|