Phase 2: Core Memory Access complete

task 2.9: implement InProcessReader with tests

Add InProcessReader: direct pointer dereference (unsafe) against own
process via Buffer.MemoryCopy. Implements MemoryBase API for in-process
scenarios (injected managed DLL).

8 tests: ImageBase, Read/Write int, ReadBytes, WriteBytes, Read/Write
struct, dispose lifecycle.

All passing: 52 tests (7 Native + 12 MarshalCache + 11 MemoryBase +
8 String + 6 Addressing + 8 InProcessReader).
This commit is contained in:
kbe
2026-07-21 17:09:36 +02:00
parent 8219ac95a6
commit 6eb78e7974
3 changed files with 238 additions and 9 deletions
+74
View File
@@ -0,0 +1,74 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using WhiteMagic.Native;
namespace WhiteMagic;
/// <summary>
/// In-process memory reader that accesses the owning process's memory through
/// direct pointer dereference (<c>unsafe</c>). Use this reader from within a
/// managed DLL injected into the target process.
/// </summary>
public sealed class InProcessReader : MemoryBase
{
private readonly SafeMemoryHandle _handle;
private readonly IntPtr _imageBase;
private bool _disposed;
/// <summary>
/// Creates an in-process reader for the current process.
/// </summary>
public InProcessReader()
{
Process current = Process.GetCurrentProcess();
_handle = NativeMethods.OpenProcess(
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation,
false,
current.Id);
_imageBase = current.MainModule?.BaseAddress ?? IntPtr.Zero;
}
/// <inheritdoc />
public override IntPtr ImageBase => _imageBase;
/// <inheritdoc />
public override SafeMemoryHandle Handle => _handle;
/// <inheritdoc />
public override unsafe byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
{
if (isRelative)
address = GetAbsolute(address);
byte[] buffer = new byte[count];
fixed (byte* ptr = buffer)
{
Buffer.MemoryCopy((void*)address, ptr, count, count);
}
return buffer;
}
/// <inheritdoc />
public override unsafe int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false)
{
if (isRelative)
address = GetAbsolute(address);
fixed (byte* ptr = bytes)
{
Buffer.MemoryCopy(ptr, (void*)address, bytes.Length, bytes.Length);
}
return bytes.Length;
}
/// <inheritdoc />
public override void Dispose()
{
if (!_disposed)
{
_disposed = true;
_handle.Dispose();
}
}
}
+155
View File
@@ -0,0 +1,155 @@
using System.Runtime.InteropServices;
using WhiteMagic;
namespace WhiteMagicTest;
/// <summary>
/// Tests for <see cref="InProcessReader"/> — direct pointer dereference against
/// the own process. Verifies the shared <see cref="MemoryBase"/> API works for
/// both external and in-process readers.
/// </summary>
public class InProcessReaderTests
{
private static InProcessReader CreateReader()
{
return new InProcessReader();
}
[Fact]
public void ImageBase_is_nonzero()
{
using var reader = CreateReader();
Assert.NotEqual(IntPtr.Zero, reader.ImageBase);
}
[Fact]
public void Read_int_reads_known_value_from_own_memory()
{
using var reader = CreateReader();
int expected = 0x12345678;
GCHandle pin = GCHandle.Alloc(expected, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
int result = reader.Read<int>(addr);
Assert.Equal(expected, result);
}
finally
{
pin.Free();
}
}
[Fact]
public void Write_int_writes_and_reads_back()
{
using var reader = CreateReader();
int slot = 0;
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.Write(addr, unchecked((int)0xCAFEBABE)));
Assert.Equal(unchecked((int)0xCAFEBABE), reader.Read<int>(addr));
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_bytes_reads_known_bytes()
{
using var reader = CreateReader();
byte[] expected = [0x0A, 0x0B, 0x0C, 0x0D];
GCHandle pin = GCHandle.Alloc(expected, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
byte[] result = reader.ReadBytes(addr, 4);
Assert.Equal(expected, result);
}
finally
{
pin.Free();
}
}
[Fact]
public void Write_bytes_writes_and_reads_back()
{
using var reader = CreateReader();
byte[] slot = new byte[4];
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
byte[] expected = [0xDE, 0xAD, 0xBE, 0xEF];
int written = reader.WriteBytes(addr, expected);
Assert.Equal(4, written);
byte[] result = reader.ReadBytes(addr, 4);
Assert.Equal(expected, result);
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_struct_via_InProcessReader()
{
using var reader = CreateReader();
var slot = new TestStruct { X = 10, Y = 20 };
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
var result = reader.Read<TestStruct>(addr);
Assert.Equal(10, result.X);
Assert.Equal(20, result.Y);
}
finally
{
pin.Free();
}
}
[Fact]
public void Write_struct_via_InProcessReader()
{
using var reader = CreateReader();
var slot = new TestStruct { X = 1, Y = 2 };
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.Write(addr, new TestStruct { X = 99, Y = 88 }));
var result = reader.Read<TestStruct>(addr);
Assert.Equal(99, result.X);
Assert.Equal(88, result.Y);
}
finally
{
pin.Free();
}
}
[Fact]
public void Dispose_disposes_handle()
{
var reader = CreateReader();
Assert.False(reader.Handle.IsClosed);
reader.Dispose();
Assert.True(reader.Handle.IsClosed);
}
}
@@ -8,15 +8,15 @@
## 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
- [ ] 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
- [ ] 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 + `unsafe` implementation for `InProcessReader` (direct deref against own process); verify shared `MemoryBase` API works for both readers
- [x] 2.1 Add tests for `MarshalCache<T>`: blittable size, marshal-required flag, IsIntPtr, computed-once behavior
- [x] 2.2 Implement `WhiteMagic/MarshalCache.cs` to pass 2.1
- [x] 2.3 Add tests for `MemoryBase` abstract contract + `ExternalReader` round-trip (`Read<T>`/`Write<T>`, arrays) using the current process as target
- [x] 2.4 Implement `WhiteMagic/MemoryBase.cs` (abstract) and `WhiteMagic/ExternalReader.cs` to pass 2.3
- [x] 2.5 Add tests for string read/write with encoding, null-terminator stop, and max length
- [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
## 3. Managed Assembler (spec: managed-assembler)