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)