diff --git a/WhiteMagic/InProcessReader.cs b/WhiteMagic/InProcessReader.cs new file mode 100644 index 0000000..518275d --- /dev/null +++ b/WhiteMagic/InProcessReader.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using WhiteMagic.Native; + +namespace WhiteMagic; + +/// +/// In-process memory reader that accesses the owning process's memory through +/// direct pointer dereference (unsafe). Use this reader from within a +/// managed DLL injected into the target process. +/// +public sealed class InProcessReader : MemoryBase +{ + private readonly SafeMemoryHandle _handle; + private readonly IntPtr _imageBase; + private bool _disposed; + + /// + /// Creates an in-process reader for the current process. + /// + 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; + } + + /// + public override IntPtr ImageBase => _imageBase; + + /// + public override SafeMemoryHandle Handle => _handle; + + /// + 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; + } + + /// + public override unsafe int WriteBytes(IntPtr address, ReadOnlySpan 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; + } + + /// + public override void Dispose() + { + if (!_disposed) + { + _disposed = true; + _handle.Dispose(); + } + } +} diff --git a/WhiteMagicTest/InProcessReaderTests.cs b/WhiteMagicTest/InProcessReaderTests.cs new file mode 100644 index 0000000..776b5aa --- /dev/null +++ b/WhiteMagicTest/InProcessReaderTests.cs @@ -0,0 +1,155 @@ +using System.Runtime.InteropServices; +using WhiteMagic; + +namespace WhiteMagicTest; + +/// +/// Tests for — direct pointer dereference against +/// the own process. Verifies the shared API works for +/// both external and in-process readers. +/// +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(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(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(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(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); + } +} diff --git a/openspec/changes/whitemagic-foundation/tasks.md b/openspec/changes/whitemagic-foundation/tasks.md index 848a454..36af806 100644 --- a/openspec/changes/whitemagic-foundation/tasks.md +++ b/openspec/changes/whitemagic-foundation/tasks.md @@ -8,15 +8,15 @@ ## 2. Core Memory Access (spec: memory-access) -- [ ] 2.1 Add tests for `MarshalCache`: 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`/`Write`, 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`: 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`/`Write`, 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)