fix: process first thread snapshot entry during proxy startup#1338
Merged
Conversation
SvanT
force-pushed
the
fix/proxy-main-thread-enumeration
branch
from
July 12, 2026 21:44
f86951a to
8af33dc
Compare
UE4SS
approved these changes
Jul 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Thread32First()when determining the process main threadThread32Next()for subsequent entries using the standardif/do-whileenumeration patternProblem
get_main_thread_id()currently uses:for (Thread32First(snapshot, &th32); Thread32Next(snapshot, &th32);)The initializer calls
Thread32First(), but the loop body is only entered afterThread32Next(). The first snapshot entry is therefore always discarded.If the first entry is the process main thread,
get_main_thread_id()can return0or select another thread.dll_process_attached()then misclassifies normal proxy loading as manual injection and callsprocess_initialized()synchronously fromDllMain.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
c2ac246under 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:
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_ue4ssfrom the hung process. It was0, confirming that UE4SS had selected direct initialization fromDllMain. Applying the equivalent control-flow correction changed it to1; unmodified PalSchema then completed its multithreaded signature scan and the dedicated server finished booting.Testing
UE4SS/src/main_ue4ss_rewritten.cppwith clang-format 19I 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/UEPseudosubmodule. 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.