docs/plan/hybrid.md mechanism 3. A process asks for a device's interrupts with
PCIIOC_IRQ and then reads them, 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. read() returns a count rather than a
flag, so a driver that was busy sees it missed three and acts once.
Proven against an e1000e by asserting a cause through its ICS register and
reading the interrupt back out in userspace:
interrupts routed here (line 10)
interrupt delivered, count = 1
ICR = 0x80000004
0x80000004 is the cause the test set plus INT_ASSERTED -- the device's own
register, read by userspace as its acknowledgement.
**The masking is the design, not a detail.** A PCI line is level-triggered and
stays asserted until the cause is cleared at the device, which is a different
register on every chip and so is something only the driver can do. The kernel
masks the line on arrival and userspace's write() unmasks it. Signalling EOI on
a still-asserted line re-enters the handler immediately and for ever; this
kernel did exactly that by accident at 72,000 interrupts a second, and what it
looked like from outside was a clock running seven times slow (§13).
Which decides what happens when a driver dies mid-interrupt: the line is left
**masked**. The instinct is to unmask -- do not leave the device dead -- and it
is wrong, because unmasking an asserted line with no handler is a crashed
driver taking the machine down with it. Masked is safe and not permanent: the
next PCIIOC_IRQ arms it. Both halves verified by crashing a driver on purpose
and then starting another one.
A line already carrying kernel handlers is refused outright. Masking is
per-line, so handing out a shared one would silence whichever kernel driver is
also there, with nothing logged. MSI is the real answer and pci_msi_enable() is
already in the tree waiting for a caller.
Also fixed, found by running the crash twice: DMA buffers were never returned
when a process exited. The arena is 4 MB and contiguous, so a driver that
restarts a few times exhausted it -- "out of DMA memory, 3584 free" -- which is
a poor property for an interface whose whole promise is that a driver can crash
and come back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
247 lines
8.6 KiB
C
247 lines
8.6 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// pcitest -- prove that a driver could live out here.
|
|
//
|
|
// docs/plan/hybrid.md mechanisms 2 and 4, exercised end to end from a process:
|
|
// enumerate the bus, claim a device, map its registers, read them, and get
|
|
// memory the device could be pointed at.
|
|
//
|
|
// It is deliberately a test and not a driver. The question it answers is "does
|
|
// the mechanism work", which has to be answered before the first real eviction
|
|
// -- otherwise a virtio-gpu that fails in userspace has two candidate causes
|
|
// and no way to tell them apart.
|
|
//
|
|
// pcitest list the bus
|
|
// pcitest <index> claim that device, map BAR0, read it, allocate DMA
|
|
// pcitest <index> x the same, then assert an interrupt and exit WITHOUT
|
|
// acking it -- which is what a driver that crashed looks
|
|
// like from the kernel's side, and the case that used to
|
|
// leave a level-triggered line asserted with no handler
|
|
|
|
#include "types.h"
|
|
#include "user.h"
|
|
#include "fcntl.h"
|
|
#include "posix.h"
|
|
#include "pciuser.h"
|
|
|
|
static void
|
|
puts(const char *s)
|
|
{
|
|
int n = 0;
|
|
while (s[n]) n++;
|
|
write(1, (void *)s, n);
|
|
}
|
|
|
|
static void
|
|
puthex(unsigned long long v, int digits)
|
|
{
|
|
static const char h[] = "0123456789abcdef";
|
|
char b[17];
|
|
for (int i = digits - 1; i >= 0; i--) { b[i] = h[v & 0xF]; v >>= 4; }
|
|
write(1, b, digits);
|
|
}
|
|
|
|
static void
|
|
putn(int v)
|
|
{
|
|
char b[12];
|
|
int i = 0;
|
|
if (v < 0) { write(1, "-", 1); v = -v; }
|
|
if (v == 0) { write(1, "0", 1); return; }
|
|
while (v > 0) { b[i++] = '0' + (v % 10); v /= 10; }
|
|
while (i-- > 0) write(1, &b[i], 1);
|
|
}
|
|
|
|
static int
|
|
num(const char *s)
|
|
{
|
|
int v = 0;
|
|
while (*s >= '0' && *s <= '9') v = v * 10 + (*s++ - '0');
|
|
return v;
|
|
}
|
|
|
|
// What the class byte means, for the handful that matter here. Not a full
|
|
// table: a listing that says "class 02" is only marginally better than one
|
|
// that says nothing, and a full one is 200 lines of data nobody reads.
|
|
static const char *
|
|
class_name(unsigned char c)
|
|
{
|
|
switch (c) {
|
|
case 0x01: return "storage";
|
|
case 0x02: return "network";
|
|
case 0x03: return "display";
|
|
case 0x04: return "multimedia";
|
|
case 0x06: return "bridge";
|
|
case 0x0c: return "serial bus";
|
|
default: return "other";
|
|
}
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
int fd = open("/dev/pci", O_RDWR);
|
|
if (fd < 0) {
|
|
puts("pcitest: cannot open /dev/pci\n");
|
|
exit();
|
|
}
|
|
|
|
int count = 0;
|
|
if (ioctl(fd, PCIIOC_COUNT, &count) < 0) {
|
|
puts("pcitest: PCIIOC_COUNT failed\n");
|
|
exit();
|
|
}
|
|
|
|
if (argc == 1) {
|
|
puts(" # bdf vendor:device irq class driver\n");
|
|
for (int i = 0; i < count; i++) {
|
|
struct pciuser_info in;
|
|
in.index = (unsigned int)i;
|
|
if (ioctl(fd, PCIIOC_INFO, &in) < 0)
|
|
continue;
|
|
puts(" "); putn(i); puts(" ");
|
|
puthex(in.bus, 2); puts(":"); puthex(in.slot, 2);
|
|
puts("."); puthex(in.func, 1); puts(" ");
|
|
puthex(in.vendor, 4); puts(":"); puthex(in.device, 4); puts(" ");
|
|
if (in.irq == 0xFF) puts(" - "); else { putn(in.irq); puts(" "); }
|
|
puts(class_name(in.class));
|
|
puts(" ");
|
|
puts(in.driver[0] ? in.driver : "-");
|
|
puts("\n");
|
|
}
|
|
puts("\npcitest <n> to claim one and read its registers\n");
|
|
exit();
|
|
}
|
|
|
|
int idx = num(argv[1]);
|
|
struct pciuser_info in;
|
|
in.index = (unsigned int)idx;
|
|
if (ioctl(fd, PCIIOC_INFO, &in) < 0) {
|
|
puts("pcitest: no device "); putn(idx); puts("\n");
|
|
exit();
|
|
}
|
|
|
|
puts("device "); putn(idx); puts(": ");
|
|
puthex(in.vendor, 4); puts(":"); puthex(in.device, 4);
|
|
puts(" ("); puts(class_name(in.class)); puts(")");
|
|
if (in.driver[0]) { puts(", held by "); puts(in.driver); }
|
|
puts("\n");
|
|
|
|
if (ioctl(fd, PCIIOC_CLAIM, &idx) < 0) {
|
|
// EBUSY is the expected answer for anything the kernel drives, and it
|
|
// is the right answer until a driver can be told to let go. Saying so
|
|
// beats a bare failure.
|
|
puts(" claim refused -- the kernel is driving it, or someone else is\n");
|
|
exit();
|
|
}
|
|
puts(" claimed\n");
|
|
|
|
// The first memory BAR. A device with none is not one a driver reaches
|
|
// this way, and saying that is more useful than mapping nothing.
|
|
int b = -1;
|
|
for (int i = 0; i < PCIUSER_MAXBAR; i++)
|
|
if (in.bar[i].base && (in.bar[i].flags & PCIUSER_BAR_MMIO)) { b = i; break; }
|
|
if (b < 0) {
|
|
puts(" no memory BAR to map\n");
|
|
exit();
|
|
}
|
|
|
|
puts(" BAR"); putn(b); puts(" at 0x"); puthex(in.bar[b].base, 16);
|
|
puts(", "); putn((int)(in.bar[b].size / 1024)); puts(" KB\n");
|
|
|
|
unsigned long long len = in.bar[b].size;
|
|
if (len > 4096) len = 4096; // a page is enough to prove the point
|
|
|
|
volatile unsigned int *regs =
|
|
(volatile unsigned int *)mmap(0, len, PROT_READ | PROT_WRITE,
|
|
MAP_SHARED, fd, in.bar[b].base);
|
|
if (regs == 0 || (long)regs == -1) {
|
|
puts(" mmap of the BAR failed\n");
|
|
exit();
|
|
}
|
|
puts(" mapped at 0x"); puthex((unsigned long long)regs, 16); puts("\n");
|
|
|
|
puts(" regs[0..3] =");
|
|
for (int i = 0; i < 4; i++) {
|
|
puts(" 0x");
|
|
puthex(regs[i], 8);
|
|
}
|
|
puts("\n");
|
|
|
|
// All-ones across the board means the read did not reach the device: an
|
|
// unmapped PCI read returns 0xFFFFFFFF rather than faulting, so this is
|
|
// the difference between "mapped the wrong thing" and "it works", and it
|
|
// is worth checking rather than eyeballing.
|
|
if (regs[0] == 0xFFFFFFFFu && regs[1] == 0xFFFFFFFFu) {
|
|
puts(" all ones -- the mapping does not reach the device\n");
|
|
} else {
|
|
puts(" MMIO reaches the device\n");
|
|
}
|
|
|
|
struct pciuser_dma dma;
|
|
dma.size = 8192;
|
|
if (ioctl(fd, PCIIOC_DMA, &dma) < 0) {
|
|
puts(" DMA allocation failed\n");
|
|
exit();
|
|
}
|
|
puts(" DMA 8 KB at virtual 0x"); puthex(dma.uaddr, 16);
|
|
puts(", physical 0x"); puthex(dma.paddr, 16); puts("\n");
|
|
|
|
// Write through the mapping and read it back: proves the memory is really
|
|
// there and really writable, which a physical address alone does not.
|
|
volatile unsigned int *buf = (volatile unsigned int *)dma.uaddr;
|
|
buf[0] = 0x5C405; buf[1] = 0xD1DD1E;
|
|
if (buf[0] == 0x5C405u && buf[1] == 0xD1DD1Eu)
|
|
puts(" DMA buffer reads back what was written\n");
|
|
else
|
|
puts(" DMA buffer did not read back correctly\n");
|
|
|
|
// Mechanism 3. Ask for the device's interrupts, then make it interrupt.
|
|
//
|
|
// The e1000 family has an Interrupt Cause Set register at 0x00C8: writing
|
|
// a cause bit asserts the line exactly as the hardware would, which is the
|
|
// only way to test interrupt delivery without traffic arriving from
|
|
// somewhere. Reading ICR at 0x00C0 clears it, which is the ack the kernel
|
|
// cannot do for us.
|
|
if (in.vendor != 0x8086 || in.class != 0x02) {
|
|
puts("\nmechanisms 2 and 4 work: a driver could live out here.\n");
|
|
puts("(interrupts untested -- that part knows only Intel NICs)\n");
|
|
exit();
|
|
}
|
|
|
|
if (ioctl(fd, PCIIOC_IRQ, &idx) < 0) {
|
|
puts(" interrupts refused -- the line is shared with a kernel driver\n");
|
|
puts("\nmechanisms 2 and 4 work: a driver could live out here.\n");
|
|
exit();
|
|
}
|
|
puts(" interrupts routed here (line "); putn(in.irq); puts(")\n");
|
|
|
|
volatile unsigned int *IMS = ®s[0x00D0 / 4]; // interrupt mask set
|
|
volatile unsigned int *ICS = ®s[0x00C8 / 4]; // interrupt cause set
|
|
volatile unsigned int *ICR = ®s[0x00C0 / 4]; // cause read, clears
|
|
|
|
*IMS = 0x00000004; // LSC: link status change, a cause with no traffic
|
|
(void)*ICR; // clear anything already pending
|
|
*ICS = 0x00000004; // assert it
|
|
|
|
// Crash simulation: assert the cause and leave, without reading or
|
|
// acking. The line is asserted and this process is about to stop existing.
|
|
if (argc > 2 && argv[2][0] == 'x') {
|
|
puts(" interrupt asserted; exiting WITHOUT acking (simulated crash)\n");
|
|
exit();
|
|
}
|
|
|
|
unsigned long long n = 0;
|
|
if (read(fd, &n, sizeof n) != (int)sizeof n) {
|
|
puts(" read() did not return an interrupt\n");
|
|
exit();
|
|
}
|
|
puts(" interrupt delivered, count = "); putn((int)n); puts("\n");
|
|
|
|
unsigned int cause = *ICR; // ack at the device
|
|
puts(" ICR = 0x"); puthex(cause, 8); puts("\n");
|
|
write(fd, &n, sizeof n); // re-arm
|
|
|
|
puts("\nall three mechanisms work: a driver could live out here.\n");
|
|
exit();
|
|
}
|