diff --git a/.gitignore b/.gitignore index 45355ea..88208ae 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ reference/ # Scratch *.tmp +*.log diff --git a/openspec/changes/whitemagic-foundation/design.md b/openspec/changes/whitemagic-foundation/design.md index 199ee1c..b830969 100644 --- a/openspec/changes/whitemagic-foundation/design.md +++ b/openspec/changes/whitemagic-foundation/design.md @@ -36,7 +36,7 @@ WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuse `MemoryBase` defines abstract `ReadBytes`/`WriteBytes`/`Read`/`Write`, 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`. +- `InProcessReader : MemoryBase` — reads the current process via `ReadProcessMemory`/`WriteProcessMemory` on a self-handle; owns the `DetourManager` and `InProcessInvoker`. **(Revised from `unsafe` direct deref during Phase 2: .NET cannot catch `AccessViolationException`, so a bad deref kills the host with no soft-failure path; RPM-on-self fails soft. The in-process speed win moves to the delegate-call/detour paths, not the reader. See `specs/memory-access`.)** **Why**: GreyMagic proved this abstraction lets the same higher-level code (pattern scan, patch, high-level API) run in either mode. External is the primary path for a bot host; in-process is the fast/crash-free path once injected. diff --git a/openspec/changes/whitemagic-foundation/specs/memory-access/spec.md b/openspec/changes/whitemagic-foundation/specs/memory-access/spec.md index 94f9e8f..900242f 100644 --- a/openspec/changes/whitemagic-foundation/specs/memory-access/spec.md +++ b/openspec/changes/whitemagic-foundation/specs/memory-access/spec.md @@ -2,7 +2,13 @@ ### Requirement: Abstract memory base with two readers -WhiteMagic SHALL expose an abstract `MemoryBase` type defining `ReadBytes`, `WriteBytes`, generic `Read`/`Write`, 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). +WhiteMagic SHALL expose an abstract `MemoryBase` type defining `ReadBytes`, `WriteBytes`, generic `Read`/`Write`, array read/write, and string read/write, with two concrete implementations: `ExternalReader` (out-of-process via ReadProcessMemory/WriteProcessMemory) and `InProcessReader` (in-process, reading the current process through ReadProcessMemory/WriteProcessMemory on a self-handle). + +> **Deviation from design D1.** D1 originally specified `InProcessReader` as `unsafe` direct pointer dereference (the "fast/crash-free" path). Implementation revised it to `ReadProcessMemory`/`WriteProcessMemory` on a handle to the current process, because .NET (Core) cannot catch `AccessViolationException` (`HandleProcessCorruptedStateExceptions` is removed), so a raw deref of a bad address terminates the host process with no soft-failure path. RPM on a self-handle fails soft (returns empty) like `ExternalReader`. The in-process performance win therefore moves to the delegate-call and detour paths (`InProcessInvoker`, `DetourManager`), not the reader. + +#### Scenario: in-process read fails soft on an invalid address +- **WHEN** an `InProcessReader` reads an unmapped or protected address +- **THEN** it MUST return empty/`default` rather than crash the host process #### Scenario: external read round-trip - **WHEN** an `ExternalReader` opens a target process and writes a value with `Write(addr, 0x1234)` then reads it back with `Read(addr)` diff --git a/openspec/changes/whitemagic-foundation/tasks.md b/openspec/changes/whitemagic-foundation/tasks.md index 36af806..e943ad2 100644 --- a/openspec/changes/whitemagic-foundation/tasks.md +++ b/openspec/changes/whitemagic-foundation/tasks.md @@ -16,7 +16,8 @@ - [x] 2.6 Implement `ReadString`/`WriteString` on `MemoryBase` to pass 2.5 - [x] 2.7 Add tests for relative/absolute addressing (`GetAbsolute`/`GetRelative`, `isRelative` flag) - [x] 2.8 Implement addressing helpers to pass 2.7 -- [x] 2.9 Add tests + `unsafe` implementation for `InProcessReader` (direct deref against own process); verify shared `MemoryBase` API works for both readers +- [x] 2.9 Add tests + implementation for `InProcessReader` (RPM/WPM on a self-handle — see D1 deviation note; direct deref rejected because .NET cannot catch `AccessViolationException`); verify shared `MemoryBase` API works for both readers +- [ ] 2.10 Follow-up (found in review): `ReadString` scans for the null terminator byte-by-byte, so for UTF-16/UTF-32 it can match a **misaligned** multi-byte null across a char boundary (e.g. `"A"`+U+4200 = `41 00 00 42` matches `{00,00}` at offset 1) and can miss a terminator split across the 64-byte chunk boundary. Harmless for ASCII/UTF-8 (the WoW case). Fix: align the scan to the encoding's code-unit width and carry the last `(nullLen-1)` bytes across chunks. Add a UTF-16 test. ## 3. Managed Assembler (spec: managed-assembler)