sharpemu/tests/SharpEmu.Libs.Tests/Memory/GuestMemory/GuestSpaceOwnerTests.cs

448 lines
18 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.GuestMemory;
using SharpEmu.HLE.Host;
using Xunit;
using static SharpEmu.Libs.Tests.Memory.HostViews.HostViewTestSupport;
using Op = SharpEmu.Libs.Tests.Memory.GuestMemory.FailingHostViews.Op;
namespace SharpEmu.Libs.Tests.Memory.GuestMemory;
[Collection(GuestMemoryStateCollection.Name)]
public sealed unsafe partial class GuestSpaceOwnerTests
{
private const ulong Page = GuestSpaceOwner.GuestPage;
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ReserveFreeRangeFillsGapsAndRollsBackFailedReservations(bool failSecondGap)
{
if (!Supported) return;
var host = new FailingHostViews(HostViewMemory.Create());
using var owner = new GuestSpaceOwner(host, BackingSize);
var granularity = Math.Max(host.Granularity, Page);
var address = ProbeGuestAddress(host, 5 * granularity);
Assert.True(owner.TryReserveAddressRange(address + 2 * granularity, granularity));
host.Log.Clear();
if (failSecondGap)
{
host.FailNext(Op.ReserveHole, afterCalls: 1);
Assert.False(owner.TryReserveFreeRange(address, 5 * granularity));
Assert.Equal(new[] { Op.ReserveHole, Op.ReserveHole, Op.FreeHole }, host.Log);
Assert.True(owner.ContainsFreeRange(address + 2 * granularity, granularity));
Assert.Equal(address, host.ReserveHole(address, 2 * granularity));
Assert.True(host.FreeHole(address, 2 * granularity));
}
Assert.True(owner.TryReserveFreeRange(address, 5 * granularity));
Assert.True(owner.ContainsFreeRange(address, 5 * granularity));
Assert.True(owner.MapShared(address, 5 * granularity, 0, HostPageProtection.ReadWrite, out _));
*(ulong*)address = Marker;
Assert.Equal(Marker, *(ulong*)owner.AliasBase);
Assert.True(owner.UnmapShared(address, 5 * granularity));
owner.ReleaseAddressRanges();
Assert.Equal(address, host.ReserveHole(address, 5 * granularity));
Assert.True(host.FreeHole(address, 5 * granularity));
}
[Fact]
public void ReserveFreeRangeKeepsLiveMappingsInsideHostAlignmentPadding()
{
if (!Supported) return;
var host = new FailingHostViews(HostViewMemory.Create());
using var owner = new GuestSpaceOwner(host, BackingSize);
var holeSize = HoleSize(host);
var address = ProbeGuestAddress(host, 3 * holeSize);
Assert.True(owner.TryReserveAddressRange(address, holeSize), "The initial reservation failed.");
Assert.True(owner.MapShared(address, Page, 0, HostPageProtection.ReadWrite, out _), "The initial view failed.");
*(ulong*)address = Marker;
host.Log.Clear();
Assert.False(owner.TryReserveFreeRange(address, 3 * holeSize));
Assert.Empty(host.Log);
Assert.True(owner.TryReserveFreeRange(address + Page, 3 * holeSize - Page), "The reservation extension failed.");
Assert.Equal(Marker, *(ulong*)address);
Assert.True(owner.UnmapShared(address, Page), "The view release failed.");
Assert.True(owner.TryReserveFreeRange(address, 3 * holeSize), "The complete free range was not retained.");
}
[Fact]
public void ReserveFreeRangeDoesNotReplaceForeignReservations()
{
if (!Supported) return;
var host = new FailingHostViews(HostViewMemory.Create());
using var owner = new GuestSpaceOwner(host, BackingSize);
var holeSize = HoleSize(host);
var address = ProbeGuestAddress(host, 3 * holeSize);
Assert.True(owner.TryReserveAddressRange(address, holeSize));
var foreignAddress = address + 2 * holeSize;
Assert.Equal(foreignAddress, host.ReserveHole(foreignAddress, holeSize));
try
{
Assert.False(owner.TryReserveFreeRange(address, 3 * holeSize));
Assert.True(owner.ContainsFreeRange(address, holeSize));
Assert.False(owner.ContainsFreeRange(foreignAddress, holeSize));
}
finally
{
Assert.True(host.FreeHole(foreignAddress, holeSize));
}
}
private static ulong AcquireRange(GuestSpaceOwner owner, IHostViewMemory host, ulong size)
{
for (var attempt = 0; attempt < 8; attempt++)
{
var probe = ProbeGuestAddress(host, size);
if (owner.TryReserveAddressRange(probe, size))
{
return probe;
}
}
Assert.Fail("no range could be acquired after 8 attempts");
return 0;
}
[Fact]
public void SelfTest_RoundTripsAPrivatePage()
{
if (!Supported)
{
return;
}
var host = HostViewMemory.Create();
using var owner = new GuestSpaceOwner(host, BackingSize);
var hole = HoleSize(host);
var baseAddress = AcquireRange(owner, host, hole);
var address = owner.FindFreeAddress(baseAddress, baseAddress + hole, Page, Page);
Assert.Equal(baseAddress, address);
Assert.True(owner.AllocatePrivate(address, Page, HostPageProtection.ReadWrite));
*(ulong*)address = Marker;
Assert.Equal(Marker, *(ulong*)address);
Assert.True(owner.FreePrivate(address, Page));
Assert.True(owner.ContainsFreeRange(address, Page));
Assert.True(owner.ContainsFreeRange(baseAddress, hole));
}
[Fact]
public void MapShared_TwoRangesShareOneOffset()
{
if (!Supported)
{
return;
}
var host = HostViewMemory.Create();
using var owner = new GuestSpaceOwner(host, BackingSize);
var hole = HoleSize(host);
var first = AcquireRange(owner, host, hole);
var second = AcquireRange(owner, host, hole);
Assert.True(owner.MapShared(first, Page, 0, HostPageProtection.ReadWrite, out _));
Assert.True(owner.MapShared(second, Page, 0, HostPageProtection.ReadWrite, out _));
*(ulong*)first = Marker;
Assert.Equal(Marker, *(ulong*)second);
Assert.True(owner.IsBacked(first, Page));
Assert.True(owner.UnmapShared(first, Page));
Assert.Equal(Marker, *(ulong*)second);
Assert.False(owner.IsBacked(first, Page));
Assert.True(owner.ContainsFreeRange(first, hole));
Assert.Equal(first, owner.FindFreeAddress(first, first + hole, hole, Page));
Assert.True(owner.UnmapShared(second, Page));
}
[Fact]
public void OwnershipRejections_NeverReachTheHost()
{
if (!Supported)
{
return;
}
var host = new FailingHostViews(HostViewMemory.Create());
using var owner = new GuestSpaceOwner(host, BackingSize);
var hole = HoleSize(host);
var owned = AcquireRange(owner, host, hole);
var foreign = ProbeGuestAddress(host, hole);
Assert.True(owner.MapShared(owned, Page, 0, HostPageProtection.ReadWrite, out _));
Assert.True(owner.AllocatePrivate(owned + Page, Page, HostPageProtection.ReadWrite));
host.Log.Clear();
Assert.False(owner.MapShared(foreign, Page, 0, HostPageProtection.ReadWrite, out var failure));
Assert.Equal(HostViewFailure.AddressUnavailable, failure);
Assert.False(owner.UnmapShared(owned + 2 * Page, Page));
Assert.False(owner.FreePrivate(owned + 2 * Page, Page));
Assert.False(owner.MapShared(owned + Page, Page, 0, HostPageProtection.ReadWrite, out failure));
Assert.Equal(HostViewFailure.AddressUnavailable, failure);
Assert.False(owner.AllocatePrivate(owned, Page, HostPageProtection.ReadWrite));
Assert.False(owner.UnmapShared(owned + Page, Page));
Assert.False(owner.FreePrivate(owned, Page));
Assert.Empty(host.Log);
Assert.True(owner.UnmapShared(owned, Page));
Assert.True(owner.FreePrivate(owned + Page, Page));
}
[Fact]
public void SplitAndCoalesce_FollowTheHoleOperations()
{
if (!Supported)
{
return;
}
var host = new FailingHostViews(HostViewMemory.Create());
using var owner = new GuestSpaceOwner(host, BackingSize);
var hole = HoleSize(host);
var baseAddress = AcquireRange(owner, host, hole);
host.Log.Clear();
Assert.True(owner.MapShared(baseAddress + Page, Page, 0, HostPageProtection.ReadWrite, out _));
Assert.Equal(new[] { Op.SplitHole, Op.SplitHole, Op.MapView }, host.Log);
Assert.True(owner.ContainsFreeRange(baseAddress, Page));
Assert.True(owner.ContainsFreeRange(baseAddress + 2 * Page, hole - 2 * Page));
Assert.False(owner.ContainsFreeRange(baseAddress, hole));
host.Log.Clear();
Assert.True(owner.UnmapShared(baseAddress + Page, Page));
Assert.Equal(new[] { Op.UnmapView, Op.JoinHoles }, host.Log);
Assert.True(owner.ContainsFreeRange(baseAddress, hole));
Assert.Equal(baseAddress, owner.FindFreeAddress(baseAddress, baseAddress + hole, hole, Page));
}
[Fact]
public void SetTransientAccess_TouchesOnlyMappedOverlaps()
{
if (!Supported)
{
return;
}
var host = new FailingHostViews(HostViewMemory.Create());
using var owner = new GuestSpaceOwner(host, BackingSize);
var hole = HoleSize(host);
var baseAddress = AcquireRange(owner, host, hole);
Assert.True(owner.MapShared(baseAddress, Page, 0, HostPageProtection.ReadWrite, out _));
host.Log.Clear();
Assert.True(owner.SetTransientAccess(baseAddress, 2 * Page, HostPageProtection.ReadOnly));
Assert.Equal(new[] { Op.ChangeAccess }, host.Log);
*(ulong*)owner.AliasBase = Marker;
Assert.Equal(Marker, *(ulong*)baseAddress);
host.Log.Clear();
Assert.True(owner.SetTransientAccess(baseAddress + 2 * Page, Page, HostPageProtection.NoAccess));
Assert.Empty(host.Log);
Assert.True(owner.SetAccess(baseAddress, Page, HostPageProtection.ReadWrite));
Assert.True(owner.UnmapShared(baseAddress, Page));
}
[Fact]
public void FailedTransientAccessReleasesLockAndKeepsMapping()
{
if (!Supported) return;
var host = new FailingHostViews(HostViewMemory.Create());
using var owner = new GuestSpaceOwner(host, BackingSize);
var address = AcquireRange(owner, host, HoleSize(host));
Assert.True(owner.MapShared(address, Page, 0, HostPageProtection.ReadWrite, out _));
*(ulong*)address = Marker;
host.FailNext(Op.ChangeAccess);
Assert.False(owner.SetTransientAccess(address, Page, HostPageProtection.NoAccess));
Assert.Equal(Marker, *(ulong*)address);
var protectionApplied = false;
var retry = new Thread(() => protectionApplied = owner.SetTransientAccess(address, Page, HostPageProtection.ReadOnly))
{
IsBackground = true,
};
retry.Start();
Assert.True(retry.Join(TimeSpan.FromSeconds(10)), "The failed protection call retained the owner lock.");
Assert.True(protectionApplied);
Assert.Equal(Marker, *(ulong*)address);
Assert.True(owner.SetTransientAccess(address, Page, HostPageProtection.ReadWrite));
Assert.True(owner.UnmapShared(address, Page));
}
[Fact]
public void FreePrivate_AcrossTwoAllocationsReleasesBoth()
{
if (!Supported)
{
return;
}
var host = new FailingHostViews(HostViewMemory.Create());
using var owner = new GuestSpaceOwner(host, BackingSize);
var hole = HoleSize(host);
var baseAddress = AcquireRange(owner, host, hole);
Assert.True(owner.AllocatePrivate(baseAddress, Page, HostPageProtection.ReadWrite));
Assert.True(owner.AllocatePrivate(baseAddress + Page, Page, HostPageProtection.ReadWrite));
host.Log.Clear();
Assert.True(owner.FreePrivate(baseAddress, 2 * Page));
Assert.Equal(2, host.Log.Count(op => op == Op.ReleasePrivate));
Assert.Contains(Op.JoinHoles, host.Log);
Assert.True(owner.ContainsFreeRange(baseAddress, hole));
Assert.Equal(baseAddress, owner.FindFreeAddress(baseAddress, baseAddress + hole, hole, Page));
}
[Fact]
public void InjectedHostFailures_ReturnRangesToFree()
{
if (!Supported)
{
return;
}
var host = new FailingHostViews(HostViewMemory.Create());
using var owner = new GuestSpaceOwner(host, BackingSize);
var hole = HoleSize(host);
var baseAddress = AcquireRange(owner, host, hole);
host.FailNext(Op.MapView, failure: HostViewFailure.PlaceholderMapFailed);
Assert.False(owner.MapShared(baseAddress, Page, 0, HostPageProtection.ReadWrite, out var failure));
Assert.Equal(HostViewFailure.PlaceholderMapFailed, failure);
Assert.True(owner.ContainsFreeRange(baseAddress, hole));
Assert.False(owner.IsBacked(baseAddress, Page));
host.FailNext(Op.MapView, failure: HostViewFailure.ProtectFailed);
Assert.False(owner.MapShared(baseAddress, Page, 0, HostPageProtection.ReadWrite, out failure));
Assert.Equal(HostViewFailure.ProtectFailed, failure);
Assert.True(owner.ContainsFreeRange(baseAddress, hole));
host.FailNext(Op.CommitPrivate);
Assert.False(owner.AllocatePrivate(baseAddress, Page, HostPageProtection.ReadWrite));
Assert.True(owner.ContainsFreeRange(baseAddress, hole));
Assert.True(owner.MapShared(baseAddress, Page, 0, HostPageProtection.ReadWrite, out _));
host.FailNext(Op.UnmapView);
Assert.False(owner.UnmapShared(baseAddress, Page));
Assert.True(owner.IsBacked(baseAddress, Page));
Assert.False(owner.ContainsFreeRange(baseAddress, Page));
Assert.True(owner.UnmapShared(baseAddress, Page));
}
[Fact]
public void AlignmentAndOverflow_AreRejectedWithoutHostCalls()
{
if (!Supported)
{
return;
}
var host = new FailingHostViews(HostViewMemory.Create());
using var owner = new GuestSpaceOwner(host, BackingSize);
var hole = HoleSize(host);
var baseAddress = AcquireRange(owner, host, hole);
host.Log.Clear();
foreach (var (address, size) in new[]
{
(baseAddress + 0x1000, Page),
(baseAddress, Page + 0x1000),
(baseAddress, 0UL),
(ulong.MaxValue - Page + 1, 2 * Page),
})
{
Assert.False(owner.MapShared(address, size, 0, HostPageProtection.ReadWrite, out _));
Assert.False(owner.UnmapShared(address, size));
Assert.False(owner.AllocatePrivate(address, size, HostPageProtection.ReadWrite));
Assert.False(owner.FreePrivate(address, size));
Assert.False(owner.ContainsFreeRange(address, size));
}
// Transient access works on host pages; it still rejects misaligned, empty and wrapping ranges.
Assert.False(owner.SetTransientAccess(baseAddress + 0x800, 0x1000, HostPageProtection.ReadWrite));
Assert.False(owner.SetTransientAccess(baseAddress, 0, HostPageProtection.ReadWrite));
Assert.False(owner.SetTransientAccess(ulong.MaxValue - 0x1000 + 1, 0x2000, HostPageProtection.ReadWrite));
Assert.True(owner.SetTransientAccess(baseAddress + host.PageSize, host.PageSize, HostPageProtection.ReadWrite));
Assert.False(owner.TryReserveAddressRange(baseAddress + Page, hole));
Assert.False(owner.TryReserveAddressRange(baseAddress, hole + Page));
Assert.False(owner.MapShared(baseAddress, Page, 0x1000, HostPageProtection.ReadWrite, out var failure));
Assert.Equal(HostViewFailure.OffsetOutOfBounds, failure);
Assert.False(owner.MapShared(baseAddress, Page, BackingSize, HostPageProtection.ReadWrite, out failure));
Assert.Equal(HostViewFailure.OffsetOutOfBounds, failure);
Assert.Empty(host.Log);
}
[Fact]
public void Dispose_ReleasesViewsPrivatesAndHoles()
{
if (!Supported)
{
return;
}
var host = HostViewMemory.Create();
var owner = new GuestSpaceOwner(host, BackingSize);
var hole = HoleSize(host);
var baseAddress = AcquireRange(owner, host, hole);
Assert.True(owner.MapShared(baseAddress, Page, 0, HostPageProtection.ReadWrite, out _));
Assert.True(owner.AllocatePrivate(baseAddress + Page, Page, HostPageProtection.ReadWrite));
owner.Dispose();
owner.Dispose();
Assert.False(owner.TryReserveAddressRange(baseAddress, hole));
Assert.False(owner.MapShared(baseAddress, Page, 0, HostPageProtection.ReadWrite, out _));
using var fresh = new GuestSpaceOwner(host, BackingSize);
Assert.True(fresh.TryReserveAddressRange(baseAddress, hole));
Assert.True(fresh.ContainsFreeRange(baseAddress, hole));
}
[Fact]
public void FailedCoalescing_IsStillReleasedCompletelyAtDisposal()
{
if (!Supported)
{
return;
}
var raw = HostViewMemory.Create();
var host = new FailingHostViews(raw);
var owner = new GuestSpaceOwner(host, BackingSize);
var hole = HoleSize(host);
var baseAddress = AcquireRange(owner, host, hole);
Assert.True(owner.MapShared(baseAddress + Page, Page, 0, HostPageProtection.ReadWrite, out _));
host.FailNext(Op.JoinHoles);
Assert.True(owner.UnmapShared(baseAddress + Page, Page));
Assert.True(owner.ContainsFreeRange(baseAddress + Page, Page));
Assert.False(owner.ContainsFreeRange(baseAddress, hole));
owner.Dispose();
Assert.Equal(baseAddress, raw.ReserveHole(baseAddress, hole));
Assert.True(raw.FreeHole(baseAddress, hole));
}
[Fact]
public void ReleaseAddressRanges_ReservesTheGuestAddressSpaceAgain()
{
if (!Supported)
{
return;
}
const ulong start = 0x10_0000_0000;
const ulong end = 0xFC_0000_0000;
const ulong size = 0x10_0000_0000;
const ulong alignment = 0x20_0000;
using var owner = new GuestSpaceOwner(HostViewMemory.Create(), BackingSize, preReserveGuestAddressSpace: true);
Assert.NotEqual(0UL, owner.FindFreeAddress(start, end, size, alignment));
owner.ReleaseAddressRanges();
var address = owner.FindFreeAddress(start, end, size, alignment);
Assert.NotEqual(0UL, address);
Assert.True(owner.ContainsFreeRange(address, size));
}
}