[Tests] Add VirtualMemory and PhysicalVirtualMemory edge case tests (#266)

VirtualMemory: 4 new tests covering unmapped address returns false,
zero-length operations, page-boundary access, and cross-gap failures.

PhysicalVirtualMemory: 5 new tests covering lazy commit on demand,
reserve-only GetPointer, unmapped GetPointer returns null, free-list
first-fit reuse, and coalescing both neighbours on middle-range free.

Total: 9 new tests, 18 passed (8 VirtualMemory + 5 PhysicalVirtualMemory
+ 5 GuestMemoryAllocator). No production code changes.
This commit is contained in:
kostyaff
2026-07-17 03:13:38 +03:00
committed by GitHub
parent 3585519007
commit 7494792249
2 changed files with 299 additions and 0 deletions
@@ -0,0 +1,229 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Memory;
using SharpEmu.Core.Loader;
using SharpEmu.HLE.Host;
using Xunit;
namespace SharpEmu.Libs.Tests.Memory;
// PhysicalVirtualMemory is the host-backed (identity-mapped) implementation.
// Reserve-only regions (> 4 GiB, non-executable) defer commit until first
// access; TryAllocateGuestMemory serves a first-fit free-list with coalescing.
// These tests pin that behaviour through fake IHostMemory implementations.
public sealed class PhysicalVirtualMemoryTests
{
// 1. Lazy commit: a reserve-only region has its pages committed on demand
// when read; freshly committed pages read as zero.
[Fact]
public void LazyReadCommitsPageOnDemandAndReadsZero()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
// > 4 GiB, non-executable -> reserve-only with lazy commit.
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
Assert.NotEqual(0UL, address);
// Discard the priming commits AllocateAt issues up front; we want to
// observe the on-demand commit triggered by the read itself.
host.CommitCalls.Clear();
var buffer = new byte[1];
Assert.True(memory.TryRead(address, buffer));
Assert.Equal(0, buffer[0]);
// The touched page (page-aligned to `address`) was committed on demand.
var page = address & ~0xFFFUL;
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
}
// 2. Reserve-only region: GetPointer commits the page before returning it,
// so callers receive a valid (non-null) pointer. An unmapped address yields null.
[Fact]
public unsafe void GetPointerOnReserveOnlyRegionCommitsAndReturnsValidPointer()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
host.CommitCalls.Clear();
var pointer = memory.GetPointer(address + 0x123);
Assert.NotEqual(0UL, (ulong)pointer);
Assert.Equal(address + 0x123, (ulong)pointer);
var page = (address + 0x123) & ~0xFFFUL;
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
}
[Fact]
public unsafe void GetPointerOnUnmappedAddressReturnsNull()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
Assert.Equal(0UL, (ulong)memory.GetPointer(0x0001_0000));
}
// 3. Free-list reuse: a freed range is served back by first-fit allocation,
// preferring the lowest fitting free range over the larger trailing span.
[Fact]
public void FreedRangeIsReusedByFirstFitAllocation()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var second));
Assert.NotEqual(first, second);
Assert.True(memory.TryFreeGuestMemory(first));
// A smaller allocation must reuse first's freed slot (lowest fitting range),
// not the larger trailing free range.
Assert.True(memory.TryAllocateGuestMemory(0x2000, 0x1000, out var reused));
Assert.Equal(first, reused);
}
// 4. Coalescing: freeing the middle of three adjacent ranges merges both the
// left and right free neighbours in a single TryFreeGuestMemory call,
// restoring the full span for subsequent first-fit reuse.
[Fact]
public void FreeingMiddleRangeCoalescesBothNeighbours()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
// Three adjacent 0x1000 allocations: offsets 0x1000, 0x2000, 0x3000.
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var first));
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var second));
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var third));
// Free the outer ranges first, leaving two separate free ranges.
Assert.True(memory.TryFreeGuestMemory(first));
Assert.True(memory.TryFreeGuestMemory(third));
// Freeing the middle range must coalesce both neighbours at once.
Assert.True(memory.TryFreeGuestMemory(second));
// The whole arena is now one coalesced free range; a full-arena allocation
// reuses first's base address.
Assert.True(memory.TryAllocateGuestMemory(0x000F_F000, 0x1000, out var coalesced));
Assert.Equal(first, coalesced);
}
/// <summary>
/// Host memory backed by a single real, zero-initialised page. Reserve/Allocate
/// report the page-aligned buffer address so lazy-commit read paths can actually
/// dereference the returned pointer. Query always reports Reserved, so
/// EnsureRangeCommitted issues a Commit on first access.
/// </summary>
private sealed unsafe class LazyZeroedHostMemory : IHostMemory, IDisposable
{
private readonly void* _allocation;
private readonly ulong _address;
private bool _freed;
public LazyZeroedHostMemory()
{
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x2000);
_address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL;
}
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
public bool Commit(ulong address, ulong size, HostPageProtection protection)
{
CommitCalls.Add((address, size, protection));
return true;
}
public bool Free(ulong address)
{
// The real buffer is released in Dispose; keep Free a no-op so
// PhysicalVirtualMemory.Clear does not double-free it.
return true;
}
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong address, out HostRegionInfo info)
{
var pageAddress = address & ~0xFFFUL;
info = new HostRegionInfo(
pageAddress,
pageAddress,
0x1000,
HostRegionState.Reserved,
0,
HostPageProtection.NoAccess,
0,
0);
return true;
}
public void FlushInstructionCache(ulong address, ulong size)
{
}
public void Dispose()
{
if (!_freed)
{
System.Runtime.InteropServices.NativeMemory.Free(_allocation);
_freed = true;
}
}
}
// Minimal host memory for free-list tests: Allocate honours the desired
// address (or a fallback), everything else succeeds as a no-op. The guest
// allocation arena never dereferences, so no real backing is required.
private sealed class FakeHostMemory : IHostMemory
{
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
desiredAddress != 0 ? desiredAddress : 0x00007000_0000_0000;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
Allocate(desiredAddress, size, protection);
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
public bool Free(ulong address) => true;
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong address, out HostRegionInfo info)
{
info = default;
return false;
}
public void FlushInstructionCache(ulong address, ulong size)
{
}
}
}
@@ -84,4 +84,74 @@ public sealed class VirtualMemoryTests
Assert.False(memory.TryRead(0x3000, unchanged)); Assert.False(memory.TryRead(0x3000, unchanged));
Assert.True(memory.TryWrite(0x3000, [9])); Assert.True(memory.TryWrite(0x3000, [9]));
} }
[Fact]
public void TryReadAndTryWriteOnUnmappedAddressReturnFalse()
{
var memory = new VirtualMemory();
memory.Map(0x1000, 0x100, 0, [], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
// Addresses with no covering region fail for both reads and writes.
Span<byte> value = stackalloc byte[1];
Assert.False(memory.TryRead(0x2000, value));
Assert.False(memory.TryWrite(0x2000, value));
// With no regions mapped at all, every access fails.
var empty = new VirtualMemory();
Assert.False(empty.TryRead(0x1000, value));
Assert.False(empty.TryWrite(0x1000, value));
}
// Zero-length operations succeed on a mapped address and touch nothing, but
// still require a mapped address.
[Fact]
public void ZeroLengthOperationsOnMappedAddressReturnTrue()
{
var memory = new VirtualMemory();
memory.Map(0x1000, 0x100, 0, [0x01], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
Assert.True(memory.TryRead(0x1000, []));
Assert.True(memory.TryWrite(0x1000, []));
Span<byte> value = stackalloc byte[1];
Assert.True(memory.TryRead(0x1000, value));
Assert.Equal(0x01, value[0]);
Assert.False(memory.TryRead(0x2000, []));
Assert.False(memory.TryWrite(0x2000, []));
}
// A page-aligned region must be accessible at both offset 0 and the final byte.
[Fact]
public void MappingAtPageBoundarySupportsOffsetZeroAndLastByte()
{
const ulong pageSize = 0x1000;
var memory = new VirtualMemory();
memory.Map(pageSize, pageSize, 0, [], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
Assert.True(memory.TryWrite(pageSize, [0x5A]));
Assert.True(memory.TryWrite(pageSize + pageSize - 1, [0xA5]));
Span<byte> head = stackalloc byte[1];
Span<byte> tail = stackalloc byte[1];
Assert.True(memory.TryRead(pageSize, head));
Assert.True(memory.TryRead(pageSize + pageSize - 1, tail));
Assert.Equal(0x5A, head[0]);
Assert.Equal(0xA5, tail[0]);
}
// A read/write that starts in one region but extends into the unmapped gap
// between two non-adjacent regions must fail across the entire range.
[Fact]
public void ReadWriteAcrossGapBetweenNonAdjacentRegionsReturnsFalse()
{
const ulong pageSize = 0x1000;
var memory = new VirtualMemory();
const ProgramHeaderFlags protection = ProgramHeaderFlags.Read | ProgramHeaderFlags.Write;
memory.Map(pageSize, pageSize, 0, [], protection);
memory.Map(pageSize * 3, pageSize, 0, [], protection);
Assert.False(memory.TryRead(pageSize, new byte[(int)pageSize * 2]));
Assert.False(memory.TryWrite(pageSize, new byte[(int)pageSize * 2]));
}
} }