Skip to content

Fix bootstrap DLL and hook injection under Wine without SetThreadContext EIP redirects#25

Open
NayiemW wants to merge 1 commit into
Phobos-developers:masterfrom
the-real-antares:wine-injection-jmp-redirect
Open

Fix bootstrap DLL and hook injection under Wine without SetThreadContext EIP redirects#25
NayiemW wants to merge 1 commit into
Phobos-developers:masterfrom
the-real-antares:wine-injection-jmp-redirect

Conversation

@NayiemW

@NayiemW NayiemW commented Jul 10, 2026

Copy link
Copy Markdown

Fix bootstrap DLL and hook injection under Wine without SetThreadContext EIP redirects

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.ini is read.

Root cause

Syringe previously redirected execution by modifying the debuggee thread’s Eip through SetThreadContext.

Wine’s WoW64 debug-event resume path does not reliably honor these Eip changes when redirecting execution to a distant, dynamically allocated address, such as the LoadLibrary bootstrap stub in pAlloc.

This failure is particularly misleading:

  • SetThreadContext reports success.
  • A subsequent GetThreadContext call reports the requested Eip.
  • The thread nevertheless resumes from its original execution location.
  • DLL loading, feature resolution, and hook installation therefore silently do not occur.

This behavior was reproduced independently using a minimal target executable and debugger outside this codebase. In that test, redirects performed through WriteProcessMemory were honored, while redirects performed through SetThreadContext(Eip = ...) were not.

Fix

Execution is now redirected by writing an actual relative JMP instruction at the CPU’s real resume address, rather than relying on a thread-context mutation.

For an INT3 breakpoint, the CPU resumes at breakpointAddress + 1, so the redirect is written there using WriteProcessMemory.

The implementation adds the following:

WriteRedirectJmp

Writes a five-byte relative JMP at the debuggee’s resume address, redirecting execution to the required bootstrap or continuation code.

DetermineOverwriteSize

Decodes complete instructions until enough bytes are available for a safe five-byte jump, avoiding partial instruction overwrites.

BuildEntryTrampoline

Creates a trampoline for returning to the executable entry point.

Because the entry point may contain instructions larger than the required jump, the trampoline:

  1. determines how many complete instructions must be overwritten;
  2. copies and rebuilds those instructions using the existing Zydis-based RebuildInstructions logic; and
  3. jumps back to the first untouched instruction at the original entry point.

CreateCodeHooks

The 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 pcEntryPoint and waits for another breakpoint before creating the hooks, since that flow depended on the same unreliable SetThreadContext redirection.

Additional corrections

The bootstrap stub’s embedded INT3 is 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 --detach completion condition now checks bHooksCreated directly.

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-based Eip redirects.

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:

  • Ares
  • Phobos
  • CnCNet-Spawner
  • approximately 2,750 installed hooks

With this change, the game successfully launches directly into a running match instead of exiting before spawn.ini is 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.

…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.
@github-actions

Copy link
Copy Markdown

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.

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.

1 participant