FEX-Emu / FEX-Emu/FEX

32-bit x86 SEH dispatch infinite loop: FS:[0] returns 0x428 instead of valid handler (Wine/Proton on AArch64)

Open
#5,528 1 comment 0 reactions 0 assignees View on GitHub
ml-report
Dominant language
C++
Stars
8k
Forks
351
Avg merge
12h 31m
Merged PRs (30d)
102

Description

Bug Report: 32-bit x86 SEH Dispatch Infinite Loop via Corrupted FS:[0] Read


Repository: https://github.com/FEX-Emu/FEX

Component: 32-bit x86 emulation / FS segment register handling

Severity: Critical (prevents 32-bit Windows games from running)




Environment

Field | Value
-- | --
FEX Build Date | May 22 2026 (# SMP May 22 2026 13:57:52 from strings)
FEX Binary | /usr/bin/FEX, /usr/bin/FEXInterpreter
Host Architecture | AArch64 (Snapdragon 8 Elite / SM8750)
OS | RockNix nightly 20260524, BUILD_BRANCH=next
Kernel | AArch64 Linux
binfmt_misc | FEX-x86 and FEX-x86_64 both registered and enabled
Proton Version | GE-Proton9-27 (Wine-staging 9.0) via Steam
SteamLinuxRuntime | SteamLinuxRuntime_sniper
Application | Lineage 2 Gracia Epilogue client (l2.exe) — PE32 executable, Intel 80386, 32-bit



Summary


When running a 32-bit (x86) Windows application through Wine (GE-Proton9-27 / Wine-staging 9.0) on an AArch64 host using FEX as the x86 translation layer, FS:[0] consistently returns 0x428 instead of the correct SEH (Structured Exception Handling) chain pointer stored in the Thread Information Block (TIB).


This causes Wine's exception dispatcher to call an invalid handler address (0x428), which itself triggers another access violation, creating an infinite recursive exception dispatch loop that runs for millions of iterations until the thread stack overflows.




Steps to Reproduce



  1. AArch64 host with FEX binfmt registered for x86 and x86_64

  2. GE-Proton9-27 installed via Steam (Wine binaries are x86/x86_64 ELFs, intercepted by FEX binfmt)

  3. Launch any 32-bit Windows PE32 application through Proton that uses SEH (Structured Exception Handling) during initialization — specifically UE2-based games like Lineage 2 Gracia Epilogue

  4. Observe the crash


Launch options used:


PROTON_LOG=1 FEX_TSOENABLED=1 DXVK_ASYNC=1 WINEDLLOVERRIDES="dsetup=n" %command%




Expected Behavior


When a 32-bit Windows application raises an exception, Wine should:



  1. Read the SEH chain head from FS:[0] (Thread Information Block offset 0)

  2. Walk the chain to find an appropriate handler

  3. Call the handler, which returns and resumes execution

  4. This is normal UE2 engine behavior — it intentionally uses SEH for hardware capability detection




Actual Behavior


Wine's seh:dispatch_exception reads FS:[0] and receives 0x428 — an invalid address that is not a valid code pointer. This produces:


0128:trace:seh:dispatch_exception code=c0000005 flags=0 addr=216A1D68 ip=216a1d68

0128:trace:seh:dispatch_exception info[0]=00000001
0128:trace:seh:dispatch_exception info[1]=00430000
0128:warn:seh:dispatch_exception EXCEPTION_ACCESS_VIOLATION exception (code=c0000005) raised
0128:trace:seh:dispatch_exception eax=216a7829 ebx=9ab0040d ecx=ffffaf74 edx=30257aca esi=0000f219 edi=23eefd3f
0128:trace:seh:dispatch_exception ebp=15ab0014 esp=00430004 cs=0030 ss=0000 ds=0000 es=0000 fs=0063 gs=006b flags=00000206
0128:trace:seh:call_stack_handlers calling handler at 23EEFD3F code=c0000005 flags=0
0128:trace:seh:dispatch_exception code=c0000005 flags=0 addr=23EEFD3F ip=23eefd3f
0128:warn:seh:dispatch_exception EXCEPTION_ACCESS_VIOLATION exception (code=c0000005) raised
0128:trace:seh:call_stack_handlers calling handler at 7BF9F0A0 code=c0000005 flags=0
0128:trace:seh:call_stack_handlers handler at 7BF9F0A0 returned 2
0128:trace:seh:call_stack_handlers calling handler at 00000428 code=c0000005 flags=10
0128:trace:seh:dispatch_exception code=c0000005 flags=0 addr=00000428 ip=00000428

The loop then repeats infinitely with info[1]=00000428 on every iteration:


0128:trace:seh:dispatch_exception  info[1]=00000428

0128:trace:seh:dispatch_exception info[1]=00000428
0128:trace:seh:dispatch_exception info[1]=00000428
... (millions of iterations) ...
0128:trace:seh:dispatch_exception stack overflow 1472 bytes addr 0x7bfa94b3 stack 0x330a40 (0x330000-0x331000-0x2330000)

The log reached over 406 million lines before stack exhaustion. Testing with 32MB of stack reserve (SizeOfStackReserve=0x02000000 in the PE header) confirmed the loop always exhausts all available stack — the recursion depth scales with stack size, not with actual game initialization depth.




Root Cause Analysis


In 32-bit Windows x86, FS is a segment register pointing to the Thread Information Block (TIB). FS:[0] holds the head of the SEH handler chain on the stack.


FEX appears to be incorrectly resolving FS:[0] reads in 32-bit x86 code running inside Wine on AArch64. Instead of returning the valid SEH chain pointer, it returns 0x428 (decimal 1064) — a small integer that is clearly not a code address.


Evidence:



  • ecx=00000428 appears as the SEH handler address in the first cascading exception

  • info[1]=00000428 repeats across millions of exception dispatch iterations

  • The same crash pattern occurs across Proton 8.0 and GE-Proton9-27

  • The crash is fully reproducible and deterministic

  • FEX_TSOENABLED=1 partially mitigated timing but did not resolve the issue


Wine binary architecture (GE-Proton9-27):


wine:          ELF 32-bit LSB pie executable, Intel 80386  ← intercepted by FEX-x86 binfmt

wine-preloader: ELF 32-bit LSB executable, Intel 80386 ← intercepted by FEX-x86 binfmt
wine64: ELF 64-bit LSB pie executable, x86-64 ← intercepted by FEX-x86_64 binfmt
wineserver: ELF 64-bit LSB pie executable, x86-64 ← intercepted by FEX-x86_64 binfmt

All Wine binaries are x86/x86_64 ELFs running through FEX binfmt on the AArch64 host.




Additional Notes



  • prctl(PR_SET_SECCOMP, ...): Invalid argument is logged by Wine, indicating seccomp-based syscall interception is unavailable. This may be related.

  • The application (Lineage 2 / UE2) uses SEH intentionally for hardware detection — this is not a bug in the application.

  • FEX_TSOENABLED=1 extended the time before crash (from ~3900s to ~8300s in Wine log timestamps) but did not prevent it.

  • The issue is present with both Proton 8.0 and GE-Proton9-27.

  • The ~/.fex-emu/ user config directory being present (even empty) caused FEX to stop using the system RootFS config, breaking all x86 emulation. Removing the directory restored function. This may indicate a config discovery issue worth addressing separately.




Workarounds Attempted (None Successful)



  • FEX_TSOENABLED=1 — partial mitigation only

  • FEX_PARANOIDTSO=1 — broke FEX entirely (caused immediate crash of all x86 processes)

  • Increasing SizeOfStackReserve in PE header to 32MB — confirmed the loop scales with stack size, not game code depth

  • PROTON_USE_WINED3D=1 — no effect on SEH behavior

  • WINEARCH=win32 — no effect




Contact / Context


Discovered while attempting to run Lineage 2 Gracia Epilogue (private server) on an Odin 3 handheld (Snapdragon 8 Elite) running RockNix. The application fully initializes (all DLLs load, D3D9 initializes, UE2 Core.dll and Engine.dll load) before hitting this crash, suggesting the underlying emulation is otherwise functional for this workload.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.