Fix bootstrap DLL and hook injection under Wine without SetThreadContext EIP redirects#25
Open
NayiemW wants to merge 1 commit into
Open
Conversation
…ext EIP redirects Root cause: Wine's wow64 debug-event resume path does not reliably honor a SetThreadContext-based EIP change when redirecting to a distant, dynamically- allocated address (e.g. into the LoadLibrary bootstrap stub in pAlloc). GetThreadContext falsely confirms the change took effect, but the debuggee thread actually just continues from wherever it really was - meaning none of Syringe's DLL injection or hook installation ever actually happens under Wine, even though the debug loop appears to proceed normally. Confirmed with an isolated minimal repro (custom target exe + custom debugger, independent of this codebase) before touching this file. Fix: redirect execution by writing a real JMP instruction via WriteProcessMemory (proven reliable under Wine) at the actual CPU resume address (bpAddr+1, per standard INT3 semantics), instead of mutating thread context: - WriteRedirectJmp / DetermineOverwriteSize / BuildEntryTrampoline: new helpers. The entry point needs a proper trampoline (built via the existing Zydis-based RebuildInstructions) since we do not control how many bytes are safely overwritable there, unlike our own stub code where we just add padding. - CreateCodeHooks: extracted the existing hook-installation logic into its own method, now called directly once DLL loading and feature-flag resolution finish, rather than relying on redirecting back to pcEntryPoint and waiting for a fresh breakpoint there (which depended on the same broken SetThreadContext mechanism). - Do not restore the bootstrap stub's own embedded INT3 in the feature-flag loop - it is never registered via SetBP and must stay intact across re-entries, since execution is always redirected away from it via WriteRedirectJmp rather than ever falling through past it. - Pad the LoadLibrary bootstrap stub so there is always room for a 5-byte JMP right after its embedded INT3. - Simplify the --detach completion check to test bHooksCreated directly; it previously also required a single-step trap that no longer occurs now that hook creation runs synchronously. Verified end-to-end against the actual game (RA2/YR with Ares, Phobos and CnCNet-Spawner, 2750 hooks) under Wine on macOS: the game now launches directly into a running match instead of exiting before spawn.ini is ever read.
|
Nightly build for this pull request:
This comment is automatic and is meant to allow guests to get latest nightly builds for this pull request without registering. It is updated on every successful build. |
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.
Fix bootstrap DLL and hook injection under Wine without
SetThreadContextEIP redirectsProblem
When Syringe runs under Wine, including Wine-based setups on macOS, Linux, and Proton, DLL injection and hook installation can silently fail.
The debugger loop continues running and the log may report the expected number of hooks, but the target process actually runs without any of them installed.
In CnCNet’s Yuri’s Revenge setup using Ares, Phobos, and CnCNet-Spawner, this causes the game to exit before
spawn.iniis read.Root cause
Syringe previously redirected execution by modifying the debuggee thread’s
EipthroughSetThreadContext.Wine’s WoW64 debug-event resume path does not reliably honor these
Eipchanges when redirecting execution to a distant, dynamically allocated address, such as theLoadLibrarybootstrap stub inpAlloc.This failure is particularly misleading:
SetThreadContextreports success.GetThreadContextcall reports the requestedEip.This behavior was reproduced independently using a minimal target executable and debugger outside this codebase. In that test, redirects performed through
WriteProcessMemorywere honored, while redirects performed throughSetThreadContext(Eip = ...)were not.Fix
Execution is now redirected by writing an actual relative
JMPinstruction at the CPU’s real resume address, rather than relying on a thread-context mutation.For an
INT3breakpoint, the CPU resumes atbreakpointAddress + 1, so the redirect is written there usingWriteProcessMemory.The implementation adds the following:
WriteRedirectJmpWrites a five-byte relative
JMPat the debuggee’s resume address, redirecting execution to the required bootstrap or continuation code.DetermineOverwriteSizeDecodes complete instructions until enough bytes are available for a safe five-byte jump, avoiding partial instruction overwrites.
BuildEntryTrampolineCreates a trampoline for returning to the executable entry point.
Because the entry point may contain instructions larger than the required jump, the trampoline:
RebuildInstructionslogic; andCreateCodeHooksThe existing hook-generation and installation logic has been extracted into a dedicated method.
Hook creation now runs directly after DLL loading and feature-flag resolution complete. Syringe no longer redirects execution back to
pcEntryPointand waits for another breakpoint before creating the hooks, since that flow depended on the same unreliableSetThreadContextredirection.Additional corrections
The bootstrap stub’s embedded
INT3is no longer restored during the feature-flag loop.That breakpoint was created directly inside the allocated bootstrap code and was never registered through
SetBP. Attempting to restore it through the breakpoint map therefore used an invalid default entry and corrupted the bootstrap stub during subsequent iterations.The
--detachcompletion condition now checksbHooksCreateddirectly.Previously, detachment also required a single-step exception because hook creation was detected after redirecting execution back to the entry point. Hook creation now happens synchronously, so that single-step event no longer occurs and is no longer required.
Platform impact
This removes Syringe’s dependency on Wine honoring
SetThreadContext-basedEipredirects.The replacement uses ordinary process-memory patching and relative jumps, which are also supported on native Windows. The changes are therefore not expected to alter native Windows behavior.
Testing
Tested end-to-end on macOS under Wine using Red Alert 2 / Yuri’s Revenge with:
With this change, the game successfully launches directly into a running match instead of exiting before
spawn.iniis read.This has not yet been tested on native Windows. However, the changes use the same
WriteProcessMemory-based patching mechanism already used elsewhere by Syringe and should not affect Windows builds or native Windows injection behavior.