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>
62 lines
2.3 KiB
C#
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;
|
|
}
|
|
}
|