CHAPTER 2 · THE LOOP
THE RESTART LOOP
The first Windows 2000 checkpoint landed on 28 May. By the end of June the project had a wall with a name — set system security — and a failure that dressed itself as progress: a setup that answered its own wizard, wrote hundreds of thousands of sectors, failed one invisible step, rebooted, and started again. This chapter is about the loop itself, and about the procession of confident diagnoses it dissolved, one controlled experiment at a time.
A setup that answered its own wizard
The campaign opened at pace. The first checkpoint reached git on 28 May 2026 — thirteen files, with a boot log of some thirty runs already behind it — and by 31 May one install had accumulated 47 reboots. Through June, mddosem's run journals read like a machine gathering speed: text-mode setup, then the GUI phase — Installing Devices, Regional Settings, the product key, Date and Time, Networking, Installing Components — a little further every day, apparently.
The failing shape, once finally seen whole, was this. GUI setup runs all the way to
Performing Final Tasks and starts a step called set system security, in
which Windows' Security Configuration Engine stamps access-control descriptors onto
thousands of registry keys and files. Somewhere inside that step, it fails — and appends
one line to \WINNT\setuperr.log:
Then it reboots. There is nothing violent about the reboot: the 8042 keyboard controller — a tiny microcontroller every PC of the era carried — has an output pin that, for reasons rooted in 1984, is wired to the CPU's reset line, and pulsing it is how PC software politely asks the machine to start over. The emulator logged exactly that: a guest-requested reset through the keyboard controller's output port. Windows was not crashing. It was giving up tidily.
And because setup never reached its finish line, it never cleared its own breadcrumbs.
RestartSetup=1 and SystemSetupInProgress=1 stay written in the
registry; the recorded command line stays setup -newsetup. On the next boot
Windows dutifully runs the entire GUI phase again — wizard included. That re-ask is the
tell, and the 30 June journal states it in one sentence: “a real install answers
the wizard once and never re-asks.”
GuestResetSource::KeyboardControllerPulseOutputPort —
the guest's own request, not an emulator fault.Why it fooled everyone for weeks
Seen per pass, the loop is indistinguishable from an install that is nearly done. Each iteration genuinely detects devices, genuinely copies components, genuinely writes hundreds of thousands of disk sectors, and genuinely advances through every setup screen in order. Agents drove the wizard by hand over the emulator's control pipe, watched it get “further”, and logged forward progress — about thirty hours of bounded continuation runs across 23–25 June alone. The 30 June reframe opened with the sentence that stung: the frontier had not moved since 5 June. The only ground truth was the pair of completion flags, and they never changed.
The same day produced the reproduction that mattered. Driven end to end on the pure
interpreter — the emulator's reference engine
(chapter 1) — the failure reproduced exactly, appending one
more error line: 4,477 → 4,598 bytes. Whatever this was, it lived in shared emulation
code, not in the optional recompiler. That evening it got a name:
bug MDD-BUG-KILN-00004.
Two tells from the oracle
The standing controlled experiment behind all of this was the QEMU reference install — the same ISO and the same product key on a Standard-PC configuration (chapter 1) — which sails through set system security and boots to a desktop. Its value was never inspiration; it was discrimination. Two files told the story.
The first tell was setuperr.log itself: on the oracle it is empty — zero
bytes — where ours held 4,477 bytes of the same error. Not a Windows quirk, then; a
divergence with a cause on our side of the fence. The second tell was sharper.
scesetup.log records each phase of the security step, and the two logs match
phase for phase — User Rights, Group Membership, Registry Values, System
Access — until the Registry Keys phase, where ours simply stops, mid-line, without its
closing full stop:
A log that ends mid-line is a process that was killed mid-write: the guest was reset while the Security Configuration Engine was stamping a descriptor onto that very key. And on the next traced run the cut fell somewhere else entirely — further on, among help-file ACLs in the File Security phase — with the storage layer demonstrably healthy at the moment of reset (883,012 write-commits, last flush clean). A failure point that wanders under identical inputs is the fingerprint of something timing-sensitive: not a wrong answer, but a wrong moment. Hold that thought; it takes until chapter 3 to pay off.
The hypothesis board
Every long debugging campaign keeps an informal docket of accusations. This one's is worth rendering in full — because the ratio is the story. Fourteen serious hypotheses were raised against the loop and its descendants; six died by controlled experiment, three were red herrings or went stale, and only five turned out to be real.
The massacre of 30 June
With the loop finally named, 30 June became a marathon of controlled experiments — one day that raised, tested and killed more theories than the preceding month had managed to raise.
It began with database forensics. The security step keeps its working state in
secedit.sdb, a database in Microsoft's ESE format — the same engine that
underlies Exchange and Active Directory, with write-ahead transaction logs and a
checkpoint recording how far the on-disk database has caught up with them. Microsoft
ships a maintenance tool, esentutl, and pointing it at a copy of the
database extracted from the looping disk produced a genuinely alarming picture: state
Dirty Shutdown; checkpoint frozen at log generation 799; and 1,470 one-megabyte
transaction logs — about 1.47 GB of log — behind a tiny database.
The reading wrote itself: ESE logs forever and never flushes, so the emulator must be
losing the completion of asynchronous disk writes. Claude and Codex co-signed it as the
leading hypothesis. Then a syscall trace killed it with one table:
| Operation | Count | What it says |
|---|---|---|
| NtReadFile (asynchronous) | 199 | returned STATUS_PENDING — completions arrived later |
| NtReadFile (synchronous) | 12 | STATUS_SUCCESS |
| NtCreateFile | 18 | opens |
| NtClose | 18 | closes |
| NtFlushBuffersFile | 18 | explicit flushes — the engine could flush |
| NtSetInformationFile | 18 | metadata updates |
| NtQueryInformationFile | 3 | metadata reads |
| NtWriteFile | 0 | the write whose completion was “lost” was never issued |
You cannot lose the completion of a write that never happened. Whatever was wrong sat upstream of the disk — and the day's remaining controls took the rest of the docket apart in sequence:
- The clock. The database's create-time read 01/01/2113 — a date eighty-seven years in the future — and a setup log's clock jumped from 1601 to 2026. A clean run whose clock read a perfectly correct 30 June 2026 failed identically. Co-symptom, not cause.
- Accumulated corruption. A fresh, from-blank install failed byte-identically at the same step — and on the fresh disk the “frozen” checkpoint advanced perfectly normally. The 1.47 GB horror show was an artefact of the loop itself, killing the same process mid-run over and over.
- Turbo time-starvation. The repro runs used the emulator's fastest pacing; perhaps guest time was too compressed for ESE's background work? A control at 60,000 cycles per millisecond — 33× more guest time per unit of work than turbo's 2,000,000 — failed byte-identically.
- Memory pressure. At 64 MiB of guest RAM a wait classifier flagged repeated page-fault churn in 23% of samples — an MMU bug? At 128 MiB, the emulator's hard cap on guest RAM, the churn collapsed to 0.7% and the failure remained. Working-set thrashing: a shaper of symptoms, not a cause.
By nightfall the board was mostly red. The one load-bearing discovery of 30 June was somewhere else entirely.
The bug that was real
To run the VGA BIOS — 16-bit code with a 1980s pedigree — a 32-bit operating system
uses virtual-8086 mode: the 386's scheme for running real-mode code inside a
protected-mode world, one reflected interrupt at a time. Setup's security phase does
exactly this: it issues a V86-mode INT 10h (set text mode 3) into the video
BIOS. Inside that call the BIOS executes PUSHF and POPF —
instructions that are privilege-sensitive in V86 mode and fault by design, so
that the kernel's V86 monitor can emulate them. Routine; it happens constantly.
The fault path is where mddosem was wrong. When the CPU takes an exception out of V86 mode into ring 0, it loads null selectors into the data segment registers DS, ES, FS and GS. But an x86 segment register is two things at once: the visible selector, and a hidden descriptor cache holding a base, a limit and access rights. Intel's manual describes the automatic null (SDM §20.3.1.2); real silicon leaves the hidden cache usable — base zero, four-gigabyte limit — and Windows leans on exactly that. mddosem instead marked the registers unusable, the way a software-loaded null selector would be.
Windows 2000 knows what the silicon actually does, and leans on it.
KiTrap0D — the kernel's general-protection-fault handler — has a
hand-optimised fast path for V86 faults that reads kernel memory through DS without
reloading it first: mov ebx, ds:[0x80481c80] at
0x80467EC1. On mddosem, that load itself faulted. So the #GP
handler raised #GP, whose handler raised #GP — the diagnostic ring captured
182 identical faults at the same address in a single 512-event window,
and then the 8042 pulse.
The fix (commit 837fc8775, v0.91.0) is small and pointed:
SegmentCache::null_flat_usable(), applied only to the CPU's automatic null on
V86 entry — a software mov ds, 0 still yields an unusable segment, as it
must. The storm collapsed from 182 faults to 2, and setup surged: clean through device
detection, the entire wizard, Installing Components, into Performing Final Tasks — far
past the old meltdown. Then it looped anyway, with a new terminal signature. A real bug,
genuinely fixed; a wall, moved rather than removed. Remember the shape of that sentence.
The campaign was about to live inside it for three weeks.
The diagnosis that dissolved
What the post-fix loop pointed at next was, of all things, audio. The terminal state
was now an idle kernel polling a single process-state query
(NtQueryInformationProcess, twelve to fifteen times a sample, always from
the same return address at 0x77F9335C) — setup waiting on a
child process that never finished. With Codex as consulting analyst, the trail was run
all the way to a name: the exact command line of the stuck child, predicted from the setup
logs and then confirmed verbatim in the preserved trace.
WDMAUD is Windows' software audio plumbing — kernel-streaming machinery installed on
every Windows 2000 system regardless of soundcard. The SW\ prefix in its
device path means software-enumerated, which is why muting the emulated Sound
Blaster had been correctly predicted to change nothing. And the damning measurement: the
installer's IOCTL_SWENUM_INSTALL_INTERFACE came back STATUS_SUCCESS thirteen
times in four-tenths of a second — yet the audio interface never appeared in the
registry's DeviceClasses, while four sibling software interfaces sat there, published.
Accepted, never delivered. A precise, mechanistic, internally coherent diagnosis.
It was flagged architecturally significant — do not attempt blind, and on 4 July the bug was formally parked to await Arthur's steer on the deep plug-and-play work it seemed to require. Then the props went missing: by 10 July not one Windows 2000 disk image survived anywhere on the machine, and the repro had to be rebuilt from the retail SP4 ISO with an unattended answer floppy — sixteen minutes of text-mode setup, twenty-two of GUI, wall duly re-confirmed.
And then, on 13 July, the reconciliation. A fresh wall boot on the current binary,
traced verbosely for 45 minutes, contained no streamci, no WDMAUD — no
trace of the audio step whatsoever. Setup was grinding in an earlier device-install phase
(syssetup) and never reached the code the whole theory was about. The
diagnosis had been true of an older binary and had gone quietly stale against this one:
weeks of careful forensic narrowing, dissolved by a single grep.
What the loop taught
Three things were now solid. The loop was real — a deliberate, tidy, self-inflicted reboot, not an emulator crash. The diagnoses were perishable — six refuted, a stack of red herrings, one beautifully-built theory dead of staleness between binary versions. And the one fix that was real had not removed the wall but moved it downstream — a pattern that promptly repeated. When the ATA completion-edge bug was found and fixed on 11 July (chapter 3 owns that mechanism), the install surged from 2.03 million disk write-commits to 3.64 million, and one instrumented run then completed the entire set system security phase cleanly — and hung further on. This was never one bug with one address. It was a class of defect, surfacing wherever Windows happened to lean hardest on the machinery underneath.
Which is why the next move was to stop reading Windows' logs and start reading the machine's wires: how an interrupt actually travels from a drive to a CPU, and all the ways an emulator can silently drop one on the way. That is chapter 3.
Sources for this chapter — bug ledger MDD-BUG-KILN-00004 (raised 30 June 2026); the campaign journals for 25 June (the continuation churn), 30 June (the set-system-security reframe, the ESE forensics and the V86 fix, commit 837fc8775 / v0.91.0) and 13 July (the WDMAUD reconciliation). The QEMU oracle and the rules of engagement are chapter 1's; the full evidence table is in chapter 7.