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

297 lines
9.0 KiB
C#

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using WhiteMagic;
using WhiteMagic.Native;
namespace WhiteMagicTest;
/// <summary>
/// Tests for <see cref="MemoryBase"/> abstract contract and <see cref="ExternalReader"/>
/// round-trip (Read&lt;T&gt;/Write&lt;T&gt;, arrays) using the current process as target.
/// </summary>
public class MemoryBaseTests
{
private static ExternalReader OpenSelf()
{
return new ExternalReader(
Process.GetCurrentProcess(),
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
}
[Fact]
public void ImageBase_is_nonzero_for_self()
{
using var reader = OpenSelf();
Assert.NotEqual(IntPtr.Zero, reader.ImageBase);
}
[Fact]
public void Read_int_writes_and_reads_back()
{
using var reader = OpenSelf();
int slot = 0;
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.Write(addr, 0x1BADB002));
Assert.Equal(0x1BADB002, reader.Read<int>(addr));
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_byte_writes_and_reads_back()
{
using var reader = OpenSelf();
byte slot = 0;
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.Write(addr, (byte)0xAB));
Assert.Equal(0xAB, reader.Read<byte>(addr));
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_long_writes_and_reads_back()
{
using var reader = OpenSelf();
long slot = 0;
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.Write(addr, unchecked((long)0xDEADBEEF_CAFEBABE)));
Assert.Equal(unchecked((long)0xDEADBEEF_CAFEBABE), reader.Read<long>(addr));
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_blittable_struct_writes_and_reads_back()
{
using var reader = OpenSelf();
var slot = new TestStruct { X = 42, Y = 99 };
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.Write(addr, new TestStruct { X = 100, Y = 200 }));
var result = reader.Read<TestStruct>(addr);
Assert.Equal(100, result.X);
Assert.Equal(200, result.Y);
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_bytes_writes_and_reads_back()
{
using var reader = OpenSelf();
byte[] buffer = new byte[16];
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
byte[] expected = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
int written = reader.WriteBytes(addr, expected);
Assert.Equal(expected.Length, written);
byte[] actual = reader.ReadBytes(addr, expected.Length);
Assert.Equal(expected, actual);
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_int_array_writes_and_reads_back()
{
using var reader = OpenSelf();
int[] buffer = new int[4];
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
int[] expected = [10, 20, 30, 40];
Assert.True(reader.Write(addr, expected));
int[] actual = reader.Read<int>(addr, 4);
Assert.Equal(expected, actual);
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_struct_array_writes_and_reads_back()
{
using var reader = OpenSelf();
var buffer = new TestStruct[4];
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
var expected = new[]
{
new TestStruct { X = 1, Y = 2 },
new TestStruct { X = 3, Y = 4 },
new TestStruct { X = 5, Y = 6 },
new TestStruct { X = 7, Y = 8 },
};
Assert.True(reader.Write(addr, expected));
var actual = reader.Read<TestStruct>(addr, 4);
Assert.Equal(expected, actual);
}
finally
{
pin.Free();
}
}
[Fact]
public void Write_returns_false_for_invalid_address()
{
using var reader = OpenSelf();
Assert.False(reader.Write(IntPtr.Zero, 42));
}
[Fact]
public void Dispose_closes_handle()
{
var reader = OpenSelf();
Assert.False(reader.Handle.IsClosed);
reader.Dispose();
Assert.True(reader.Handle.IsClosed);
}
[Fact]
public void Double_dispose_does_not_throw()
{
var reader = OpenSelf();
reader.Dispose();
reader.Dispose();
}
// ── Marshal-path round-trip ───────────────────────────────────────────────
//
// The marshal path was previously sized using MarshalCache.Size (managed
// layout width). When a struct carries an inline marshal-expanded field
// (ByValTStr, ByValArray, etc.) this width is smaller than the actual
// read/write width — writing overflows the pinned buffer and reading
// under-fetches the remote bytes, producing silent heap corruption.
//
// The marshal path must use MarshalCache.MarshalSize (= Marshal.SizeOf<T>)
// so the pinned buffer is large enough for PtrToStructure / StructureToPtr.
[Fact]
public void Read_struct_via_marshal_path_round_trips_inline_string()
{
using var reader = OpenSelf();
// A marshal-path struct carries a reference, so it cannot be pinned; the
// target must be an unmanaged buffer of the FULL marshal width. Pre-patch,
// Write sized its scratch buffer with MarshalCache.Size (managed pointer
// width, 8) and StructureToPtr overran it, while Read under-fetched the
// remote bytes — the string came back wrong. Post-patch both use
// MarshalSize (Marshal.SizeOf<InlineStr>).
int size = Marshal.SizeOf<InlineStr>();
IntPtr addr = Marshal.AllocHGlobal(size);
try
{
InlineStr original = new InlineStr { Name = "Hello, World!" };
Assert.True(reader.Write(addr, original));
InlineStr read = reader.Read<InlineStr>(addr);
Assert.Equal("Hello, World!", read.Name);
}
finally
{
Marshal.FreeHGlobal(addr);
}
}
// ── Graceful failure on invalid addresses ───────────────────────────────
[Fact]
public void Read_int_on_invalid_address_returns_default()
{
using var reader = OpenSelf();
Assert.Equal(0, reader.Read<int>(IntPtr.Zero));
}
[Fact]
public void Read_struct_on_invalid_address_returns_default()
{
using var reader = OpenSelf();
var result = reader.Read<TestStruct>(IntPtr.Zero);
Assert.Equal(0, result.X);
Assert.Equal(0, result.Y);
}
[Fact]
public void Read_int_array_on_invalid_address_returns_empty()
{
using var reader = OpenSelf();
Assert.Empty(reader.Read<int>(IntPtr.Zero, 10));
}
[Fact]
public void Read_bytes_on_invalid_address_returns_empty()
{
using var reader = OpenSelf();
Assert.Empty(reader.ReadBytes(IntPtr.Zero, 10));
}
}
/// <summary>
/// A simple blittable struct for use in tests.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct TestStruct : IEquatable<TestStruct>
{
public int X;
public int Y;
public bool Equals(TestStruct other) => X == other.X && Y == other.Y;
public override bool Equals(object? obj) => obj is TestStruct other && Equals(other);
public override int GetHashCode() => HashCode.Combine(X, Y);
public override string ToString() => $"({X}, {Y})";
}
/// <summary>
/// A struct whose managed layout is just a reference pointer (8 bytes) but whose
/// unmanaged marshal layout carries an inline character buffer. Exercised by
/// <see cref="MemoryBaseTests.Read_struct_via_marshal_path_round_trips_inline_string"/>
/// to catch regressions where the marshal path uses the managed width instead
/// of the marshal width.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct InlineStr
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)]
public string Name;
}