Files
whitemagic/openspec/changes/whitemagic-foundation/tasks.md
T
kbeandClaude Opus 4.8 30d5a9a5ec Add optional Iced backend: arbitrary text assembly and full prologue validation
Section 8 of the whitemagic-foundation change.

- 8.1: reference Iced 1.21.0 behind IcedAssembler:IAssembler. The default
  StubAssembler path never touches Iced; only constructing IcedAssembler pulls
  it into a behavioral path.
- 8.2: IcedAssembler.Assemble bridges Intel-syntax text onto Iced's fluent
  Assembler by reflection (Iced ships no text parser). Registers, immediates and
  labels are supported with origin-relative encoding; memory operands throw
  NotSupportedException.
- 8.3: IcedAssembler.GetPrologueLength decodes arbitrary instructions via Iced's
  Decoder. DetourManager.PrologueLengthResolver (new delegate) defaults to the
  built-in PrologueDecoder and is swappable to the Iced resolver, threaded into
  each Detour. This lifts the "partial boundary safety" caveat on the hooking
  slice when Iced is opted in.

Also gitignore test-run TestResults artifacts.

Tests: 221 passing, 4 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 08:56:31 +02:00

12 KiB

1. Project Setup

  • 1.1 Create WhiteMagic/WhiteMagic.csproj targeting net8.0-windows, AllowUnsafeBlocks=true, nullable enabled, TreatWarningsAsErrors, Platforms=x86;x64;AnyCPU
  • 1.2 Create WhiteMagicTest/WhiteMagicTest.csproj (xUnit, net8.0-windows) referencing WhiteMagic
  • 1.3 Create WhiteMagic.slnx (SDK 10 default solution format) and add both projects. (Built on SDK 10; net8.0-windows targeting pack auto-restored.)
  • 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.slnx — zero errors, zero warnings

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. Deviation (review): split Size (managed Unsafe.SizeOf<T>, blittable/MemoryMarshal path) from MarshalSize (Marshal.SizeOf<T>, marshal path). A single size mis-sized structs whose unmanaged width differs — a bool field (managed 1 / unmanaged 4) over-read the blittable path; an inline ByValTStr/ByValArray under-sized the marshal path and overran the pinned buffer (heap corruption on write). MemoryBase now picks per TypeRequiresMarshal at all four IO sites. Unused fields (SizeU, IsIntPtr, TypeCode, RealType) dropped.
  • 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. Deviation (review): shared RPM/WPM extracted to WhiteMagic/RpmHelper.cs so ExternalReader and InProcessReader stay byte-consistent — partial reads honored (returns exactly bytesRead), write returns actual bytes / 0 on total failure; InProcessReader guards MainModule like ExternalReader.
  • 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 + 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 (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)

  • 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. Deviation (review): BuildCallStub takes nuint[] (was uint[]). x64 stub is Microsoft-x64-ABI compliant: allocates 32-byte shadow space, keeps 16-byte stack alignment at the inner call (frame K ≡ 8 (mod 16), K ≥ 0x20 + 8·stackArgs), loads RCX/RDX/R8/R9 with full 64-bit imm64 (no >4 GiB pointer truncation), and writes stack args above the shadow window (no return-address clobber). x86 rejects args > uint.MaxValue. Argument count bounded by MaxArguments (256) to keep frame arithmetic overflow-free. Runtime ABI now proven: live-execution tests via CreateRemoteThread cover 5-arg register+stack delivery (Execute_sums_register_and_stack_arguments), 16-byte entry alignment arithmetically (Execute_delivers_16byte_aligned_stack_to_callee), and a hardware-alignment-sensitive SSE callee (Execute_runs_sse_callee_with_five_args: aligned movaps that #GPs unless the stub delivers a 16-byte-aligned stack, combined with a 5th stack arg).
  • 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. Resolved (8.3): DetourManager.PrologueLengthResolver now accepts IcedAssembler.GetPrologueLength for full instruction-boundary validation of arbitrary prologues; the built-in decoder remains the zero-dependency default.
  • 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. Deviation: export resolution added to PeHeaderParser.GetExportAddress (PE32/PE32+ export directory walk) and follows export forwarders (e.g. kernel32!HeapAllocNTDLL.RtlAllocateHeap) into other loaded modules; ordinal forwarders and unresolvable API-set targets throw NotSupportedException. RemoteModule resolves the base via Process.Modules (name match tolerant of .dll/case). RemoteFunction.Execute<T> defaults to the always-available RemoteThreadExecutor; Address is exposed for pump routing and CreateDelegate<T> for the in-process tier. Tests cross-check resolved addresses against the OS GetProcAddress (direct export + forwarder) and execute kernel32!GetCurrentProcessId end-to-end.
  • 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. Iced 1.21.0 added to WhiteMagic.csproj; only constructing IcedAssembler pulls it into a behavioral path. StubAssembler never references it.
  • 8.2 Add tests + implement IcedAssembler.Assemble(text, origin) for arbitrary mnemonics and origin-relative encoding. Deviation: Iced ships a fluent code assembler and a decoder but no text parser, so Assemble bridges Intel-syntax text onto Iced's Assembler by reflection — the mnemonic selects the matching fluent method and operands bind to registers (reflected from AssemblerRegisters), immediates, or labels; origin-relative encoding via Assembler.Assemble(writer, origin). Register/immediate/label operands and label-relative branches are supported; memory operands ([reg+disp]) throw NotSupportedException (a caller needing those emits bytes directly). Tests round-trip via Iced's decoder and assert origin-relative branch targets.
  • 8.3 Add tests + wire full prologue instruction-boundary validation (D5) using the Iced disassembler when present. IcedAssembler.GetPrologueLength decodes arbitrary instructions via Iced's Decoder; DetourManager.PrologueLengthResolver (a PrologueLengthResolver delegate) defaults to the built-in PrologueDecoder and is swappable to the Iced resolver, threaded into each Detour. Tests prove Iced resolves a prologue (mov rax,rcx = 48 8B C1) the built-in decoder rejects.

9. Verification

  • 9.1 Run full test suite: dotnet test WhiteMagicTest/WhiteMagicTest.csproj — all pass (180 pass, 4 integration/interactive skipped)
  • 9.2 Run full build (dotnet build WhiteMagic.slnx) — zero errors, zero new warnings in WhiteMagic
  • 9.3 Confirm existing BlackMagic/its tests are unchanged and still green. WhiteMagic is a separate, git-ignored project under reference/ sharing no source or build with BlackMagic; dotnet test reference/Blackmagic/BlackMagic.slnx = 17 passing, 0 failing (only pre-existing XML-doc warnings).
  • 9.4 Update docs/memory-library-comparison.md "WhiteMagic — synthesis" section with any deviations discovered during implementation. Added a "Deviations discovered during implementation" subsection (D1 RPM-on-self, MarshalCache size split, x64 ABI + SSE proof, export forwarders, partial prologue safety, injection bitness, bounds/protection hardening) and corrected the InProcessReader line in the architecture diagram.