CHAPTER 3 · INTERRUPT DELIVERY
EDGES & WIRES
A Windows 2000 install is millions of disk writes, and every one of them ends the same way: a wire goes high, a controller latches an edge, a processor is interrupted. The emulator had the letter of that sequence right and the physics wrong — at layer after layer. This chapter is how interrupts really work, and how an emulator can quietly lose them.
How a disk becomes code
When Windows 2000 writes a sector, the driver programs the drive and then puts the calling thread to sleep. Nothing about the write is synchronous: the drive works alone, and when it finishes it announces the fact electrically — it raises a wire called INTRQ, the interrupt request line it shares with the interrupt controller.
That controller is the Intel 8259 — two of them, in the classic PC arrangement, a master and a slave chained together; the disk's line, IRQ 14, arrives at the slave. Inside the 8259 sits the IRR, the Interrupt Request Register, and on an edge-triggered line the chip's job is to capture the transition: the instant INTRQ rises, the IRR bit is latched. A second register, the IMR, is a mask — a request can be latched yet held back. When a latched request is unmasked, the controller asserts INT to the processor; the processor acknowledges, receives a vector, and jumps through the interrupt table into the driver's interrupt service routine.
The ISR's first meaningful act is to read the drive's Status register at port
0x1F7 — and that read is itself part of the protocol. Per the ATA
specification (ATA-4 §6.3.5), reading Status tells the drive its interrupt has been
noticed, and the drive answers by deasserting INTRQ. There are two acknowledgements at
two layers: the Status read quenches the drive, and the EOI later releases the
controller. Between them sits the hardware truth this whole chapter turns on:
the falling wire cannot reach back into the 8259 and erase the edge it already
captured. By the time any software can read Status, the request is the
controller's property.
A latch with too many erasers
mddosem does not deliver interrupts in the middle of an
instruction. Its device models latch a one-shot pending-edge flag —
irq_pic_edge_pending — and a delivery pump collects those flags and presents
them to the emulated 8259 at quantum boundaries, roughly once per emulated millisecond.
Deferred delivery is a respectable emulator architecture; QEMU's timing is similarly
coarse. It has one absolute requirement: between assertion and delivery, the latch is
sacred.
It was not. The code paths that deassert INTRQ on a real drive — a guest read of the Status port, the issue of a new command, and, in the ATAPI model, the issue of a packet — also cleared the flag. So a completion whose Status the driver happened to poll before the pump ran was silently destroyed: the sector was on the disk, no system call had failed, the drive looked healthy — and the interrupt announcing all of this had never existed. Which completions died was decided by host-scheduling jitter, so the failure moved every time anyone looked at it. And it reproduced on both execution engines, because the storage models are shared.
A Fable 5 consult on 11 July pinned the mechanism; the reasoning was verified in the code and confirmed by reproduction before the fix was applied. The fix itself is almost an anticlimax: the pending edge may be consumed by the delivery pump or by a device reset, and by nothing else. Two existing unit tests turned out to have encoded the wrong behaviour — one solemnly asserting that a Status read revokes the latch, the other that nIEN does — and were rewritten rather than weakened. The effect was immediate: the install, which had been dying at around 2.03 million guest disk write-commits, ran on to 3.64 million — straight into a new wall, but that is the shape of this whole story.
A sibling fix the same evening closed the last eraser. nIEN is a bit in the drive's Device Control register that masks its interrupt output, and the ATA-6 standard (§7.9) is precise about what masking means: the drive tri-states INTRQ — electrically disconnects the wire. A disconnected wire is a falling edge, and a falling edge cannot unlatch a captured request. The old code revoked the latched edge whenever the guest set nIEN; the fix deletes that branch and keeps only the legitimate behaviour, re-driving the line when nIEN falls while a request is still pending.
The NT amplifier
Why had years of DOS software never tripped any of this? Because DOS closes the window
almost before it opens: a DOS handler runs with the request already delivered, and the
interval in which a latched edge sits waiting is measured in microseconds. NT is
different by design. Its Standard PC HAL implements interrupt priority — IRQL — by
rewriting the 8259's mask register on every KfRaiseIrql and
KfLowerIrql, and it sends EOI at ISR entry. Under NT, requests
routinely sit latched-but-masked in the IRR for whole ISR-plus-DPC windows — the handler
and the deferred work it queues — which is to say milliseconds. As the
wire-conformance analysis put it, every destruction bug in this class has a window
thousands of times wider under NT than under DOS.
The emulator's own deferral stacked on top: the delivery pump stretches the vulnerable interval from near-zero instructions to as much as a full quantum. Deferral turns a latent inconsistency into a live bug — and Windows 2000, the first guest to hold requests masked for so long, was the first workload wide enough to catch it.
One more piece of ground truth was dumped from the guest itself before the seam was
rebuilt. On PCI-era chipsets, edge versus level is programmable per line through the
ELCR, the Edge/Level Control Register. On 14 July the team read back
what Windows 2000 actually programs: 0x0C00. Only IRQ 10 and 11 — the
PCI interrupt lines — are level-triggered; IRQ 3, 4, 6 and 14 are all edge. The
disk interrupt really is an edge, with everything that implies.
Three oracles, one hybrid
The ATA fix repaired one device; the disease was more general. The emulated 8259's own
lower_irq cleared the latched IRR bit whenever a device's line fell —
"line-follows" semantics — so any device whose wire dropped before the pump ran
could lose an interrupt. Serial resync, the real-time clock's Status-C read and the ATA
soft reset were all members of the same lost-interrupt class. Before changing a contract
that the entire DOS-era corpus had been passing against, the team asked the references.
They disagreed.
| Reference | On deassert of an edge line | Why it gets away with it |
|---|---|---|
QEMU (hw/intc/i8259.c) |
IRR bit retained — capture-at-assert | Coarse timing: it cannot guarantee the CPU samples INT before the line drops, so the latch must survive |
DOSBox-X (pic.cpp) |
Request cleared — line-follows | Delivery in effectively zero time: the edge is consumed before anything can clear it |
| Real 8259A silicon | Nearest DOSBox-X — a request that drops before acknowledge becomes the classic spurious IRQ 7 | Nanosecond electronics; the race window barely exists |
| mddosem, pre-fix | Cleared — line-follows, with delivery deferred to the next quantum | It didn't. The hybrid opened a window of up to a full quantum in which a latched edge could die |
The disagreement is decisive rather than confusing once timing enters the picture. DOSBox-X survives line-follows because it delivers interrupts at once; QEMU retains the IRR bit because its timing is too coarse to promise that. mddosem's timing is QEMU-shaped, so QEMU's contract — capture-at-assert — is the correct one for it. And the verdict on the pre-fix emulator was the sharpest line of the campaign: Windows 2000 installs on both QEMU and DOSBox-X. "What W2K cannot survive is our hybrid" — line-follows semantics bolted to quantum-deferred delivery.
Adopting capture-at-assert initially broke sixteen tests, which looked like evidence the old semantics were load-bearing. A five-analyst read-only review, a synthesis, and an adversarial refutation pass — "cannot refute" was the required standard — established the opposite: the collateral was self-inflicted. The spurious-IRQ 7 path that appeared to depend on line-follows had no production caller at all, and the failing tests were asserting the emulator's habits, not the hardware's. All sixteen were updated, not weakened; two were rewritten outright because their premise was void. The cheapest decisive check was a divergence counter placed on the exact state where the two models differ: across DOOM and F1GP it fired zero times, in Civilization four — every firing an IRQ 0 timer tick the new model correctly retained — and a wider fourteen-game sweep found only a handful more, all equally benign. The change landed ELCR-aware on 14 July (mddosem-hw 0.41.0): edge lines keep their captured requests; level lines still follow the wire, as real level lines do.
The same work left behind a permanent instrument: a wire-conformance oracle that drives the production PIC and a deliberately simple reference 8259 through adversarial pump-boundary schedules, with calibration rows that must re-find each already-fixed bug — "an oracle that misses the bugs we already found is worthless". The reflection on method belongs to chapter 7; the machinery lives on in mddosem-hw's test suite.
The southbridge that lied by omission
Between the drive and the 8259 sits one more chip this chapter owes an account of:
the Intel PIIX3, the southbridge of the emulated board, whose IDE function answers PCI
enumeration as device 8086:7010. When the Windows 2000 campaign began,
mddosem's PIIX3 IDE was a configuration-space stub: programming-interface byte
0x00, no BAR4 — no bus-master register window at all. DOS never noticed,
because DOS talks to the legacy ports. NT interrogates the controller. Its IDE drivers —
intelide, pciide — bind to the PIIX3 and require the bus-master
window to initialise the storage stack; without it, initialisation fails and the kernel
bugchecks STOP 0x0000007B INACCESSIBLE_BOOT_DEVICE — which is
exactly what a transplanted QEMU-installed disk hit during the July transplant
experiments (the boot chain is chapter 1's subject; the
transplant end-run itself later died of a transplant-inherent wall and was rejected as
doctrine-violating anyway).
So on 13 July the PIIX3 grew a real engine: bus-master command, status and
descriptor-pointer registers in a sixteen-byte I/O window at BAR4 (placed at
0xC000, QEMU's conventional base); a walker for the PRD scatter-gather
tables that describe bus-master transfers; READ DMA and WRITE DMA command
support; an IDENTIFY block honestly advertising the DMA modes. The irony is complete:
the installed Windows 2000 ran its disk transfers in PIO mode throughout. The active
ingredient was the window's existence — BAR4 being there let
intelide initialise, and the storage stack came up.
Fidelity kept mattering after the engine worked. During a bus-master transfer the emulation briefly exposed two states no real hardware ever produces: the drive reported idle-and-ready with a DMA command still outstanding, and the controller's bus-master Active bit was set and cleared inside a single pump call, so no guest could ever observe it as 1. Windows' IdePort tick handler polls both. The 15 July fix holds BSY from command acceptance to completion, per the ATA-6 command model, and ties Active to the Start bit for the whole in-flight window — a deliberate, documented divergence from modern QEMU (≥ 2.8), which uses BSY=0/DRQ=1 during transfer.
And then the status bit itself. On a real PIIX3, the moment a drive asserts INTRQ the controller latches that channel's Interrupt bit in BMISTA, its bus-master status register — whether or not a DMA transfer is running. mddosem raised IRQ 14 with BMISTA reading zero, and the secondary channel's status register was hardwired to zero, so IRQ 15 could never appear there at all. A PCI IDE interrupt service routine is entitled to look down at its controller, see "no channel interrupt", and dismiss the IRQ as not-mine — the strongest concrete explanation on record for setup dismissing its own final completions, though the exact terminal IRQ was never caught in the act. The diagnosis came out of the RAM-dump census that chapter 4 recounts. An independent review sharpened the fix before it landed: latching at the quantum-end pump was still too late, because the interpreter can execute another I/O instruction in the same batch and read stale status — so the latch must be visible before the I/O instruction that provoked it returns. The final engine keeps two independent channel banks, write-one-to-clear semantics and Start/Stop cancellation, and 5,544 mddosem-hw tests were green when it landed at 01:09 on 16 July. This is the blue pane of the four-fix flag :
The other three panes — the interrupt shadow, the parked timer and the four-megabyte page — are CPU-level stories, and they belong to chapter 6.
The wall moves downstream
Notice the drumbeat. The edge-destruction fix was real hardware truth, and the wall moved from 2.03 to 3.64 million write-commits. Capture-at-assert was real, and by its own commit message unblocked nothing. The bus-master engine was real, and carried the kernel from STOP 0x7B into the boot splash and driver load. Each correction removed a genuine lie the emulator had been telling, and each time Windows walked further before stopping somewhere new — until it stopped somewhere no log, no trace and no oracle could see: every thread waiting, on a screen that stayed black for four days. Making that silence speak took a kernel debugger with no symbols and a census of a 128 MiB RAM dump — chapter 4.
Sources for this chapter — the ATA/ATAPI completion-IRQ edge-destruction HLD (2026.07.11, mechanism pinned by a Fable 5 consult) and the PIC wire-conformance oracle HLD (2026.07.13–14, authored by Fable 5, verified by Claude); the bug ledger MDD-BUG-KILN-00004; the commit trail of 11–16 July 2026 (mddosem-hw 0.34.0 → 0.42.0, capture-at-assert at 0.41.0, BMISTA latch-at-assert e0e9fe9a5); the guest ELCR dump of 14 July. Diagram states are recreations from the HLDs, not traces. Full evidence: how we know.