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:
Nexoniarz 2026-09-22 16:53:16 +02:00
commit e72205cdc0
10 changed files with 567 additions and 9 deletions

View file

@ -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