Files
kbe 1911514120 Fix bounds, memory protection, and completion race in core helpers
- AllocatedMemory.Read<T>/Write<T>/ReadBytes/WriteBytes now validate that
  the requested byte range stays within the allocated block before calling
  into the memory accessor.
- Patch.Apply/Remove temporarily changes the target page to read-write and
  restores the original protection, mirroring the Detour behavior.
- MainThreadPump.WorkItem uses TrySetResult/TrySetException and swallows the
  InvalidOperationException raised when a completion source is already
  completed, preventing Dispose from failing during concurrent pump drainage.

Regression tests added for all three fixes.

Tests: 206 passing, 4 skipped.
2026-07-22 02:16:23 +02:00

324 lines
8.8 KiB
C#

using System.ComponentModel;
using WhiteMagic;
using WhiteMagic.Memory;
using WhiteMagic.Native;
namespace WhiteMagicTest.Memory;
/// <summary>
/// Tests for <see cref="AllocatedMemory"/>.
/// </summary>
public class AllocatedMemoryTests
{
private static InProcessReader CreateReader()
{
return new InProcessReader();
}
[Fact]
public void Constructor_allocates_memory_with_execute_read_write_protection()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
Assert.NotEqual(IntPtr.Zero, allocated.BaseAddress);
Assert.Equal(4096, allocated.Size);
}
[Fact]
public void Constructor_with_custom_protection()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096, MemoryProtectionType.ReadOnly);
Assert.NotEqual(IntPtr.Zero, allocated.BaseAddress);
}
[Fact]
public void Constructor_throws_on_negative_size()
{
using var reader = CreateReader();
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
new AllocatedMemory(reader, -1));
Assert.Equal("size", ex.ParamName);
}
[Fact]
public void Constructor_throws_on_zero_size()
{
using var reader = CreateReader();
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
new AllocatedMemory(reader, 0));
Assert.Equal("size", ex.ParamName);
}
[Fact]
public void AddRegion_adds_named_region()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
allocated.AddRegion("test", 100);
Assert.Equal(100, allocated.AddressOf("test") - allocated.BaseAddress);
}
[Fact]
public void AddRegion_throws_on_duplicate_name()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
allocated.AddRegion("test", 100);
var ex = Assert.Throws<ArgumentException>(() =>
allocated.AddRegion("test", 200));
Assert.Contains("already exists", ex.Message);
}
[Fact]
public void AddRegion_throws_on_negative_offset()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
allocated.AddRegion("test", -1));
Assert.Equal("offset", ex.ParamName);
}
[Fact]
public void AddRegion_throws_on_offset_exceeding_size()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
allocated.AddRegion("test", 4096));
Assert.Equal("offset", ex.ParamName);
}
[Fact]
public void AddressOf_returns_correct_address()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
allocated.AddRegion("region1", 0);
allocated.AddRegion("region2", 100);
allocated.AddRegion("region3", 200);
Assert.Equal(allocated.BaseAddress, allocated.AddressOf("region1"));
Assert.Equal(allocated.BaseAddress + 100, allocated.AddressOf("region2"));
Assert.Equal(allocated.BaseAddress + 200, allocated.AddressOf("region3"));
}
[Fact]
public void AddressOf_throws_on_unknown_region()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
var ex = Assert.Throws<ArgumentException>(() =>
allocated.AddressOf("unknown"));
Assert.Contains("does not exist", ex.Message);
}
[Fact]
public void Write_and_Read_int_roundtrip()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
allocated.AddRegion("value", 0);
int original = unchecked((int)0xDEADBEEF);
Assert.True(allocated.Write("value", original));
int read = allocated.Read<int>("value");
Assert.Equal(original, read);
}
[Fact]
public void Write_and_Read_long_roundtrip()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
allocated.AddRegion("value", 8);
long original = 0x123456789ABCDEF0;
Assert.True(allocated.Write("value", original));
long read = allocated.Read<long>("value");
Assert.Equal(original, read);
}
[Fact]
public void WriteBytes_and_ReadBytes_roundtrip()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
allocated.AddRegion("buffer", 0);
byte[] original = { 0x01, 0x02, 0x03, 0x04, 0x05 };
int written = allocated.WriteBytes("buffer", original);
Assert.Equal(original.Length, written);
byte[] read = allocated.ReadBytes("buffer", original.Length);
Assert.Equal(original, read);
}
[Fact]
public void Dispose_frees_memory()
{
using var reader = CreateReader();
var allocated = new AllocatedMemory(reader, 4096);
IntPtr baseAddr = allocated.BaseAddress;
Assert.NotEqual(IntPtr.Zero, baseAddr);
allocated.Dispose();
// After dispose, accessing properties should throw ObjectDisposedException
Assert.Throws<ObjectDisposedException>(() =>
allocated.AddressOf("any"));
}
[Fact]
public void Multiple_regions_independent_access()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
allocated.AddRegion("a", 0);
allocated.AddRegion("b", 4);
allocated.AddRegion("c", 8);
Assert.True(allocated.Write("a", 0x11111111));
Assert.True(allocated.Write("b", 0x22222222));
Assert.True(allocated.Write("c", 0x33333333));
Assert.Equal(0x11111111, allocated.Read<int>("a"));
Assert.Equal(0x22222222, allocated.Read<int>("b"));
Assert.Equal(0x33333333, allocated.Read<int>("c"));
}
[Fact]
public void Write_to_unknown_region_throws()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
var ex = Assert.Throws<ArgumentException>(() =>
allocated.Write("unknown", 42));
Assert.Contains("does not exist", ex.Message);
}
[Fact]
public void Read_from_unknown_region_throws()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 4096);
var ex = Assert.Throws<ArgumentException>(() =>
allocated.Read<int>("unknown"));
Assert.Contains("does not exist", ex.Message);
}
[Fact]
public void Read_T_throws_when_value_exceeds_allocation()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 100);
allocated.AddRegion("boundary", 99);
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
allocated.Read<int>("boundary"));
Assert.Equal("name", ex.ParamName);
}
[Fact]
public void Write_T_throws_when_value_exceeds_allocation()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 100);
allocated.AddRegion("boundary", 99);
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
allocated.Write("boundary", 42));
Assert.Equal("name", ex.ParamName);
}
[Fact]
public void ReadBytes_throws_when_count_exceeds_allocation()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 100);
allocated.AddRegion("boundary", 99);
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
allocated.ReadBytes("boundary", 2));
Assert.Equal("count", ex.ParamName);
}
[Fact]
public void WriteBytes_throws_when_span_exceeds_allocation()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 100);
allocated.AddRegion("boundary", 99);
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
allocated.WriteBytes("boundary", new byte[2]));
Assert.Equal("bytes", ex.ParamName);
}
[Fact]
public void Read_and_Write_at_exact_allocation_boundary_succeed()
{
using var reader = CreateReader();
using var allocated = new AllocatedMemory(reader, 100);
allocated.AddRegion("boundary", 96);
const int expected = unchecked((int)0xDEADBEEF);
Assert.True(allocated.Write("boundary", expected));
Assert.Equal(expected, allocated.Read<int>("boundary"));
}
}