panic: the crash screen reaches every display, not just the console's
"it also doesn't show crash screen on virtio gpu or just other gpu than default
vga" -- true, and on the virtio-gpu it was worse than nothing. The head kept
showing the console from a second before the panic: one screen saying the
kernel is dead, the other showing a shell prompt.
panic() painted into the console framebuffer and stopped there. Two cases were
missing, and they are not the same problem.
**The kernel still drives it.** display_acquire() hands back a buffer; the
report is copied in and presented. Ordinary, and a no-op when the display
class's buffer *is* the console's, so a one-screen machine does not do the work
twice.
**A process drives it.** The hard one, and the one hybrid.md owed an answer
for. The kernel let go of the device deliberately, and the process holding it
will never run again -- there is no scheduling after a panic -- so there is
nobody left to ask. It is not a race that can be won by being careful.
So the userspace driver says where it is scanning out *in advance*:
PCIIOC_PANICFB, registered by vgpud after bring-up and after every modeset. On
a panic the kernel paints the report into that memory and asks the kernel
driver the device was taken from to put it on screen one last time --
`panic_present` in struct pci_driver, which virtio-gpu implements by resetting
the device out from under a driver that is still nominally running. That is
safe here and nowhere else, because nothing is going to run again.
The panic path allocates nothing. It cannot: a corrupted heap is one of the
ways a machine gets here, and an allocator that deadlocks takes the crash
screen *and* the serial log's last chance to be read. So the driver keeps its
BAR mappings across the handover, rebuilds one queue from 20 KB of static
memory, and bounds every spin -- including the reset acknowledge, which is an
unbounded `while` in probe() and correct there.
**The registration is checked like the promise it is.** The range must lie
inside a block that process got from PCIIOC_DMA on that device. Without that,
a claimed device would be a way to nominate any physical address in the machine
-- page tables, kernel text -- to be overwritten with four megabytes of crash
report at the one moment nothing is left to notice. That is a worse bug than
the one this fixes. It goes away with the claim and with the process.
What it buys is worth stating plainly: a device can be evicted to userspace
without the kernel losing its ability to tell you the machine died. The
alternative was a driver boundary that is also a blind spot.
Verified both ways under QEMU with two heads, `echo c > /proc/sysrq`, and a
screendump of each over QMP:
vgpud holding the device both heads byte-identical, serial says
"The report is on the virtio-gpu display too."
kernel holding it same, via display_acquire/display_present
(/proc/pci shows 1af4:1050 [virtio-gpu])
Both supported configurations: one boot each, no traps, no panics, socktest and
memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
38ea83bf05
commit
e72205cdc0
10 changed files with 567 additions and 9 deletions
|
|
@ -160,7 +160,7 @@ find_cfg(struct pci_dev *d, int want, uint32 *out_mult)
|
|||
// --- virtqueue -------------------------------------------------------------
|
||||
|
||||
static int
|
||||
queue_setup(struct vgpu *g)
|
||||
queue_setup_mem(struct vgpu *g, uint8 *mem)
|
||||
{
|
||||
w16(g->common, VCOMMON_Q_SELECT, CTRL_QUEUE);
|
||||
|
||||
|
|
@ -179,8 +179,6 @@ queue_setup(struct vgpu *g)
|
|||
uint32 avail_sz = 6 + 2 * qsize;
|
||||
uint32 used_sz = 6 + 8 * qsize;
|
||||
|
||||
uint8 *mem = dma_alloc(4096 * 3, 4096);
|
||||
g->ring = mem;
|
||||
if (!mem)
|
||||
return -1;
|
||||
memset(mem, 0, 4096 * 3);
|
||||
|
|
@ -197,6 +195,13 @@ queue_setup(struct vgpu *g)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
queue_setup(struct vgpu *g)
|
||||
{
|
||||
g->ring = dma_alloc(4096 * 3, 4096);
|
||||
return queue_setup_mem(g, g->ring);
|
||||
}
|
||||
|
||||
// Submit one command and wait for the reply.
|
||||
//
|
||||
// Two descriptors: the request, which the device reads, and the response, which
|
||||
|
|
@ -359,10 +364,8 @@ vgpu_acquire(struct device *d, uint32 *stride_out)
|
|||
}
|
||||
|
||||
static void
|
||||
vgpu_present(struct device *d, int x, int y, int w, int h)
|
||||
present_g(struct vgpu *g, int x, int y, int w, int h)
|
||||
{
|
||||
struct vgpu *g = vgpu_of(d);
|
||||
|
||||
if (!g || !g->ready)
|
||||
return;
|
||||
if (x < 0) { w += x; x = 0; }
|
||||
|
|
@ -390,6 +393,12 @@ vgpu_present(struct device *d, int x, int y, int w, int h)
|
|||
}
|
||||
}
|
||||
|
||||
static void
|
||||
vgpu_present(struct device *d, int x, int y, int w, int h)
|
||||
{
|
||||
present_g(vgpu_of(d), x, y, w, h);
|
||||
}
|
||||
|
||||
// No wait_vblank. The device presents whole rectangles atomically, so there is
|
||||
// nothing to synchronise against -- and a null op is more honest than one that
|
||||
// spins and achieves nothing. display_wait_vblank() handles the absence.
|
||||
|
|
@ -512,6 +521,14 @@ vgpu_probe(struct pci_dev *d)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Registers kept from before a handover, per device. Filled in by
|
||||
// vgpu_release() and read only by vgpu_panic_present(); see both.
|
||||
static struct {
|
||||
struct pci_dev *pdev;
|
||||
volatile uint8 *common, *notify;
|
||||
uint32 notify_mult;
|
||||
} handed_over[VGPU_MAX];
|
||||
|
||||
// Hand the device to a userspace driver.
|
||||
//
|
||||
// docs/plan/hybrid.md's first eviction, and the reason it is this device: the
|
||||
|
|
@ -576,12 +593,162 @@ vgpu_release(struct pci_dev *d)
|
|||
if (g->cmd) { dma_free(g->cmd, 4096); g->cmd = 0; }
|
||||
if (g->resp) { dma_free(g->resp, 4096); g->resp = 0; }
|
||||
|
||||
// Keep the addresses, and only the addresses.
|
||||
//
|
||||
// This is what makes the crash screen possible on a device the kernel no
|
||||
// longer drives: panic() cannot ioremap -- that allocates, and the allocator
|
||||
// is exactly the sort of thing that may have been what died -- so the
|
||||
// mappings made at probe are kept, and the panic path finds the device
|
||||
// through them rather than through anything it has to build. Registers, not
|
||||
// state: there is no queue here, no framebuffer, nothing the userspace
|
||||
// driver could be confused by.
|
||||
for (int i = 0; i < VGPU_MAX; i++)
|
||||
if (handed_over[i].pdev == d || handed_over[i].pdev == 0) {
|
||||
handed_over[i].pdev = d;
|
||||
handed_over[i].common = g->common;
|
||||
handed_over[i].notify = g->notify;
|
||||
handed_over[i].notify_mult = g->notify_mult;
|
||||
break;
|
||||
}
|
||||
|
||||
g->ready = 0;
|
||||
g->pdev = 0;
|
||||
kinfo("virtio-gpu: released to userspace; screen is the VBE framebuffer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- the crash screen, on a device this driver was told to let go ----------
|
||||
//
|
||||
// docs/plan/hybrid.md's bargain has a clause nobody wrote down: a device the
|
||||
// kernel hands away is a device the kernel cannot use to tell you the machine
|
||||
// died. The second monitor sat there showing a shell prompt from before the
|
||||
// panic, which is the most misleading thing a screen can do.
|
||||
//
|
||||
// It does not have to be that way, and the reason is the whole argument for
|
||||
// virtio: presenting is an explicit operation over a queue, and a queue can be
|
||||
// rebuilt from nothing in a few hundred instructions. So on a panic the kernel
|
||||
// takes the device back -- resets it, builds one queue out of static memory,
|
||||
// points a scanout at the framebuffer the userspace driver registered, and
|
||||
// flushes it once.
|
||||
//
|
||||
// The 20 KB below is the price, and it is paid whether or not the machine ever
|
||||
// panics. Allocating it on demand would be cheaper and would not work: panic
|
||||
// context has no allocator it can trust, because a corrupted heap is one of
|
||||
// the things that gets a machine here.
|
||||
static uint8 panic_pool[4096 * 5] __attribute__((aligned(4096)));
|
||||
|
||||
// Every spin in here is bounded. The unbounded ones in vgpu_probe() are
|
||||
// correct where they are -- a device that never answers during boot is a
|
||||
// machine that cannot work -- but a crash screen that hangs instead of
|
||||
// appearing has taken away the serial log's last chance to be read.
|
||||
static int
|
||||
panic_wait_status(volatile uint8 *common, uint8 want)
|
||||
{
|
||||
for (int i = 0; i < 1000000; i++)
|
||||
if (r8(common, VCOMMON_STATUS) == want)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
vgpu_panic_present(struct pci_dev *d, uint64 fb_paddr, int w, int h)
|
||||
{
|
||||
static struct vgpu pg; // static: panic context allocates nothing
|
||||
struct vgpu *g = &pg;
|
||||
int slot = -1;
|
||||
|
||||
for (int i = 0; i < VGPU_MAX; i++)
|
||||
if (handed_over[i].pdev == d) { slot = i; break; }
|
||||
if (slot < 0 || !handed_over[slot].common || !handed_over[slot].notify)
|
||||
return -1;
|
||||
if (w <= 0 || h <= 0)
|
||||
return -1;
|
||||
|
||||
memset(g, 0, sizeof(*g));
|
||||
g->common = handed_over[slot].common;
|
||||
g->notify = handed_over[slot].notify;
|
||||
g->notify_mult = handed_over[slot].notify_mult;
|
||||
g->width = w;
|
||||
g->height = h;
|
||||
|
||||
// Memory space and bus mastering. The userspace driver has them on, but
|
||||
// asserting them costs one config write and covers the case where it died
|
||||
// and pciuser_stop() turned them off -- which is a likely way to arrive
|
||||
// here.
|
||||
pci_enable(d, 0x0006);
|
||||
|
||||
// Reset. Everything the userspace driver set up -- its queue, its resources,
|
||||
// its scanout -- goes away here, which is the point: what follows must not
|
||||
// depend on any of it, because a driver that crashed may have left it in any
|
||||
// state at all.
|
||||
w8(g->common, VCOMMON_STATUS, 0);
|
||||
if (panic_wait_status(g->common, 0) != 0)
|
||||
return -1;
|
||||
w8(g->common, VCOMMON_STATUS, VSTATUS_ACK);
|
||||
w8(g->common, VCOMMON_STATUS, VSTATUS_ACK | VSTATUS_DRIVER);
|
||||
|
||||
w32(g->common, VCOMMON_DFEAT_SEL, 1);
|
||||
if (!(r32(g->common, VCOMMON_DFEAT) & (1u << (VIRTIO_F_VERSION_1 - 32))))
|
||||
return -1;
|
||||
w32(g->common, VCOMMON_GFEAT_SEL, 0);
|
||||
w32(g->common, VCOMMON_GFEAT, 0);
|
||||
w32(g->common, VCOMMON_GFEAT_SEL, 1);
|
||||
w32(g->common, VCOMMON_GFEAT, 1u << (VIRTIO_F_VERSION_1 - 32));
|
||||
w8(g->common, VCOMMON_STATUS,
|
||||
VSTATUS_ACK | VSTATUS_DRIVER | VSTATUS_FEATURES_OK);
|
||||
if (!(r8(g->common, VCOMMON_STATUS) & VSTATUS_FEATURES_OK))
|
||||
return -1;
|
||||
|
||||
g->cmd = panic_pool + 4096 * 3;
|
||||
g->resp = panic_pool + 4096 * 4;
|
||||
if (queue_setup_mem(g, panic_pool) != 0)
|
||||
return -1;
|
||||
|
||||
w8(g->common, VCOMMON_STATUS,
|
||||
VSTATUS_ACK | VSTATUS_DRIVER | VSTATUS_FEATURES_OK | VSTATUS_DRIVER_OK);
|
||||
|
||||
// A resource the size of the report, backed by the memory the userspace
|
||||
// driver registered and the panic path has already painted into.
|
||||
{
|
||||
struct virtio_gpu_resource_create_2d *c = g->cmd;
|
||||
hdr_init(c, VIRTIO_GPU_CMD_RESOURCE_CREATE_2D);
|
||||
c->resource_id = 1;
|
||||
c->format = VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM;
|
||||
c->width = (uint32)w;
|
||||
c->height = (uint32)h;
|
||||
if (cmd(g, sizeof(*c), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
struct virtio_gpu_resource_attach_backing *a = g->cmd;
|
||||
hdr_init(a, VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING);
|
||||
a->resource_id = 1;
|
||||
a->nr_entries = 1;
|
||||
struct virtio_gpu_mem_entry *e = (struct virtio_gpu_mem_entry *)(a + 1);
|
||||
e->addr = fb_paddr;
|
||||
e->length = (uint32)w * (uint32)h * 4;
|
||||
e->padding = 0;
|
||||
if (cmd(g, sizeof(*a) + sizeof(*e),
|
||||
sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
struct virtio_gpu_set_scanout *so = g->cmd;
|
||||
hdr_init(so, VIRTIO_GPU_CMD_SET_SCANOUT);
|
||||
so->r.x = so->r.y = 0;
|
||||
so->r.width = (uint32)w;
|
||||
so->r.height = (uint32)h;
|
||||
so->scanout_id = 0;
|
||||
so->resource_id = 1;
|
||||
if (cmd(g, sizeof(*so), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
g->ready = 1; // so vgpu_present will talk to the device
|
||||
present_g(g, 0, 0, w, h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct pci_match virtio_gpu_match[] = {
|
||||
{ VIRTIO_VENDOR, VIRTIO_DEV_GPU, 0, 0 },
|
||||
{ 0, 0, 0, 0 },
|
||||
|
|
@ -607,4 +774,5 @@ const struct pci_driver virtio_gpu_driver = {
|
|||
.probe = vgpu_probe,
|
||||
.attached = vgpu_attached,
|
||||
.release = vgpu_release,
|
||||
.panic_present = vgpu_panic_present,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue