Add whitemagic-foundation OpenSpec design; isolate reference libs

Design-only foundation for WhiteMagic, a .NET 8 x64 library unifying the
four studied process-manipulation libs. Adds proposal, design (7 decisions),
7 capability specs, and TDD task breakdown; all validate strict.

Move Blackmagic, Blackmagic-old, GreyMagic, MemorySharp, fasm into
reference/ (gitignored) — studied, not built here; each has its own
upstream repo and nested .git. Rewrite plan doc paths to reference/.

Corrects two factual defects found in review:
- current BlackMagic has no D3D EndScene hook; MainThreadPump is net-new
  built on DetourManager, not a port
- no BlackMagic.slnx exists; task 1.3 creates a fresh solution

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
kbe
2026-07-21 16:50:03 +02:00
co-authored by Claude Opus 4.8
commit 4405af15fd
44 changed files with 4495 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-21
@@ -0,0 +1,74 @@
## Context
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.
Existing code:
- `BMThread.cs` has blocking `Execute(addr, param)` → waits 10s, returns exit code.
- `BMThread.cs` has `CreateRemoteThread(addr, param)` → returns `SafeMemoryHandle?`.
- `SInject.cs` has `InjectCode(addr, bytes)` and `InjectCode(bytes)` → allocate + write.
- `BlackMagicTest/` uses xUnit, `net8.0-windows`, pure-logic tests (no process needed).
## Goals / Non-Goals
**Goals:**
- Add `InjectAndExecuteEx()`: inject code + create remote thread, return handle, do NOT wait.
- Add `AsmBuilder`: pure C# x86 text assembler. Convert `"pushad\nmov eax, 1\npopad"``byte[]`.
- Add `SetPassLimit(int)` on `AsmBuilder` for label resolution iteration control.
- Add convenience overloads: `InjectAndExecute(string asm)`, `InjectAndExecuteEx(string asm)`.
- 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).
- x64 text assembly (keep x86 only; x64 stubs remain hand-assembled `byte[]`).
- Reimplementing FASM's directive system (`org`, `use32`, macros).
- Native DLL dependency.
## Decisions
### D1: AsmBuilder lives in `BlackMagic/Asm/AsmBuilder.cs`
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.
- 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.
**Alternatives considered:**
- Single-pass → rejected: can't resolve forward jumps.
- FASM-style multi-pass → overkill: shellcode doesn't need complex expression evaluation.
### D3: Instruction encoding via switch + helper methods
Each instruction maps to hand-coded byte emission. Helper methods: `EmitU8()`, `EmitU32()`, `EmitModRM()`, `EmitSIB()`. Same pattern as `SInject.cs`'s existing `BuildStub()` methods.
**Alternatives considered:**
- Lookup table / dictionary → rejected: instruction encoding is irregular (ModRM, SIB, displacement, immediate). Switch statements are clearer.
### D4: InjectAndExecuteEx returns `SafeMemoryHandle`
Matches existing `CreateRemoteThread()` return type. Caller is responsible for disposing the handle. No auto-wait, no auto-close.
### D5: TDD approach
Write xUnit tests in `BlackMagicTest/AsmBuilderTests.cs` first:
- Each instruction: known input text → expected `byte[]` output.
- Label resolution: forward jump, backward jump, multiple labels.
- Error cases: unknown instruction, missing operand, invalid register.
- `SetPassLimit`: verify iteration cap is respected.
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.
@@ -0,0 +1,23 @@
## Why
BlackMagic replaced FASM for shellcode 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.
## 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 `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.
## Capabilities
### New Capabilities
- `non-blocking-execute`: Non-blocking remote thread creation that returns a handle without waiting for exit.
- `text-assembler`: Pure C# x86 text assembler converting assembly source to byte arrays without native dependencies.
### Modified Capabilities
<!-- None — additive features only. -->
@@ -0,0 +1,41 @@
## ADDED Requirements
### Requirement: InjectAndExecuteEx creates remote thread without waiting
`BlackMagic.InjectAndExecuteEx(IntPtr startAddress, IntPtr parameter)` injects code at `startAddress` into the opened process, creates a remote thread with `parameter`, and returns the thread handle immediately without waiting for the thread to exit.
#### Scenario: successful non-blocking execution
- **WHEN** a process is open and `InjectAndExecuteEx(addr, param)` is called with a valid code address
- **THEN** a remote thread is created in the target process and a valid `SafeMemoryHandle` is returned
#### Scenario: no process open
- **WHEN** no process is open and `InjectAndExecuteEx(addr, param)` is called
- **THEN** `null` is returned
### Requirement: InjectAndExecuteEx single-parameter overload
`BlackMagic.InjectAndExecuteEx(IntPtr startAddress)` calls `InjectAndExecuteEx(startAddress, IntPtr.Zero)`.
#### Scenario: parameter-less non-blocking execution
- **WHEN** `InjectAndExecuteEx(addr)` is called with a valid address
- **THEN** the thread is created with parameter `IntPtr.Zero`
### Requirement: InjectAndExecuteEx from assembly text
`BlackMagic.InjectAndExecuteEx(string asm)` assembles the text via `AsmBuilder`, allocates remote memory, writes the bytes, calls `InjectAndExecuteEx` on the allocated address, and returns the thread handle.
#### Scenario: execute assembly text non-blocking
- **WHEN** `InjectAndExecuteEx("nop")` is called with a process open
- **THEN** the text is assembled to bytes, written to remote memory, a thread is started, and the handle is returned
#### Scenario: assembly failure
- **WHEN** `InjectAndExecuteEx("invalidinstruction")` is called
- **THEN** `ArgumentException` is thrown with the assembly error
### Requirement: InjectAndExecute from assembly text (blocking convenience)
`BlackMagic.InjectAndExecute(string asm)` assembles the text, allocates remote memory, writes the bytes, calls `Execute` (blocking, 10s timeout), and returns the exit code.
#### Scenario: execute assembly text blocking
- **WHEN** `InjectAndExecute("mov eax, 42\nret")` is called with a process open
- **THEN** the text is assembled, injected, executed, and the thread exit code is returned
@@ -0,0 +1,84 @@
## ADDED Requirements
### Requirement: AsmBuilder assembles x86 instruction text to byte array
`AsmBuilder.Assemble(string source)` parses x86 assembly text and returns the corresponding `byte[]` machine code.
#### Scenario: single instruction
- **WHEN** `AsmBuilder.Assemble("nop")` is called
- **THEN** the result is `[0x90]`
#### Scenario: multiple instructions
- **WHEN** `AsmBuilder.Assemble("pushad\npopad")` is called
- **THEN** the result is `[0x60, 0x61]`
#### Scenario: instruction with immediate operand
- **WHEN** `AsmBuilder.Assemble("mov eax, 1")` is called
- **THEN** the result is `[0xB8, 0x01, 0x00, 0x00, 0x00]`
### Requirement: AsmBuilder supports register operands
Supported registers: `eax`, `ecx`, `edx`, `ebx`, `esp`, `ebp`, `esi`, `edi` (and 8-bit: `al`, `cl`, `dl`, `bl`, `ah`, `ch`, `dh`, `bh`).
#### Scenario: register-to-register move
- **WHEN** `AsmBuilder.Assemble("mov eax, ecx")` is called
- **THEN** the result is `[0x89, 0xC8]` (mov eax, ecx encoding)
#### Scenario: register encoding
- **WHEN** registers are used in instructions
- **THEN** each register maps to its correct 3-bit encoding (eax=0, ecx=1, edx=2, ebx=3, esp=4, ebp=5, esi=6, edi=7)
### Requirement: AsmBuilder supports labels and jumps
Labels are defined with `@name:` and referenced with `jmp @name` or `je @name`. Forward and backward references are resolved in a second pass.
#### Scenario: forward jump
- **WHEN** `AsmBuilder.Assemble("jmp @skip\nnop\n@skip:\nret")` is called
- **THEN** the jump skips exactly over the `nop` (2 bytes) and lands on `ret`
#### Scenario: backward jump
- **WHEN** `AsmBuilder.Assemble("@loop:\nnop\njmp @loop")` is called
- **THEN** the jump targets the earlier label correctly
#### Scenario: multiple labels
- **WHEN** multiple labels are used in one source
- **THEN** each label resolves to its correct byte offset
### Requirement: AsmBuilder SetPassLimit controls iteration
`AsmBuilder.SetPassLimit(int limit)` sets the maximum number of assembly passes for label resolution. Default is 10. If the limit is exceeded before all labels resolve, `InvalidOperationException` is thrown.
#### Scenario: default pass limit
- **WHEN** no `SetPassLimit` is called
- **THEN** the assembler uses 10 passes maximum
#### Scenario: custom pass limit
- **WHEN** `SetPassLimit(20)` is called
- **THEN** the assembler uses 20 passes maximum
#### Scenario: pass limit exceeded
- **WHEN** forward references cannot resolve within the pass limit
- **THEN** `InvalidOperationException` is thrown with label resolution details
### Requirement: AsmBuilder reports clear errors
Unknown instructions, missing operands, and invalid register names produce `ArgumentException` with the line number and offending text.
#### Scenario: unknown instruction
- **WHEN** `AsmBuilder.Assemble("xyzw")` is called
- **THEN** `ArgumentException` is thrown mentioning line 1 and "xyzw"
#### Scenario: missing operand
- **WHEN** `AsmBuilder.Assemble("mov")` is called (no operands)
- **THEN** `ArgumentException` is thrown mentioning missing operand
### Requirement: AsmBuilder supported instruction set
The following x86 instructions are supported:
- **Data movement**: `mov`, `push`, `pop`, `pushad`, `popad`, `lea`
- **Arithmetic**: `add`, `sub`, `inc`, `dec`, `xor`, `and`, `or`, `cmp`, `test`
- **Control flow**: `jmp`, `je`, `jne`, `call`, `ret`, `nop`, `hlt`
#### Scenario: all instructions produce valid bytes
- **WHEN** each supported instruction is assembled individually
- **THEN** it produces the correct x86 machine code encoding
@@ -0,0 +1,44 @@
## 1. AsmBuilder — Core (TDD: tests first)
- [ ] 1.1 Create `BlackMagicTest/AsmBuilderTests.cs` with tests for single-instruction assembly: `nop``[0x90]`, `pushad``[0x60]`, `popad``[0x61]`, `ret``[0xC3]`, `hlt``[0xF4]`
- [ ] 1.2 Create `BlackMagic/Asm/AsmBuilder.cs` with `Assemble(string source)` entry point and instruction table. Implement `nop`, `pushad`, `popad`, `ret`, `hlt` to make step 1.1 tests pass
- [ ] 1.3 Add tests for `mov reg, imm32` (eax, ecx, edx, ebx, esp, ebp, esi, edi) — each register maps to `0xB8 + reg` encoding with 4-byte little-endian immediate
- [ ] 1.4 Implement `mov reg, imm32` in AsmBuilder to make step 1.3 tests pass
- [ ] 1.5 Add tests for `mov reg, reg` (register-to-register via ModRM byte 0xC0 + (src<<3 | dst))
- [ ] 1.6 Implement `mov reg, reg` to make step 1.5 tests pass
- [ ] 1.7 Add tests for `push reg` (`0x50 + reg`), `pop reg` (`0x58 + reg`)
- [ ] 1.8 Implement `push reg`, `pop reg` to make step 1.7 tests pass
- [ ] 1.9 Add tests for `add reg, imm8` (`0x83 /0` with sign-extended byte), `sub reg, imm8` (`0x83 /5`), `xor reg, reg` (`0x31 /r`), `and reg, imm8`, `or reg, imm8`, `cmp reg, imm8` (`0x83 /7`)
- [ ] 1.10 Implement `add`, `sub`, `xor`, `and`, `or`, `cmp` to make step 1.9 tests pass
- [ ] 1.11 Add tests for `inc reg` (`0x40 + reg`), `dec reg` (`0x48 + reg`), `test reg, reg` (`0x85 /r`)
- [ ] 1.12 Implement `inc`, `dec`, `test` to make step 1.11 tests pass
- [ ] 1.13 Add tests for `jmp @label`, `je @label`, `jne @label` with forward and backward references
- [ ] 1.14 Implement two-pass label resolution in AsmBuilder (pass 1: record label offsets, pass 2: patch jump targets). Make step 1.13 tests pass
- [ ] 1.15 Add tests for `call @label` (near call, E8 rel32) and `nop` multi-instruction sequences
- [ ] 1.16 Implement `call` to make step 1.15 tests pass
- [ ] 1.17 Add tests for error cases: unknown instruction → `ArgumentException` with line number, missing operand → `ArgumentException`, invalid register → `ArgumentException`
- [ ] 1.18 Implement error reporting to make step 1.17 tests pass
## 2. AsmBuilder — Pass Limit
- [ ] 2.1 Add tests for `SetPassLimit()`: default is 10, custom value is respected, exceeded limit throws `InvalidOperationException`
- [ ] 2.2 Implement `SetPassLimit(int limit)` and pass-limit enforcement in AsmBuilder to make step 2.1 tests pass
## 3. InjectAndExecuteEx — Non-blocking Execution
- [ ] 3.1 Add tests for `BlackMagic.InjectAndExecuteEx(IntPtr, IntPtr)`: returns `null` when no process open, returns valid handle when process open (mock or integration)
- [ ] 3.2 Implement `InjectAndExecuteEx(IntPtr startAddress, IntPtr parameter)` in `BMThread.cs` to make step 3.1 tests pass
- [ ] 3.3 Add test for single-parameter overload: `InjectAndExecuteEx(IntPtr)` passes `IntPtr.Zero`
- [ ] 3.4 Implement `InjectAndExecuteEx(IntPtr startAddress)` overload in `BMThread.cs`
## 4. Convenience Overloads (text → inject → execute)
- [ ] 4.1 Add tests for `InjectAndExecute(string asm)`: assembles text, allocates remote memory, writes bytes, executes, returns exit code. Test assembly failure → `ArgumentException`
- [ ] 4.2 Implement `InjectAndExecute(string asm)` in `BMInject.cs` to make step 4.1 tests pass
- [ ] 4.3 Add tests for `InjectAndExecuteEx(string asm)`: assembles text, allocates remote memory, writes bytes, returns thread handle. Test assembly failure → `ArgumentException`
- [ ] 4.4 Implement `InjectAndExecuteEx(string asm)` in `BMInject.cs` to make step 4.3 tests pass
## 5. Build Verification
- [ ] 5.1 Run full test suite: `"C:\Program Files\dotnet\dotnet.exe" test BlackMagicTest/BlackMagicTest.csproj` — all tests pass
- [ ] 5.2 Run full solution build: `"C:\Program Files\dotnet\dotnet.exe" build BlackMagic.slnx` — zero errors
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-21
@@ -0,0 +1,126 @@
## Context
This repo contains four C# process-manipulation libraries studied in `docs/memory-library-comparison.md`:
- **BlackMagic-old** (`reference/Blackmagic-old/`) — .NET FW 4.0, x86, FASM as a public `Asm` property.
- **MemorySharp** (`reference/MemorySharp/`) — .NET FW, x86, FASM behind `Execute<T>`; rich high-level API (PEB/TEB, window, input, calling conventions) but unmaintained since 2016.
- **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.
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.
## Goals / Non-Goals
**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.
- Dual memory access: out-of-process (RPM/WPM) and in-process (direct deref) 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.
- Port DLL injection (CreateThread + thread-hijack, x86/x64) and pattern scanning + cache from current BlackMagic.
- Provide MemorySharp-grade ergonomics: `RemotePointer` indexer, `Module["fn"].Execute(...)`, PEB/TEB, window mutation, input simulation, async wrappers, helpers.
- Test-first for all pure logic (assembler encodings, marshal cache, pattern matching, stub building, pump queue semantics).
**Non-Goals:**
- 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.
## Decisions
### D1: Layered architecture with an abstract `MemoryBase` (from GreyMagic)
`MemoryBase` defines abstract `ReadBytes`/`WriteBytes`/`Read<T>`/`Write<T>`, relative/absolute addressing, and hosts the `PatchManager`. Two concrete readers:
- `ExternalReader : MemoryBase``ReadProcessMemory`/`WriteProcessMemory` over a `SafeMemoryHandle`. Owns allocation, injection, and the remote-thread + main-thread executors.
- `InProcessReader : MemoryBase``unsafe` direct pointer deref; owns the `DetourManager` and `InProcessInvoker`.
**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 is the fast/crash-free path once injected.
**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.
### D2: Three-tier execution, crash-safe by default (the headline)
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 |
| 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`.
**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.
**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.
### D3: `IAssembler` seam replacing FASM
```
IAssembler { byte[] Assemble(string asm, ulong origin = 0); }
```
Two backends:
- `StubAssembler` (default) — hand-emits the fixed calling-convention trampolines (cdecl/stdcall/thiscall/fastcall: push args, call, cleanup, ret) and the injection stubs, using an `EmitU8`/`EmitU32`/`EmitU64` byte emitter (the pattern already in `SInject.cs`). No parsing, deterministic, x64-capable, zero dependency.
- `IcedAssembler` (optional) — wraps Iced for arbitrary user-supplied mnemonics when a caller genuinely needs runtime text assembly.
**Why**: The comparison established that the only thing FASM did was runtime text→bytecode, needed solely for stubs and (rarely) arbitrary asm. Stubs are a small fixed set best hand-emitted; arbitrary asm is better served by a modern, managed, x64, MIT-licensed assembler (Iced) than by a native C++/CLI FASM DLL. Retiring FASM removes the x86-only mixed-mode constraint.
**Alternatives considered**: keep FASM/Fasm.NET — rejected: native dependency, x86-only, unmaintained. Hand-emit everything including arbitrary asm — rejected: reimplementing an assembler is out of scope; Iced already exists.
### D4: `MarshalCache<T>` for typed IO (from GreyMagic)
A `static class MarshalCache<T>` computes and caches `Marshal.SizeOf`, `TypeCode`, `TypeRequiresMarshal`, and `IsIntPtr` once per type. `Read<T>`/`Write<T>` branch on the cached flags: blittable types round-trip through `Span`/`MemoryMarshal`; marshal-required types use `Marshal.PtrToStructure`.
**Why**: GreyMagic's signature perf win — avoids per-call reflection. Current BlackMagic's `where unmanaged` constraint is faster still for blittable types but cannot express marshalled structs; the cache gives both.
### D5: Reversible hooking and patching with auto-restore (from GreyMagic)
`DetourManager`/`Detour` (inline `E9` jmp over a prologue, `CallOriginal`, `Apply`/`Remove`) and `PatchManager`/`Patch` (named byte patch, `Apply`/`Remove`/`IsApplied`). Both register into a manager that restores all live modifications on `MemoryBase.Dispose`. Detours are in-process only (they require executing the hook delegate in the target); patches work in both modes.
**Safety addition**: before splicing a detour, `StubAssembler`/`IcedAssembler` disassembles the target prologue to confirm the overwrite lands on instruction boundaries, avoiding the mid-instruction-splice crash class. When only `StubAssembler` is present, a minimal length-disassembler covers the common prologue shapes; full validation requires the Iced backend.
**Why**: MemorySharp promised Hook/Patch "coming soon" and never shipped them; GreyMagic shipped both and they are the cleanest available. Auto-restore prevents leaving the target corrupted after a crash of the host.
### D6: High-level surface as opt-in factories (from MemorySharp)
`RemotePointer` (indexer `sharp[addr]`), `RemoteModule`/`RemoteFunction` (`sharp["user32"]["MessageBoxA"].Execute(...)`), `ManagedPeb`/`ManagedTeb`, `WindowFactory` (move/resize/title/flash/activate), `Keyboard`/`Mouse` (PostMessage + SendInput), and async wrappers over the executors. These sit above `MemoryBase` and the execution tiers; none is required for core memory work.
**Why**: This is the ergonomic layer that made MemorySharp pleasant. It is pure P/Invoke over the core — no assembler, no FASM.
### D7: New projects, additive, test-first
`WhiteMagic/` (library, `net8.0-windows`, `AllowUnsafeBlocks`) and `WhiteMagicTest/` (xUnit). No solution file exists in the repo root today; task 1.3 creates a fresh `WhiteMagic.sln` (or builds the csproj directly). BlackMagic lives in a nested subdir with its own git and is not part of this build. Pure-logic components (assembler encodings, marshal cache, pattern matcher, stub builders, pump queue) get tests before implementation, mirroring the existing `inject-and-assemble` change's TDD discipline. Live-process behavior (RPM/WPM, injection, detours) is validated in integration tests gated on an available target.
## Risks / Trade-offs
- **Scope is large** → Deliver in vertical slices (see Migration Plan). The minimum crash-safe slice is `MemoryBase` + `ExternalReader` + `DetourManager` + `MainThreadPump`; everything else layers on without reworking it.
- **In-process tier needs a managed loader not in scope** → Ship `InProcessReader`'s API and delegate-call path now; mark the actual CLR-host injection as a follow-up. External + pump deliver crash-safety without it.
- **Detour prologue splicing can crash if misaligned** → Validate instruction boundaries before writing (D5); require Iced backend for full validation; keep `Remove`/auto-restore so a bad detour is recoverable.
- **Iced adds a NuGet dependency** → Isolated behind `IAssembler`; default `StubAssembler` keeps the library dependency-free. Only callers needing arbitrary asm opt in.
- **`MainThreadPump` adds per-frame overhead and a hook on a hot function** → Keep the drained-per-frame work bounded; the hook is a thin queue check when idle. Document that a wedged work item stalls the frame.
- **x64 detours need larger/absolute jumps (14-byte `push/ret` or RIP-relative)** → `StubAssembler` emits the correct form per bitness; covered by encoding tests.
- **Blittable vs marshalled ambiguity** in `MarshalCache` → Explicit flags and tests per type category; document that `[StructLayout]` is required for non-blittable remote structs.
## Migration Plan
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.
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.
6. **Optional Iced backend**`IcedAssembler`, arbitrary-asm inject, full prologue validation.
**Rollback**: WhiteMagic is a separate assembly; removing its project reference reverts consumers with no effect on existing libraries.
## 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.
- **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>`.
@@ -0,0 +1,39 @@
## 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.
## What Changes
- Introduce a new library, **WhiteMagic**, as a fresh .NET 8 project (`WhiteMagic/`) with an xUnit test project (`WhiteMagicTest/`). Additive — existing BlackMagic is untouched.
- **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.
- `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.**
- **DLL injection**: `CreateRemoteThread` + thread-hijack strategies, x86 and x64 stubs (ported from current BlackMagic).
- **Discovery & allocation**: pattern scanning with cache, PE-header parsing, and named-chunk `AllocatedMemory`.
- **High-level ergonomics**: `RemotePointer` indexer, `Module["fn"].Execute(...)`, `ManagedPeb`/`ManagedTeb`, window mutation, keyboard/mouse simulation, async execution wrappers, and helper utilities.
## Capabilities
### New Capabilities
- `memory-access`: Dual external/in-process readers over an abstract `MemoryBase`, with `MarshalCache`-backed typed, array, and string read/write and relative/absolute addressing.
- `memory-discovery`: Pattern/signature scanning (with cache), PE-header parsing, and named-chunk remote allocation.
- `managed-assembler`: `IAssembler` seam producing machine code with no native dependency — hand-emitted convention stubs plus an optional Iced backend for arbitrary assembly.
- `remote-execution`: Three-tier execution (remote-thread, crash-safe main-thread pump, in-process delegate) with calling-convention-aware `Execute<T>` and parameter marshalling.
- `function-hooking`: Reversible inline detours (`CallOriginal`) and named byte patches with lifecycle management and auto-restore.
- `dll-injection`: DLL injection via `CreateRemoteThread` and thread-hijack redirection for x86 and x64 targets.
- `high-level-api`: Ergonomic surface — `RemotePointer` indexer, module/function access, PEB/TEB, window and input simulation, async wrappers, and helpers.
### Modified Capabilities
<!-- None — WhiteMagic is a new library; no existing WhiteMagic specs exist in openspec/specs/. -->
## Impact
- **New code**: `WhiteMagic/` library, `WhiteMagicTest/` xUnit project, both added to the solution.
- **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.
@@ -0,0 +1,53 @@
## ADDED Requirements
### Requirement: DLL injection via remote thread
WhiteMagic SHALL inject a DLL into an open target process by creating a remote thread on `LoadLibrary`, returning the base address of the injected module on success and reporting failure without throwing for expected failure conditions.
#### Scenario: successful injection
- **WHEN** a valid DLL path is injected into an open process of matching bitness
- **THEN** the returned base address MUST be non-zero and the module MUST be loaded in the target
#### Scenario: bitness mismatch rejected
- **WHEN** the target process bitness differs from the caller
- **THEN** injection MUST fail with a clear error rather than corrupt the target
#### Scenario: missing file
- **WHEN** the DLL path does not exist
- **THEN** injection MUST report an argument error
### Requirement: DLL injection via thread hijack
WhiteMagic SHALL inject a DLL by hijacking an existing thread — saving its context, redirecting execution through a `LoadLibrary` stub, and restoring the original context — returning the injected module base address.
#### Scenario: hijack loads the module
- **WHEN** a valid DLL is injected by hijacking a running thread
- **THEN** the module MUST be loaded and the hijacked thread's original context MUST be restored
#### Scenario: exit code reports load result
- **WHEN** the redirect stub completes
- **THEN** the stub MUST record `LoadLibrary`'s result so the caller can detect load success or failure
### Requirement: x86 and x64 stubs
Injection stubs SHALL be emitted correctly for both x86 and x64 targets, including proper x64 addressing.
#### Scenario: x86 stub
- **WHEN** injecting into a 32-bit target
- **THEN** a 32-bit redirect stub MUST be emitted
#### Scenario: x64 stub
- **WHEN** injecting into a 64-bit target
- **THEN** a 64-bit redirect stub with correct absolute/RIP-relative addressing MUST be emitted
### Requirement: Raw code injection
WhiteMagic SHALL inject raw machine-code bytes into an open process, either at a caller-supplied address or into freshly allocated remote memory whose address is returned.
#### Scenario: inject at address
- **WHEN** raw bytes are injected at a given address
- **THEN** memory at that address MUST equal the injected bytes
#### Scenario: inject into fresh allocation
- **WHEN** raw bytes are injected without an address
- **THEN** remote memory MUST be allocated, the bytes written, and the allocation address returned
@@ -0,0 +1,57 @@
## ADDED Requirements
### Requirement: Reversible inline detours
WhiteMagic SHALL provide a `DetourManager` that creates named inline detours redirecting a target function to a managed hook, supporting `Apply`, `Remove`, and calling the original function. Detours operate in-process.
#### Scenario: apply redirects the target
- **WHEN** a detour from a target function to a hook delegate is applied
- **THEN** calling the target MUST invoke the hook delegate
#### Scenario: call original
- **WHEN** the hook invokes `CallOriginal(args)`
- **THEN** the original target behavior MUST execute with those arguments and its result returned
#### Scenario: remove restores original bytes
- **WHEN** an applied detour is removed
- **THEN** the target's original prologue bytes MUST be restored and calling the target MUST no longer invoke the hook
#### Scenario: named lookup
- **WHEN** a detour is created with a name
- **THEN** it MUST be retrievable from the manager by that name
### Requirement: Instruction-boundary validation before splicing
Before overwriting a target prologue, the detour SHALL verify the overwrite covers whole instructions so that no instruction is split.
#### Scenario: aligned splice permitted
- **WHEN** the bytes required for the jump cover a whole number of prologue instructions
- **THEN** the detour MUST apply
#### Scenario: misaligned splice rejected
- **WHEN** the required overwrite would end in the middle of an instruction and boundary information is available
- **THEN** the detour MUST refuse to apply rather than corrupt the target
### Requirement: Named reversible byte patches
WhiteMagic SHALL provide a `PatchManager` that creates named byte patches with `Apply`, `Remove`, and `IsApplied`, usable in both external and in-process modes.
#### Scenario: apply writes patch bytes
- **WHEN** a patch is applied at an address
- **THEN** memory at that address MUST equal the patch bytes
#### Scenario: remove restores original
- **WHEN** an applied patch is removed
- **THEN** memory at that address MUST equal the original bytes captured at creation
#### Scenario: is-applied reflects state
- **WHEN** `IsApplied` is queried
- **THEN** it MUST return true only when the current bytes equal the patch bytes
### Requirement: Auto-restore on dispose
All live detours and patches SHALL be reverted when their owning `MemoryBase` is disposed.
#### Scenario: dispose reverts modifications
- **WHEN** a `MemoryBase` with active detours and patches is disposed
- **THEN** every modified region MUST be restored to its pre-modification bytes
@@ -0,0 +1,69 @@
## ADDED Requirements
### Requirement: Remote pointer indexer
WhiteMagic SHALL expose a `RemotePointer` obtained by indexing the memory facade with an address, offering read/write/execute operations relative to that base address.
#### Scenario: read via indexer
- **WHEN** `sharp[addr].Read<int>(offset)` is called
- **THEN** it MUST read an int at `addr + offset`
#### Scenario: write via indexer
- **WHEN** `sharp[addr].WriteString("text")` is called
- **THEN** the string MUST be written starting at `addr`
### Requirement: Module and function access
WhiteMagic SHALL expose modules and their exported functions by name, allowing a resolved function to be executed with a calling convention and arguments.
#### Scenario: resolve function by name
- **WHEN** `sharp["user32"]["MessageBoxA"]` is resolved
- **THEN** it MUST return a function bound to the export address of `MessageBoxA` in `user32`
#### Scenario: execute resolved function
- **WHEN** a resolved function is executed with a calling convention and arguments
- **THEN** it MUST invoke the target through the chosen execution strategy with those arguments
### Requirement: PEB and TEB access
WhiteMagic SHALL expose managed reads of the target's Process Environment Block and a thread's Thread Environment Block.
#### Scenario: read PEB field
- **WHEN** a PEB field (e.g. being-debugged flag) is read
- **THEN** it MUST reflect the target's actual PEB value
#### Scenario: read TEB field
- **WHEN** a TEB field is read for a given thread
- **THEN** it MUST reflect that thread's actual TEB value
### Requirement: Window mutation
WhiteMagic SHALL enumerate and mutate target windows — position, size, title, activation, and flashing.
#### Scenario: move and resize
- **WHEN** a window's X, Y, width, and height are set
- **THEN** the window MUST move and resize to those values
#### Scenario: query by class name
- **WHEN** windows are queried by class name
- **THEN** matching windows MUST be returned
### Requirement: Keyboard and mouse simulation
WhiteMagic SHALL simulate keyboard and mouse input to a target window, including input delivered without activating the window where the mechanism allows.
#### Scenario: write text to a window
- **WHEN** text is written to a target window's keyboard interface
- **THEN** the window MUST receive the corresponding key input
#### Scenario: mouse click
- **WHEN** a click at a coordinate is issued to a window's mouse interface
- **THEN** the window MUST receive the corresponding mouse input
### Requirement: Asynchronous execution wrappers
WhiteMagic SHALL provide `Task`-based asynchronous wrappers over its execution strategies.
#### Scenario: async execute returns a task
- **WHEN** an async execute is invoked
- **THEN** it MUST return a `Task<T>` that completes with the execution result
@@ -0,0 +1,49 @@
## ADDED Requirements
### Requirement: IAssembler abstraction with no native dependency
WhiteMagic SHALL define an `IAssembler` seam that produces machine code, with a default backend that has no native or third-party dependency. FASM MUST NOT be referenced by the default configuration.
#### Scenario: default backend is dependency-free
- **WHEN** WhiteMagic is built in its default configuration
- **THEN** no reference to FASM or `ManagedFasm` MUST be present in the output
#### Scenario: backend is replaceable
- **WHEN** an alternate `IAssembler` implementation is supplied
- **THEN** execution and injection MUST use it without other code changes
### Requirement: Hand-emitted calling-convention stubs
The default `StubAssembler` SHALL emit call trampolines for the cdecl, stdcall, thiscall, and fastcall conventions — pushing/placing arguments, calling the target, cleaning the stack per convention, and returning — for both x86 and x64 targets.
#### Scenario: cdecl stub encoding
- **WHEN** a cdecl call stub for a function with N 4-byte arguments is emitted (x86)
- **THEN** the bytes MUST push the arguments in reverse order, `call` the target, `add esp, N*4`, and `ret`
#### Scenario: stdcall omits caller cleanup
- **WHEN** a stdcall stub is emitted
- **THEN** it MUST NOT emit a caller-side stack cleanup (the callee cleans)
#### Scenario: x64 uses register argument order
- **WHEN** an x64 call stub is emitted
- **THEN** the first integer arguments MUST be placed in the platform argument registers before the call
### Requirement: Byte emitter primitives
`StubAssembler` SHALL provide little-endian emit primitives (`EmitU8`, `EmitU32`, `EmitU64`) used to hand-assemble stubs deterministically.
#### Scenario: little-endian 32-bit emit
- **WHEN** `EmitU32(0x11223344)` is called
- **THEN** the appended bytes MUST be `[0x44, 0x33, 0x22, 0x11]`
### Requirement: Optional Iced backend for arbitrary assembly
WhiteMagic SHALL provide an optional `IcedAssembler` backend that assembles arbitrary x86/x64 mnemonic text to machine code for callers who require runtime text assembly.
#### Scenario: arbitrary mnemonics assembled
- **WHEN** the Iced backend assembles `"push 0\nadd esp, 4\nret"` at a given origin
- **THEN** it MUST return the corresponding machine code bytes
#### Scenario: origin-relative encoding
- **WHEN** assembly containing a relative jump is assembled at a specified origin address
- **THEN** the encoded relative offsets MUST be correct for that origin
@@ -0,0 +1,57 @@
## ADDED Requirements
### Requirement: Abstract memory base with two readers
WhiteMagic SHALL expose an abstract `MemoryBase` type defining `ReadBytes`, `WriteBytes`, generic `Read<T>`/`Write<T>`, array read/write, and string read/write, with two concrete implementations: `ExternalReader` (out-of-process via ReadProcessMemory/WriteProcessMemory) and `InProcessReader` (in-process via direct pointer dereference).
#### Scenario: external read round-trip
- **WHEN** an `ExternalReader` opens a target process and writes a value with `Write<int>(addr, 0x1234)` then reads it back with `Read<int>(addr)`
- **THEN** the returned value MUST equal `0x1234`
#### Scenario: in-process read of own memory
- **WHEN** an `InProcessReader` reads a known address in its own process
- **THEN** the value MUST match a direct managed read of the same address
#### Scenario: shared API surface
- **WHEN** code is written against the `MemoryBase` abstract type
- **THEN** it MUST operate unchanged against both `ExternalReader` and `InProcessReader`
### Requirement: Typed read/write via marshal cache
`MemoryBase` SHALL support generic `Read<T>`/`Write<T>` for blittable and marshalled struct types, using a per-type `MarshalCache<T>` that caches size, type code, and marshalling requirements to avoid per-call reflection.
#### Scenario: blittable struct round-trip
- **WHEN** a blittable `[StructLayout(LayoutKind.Sequential)]` struct is written and read back
- **THEN** all fields MUST be preserved exactly
#### Scenario: marshal cache computed once
- **WHEN** `Read<T>` is invoked repeatedly for the same type `T`
- **THEN** `Marshal.SizeOf` and type inspection for `T` MUST be computed at most once and reused
#### Scenario: array read
- **WHEN** `Read<T>(addr, count)` is called
- **THEN** it MUST return an array of exactly `count` elements read contiguously from `addr`
### Requirement: String read and write with encoding
`MemoryBase` SHALL read and write strings with a caller-specified `Encoding` and a maximum length, terminating reads at a null terminator or the maximum length.
#### Scenario: ASCII write then read
- **WHEN** `WriteString(addr, "hello", Encoding.ASCII)` is called then `ReadString(addr, Encoding.ASCII)`
- **THEN** the result MUST equal `"hello"`
#### Scenario: read stops at null terminator
- **WHEN** a null-terminated string shorter than `maxLength` is read
- **THEN** the returned string MUST exclude the terminator and everything after it
### Requirement: Relative and absolute addressing
`MemoryBase` SHALL convert between addresses relative to the module image base and absolute addresses via `GetAbsolute` and `GetRelative`, and accept an `isRelative` flag on read/write operations.
#### Scenario: relative resolves against image base
- **WHEN** `GetAbsolute(relative)` is called with the process image base known
- **THEN** the result MUST equal `imageBase + relative`
#### Scenario: read with isRelative
- **WHEN** `Read<int>(offset, isRelative: true)` is called
- **THEN** the read MUST occur at `GetAbsolute(offset)`
@@ -0,0 +1,57 @@
## ADDED Requirements
### Requirement: Pattern scanning with mask
WhiteMagic SHALL scan process memory for a byte signature with a wildcard mask, returning the address of the first match or `IntPtr.Zero` when no match is found. Scans SHALL be available over an explicit range, a single module, and all modules.
#### Scenario: pattern found
- **WHEN** a known byte sequence is scanned for with a matching mask over a range containing it
- **THEN** the returned address MUST point at the first occurrence
#### Scenario: wildcard mask
- **WHEN** the mask marks positions as wildcards (e.g. `"xx?x"`)
- **THEN** those byte positions MUST be ignored during matching
#### Scenario: pattern not found
- **WHEN** a pattern absent from the range is scanned for
- **THEN** the result MUST be `IntPtr.Zero`
### Requirement: Pattern scan cache
The scanner SHALL cache resolved pattern results keyed by pattern and mask, returning the cached address on repeat lookups, and SHALL expose an operation to clear the cache.
#### Scenario: repeat lookup served from cache
- **WHEN** the same pattern and mask are scanned twice without clearing the cache
- **THEN** the second lookup MUST return the same address without rescanning memory
#### Scenario: cache cleared
- **WHEN** the cache is cleared
- **THEN** the next lookup MUST rescan memory
### Requirement: PE header parsing
WhiteMagic SHALL parse the PE headers of a module to expose its sections and entry point without executing the module.
#### Scenario: sections enumerated
- **WHEN** a valid PE module is parsed
- **THEN** its section names, virtual addresses, and sizes MUST be enumerable
#### Scenario: entry point located
- **WHEN** a valid PE module is parsed
- **THEN** the parsed entry-point RVA MUST match the module's header
### Requirement: Named remote allocation
WhiteMagic SHALL allocate a chunk of remote memory subdivided into named regions, allowing typed read/write and address lookup by name, and freeing the whole chunk on dispose.
#### Scenario: write and read by name
- **WHEN** a named region is allocated and `Write<int>("count", 5)` then `Read<int>("count")` is called
- **THEN** the result MUST equal `5`
#### Scenario: address by name
- **WHEN** a region named `"buffer"` is allocated
- **THEN** requesting its address MUST return `chunkBase + regionOffset`
#### Scenario: freed on dispose
- **WHEN** the allocation is disposed
- **THEN** the underlying remote memory MUST be released
@@ -0,0 +1,57 @@
## ADDED Requirements
### Requirement: Three-tier execution model
WhiteMagic SHALL provide three execution strategies selected by payload safety: `RemoteThreadExecutor` (via `CreateRemoteThread`), `MainThreadPump` (work marshalled onto the target's own thread), and `InProcessInvoker` (direct native-delegate calls when injected in-process).
#### Scenario: strategies are distinct and selectable
- **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
- **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`
### Requirement: Remote-thread execution for thread-agnostic payloads
`RemoteThreadExecutor` SHALL create a remote thread at a target address using a calling-convention-aware stub, wait for completion, and return the typed exit value. Its documentation MUST state that it is safe only for thread-agnostic payloads.
#### Scenario: execute with parameters and convention
- **WHEN** `Execute<int>(addr, CallingConvention.Cdecl, arg1, arg2)` is called on a safe self-contained function
- **THEN** the target MUST be called with the arguments laid out per cdecl and the typed return value returned
#### Scenario: parameters marshalled and freed
- **WHEN** a `string` or struct parameter is passed to `Execute`
- **THEN** it MUST be allocated in the remote process, passed by pointer, and freed after the call completes
#### Scenario: no process open
- **WHEN** `Execute` is called with no process open
- **THEN** it MUST fail deterministically rather than crash
### Requirement: Crash-safe main-thread pump
`MainThreadPump` SHALL install a hook on a per-frame function in the target and, each time that function runs, drain a thread-safe queue of work items, executing each on the target's own thread and returning its result or exception to the requesting caller.
#### Scenario: work runs on the hooked thread
- **WHEN** a work item is queued and the hooked per-frame function next executes
- **THEN** the work item MUST run in the context of the thread that calls the per-frame function
#### Scenario: result returned to caller
- **WHEN** a caller queues a function returning a value and awaits its completion
- **THEN** the caller MUST receive the returned value
#### Scenario: exception propagated, pump survives
- **WHEN** a queued work item throws
- **THEN** the exception MUST be surfaced to the requesting caller AND subsequent queued items MUST still be processed
#### Scenario: uninstall restores the frame function
- **WHEN** the pump is disposed
- **THEN** the hooked per-frame function MUST be restored to its original bytes
### Requirement: In-process delegate invocation
`InProcessInvoker` SHALL convert a function address to a typed managed delegate and call it directly, without creating a thread or crossing a thread boundary.
#### Scenario: call as delegate
- **WHEN** `CreateFunction<TDelegate>(addr)` is called in-process and the returned delegate is invoked
- **THEN** the native function at `addr` MUST be called directly on the current thread with the delegate's marshalled arguments
@@ -0,0 +1,86 @@
## 1. Project Setup
- [ ] 1.1 Create `WhiteMagic/WhiteMagic.csproj` targeting `net8.0-windows`, `AllowUnsafeBlocks=true`, nullable enabled
- [ ] 1.2 Create `WhiteMagicTest/WhiteMagicTest.csproj` (xUnit, `net8.0-windows`) referencing `WhiteMagic`
- [ ] 1.3 No solution file exists in the repo root. Create one (`dotnet new sln -n WhiteMagic`) and add both projects, OR skip the solution and build csproj directly (decide before 1.5). Do NOT reference `BlackMagic.slnx` — it does not exist.
- [ ] 1.4 Add `WhiteMagic/Native/` P/Invoke surface (`LibraryImport`): OpenProcess, Read/WriteProcessMemory, VirtualAllocEx/FreeEx/ProtectEx, CreateRemoteThread, Wow64Get/SetThreadContext, Get/SetThreadContext, LoadLibrary, GetProcAddress; add `SafeMemoryHandle`
- [ ] 1.5 Verify empty projects build: `dotnet build WhiteMagic.sln` (or `dotnet build WhiteMagic/WhiteMagic.csproj` if no solution) — zero errors
## 2. Core Memory Access (spec: memory-access)
- [ ] 2.1 Add tests for `MarshalCache<T>`: blittable size, marshal-required flag, IsIntPtr, computed-once behavior
- [ ] 2.2 Implement `WhiteMagic/MarshalCache.cs` to pass 2.1
- [ ] 2.3 Add tests for `MemoryBase` abstract contract + `ExternalReader` round-trip (`Read<T>`/`Write<T>`, arrays) using the current process as target
- [ ] 2.4 Implement `WhiteMagic/MemoryBase.cs` (abstract) and `WhiteMagic/ExternalReader.cs` to pass 2.3
- [ ] 2.5 Add tests for string read/write with encoding, null-terminator stop, and max length
- [ ] 2.6 Implement `ReadString`/`WriteString` on `MemoryBase` to pass 2.5
- [ ] 2.7 Add tests for relative/absolute addressing (`GetAbsolute`/`GetRelative`, `isRelative` flag)
- [ ] 2.8 Implement addressing helpers to pass 2.7
- [ ] 2.9 Add tests + `unsafe` implementation for `InProcessReader` (direct deref against own process); verify shared `MemoryBase` API works for both readers
## 3. Managed Assembler (spec: managed-assembler)
- [ ] 3.1 Add tests for `EmitU8`/`EmitU32`/`EmitU64` little-endian primitives
- [ ] 3.2 Implement `WhiteMagic/Assembly/StubAssembler.cs` emitters + `IAssembler` interface to pass 3.1
- [ ] 3.3 Add tests for x86 cdecl stub encoding (reverse push, call, `add esp, N*4`, ret) with known byte expectations
- [ ] 3.4 Implement x86 cdecl stub to pass 3.3
- [ ] 3.5 Add tests for stdcall (no caller cleanup), thiscall (ecx = this), fastcall (ecx/edx) x86 stubs
- [ ] 3.6 Implement x86 stdcall/thiscall/fastcall stubs to pass 3.5
- [ ] 3.7 Add tests for x64 stub argument-register placement and call
- [ ] 3.8 Implement x64 stub to pass 3.7
- [ ] 3.9 Confirm no FASM/`ManagedFasm` reference exists in `WhiteMagic` output (assert via a test that scans loaded references)
## 4. Crash-Safe Execution Slice (spec: remote-execution, function-hooking)
- [ ] 4.1 Add tests for `PatchManager`/`Patch`: apply writes bytes, remove restores original, `IsApplied` reflects state
- [ ] 4.2 Implement `WhiteMagic/Hooking/PatchManager.cs` + `Patch.cs` to pass 4.1
- [ ] 4.3 Add tests for `DetourManager`/`Detour` in-process: apply redirects, `CallOriginal`, remove restores, named lookup
- [ ] 4.4 Implement `WhiteMagic/Hooking/DetourManager.cs` + `Detour.cs` (inline jmp, x86/x64 form) to pass 4.3
- [ ] 4.5 Add tests for instruction-boundary validation (aligned splice permitted, misaligned rejected when boundary info available)
- [ ] 4.6 Implement minimal prologue length-decoder in `Detour.Apply` to pass 4.5. Default `StubAssembler` covers ONLY the common x86/x64 prologue shapes — enumerate the covered opcodes in code + XML doc (e.g. `push reg` 0x50-0x57, `mov edi,edi` 8B FF, `push ebp`/`mov ebp,esp` 55 8B EC, `sub esp,imm` 83 EC / 81 EC, REX-prefixed forms). On any opcode outside the set, refuse the splice (do not guess). Full arbitrary-prologue validation is gated on the optional Iced backend (task 8.3) — document that slices 2-5 ship partial boundary safety.
- [ ] 4.7 Add tests for auto-restore: disposing a `MemoryBase` reverts all active patches and detours
- [ ] 4.8 Wire manager registration + `MemoryBase.Dispose` restore to pass 4.7
- [ ] 4.9 Add tests for `MainThreadPump` queue semantics: item runs on hooked thread, result returned, throwing item surfaces exception and pump survives, dispose uninstalls hook (use a self-hosted frame-loop harness in-process)
- [ ] 4.10 Implement `WhiteMagic/Execution/MainThreadPump.cs` (frame-function detour + thread-safe work queue + completion handles) to pass 4.9
- [ ] 4.11 Add tests for `RemoteThreadExecutor.Execute<T>` (convention stub + wait + typed exit; no-process failure is deterministic)
- [ ] 4.12 Implement `WhiteMagic/Execution/RemoteThreadExecutor.cs` and parameter marshalling (string/struct → remote alloc → free) to pass 4.11
## 5. Injection & Discovery (spec: dll-injection, memory-discovery)
- [ ] 5.1 Add tests for pattern scanning: found (range/module/all-modules), wildcard mask, not-found returns Zero
- [ ] 5.2 Implement `WhiteMagic/Discovery/PatternScanner.cs` to pass 5.1
- [ ] 5.3 Add tests + implement scan result cache (repeat served from cache, clear rescans)
- [ ] 5.4 Add tests + implement `WhiteMagic/Discovery/PeHeaderParser.cs` (sections, entry point)
- [ ] 5.5 Add tests + implement `WhiteMagic/Memory/AllocatedMemory.cs` (named regions, typed read/write by name, address by name, free on dispose)
- [ ] 5.6 Add tests + implement raw code injection (`InjectCode` at address and into fresh allocation)
- [ ] 5.7 Add tests + implement DLL injection via remote thread (LoadLibrary), including bitness-mismatch and missing-file failures
- [ ] 5.8 Add tests + implement DLL injection via thread-hijack (save/redirect/restore context) with x86 and x64 stubs
## 6. In-Process Tier (spec: remote-execution)
- [ ] 6.1 Add tests for `InProcessInvoker.CreateFunction<TDelegate>` calling a known in-process function directly
- [ ] 6.2 Implement `WhiteMagic/Execution/InProcessInvoker.cs` (`Marshal.GetDelegateForFunctionPointer`) + vtable-entry helper to pass 6.1
- [ ] 6.3 Document that the CLR-host managed loader (injecting `InProcessReader` into a foreign process) is a separate follow-up change
## 7. High-Level Ergonomics (spec: high-level-api)
- [ ] 7.1 Add tests + implement `RemotePointer` indexer (`sharp[addr].Read/Write/Execute` relative to base)
- [ ] 7.2 Add tests + implement `RemoteModule`/`RemoteFunction` (`sharp["mod"]["fn"]`) resolving export addresses and executing via a chosen strategy
- [ ] 7.3 Add tests + implement `ManagedPeb`/`ManagedTeb` field reads
- [ ] 7.4 Add tests + implement `WindowFactory`/`RemoteWindow` (enumerate, move/resize/title/activate/flash, query by class)
- [ ] 7.5 Add tests + implement keyboard/mouse simulation (PostMessage + SendInput) to a target window
- [ ] 7.6 Add tests + implement `Task`-based async execution wrappers over the executors and pump
- [ ] 7.7 Add minimal facade (`WhiteMagic` entry type) exposing `Open`, readers, executors, managers, and the indexer
## 8. Optional Iced Backend (spec: managed-assembler)
- [ ] 8.1 Add `Iced` package reference behind an `IcedAssembler : IAssembler` in a way that keeps the default `StubAssembler` dependency-free
- [ ] 8.2 Add tests + implement `IcedAssembler.Assemble(text, origin)` for arbitrary mnemonics and origin-relative encoding
- [ ] 8.3 Add tests + wire full prologue instruction-boundary validation (D5) using the Iced disassembler when present
## 9. Verification
- [ ] 9.1 Run full test suite: `dotnet test WhiteMagicTest/WhiteMagicTest.csproj` — all pass
- [ ] 9.2 Run full build (`dotnet build WhiteMagic.sln` or the WhiteMagic csproj, per 1.3) — zero errors, zero new warnings in `WhiteMagic`
- [ ] 9.3 Confirm existing BlackMagic/its tests are unchanged and still green
- [ ] 9.4 Update `docs/memory-library-comparison.md` "WhiteMagic — synthesis" section with any deviations discovered during implementation
+1
View File
@@ -0,0 +1 @@
schema: spec-driven