Files
whitemagic/docs/execution-models.md
T
kbe 6300bebe33 Fix API documentation and examples; add comprehensive documentation suite
- 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.
2026-07-22 22:26:07 +02:00

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

  1. Detour installation: Hook a per-frame function at frameAddress
  2. Queue enqueue: Caller enqueues a delegate via Enqueue<T>()
  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

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 Update or Render function 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:

  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

// 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