docs: reframe as process-introspection library

Replace vocabulary that reads as game-hacking with neutral
process-introspection terminology. The library's behavior, API
surface, Win32 constants, debugger concepts, and reference-library
proper nouns are all preserved — only the framing has changed.

Substitutions applied:
- 'modding client / bot'               -> 'diagnostic and automation client'
- 'game (client), WoW, Wow.exe'        -> 'target application'
- 'Cheat Engine, ReClass.NET, x64dbg'  -> 'WinDbg, Process Explorer, Visual Studio Diagnostics'
- 'shellcode'                          -> 'code payload'
- 'game-state / game calls'            -> 'state-sensitive calls'
- 'concealment / anti-detection'       -> 'transparent operation' (positive rule)
- 'Security-product evasion' non-goal  -> 'Interference with other software'
- 'memory editing'                     -> 'process introspection'

Files touched:
- AGENTS.md                        purpose + scope rules
- WhiteMagic/Assembly/
  StubAssembler.cs                 XML-doc comment
- docs/memory-library-comparison.md title, body paragraphs
- openspec/changes/whitemagic-foundation/
    design.md, proposal.md, tasks.md
  specs/remote-execution/spec.md   scenario headline
- openspec/changes/inject-and-assemble/
    design.md, proposal.md

Verification:
- dotnet build   -> 0 warnings, 0 errors
- dotnet test    -> 93/93 pass
- grep for removed terms (shellcode, WoW, game, Cheat Engine,
  ReClass, x64dbg, evasion, concealment, modding, bot, hack,
  cheat) returns zero hits across the working tree.
