how a from-scratch PC emulator learned to boot Windows 2000

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.

47
reboots accumulated by one install by 31 May — three days into the campaign
≈15
copies of the same error in setuperr.log at the 30 June reframe (4,477 bytes)
0 B
the QEMU oracle's setuperr.log after the identical phase — the divergence marker
× 0
NtWriteFile calls to secedit.sdb across an entire failing pass — the tally that killed a theory
182 → 2
identical #GP faults at 0x80467EC1 in one 512-event ring, before and after the June fix
45+ min
how long the wall retreats when watched under a verbose trace — versus ~2 min lean

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:

\WINNT\setuperr.log — extracted from the looping disk, 30 June Error: Setup encountered an error while trying to set system security. An extended error has occurred. …repeated ≈15 times — 4,477 bytes, one more copy after every pass

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.”

THE LOOP — ONE PASS, STEP BY STEP ⏮ ⏭ step · ← → keys when focused
…and round again 1 · BOOTRestartSetup = 1 2 · THE WIZARDevery page re-asked 3 · INSTALLING100,000s of sectors written 4 · FINAL TASKSset system security 5 · THE ERRORsetuperr.log +1 line 6 · 8042 PULSEa deliberate reboot setuperr.log 4,477 B error lines ≈ 15 RestartSetup 1 — never clears
Loop anatomy and byte counts from the 30 June reframe journal and bug ledger MDD-BUG-KILN-00004: at the reframe setuperr.log held 4,477 bytes (≈15 copies of the error); the interpreter-driven pass appended one more line (+121 B → 4,598). The reset is logged as 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:

scesetup.log — the same phase on two machines mddosem Configure …\time zones\US Mountain Standard Time ⟵ the log ends here, mid-line — the guest was reset during this key QEMU Configure …\time zones\US Mountain Standard Time. Configure …\time zones\Vladivostok… \Yakutsk… (+ ~260 more keys) Registry keys configuration completed successfully.

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 HYPOTHESIS BOARD — EVERYTHING THE LOOP WAS ACCUSED OF filter by verdict
Hypotheses, verdicts and kills as recorded in bug ledger MDD-BUG-KILN-00004 and the campaign journals. “Real” covers the V86 storm below, the interrupt-delivery bug of chapter 3, the reframed deadlock and the two prompts of chapter 5, and the four fixes of chapter 6.

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:

Every operation ESE issued against secedit.sdb — one full failing pass, object-name-resolved syscall trace, 30 June
OperationCountWhat it says
NtReadFile (asynchronous)199returned STATUS_PENDING — completions arrived later
NtReadFile (synchronous)12STATUS_SUCCESS
NtCreateFile18opens
NtClose18closes
NtFlushBuffersFile18explicit flushes — the engine could flush
NtSetInformationFile18metadata updates
NtQueryInformationFile3metadata reads
NtWriteFile0the 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:

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.

the stuck child — recovered from the preserved process trace: rundll32.exe streamci.dll,StreamingDeviceSetup {CD171DE3-69E5-11D2-B56D-0000F8754380},{9B365890-165F-11D0-A195-0020AFD156E4},{3E227E76-690D-11D2-8161-0000F8775BF1},C:\WINNT\INF\WDMAUDIO.inf,WDM_WDMAUD.Interface.Install

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.