Files
whitemagic/openspec/changes/whitemagic-foundation/specs/memory-access/spec.md
T
2026-07-21 22:30:10 +02:00

3.9 KiB

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