Skip to content

[wowbox64] WoW 3.3.5a (32-bit WOW64 guest) crashes or freezes at character list — EmuRun host-pointer corruption and recursive exception-delivery guard-page loop #4380

Description

@Robxmai

Summary

Running a 32-bit Windows guest (World of Warcraft 3.3.5a) under Hangover
WowBox64 on ARM64 Android fails at the character list in two different ways,
while the same prefix works under libwow64fex.dll (FEX) and under the legacy
x86-64-on-box64 path:

  1. Crash form: the host register x19, which holds the C local emu
    pointer inside EmuRun (src/dynarec/dynarec.c), is corrupted to 0x14
    during a JIT excursion. The next use, ldr w1, [x19, #0x548]
    (emu->fork), reads address 0x55C and the guest reports ERROR Map bricks below first 2GB of address space #132
    (0xC0000005, read at 0x55C).
  2. Freeze form: Wine's 32-bit exception delivery
    (wow64.dll!call_user_exception_dispatcher) repeatedly builds a
    WOW64_CONTEXT delivery frame on the guest stack. Each delivery consumes
    0x340 bytes; after enough recursive deliveries the frame write lands in a
    PROT_NONE guard page below the thread stack. The faulting store loops
    forever at 100% CPU.

Both failures begin immediately after the client requests the character list
(COP_GET_CHARACTERS).

Environment

  • Device: Xiaomi Pad 6S Pro 12.4 (24018RPACC), Snapdragon 8 Gen 2,
    Adreno 740, Android 16, aarch64
  • Runtime: Termux; Hangover hangover-wine 11.16 and
    hangover-wowbox64 11.16 (WowBox64 arm64 v0.4.4)
  • Graphics: DXVK 2.4.1, Turnip Mesa 26.0.6, X11 3048x2032@120
  • Guest: World of Warcraft 3.3.5a build 12340 (32-bit, Wow.exe)
  • Translator: default HODLL=wowbox64.dll; CPU affinity 4-7
  • Box64 source: tag v0.4.4, commit
    2f130fab1d6e1a4ee8a71dc60cfdfcc839ad192a

Reproduction

  1. Start the 32-bit client with WowBox64/Hangover.
  2. Log in (auth succeeds: LOGIN_STATE_AUTHENTICATED, AUTH_OK).
  3. Wait for the character list. The failure appears within ~0.1-1 s of
    COP_GET_CHARACTERS.

With libwow64fex.dll the same character list loads. The legacy
box64/Wine path also passes. On Windows the client does not crash here.

Crash form evidence

Crash text (one of several runs, all equivalent):

ERROR #132 (0x85100084) Fatal Exception
Exception: 0xC0000005 (ACCESS_VIOLATION) at 0023:7A953828
The instruction at "0x7A953828" referenced memory at "0x0000055C".
EAX=00401000 EBX=024DF9EC ECX=00000000 EDX=00000000 ESI=EDI=00002340
EBP=024DFA04 ESP=024DF98C EIP=7BDEBC20 FLG=00000206
CS=0023 DS=002B ES=002B SS=002B FS=0053 GS=002B

0x7A953828 is inside the relocated image of wowbox64.dll. Symbol lookup
(COFF symbols in the installed DLL) and disassembly of the live process
resolve it to EmuRun, immediately after a JIT entry call:

4e3a10: mov  x0, x19
4e3a14: bl   arm64_prolog          ; enter JIT (x0=emu, x1=jblock)
4e3a18: mov  w26, wzr
4e3a1c: ldr  w1, [x19, #0x548]     ; emu->fork; faults when x19 is corrupt
4e3a20: cbnz w1, <fork-handling>

At the fault x19 = 0x14, so [x19 + 0x548] = 0x55C. The same 0x55C
signature appears at several offsets inside EmuRun across runs
(0x7A953828, 0x7A953858, 0x7A953948, 0x7A953A1C; the artifact layout
differs between builds, so absolute addresses are not directly comparable).
An earlier crash at 0x7A960124 occurred in the same relocated module region
but the minidump for that run did not record exception parameters.

arm64_prolog/arm64_epilog save and restore the host callee-saved
registers (x19..x28, d8..d15) on a stack frame addressed by x28. The C
code assumes x19 is preserved across the JIT excursion.

A diagnostic build that logged the first WOW64 context transition produced
this event immediately before the fatal state:

event: wow64_exception_reset
exception_code:   0xC0000005
exception_address:000000007A953A1C
param0 (read):    0
param1:           0x55C
native_pc:        000000007A953A1C
guest_pc:         000000007BDEBC20   (i386 ntdll)
guest_sp:         00000000024DF98C
guest_eflags:     0x246
in_jit:           0

FindDynablockFromNativeAddress() returned 0 for the faulting PC even though
it lies inside the anonymous executable region that holds the relocated
wowbox64.dll text (the image is mapped in the 32-bit address range in this
configuration).

Freeze form evidence

Live GDB capture of the spinning main thread (100% CPU, 34 threads; all
other threads waiting on futex/poll/select/pipe):

PC:          wow64.dll+0x1c1d8  (call_user_exception_dispatcher)
Instruction: stp w8, w22, [x24]
x24 = 0x01ce0e8c     (frame base; inside a PROT_NONE guard page)
x8  = x24 + 8
x22 = x24 + 0x58     (WOW64_CONTEXT destination)
x2  = 0x2cc          (716 = sizeof(WOW64_CONTEXT))

Maps at the fault:

01ce0000-01ce1000 ---p  (guard page, contains 0x01ce0e8c)
01ce1000-025e0000 rwxp  (thread's 32-bit stack/anonymous region)

The source structure passed to the frame copy was decoded as a 32-bit
exception record:

ExceptionCode:      0xC0000005
ExceptionAddress:   0x7A96FB40   (inside interpreter `Run`, RVA 0x3FB40)
NumberParameters:   2
ExceptionInformation[0] = 0      (read)
ExceptionInformation[1] = 0x18

The WOW64_CONTEXT being delivered contained:

ContextFlags: 0x0001003F
Eip:          0x7BDECC8C   (i386 ntdll: KiUserExceptionDispatcher)
Esp:          0x01CE11CC
Ebp:          0x024DFA04
Ebx:          0x024DF9EC
Eax:          0x00401000
SegFs:        0x0053

Esp = 0x01CE11CC is frame_base + 0x340, i.e. the frame is built exactly
0x340 bytes below the guest ESP. The guest stack is a 9.2 MB region whose
bottom is 0x01CE1000; after repeated deliveries the next frame base falls
into the guard page (0x01CE0E8C), the store faults, and the dispatcher
re-enters itself forever. Multiple identical delivery frames were present on
the stack at 0x1CE11CC, 0x1CE324C, etc.

Live breakpoint tracing (8/8 identical hits) shows the host side of the same
freeze spinning in Wine's own unwinder: virtual_unwind calls
RtlLookupFunctionEntry for ControlPc 0x106BEB520 — an address inside the
anonymous rwxp mapping 106770000-106d70000, i.e. JIT-generated code with
no unwind data — gets NULL back, and retries the same frame forever (each
round trips the loader lock: RtlEnterCriticalSection /
RtlLeaveCriticalSection, ~100% of one core). So the freeze is a
no-progress unwind/dispatch cycle: the unwinder cannot step past a JIT frame,
and the 32-bit dispatcher cannot write its frame. Box64 v0.4.4 sources
contain no dynamic unwind-table registration (RtlInstallFunctionTableCallback
/ RtlAddFunctionTable / .pdata emission for JIT blocks), although the API
exists in the installed ntdll.dll.

Related observation (may be a separate bug): src/os/os_wine.c implements

void* GetSegmentBase(void* emu, uint32_t desc)
{
    printf_log(LOG_NONE, "GetSegmentBase does not apply to Wine dlls\n");
    return NULL;
}

GetSegmentBaseEmu() stores that NULL into emu->segs_offs[seg]. Both the
interpreter (x64run.c) and generated dynarec code read segs_offs[] at
runtime, so an interpreted instruction carrying a segment override (fs:,
selector 0x53) zeroes the FS base for subsequent accesses until the next
BTCpuSimulate recomputes it. The freeze's original fault — a read of
absolute 0x18 (fs:[0x18] is the x86 TEB self pointer) from the
interpreter — is consistent with a lost FS base. A candidate patch returning
calculate_fs() for selector 0x53 was built and tested: it removed the
GetSegmentBase log flood and startup worked, but the character-list crash
signature was unchanged, so it is not a fix for the crash form and its effect
on the freeze form could not be isolated (that run crashed instead of
freezing). Flagging it as a suspicious implementation gap rather than a
confirmed cause.

Experiments performed

  • BOX64_DYNAREC_BIGBLOCK=0: freeze persists; run log byte-identical
    (616,348 bytes, 10,075 segment-base messages).
  • BOX64_DYNAREC_LOG=2 (workaround mentioned in issue [WoWBox64] Freeze Issue, but works with BOX64_DYNAREC_LOG=2 #3018): no effect;
    produced a ~914 MB log.
  • BOX64_DYNAREC_CALLRET=2: unsupported for WowBox64 per docs; process
    became a zombie; not pursued.
  • BOX64_DYNAREC_CALLRET=0: game exits silently during startup (2.6 KB log,
    ends after DXVK init, no crash files); untestable for the character list.
  • BOX64_DYNAREC_STRONGMEM=1 + WEAKBARRIER=0: user-observed freeze at the
    character list persists (strictest memory ordering changes nothing).
  • BOX64_DYNACACHE=0: identical silent startup exit as CALLRET=0 (2,662-byte
    log, ends after DXVK init, no crash files); untestable for the character
    list. Both translation-machinery flags break startup outright, leaving the
    character-list defect itself untested under them.
  • BOX64_NOSIGSEGV=1: identical silent startup exit again (2,662-byte log,
    same cutoff point, no crash files). Three independent deviations from the
    default translation/signal configuration all prevent startup entirely,
    which points to unmaintained non-default paths in this WowBox64 build
    rather than anything game-specific.
  • BOX64_DYNAREC_INTERP_SIGNAL=1: starts normally (unlike the three above)
    and freezes at the character list with the identical signature — faulting
    PC 0x7fff85c1d8 (wow64.dll store), frame base x24 = 0x1ce0e6c inside
    the same PROT_NONE guard page. Interpreter-based signal handling changes
    neither the failure mode nor its location.
  • M2UseThreads=0 in the game config: no change.
  • FEX via --hodll libwow64fex.dll: passes the character list (in-world
    performance is worse for this user, but that is a performance issue, not
    this crash).
  • Legacy x86-64-on-box64 Wine lane: passes.
  • Interpreter-only trial (BOX64_DYNAREC=0): inconclusive; did not reach a
    usable window in the allotted time.
  • FS-base patch described above: no change to the crash form.

Questions

  1. Does arm64_prolog/arm64_epilog guarantee restoration of all callee
    saved host registers (x19..x28, d8..d15) on every return path,
    including blocks that exit to C through callbacks and blocks that end
    after a guest exception? Is there a known issue with secondary entry
    points (block->sep[]) in this respect?
  2. In BTCpuResetToConsistentState, when ctx->Pc is not recognized as JIT
    code (FindDynablockFromNativeAddress() returns 0), the context is
    forwarded unchanged. Could a guest exception raised inside the emulator
    (with ctx->Pc inside wowbox64's own image) leave emu/x19 holding
    guest register values when control returns to EmuRun?
  3. Which supported diagnostic would you recommend to trace JIT entry/exit
    and the segs_offs[] lifecycle for a 32-bit Wine guest?
  4. For the freeze form: how should 32-bit exception delivery behave when the
    guest stack is at its guard page? Is the expectation that Wine grows the
    stack, or should the delivery avoid re-entering
    call_user_exception_dispatcher from an identical context?
  5. Should WowBox64 register its JIT code with the host unwinder (e.g. via
    RtlInstallFunctionTableCallback, which exists in this ntdll.dll), or
    is there an existing mechanism that is supposed to keep virtual_unwind
    from looping on JIT PCs that have no unwind data?

Attachments

Available privately on request (sanitized to remove session/account data):

  • Crash.txt + Crash.dmp from the crash form (several runs)
  • Crash.txt + Crash.dmp from the freeze-form crash
  • Full GDB captures of the freeze loop and the EmuRun fault
  • Custom JSONL trace of WOW64 entry/exception events
  • Built artifacts with SHA-256:
    • baseline wowbox64 (Hangover 11.16): 58d036888d638f05a72193702f97071aabc9baf7959e7736d48d9c450acc3440
    • diagnostics build (WOW64 probes): ac9dab62c5494f6bd7a199f5849d394f6a33c39ba808062fa8ad53b2a6e935af
    • diagnostics build 2 (exception context): aafcf6adc9da3a6af0c5eb945f86f311df400a8479ac546bf5e3ad34901d5681
    • FS-base candidate build: 0cf22d12e66ef3f8d7e1d651b6f0a39c23a521c70227b0aa928b294e8675c434

The raw dumps contain user/session data and are not posted publicly.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions