Files
kbeandClaude Opus 4.8 12b9b6c03e Fix x64 stub ABI and marshal-path sizing; dedupe memory readers
x64 call stub was ABI-broken: fixed 0x20 frame left rsp misaligned at the
inner call (callee entry rsp ≡ 0, ABI requires ≡ 8) and, for 5+ args, wrote
stack args over the return address. Compute frame K ≡ 8 (mod 16), K ≥
0x20 + 8*stackArgs, so the callee sees a 16-aligned stack and stack args land
above the shadow window. Load register args as full 64-bit imm64 (was imm32,
which truncated pointers > 4 GiB). BuildCallStub now takes nuint[]; x86 range-
checks each arg against uint.MaxValue instead of silently truncating.

MarshalCache conflated managed and unmanaged width in one Size field: the
blittable path needs Unsafe.SizeOf<T> (bool = 1) while the marshal path needs
Marshal.SizeOf<T> (inline ByValTStr/ByValArray expand past the managed
pointer). Add MarshalSize; MemoryBase picks per TypeRequiresMarshal at all four
IO sites. Prevents PtrToStructure/StructureToPtr from over-reading/overwriting
the pinned scratch buffer (heap corruption on write).

Extract shared RPM/WPM into RpmHelper: honor partial reads (dead Array.Resize
removed), consistent write-return semantics; InProcessReader now guards
MainModule like ExternalReader.

Tests: x64 frame-alignment property + inline-marshal round-trip added (both
fail against the pre-fix code); existing x64 byte-expectation tests updated to
the new frame. Build clean, 100/100 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:30:10 +02:00

62 lines
2.3 KiB
C#

using System.Runtime.InteropServices;
using WhiteMagic.Native;
namespace WhiteMagic;
/// <summary>
/// Shared ReadProcessMemory / WriteProcessMemory wrappers used by both
/// <see cref="ExternalReader"/> and <see cref="InProcessReader"/>. Kept in a single
/// location to keep the two readers byte-for-byte consistent on partial-read handling,
/// write-return semantics, and failure modes.
/// </summary>
internal static class RpmHelper
{
/// <summary>
/// Reads up to <paramref name="count"/> bytes from <paramref name="address"/> in
/// the process identified by <paramref name="handle"/>. Returns:
/// <list type="bullet">
/// <item>An empty array if <see cref="NativeMethods.ReadProcessMemory"/> fails and
/// reports zero bytes read.</item>
/// <item>A truncated array of exactly <c>bytesRead</c> bytes when the call returns
/// <see langword="false"/> but the OS has placed a partial copy in the buffer
/// (for example, <c>ERROR_PARTIAL_COPY</c>).</item>
/// <item>The full buffer on success.</item>
/// </list>
/// </summary>
public static byte[] ReadBytes(SafeMemoryHandle handle, IntPtr address, int count)
{
byte[] buffer = new byte[count];
bool ok = NativeMethods.ReadProcessMemory(handle, address, buffer, count, out nint bytesRead);
if (!ok && bytesRead == 0)
{
return [];
}
if ((int)bytesRead < count)
{
// Either a successful short read, or a failed-but-partial RPM. In both
// cases honor the bytes the OS actually produced rather than padding.
byte[] partial = new byte[(int)bytesRead];
Buffer.BlockCopy(buffer, 0, partial, 0, (int)bytesRead);
return partial;
}
return buffer;
}
/// <summary>
/// Writes <paramref name="bytes"/> to <paramref name="address"/> in the process
/// identified by <paramref name="handle"/>. Returns the number of bytes actually
/// written, or 0 on total failure.
/// </summary>
public static int WriteBytes(SafeMemoryHandle handle, IntPtr address, ReadOnlySpan<byte> bytes)
{
if (!NativeMethods.WriteProcessMemory(handle, address, bytes, bytes.Length, out nint written))
{
return 0;
}
return (int)written;
}
}