sharpemu/tests/SharpEmu.Libs.Tests/Memory/HostViews/HostViewMemoryTests.cs

352 lines
12 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE.Host;
using SharpEmu.HLE.Host.Posix;
using SharpEmu.HLE.Host.Windows;
using Xunit;
using static SharpEmu.Libs.Tests.Memory.HostViews.HostViewTestSupport;
namespace SharpEmu.Libs.Tests.Memory.HostViews;
[CollectionDefinition(HostViewStateCollection.Name, DisableParallelization = true)]
public sealed class HostViewStateCollection
{
public const string Name = "HostViewState";
}
[Collection(HostViewStateCollection.Name)]
public sealed unsafe class HostViewMemoryTests
{
private const uint MEM_COMMIT = 0x1000;
private const uint MEM_RESERVE = 0x2000;
private static uint QueryState(ulong address)
{
Assert.NotEqual((nuint)0, VirtualQuery((void*)address, out var info, (nuint)sizeof(MemoryBasicInformation)));
return info.State;
}
[Fact]
public void CommitWriteRelease_RoundTripsThroughAPlaceholder()
{
if (!Supported)
{
return;
}
var views = HostViewMemory.Create();
var hole = HoleSize(views);
var baseAddress = ReserveFreeHole(views, hole);
Assert.True(views.SplitHole(baseAddress, Segment));
Assert.True(views.CommitPrivate(baseAddress, Segment, HostPageProtection.ReadWrite));
*(ulong*)baseAddress = Marker;
Assert.Equal(Marker, *(ulong*)baseAddress);
Assert.True(views.ReleasePrivate(baseAddress, Segment));
if (OperatingSystem.IsWindows())
{
Assert.Equal(MEM_RESERVE, QueryState(baseAddress));
}
Assert.True(views.CommitPrivate(baseAddress, Segment, HostPageProtection.ReadWrite));
Assert.True(views.ReleasePrivate(baseAddress, Segment));
Assert.True(views.JoinHoles(baseAddress, hole));
Assert.True(views.FreeHole(baseAddress, hole));
}
[Fact]
public void SubGranularityView_SharesBytesWithTheAlias()
{
if (!Supported)
{
return;
}
var views = HostViewMemory.Create();
using var backing = CreateBacking(views);
var hole = HoleSize(views);
var baseAddress = ReserveFreeHole(views, hole);
var viewAddress = baseAddress + Segment;
Assert.True(views.SplitHole(baseAddress, Segment));
Assert.True(views.SplitHole(viewAddress, Segment));
Assert.True(views.TryMapView(backing, viewAddress, Segment, Segment, HostPageProtection.ReadWrite, out var failure));
Assert.Equal(HostViewFailure.None, failure);
*(ulong*)viewAddress = Marker;
Assert.Equal(Marker, *(ulong*)(backing.AliasBase + Segment));
*(ulong*)(backing.AliasBase + Segment + 8) = ~Marker;
Assert.Equal(~Marker, *(ulong*)(viewAddress + 8));
new Span<byte>((void*)viewAddress, (int)Segment).Clear();
Assert.Equal(0UL, *(ulong*)(backing.AliasBase + Segment));
Assert.True(views.UnmapView(viewAddress, Segment));
Assert.True(views.JoinHoles(baseAddress, hole));
Assert.True(views.FreeHole(baseAddress, hole));
}
[Fact]
public void AliasWrites_AreVisibleThroughANoAccessThenReadOnlyView()
{
if (!Supported)
{
return;
}
var views = HostViewMemory.Create();
using var backing = CreateBacking(views);
var hole = HoleSize(views);
var baseAddress = ReserveFreeHole(views, hole);
Assert.True(views.SplitHole(baseAddress, Segment));
Assert.True(views.TryMapView(backing, baseAddress, 0, Segment, HostPageProtection.NoAccess, out _));
*(ulong*)backing.AliasBase = Marker;
Assert.True(views.UnmapView(baseAddress, Segment));
Assert.True(views.TryMapView(backing, baseAddress, 0, Segment, HostPageProtection.ReadOnly, out _));
Assert.Equal(Marker, *(ulong*)baseAddress);
Assert.True(views.UnmapView(baseAddress, Segment));
Assert.True(views.JoinHoles(baseAddress, hole));
Assert.True(views.FreeHole(baseAddress, hole));
}
[Fact]
public void ChangeAccess_ReadOnlyViewStillSeesAliasWrites()
{
if (!Supported)
{
return;
}
var views = HostViewMemory.Create();
using var backing = CreateBacking(views);
var hole = HoleSize(views);
var baseAddress = ReserveFreeHole(views, hole);
Assert.True(views.SplitHole(baseAddress, Segment));
Assert.True(views.TryMapView(backing, baseAddress, 0, Segment, HostPageProtection.ReadWrite, out _));
Assert.True(views.ChangeAccess(baseAddress, Segment, HostPageProtection.ReadOnly));
*(ulong*)backing.AliasBase = Marker;
Assert.Equal(Marker, *(ulong*)baseAddress);
Assert.True(views.ChangeAccess(baseAddress, Segment, HostPageProtection.ReadWrite));
*(ulong*)baseAddress = ~Marker;
Assert.Equal(~Marker, *(ulong*)backing.AliasBase);
Assert.True(views.UnmapView(baseAddress, Segment));
Assert.True(views.JoinHoles(baseAddress, hole));
Assert.True(views.FreeHole(baseAddress, hole));
}
[Fact]
public void TwoViewsOfOneOffset_ShareBytesAndSurviveEachOther()
{
if (!Supported)
{
return;
}
var views = HostViewMemory.Create();
using var backing = CreateBacking(views);
var hole = HoleSize(views);
var first = ReserveFreeHole(views, hole);
var second = ReserveFreeHole(views, hole);
Assert.True(views.SplitHole(first, Segment));
Assert.True(views.SplitHole(second, Segment));
Assert.True(views.TryMapView(backing, first, 0, Segment, HostPageProtection.ReadWrite, out _));
Assert.True(views.TryMapView(backing, second, 0, Segment, HostPageProtection.ReadWrite, out _));
*(ulong*)first = Marker;
Assert.Equal(Marker, *(ulong*)second);
Assert.True(views.UnmapView(first, Segment));
Assert.Equal(Marker, *(ulong*)second);
Assert.True(views.UnmapView(second, Segment));
Assert.True(views.JoinHoles(first, hole));
Assert.True(views.JoinHoles(second, hole));
Assert.True(views.FreeHole(first, hole));
Assert.True(views.FreeHole(second, hole));
}
[Fact]
public void OccupiedAddress_IsRejectedByWindowsPlaceholders()
{
if (!OperatingSystem.IsWindows())
{
return;
}
var views = HostViewMemory.Create();
using var backing = CreateBacking(views);
var memory = PlatformMemory;
var hole = HoleSize(views);
var probe = ProbeFreeAddress(views, hole);
var occupied = memory.Allocate(probe, hole, HostPageProtection.ReadWrite);
Assert.Equal(probe, occupied);
*(ulong*)occupied = Marker;
Assert.Equal(0UL, views.ReserveHole(occupied, hole));
Assert.False(views.TryMapView(backing, occupied, 0, Segment, HostPageProtection.ReadWrite, out var failure));
Assert.Equal(HostViewFailure.PlaceholderMapFailed, failure);
Assert.Equal(Marker, *(ulong*)occupied);
Assert.Equal(MEM_COMMIT, QueryState(occupied));
Assert.True(memory.Free(occupied));
}
[Fact]
public void ProtectFailure_PreservesThePlaceholderOnWindows()
{
if (!OperatingSystem.IsWindows())
{
return;
}
var views = HostViewMemory.Create();
using var backing = CreateBacking(views);
var hole = HoleSize(views);
var baseAddress = ReserveFreeHole(views, hole);
Assert.True(views.SplitHole(baseAddress, Segment));
WindowsHostViews.FailProtectForTests = true;
try
{
Assert.False(views.TryMapView(backing, baseAddress, 0, Segment, HostPageProtection.ReadWrite, out var failure));
Assert.Equal(HostViewFailure.ProtectFailed, failure);
}
finally
{
WindowsHostViews.FailProtectForTests = false;
}
Assert.Equal(MEM_RESERVE, QueryState(baseAddress));
Assert.True(views.TryMapView(backing, baseAddress, 0, Segment, HostPageProtection.ReadWrite, out _));
Assert.True(views.UnmapView(baseAddress, Segment));
Assert.True(views.JoinHoles(baseAddress, hole));
Assert.True(views.FreeHole(baseAddress, hole));
}
[Fact]
public void InvalidRanges_AreRejectedBeforeAnyHostCall()
{
if (!Supported)
{
return;
}
var views = HostViewMemory.Create();
Assert.False(views.TryCreateBacking(0, out var none, out var createFailure));
Assert.Null(none);
Assert.Equal(HostViewFailure.BackingUnavailable, createFailure);
using var backing = CreateBacking(views);
var hole = HoleSize(views);
var baseAddress = ReserveFreeHole(views, hole);
var page = views.PageSize;
Assert.Equal(HostViewFailure.OffsetOutOfBounds, MapFailure(views, backing, baseAddress, 0, 0));
Assert.Equal(HostViewFailure.OffsetOutOfBounds, MapFailure(views, backing, baseAddress, BackingSize, page));
Assert.Equal(HostViewFailure.OffsetOutOfBounds, MapFailure(views, backing, baseAddress, 0, BackingSize + page));
Assert.Equal(HostViewFailure.OffsetOutOfBounds, MapFailure(views, backing, baseAddress, BackingSize - page, 2 * page));
Assert.Equal(HostViewFailure.OffsetOutOfBounds, MapFailure(views, backing, baseAddress, ulong.MaxValue - 0xFFF, 2 * page));
Assert.Equal(HostViewFailure.OffsetOutOfBounds, MapFailure(views, backing, baseAddress, 0x800, page));
Assert.Equal(HostViewFailure.WrongHostAddress, MapFailure(views, backing, baseAddress + 1, 0, page));
Assert.Equal(HostViewFailure.WrongHostAddress, MapFailure(views, backing, 0, 0, page));
Assert.Equal(0UL, views.ReserveHole(0, hole));
Assert.Equal(0UL, views.ReserveHole(ulong.MaxValue - 0xFFF, 0x10000));
Assert.True(views.FreeHole(baseAddress, hole));
}
[Fact]
public void InjectedAliasFailure_ReleasesThePartialObject()
{
if (!Supported)
{
return;
}
var views = HostViewMemory.Create();
SetAliasFailure(true);
try
{
Assert.False(views.TryCreateBacking(BackingSize, out var backing, out var failure));
Assert.Null(backing);
Assert.Equal(HostViewFailure.BackingUnavailable, failure);
}
finally
{
SetAliasFailure(false);
}
using var recovered = CreateBacking(views);
Assert.Equal(BackingSize, recovered.Size);
}
[Fact]
public void Dispose_IsIdempotentAndBlocksLaterMaps()
{
if (!Supported)
{
return;
}
var views = HostViewMemory.Create();
var backing = CreateBacking(views);
var hole = HoleSize(views);
var baseAddress = ReserveFreeHole(views, hole);
Assert.True(views.SplitHole(baseAddress, Segment));
Assert.True(views.TryMapView(backing, baseAddress, 0, Segment, HostPageProtection.ReadWrite, out _));
*(ulong*)baseAddress = Marker;
Assert.Equal(Marker, *(ulong*)backing.AliasBase);
Assert.True(views.UnmapView(baseAddress, Segment));
backing.Dispose();
backing.Dispose();
Assert.False(views.TryMapView(backing, baseAddress, 0, Segment, HostPageProtection.ReadWrite, out var failure));
Assert.Equal(HostViewFailure.BackingUnavailable, failure);
Assert.True(views.JoinHoles(baseAddress, hole));
Assert.True(views.FreeHole(baseAddress, hole));
}
private static HostViewFailure MapFailure(IHostViewMemory views, HostBackingObject backing, ulong address, ulong offset, ulong size)
{
Assert.False(views.TryMapView(backing, address, offset, size, HostPageProtection.ReadWrite, out var failure));
return failure;
}
private static void SetAliasFailure(bool fail)
{
if (OperatingSystem.IsWindows())
{
WindowsHostViews.FailAliasMapForTests = fail;
}
else
{
PosixHostViews.FailAliasMapForTests = fail;
}
}
[StructLayout(LayoutKind.Sequential)]
private struct MemoryBasicInformation
{
public ulong BaseAddress;
public ulong AllocationBase;
public uint AllocationProtect;
public uint Alignment1;
public ulong RegionSize;
public uint State;
public uint Protect;
public uint Type;
public uint Alignment2;
}
[DllImport("kernel32.dll")]
private static extern nuint VirtualQuery(void* address, out MemoryBasicInformation info, nuint length);
}