- Example5: Fix format string bugs (alignment specifier placement, SafeMemoryHandle formatting) - Example4: Fix DetourManager.Detour() → Create() in all doc strings - Example3: Fix MemoryBase.CreateFunction() → InProcessInvoker.CreateFunction() in doc strings - Examples 1-5: Correct all runtime errors and API mismatches vs actual WhiteMagic API - README: Fix DetourManager.Detour() examples to use Create(); add missing code fence markers - Add WhiteMagic.Examples project with 5 comprehensive example files (40+ sub-examples) - Add docfx.json and toc.md for DocFX API reference generation - Add 5 conceptual guides: architecture, memory-access, execution-models, hooking, troubleshooting - Ensure zero errors, zero warnings across all projects (net8.0-windows) Doc strings now teach correct APIs; runtime format bugs eliminated; build succeeds.
9.4 KiB
Execution Models in WhiteMagic
WhiteMagic provides three distinct execution strategies, each designed for specific payload safety requirements. Choosing the right model is critical for avoiding crashes and ensuring reliable operation.
The Problem: Thread-Affinity Crashes
When automating or debugging a target application, the most common failure mode is calling target functions on the wrong thread. Many applications (especially games, UI apps, and applications with scripting engines) have single-threaded state:
- Scripting VMs (Lua, Python, custom engines)
- Render contexts (Direct3D, OpenGL)
- Object models and state machines
- UI message pumps
If you call these functions from a thread you created via CreateRemoteThread, they race the target's main thread → memory corruption → crash.
WhiteMagic's solution: Split execution by payload safety, making the crash-safe path the default.
Model 1: RemoteThreadExecutor (CreateRemoteThread)
Mechanism
Creates a new thread in the target process via CreateRemoteThread, executes a call stub, and waits for the exit code.
Use Cases ✅
SAFE for thread-agnostic payloads:
- Pure WinAPI calls (
GetTickCount,GetCurrentProcessId) - Self-contained computations
LoadLibrary(DLL injection)- Code that touches only memory you own
Avoid ❌
UNSAFE for single-threaded target state:
- Scripting engine entry points
- Render operations (Direct3D calls)
- Game state queries/modifications
- UI interactions
- Object model traversal
Usage Example
using WhiteMagic;
using WhiteMagic.Assembly;
using var magic = Magic.Open(targetProcess);
// Example: Call GetTickCount (thread-safe WinAPI)
var getTickCount = magic["kernel32"]["GetTickCount"];
uint ticks = getTickCount.Execute<uint>(CallConvention.Stdcall);
// Example: Call a function that doesn't touch thread-local state
int result = magic.RemoteThread.Execute<int>(
functionAddress,
CallConvention.Cdecl,
arg1, arg2, arg3
);
Performance
- Latency: ~1-2ms (thread creation + execution + join)
- Throughput: Limited by thread creation overhead
- Best for: One-shot calls, initialization, DLL injection
Risks
- Crash risk: HIGH if touching single-threaded state
- Detection: Easily detected by anti-cheat (new thread creation)
- Overhead: Thread creation is not cheap
Model 2: MainThreadPump (Crash-Safe) ⭐
Mechanism
Installs a detour on a per-frame function (a function called every frame, like D3D's EndScene) and drains a thread-safe work queue there each frame.
How It Works
- Detour installation: Hook a per-frame function at
frameAddress - Queue enqueue: Caller enqueues a delegate via
Enqueue<T>() - Frame execution: Target's main thread runs the hook each frame
- Queue drain: Hook checks queue, executes pending work items
- Result return:
TaskCompletionSourcedelivers result/exception
Use Cases ✅
SAFE for state-sensitive calls:
- Game state modifications (health, position, inventory)
- Scripting engine calls
- UI interactions
- Render operations
- Object model traversal
- Any function that assumes main-thread context
Usage Example
using WhiteMagic;
using WhiteMagic.Execution;
using var magic = Magic.Open(targetProcess);
// Create a pump that hooks a per-frame function
// (e.g., D3D9 EndScene, or any per-frame handler)
var pump = magic.CreateMainThreadPump(frameAddress);
// Enqueue work that runs on the target's main thread
int health = await pump.Enqueue(() =>
{
// Safe to touch game state here
return magic.Memory.Read<int>(healthAddress);
});
// Modify game state safely
await pump.Enqueue(() =>
{
magic.Memory.Write(healthAddress, 999);
});
// Call a function that requires main-thread context
var result = await pump.Enqueue(() =>
{
var fn = magic["target"]["ProcessInput"];
return fn.Execute<int>(CallConvention.ThisCall, inputPtr);
});
Finding a Frame Function
Common per-frame functions:
- Direct3D 9:
EndScene(device + 0x44 vtable entry) - Direct3D 11: Present callbacks
- OpenGL: SwapBuffers callbacks
- Custom: Many games have a
UpdateorRenderfunction per frame
Helper for D3D9:
// Find D3D9 device and resolve EndScene
IntPtr d3dDevice = FindD3D9Device(magic);
IntPtr endScene = magic.Memory.Read<IntPtr>(d3dDevice + 0x44); // VTable
var pump = magic.CreateMainThreadPump(endScene);
Performance
- Latency: ~1 frame (16-33ms at 30-60 FPS)
- Throughput: Limited by frame rate and work item duration
- Best for: Repeated state-sensitive calls, game mods, automation
Safety Features
- Crash-safe: Runs on target's own thread
- Exception propagation: Exceptions in work items propagate to caller
- Timeout handling: Can detect wedged work items
- Queue bounded: Prevents unlimited queue growth
Risks
- Frame overhead: Hook adds per-frame overhead (keep work items short)
- Wedged work item: A stuck work item stalls the frame (detectable via timeout)
- Frame function needed: Requires finding a per-frame function (application-specific)
Model 3: InProcessInvoker (Direct Delegates)
Mechanism
Once a managed DLL is injected into the target, creates native delegates via Marshal.GetDelegateForFunctionPointer and calls them directly.
Use Cases ✅
When injected in-process:
- Direct function calls with zero thread crossing
- High-performance repeated calls
- Full .NET interop capabilities
Usage Example
using WhiteMagic;
using WhiteMagic.Execution;
// Only works when injected in-process
using var magic = Magic.OpenInProcess();
// Create a delegate to a native function
var getName = magic.Memory.CreateFunction<GetNameDelegate>(
getNameAddress
);
// Call directly as a delegate
string name = getName(12345);
// Delegate signature
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate string GetNameDelegate(int id);
When to Use
- After injection: Your managed DLL is already in the target
- Performance-critical: Need sub-microsecond call latency
- Complex interop: Need to pass complex structures or callbacks
Performance
- Latency: <1μs (direct function call)
- Throughput: Highest (no thread crossing)
- Best for: In-process tools, profilers, injected helpers
Limitations
- In-process only: Requires managed DLL injection
- No crash-safety benefit: Still subject to thread-affinity issues
- Requires loader: Need a CLR host or injection bootstrapper
Choosing the Right Model
Decision Flowchart
Are you injected in-process?
├─ Yes → Use InProcessInvoker (direct delegates)
└─ No → Does the call touch single-threaded state?
├─ Yes → Use MainThreadPump (crash-safe)
└─ No → Use RemoteThreadExecutor (CreateRemoteThread)
Practical Guidelines
| Scenario | Model | Reason |
|---|---|---|
| DLL injection | RemoteThreadExecutor | LoadLibrary is thread-safe |
| Read health bar | MainThreadPump | Game state is main-thread-affine |
| Call GetTickCount | RemoteThreadExecutor | WinAPI, no thread affinity |
| Script engine call | MainThreadPump | Script VM is main-thread-only |
| Injected profiling | InProcessInvoker | Already in-process, max performance |
| Window mutation | RemoteThreadExecutor | WinAPI SetWindowPos is thread-safe |
Common Mistakes
❌ Wrong: Using RemoteThreadExecutor for game state reads
// UNSAFE: Will crash in most games
int health = magic.RemoteThread.Execute<int>(
readHealthFn,
CallConvention.Cdecl
);
✅ Right: Using MainThreadPump for game state
// SAFE: Runs on game's main thread
int health = await pump.Enqueue(() =>
magic.Memory.Read<int>(healthAddress)
);
Comparison Summary
| Feature | RemoteThreadExecutor | MainThreadPump | InProcessInvoker |
|---|---|---|---|
| Safety | Thread-agnostic only | Crash-safe | In-process context |
| Latency | ~1-2ms | ~1 frame (16-33ms) | <1μs |
| Detection risk | High (new thread) | Low (detour) | None (in-process) |
| Best for | One-shot calls, DLL injection | State-sensitive calls | In-process tools |
| Setup cost | Low | Medium (need frame fn) | High (need injection) |
| Throughput | Low | Medium | Highest |
Advanced Topics
Bypassing Anti-Cheat
RemoteThreadExecutor is easily detected (new thread creation). For stealth:
- Use MainThreadPump: Detours are harder to detect than thread creation
- Thread hijacking: For one-shot calls, hijack an existing thread (see
CodeInjector)
Combining Models
// Use RemoteThreadExecutor to inject DLL
magic.RemoteThread.Execute<IntPtr>(
loadLibraryAddress,
CallConvention.Stdcall,
dllPathPtr
);
// Now injected, switch to InProcessInvoker
using var inProcess = Magic.OpenInProcess();
var fn = inProcess.Memory.CreateFunction<MyDelegate>(address);
Error Handling
try
{
int result = await pump.Enqueue(() =>
magic.Memory.Read<int>(address)
);
}
catch (AccessViolationException)
{
// Address not readable
}
catch (TimeoutException)
{
// Work item wedged (stuck the frame)
}
Further Reading
- Architecture — Overall system design
- Function Hooking — DetourManager internals
- Memory Access — MemoryBase and MarshalCache