"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>
185 lines
8.1 KiB
C
185 lines
8.1 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// /dev/pci -- device access from userspace.
|
|
//
|
|
// docs/plan/hybrid.md, mechanisms 2 and 4: MMIO and DMA to a process. This is
|
|
// the interface a driver that does not live in the kernel talks to, and it is
|
|
// deliberately about *PCI devices*, not about any particular driver model.
|
|
//
|
|
// That generality is the point, and it is worth being explicit about why.
|
|
// The first users of this will be SchOS's own drivers, evicted one at a time
|
|
// -- virtio-gpu, then xHCI -- and the interface has to be judged by them.
|
|
// Something that hosts foreign drivers later is then a consumer of a general
|
|
// mechanism rather than a hole shaped like one, which is both the honest
|
|
// design and the only one that stays honest under examination.
|
|
//
|
|
// What a driver needs from a kernel, and all it needs:
|
|
//
|
|
// - to find its device, and read what the bus says about it INFO
|
|
// - to be given it, exclusively CLAIM
|
|
// - to reach its registers mmap(BAR)
|
|
// - memory it can hand to the device DMA
|
|
// - to be told when the device wants attention IRQ + read()
|
|
//
|
|
// There is no IOMMU. A process holding a DMA buffer can point the device at
|
|
// any physical address, so this isolates a driver against its own *bugs* and
|
|
// not against its own *intent*. docs/plan/hybrid.md says so at length and it
|
|
// remains true here: a crashed driver that restarts instead of taking the
|
|
// machine down is the whole benefit, and it is not a security boundary.
|
|
|
|
#ifndef PCIUSER_H
|
|
#define PCIUSER_H
|
|
|
|
#define PCIUSER_MAJOR 9
|
|
|
|
#define PCIUSER_MAXBAR 6
|
|
|
|
struct pciuser_bar {
|
|
unsigned long long base; // physical address, 0 when the BAR is unused
|
|
unsigned long long size;
|
|
unsigned int flags; // PCIUSER_BAR_*
|
|
unsigned int pad;
|
|
};
|
|
|
|
#define PCIUSER_BAR_MMIO 0x01 // memory space; mappable
|
|
#define PCIUSER_BAR_IO 0x02 // I/O port space; not mappable
|
|
#define PCIUSER_BAR_PREFETCH 0x04
|
|
|
|
// One device, as the bus describes it. `index` is the only field the caller
|
|
// fills in; everything else comes back.
|
|
struct pciuser_info {
|
|
unsigned int index; // in: 0 .. PCIIOC_COUNT-1
|
|
unsigned char bus, slot, func;
|
|
unsigned char header_type;
|
|
|
|
unsigned short vendor, device;
|
|
unsigned char class, subclass, prog_if, revision;
|
|
|
|
unsigned char irq; // interrupt line, 0xFF when unrouted
|
|
unsigned char claimed; // 1 when a userspace process holds it
|
|
unsigned short pad;
|
|
|
|
// The name of whatever owns it: an in-kernel driver, "userspace" when a
|
|
// process has claimed it, or empty when nothing has.
|
|
char driver[16];
|
|
|
|
struct pciuser_bar bar[PCIUSER_MAXBAR];
|
|
};
|
|
|
|
// A DMA buffer. The kernel allocates it from the DMA arena, maps it into the
|
|
// caller, and reports the physical address the *device* must be given -- which
|
|
// is the entire reason a driver cannot do this for itself with mmap.
|
|
// A dword of PCI configuration space. A virtio device says where its registers
|
|
// live in vendor capabilities, which are here and nowhere else -- so without
|
|
// this a driver cannot find its own device.
|
|
struct pciuser_cfg {
|
|
unsigned int index; // in
|
|
unsigned int offset; // in: dword-aligned, 0..0xFC
|
|
unsigned int value; // out
|
|
unsigned int pad;
|
|
};
|
|
|
|
struct pciuser_dma {
|
|
unsigned long long size; // in: bytes, rounded up to a page
|
|
unsigned long long uaddr; // out: where it landed in this process
|
|
unsigned long long paddr; // out: what to program into the device
|
|
};
|
|
|
|
#define PCIIOC_COUNT 0x9000 // out: int, how many devices the bus has
|
|
#define PCIIOC_INFO 0x9001 // struct pciuser_info
|
|
#define PCIIOC_CLAIM 0x9002 // in: int index -- take it from the kernel
|
|
#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
|
|
|
|
// Interrupts arrive by *reading* this device, which is how Linux's uio does it
|
|
// and for the same reason: a descriptor composes with poll, and a driver that
|
|
// stops reading falls behind rather than losing events.
|
|
//
|
|
// ioctl(fd, PCIIOC_IRQ, &index);
|
|
// for (;;) {
|
|
// unsigned long long n;
|
|
// read(fd, &n, sizeof n); // blocks until the device interrupts
|
|
// ... service the device, and ACK IT AT THE DEVICE ...
|
|
// write(fd, &n, sizeof n); // re-arm
|
|
// }
|
|
//
|
|
// **The write is not optional.** A PCI interrupt line is level-triggered: it
|
|
// stays asserted until the driver clears the cause *at the device*, and the
|
|
// kernel cannot do that because clearing it is device-specific -- it is a
|
|
// different register on every chip. So the kernel masks the line when the
|
|
// interrupt arrives and leaves it masked until the write says the device has
|
|
// been dealt with.
|
|
//
|
|
// Without that, EOI is signalled on a line that is still asserted and the
|
|
// interrupt re-fires immediately, for ever. That is not hypothetical: it
|
|
// happened in this kernel, at 72,000 interrupts a second, and what it looked
|
|
// like from userspace was a clock running seven times slow.
|
|
// See docs/history/kernel.md section 13.
|
|
//
|
|
// One device per process. A driver drives one thing; if you need two, run two.
|
|
|
|
|
|
|
|
// --- 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*:
|
|
//
|
|
// ioctl(fd, PCIIOC_INFO, &info);
|
|
// ioctl(fd, PCIIOC_CLAIM, &info.index);
|
|
// regs = mmap(0, info.bar[0].size, PROT_READ|PROT_WRITE, MAP_SHARED,
|
|
// fd, info.bar[0].base);
|
|
//
|
|
// The kernel refuses an offset that is not inside a BAR of a device this
|
|
// process has claimed, which is what stops this being /dev/mem with extra
|
|
// steps.
|
|
|
|
#endif // PCIUSER_H
|