Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions AffinityPluginLoader/Native/NativeHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using AffinityPluginLoader.Core;

namespace AffinityPluginLoader.Native
{
/// <summary>
/// Inline function detouring for native code. Overwrites a function's prologue
/// with a jump to a managed delegate, and creates an executable trampoline
/// containing the original prologue bytes so the hook can call through to the
/// original function.
///
/// Supports two modes:
/// - Large prologue (>= 12 bytes): direct absolute jump at target site
/// - Small prologue (>= 5 bytes): relative jump to a nearby relay thunk
///
/// IMPORTANT: prologueSize must end on an instruction boundary, and the
/// relocated bytes must not contain RIP-relative instructions (use a larger
/// or smaller prologue to avoid them).
/// </summary>
public static class NativeHook
{
private const int AbsJmpSize = 12; // mov rax, imm64 (10) + jmp rax (2)
private const int RelJmpSize = 5; // jmp rel32
private static readonly List<Delegate> _pinnedDelegates = new();
private static readonly List<IntPtr> _allocations = new();

/// <summary>
/// Hook a native function at the given address.
/// </summary>
/// <typeparam name="T">Delegate type with [UnmanagedFunctionPointer]</typeparam>
/// <param name="target">Address of the function to hook</param>
/// <param name="prologueSize">
/// Number of bytes to overwrite. Must be >= 5 and end on an instruction boundary.
/// The overwritten bytes must not contain RIP-relative instructions.
/// </param>
/// <param name="hook">Replacement delegate</param>
/// <returns>Delegate that calls the original function</returns>
public static T Hook<T>(IntPtr target, int prologueSize, T hook) where T : class
{
if (target == IntPtr.Zero)
throw new ArgumentNullException(nameof(target));
if (prologueSize < RelJmpSize)
throw new ArgumentException($"prologueSize must be >= {RelJmpSize}");
if (!(hook is Delegate hookDel))
throw new ArgumentException("T must be a delegate type");

IntPtr hookPtr = Marshal.GetFunctionPointerForDelegate(hookDel);

// Allocate trampoline (for calling original): prologue + absolute jump back
int trampolineSize = prologueSize + AbsJmpSize;
IntPtr trampoline = VirtualAlloc(IntPtr.Zero, (UIntPtr)trampolineSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (trampoline == IntPtr.Zero)
throw new OutOfMemoryException("VirtualAlloc failed for trampoline");

// Copy original prologue to trampoline + jump back
byte[] originalBytes = new byte[prologueSize];
Marshal.Copy(target, originalBytes, 0, prologueSize);
Marshal.Copy(originalBytes, 0, trampoline, prologueSize);
WriteAbsoluteJump(trampoline + prologueSize, target + prologueSize);

// Overwrite target prologue
VirtualProtect(target, (UIntPtr)prologueSize, PAGE_EXECUTE_READWRITE, out uint oldProtect);

if (prologueSize >= AbsJmpSize)
{
// Direct absolute jump to hook
WriteAbsoluteJump(target, hookPtr);
}
else
{
// Allocate relay near target for absolute jump, use rel32 at target
IntPtr relay = AllocateNear(target, AbsJmpSize);
if (relay == IntPtr.Zero)
throw new OutOfMemoryException("Failed to allocate relay near target");
WriteAbsoluteJump(relay, hookPtr);
WriteRelativeJump(target, relay);
}

// NOP remaining bytes
for (int i = (prologueSize >= AbsJmpSize ? AbsJmpSize : RelJmpSize); i < prologueSize; i++)
Marshal.WriteByte(target + i, 0x90);

VirtualProtect(target, (UIntPtr)prologueSize, oldProtect, out _);

_pinnedDelegates.Add(hookDel);
_allocations.Add(trampoline);

T original = Marshal.GetDelegateForFunctionPointer<T>(trampoline);

Logger.Debug($"NativeHook: detoured 0x{target.ToInt64():X} -> 0x{hookPtr.ToInt64():X} " +
$"(trampoline at 0x{trampoline.ToInt64():X}, {prologueSize} bytes relocated)");

return original;
}

private static unsafe void WriteAbsoluteJump(IntPtr site, IntPtr target)
{
byte* p = (byte*)site;
p[0] = 0x48; p[1] = 0xB8; // mov rax, imm64
*(long*)(p + 2) = target.ToInt64();
p[10] = 0xFF; p[11] = 0xE0; // jmp rax
}

private static unsafe void WriteRelativeJump(IntPtr site, IntPtr target)
{
byte* p = (byte*)site;
p[0] = 0xE9; // jmp rel32
*(int*)(p + 1) = (int)(target.ToInt64() - site.ToInt64() - 5);
}

/// <summary>
/// Allocate executable memory within ±2GB of the given address.
/// </summary>
private static IntPtr AllocateNear(IntPtr target, int size)
{
long addr = target.ToInt64();
long low = Math.Max(addr - 0x7FFF0000L, 0x10000L);
long high = addr + 0x7FFF0000L;

// Scan in 64KB increments (allocation granularity)
for (long a = addr & ~0xFFFFL; a >= low; a -= 0x10000)
{
IntPtr result = VirtualAlloc((IntPtr)a, (UIntPtr)size,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (result != IntPtr.Zero)
{
_allocations.Add(result);
return result;
}
}
for (long a = (addr + 0x10000) & ~0xFFFFL; a <= high; a += 0x10000)
{
IntPtr result = VirtualAlloc((IntPtr)a, (UIntPtr)size,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (result != IntPtr.Zero)
{
_allocations.Add(result);
return result;
}
}
return IntPtr.Zero;
}

private const uint MEM_COMMIT = 0x1000;
private const uint MEM_RESERVE = 0x2000;
private const uint PAGE_EXECUTE_READWRITE = 0x40;

[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr VirtualAlloc(IntPtr lpAddress, UIntPtr dwSize,
uint flAllocationType, uint flProtect);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpflOldProtect);

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr GetModuleHandle(string lpModuleName);

[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
}
}
164 changes: 164 additions & 0 deletions WineFix/Patches/BezierSplitBudgetPatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System;
using System.Runtime.InteropServices;
using AffinityPluginLoader.Core;
using AffinityPluginLoader.Native;

namespace WineFix.Patches
{
/// <summary>
/// Caps the number of bezier control triangle splits in Wine's geometry
/// processing to prevent unbounded segment growth and application freezes.
///
/// Wine's d2d_geometry_resolve_beziers (inlined in d2d_geometry_sink_Close)
/// calls d2d_geometry_split_bezier in an unbounded loop. Pathological
/// geometries (e.g. overlapping beziers in embedded SVGs) cause the loop
/// to run forever, hanging Affinity.
///
/// This patch hooks d2d_geometry_split_bezier with a thread-local call
/// counter that returns failure after 512 calls, causing the caller to
/// stop splitting.
///
/// Based on:
/// 0005-d2d1-prevent-runaway-bezier-splitting-and-recursion-.patch
/// by Arecsu (https://github.com/Arecsu/wine-affinity)
/// </summary>
public static class BezierSplitBudgetPatch
{
// d2d_geometry_split_bezier.isra.0(figures_ptr, segment_idx_ptr) -> BOOL
// .isra = GCC interprocedural SRA; takes two pointer args after optimization
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate int SplitBezierFn(IntPtr figures, IntPtr idx);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate int SinkCloseFn(IntPtr self);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate int CreatePathGeometryFn(IntPtr factory, out IntPtr pathGeometry);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate int OpenFn(IntPtr pathGeometry, out IntPtr geometrySink);

private static SplitBezierFn _original;
private static SinkCloseFn _origClose;

// Prologue: push rsi (56) + push rbx (53) + sub rsp,0x48 (48 83 ec 48)
// + first 4 bytes of movq xmm2,[rip+disp] (f3 0f 7e 15) for pattern uniqueness
// Only the first 6 bytes are relocated (before the RIP-relative instruction)
private static readonly byte[] SplitBezierPattern = {
0x56, 0x53, 0x48, 0x83, 0xEC, 0x48, 0xF3, 0x0F, 0x7E, 0x15
};

private const int PrologueSize = 6; // push+push+sub = 6 bytes (>= 5 for rel32 jmp)
private const int MaxSplitsPerOperation = 512;

// ID2D1SimplifiedGeometrySink::Close = vtable index 9
private const int Sink_Close = 9;
// ID2D1Factory::CreatePathGeometry = vtable index 10
private const int Factory_CreatePathGeometry = 10;
// ID2D1PathGeometry::Open = vtable index 17
private const int PathGeometry_Open = 17;

[ThreadStatic] private static int _splitCount;

[DllImport("d2d1.dll")]
private static extern int D2D1CreateFactory(
int factoryType, [MarshalAs(UnmanagedType.LPStruct)] Guid riid,
IntPtr factoryOptions, out IntPtr factory);

private static readonly Guid IID_ID2D1Factory =
new Guid("06152247-6f50-465a-9245-118bfd3b6007");

public static void Apply()
{
Logger.Info("Applying bezier split budget (NativeHook + ComHook)...");

try
{
// 1. Hook d2d_geometry_split_bezier via NativeHook
if (!NativePatch.TryGetSection("d2d1", ".text", out IntPtr textStart, out int textSize))
{
Logger.Warning("Bezier split budget: .text section not found in d2d1.dll");
return;
}

IntPtr funcAddr = ScanForPattern(textStart, textSize, SplitBezierPattern);
if (funcAddr == IntPtr.Zero)
{
Logger.Warning("Bezier split budget: split_bezier pattern not found in d2d1.dll");
return;
}

_original = NativeHook.Hook<SplitBezierFn>(
funcAddr, PrologueSize, new SplitBezierFn(OnSplitBezier));

Logger.Info($"Bezier split budget: split_bezier hooked at d2d1+0x{(funcAddr.ToInt64() - GetModuleHandle("d2d1").ToInt64()):X}");

// 2. Hook ID2D1GeometrySink::Close via ComHook to reset counter
IntPtr factory = IntPtr.Zero, pathGeometry = IntPtr.Zero, sink = IntPtr.Zero;
try
{
int hr = D2D1CreateFactory(0, IID_ID2D1Factory, IntPtr.Zero, out factory);
if (hr < 0) return;

var createPG = ComHook.GetMethod<CreatePathGeometryFn>(factory, Factory_CreatePathGeometry);
hr = createPG(factory, out pathGeometry);
if (hr < 0) return;

var open = ComHook.GetMethod<OpenFn>(pathGeometry, PathGeometry_Open);
hr = open(pathGeometry, out sink);
if (hr < 0) return;

_origClose = ComHook.Hook<SinkCloseFn>(sink, Sink_Close, new SinkCloseFn(OnClose));
Logger.Info("Bezier split budget: sink Close hooked for counter reset");
}
finally
{
if (sink != IntPtr.Zero) Marshal.Release(sink);
if (pathGeometry != IntPtr.Zero) Marshal.Release(pathGeometry);
if (factory != IntPtr.Zero) Marshal.Release(factory);
}
}
catch (Exception ex)
{
Logger.Error("Failed to install bezier split budget", ex);
}
}

private static int OnSplitBezier(IntPtr figures, IntPtr idx)
{
if (++_splitCount > MaxSplitsPerOperation)
{
if (_splitCount == MaxSplitsPerOperation + 1)
Logger.Debug($"Bezier split budget: capped at {MaxSplitsPerOperation} splits");
return 0; // FALSE — tell caller to stop splitting
}

return _original(figures, idx);
}

private static int OnClose(IntPtr self)
{
_splitCount = 0;
return _origClose(self);
}

private static unsafe IntPtr ScanForPattern(IntPtr start, int size, byte[] pattern)
{
byte* ptr = (byte*)start;
int limit = size - pattern.Length;
for (int i = 0; i <= limit; i++)
{
bool match = true;
for (int j = 0; j < pattern.Length; j++)
{
if (ptr[i + j] != pattern[j]) { match = false; break; }
}
if (match) return (IntPtr)(ptr + i);
}
return IntPtr.Zero;
}

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
}
}
Loading
Loading