Skip to content

fix: process first thread snapshot entry during proxy startup#1338

Merged
UE4SS merged 1 commit into
UE4SS-RE:mainfrom
dathost:fix/proxy-main-thread-enumeration
Jul 13, 2026
Merged

fix: process first thread snapshot entry during proxy startup#1338
UE4SS merged 1 commit into
UE4SS-RE:mainfrom
dathost:fix/proxy-main-thread-enumeration

Conversation

@SvanT

@SvanT SvanT commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • process the thread entry returned by Thread32First() when determining the process main thread
  • retain Thread32Next() for subsequent entries using the standard if/do-while enumeration pattern
  • close the thread snapshot handle before returning
  • document the proxy initialization fix in the changelog

Problem

get_main_thread_id() currently uses:

for (Thread32First(snapshot, &th32); Thread32Next(snapshot, &th32);)

The initializer calls Thread32First(), but the loop body is only entered after Thread32Next(). The first snapshot entry is therefore always discarded.

If the first entry is the process main thread, get_main_thread_id() can return 0 or select another thread. dll_process_attached() then misclassifies normal proxy loading as manual injection and calls process_initialized() synchronously from DllMain.

That reintroduces the loader-lock problem which the proxy startup path is intended to avoid. A C++ mod that creates and waits for worker threads during startup can deadlock: new workers need to perform DLL thread attachment under the loader lock, while the thread holding that lock waits for the workers.

This surfaced with Palworld 1.0, PalSchema 0.6.0, and UE4SS c2ac246 under Wine. The matching PalSchema report is Okaetsu/PalSchema#118.

The function also did not close the handle returned by CreateToolhelp32Snapshot(). This change closes it after enumeration.

Reproduction

A standalone Win32 executable containing the old and fixed enumeration functions was compiled with MSVC 19.44.35228 and run under Wine 11.7 Staging. With one application thread, it produced:

current=36 broken=0 fixed=36

The old implementation skipped the only matching process entry. The fixed implementation returned GetCurrentThreadId() as expected.

In the original Palworld deadlock, the UE4SS PDB was used to read s_wait_for_ue4ss from the hung process. It was 0, confirming that UE4SS had selected direct initialization from DllMain. Applying the equivalent control-flow correction changed it to 1; unmodified PalSchema then completed its multithreaded signature scan and the dedicated server finished booting.

Testing

  • formatted UE4SS/src/main_ue4ss_rewritten.cpp with clang-format 19
  • compiled and ran the standalone old-versus-fixed Win32 reproduction with MSVC 19.44 under Wine 11.7 Staging
  • validated the equivalent startup control flow with Palworld 1.0 and official PalSchema 0.6.0 on a dedicated server under Wine 11.7 Staging
  • verified PalSchema completed its multithreaded scanner and UE4SS continued ordinary startup

I could not run a complete UE4SS build because the GitHub identity available in this environment does not currently have access to the Epic-licensed Re-UE4SS/UEPseudo submodule. The changed code is Win32 process/thread enumeration and does not depend on an Unreal Engine version. The end-to-end validation used Palworld's UE5 build; an older UE4 title was not available in the test environment.

@SvanT
SvanT force-pushed the fix/proxy-main-thread-enumeration branch from f86951a to 8af33dc Compare July 12, 2026 21:44
@UE4SS
UE4SS merged commit b50986b into UE4SS-RE:main Jul 13, 2026
@SvanT
SvanT deleted the fix/proxy-main-thread-enumeration branch July 13, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants