SchOS/include/pciuser.h
Nexoniarz 72074a960e hybrid: interrupts to userspace, and a crashed driver that stays crashed
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
2026-09-13 18:11:00 +02:00

127 lines
5.4 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.
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_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.
// 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