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
468 lines
14 KiB
C
468 lines
14 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// /dev/pci -- PCI devices, reachable from userspace.
|
|
//
|
|
// See include/pciuser.h for what this is and why the interface is about PCI
|
|
// rather than about drivers. This file is the kernel half: enumeration, the
|
|
// claim, the check that makes mmap safe, and DMA memory a device can be
|
|
// pointed at.
|
|
//
|
|
// The claim is the part worth reading carefully. A device that a process has
|
|
// claimed must no longer be touched by the kernel, or two drivers write the
|
|
// same registers and the failure looks like hardware that has gone mad. So
|
|
// claiming records the owner, and the kernel's own drivers are expected to
|
|
// have been told to let go first -- which for now means "do not claim a device
|
|
// the kernel is driving", enforced below rather than assumed.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "spinlock.h"
|
|
#include "sleeplock.h"
|
|
#include "fs.h"
|
|
#include "file.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "proc.h"
|
|
#include "pci.h"
|
|
#include "errno.h"
|
|
#include "uaccess.h"
|
|
#include "irq.h"
|
|
#include "pciuser.h"
|
|
int mappages(pml4e_t *pgdir, void *va, uint64 size, uint64 pa, int perm);
|
|
|
|
// Who holds each device, or 0. Indexed the same way pci_get() is, because the
|
|
// index a caller passes in is the one the bus scan produced and nothing
|
|
// renumbers it afterwards.
|
|
static struct proc *owner[NPCI];
|
|
|
|
static struct spinlock pciuser_lock;
|
|
|
|
// Interrupt delivery, one device per process.
|
|
//
|
|
// The count is what read() returns and what the reader compares against: a
|
|
// driver that was slow sees the count jump rather than missing an edge, which
|
|
// is the property that makes a count better than a flag.
|
|
struct irqdev {
|
|
struct proc *proc; // who reads, 0 when unused
|
|
int index; // pci_get() index
|
|
int irq; // the line
|
|
uint64 count; // interrupts taken
|
|
uint64 seen; // what the reader has consumed
|
|
int masked;
|
|
};
|
|
static struct irqdev irqdev;
|
|
|
|
// DMA buffers handed out, so they can be taken back.
|
|
//
|
|
// The arena is small and contiguous -- it has to be, a device cannot follow a
|
|
// free list -- so a driver that exits without returning its buffers exhausts
|
|
// it in a handful of restarts. Which is precisely the workload this interface
|
|
// invites: the whole promise is that a driver can crash and be restarted, and
|
|
// a promise that leaks a few kilobytes every time it is kept is not one.
|
|
#define PCIUSER_MAXDMA 16
|
|
struct dmablk {
|
|
struct proc *proc;
|
|
void *kva;
|
|
uint64 size;
|
|
};
|
|
static struct dmablk dmablk[PCIUSER_MAXDMA];
|
|
|
|
// The handler. Runs in interrupt context, so it does the two things that
|
|
// cannot wait and nothing else.
|
|
//
|
|
// Masking is the important one. The line is level-triggered and still asserted
|
|
// -- only the device's own driver can clear that, and its driver is a process
|
|
// that is not running yet. Signalling EOI on an asserted line re-enters here
|
|
// immediately and for ever; see docs/history/kernel.md section 13 for what
|
|
// that looks like from outside. So the line goes quiet until userspace says
|
|
// it has dealt with the device.
|
|
static void
|
|
pciuser_irq_handler(void *arg)
|
|
{
|
|
(void)arg;
|
|
irqdev.count++;
|
|
if (!irqdev.masked) {
|
|
ioapicmask(irqdev.irq);
|
|
irqdev.masked = 1;
|
|
}
|
|
wakeup(&irqdev);
|
|
}
|
|
|
|
void
|
|
pciuser_init(void)
|
|
{
|
|
initlock(&pciuser_lock, "pciuser");
|
|
}
|
|
|
|
// Does `phys` fall inside a memory BAR of a device this process has claimed?
|
|
//
|
|
// This is the whole security story for mmap, such as it is. Without it the
|
|
// device is /dev/mem with extra steps: any process could map any physical
|
|
// address, including the kernel's own memory. With it, a process reaches
|
|
// exactly the registers of the devices it was given.
|
|
//
|
|
// Length matters as much as the base. A caller asking for one page at the end
|
|
// of a BAR must not be handed the page after it, which on a real machine is
|
|
// the next device's registers.
|
|
int
|
|
pciuser_mmio_ok(uint64 phys, uint64 len)
|
|
{
|
|
struct proc *p = myproc();
|
|
int n = pci_count();
|
|
|
|
if (len == 0)
|
|
return 0;
|
|
|
|
for (int i = 0; i < n && i < NPCI; i++) {
|
|
if (owner[i] != p)
|
|
continue;
|
|
const struct pci_dev *d = pci_get(i);
|
|
if (!d)
|
|
continue;
|
|
for (int b = 0; b < PCIUSER_MAXBAR; b++) {
|
|
uint64 base = d->bar[b].base, size = d->bar[b].size;
|
|
if (!base || !size || d->bar[b].is_io)
|
|
continue;
|
|
if (phys >= base && phys + len > phys && phys + len <= base + size)
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Release everything a process held. Called when it exits, because a driver
|
|
// that crashed must not keep its device locked away from its replacement --
|
|
// that is the entire point of running drivers in processes.
|
|
void
|
|
pciuser_release_all(struct proc *p)
|
|
{
|
|
acquire(&pciuser_lock);
|
|
for (int i = 0; i < NPCI; i++)
|
|
if (owner[i] == p)
|
|
owner[i] = 0;
|
|
|
|
// And its interrupt line -- unregistered, and left MASKED.
|
|
//
|
|
// The instinct is to unmask on the way out, so the device is not left dead.
|
|
// That is exactly wrong, and for the reason running through this whole file:
|
|
// the driver died, so nobody cleared the cause at the device, so the line is
|
|
// very likely still asserted. Unmasking it with no handler attached means
|
|
// every EOI re-enters an empty dispatch, for ever -- a crashed driver taking
|
|
// the machine down with it, which is the precise opposite of what running
|
|
// drivers in processes is for.
|
|
//
|
|
// Masked is safe and it is not permanent: PCIIOC_IRQ arms the line when the
|
|
// next driver claims the device.
|
|
// Its DMA buffers. The process's page tables are torn down by exit() either
|
|
// way; what needs saying here is that the *arena* gets its pages back.
|
|
for (int i = 0; i < PCIUSER_MAXDMA; i++) {
|
|
if (dmablk[i].proc != p)
|
|
continue;
|
|
if (dmablk[i].kva)
|
|
dma_free(dmablk[i].kva, dmablk[i].size);
|
|
dmablk[i].proc = 0;
|
|
dmablk[i].kva = 0;
|
|
dmablk[i].size = 0;
|
|
}
|
|
|
|
if (irqdev.proc == p) {
|
|
irq_unregister(irqdev.irq, pciuser_irq_handler, 0);
|
|
ioapicmask(irqdev.irq);
|
|
irqdev.masked = 1;
|
|
irqdev.proc = 0;
|
|
}
|
|
release(&pciuser_lock);
|
|
}
|
|
|
|
static void
|
|
copy_name(char *dst, const char *src, int cap)
|
|
{
|
|
int i = 0;
|
|
if (src)
|
|
for (; i < cap - 1 && src[i]; i++)
|
|
dst[i] = src[i];
|
|
dst[i] = 0;
|
|
}
|
|
|
|
static int
|
|
do_info(uint64 argp)
|
|
{
|
|
struct pciuser_info *ui = (struct pciuser_info *)user_ptr(argp, sizeof(*ui));
|
|
if (!ui)
|
|
return -EFAULT;
|
|
|
|
unsigned int idx = ui->index;
|
|
if (idx >= (unsigned int)pci_count())
|
|
return -ENODEV;
|
|
|
|
const struct pci_dev *d = pci_get((int)idx);
|
|
if (!d)
|
|
return -ENODEV;
|
|
|
|
memset(ui, 0, sizeof(*ui));
|
|
ui->index = idx;
|
|
ui->bus = d->bus;
|
|
ui->slot = d->slot;
|
|
ui->func = d->func;
|
|
ui->header_type = d->header_type;
|
|
ui->vendor = d->vendor;
|
|
ui->device = d->device;
|
|
ui->class = d->class;
|
|
ui->subclass = d->subclass;
|
|
ui->prog_if = d->prog_if;
|
|
ui->revision = d->revision;
|
|
ui->irq = d->irq;
|
|
ui->claimed = owner[idx] ? 1 : 0;
|
|
|
|
if (owner[idx])
|
|
copy_name(ui->driver, "userspace", sizeof(ui->driver));
|
|
else
|
|
copy_name(ui->driver, d->driver, sizeof(ui->driver));
|
|
|
|
for (int b = 0; b < PCIUSER_MAXBAR; b++) {
|
|
ui->bar[b].base = d->bar[b].base;
|
|
ui->bar[b].size = d->bar[b].size;
|
|
ui->bar[b].flags = 0;
|
|
if (d->bar[b].base) {
|
|
ui->bar[b].flags |= d->bar[b].is_io ? PCIUSER_BAR_IO : PCIUSER_BAR_MMIO;
|
|
if (d->bar[b].prefetch)
|
|
ui->bar[b].flags |= PCIUSER_BAR_PREFETCH;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
do_claim(uint64 argp)
|
|
{
|
|
int idx;
|
|
if (copy_from_user(&idx, argp, sizeof(idx)) < 0)
|
|
return -EFAULT;
|
|
if (idx < 0 || idx >= pci_count() || idx >= NPCI)
|
|
return -ENODEV;
|
|
|
|
const struct pci_dev *d = pci_get(idx);
|
|
if (!d)
|
|
return -ENODEV;
|
|
|
|
// Refuse a device the kernel is already driving.
|
|
//
|
|
// Two drivers on one device is not a subtle failure -- it is two writers of
|
|
// the same registers, and what it looks like from outside is hardware that
|
|
// has started lying. The eviction path (docs/plan/hybrid.md) is for the
|
|
// kernel to *hand over* a device deliberately; until a driver can be told to
|
|
// let go, the honest answer to "may I have the one you are using" is no.
|
|
if (d->driver)
|
|
return -EBUSY;
|
|
|
|
acquire(&pciuser_lock);
|
|
if (owner[idx] && owner[idx] != myproc()) {
|
|
release(&pciuser_lock);
|
|
return -EBUSY;
|
|
}
|
|
owner[idx] = myproc();
|
|
release(&pciuser_lock);
|
|
|
|
// Bus mastering and memory space, because a driver that has to ask for this
|
|
// separately will forget, and the symptom -- DMA that silently does nothing
|
|
// -- costs a day to find.
|
|
pci_enable((struct pci_dev *)d, 0x0006);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
do_release(uint64 argp)
|
|
{
|
|
int idx;
|
|
if (copy_from_user(&idx, argp, sizeof(idx)) < 0)
|
|
return -EFAULT;
|
|
if (idx < 0 || idx >= NPCI)
|
|
return -ENODEV;
|
|
|
|
acquire(&pciuser_lock);
|
|
if (owner[idx] == myproc())
|
|
owner[idx] = 0;
|
|
release(&pciuser_lock);
|
|
return 0;
|
|
}
|
|
|
|
// Memory a device can be pointed at.
|
|
//
|
|
// mmap cannot do this: a process can map memory, but nothing tells it the
|
|
// *physical* address, and a device only understands physical addresses. So
|
|
// the kernel allocates from the DMA arena -- which is contiguous, which
|
|
// kalloc's free list is not -- maps it in, and reports both.
|
|
static int
|
|
do_dma(uint64 argp)
|
|
{
|
|
struct pciuser_dma *req = (struct pciuser_dma *)user_ptr(argp, sizeof(*req));
|
|
if (!req)
|
|
return -EFAULT;
|
|
|
|
uint64 size = PGROUNDUP(req->size);
|
|
if (size == 0 || size > 4 * 1024 * 1024)
|
|
return -EINVAL;
|
|
|
|
// A slot to remember it by, found before allocating: handing out memory that
|
|
// cannot be recorded is how the leak this fixes got written in the first
|
|
// place.
|
|
struct dmablk *blk = 0;
|
|
acquire(&pciuser_lock);
|
|
for (int i = 0; i < PCIUSER_MAXDMA; i++)
|
|
if (!dmablk[i].proc) { blk = &dmablk[i]; blk->proc = myproc(); break; }
|
|
release(&pciuser_lock);
|
|
if (!blk)
|
|
return -ENOMEM;
|
|
|
|
void *kva = dma_alloc(size, PGSIZE);
|
|
if (!kva) {
|
|
blk->proc = 0;
|
|
return -ENOMEM;
|
|
}
|
|
|
|
// Its own mapping in the caller, taken from the same bump allocator mmap
|
|
// uses so the two cannot collide.
|
|
struct proc *p = myproc();
|
|
if (p->mmap_next == 0)
|
|
p->mmap_next = 0x40000000;
|
|
uint64 va = p->mmap_next;
|
|
p->mmap_next += size;
|
|
|
|
uint64 phys = dma_phys(kva);
|
|
if (mappages(p->pgdir, (void *)va, size, phys, PTE_W | PTE_U) < 0) {
|
|
dma_free(kva, size);
|
|
blk->proc = 0;
|
|
return -ENOMEM;
|
|
}
|
|
blk->kva = kva;
|
|
blk->size = size;
|
|
|
|
req->uaddr = va;
|
|
req->paddr = phys;
|
|
return 0;
|
|
}
|
|
|
|
// Hand a device's interrupts to the calling process.
|
|
static int
|
|
do_irq(uint64 argp)
|
|
{
|
|
int idx;
|
|
if (copy_from_user(&idx, argp, sizeof(idx)) < 0)
|
|
return -EFAULT;
|
|
if (idx < 0 || idx >= pci_count() || idx >= NPCI)
|
|
return -ENODEV;
|
|
if (owner[idx] != myproc())
|
|
return -EACCES; // claim it first
|
|
|
|
const struct pci_dev *d = pci_get(idx);
|
|
if (!d || d->irq == 0xFF)
|
|
return -ENODEV; // the firmware routed it nowhere
|
|
|
|
// Refuse a line the kernel is already listening on.
|
|
//
|
|
// This handler masks the line, and a masked line is masked for *everyone* on
|
|
// it. Doing that to a shared line silences whichever kernel driver is also
|
|
// there, and the symptom -- a device that simply stops, with nothing logged
|
|
// -- is about as hard to find as bugs get. MSI is the real answer, since it
|
|
// is per-device and cannot be shared; pci_msi_enable() is already in the
|
|
// tree waiting for a caller.
|
|
if (irq_handler_count(d->irq) > 0)
|
|
return -EBUSY;
|
|
|
|
if (irqdev.proc && irqdev.proc != myproc())
|
|
return -EBUSY; // one device per process, and one process
|
|
|
|
irqdev.proc = myproc();
|
|
irqdev.index = idx;
|
|
irqdev.irq = d->irq;
|
|
irqdev.count = 0;
|
|
irqdev.seen = 0;
|
|
irqdev.masked = 0;
|
|
|
|
// Level-triggered and active low, like every PCI INTx line.
|
|
irq_register(d->irq, pciuser_irq_handler, 0, "userspace", 1);
|
|
|
|
// Arm it. The line may have been left masked by whatever held this device
|
|
// before -- see pciuser_release_all -- and a driver that has to write()
|
|
// before its first read() to get going is an interface nobody will guess.
|
|
ioapicroute(d->irq, 0, 1);
|
|
irqdev.masked = 0;
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
pciuser_ioctl(uint64 request, uint64 argp)
|
|
{
|
|
switch (request) {
|
|
case PCIIOC_COUNT: {
|
|
int n = pci_count();
|
|
put_user(n, argp);
|
|
return 0;
|
|
}
|
|
case PCIIOC_INFO: return do_info(argp);
|
|
case PCIIOC_CLAIM: return do_claim(argp);
|
|
case PCIIOC_RELEASE: return do_release(argp);
|
|
case PCIIOC_DMA: return do_dma(argp);
|
|
case PCIIOC_IRQ: return do_irq(argp);
|
|
default: return -ENOTTY;
|
|
}
|
|
}
|
|
|
|
// Wait for the device to interrupt.
|
|
//
|
|
// Returns the total count rather than a flag, so a driver that was busy can
|
|
// see it missed three and act once -- which is what you want, because the
|
|
// alternative is either losing them or queueing them, and a device does not
|
|
// care how many times you noticed it.
|
|
int
|
|
pciuser_read(struct inode *ip, uint64 off, char *dst, int n)
|
|
{
|
|
(void)off;
|
|
|
|
if (irqdev.proc != myproc())
|
|
return -1;
|
|
if (n < (int)sizeof(uint64))
|
|
return -1;
|
|
|
|
iunlock(ip);
|
|
acquire(&pciuser_lock);
|
|
while (irqdev.count == irqdev.seen) {
|
|
if (myproc()->killed) {
|
|
release(&pciuser_lock);
|
|
ilock(ip);
|
|
return -1;
|
|
}
|
|
sleep(&irqdev, &pciuser_lock);
|
|
}
|
|
uint64 c = irqdev.count;
|
|
irqdev.seen = c;
|
|
release(&pciuser_lock);
|
|
ilock(ip);
|
|
|
|
memmove(dst, &c, sizeof(c));
|
|
return (int)sizeof(c);
|
|
}
|
|
|
|
// "I have dealt with the device" -- unmask the line.
|
|
//
|
|
// The driver must have cleared the interrupt *at the device* before this, or
|
|
// the line is still asserted and unmasking simply re-enters the handler. That
|
|
// is the contract, it cannot be checked from here, and getting it wrong is the
|
|
// 72,000-interrupts-a-second failure in docs/history/kernel.md section 13.
|
|
int
|
|
pciuser_write(struct inode *ip, uint64 off, char *src, int n)
|
|
{
|
|
(void)ip; (void)off; (void)src;
|
|
|
|
if (irqdev.proc != myproc())
|
|
return -1;
|
|
|
|
acquire(&pciuser_lock);
|
|
if (irqdev.masked) {
|
|
ioapicroute(irqdev.irq, 0, 1); // level, active low: a PCI INTx line
|
|
irqdev.masked = 0;
|
|
}
|
|
release(&pciuser_lock);
|
|
return n;
|
|
}
|