diff --git a/cmd/vgpud/main.c b/cmd/vgpud/main.c index d2c1f16..ab5054e 100644 --- a/cmd/vgpud/main.c +++ b/cmd/vgpud/main.c @@ -802,6 +802,37 @@ mirror_pass(void) present_rect(mirror_x, mirror_y + first, mirror_w, last - first + 1); } +// Tell the kernel where the scanout is, so it can put a crash screen there. +// +// The one thing a userspace display driver cannot do for itself. When the +// machine panics this process does not get another turn -- there is no +// scheduling after a panic -- so the head it owns would sit there showing a +// shell prompt from before the crash, which is the most misleading thing a +// screen can do. +// +// So the kernel is told, in advance, where to paint: it writes the report into +// this framebuffer and asks the kernel driver it took this device from to +// reset it and flush one frame. See PCIIOC_PANICFB in include/pciuser.h. +// +// Called again after every modeset, because the size is half of what was +// registered. Failing is not fatal to anything: it costs the crash screen on +// this head and nothing else, which is exactly what the situation was before +// the kernel could do this at all. +static void +register_panic_fb(void) +{ + struct pciuser_panicfb pfb; + + pfb.index = (unsigned int)index_; + pfb.paddr = fb_paddr; + pfb.width = (unsigned int)width; + pfb.height = (unsigned int)height; + pfb.stride = (unsigned int)width * 4; + if (ioctl(pcifd, PCIIOC_PANICFB, &pfb) < 0) + vputs("vgpud: the kernel would not take a panic framebuffer; " + "a crash will leave this head as it is\n"); +} + // --- the modeset -------------------------------------------------------------- // // docs/plan/hybrid.md milestone H3: *"a modeset API, which is the thing an @@ -981,6 +1012,7 @@ serve(int fd) // The ground first, then the console re-laid onto it: the // mirror's placement is derived from both sizes, and one // of them just changed. + register_panic_fb(); draw_mark(); mirror_geometry(); mirror_pass(); @@ -1103,6 +1135,7 @@ main(int argc, char *argv[]) int mirroring = console_open() == 0; bring_up(); + register_panic_fb(); draw_mark(); if (mirroring) mirror_geometry(); diff --git a/docs/architecture/graphics.md b/docs/architecture/graphics.md index ee69952..8b02daa 100644 --- a/docs/architecture/graphics.md +++ b/docs/architecture/graphics.md @@ -337,6 +337,40 @@ when the crash happens before there is a framebuffer at all. Once painted, `panicked` is set and `consputc` spins rather than writing, so nothing can scribble over the report. +### It reaches every display, including the ones the kernel gave away + +The report used to go to the console framebuffer and stop there. On a machine +with two heads that meant one screen saying the kernel was dead and another +showing a shell prompt from a few seconds earlier — the most misleading thing +a screen can do. + +`display_panic_present()` in `kernel/dev/device.c` walks the rest of them, and +there are two cases: + +**The kernel still drives it.** `display_acquire()` hands back a buffer; the +report is copied in and presented. This is the ordinary path, and it does +nothing at all when the display class's buffer *is* the console's — a machine +with one screen does not get the work done twice. + +**A process drives it.** Harder, and the one worth having. 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 `vgpud` registers its scanout in +advance (`PCIIOC_PANICFB`), the kernel paints the report into that memory, and +the driver the device was taken from is asked to put it on screen one last +time: `panic_present` in `struct pci_driver`. + +For virtio-gpu that means resetting the device out from under a driver that is +still nominally running — safe here and nowhere else, because nothing is going +to run again — and rebuilding one queue from 20 KB of static memory. No +allocation, no locks, no sleeping, and every spin bounded: a crash screen that +hangs has taken away the serial log's last chance to be read. + +Both cases say so on the serial line: + +``` + The report is on the virtio-gpu display too. +``` + ### It is no longer the only copy This screen used to be the only artefact of a panic — this file said so, and so diff --git a/docs/plan/hybrid.md b/docs/plan/hybrid.md index 0b4361f..d08eb00 100644 --- a/docs/plan/hybrid.md +++ b/docs/plan/hybrid.md @@ -241,6 +241,9 @@ VBE, and the driver restarts and takes the device back. During the gap the VBE head carries a live console and the virtio-gpu head still shows the dead driver's last frame, which is exactly what a device nobody is driving does. +(That last sentence is still true of a *crashed driver*, and no longer true of +a crashed *kernel* -- see "The clause nobody wrote down" below.) + Reattachment is a Schcom milestone, not a hybrid one, and it is recorded that way rather than counted here. @@ -362,6 +365,45 @@ the region back. --- +## The clause nobody wrote down: a panic has to reach the screen + +Handing a device away costs something that was not on the list. The kernel +panics, paints the report into the console framebuffer, and halts — and the +display it gave to a process keeps showing whatever that process last put +there. On a machine with two heads, one says the kernel is dead and the other +shows a shell prompt. + +It is not a race that can be won by being careful. There is no scheduling after +a panic, so the process holding the device is never going to run again, and the +kernel let go of the device deliberately. + +**So the userspace driver says in advance where it is scanning out**, with +`PCIIOC_PANICFB` (see `include/pciuser.h`), and the kernel does the rest: + +1. paints the report into that memory, +2. asks the *kernel* driver the device was taken from to put it on screen — + `panic_present` in `struct pci_driver`, +3. which resets the device and rebuilds one queue from **static** memory: no + allocation, no locks, no sleeping, because a corrupted heap is one of the + ways a machine gets here in the first place. + +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 it would +be a way to nominate any physical address in the machine to be overwritten at +panic, which is a worse bug than the one it fixes. + +What this buys is a property worth stating plainly: **a device can be evicted +to userspace without the kernel losing its ability to tell you the machine +died.** That is the difference between a driver boundary and a blind spot, and +it is one more thing the in-kernel arrangement got for free and the hybrid one +has to earn. + +It costs 20 KB of static memory in the virtio-gpu driver, paid whether or not +the machine ever panics. Allocating it on demand would be cheaper and would not +work. + +--- + ## The honest limitation > **There is no IOMMU, so a userspace driver is isolated against its own bugs and diff --git a/include/defs.h b/include/defs.h index 166e4ac..f79d32e 100644 --- a/include/defs.h +++ b/include/defs.h @@ -610,6 +610,14 @@ void fbclose(struct inode*); // Called from sys_mmap: a mapping of /dev/fb0 is a licence to draw with no // further syscalls, so it is the last the kernel hears about it. void fb_mapped(void); + +// The crash screen, on displays other than the console's. kernel/dev/device.c. +void display_panic_present(void); +// Where a userspace display driver said it is scanning out, and which kernel +// driver it took the device from. kernel/dev/pciuser.c. +int pciuser_panic_surface(int idx, uint64 *paddr, int *w, int *h, + uint32 *stride, + const struct pci_driver **drv); int fbread(struct inode*, uint64, char*, int); int fbwrite(struct inode*, uint64, char*, int); int fb_ioctl(int request, addr_t arg); diff --git a/include/pci.h b/include/pci.h index 77e725f..acaea9e 100644 --- a/include/pci.h +++ b/include/pci.h @@ -132,4 +132,21 @@ struct pci_driver { // implement it keeps its device: refusing is always safe, and for anything // on the path to a shell it is also correct. int (*release)(struct pci_dev *); + // Put one last frame on a device this driver was told to let go of. + // + // Called from panic(), and only from there: interrupts are off, no other + // CPU is coming back, and the process that was driving the device will not + // run again. The framebuffer is one the userspace driver registered with + // PCIIOC_PANICFB and the kernel has already painted the report into; all + // that is left is to make the device show it. + // + // The contract is what panic context allows: **no allocation, no locks, no + // sleeping.** Whatever memory this needs has to already exist, because the + // allocator may be exactly what was being held when the machine died. A + // driver that cannot meet that leaves this null, and the screen it handed + // over stays frozen -- which is what happens today and is not a + // regression. + // + // Returns 0 if something was put on screen. + int (*panic_present)(struct pci_dev *, uint64 fb_paddr, int w, int h); }; diff --git a/include/pciuser.h b/include/pciuser.h index 77754c3..ee5f625 100644 --- a/include/pciuser.h +++ b/include/pciuser.h @@ -91,6 +91,7 @@ struct pciuser_dma { #define PCIIOC_RELEASE 0x9003 // in: int index #define PCIIOC_DMA 0x9004 // struct pciuser_dma #define PCIIOC_CFG 0x9006 // struct pciuser_cfg -- read config space +#define PCIIOC_PANICFB 0x9007 // struct pciuser_panicfb -- see below #define PCIIOC_IRQ 0x9005 // in: int index -- deliver this device's // interrupts to this process @@ -123,6 +124,52 @@ struct pciuser_dma { +// --- the crash screen, on a display the kernel no longer drives ------------ +// +// A userspace display driver leaves a hole the kernel cannot see around: when +// the machine panics, the screen this process is scanning out is frozen at +// whatever it last put there. The machine is dead and its second monitor is +// still showing a shell prompt. +// +// The kernel cannot fix that by itself. It let go of the device deliberately +// (PCIIOC_CLAIM), and the process that took it will not run again -- panic +// means no scheduling, so there is nobody left to ask. +// +// So a userspace display driver tells the kernel, in advance, where its +// scanout lives: +// +// struct pciuser_panicfb pfb; +// pfb.index = index; // the device, already claimed +// pfb.paddr = dma.paddr; // the framebuffer, from PCIIOC_DMA +// pfb.width = 1280; +// pfb.height = 800; +// pfb.stride = 1280 * 4; // bytes per row; XRGB8888 only +// ioctl(fd, PCIIOC_PANICFB, &pfb); +// +// and on a panic the kernel paints the crash report into that memory and asks +// the driver it handed the device to -- its *kernel* driver, the one that was +// told to let go -- to put it on screen one last time. That driver resets the +// device and brings up a minimal queue from static memory: no allocation, no +// locks, nothing that can fail differently depending on what the machine was +// doing when it died. +// +// Registering is a promise about memory, so it is checked like one: the range +// must lie inside a block this process got from PCIIOC_DMA on this device. +// Without that check it would be a way to nominate any physical address in the +// machine to be overwritten at panic, which is a worse bug than the one it +// fixes. +// +// A zero paddr unregisters. The registration goes away with the claim and with +// the process, so a driver that exits leaves nothing behind. +struct pciuser_panicfb { + unsigned int index; // in: the claimed device + unsigned long long paddr; // in: physical address of the scanout, 0 to + // unregister + unsigned int width; // in: pixels + unsigned int height; + unsigned int stride; // in: bytes per row +}; + // Registers are reached with mmap() on this device, where the *offset is the // physical address of the BAR*: // diff --git a/kernel/core/panic.c b/kernel/core/panic.c index ab77b6e..444fad1 100644 --- a/kernel/core/panic.c +++ b/kernel/core/panic.c @@ -525,8 +525,13 @@ panic(char *s) gather(s, pcs, 10); report_serial(s, pcs, 10); - if (gfx_ready()) + if (gfx_ready()) { paint(s, pcs, 10); + // And on every other display the machine has, including the ones the + // kernel handed to a process. Deliberately after the console's copy: a + // fault in there must not cost the screen that is already right. + display_panic_present(); + } panicked = 1; for (;;) diff --git a/kernel/dev/device.c b/kernel/dev/device.c index 9c4b300..b77305a 100644 --- a/kernel/dev/device.c +++ b/kernel/dev/device.c @@ -18,6 +18,10 @@ #include "netdev.h" #include "theme.h" #include "text.h" +#include "gfx.h" +#include "pci.h" +#include "mmu.h" +#include "memlayout.h" static struct device devices[NDEVICE]; static int ndevices; @@ -186,6 +190,104 @@ display_wait_vblank(void) ((struct display_ops *)d->ops)->wait_vblank(d); } +// --- the crash screen, on every display the machine has --------------------- +// +// panic() paints the report into the console framebuffer, which is one screen +// on a machine that may have several. The others showed whatever they were +// showing when the machine died -- a shell prompt, most misleadingly -- and +// this is what walks them and puts the report there instead. +// +// Called from panic(), with interrupts off and everything the rules of that +// place imply: no allocation, no locks, no sleeping. It is deliberately the +// last thing the paint path does, so that a fault in here cannot cost the +// screen that was already drawn correctly. +// +// Two cases, and they are not the same: +// +// the kernel still drives it display_acquire() hands back a buffer; copy +// and present, the ordinary path +// userspace drives it the kernel let go of the device and the +// process holding it will not run again, so +// the driver it was taken from is asked to +// take it back for one frame +// +// The second is the one worth having. It is also the one that only works +// because the userspace driver said in advance where it was scanning out -- +// see PCIIOC_PANICFB in include/pciuser.h. + +// Copy the console framebuffer into a surface, with the same rules vgpud's +// mirror uses: the overlap, centred if the surface is larger, top-left if it +// is smaller. Both sides are XRGB8888; anything else is refused by whoever +// registered it. +static void +panic_copy_console(volatile uchar *dst, int dw, int dh, uint32 dstride) +{ + const volatile uchar *src = gfx_fb_ptr(); + int sw = (int)gfx_width(), sh = (int)gfx_height(); + uint32 sstride = gfx_fb_stride(); + int cw = sw < dw ? sw : dw; + int ch = sh < dh ? sh : dh; + int x0 = (dw - cw) / 2, y0 = (dh - ch) / 2; + + if (!src || gfx_fb_bpp() != 32) + return; + + for (int y = 0; y < ch; y++) { + const volatile uint32 *s = (const volatile uint32 *)(src + (uint64)y * sstride); + volatile uint32 *d = (volatile uint32 *)(dst + (uint64)(y0 + y) * dstride); + for (int x = 0; x < cw; x++) + d[x0 + x] = s[x]; + } +} + +void +display_panic_present(void) +{ + if (!gfx_ready()) + return; + + // The kernel's own display, when it is not the console's. On a machine where + // the console is the VBE framebuffer and a virtio-gpu is the display class, + // these are two different screens and only one of them has the report. + { + struct display_mode m; + uint32 stride = 0; + struct device *d = display_get(); + volatile uchar *buf = display_acquire(&stride); + + if (buf && display_mode(&m) == 0 && m.bpp == 32 && + (uint64)buf != (uint64)gfx_fb_ptr()) { + panic_copy_console(buf, m.width, m.height, stride ? stride : m.stride); + display_present(0, 0, m.width, m.height); + cprintf(" The report is on the %s display too.\n", + d ? d->name : "second"); + } + } + + // And every display the kernel handed to a process. The report is painted + // into memory that process registered, then its old kernel driver is asked + // to put it on screen -- which means resetting the device out from under a + // driver that is still nominally running. That is safe here and nowhere + // else: nothing is going to run again. + for (int i = 0; i < pci_count() && i < NPCI; i++) { + const struct pci_driver *drv = 0; + uint64 paddr = 0; + uint32 stride = 0; + int w = 0, h = 0; + + if (!pciuser_panic_surface(i, &paddr, &w, &h, &stride, &drv)) + continue; + // A packed surface, because that is what the device is about to be told it + // is. A padded one would shear, which is worse than not appearing. + if (stride != (uint32)w * 4) + continue; + + panic_copy_console((volatile uchar *)P2V(paddr), w, h, stride); + if (drv->panic_present((struct pci_dev *)pci_get(i), paddr, w, h) == 0) + cprintf(" The report is on the %s display too.\n", drv->name); + } +} + // Drive the whole display path once and report what happened: find the class, // read the mode, acquire a back buffer, draw into RAM, present it. // diff --git a/kernel/dev/pciuser.c b/kernel/dev/pciuser.c index 0045d62..95eea90 100644 --- a/kernel/dev/pciuser.c +++ b/kernel/dev/pciuser.c @@ -39,6 +39,24 @@ int mappages(pml4e_t *pgdir, void *va, uint64 size, uint64 pa, int perm); // renumbers it afterwards. static struct proc *owner[NPCI]; +// What used to drive each device, for the one thing a kernel driver is still +// needed for after it has been told to let go: the crash screen. +// +// d->drv is cleared when the claim succeeds, deliberately -- a driver that has +// handed a device over must not be reachable through the device it no longer +// drives. This is the exception, and it is narrow: nothing calls through here +// except panic(), the device is reset first, and by then the process that was +// driving it is not going to run again. +static const struct pci_driver *released_drv[NPCI]; + +// Where a userspace display driver is scanning out, so panic() can put the +// report there. See PCIIOC_PANICFB in include/pciuser.h. +struct panicfb { + uint64 paddr; + uint32 width, height, stride; +}; +static struct panicfb panicfb[NPCI]; + static struct spinlock pciuser_lock; // Interrupt delivery, one device per process. @@ -339,8 +357,10 @@ pciuser_release_all(struct proc *p) if (mine) owner[i] = 0; release(&pciuser_lock); - if (mine) + if (mine) { + panicfb[i].paddr = 0; pciuser_stop(i); + } } // And its ports. The bitmap itself is closed by the next switchuvm() on each @@ -469,6 +489,7 @@ do_claim(uint64 argp) return -EBUSY; if (d->drv->release((struct pci_dev *)d) != 0) return -EBUSY; + released_drv[idx] = d->drv; ((struct pci_dev *)d)->driver = 0; ((struct pci_dev *)d)->drv = 0; } @@ -554,8 +575,10 @@ do_release(uint64 argp) acquire(&pciuser_lock); int mine = (owner[idx] == myproc()); - if (mine) + if (mine) { owner[idx] = 0; + panicfb[idx].paddr = 0; + } release(&pciuser_lock); // Same halt as the death path, for the same reason. A driver that releases @@ -685,6 +708,84 @@ do_dma(uint64 argp) return 0; } +// Register where this process is scanning out, so panic() can draw there. +// +// The check is the whole of the security of this call: the range has to lie +// inside a block this process got from PCIIOC_DMA. Without it, a claimed +// device would be a way to nominate any physical address in the machine as the +// place the kernel writes four megabytes of crash report -- page tables, the +// kernel's own text -- at the one moment nothing is left to notice. +static int +do_panicfb(uint64 argp) +{ + struct pciuser_panicfb req; + struct proc *p = myproc(); + + if (copy_from_user(&req, argp, sizeof(req)) < 0) + return -EFAULT; + if ((int)req.index >= pci_count() || (int)req.index >= NPCI) + return -ENODEV; + if (owner[req.index] != p) + return -EACCES; // claim it first + + if (req.paddr == 0) { // unregister + acquire(&pciuser_lock); + panicfb[req.index].paddr = 0; + release(&pciuser_lock); + return 0; + } + + if (req.width == 0 || req.height == 0 || req.stride < req.width * 4) + return -EINVAL; + + uint64 need = (uint64)req.height * req.stride; + uint64 end = req.paddr + need; + if (end < req.paddr) + return -EINVAL; + + int ok = 0; + acquire(&pciuser_lock); + for (int i = 0; i < PCIUSER_MAXDMA; i++) { + uint64 base, lim; + if (dmablk[i].proc != p || !dmablk[i].kva) + continue; + base = dma_phys(dmablk[i].kva); + lim = base + dmablk[i].size; + if (req.paddr >= base && end <= lim) { ok = 1; break; } + } + if (ok) { + panicfb[req.index].paddr = req.paddr; + panicfb[req.index].width = req.width; + panicfb[req.index].height = req.height; + panicfb[req.index].stride = req.stride; + } + release(&pciuser_lock); + + // Not EINVAL: the argument is fine, the memory is not this process's to + // nominate. EACCES is the difference between "you asked wrong" and "no". + return ok ? 0 : -EACCES; +} + +// The panic path's view of the above. Called with interrupts off, from a +// machine that is already dead: no locks, because whoever held one is not +// going to release it, and a crash screen that deadlocks is worse than no +// crash screen. +int +pciuser_panic_surface(int idx, uint64 *paddr, int *w, int *h, uint32 *stride, + const struct pci_driver **drv) +{ + if (idx < 0 || idx >= NPCI || owner[idx] == 0 || panicfb[idx].paddr == 0) + return 0; + if (!released_drv[idx] || !released_drv[idx]->panic_present) + return 0; + *paddr = panicfb[idx].paddr; + *w = (int)panicfb[idx].width; + *h = (int)panicfb[idx].height; + *stride = panicfb[idx].stride; + *drv = released_drv[idx]; + return 1; +} + // Hand a device's interrupts to the calling process. static int do_irq(uint64 argp) @@ -795,6 +896,7 @@ pciuser_ioctl(uint64 request, uint64 argp) case PCIIOC_DMA: return do_dma(argp); case PCIIOC_IRQ: return do_irq(argp); case PCIIOC_CFG: return do_cfg(argp); + case PCIIOC_PANICFB: return do_panicfb(argp); default: return -ENOTTY; } } diff --git a/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c b/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c index 3ef4fe4..284e050 100644 --- a/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c +++ b/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c @@ -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, };