# 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 ```csharp 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(CallConvention.Stdcall); // Example: Call a function that doesn't touch thread-local state int result = magic.RemoteThread.Execute( 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 1. **Detour installation:** Hook a per-frame function at `frameAddress` 2. **Queue enqueue:** Caller enqueues a delegate via `Enqueue()` 3. **Frame execution:** Target's main thread runs the hook each frame 4. **Queue drain:** Hook checks queue, executes pending work items 5. **Result return:** `TaskCompletionSource` delivers 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 ```csharp 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(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(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 `Update` or `Render` function per frame **Helper for D3D9:** ```csharp // Find D3D9 device and resolve EndScene IntPtr d3dDevice = FindD3D9Device(magic); IntPtr endScene = magic.Memory.Read(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 ```csharp 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( 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 ```csharp // UNSAFE: Will crash in most games int health = magic.RemoteThread.Execute( readHealthFn, CallConvention.Cdecl ); ``` ✅ **Right:** Using `MainThreadPump` for game state ```csharp // SAFE: Runs on game's main thread int health = await pump.Enqueue(() => magic.Memory.Read(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: 1. **Use MainThreadPump:** Detours are harder to detect than thread creation 2. **Thread hijacking:** For one-shot calls, hijack an existing thread (see `CodeInjector`) ### Combining Models ```csharp // Use RemoteThreadExecutor to inject DLL magic.RemoteThread.Execute( loadLibraryAddress, CallConvention.Stdcall, dllPathPtr ); // Now injected, switch to InProcessInvoker using var inProcess = Magic.OpenInProcess(); var fn = inProcess.Memory.CreateFunction(address); ``` ### Error Handling ```csharp try { int result = await pump.Enqueue(() => magic.Memory.Read(address) ); } catch (AccessViolationException) { // Address not readable } catch (TimeoutException) { // Work item wedged (stuck the frame) } ``` --- ## Further Reading - [Architecture](./architecture.md) — Overall system design - [Function Hooking](./hooking.md) — DetourManager internals - [Memory Access](./memory-access.md) — MemoryBase and MarshalCache