Fix bootstrap DLL and hook injection under Wine, and select the resume mechanism per host#25
Open
NayiemW wants to merge 2 commits 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. |
The JMP-redirect bootstrap resume added for mainline Wine faults under CrossOver's x86->ARM translator: executing the freshly-written entry trampoline raises an access violation at the entry point (0xC0000005), so the game never launches. CrossOver, unlike mainline Wine, honors a SetThreadContext Eip change - the original resume primitive works there. Detect the host at startup and choose accordingly: - native Windows and CrossOver -> SetThreadContext (no entry trampoline) - mainline/Whisky Wine -> JMP-redirect trampoline Native Windows is identified by the absence of Wine's ntdll exports, with the HKCU\Software\Wine key as a fallback for Wine builds configured to hide them. CrossOver is distinguished from mainline Wine by the registry keys it writes into its bottles - HKCU\Software\CrossOver or HKLM\Software\CodeWeavers\CrossOver - since the Wine build-id string is unreliable (CrossOver reports a plain wine-11.0 build indistinguishable from mainline). On the SetThreadContext path the entry trampoline is never built and the entry point is resumed pristine; the rest of the bootstrap (synchronous hook creation, no single-stepping) matches the JMP path rather than stock Syringe's re-entry flow. The JMP path itself is unchanged from the previous commit.
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, and select the resume mechanism per host
Problem
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 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.Resume mechanism selection per host
The written-
JMPredirect is required on mainline Wine, but it is not correct everywhere.Under CrossOver on Apple Silicon, the x86-to-ARM translator faults when the freshly written entry trampoline is executed, raising an access violation at the entry point before the game starts. CrossOver, unlike mainline Wine, does honor a
SetThreadContextEipredirect — the original mechanism works there.The two resume strategies are therefore complementary, so Syringe now detects the host once at startup and chooses accordingly:
SetThreadContext. On these hosts the entry trampoline is never built and the entry point is resumed unmodified.JMPtrampoline described above.This only changes the resume primitive per host; the surrounding bootstrap flow (synchronous hook creation, no single-stepping) is the same on both paths.
Detection uses registry and export signals rather than the Wine version string, which is not usable here — CrossOver reports a plain
wine-11.0-...build indistinguishable from mainline Wine:ntdllexports (wine_get_version/wine_get_build_id), with theHKCU\Software\Wineconfiguration key as a fallback for Wine builds configured to hide those exports. Native Windows has neither.HKCU\Software\CrossOverorHKLM\Software\CodeWeavers\CrossOver; either is sufficient. Plain Wine has neither.A single helper,
RedirectExecution, encapsulates the branch: it setsEipon the context path and writes aJMPon the trampoline path. All bootstrap and continuation redirects go through it, so the mainline-Wine path remains byte-for-byte the written-JMPbehavior.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
On native Windows the resume primitive is
SetThreadContext, exactly as stock Syringe used, and the entry trampoline is never built. The surrounding bootstrap follows this PR's synchronous hook-creation flow, shared with the Wine path. TheWriteProcessMemory-basedJMPredirect is used only on mainline Wine, and it relies on ordinary process-memory patching and relative jumps that are also valid on Windows.Testing
Tested end-to-end on macOS using Red Alert 2 / Yuri's Revenge with Ares, Phobos, CnCNet-Spawner, and approximately 2,750 installed hooks, on both host types:
spawn.iniis read. This is the case the written-JMPredirect fixes.SetThreadContext, feature-flag resolution and all hooks install, and the earlier entry-point access violation no longer occurs.Native Windows has not been retested on hardware, but its resume path is unchanged from stock Syringe (
SetThreadContext, no trampoline), and the memory-patching mechanism used on the Wine path is the same one Syringe already uses elsewhere.