fix 7 correctness and cleanup issues

Blocking fixes:
1. Read<T> now returns default(T) on failed/partial read instead of crash
   (applies to single Read<T>, array Read<T>, and ReadBytes)
2. InProcessReader uses ReadProcessMemory via handle instead of unsafe
   Buffer.MemoryCopy — fails soft on bad address instead of AV'ing
3. GetRelative now returns absolute - ImageBase (inverse of GetAbsolute).
   Fix round-trip test to validate at arbitrary offsets, not just ImageBase
4. ReadString reads in 64-byte chunks with encoding-aware null-terminator
   pattern matching (handles UTF-16's 2-byte null, UTF-32's 4-byte null)

Cleanup:
5. InProcessReader validates handle on open and uses RPM through it
   (handle is no longer unused)
6. Array marshal read: pin raw buffer once, PtrToStructure at offset
7. StructureToByteArray(Span) delegates to byte[] overload, no duplicate

New tests: 4 invalid-address grace tests (returns default/empty/false).
All 57 passing.
This commit is contained in:
kbe
2026-07-21 19:24:18 +02:00
parent 6eb78e7974
commit 8374650aac
4 changed files with 182 additions and 63 deletions
+25 -9
View File
@@ -6,8 +6,11 @@ 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.
/// <see cref="NativeMethods.ReadProcessMemory"/> and
/// <see cref="NativeMethods.WriteProcessMemory"/> on a handle to the current
/// process. Unlike the unsafe-deref approach, this fails softly (returns
/// empty / zero bytes) on invalid or protected addresses instead of crashing
/// the host process with an <see cref="AccessViolationException"/>.
/// </summary>
public sealed class InProcessReader : MemoryBase
{
@@ -25,6 +28,12 @@ public sealed class InProcessReader : MemoryBase
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation,
false,
current.Id);
if (_handle.IsInvalid)
{
int error = Marshal.GetLastPInvokeError();
throw new InvalidOperationException(
$"OpenProcess failed for PID {current.Id}: error {error}");
}
_imageBase = current.MainModule?.BaseAddress ?? IntPtr.Zero;
}
@@ -36,30 +45,37 @@ public sealed class InProcessReader : MemoryBase
public override SafeMemoryHandle Handle => _handle;
/// <inheritdoc />
public override unsafe byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
public override byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
{
if (isRelative)
address = GetAbsolute(address);
byte[] buffer = new byte[count];
fixed (byte* ptr = buffer)
if (!NativeMethods.ReadProcessMemory(_handle, address, buffer, count, out nint bytesRead))
{
Buffer.MemoryCopy((void*)address, ptr, count, count);
return [];
}
if ((int)bytesRead != count)
{
Array.Resize(ref buffer, (int)bytesRead);
}
return buffer;
}
/// <inheritdoc />
public override unsafe int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false)
public override int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false)
{
if (isRelative)
address = GetAbsolute(address);
fixed (byte* ptr = bytes)
if (!NativeMethods.WriteProcessMemory(_handle, address, bytes, bytes.Length, out nint written))
{
Buffer.MemoryCopy(ptr, (void*)address, bytes.Length, bytes.Length);
return 0;
}
return bytes.Length;
return (int)written;
}
/// <inheritdoc />