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
This commit is contained in:
Nexoniarz 2026-09-13 18:11:00 +02:00
commit 72074a960e
6 changed files with 331 additions and 12 deletions

View file

@ -68,7 +68,41 @@ link-up set -- which is how you tell a working mapping from one that reads
refused, and everything a process held is released when it exits, because a
driver that crashed must not keep its hardware away from its replacement.
**Interrupts (mechanism 3) and the control channel (mechanism 1) are next.**
**Mechanism 3 exists too.** A process asks for a device's interrupts and then
reads them, uio-style:
```
interrupts routed here (line 10)
interrupt delivered, count = 1
ICR = 0x80000004
```
`0x80000004` is the e1000e's cause register with the bit the test asserted and
INT_ASSERTED set, read back by userspace as its acknowledgement.
The line is **masked by the kernel on arrival and unmasked by userspace's
write()**, and that is the entire design rather than a detail. A PCI interrupt
line is level-triggered: it stays asserted until the cause is cleared *at the
device*, which is a different register on every chip and therefore something
only the driver can do. Signalling EOI on a line that is still asserted
re-enters the handler immediately and for ever -- 72,000 times a second, when
this kernel last did it by accident (history/kernel.md §13).
So a driver that dies mid-interrupt leaves the line **masked, not unmasked**.
The instinct is the other way round -- do not leave the device dead -- and it
is wrong: unmasking an asserted line with no handler attached is a crashed
driver taking the machine down with it. Verified by killing one on purpose:
```
interrupt asserted; exiting WITHOUT acking (simulated crash)
timer: 3431 ticks taken, 3436 expected
```
Five ticks lost in seven seconds, which is the ordinary background. The next
driver to claim the device re-arms the line, so masked is safe without being
permanent -- also verified.
**The control channel (mechanism 1) is what remains.**
### One thing the interface is deliberately not