- Example5: Fix format string bugs (alignment specifier placement, SafeMemoryHandle formatting)
- Example4: Fix DetourManager.Detour() → Create() in all doc strings
- Example3: Fix MemoryBase.CreateFunction() → InProcessInvoker.CreateFunction() in doc strings
- Examples 1-5: Correct all runtime errors and API mismatches vs actual WhiteMagic API
- README: Fix DetourManager.Detour() examples to use Create(); add missing code fence markers
- Add WhiteMagic.Examples project with 5 comprehensive example files (40+ sub-examples)
- Add docfx.json and toc.md for DocFX API reference generation
- Add 5 conceptual guides: architecture, memory-access, execution-models, hooking, troubleshooting
- Ensure zero errors, zero warnings across all projects (net8.0-windows)
Doc strings now teach correct APIs; runtime format bugs eliminated; build succeeds.
- TryBind wraps Convert.ChangeType in TryChangeType so an immediate that
overflows a candidate parameter type returns false (letting a wider overload
be tried) instead of throwing OverflowException out of assembly. Verified:
"mov eax, 4294967295" and "mov eax, -2147483649" no longer crash.
- Immediate now carries a boxed long OR ulong; TryParseImmediate parses decimal
values above long.MaxValue via a ulong fallback, and hex via ulong. Previously
such literals were rejected at parse time.
- Unwrap TargetInvocationException from method.Invoke so callers see the real
Iced failure, not the reflection wrapper.
Tests: 231 passing, 4 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- PeHeaderParser: split export forwarders on the FIRST dot (IndexOf), not the
last. A forwarder is "Module.Function" and the module name has no extension, so
the last-dot split misparsed export names that themselves contain a dot.
- PeHeaderParser: document that API-set (api-ms-win-*/ext-ms-*) and ordinal
forwarders are unsupported and should be resolved via the OS loader.
- RemoteFunction.CreateDelegate now throws InvalidOperationException unless the
session is in-process; an external target's address is not host-mapped and a
delegate to it would access-violate on invocation. Tests cover both paths.
- Reword the SSE-payload comment: the 16-byte scratch sits below the saved
return address, which the aligned store leaves intact (it never overwrote it).
Tests: 223 passing, 4 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- 9.3: BlackMagic reference tests confirmed green (17 passing, 0 failing);
WhiteMagic is additive and shares no source or build with it.
- 9.4: document the implementation deviations in the comparison doc's
synthesis section and correct the InProcessReader architecture line
(RPM-on-self, not direct deref).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 7.2: export resolution + module/function facade.
- PeHeaderParser.GetExportAddress walks the PE32/PE32+ export directory and
follows export forwarders (e.g. kernel32!HeapAlloc -> NTDLL.RtlAllocateHeap)
into other loaded modules; ordinal and unresolvable API-set forwarders throw
NotSupportedException.
- RemoteModule resolves a module base via Process.Modules (name match tolerant
of .dll/case); RemoteFunction executes via RemoteThreadExecutor by default,
exposes Address for pump routing and CreateDelegate<T> for in-process.
- Magic gains a string indexer: magic["user32"]["MessageBoxA"].
Task 3.8: add the missing live-execution ABI test - an SSE callee whose aligned
movaps #GPs unless the stub delivers a 16-byte-aligned stack, combined with a
5th stack argument. Runtime-proves shadow space, alignment, and arg placement.
Tests: 214 passing, 4 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Because DllInjector enforces matching host/target bitness, a 32-bit caller
always handles a 32-bit target. The correct API is native
GetThreadContext/SetThreadContext with Context32; the Wow64 APIs are only for
64-bit processes inspecting WOW64 targets, which never happens here.
- Collapse the 32-bit path to always use GetThreadContext/SetThreadContext.
- Remove the now-unused Wow64GetThreadContext/Wow64SetThreadContext
declarations.
- Update Context32 doc comment to describe the x86 usage.
- ExternalReader access guard now also accepts QueryLimitedInformation.
Tests: 207 passing, 4 skipped.
- Add GetThreadContext/SetThreadContext overloads accepting Context32 so a
32-bit process on a native 32-bit OS can capture x86 thread context.
- DllInjector.InjectWithThreadHijack now selects the context API based on
both process bitness and OS bitness:
* 64-bit process -> native 64-bit context
* 32-bit process on 64-bit OS -> WOW64 context
* 32-bit process on 32-bit OS -> native x86 context
- ExternalReader now validates that the caller supplied
ProcessAccess.QueryInformation, and surfaces any IsWow64Process failure
instead of silently falling back to host bitness.
Tests: 207 passing, 4 skipped.
- AllocatedMemory.Read<T>/Write<T>/ReadBytes/WriteBytes now validate that
the requested byte range stays within the allocated block before calling
into the memory accessor.
- Patch.Apply/Remove temporarily changes the target page to read-write and
restores the original protection, mirroring the Detour behavior.
- MainThreadPump.WorkItem uses TrySetResult/TrySetException and swallows the
InvalidOperationException raised when a completion source is already
completed, preventing Dispose from failing during concurrent pump drainage.
Regression tests added for all three fixes.
Tests: 206 passing, 4 skipped.
Replace the previous partial develop snapshot with the completed
whitemagic-foundation implementation. This includes the managed memory
access layer, diagnostic interop helpers, process introspection utilities,
and supporting test suite.
Prior develop state kept in branch develop-backup-20260722-014135.
Replace the two-attempt read with a single read sized to the smaller of:
- detourLength + 16 (the decoder's preferred window), and
- bytes remaining in the current page (so ReadProcessMemory does not fail whole read).
Reading only detourLength bytes could leave the instruction analyzer without enough
bytes to resolve a multi-byte instruction that crosses the splice point on
sparse prologues. Reading up to the page boundary gives the largest safe window.
Tests: 199 passing, 4 integration/interactive skipped.
Risk mitigation:
- redirect now falls back to reading the minimum required bytes (detourLength) if the full buffer (detourLength + 16) cannot be read due to page boundaries.
- First attempt: read detourLength + 16 bytes for the instruction analyzer (preferred).
- Second attempt: read only detourLength bytes if the first attempt fails (bare minimum).
- Throw only if both attempts fail.
This prevents crashes when function interception functions that sit at the very end of a committed page.
Tests: 199 passing, 4 integration/interactive skipped.
Bug fixes:
- InputSimulator: Pass correct button state (MK_LBUTTON/MK_RBUTTON) in wParam for button-down messages instead of 0.
- PeHeaderParser: ParseOptionalHeader now reads only the optional header, not section headers (fixes double-parse waste).
- EntryPoint: Removed useless isPe32Plus branch (AddressOfEntryPoint is at offset 16 in both PE32 and PE32+).
- RemoteWindow: Handle null foreground window case in Activate to avoid calling GetWindowThreadProcessId with HWND 0.
- RemotePointer: Remove dead null-conditional operators (encoding ??) since encoding is non-nullable.
Constants added:
- SystemMethods: MkLButton (0x0001) and MkRButton (0x0002) for mouse button state flags.
Tests: 199 passing, 4 integration/interactive skipped.
- Track whether transferred thread's original context was successfully restored.
- In the catch block, leak the remote allocation instead of freeing it if the thread was not restored; this prevents the target process from executing freed memory.
- Remove redundant 'op == 0x55' check in InstructionAnalyzer (already matched by (op & 0xF8) == 0x50).
- Simplify MemoryBase ReadString align-down expression to previousLen - (previousLen % nullLen).
- Change StubAllocator size parameter from nint to int for clarity (internal test seam).
Tests: 199 passing, 4 integration/interactive skipped.
A non-faulting probe payload returns (rsp+8)&15 from the callee, which is 0
only when the stub delivers callee entry rsp ≡ 8 (mod 16) per the Microsoft
x64 ABI. Proves the stub frame alignment end-to-end through CreateRemoteThread
without risking a #GP that would crash the in-process test host. Verified to
fail against the old fixed-0x20 frame.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add RemoteAllocator / RemoteReleaser internal test seams for string/struct scratch memory.
- Route all scratch allocation/freeing through the seams so tests can observe leaks.
- Fix inverted StubAllocator ownership: a caller-provided stub is now never freed by the executor.
- Rewrite Execute_releases_allocated_remote_memory_on_write_failure to fail pre-fix by tracking fake allocations through the seams.
- Update WriteFailingMemoryBase to carry a valid self-handle so the executor reaches the marshal/write path.
Tests: 198 passing, 4 integration/interactive skipped.
- MainThreadDispatcher: guard DispatchHook with try/catch so exceptions never escape to native caller; drain and fault pending work on Dispose; synchronize Execute/ExecuteAsync/Dispose against race/dispose.
- InstructionAnalyzer: require ModRM 0xEC for 0x83/0x81 sub-esp/rsp forms, rejecting unsafe RIP-relative or memory forms.
- PatternScannerCache: implement value equality on CacheKey so repeated scans actually hit cache.
- BackgroundTaskExecutor: add remote allocations to the free list immediately after VirtualAllocEx, before any write that could fail and leak.
- redirect: capture and restore original page protection in Apply/Remove instead of leaving target RWX.
- Regression tests for all six fixes.
Tests: 198 passing, 4 integration/interactive skipped.
The x64 frame math (0x20 + 8*stackArgs) and x86 arg buffer size grow with the
argument count. An absurdly large count could overflow int and produce a bogus
or negative frame. Add a MaxArguments (256) bound checked at the public entry —
far above any real calling convention — so the arithmetic stays in range. Add a
test asserting the cap is inclusive and count+1 throws.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
x64 call stub was ABI-broken: fixed 0x20 frame left rsp misaligned at the
inner call (callee entry rsp ≡ 0, ABI requires ≡ 8) and, for 5+ args, wrote
stack args over the return address. Compute frame K ≡ 8 (mod 16), K ≥
0x20 + 8*stackArgs, so the callee sees a 16-aligned stack and stack args land
above the shadow window. Load register args as full 64-bit imm64 (was imm32,
which truncated pointers > 4 GiB). BuildCallStub now takes nuint[]; x86 range-
checks each arg against uint.MaxValue instead of silently truncating.
MarshalCache conflated managed and unmanaged width in one Size field: the
blittable path needs Unsafe.SizeOf<T> (bool = 1) while the marshal path needs
Marshal.SizeOf<T> (inline ByValTStr/ByValArray expand past the managed
pointer). Add MarshalSize; MemoryBase picks per TypeRequiresMarshal at all four
IO sites. Prevents PtrToStructure/StructureToPtr from over-reading/overwriting
the pinned scratch buffer (heap corruption on write).
Extract shared RPM/WPM into RpmHelper: honor partial reads (dead Array.Resize
removed), consistent write-return semantics; InProcessReader now guards
MainModule like ExternalReader.
Tests: x64 frame-alignment property + inline-marshal round-trip added (both
fail against the pre-fix code); existing x64 byte-expectation tests updated to
the new frame. Build clean, 100/100 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The x64 frame math (0x20 + 8*stackArgs) and x86 arg buffer size grow with the
argument count. An absurdly large count could overflow int and produce a bogus
or negative frame. Add a MaxArguments (256) bound checked at the public entry —
far above any real calling convention — so the arithmetic stays in range. Add a
test asserting the cap is inclusive and count+1 throws.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
x64 call stub was ABI-broken: fixed 0x20 frame left rsp misaligned at the
inner call (callee entry rsp ≡ 0, ABI requires ≡ 8) and, for 5+ args, wrote
stack args over the return address. Compute frame K ≡ 8 (mod 16), K ≥
0x20 + 8*stackArgs, so the callee sees a 16-aligned stack and stack args land
above the shadow window. Load register args as full 64-bit imm64 (was imm32,
which truncated pointers > 4 GiB). BuildCallStub now takes nuint[]; x86 range-
checks each arg against uint.MaxValue instead of silently truncating.
MarshalCache conflated managed and unmanaged width in one Size field: the
blittable path needs Unsafe.SizeOf<T> (bool = 1) while the marshal path needs
Marshal.SizeOf<T> (inline ByValTStr/ByValArray expand past the managed
pointer). Add MarshalSize; MemoryBase picks per TypeRequiresMarshal at all four
IO sites. Prevents PtrToStructure/StructureToPtr from over-reading/overwriting
the pinned scratch buffer (heap corruption on write).
Extract shared RPM/WPM into RpmHelper: honor partial reads (dead Array.Resize
removed), consistent write-return semantics; InProcessReader now guards
MainModule like ExternalReader.
Tests: x64 frame-alignment property + inline-marshal round-trip added (both
fail against the pre-fix code); existing x64 byte-expectation tests updated to
the new frame. Build clean, 100/100 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Describe the library through its Win32 API surface and consent
context instead of capability keywords. Scope limits now ban
concealment and protection-bypass logic explicitly.
Second-review hardening of the memory layer (char sizing, reference-struct
routing, count guards, ReadString partial-advance, ExternalReader access +
MainModule guard, DWORD signatures) plus the in-progress Phase 3 StubAssembler
work carried on the branch. 76 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second-review fixes, each covered by a regression test in MemoryHardeningTests:
- MarshalCache: special-case char (Size=2; Marshal.SizeOf reports 1/ANSI but the
blittable path reads a 2-byte UTF-16 unit). TypeRequiresMarshal now also trips on
RuntimeHelpers.IsReferenceOrContainsReferences<T>() so reference-carrying structs
route to the marshal path instead of throwing in MemoryMarshal.Read. Document that
the MarshalAs scan is top-level only.
- MemoryBase.Read<T>(count): reject negative count (ArgumentOutOfRangeException) and
guard elementSize*count overflow. Same overflow guard on Write<T>(values).
- MemoryBase.ReadString: advance by bytes actually read, not the requested amount, so
a partial read no longer skips the unread tail of the window.
- ExternalReader: default to a minimal access set (not AllAccess, which over-requests
and fails on protected processes); wrap Process.MainModule in try/catch so a
bitness-mismatched or protected target yields ImageBase=Zero instead of throwing.
- NativeMethods: WaitForSingleObject and CreateRemoteThread's threadId are DWORD (uint),
not int — the signatures no longer sign-flip.
Deferred: hoisting the identical ExternalReader/InProcessReader byte-IO into MemoryBase
(cosmetic; skipped to avoid colliding with concurrent Phase 3 edits).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stride the scan by pattern.Length (1 for ASCII/UTF-8, 2 for UTF-16,
4 for UTF-32) to avoid matching a misaligned null-terminator pattern
mid-character.
Record the D1 deviation: InProcessReader reads the current process through
ReadProcessMemory/WriteProcessMemory on a self-handle, not unsafe direct
deref. Rationale: .NET cannot catch AccessViolationException, so a raw deref
of a bad address terminates the host with no soft-failure path. Update the
memory-access spec (new fail-soft scenario), design D1, and task 2.9.
Log task 2.10: ReadString null-terminator scan is not code-unit aligned, so
UTF-16/UTF-32 can match a misaligned multi-byte null or miss one split
across a chunk boundary (harmless for ASCII/UTF-8, the WoW case).
Ignore *.log (testrun.log).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking fixes:
1. Read<T> now returns default(T) on failed/partial read instead of crash
(applies to single Read<T>, array Read<T>, and ReadBytes)
2. InProcessReader uses ReadProcessMemory via handle instead of unsafe
Buffer.MemoryCopy — fails soft on bad address instead of AV'ing
3. GetRelative now returns absolute - ImageBase (inverse of GetAbsolute).
Fix round-trip test to validate at arbitrary offsets, not just ImageBase
4. ReadString reads in 64-byte chunks with encoding-aware null-terminator
pattern matching (handles UTF-16's 2-byte null, UTF-32's 4-byte null)
Cleanup:
5. InProcessReader validates handle on open and uses RPM through it
(handle is no longer unused)
6. Array marshal read: pin raw buffer once, PtrToStructure at offset
7. StructureToByteArray(Span) delegates to byte[] overload, no duplicate
New tests: 4 invalid-address grace tests (returns default/empty/false).
All 57 passing.
Add GetAbsolute/GetRelative with nint-based arithmetic (bitness-agnostic).
Add 6 tests: absolute resolution, relative computation, base-round-trip,
isRelative flag on Read/Write/ReadBytes.
Fix MemoryBase.cs: restore for-loop body damaged by prior edit, clean up
StructureToByteArray overloads.
All passing (total: 44).
Add abstract MemoryBase base class with typed Read<T>/Write<T>, array
IO, string IO, and relative/absolute addressing. Uses MarshalCache<T>
to branch between blittable (MemoryMarshal) and marshal-required paths.
Add ExternalReader (out-of-process via ReadProcessMemory/WriteProcessMemory)
with SafeMemoryHandle lifecycle management.
11 new tests: ImageBase, Read/Write of int/byte/long/struct, byte array,
int array, struct array, invalid address, dispose, double-dispose.
All passing (total: 30).
Add MarshalCache<T> static class that computes Size, SizeU,
TypeRequiresMarshal, IsIntPtr, TypeCode, and RealType once per type
in the static constructor. Handles bool (size=1), enums (underlying
type), and MarshalAs-attributed fields (TypeRequiresMarshal).
12 new tests covering: blittable sizes, bool size, enum size, struct
size, marshal-required flag, IsIntPtr, computed-once caching.
All passing.
Create the WhiteMagic net8.0-windows class library and the WhiteMagicTest
xUnit project, grouped in WhiteMagic.slnx (SDK 10 default format). Library
enables nullable, unsafe blocks, x86/x64 platforms, warnings-as-errors.
Empty solution builds clean (0 errors, 0 warnings).
Add AGENTS.md (ASD-STE100) defining the build/test commands, the test-first
rule, and the one-feature-one-branch workflow with review before merge to
master.
Mark project-setup tasks 1.1-1.3, 1.5 done; 1.4 (Native P/Invoke surface)
is the first feature branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>