Diagnosed and fixed the crash that killed HAVOC_NOCD.EXE ~1s after launch
(dgVoodoo window appears black, then the process exits). It was not a clean
exit: an ACCESS_VIOLATION (null-pointer read at VA 0x444E6A) during init.
Root cause is a copy-protection check in the WORLDS\ data-file loader
(0x42a480). It opens each file (GRAFIX/LAND/MAP/STUF) from both a local
"WORLDS\<name>" path and a CD-drive path (HAVOC.INI [SETUP] DRIVE=D:\), and
returns a valid stream only when the local open fails and the CD-drive open
succeeds. With files present locally and no D: drive it returned NULL, and the
GRAFIX loader (0x444db0) dereferenced that NULL without checking.
Fix: 14-byte patch at FO 0x29a2b rewrites the return decision to hand back the
successfully-opened local object. The game now boots to the title screen
("HAVOC(tm) by Reality Bytes"), responsive, main loop running.
- docs/PATCHES.md: full patch table (28 patches) + crash write-up
- tools/: RE + patching scripts (r2pipe disasm, minidump parser, ctypes
debugger, PE/IAT/import analysis)
- run_probe.bat / run_and_log.bat: reliable native launch for repro
- .gitignore: exclude CD images, Ghidra install/project, dgVoodoo, *.ini
Diagnosed via WER minidumps (%LOCALAPPDATA%\CrashDumps) parsed in pure Python
(no cdb/windbg available).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
5 KiB
HAVOC_NOCD.EXE — Binary Patches (run on modern Windows without CD)
HAVOC_NOCD.EXE is a patched copy of the original HAVOC.EXE (485,376 bytes, 32-bit PE i386,
DirectDraw + DirectSound). The patches let it run from a hard-drive install on Windows 11 via
dgVoodoo2 (DirectDraw wrapper, windowed mode), with no CD mounted
and no admin rights.
Do not commit HAVOC_NOCD.EXE or the original HAVOC.EXE (copyright).
Section layout / file-offset (FO) to virtual-address (VA) mapping
| Section | Raw FO start | RVA | FO → VA formula |
|---|---|---|---|
.text |
0x00400 | 0x01000 | VA = FO + 0x400C00 |
.data |
0x69400 | 0x7C000 | VA = FO + 0x412C00 |
.rdata |
0x67000 | 0x79000 | VA = FO - 0x67000 + 0x79000 + 0x400000 |
.idata |
0x6EC00 | 0x82000 | VA = FO + 0x413400 |
.text VirtualSize was extended from 0x66bbd to 0x66c00 (PE header FO 0x180) so the Patch-V
code cave at VA 0x467bce is mapped into memory.
Patch table
| FO | VA | Bytes (→) | Purpose |
|---|---|---|---|
| 0x00180 | (PE hdr) | bd6b→006c |
Extend .text VirtualSize to cover Patch-V cave |
| 0x094fd | 0x40a0fd | 66 b8 01 00→66 33 c0 90 |
Patch S: CreateSoundBuffer fail returns AX=0 (continue w/o sound) |
| 0x20277 | 0x420e77 | 75→eb |
Force a Jcc branch (skip) |
| 0x29613 | 0x42a213 | 8844242c→90909090 |
Patch D: NOP drive-letter overwrite (INTRFACE) |
| 0x29623 | 0x42a223 | 88442410→90909090 |
Patch D: NOP drive-letter overwrite (MUSIC) |
| 0x296cc | 0x42a2cc | 7c→eb |
Patch Q: skip check |
| 0x29b02 | 0x42a702 | 18 bytes → NOP | Remove drive-type compare/branches |
| 0x29b38 | 0x42a738 | 75→eb |
Drive-check skip |
| 0x29b80 | 0x42a780 | 74→eb |
CD-check skip |
| 0x29bb0 | 0x42a7b0 | 81ec6c010000→b001c39090 90 |
NOCD stub = MOV AL,1; RET |
| 0x29d63 | 0x42a963 | 75→eb |
Extra CD skip |
| 0x29e10 | 0x42aa10 | 74→eb |
CD skip 2 |
| 0x29a2b | 0x42a62b | 8b442414 85c0 8b442410 752a 85c0 → 8b442410 85c0 754e 8b442414 eb48 |
Copy-protection / GRAFIX fix (see below) |
| 0x2cd70 | 0x42d970 | 81→c3 |
Patch N3: show_error_dialog returns immediately (silence popups) |
| 0x2cf2e | 0x42db2e | 74→eb 16 |
Corrupt/unavailable bypass |
| 0x30393 | 0x430f93 | e888c9ffff→NOP |
NOP a validation CALL |
| 0x42514 | 0x443114 | b8010000→e9b54a02 |
Patch V: JMP to vtable-setter code cave |
| 0x66fce | 0x467bce | cave | Patch V cave: sets vtable=1, returns EAX=1 |
| 0x6b27c | 0x47de7c | C:\MUSIC.FF→MUSIC.FF |
Strip drive prefix |
| 0x6b288 | 0x47de88 | C:\INTRFACE.FF→INTRFACE.FF |
Strip drive prefix |
| 0x6b2b8 | 0x47deb8 | C:\WORLDS\→WORLDS\ |
Strip drive prefix |
| 0x6b390 | 0x47df90 | C:\WORLDS\LAND0101.DAT→WORLDS\... |
Strip drive prefix |
| 0x6b3a8 | 0x47dfa8 | C:\HAVOCDAT.DAT→HAVOCDAT.DAT |
Strip drive prefix |
| 0x6b3b8 | 0x47dfb8 | C:\BIGFILE.DAT→BIGFILE.DAT |
Strip drive prefix |
The copy-protection / GRAFIX crash (the "black window then closes" bug)
Symptom: dgVoodoo window appears for ~1 second, all black, then the process disappears.
Actual cause: an ACCESS_VIOLATION (NULL-pointer read) at VA 0x444E6A during init — found
by parsing the WER minidump in %LOCALAPPDATA%\CrashDumps (the crash call stack shows
0x434073 → 0x407CCB, i.e. inside WinMain → 0x434040 init).
The WORLDS\ data-file loader 0x42a480 opens each file (GRAFIX00-03.DAT, LAND*, MAP*,
STUF*) from two locations: the local WORLDS\<name> path and a path built from the CD
drive letter in HAVOC.INI ([SETUP] DRIVE=D:\). Its return logic returns a valid stream
only when the local open fails and the CD-drive open succeeds — a copy-protection check that
assumes data lives on the CD. With the files present locally and no D: drive, it returns NULL;
the caller 0x444db0 (GRAFIX loader) dereferences that NULL without checking → crash.
Fix (FO 0x29a2b): rewrite the return decision to prefer the successfully-opened local object:
0x42a62b: mov eax,[esp+0x10] ; local WORLDS\ stream object
0x42a62f: test eax,eax
0x42a631: jnz 0x42a681 ; if local open OK, return it
0x42a633: mov eax,[esp+0x14] ; else return the drive object
0x42a637: jmp 0x42a681
After this patch the game runs (window title "HAVOC(tm) by Reality Bytes", responsive, main loop executing) instead of crashing.
Reproducing / testing
The crash is launch-environment dependent (it reproduces when launched normally but not under a debugger). Reliable native run:
:: run_probe.bat
cd /d Z:\Development\devl\Havoc
.\HAVOC_NOCD.EXE
Notes:
NoDefaultCurrentDirectoryInExePathmay be set — invoke the exe with a.\prefix.- Launching via MSYS/Git-Bash segfaults (exit 139) and does not produce a WER dump; use
cmd. - Minidumps can be parsed with plain Python (no cdb/windbg needed) — see the exception stream (type 6) for the faulting EIP and the memory streams (5/9) to walk the stack.