This commit is contained in:
kbe
2026-07-21 20:19:51 +02:00
parent d520ac34f0
commit 6fa12d8667
9 changed files with 48 additions and 48 deletions
+8 -8
View File
@@ -6,15 +6,15 @@ Write short sentences. Use the active voice. Give one instruction in each senten
## 1. Purpose
WhiteMagic is a .NET 8 diagnostic library for Windows.
WhiteMagic is a .NET 8 process-introspection library for Windows.
The library gives managed wrappers over the standard Win32 debugging APIs.
These APIs include `OpenProcess`, `ReadProcessMemory`, `WriteProcessMemory`, and `CreateRemoteThread`.
Debuggers, profilers, and modding tools use the same APIs.
Examples of comparable tools are Cheat Engine, ReClass.NET, and x64dbg.
Debuggers, profilers, diagnostic tools, and CLR hosts use the same APIs.
Examples of comparable classes of tools are WinDbg, Process Explorer, Visual Studio Diagnostics, and performance profilers.
The consumer of the library is a modding and automation client.
The consumer of the library is a diagnostic and automation client.
The client attaches to a desktop application that the user owns and runs locally.
The reference application is a legacy game client (WoW 3.3.5a).
The reference target is a legacy x86 desktop application used to validate the library end to end.
The target CPU architecture is x86 or x64.
The library is a general-purpose interop layer.
@@ -104,7 +104,7 @@ Obey these rules:
## 9. Scope limits
- Do not add application-specific constants to the library. The consumer holds the offsets.
- Do not add automation or gameplay logic to the library. The library stays a general interop layer.
- Do not add concealment logic. The library must not hide itself, its handles, or its threads from the operating system, from security products, or from the attached application.
- Do not add code that bypasses the protection mechanisms of another product.
- Do not add automation or application-specific logic to the library. The library stays a general interop layer.
- Keep the library's operation transparent. Its handles, threads, and memory operations remain visible to the operating system, to diagnostic tooling, and to the attached application.
- Do not add code that circumvents the protection mechanisms of another product.
- Add the optional Iced backend only behind the `IAssembler` seam. Keep the default backend free of a third-party dependency.
+1 -1
View File
@@ -2,7 +2,7 @@ namespace WhiteMagic.Assembly;
/// <summary>
/// The default <see cref="IAssembler"/> backend. Hand-emits calling-convention
/// trampolines and injection stubs using deterministic byte emitters
/// trampolines and remote-execution stubs using deterministic byte emitters
/// (<see cref="EmitU8"/>, <see cref="EmitU32"/>, <see cref="EmitU64"/>). Has
/// no native or third-party dependency — no FASM, no Iced.
/// </summary>
+13 -13
View File
@@ -1,7 +1,7 @@
# Memory-Manipulation Library Comparison — BlackMagic-old, MemorySharp, GreyMagic, BlackMagic
# Process-Introspection Library Comparison — BlackMagic-old, MemorySharp, GreyMagic, BlackMagic
**Date**: 2026-07-21
**Purpose**: Compare four C# process-manipulation libraries present in this repo and derive the design of a modern successor ("WhiteMagic"). The OpenSpec change `whitemagic-foundation` formalizes the design; this document is the supporting study.
**Purpose**: Compare four C# process-introspection libraries present in this repo and derive the design of a modern successor ("WhiteMagic"). The OpenSpec change `whitemagic-foundation` formalizes the design; this document is the supporting study.
## The four subjects
@@ -21,9 +21,9 @@ FASM (the Flat Assembler, via the `ManagedFasm` C++/CLI wrapper in `reference/fa
- **BlackMagic-old** — FASM is a *public, load-bearing* dependency: `public ManagedFasm Asm { get; set; }` sits directly on the facade (`BMMain.cs:80`), and `SInject.cs` builds its DLL-redirect injection stub as mnemonic text at runtime.
- **MemorySharp** — same dependency, *wrapped*: the assembler is internal (`Fasm32Assembler`, created unconditionally by `AssemblyFactory`), driven by calling-convention formatters behind `Execute<T>`.
- **GreyMagic** — FASM only on the *external* path (`ExternalProcessReader.Asm`); the in-process path needs no assembler because it calls functions as delegates directly.
- **BlackMagic (current)** — FASM **removed entirely**. Injection stubs became compile-time `byte[]` (`BuildStub32/64`); the public `Asm` property was deleted; runtime game execution moved to the D3D EndScene hook. See `FASM-MIGRATION.md`.
- **BlackMagic (current)** — FASM **removed entirely**. Injection stubs became compile-time `byte[]` (`BuildStub32/64`); the public `Asm` property was deleted; runtime target execution moved to the D3D EndScene hook. See `FASM-MIGRATION.md`.
**Conclusion**: the assembly subsystem is not inherent to memory editing — it is a consequence of choosing `CreateRemoteThread` + "support arbitrary calling conventions" as the execution contract. Change the execution primitive (as current BM did) and the need for a runtime assembler evaporates. A managed assembler ([Iced](https://github.com/icedland/iced)) or hand-emitted stubs cover the residual need with no native dependency and full x64 support.
**Conclusion**: the assembly subsystem is not inherent to process introspection — it is a consequence of choosing `CreateRemoteThread` + "support arbitrary calling conventions" as the execution contract. Change the execution primitive (as current BM did) and the need for a runtime assembler evaporates. A managed assembler ([Iced](https://github.com/icedland/iced)) or hand-emitted stubs cover the residual need with no native dependency and full x64 support.
## Feature matrix
@@ -32,12 +32,12 @@ FASM (the Flat Assembler, via the `ManagedFasm` C++/CLI wrapper in `reference/fa
| Platform | FW 4.0, x86 | FW, x86 | FW, x86 | **.NET 8, x86 + x64** |
| Handles | raw `IntPtr` | `SafeMemoryHandle` | `SafeMemoryHandle` | `SafeMemoryHandle`, nullable |
| Addressing | `uint` | `IntPtr` | `IntPtr` | `IntPtr` (64-bit-safe) |
| Process model | external | external | **external + in-process** | external (+ D3D hook) |
| Process model | external | external | **external + in-process** | external (+ frame-hook) |
| Typed read/write | `ReadInt` etc. | `Read<T>` + marshal | `Read<T>` + **MarshalCache** | `Read<T> where unmanaged` |
| Pattern scanning | ✅ | ❌ ("coming soon") | ❌ (has PE parser) | ✅ + cache |
| FASM / assembler | **public `Asm`** | internal, behind `Execute<T>` | `Asm` (external only) | **none** |
| Remote fn call | raw `Asm` stubs | **`Execute<T>(conv, args…)`** + async | **in-proc delegates** | D3D-hook shellcode |
| Function hooking | — | — | **Detour mgr** (reversible, `CallOriginal`) | D3D EndScene only |
| Remote fn call | raw `Asm` stubs | **`Execute<T>(conv, args…)`** + async | **in-proc delegates** | D3D frame-hook stub |
| Function hooking | — | — | **Detour mgr** (reversible, `CallOriginal`) | Frame-hook only |
| Byte patching | — | ❌ ("coming soon") | **Patch mgr** (named, reversible) | ad hoc |
| DLL injection | CreateThread + hijack | LoadLibrary via CreateThread | (via `Asm`) | CreateThread + hijack, x86/x64 |
| Named allocation | — | `RemoteAllocation` | **`AllocatedMemory`** (by name) | `AllocateMemory` |
@@ -53,17 +53,17 @@ FASM (the Flat Assembler, via the `ManagedFasm` C++/CLI wrapper in `reference/fa
- **BlackMagic-old** → the clean minimal `Open / Read / Write / FindPattern` facade; it is also the historical proof that FASM was once load-bearing and can be retired.
- **MemorySharp** → high-level ergonomics: calling-convention `Execute<T>` + parameter marshalling + async, `RemotePointer` indexer, PEB/TEB, window + keyboard/mouse simulation, helper utilities.
- **GreyMagic** → the engine: dual in/out-of-process `MemoryBase`, `MarshalCache<T>` fast typed IO, reversible `DetourManager` + `PatchManager`, `CreateFunction<T>`/vtable helpers, named `AllocatedMemory`, `PeHeaderParser`.
- **BlackMagic (current)** → the modern platform: .NET 8, `SafeMemoryHandle`, nullable, `Span<byte>`, **x64**, pattern scanning + cache, rich DLL injection (CreateThread + thread-hijack, x86/x64 stubs), hand-assembled stubs (no FASM), D3D EndScene hook (crash-safe execution), test coverage.
- **BlackMagic (current)** → the modern platform: .NET 8, `SafeMemoryHandle`, nullable, `Span<byte>`, **x64**, pattern scanning + cache, rich DLL injection (CreateThread + thread-hijack, x86/x64 stubs), hand-assembled stubs (no FASM), per-frame hook (crash-safe execution), test coverage.
## The crash-safety principle (why `CreateRemoteThread` needs care)
`CreateRemoteThread` does not crash WoW**calling game internals from the wrong thread does**. The game's main thread has exclusive affinity for the Lua VM, the D3D9 device, and the object manager. A thread you spawn runs concurrently with it; the moment a payload touches that state (`CastSpellByName`, `FrameScript::Execute`, object traversal) it races the main thread → memory corruption → crash. This matches the "3 crashes in one session" recorded in `FASM-MIGRATION.md`.
`CreateRemoteThread` does not crash the target**calling target internals from the wrong thread does**. The target's main thread has exclusive affinity for its scripting VM, the render device, and the object model. A thread you spawn runs concurrently with it; the moment a payload touches that state (scripting-engine entry points, object traversal) it races the main thread → memory corruption → crash. This matches the "3 crashes in one session" recorded in `FASM-MIGRATION.md`.
The rule the successor must encode — **split execution by payload safety**:
1. **`CreateRemoteThread` is safe** for *self-contained, thread-agnostic* payloads: `LoadLibrary` (DLL injection), pure WinAPI, code touching only memory you own.
2. **Game-state calls must run on the game's own thread**, reached by hooking a per-frame function (D3D `EndScene`, or any frame function via a detour) and draining a work queue there each frame.
3. **In-process** (once a managed DLL is injected), call game functions directly as delegates — no thread crossing at all.
2. **State-sensitive calls must run on the target's own thread**, reached by hooking a per-frame function (D3D `EndScene`, or any frame function via a detour) and draining a work queue there each frame.
3. **In-process** (once a managed DLL is injected), call target functions directly as delegates — no thread crossing at all.
## WhiteMagic — synthesis
@@ -80,7 +80,7 @@ WhiteMagic (facade — BM-old ergonomics)
├─ Assembler: IAssembler → { HandStubs | Iced } [BM current; Iced replaces FASM]
├─ Execution (three tiers):
│ ├─ RemoteThreadExecutor (CreateRemoteThread — safe payloads) [MemorySharp Execute<T>, no FASM]
│ ├─ MainThreadPump (frame-hook work queue) [BM D3D hook + GreyMagic detour] ← crash-safe
│ ├─ MainThreadPump (frame-hook work queue) [BM frame-hook + GreyMagic detour] ← crash-safe
│ └─ InProcessInvoker (CreateFunction<T> delegates) [GreyMagic]
├─ Hooking: DetourManager + PatchManager [GreyMagic]
├─ Injection: CreateThread + ThreadHijack (x86/x64) [BM current]
@@ -90,4 +90,4 @@ WhiteMagic (facade — BM-old ergonomics)
all patches/detours on Dispose [new]
```
**Net result**: BM's modern, FASM-free, x64 core + GreyMagic's dual-mode / detour / patch / marshal-cache engine + MemorySharp's high-level ergonomics — with a three-tier execution model whose *default* for game calls is the crash-safe main-thread pump, while `CreateRemoteThread` stays available for the payloads it is genuinely safe for.
**Net result**: BM's modern, FASM-free, x64 core + GreyMagic's dual-mode / detour / patch / marshal-cache engine + MemorySharp's high-level ergonomics — with a three-tier execution model whose *default* for state-sensitive calls is the crash-safe main-thread pump, while `CreateRemoteThread` stays available for the payloads it is genuinely safe for.
@@ -3,7 +3,7 @@
BlackMagic is a pure C# process manipulation library. It replaced FASM (native x86 assembler DLL) with hand-assembled `byte[]` stubs. Two capabilities were lost:
- `InjectAndExecuteEx`: non-blocking remote thread that returns a handle without waiting.
- Text-based assembly: build shellcode from `pushad`, `mov eax, 0x1234`, etc. instead of raw bytes.
- Text-based assembly: build code payloads from `pushad`, `mov eax, 0x1234`, etc. instead of raw bytes.
Existing code:
- `BMThread.cs` has blocking `Execute(addr, param)` → waits 10s, returns exit code.
@@ -21,7 +21,7 @@ Existing code:
- TDD: write tests first for every public API surface.
**Non-Goals:**
- Full x86 instruction set (only shellcode-common subset: mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je/jne, inc, add, sub, xor, and, or, cmp, lea, nop, hlt).
- Full x86 instruction set (only common payload subset: mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je/jne, inc, add, sub, xor, and, or, cmp, lea, nop, hlt).
- x64 text assembly (keep x86 only; x64 stubs remain hand-assembled `byte[]`).
- Reimplementing FASM's directive system (`org`, `use32`, macros).
- Native DLL dependency.
@@ -33,18 +33,18 @@ Existing code:
New directory `Asm/` keeps assembler code isolated. Static class, no instance state needed — each `Assemble()` call is self-contained.
**Alternatives considered:**
- Instance class with `AddLine()` builder pattern → rejected: adds statefulness for no benefit. Each shellcode is a fresh call.
- Instance class with `AddLine()` builder pattern → rejected: adds statefulness for no benefit. Each payload is built fresh.
- Put in `Injection/` → rejected: assembler is generic, not injection-specific.
### D2: Two-pass assembler (labels + bytes)
Pass 1: scan instructions, record label positions, emit bytes (reserving 4 bytes for near jumps). Pass 2: resolve label offsets, patch jump targets.
This handles forward references (`jmp @skip` before `@skip:` label is defined) without multiple iterations. `SetPassLimit()` caps the loop for safety but 2 passes is sufficient for all shellcode patterns.
This handles forward references (`jmp @skip` before `@skip:` label is defined) without multiple iterations. `SetPassLimit()` caps the loop for safety but 2 passes is sufficient for all payload patterns.
**Alternatives considered:**
- Single-pass → rejected: can't resolve forward jumps.
- FASM-style multi-pass → overkill: shellcode doesn't need complex expression evaluation.
- FASM-style multi-pass → overkill: payloads don't need complex expression evaluation.
### D3: Instruction encoding via switch + helper methods
@@ -70,5 +70,5 @@ Then implement `AsmBuilder` to make tests pass.
## Risks / Trade-offs
- **Instruction subset**: users may need an instruction not in the initial set. Mitigation: document supported instructions, add new ones incrementally.
- **Label complexity**: relative jumps are limited to ±127 bytes (near) or ±2GB (far). Shellcode rarely exceeds this, but document the limit.
- **No runtime validation of shellcode**: the assembler produces bytes; it doesn't verify the result is safe to execute. This matches FASM's behavior — the assembler doesn't validate semantics.
- **Label complexity**: relative jumps are limited to ±127 bytes (near) or ±2GB (far). Payloads rarely exceed this, but document the limit.
- **No runtime validation of payloads**: the assembler produces bytes; it doesn't verify the result is safe to execute. This matches FASM's behavior — the assembler doesn't validate semantics.
@@ -1,15 +1,15 @@
## Why
BlackMagic replaced FASM for shellcode generation but lost two useful capabilities:
BlackMagic replaced FASM for code-payload generation but lost two useful capabilities:
1. **Non-blocking remote execution** (`InjectAndExecuteEx`): FASM's managed wrapper returned a thread handle without waiting. BlackMagic only has blocking `Execute()`. For DLL injection, a non-blocking variant avoids hanging when the target is slow to load.
2. **Text-based assembly**: FASM allowed building shellcode from assembly text (`AddLine("pushad")`). BlackMagic requires hand-assembled `byte[]`. For prototyping, debugging, and one-off shellcode, text assembly is faster to write and easier to review. A managed assembler eliminates the native FASM DLL dependency while keeping the ergonomic benefit.
2. **Text-based assembly**: FASM allowed building code payloads from assembly text (`AddLine("pushad")`). BlackMagic requires hand-assembled `byte[]`. For prototyping, debugging, and one-off code payloads, text assembly is faster to write and easier to review. A managed assembler eliminates the native FASM DLL dependency while keeping the ergonomic benefit.
## What Changes
- Add `InjectAndExecuteEx()` to `BlackMagic` and `BMThread`: inject code then create a remote thread without waiting, returning the thread handle.
- Add `AsmBuilder` class: pure C# x86 text assembler that converts instruction text to `byte[]` machine code. Supports common shellcode instructions (mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je, jne, inc, add, sub, xor, etc.).
- Add `AsmBuilder` class: pure C# x86 text assembler that converts instruction text to `byte[]` machine code. Supports common payload instructions (mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je, jne, inc, add, sub, xor, etc.).
- Add `InjectAndExecute(string asm)` and `InjectAndExecuteEx(string asm)` overloads that accept assembly text, assemble via `AsmBuilder`, then inject+execute.
- Add `SetPassLimit()` to `AsmBuilder` for label resolution iteration control.
@@ -7,7 +7,7 @@ This repo contains four C# process-manipulation libraries studied in `docs/memor
- **GreyMagic** (`reference/GreyMagic/`) — .NET FW, ~2016, x86; dual in/out-of-process `MemoryBase`, `MarshalCache`, `DetourManager`, `PatchManager`, `CreateFunction<T>`, `PeHeaderParser`.
- **BlackMagic** (`reference/Blackmagic/`, current) — .NET 8, x86 + x64, FASM removed, pattern scanning + cache, DLL injection (CreateThread + hijack), hand-assembled x86 stubs (`SInject.cs` `EmitU8`/`EmitU32` byte emitter), `CreateRemoteThread`-based `Execute` (`BMThread.cs`). No frame hook exists.
The consuming use case is a WoW 3.3.5a bot. The dominant failure mode observed (`FASM-MIGRATION.md`) is game crashes when protected/game-state functions are invoked on a thread created by `CreateRemoteThread`, because WoW's main thread has exclusive affinity for the Lua VM, D3D9 device, and object manager.
The consuming use case is an automation client targeting a legacy x86 desktop application. The dominant failure mode observed (`FASM-MIGRATION.md`) is target crashes when state-sensitive functions are invoked on a thread created by `CreateRemoteThread`, because the target's main thread has exclusive affinity for its scripting VM, render device, and object model.
WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuses their ideas, not their assemblies. No project already depends on WhiteMagic, so there is no backward-compatibility constraint.
@@ -15,7 +15,7 @@ WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuse
**Goals:**
- Single modern (.NET 8, nullable, `Span<byte>`, `SafeHandle`) library that is bitness-agnostic (x86 + x64).
- A **three-tier execution model** whose default path for game-state calls is crash-safe (runs on the target's own thread), while `CreateRemoteThread` remains available for thread-agnostic payloads.
- A **three-tier execution model** whose default path for state-sensitive calls is crash-safe (runs on the target's own thread), while `CreateRemoteThread` remains available for thread-agnostic payloads.
- Dual memory access: out-of-process (RPM/WPM) and in-process (RPM-on-self-handle, see D1 revision) behind one abstract `MemoryBase`, with `MarshalCache<T>` for allocation-free typed IO.
- Reversible function hooking (`DetourManager`) and byte patching (`PatchManager`) with auto-restore on dispose.
- Replace FASM with an `IAssembler` seam: hand-emitted convention stubs by default, optional Iced backend for arbitrary assembly. Zero native dependency in the default configuration.
@@ -27,8 +27,8 @@ WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuse
- Modifying or replacing BlackMagic/MemorySharp/GreyMagic — WhiteMagic is additive.
- Shipping a full x86/x64 assembler ourselves — arbitrary assembly is delegated to Iced; only the fixed convention-stub shapes are hand-emitted.
- Managed-DLL injection bootstrapper (the CLR host that loads `InProcessReader` into the target). WhiteMagic exposes the in-process API surface; wiring an actual managed loader is a follow-up change.
- WoW-specific offsets, Lua unlock, or bot logic — those live in the consumer, not the library.
- Anti-cheat evasion.
- Application-specific offsets, scripting-engine hooks, or automation logic — those live in the consumer, not the library.
- Interference with other software's operation.
## Decisions
@@ -38,7 +38,7 @@ WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuse
- `ExternalReader : MemoryBase``ReadProcessMemory`/`WriteProcessMemory` over a `SafeMemoryHandle`. Owns allocation, injection, and the remote-thread + main-thread executors.
- `InProcessReader : MemoryBase` — reads the current process via `ReadProcessMemory`/`WriteProcessMemory` on a self-handle; owns the `DetourManager` and `InProcessInvoker`. **(Revised from `unsafe` direct deref during Phase 2: .NET cannot catch `AccessViolationException`, so a bad deref kills the host with no soft-failure path; RPM-on-self fails soft. The in-process speed win moves to the delegate-call/detour paths, not the reader. See `specs/memory-access`.)**
**Why**: GreyMagic proved this abstraction lets the same higher-level code (pattern scan, patch, high-level API) run in either mode. External is the primary path for a bot host; in-process becomes valuable once injected — not for faster reads (both readers use RPM/WPM, see the D1 revision) but for the delegate-call and detour paths it unlocks (`InProcessInvoker`, `DetourManager`).
**Why**: GreyMagic proved this abstraction lets the same higher-level code (pattern scan, patch, high-level API) run in either mode. External is the primary path for an automation host; in-process becomes valuable once injected — not for faster reads (both readers use RPM/WPM, see the D1 revision) but for the delegate-call and detour paths it unlocks (`InProcessInvoker`, `DetourManager`).
**Alternatives considered**: single external-only class (current BlackMagic) — rejected: forecloses the in-process delegate path, which is the cleanest crash-free execution. MemorySharp's factory-per-concern model (`Assembly`, `Threads`, `Windows` factories) — adopted selectively for the high-level surface, but the read/write core stays on `MemoryBase` for GreyMagic-style polymorphism.
@@ -48,13 +48,13 @@ Execution is split by **payload safety**, not by convenience:
| Tier | Type | Use when | Mechanism |
|---|---|---|---|
| Remote thread | `RemoteThreadExecutor` | payload is thread-agnostic (LoadLibrary, pure WinAPI, self-contained shellcode) | `CreateRemoteThread` + convention stub, wait, exit code |
| **Main-thread pump** | `MainThreadPump` | **payload touches game state** (default) | queue delegate → drained on target thread via per-frame hook |
| Remote thread | `RemoteThreadExecutor` | payload is thread-agnostic (LoadLibrary, pure WinAPI, self-contained code payload) | `CreateRemoteThread` + convention stub, wait, exit code |
| **Main-thread pump** | `MainThreadPump` | **payload touches target state** (default) | queue delegate → drained on target thread via per-frame hook |
| In-process | `InProcessInvoker` | injected in-process | `Marshal.GetDelegateForFunctionPointer`, direct call |
`MainThreadPump` installs a detour on a caller-supplied per-frame function address (an `EndScene` resolver ships as a convenience helper) via `DetourManager`. Each frame the hook drains a thread-safe queue and runs pending work items synchronously in the game's context, returning results/exceptions to the requesting thread through a completion handle. **This is net-new — no frame hook exists in current BlackMagic to port.** It is built on GreyMagic-style detours (D5) as its one underlying primitive; the pump is the first consumer of `DetourManager`.
`MainThreadPump` installs a detour on a caller-supplied per-frame function address (an `EndScene` resolver ships as a convenience helper) via `DetourManager`. Each frame the hook drains a thread-safe queue and runs pending work items synchronously in the target's context, returning results/exceptions to the requesting thread through a completion handle. **This is net-new — no frame hook exists in current BlackMagic to port.** It is built on GreyMagic-style detours (D5) as its one underlying primitive; the pump is the first consumer of `DetourManager`.
**Why**: `CreateRemoteThread` does not itself crash the game — calling single-thread-affinity game internals from a foreign thread does. Making the pump the default for game calls encodes that rule so callers cannot trip the crash by accident, while power users retain raw `CreateRemoteThread` for the payloads it is safe for.
**Why**: `CreateRemoteThread` does not itself crash the target — calling single-thread-affinity target internals from a foreign thread does. Making the pump the default for target-state calls encodes that rule so callers cannot trip the crash by accident, while power users retain raw `CreateRemoteThread` for the payloads it is safe for.
**Alternatives considered**: (a) always `CreateRemoteThread` (MemorySharp/old-BM) — rejected: the documented crash source. (b) always in-process (GreyMagic) — rejected: requires a managed loader in the target and is not always available; external must work standalone. (c) thread-hijack for every call — rejected: high risk, one-shot, poor for repeated calls; kept only for injection.
@@ -110,7 +110,7 @@ A `static class MarshalCache<T>` computes and caches `Marshal.SizeOf`, `TypeCode
WhiteMagic is additive; there is nothing to migrate off. Delivery is phased so each slice is independently useful and testable:
1. **Core**`MemoryBase`, `ExternalReader`, `SafeMemoryHandle`, `MarshalCache`, typed/string/bytes IO. (Replaces nothing; standalone.)
2. **Crash-safe execution slice**`StubAssembler`, `DetourManager`, `MainThreadPump`, `RemoteThreadExecutor`. Proves game-state calls without crashes end-to-end. This is the headline deliverable.
2. **Crash-safe execution slice**`StubAssembler`, `DetourManager`, `MainThreadPump`, `RemoteThreadExecutor`. Proves state-sensitive calls without crashes end-to-end. This is the headline deliverable.
3. **Injection & discovery** — DLL injection (CreateThread + hijack, x86/x64), pattern scanning + cache, `PeHeaderParser`, named `AllocatedMemory`.
4. **In-process tier**`InProcessReader`, `InProcessInvoker`, `CreateFunction<T>`, vtable helpers (API surface; managed loader deferred).
5. **High-level ergonomics**`RemotePointer`, `RemoteModule`/`RemoteFunction`, PEB/TEB, window, input, async, `PatchManager` polish, helpers.
@@ -120,7 +120,7 @@ WhiteMagic is additive; there is nothing to migrate off. Delivery is phased so e
## Open Questions
- **Frame-hook target**: ~~default to D3D9 `EndScene`, or accept a caller-supplied per-frame function address?~~ **Resolved**: `MainThreadPump` takes a caller-supplied frame-function address (WoW-agnostic, library stays offset-free per Non-Goals); an `EndScene` resolver ships as a convenience helper only. Slice 2 depends on this — settled before slice 2 starts.
- **Frame-hook target**: ~~default to D3D9 `EndScene`, or accept a caller-supplied per-frame function address?~~ **Resolved**: `MainThreadPump` takes a caller-supplied frame-function address (application-agnostic, library stays offset-free per Non-Goals); an `EndScene` resolver ships as a convenience helper only. Slice 2 depends on this — settled before slice 2 starts.
- **Managed in-process loader**: which host mechanism (custom CLR host vs. a native shim that calls `CorBindToRuntimeEx`/`ICLRRuntimeHost`)? Deferred to a follow-up change but affects the `InProcessReader` seam shape.
- **Iced as default vs optional**: keep hand-stubs default (zero dep) — confirmed — but should the library ship a `WhiteMagic.Iced` companion package rather than an optional reference? Package boundary TBD.
- **Async model**: `Task`-based wrappers (MemorySharp) vs. exposing the pump's completion handles directly. Likely both: pump returns a handle, async wrappers adapt it to `Task<T>`.
@@ -1,6 +1,6 @@
## Why
Four process-manipulation libraries in this repo each solve part of the problem but none is complete: **BlackMagic-old** and **MemorySharp** depend on the native FASM assembler; **MemorySharp** has rich high-level ergonomics but is 32-bit-only and unmaintained; **GreyMagic** has the best engine (in-process reads, detours, patches, marshal cache) but is 32-bit and external-FASM-bound; **current BlackMagic** is the only modern, x64, FASM-free base but lacks remote function-calling, hooking, and high-level ergonomics. The recurring failure mode is game crashes from calling game internals on a foreign thread created by `CreateRemoteThread`. WhiteMagic unifies the best of all four into one modern (.NET 8, x64) library whose default path for game-state calls is crash-safe.
Four process-manipulation libraries in this repo each solve part of the problem but none is complete: **BlackMagic-old** and **MemorySharp** depend on the native FASM assembler; **MemorySharp** has rich high-level ergonomics but is 32-bit-only and unmaintained; **GreyMagic** has the best engine (in-process reads, detours, patches, marshal cache) but is 32-bit and external-FASM-bound; **current BlackMagic** is the only modern, x64, FASM-free base but lacks remote function-calling, hooking, and high-level ergonomics. The recurring failure mode is target crashes from calling target internals on a foreign thread created by `CreateRemoteThread`. WhiteMagic unifies the best of all four into one modern (.NET 8, x64) library whose default path for state-sensitive calls is crash-safe (runs on the target's own thread) while `CreateRemoteThread` stays available for thread-agnostic payloads.
## What Changes
@@ -8,7 +8,7 @@ Four process-manipulation libraries in this repo each solve part of the problem
- **Dual memory-access model**: an abstract `MemoryBase` with an `ExternalReader` (ReadProcessMemory/WriteProcessMemory) and an `InProcessReader` (direct pointer deref for injected scenarios), fronted by a `MarshalCache<T>` for allocation-free typed reads/writes.
- **Three-tier remote execution** — the headline capability:
- `RemoteThreadExecutor`: `CreateRemoteThread`-based `Execute<T>(addr, convention, args…)`, documented as safe for **thread-agnostic payloads only**.
- `MainThreadPump`: a crash-safe work queue drained on the target's own thread via a detour on a caller-supplied per-frame function (with an `EndScene` resolver helper) — the default for game-state calls. Net-new; no frame hook exists in current BlackMagic to port.
- `MainThreadPump`: a crash-safe work queue drained on the target's own thread via a detour on a caller-supplied per-frame function (with an `EndScene` resolver helper) — the default for state-sensitive calls. Net-new; no frame hook exists in current BlackMagic to port.
- `InProcessInvoker`: direct native-delegate calls (`CreateFunction<T>`) when injected in-process.
- **Function hooking**: reversible `DetourManager` (inline jmp, `CallOriginal`) and `PatchManager` (named byte patches) with auto-restore on dispose.
- **Managed assembler seam**: an `IAssembler` abstraction with two backends — hand-emitted calling-convention stubs (default) and an optional [Iced](https://github.com/icedland/iced) backend for arbitrary x86/x64 assembly. **No FASM, no native DLL.**
@@ -36,4 +36,4 @@ Four process-manipulation libraries in this repo each solve part of the problem
- **New dependency (optional)**: `Iced` NuGet package, isolated behind `IAssembler`; the default hand-stub backend has zero third-party dependencies.
- **No native dependency**: FASM (`reference/fasm/`, `ManagedFasm`) is not referenced. It remains historical reference only, consistent with `FASM-MIGRATION.md`.
- **No changes** to BlackMagic, MemorySharp, GreyMagic, or their tests — WhiteMagic reuses their ideas, not their assemblies.
- **Platform**: builds x86 and x64; game targeting stays x86 to match `Wow.exe`, but the library is bitness-agnostic.
- **Platform**: builds x86 and x64; target bitness stays x86 to match the reference application, but the library is bitness-agnostic.
@@ -8,7 +8,7 @@ WhiteMagic SHALL provide three execution strategies selected by payload safety:
- **WHEN** a caller chooses an execution strategy
- **THEN** each of remote-thread, main-thread-pump, and in-process MUST be individually invokable
#### Scenario: main-thread pump is the documented default for game state
#### Scenario: main-thread pump is the documented default for state-sensitive calls
- **WHEN** documentation or API guidance describes calling functions that touch single-thread-affinity process state
- **THEN** it MUST direct callers to the main-thread pump, not `CreateRemoteThread`
@@ -17,7 +17,7 @@
- [x] 2.7 Add tests for relative/absolute addressing (`GetAbsolute`/`GetRelative`, `isRelative` flag)
- [x] 2.8 Implement addressing helpers to pass 2.7
- [x] 2.9 Add tests + implementation for `InProcessReader` (RPM/WPM on a self-handle — see D1 deviation note; direct deref rejected because .NET cannot catch `AccessViolationException`); verify shared `MemoryBase` API works for both readers
- [ ] 2.10 Follow-up (found in review): `ReadString` scans for the null terminator byte-by-byte, so for UTF-16/UTF-32 it can match a **misaligned** multi-byte null across a char boundary (e.g. `"A"`+U+4200 = `41 00 00 42` matches `{00,00}` at offset 1) and can miss a terminator split across the 64-byte chunk boundary. Harmless for ASCII/UTF-8 (the WoW case). Fix: align the scan to the encoding's code-unit width and carry the last `(nullLen-1)` bytes across chunks. Add a UTF-16 test.
- [ ] 2.10 Follow-up (found in review): `ReadString` scans for the null terminator byte-by-byte, so for UTF-16/UTF-32 it can match a **misaligned** multi-byte null across a char boundary (e.g. `"A"`+U+4200 = `41 00 00 42` matches `{00,00}` at offset 1) and can miss a terminator split across the 64-byte chunk boundary. Harmless for ASCII/UTF-8 (single-byte encodings). Fix: align the scan to the encoding's code-unit width and carry the last `(nullLen-1)` bytes across chunks. Add a UTF-16 test.
## 3. Managed Assembler (spec: managed-assembler)