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>
297 lines
9.0 KiB
C#
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<T>/Write<T>, 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;
|
|
}
|