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