Initial commit

This commit is contained in:
kbe
2026-07-21 22:30:10 +02:00
parent 21d2dd0460
commit 0380705e76
67 changed files with 7356 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using WhiteMagic;
using WhiteMagic.Native;
namespace WhiteMagicTest;
/// <summary>
/// Tests for relative/absolute addressing in <see cref="MemoryBase"/>.
/// GetAbsolute(relative) = ImageBase + relative.
/// GetRelative(absolute) = absolute - ImageBase (inverse of GetAbsolute).
/// </summary>
public class AddressingTests
{
private static ExternalReader OpenSelf()
{
return new ExternalReader(
Process.GetCurrentProcess(),
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
}
[Fact]
public void GetAbsolute_resolves_relative_offset()
{
using var reader = OpenSelf();
IntPtr imageBase = reader.ImageBase;
IntPtr result = reader.GetAbsolute((IntPtr)0x1000);
Assert.Equal(imageBase + 0x1000, result);
}
[Fact]
public void GetRelative_returns_absolute_minus_image_base()
{
using var reader = OpenSelf();
IntPtr imageBase = reader.ImageBase;
IntPtr absolute = imageBase + 0x2000;
IntPtr relative = reader.GetRelative(absolute);
Assert.Equal((IntPtr)((nint)absolute - (nint)imageBase), relative);
}
[Fact]
public void GetAbsolute_and_GetRelative_are_inverses()
{
using var reader = OpenSelf();
IntPtr offset = (IntPtr)0x3000;
// Round-trip: offset -> absolute -> back to offset
IntPtr absolute = reader.GetAbsolute(offset);
IntPtr back = reader.GetRelative(absolute);
Assert.Equal(offset, back);
// Reverse round-trip: absolute -> offset -> back to absolute
IntPtr relative = reader.GetRelative(absolute);
IntPtr absoluteAgain = reader.GetAbsolute(relative);
Assert.Equal(absolute, absoluteAgain);
}
[Fact]
public void GetRelative_on_ImageBase_returns_zero()
{
using var reader = OpenSelf();
IntPtr relative = reader.GetRelative(reader.ImageBase);
Assert.Equal(IntPtr.Zero, relative);
}
[Fact]
public void Read_with_isRelative_true_uses_image_base()
{
using var reader = OpenSelf();
// DOS header 'MZ' at the image base
byte firstByte = reader.Read<byte>(IntPtr.Zero, isRelative: true);
Assert.Equal(0x4D, firstByte);
}
[Fact]
public void Write_with_isRelative_true_resolves_correctly()
{
using var reader = OpenSelf();
int slot = 0;
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr absolute = pin.AddrOfPinnedObject();
IntPtr relative = reader.GetRelative(absolute);
Assert.True(reader.Write(relative, 42, isRelative: true));
Assert.Equal(42, reader.Read<int>(absolute));
}
finally
{
pin.Free();
}
}
[Fact]
public void ReadBytes_with_isRelative_true_resolves_correctly()
{
using var reader = OpenSelf();
byte[] data = reader.ReadBytes(IntPtr.Zero, 2, isRelative: true);
Assert.Equal(0x4D, data[0]);
Assert.Equal(0x5A, data[1]);
}
}
+155
View File
@@ -0,0 +1,155 @@
using System.Runtime.InteropServices;
using WhiteMagic;
namespace WhiteMagicTest;
/// <summary>
/// Tests for <see cref="InProcessReader"/> — direct pointer dereference against
/// the own process. Verifies the shared <see cref="MemoryBase"/> API works for
/// both external and in-process readers.
/// </summary>
public class InProcessReaderTests
{
private static InProcessReader CreateReader()
{
return new InProcessReader();
}
[Fact]
public void ImageBase_is_nonzero()
{
using var reader = CreateReader();
Assert.NotEqual(IntPtr.Zero, reader.ImageBase);
}
[Fact]
public void Read_int_reads_known_value_from_own_memory()
{
using var reader = CreateReader();
int expected = 0x12345678;
GCHandle pin = GCHandle.Alloc(expected, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
int result = reader.Read<int>(addr);
Assert.Equal(expected, result);
}
finally
{
pin.Free();
}
}
[Fact]
public void Write_int_writes_and_reads_back()
{
using var reader = CreateReader();
int slot = 0;
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.Write(addr, unchecked((int)0xCAFEBABE)));
Assert.Equal(unchecked((int)0xCAFEBABE), reader.Read<int>(addr));
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_bytes_reads_known_bytes()
{
using var reader = CreateReader();
byte[] expected = [0x0A, 0x0B, 0x0C, 0x0D];
GCHandle pin = GCHandle.Alloc(expected, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
byte[] result = reader.ReadBytes(addr, 4);
Assert.Equal(expected, result);
}
finally
{
pin.Free();
}
}
[Fact]
public void Write_bytes_writes_and_reads_back()
{
using var reader = CreateReader();
byte[] slot = new byte[4];
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
byte[] expected = [0xDE, 0xAD, 0xBE, 0xEF];
int written = reader.WriteBytes(addr, expected);
Assert.Equal(4, written);
byte[] result = reader.ReadBytes(addr, 4);
Assert.Equal(expected, result);
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_struct_via_InProcessReader()
{
using var reader = CreateReader();
var slot = new TestStruct { X = 10, Y = 20 };
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
var result = reader.Read<TestStruct>(addr);
Assert.Equal(10, result.X);
Assert.Equal(20, result.Y);
}
finally
{
pin.Free();
}
}
[Fact]
public void Write_struct_via_InProcessReader()
{
using var reader = CreateReader();
var slot = new TestStruct { X = 1, Y = 2 };
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.Write(addr, new TestStruct { X = 99, Y = 88 }));
var result = reader.Read<TestStruct>(addr);
Assert.Equal(99, result.X);
Assert.Equal(88, result.Y);
}
finally
{
pin.Free();
}
}
[Fact]
public void Dispose_disposes_handle()
{
var reader = CreateReader();
Assert.False(reader.Handle.IsClosed);
reader.Dispose();
Assert.True(reader.Handle.IsClosed);
}
}
+111
View File
@@ -0,0 +1,111 @@
using System.Runtime.InteropServices;
using WhiteMagic;
namespace WhiteMagicTest;
/// <summary>
/// Tests for <see cref="MarshalCache{T}"/>: blittable size, marshal-required flag,
/// IsIntPtr, and computed-once behavior.
/// </summary>
public class MarshalCacheTests
{
[Fact]
public void Size_for_int_is_4()
{
Assert.Equal(4, MarshalCache<int>.Size);
}
[Fact]
public void Size_for_byte_is_1()
{
Assert.Equal(1, MarshalCache<byte>.Size);
}
[Fact]
public void Size_for_IntPtr_matches_native_pointer_size()
{
Assert.Equal(IntPtr.Size, MarshalCache<IntPtr>.Size);
}
[Fact]
public void Size_for_bool_is_1()
{
Assert.Equal(1, MarshalCache<bool>.Size);
}
[Fact]
public void Size_for_enum_matches_underlying_type()
{
Assert.Equal(4, MarshalCache<DayOfWeek>.Size);
}
[Fact]
public void Size_for_blittable_struct_is_accurate()
{
Assert.Equal(8, MarshalCache<BlittableStruct>.Size);
}
[Fact]
public void TypeRequiresMarshal_is_false_for_blittable_types()
{
Assert.False(MarshalCache<int>.TypeRequiresMarshal);
Assert.False(MarshalCache<long>.TypeRequiresMarshal);
Assert.False(MarshalCache<BlittableStruct>.TypeRequiresMarshal);
}
[Fact]
public void TypeRequiresMarshal_is_true_for_types_with_MarshalAs_field()
{
Assert.True(MarshalCache<MarshalAsStruct>.TypeRequiresMarshal);
}
[Fact]
public void IsIntPtr_is_true_for_IntPtr()
{
Assert.True(MarshalCache<IntPtr>.IsIntPtr);
}
[Fact]
public void IsIntPtr_is_false_for_non_IntPtr_types()
{
Assert.False(MarshalCache<int>.IsIntPtr);
Assert.False(MarshalCache<long>.IsIntPtr);
Assert.False(MarshalCache<BlittableStruct>.IsIntPtr);
}
[Fact]
public void All_properties_are_computed_once_and_cached()
{
int size1 = MarshalCache<int>.Size;
bool marshal1 = MarshalCache<int>.TypeRequiresMarshal;
bool intPtr1 = MarshalCache<int>.IsIntPtr;
int size2 = MarshalCache<int>.Size;
bool marshal2 = MarshalCache<int>.TypeRequiresMarshal;
bool intPtr2 = MarshalCache<int>.IsIntPtr;
Assert.Equal(size1, size2);
Assert.Equal(marshal1, marshal2);
Assert.Equal(intPtr1, intPtr2);
}
[Fact]
public void SizeU_matches_Size_as_uint()
{
Assert.Equal((uint)MarshalCache<int>.Size, MarshalCache<int>.SizeU);
}
[StructLayout(LayoutKind.Sequential)]
private struct BlittableStruct
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
private struct MarshalAsStruct
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] Data;
}
}
+245
View File
@@ -0,0 +1,245 @@
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();
}
// ── 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})";
}
+189
View File
@@ -0,0 +1,189 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using WhiteMagic;
using WhiteMagic.Native;
namespace WhiteMagicTest;
/// <summary>
/// Regression tests for the edge-case defects found in the second review pass:
/// char sizing, reference-containing structs, unchecked counts, the ReadString
/// partial-chunk skip, and ExternalReader construction against awkward targets.
/// Each test fails against the pre-fix code.
/// </summary>
public class MemoryHardeningTests
{
private static ExternalReader OpenSelf()
{
return new ExternalReader(
Process.GetCurrentProcess(),
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
}
// ── char sizing (MarshalCache / MemoryBase.Read<char>) ──────────────────
[Fact]
public void MarshalCache_char_size_is_two_bytes()
{
Assert.Equal(2, MarshalCache<char>.Size);
}
[Fact]
public void Read_char_writes_and_reads_back()
{
using var reader = OpenSelf();
char slot = '\0';
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.Write(addr, 'Z'));
Assert.Equal('Z', reader.Read<char>(addr));
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_char_array_writes_and_reads_back()
{
using var reader = OpenSelf();
char[] slot = new char[4];
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
char[] expected = ['w', 'o', 'w', '!'];
Assert.True(reader.Write(addr, expected));
Assert.Equal(expected, reader.Read<char>(addr, 4));
}
finally
{
pin.Free();
}
}
// ── reference-containing structs route to the marshal path ──────────────
[Fact]
public void MarshalCache_flags_struct_with_reference_field_as_marshal_required()
{
// Has a string field but no [MarshalAs]; the blittable path (MemoryMarshal.Read)
// throws for a reference-containing T, so the cache must route it to the marshal
// path. A struct with a managed reference cannot be pinned, so the routing flag —
// not a live round-trip — is the regression guard here.
Assert.True(MarshalCache<StructWithReference>.TypeRequiresMarshal);
}
// ── unchecked count guards ──────────────────────────────────────────────
[Fact]
public void Read_array_with_negative_count_throws_argument_out_of_range()
{
using var reader = OpenSelf();
int dummy = 0;
GCHandle pin = GCHandle.Alloc(dummy, GCHandleType.Pinned);
try
{
Assert.Throws<ArgumentOutOfRangeException>(() => reader.Read<int>(pin.AddrOfPinnedObject(), -1));
}
finally
{
pin.Free();
}
}
[Fact]
public void Read_array_with_zero_count_returns_empty()
{
using var reader = OpenSelf();
int dummy = 0;
GCHandle pin = GCHandle.Alloc(dummy, GCHandleType.Pinned);
try
{
Assert.Empty(reader.Read<int>(pin.AddrOfPinnedObject(), 0));
}
finally
{
pin.Free();
}
}
// ── ReadString partial-chunk advance ────────────────────────────────────
[Fact]
public void ReadString_advances_by_actual_bytes_when_reads_are_partial()
{
// The reader serves at most 3 bytes per call. The string is longer than one
// chunk with the terminator well past it. If ReadString advanced by the
// requested count instead of the bytes actually returned, it would skip
// data and truncate the result.
byte[] data = Encoding.ASCII.GetBytes("ABCDEFGHIJ\0");
var reader = new PartialReader(data, maxChunk: 3);
string result = reader.ReadString(IntPtr.Zero, Encoding.ASCII, maxLength: 64);
Assert.Equal("ABCDEFGHIJ", result);
}
[Fact]
public void ReadString_stops_at_null_across_partial_chunks()
{
byte[] data = Encoding.ASCII.GetBytes("hi\0garbage");
var reader = new PartialReader(data, maxChunk: 1);
string result = reader.ReadString(IntPtr.Zero, Encoding.ASCII, maxLength: 64);
Assert.Equal("hi", result);
}
// ── ExternalReader construction ─────────────────────────────────────────
[Fact]
public void ExternalReader_opens_self_with_default_access()
{
// The default access set must be small enough to open a normal process.
using var reader = new ExternalReader(Process.GetCurrentProcess());
Assert.False(reader.Handle.IsInvalid);
}
/// <summary>
/// A <see cref="MemoryBase"/> that serves bytes from an in-memory buffer and
/// caps every read to <c>maxChunk</c> bytes, to exercise partial-read handling.
/// The address is treated as a zero-based index into the buffer.
/// </summary>
private sealed class PartialReader(byte[] data, int maxChunk) : MemoryBase
{
public override IntPtr ImageBase => IntPtr.Zero;
public override SafeMemoryHandle Handle => null!;
public override byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
{
int start = (int)address;
if (start < 0 || start >= data.Length || count <= 0)
return [];
int n = Math.Min(Math.Min(count, maxChunk), data.Length - start);
return data[start..(start + n)];
}
public override int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false)
=> throw new NotSupportedException();
public override void Dispose() { }
}
}
/// <summary>
/// A struct that carries a managed reference. <see cref="RuntimeHelpers.IsReferenceOrContainsReferences{T}"/>
/// reports <see langword="true"/>, so it cannot travel the blittable read path.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct StructWithReference
{
public int Id;
public string Name;
}
+112
View File
@@ -0,0 +1,112 @@
using System.Runtime.InteropServices;
using WhiteMagic.Native;
namespace WhiteMagicTest.Native;
/// <summary>
/// Integration tests that exercise the P/Invoke surface against the current
/// process. They prove the marshalling signatures are correct end-to-end.
/// </summary>
public class NativeSurfaceTests
{
private static SafeMemoryHandle OpenSelf(ProcessAccess access)
{
SafeMemoryHandle handle = NativeMethods.OpenProcess(access, false, Environment.ProcessId);
Assert.False(handle.IsInvalid, $"OpenProcess failed: {Marshal.GetLastPInvokeError()}");
return handle;
}
[Fact]
public void OpenProcess_on_self_returns_valid_handle_and_closes_on_dispose()
{
SafeMemoryHandle handle = OpenSelf(ProcessAccess.QueryInformation);
Assert.False(handle.IsClosed);
handle.Dispose();
Assert.True(handle.IsClosed);
}
[Fact]
public void ReadProcessMemory_reads_a_known_value_from_own_memory()
{
int value = 0x1BADB002;
GCHandle pin = GCHandle.Alloc(value, GCHandleType.Pinned);
try
{
using SafeMemoryHandle handle = OpenSelf(ProcessAccess.VmRead | ProcessAccess.QueryInformation);
Span<byte> buffer = stackalloc byte[sizeof(int)];
bool ok = NativeMethods.ReadProcessMemory(
handle, pin.AddrOfPinnedObject(), buffer, buffer.Length, out nint read);
Assert.True(ok, $"ReadProcessMemory failed: {Marshal.GetLastPInvokeError()}");
Assert.Equal(sizeof(int), (int)read);
Assert.Equal(value, BitConverter.ToInt32(buffer));
}
finally
{
pin.Free();
}
}
[Fact]
public void WriteProcessMemory_writes_a_value_into_own_memory()
{
int slot = 0;
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
using SafeMemoryHandle handle = OpenSelf(
ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
ReadOnlySpan<byte> payload = BitConverter.GetBytes(0x5EED);
bool ok = NativeMethods.WriteProcessMemory(
handle, pin.AddrOfPinnedObject(), payload, payload.Length, out nint written);
Assert.True(ok, $"WriteProcessMemory failed: {Marshal.GetLastPInvokeError()}");
Assert.Equal(payload.Length, (int)written);
Assert.Equal(0x5EED, Marshal.ReadInt32(pin.AddrOfPinnedObject()));
}
finally
{
pin.Free();
}
}
[Fact]
public void VirtualAllocEx_commits_then_protects_then_frees()
{
using SafeMemoryHandle handle = OpenSelf(ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
IntPtr region = NativeMethods.VirtualAllocEx(
handle, IntPtr.Zero, 0x1000,
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
MemoryProtectionType.ReadWrite);
Assert.NotEqual(IntPtr.Zero, region);
bool protect = NativeMethods.VirtualProtectEx(
handle, region, 0x1000, MemoryProtectionType.ExecuteReadWrite, out MemoryProtectionType old);
Assert.True(protect, $"VirtualProtectEx failed: {Marshal.GetLastPInvokeError()}");
Assert.Equal(MemoryProtectionType.ReadWrite, old);
bool free = NativeMethods.VirtualFreeEx(handle, region, 0, MemoryFreeType.Release);
Assert.True(free, $"VirtualFreeEx failed: {Marshal.GetLastPInvokeError()}");
}
[Fact]
public void LoadLibrary_then_GetProcAddress_resolves_an_export()
{
IntPtr module = NativeMethods.LoadLibrary("kernel32.dll");
Assert.NotEqual(IntPtr.Zero, module);
IntPtr proc = NativeMethods.GetProcAddress(module, "CloseHandle");
Assert.NotEqual(IntPtr.Zero, proc);
}
[Theory]
[InlineData(typeof(Context32), 716)]
[InlineData(typeof(Context64), 1232)]
public void Thread_context_struct_has_the_exact_native_size(Type contextType, int expectedSize)
{
Assert.Equal(expectedSize, Marshal.SizeOf(contextType));
}
}
+183
View File
@@ -0,0 +1,183 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using WhiteMagic;
using WhiteMagic.Native;
namespace WhiteMagicTest;
/// <summary>
/// Tests for <see cref="MemoryBase.ReadString"/> and <see cref="MemoryBase.WriteString"/>
/// with encoding, null-terminator stop, and max-length behavior.
/// </summary>
public class StringReadWriteTests
{
private static ExternalReader OpenSelf()
{
return new ExternalReader(
Process.GetCurrentProcess(),
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
}
[Fact]
public void WriteString_ascii_then_ReadString_round_trips()
{
using var reader = OpenSelf();
byte[] slot = new byte[64];
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.WriteString(addr, "hello", Encoding.ASCII));
string result = reader.ReadString(addr, Encoding.ASCII);
Assert.Equal("hello", result);
}
finally
{
pin.Free();
}
}
[Fact]
public void WriteString_utf8_then_ReadString_round_trips()
{
using var reader = OpenSelf();
byte[] slot = new byte[64];
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.WriteString(addr, "héllo wörld", Encoding.UTF8));
string result = reader.ReadString(addr, Encoding.UTF8);
Assert.Equal("héllo wörld", result);
}
finally
{
pin.Free();
}
}
[Fact]
public void WriteString_unicode_then_ReadString_round_trips()
{
using var reader = OpenSelf();
byte[] slot = new byte[128];
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
Assert.True(reader.WriteString(addr, "Hello\u00A9\u00AE\u20AC", Encoding.Unicode));
string result = reader.ReadString(addr, Encoding.Unicode);
Assert.Equal("Hello\u00A9\u00AE\u20AC", result);
}
finally
{
pin.Free();
}
}
[Fact]
public void ReadString_stops_at_null_terminator()
{
using var reader = OpenSelf();
byte[] slot = Encoding.ASCII.GetBytes("hello\0world");
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 64);
Assert.Equal("hello", result);
}
finally
{
pin.Free();
}
}
[Fact]
public void ReadString_respects_max_length()
{
using var reader = OpenSelf();
byte[] slot = Encoding.ASCII.GetBytes("hello world this is a test");
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 5);
Assert.Equal("hello", result);
}
finally
{
pin.Free();
}
}
[Fact]
public void WriteString_appends_null_terminator_automatically()
{
using var reader = OpenSelf();
byte[] slot = new byte[32];
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
// Write without terminator
Assert.True(reader.WriteString(addr, "test", Encoding.ASCII));
// The written bytes should end with \0
byte[] read = reader.ReadBytes(addr, 8);
Assert.Equal((byte)'t', read[0]);
Assert.Equal((byte)'e', read[1]);
Assert.Equal((byte)'s', read[2]);
Assert.Equal((byte)'t', read[3]);
Assert.Equal(0, read[4]); // null terminator
}
finally
{
pin.Free();
}
}
[Fact]
public void ReadString_empty_buffer_returns_empty_string()
{
using var reader = OpenSelf();
byte[] slot = new byte[1] { 0 };
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 1);
Assert.Equal("", result);
}
finally
{
pin.Free();
}
}
[Fact]
public void WriteString_empty_string_writes_only_null()
{
using var reader = OpenSelf();
byte[] slot = new byte[8];
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
// Write a marker first
reader.WriteBytes(addr, [0xAB, 0xCD, 0xEF, 0x00]);
// Now overwrite with empty string
Assert.True(reader.WriteString(addr, "", Encoding.ASCII));
byte[] read = reader.ReadBytes(addr, 4);
Assert.Equal(0, read[0]); // null
}
finally
{
pin.Free();
}
}
}
+237
View File
@@ -0,0 +1,237 @@
using WhiteMagic.Assembly;
namespace WhiteMagicTest;
public class StubAssemblerTests
{
private static StubAssembler Create() => new();
// ── Emit primitives ────────────────────────────────────────────────────
[Fact] public void EmitU8_appends_a_single_byte() { var s=Create(); var b=new List<byte>(); s.EmitU8(b,0xAB); Assert.Equal([0xAB],b); }
[Fact] public void EmitU32_appends_little_endian() { var s=Create(); var b=new List<byte>(); s.EmitU32(b,0x11223344); Assert.Equal([0x44,0x33,0x22,0x11],b); }
[Fact] public void EmitU32_appends_zero() { var s=Create(); var b=new List<byte>(); s.EmitU32(b,0); Assert.Equal([0,0,0,0],b); }
[Fact] public void EmitU64_appends_little_endian() { var s=Create(); var b=new List<byte>(); s.EmitU64(b,0x1122334455667788); Assert.Equal([0x88,0x77,0x66,0x55,0x44,0x33,0x22,0x11],b); }
[Fact] public void EmitU64_appends_high_bits() { var s=Create(); var b=new List<byte>(); s.EmitU64(b,0xDEADBEEF_CAFEBABE); Assert.Equal([0xBE,0xBA,0xFE,0xCA,0xEF,0xBE,0xAD,0xDE],b); }
[Fact] public void StubAssembler_is_IAssembler() { Assert.IsAssignableFrom<IAssembler>(Create()); }
[Fact] public void Assemble_throws() { Assert.Throws<NotSupportedException>(()=>Create().Assemble("nop",0)); }
// ── x86 cdecl ──────────────────────────────────────────────────────────
[Fact]
public void Cdecl_0args()
{
uint r = 0x12345678u-(0x10000000u+5);
Assert.Equal([0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[],4,CallConvention.Cdecl));
}
[Fact]
public void Cdecl_1arg()
{
uint ca=0x10000000u+5,r=0x12345678u-(ca+5);
Assert.Equal([
0x68,0xDD,0xCC,0xBB,0xAA,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),
0x83,0xC4,0x04,0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAABBCCDD],4,CallConvention.Cdecl));
}
[Fact]
public void Cdecl_2args()
{
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
Assert.Equal([
0x68,0x22,0x22,0x22,0x22,
0x68,0x11,0x11,0x11,0x11,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),
0x83,0xC4,0x08,0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0x11111111,0x22222222],4,CallConvention.Cdecl));
}
// ── x86 stdcall ──────────────────────────────────────────────────────
[Fact]
public void Stdcall_1arg()
{
uint ca=0x10000000u+5,r=0x12345678u-(ca+5);
Assert.Equal([
0x68,0xDD,0xCC,0xBB,0xAA,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAABBCCDD],4,CallConvention.Stdcall));
}
[Fact]
public void Stdcall_2args()
{
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
Assert.Equal([
0x68,0x22,0x22,0x22,0x22,
0x68,0x11,0x11,0x11,0x11,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0x11111111,0x22222222],4,CallConvention.Stdcall));
}
// ── x86 thiscall ─────────────────────────────────────────────────────
[Fact]
public void Thiscall_ecx_then_stack()
{
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
Assert.Equal([
0xB9,0x55,0x55,0xAA,0xAA,
0x68,0x66,0x66,0xBB,0xBB,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAAAA5555,0xBBBB6666],4,CallConvention.Thiscall));
}
[Fact]
public void Thiscall_1arg_ecx_only()
{
uint ca=0x10000000u+5,r=0x12345678u-(ca+5);
byte[] s=Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xCAFEBABE],4,CallConvention.Thiscall);
Assert.Equal(11,s.Length); Assert.Equal(0xB9,s[0]); Assert.Equal(0xCAFEBABE,BitConverter.ToUInt32(s,1));
Assert.Equal(0xE8,s[5]); Assert.Equal(r,BitConverter.ToUInt32(s,6)); Assert.Equal(0xC3,s[10]);
}
[Fact]
public void Thiscall_0args_throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[],4,CallConvention.Thiscall));
}
// ── x86 fastcall ────────────────────────────────────────────────────
[Fact]
public void Fastcall_ecx_edx_stack()
{
uint ca=0x10000000u+15,r=0x12345678u-(ca+5);
Assert.Equal([
0xB9,0x11,0x11,0x11,0x11,
0xBA,0x22,0x22,0x22,0x22,
0x68,0x33,0x33,0x33,0x33,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0x11111111,0x22222222,0x33333333],4,CallConvention.Fastcall));
}
[Fact]
public void Fastcall_2args_registers_only()
{
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
byte[] s=Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAAAAAAAA,0xBBBBBBBB],4,CallConvention.Fastcall);
Assert.Equal(16,s.Length); Assert.Equal(0xB9,s[0]); Assert.Equal(0xAAAAAAAA,BitConverter.ToUInt32(s,1));
Assert.Equal(0xBA,s[5]); Assert.Equal(0xBBBBBBBB,BitConverter.ToUInt32(s,6));
Assert.Equal(0xE8,s[10]); Assert.Equal(r,BitConverter.ToUInt32(s,11)); Assert.Equal(0xC3,s[15]);
}
[Fact]
public void Fastcall_0args_is_valid()
{
uint r=0x12345678u-(0x10000000u+5);
Assert.Equal([0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[],4,CallConvention.Fastcall));
}
// ── x64 ─────────────────────────────────────────────────────────────
[Fact]
public void X64_0args()
{
var s=Create(); ulong a=0x100000000,t=0x123456788;
uint r=(uint)(t-(a+5));
byte[] stub=s.BuildCallStub((IntPtr)(nint)a,(IntPtr)(nint)t,[],8,CallConvention.Cdecl);
Assert.Equal(6,stub.Length); Assert.Equal(0xE8,stub[0]); Assert.Equal(r,BitConverter.ToUInt32(stub,1)); Assert.Equal(0xC3,stub[5]);
}
[Fact]
public void X64_1arg_mov_ecx()
{
var s=Create(); ulong a=0x100000000,t=0x123456788;
uint r=(uint)(t-(a+5+5));
Assert.Equal([
0xB9,0xDD,0xCC,0xBB,0xAA,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
s.BuildCallStub((IntPtr)(nint)a,(IntPtr)(nint)t,[0xAABBCCDD],8,CallConvention.Cdecl));
}
[Fact]
public void X64_4args_rcx_rdx_r8_r9()
{
var s=Create(); ulong a=0x100000000,t=0x123456788;
uint ca=(uint)a+5+5+6+6,r=(uint)(t-(ca+5));
Assert.Equal([
0xB9,0x11,0x11,0x11,0x11, 0xBA,0x22,0x22,0x22,0x22,
0x41,0xB8,0x33,0x33,0x33,0x33, 0x41,0xB9,0x44,0x44,0x44,0x44,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
s.BuildCallStub((IntPtr)(nint)a,(IntPtr)(nint)t,[0x11111111,0x22222222,0x33333333,0x44444444],8,CallConvention.Cdecl));
}
[Fact]
public void X64_5args_push_cleanup()
{
var s=Create(); ulong a=0x100000000,t=0x123456788;
uint ca=(uint)a+5+5+6+6+5,r=(uint)(t-(ca+5));
Assert.Equal([
0xB9,1,0,0,0, 0xBA,2,0,0,0,
0x41,0xB8,3,0,0,0, 0x41,0xB9,4,0,0,0,
0x68,5,0,0,0,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),
0x48,0x83,0xC4,8, 0xC3],
s.BuildCallStub((IntPtr)(nint)a,(IntPtr)(nint)t,[1,2,3,4,5],8,CallConvention.Cdecl));
}
// ── Edge cases ────────────────────────────────────────────────────────
[Fact]
public void Far_target_throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
Create().BuildCallStub(IntPtr.Zero, unchecked((IntPtr)(nint)0xC0000000), [], 4, CallConvention.Cdecl));
}
[Fact]
public void Many_args_cleanup_uses_imm32_form()
{
var args = new uint[33];
for (int i = 0; i < 33; i++) args[i] = (uint)(i * 0x10000 + i);
byte[] stub = Create().BuildCallStub(
(IntPtr)0x10000000, (IntPtr)0x12345678, args, 4, CallConvention.Cdecl);
for (int i = 0; i < stub.Length - 5; i++)
{
if (stub[i] == 0x81 && stub[i + 1] == 0xC4)
{
Assert.Equal(132, BitConverter.ToInt32(stub, i + 2));
return;
}
}
Assert.Fail("Expected 0x81 0xC4 (add esp, imm32) not found");
}
[Fact]
public void Invalid_pointerSize_throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
Create().BuildCallStub((IntPtr)0x10000000, (IntPtr)0x12345678, [], 2, CallConvention.Cdecl));
}
[Fact]
public void Invalid_calling_convention_throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
Create().BuildCallStub((IntPtr)0x10000000, (IntPtr)0x12345678, [], 4, (CallConvention)99));
}
// ── No-FASM ─────────────────────────────────────────────────────────
[Fact]
public void No_fasm_reference_in_output()
{
var asm = typeof(StubAssembler).Assembly;
var refs = asm.GetReferencedAssemblies();
Assert.DoesNotContain(refs, r =>
r.Name!.Contains("Fasm", StringComparison.OrdinalIgnoreCase) ||
r.Name!.Contains("ManagedFasm", StringComparison.OrdinalIgnoreCase));
}
}
+25
View File
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WhiteMagic\WhiteMagic.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>