using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using WhiteMagic;
using WhiteMagic.Hooking;
using WhiteMagic.Native;
using Xunit;
namespace WhiteMagicTest.Hooking;
///
/// Tests for , and
/// operating in-process.
///
public class HookingTests
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int FrameFunc();
private const int FrameResult = 42;
private static InProcessReader CreateReader()
{
return new InProcessReader();
}
///
/// Allocates a tiny executable function whose prologue is made entirely of
/// covered instruction shapes, so detours apply cleanly in tests.
///
private static IntPtr AllocateFrameStub(MemoryBase reader, out IntPtr allocationBase)
{
// x64: push rbp; push rdi; push rsi; push rbx; sub rsp, 0x28; sub rsp, 0x12345678;
// mov eax, 42; add rsp, 0x12345678; add rsp, 0x28; pop rbx; pop rsi; pop rdi; pop rbp; ret
byte[] code =
[
0x55, // push rbp
0x57, // push rdi
0x56, // push rsi
0x53, // push rbx
0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28
0x48, 0x81, 0xEC, 0x78, 0x56, 0x34, 0x12, // sub rsp, 0x12345678
0xB8, 0x2A, 0x00, 0x00, 0x00, // mov eax, 42
0x48, 0x81, 0xC4, 0x78, 0x56, 0x34, 0x12, // add rsp, 0x12345678
0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28
0x5B, // pop rbx
0x5E, // pop rsi
0x5F, // pop rdi
0x5D, // pop rbp
0xC3 // ret
];
allocationBase = NativeMethods.VirtualAllocEx(
reader.Handle,
IntPtr.Zero,
code.Length,
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
MemoryProtectionType.ExecuteReadWrite);
Assert.NotEqual(IntPtr.Zero, allocationBase);
reader.WriteBytes(allocationBase, code);
return allocationBase;
}
[Fact]
public void Patch_apply_writes_bytes_and_remove_restores_original()
{
using var reader = CreateReader();
byte[] slot = new byte[8];
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
byte[] original = reader.ReadBytes(addr, 4);
byte[] patchBytes = [0x90, 0x90, 0x90, 0x90];
Patch patch = reader.PatchManager.Create("nop", addr, patchBytes);
Assert.False(patch.IsApplied);
patch.Apply();
Assert.True(patch.IsApplied);
Assert.Equal(patchBytes, reader.ReadBytes(addr, 4));
patch.Remove();
Assert.False(patch.IsApplied);
Assert.Equal(original, reader.ReadBytes(addr, 4));
}
finally
{
pin.Free();
}
}
[Fact]
public void Detour_apply_redirects_callOriginal_remove_restores()
{
using var reader = CreateReader();
IntPtr targetPtr = AllocateFrameStub(reader, out IntPtr allocation);
try
{
int hookCalls = 0;
Detour? detour = null;
FrameFunc hook = () =>
{
hookCalls++;
return (int?)detour?.CallOriginal() ?? 0;
};
detour = reader.DetourManager.Create("frame", targetPtr, hook);
detour.Apply();
FrameFunc routed = Marshal.GetDelegateForFunctionPointer(targetPtr);
int result = routed();
Assert.True(hookCalls > 0);
Assert.Equal(FrameResult, result);
detour.Remove();
hookCalls = 0;
result = routed();
Assert.Equal(0, hookCalls);
Assert.Equal(FrameResult, result);
GC.KeepAlive(hook);
}
finally
{
NativeMethods.VirtualFreeEx(reader.Handle, allocation, 0, MemoryFreeType.Release);
}
}
[Fact]
public void Detour_named_lookup_returns_existing_detour()
{
using var reader = CreateReader();
IntPtr targetPtr = AllocateFrameStub(reader, out IntPtr allocation);
try
{
Detour detour = reader.DetourManager.Create("lookup", targetPtr, (FrameFunc)(() => FrameResult));
Assert.Same(detour, reader.DetourManager["lookup"]);
}
finally
{
NativeMethods.VirtualFreeEx(reader.Handle, allocation, 0, MemoryFreeType.Release);
}
}
[Fact]
public void Detour_aligned_prologue_applies_and_unknown_prologue_rejects()
{
using var reader = CreateReader();
// A normal JIT-compiled function has a covered prologue shape.
IntPtr targetPtr = AllocateFrameStub(reader, out IntPtr goodAllocation);
try
{
Detour good = reader.DetourManager.Create("good", targetPtr, (FrameFunc)(() => FrameResult));
good.Apply();
good.Remove();
}
finally
{
NativeMethods.VirtualFreeEx(reader.Handle, goodAllocation, 0, MemoryFreeType.Release);
}
// Allocate a small executable region whose first instruction is outside the
// covered set. The decoder must refuse to splice it.
IntPtr code = NativeMethods.VirtualAllocEx(
reader.Handle,
IntPtr.Zero,
32,
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
MemoryProtectionType.ExecuteReadWrite);
Assert.NotEqual(IntPtr.Zero, code);
try
{
// 0x0F 0x05 = syscall (not covered), followed by padding and a ret.
byte[] unknown = [0x0F, 0x05, 0xC3, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC];
reader.WriteBytes(code, unknown);
Detour bad = reader.DetourManager.Create("bad", code, (FrameFunc)(() => FrameResult));
Assert.Throws(() => bad.Apply());
}
finally
{
NativeMethods.VirtualFreeEx(reader.Handle, code, 0, MemoryFreeType.Release);
}
}
[Fact]
public void Dispose_restores_active_patches_and_detours()
{
InProcessReader reader = CreateReader();
byte[] slot = new byte[8];
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
byte[] original = reader.ReadBytes(addr, 2);
Patch patch = reader.PatchManager.Create("dispose-patch", addr, [0x90, 0x90]);
patch.Apply();
reader.Dispose();
// Verify with a fresh reader; the original handle was closed by Dispose.
using var verify = CreateReader();
Assert.Equal(original, verify.ReadBytes(addr, 2));
}
finally
{
pin.Free();
}
}
[Fact]
public async Task MainThreadPump_drains_work_on_frame_call_and_uninstalls_on_dispose()
{
using var reader = CreateReader();
IntPtr targetPtr = AllocateFrameStub(reader, out IntPtr allocation);
try
{
var pump = new WhiteMagic.Execution.MainThreadPump(reader.DetourManager, targetPtr);
pump.Install();
Task work = pump.ExecuteAsync(() => 123);
// Drive the frame function manually. The detoured frame runs the pump hook on
// this thread, drains the work queue, then calls the original frame function.
FrameFunc routed = Marshal.GetDelegateForFunctionPointer(targetPtr);
int frameResult = routed();
Assert.Equal(FrameResult, frameResult);
Assert.Equal(123, await work);
pump.Dispose();
// After uninstall, calling the frame function should behave like the original.
routed = Marshal.GetDelegateForFunctionPointer(targetPtr);
Assert.Equal(FrameResult, routed());
}
finally
{
NativeMethods.VirtualFreeEx(reader.Handle, allocation, 0, MemoryFreeType.Release);
}
}
[Fact]
public async Task MainThreadPump_exception_survives_and_does_not_kill_pump()
{
using var reader = CreateReader();
IntPtr targetPtr = AllocateFrameStub(reader, out IntPtr allocation);
try
{
var pump = new WhiteMagic.Execution.MainThreadPump(reader.DetourManager, targetPtr);
pump.Install();
Task bad = pump.ExecuteAsync(() => throw new InvalidOperationException("boom"));
Task good = pump.ExecuteAsync(() => 7);
FrameFunc routed = Marshal.GetDelegateForFunctionPointer(targetPtr);
routed();
await Assert.ThrowsAsync(() => bad);
Assert.Equal(7, await good);
pump.Dispose();
}
finally
{
NativeMethods.VirtualFreeEx(reader.Handle, allocation, 0, MemoryFreeType.Release);
}
}
}