268 lines
12 KiB
C#
268 lines
12 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.HLE.GuestMemory;
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.HLE.GpuMemory;
|
|
using SharpEmu.HLE.Host;
|
|
using Xunit;
|
|
using static SharpEmu.Libs.Tests.Memory.HostViews.HostViewTestSupport;
|
|
|
|
namespace SharpEmu.Libs.Tests.Memory.GuestMemory;
|
|
|
|
public sealed unsafe partial class GuestSpaceOwnerTests
|
|
{
|
|
private const ulong ProtectionBlockBytes = 4 * 1024 * 1024;
|
|
|
|
[Fact]
|
|
public void ProtectionWaitsUntilAMappingIsPublished()
|
|
{
|
|
if (!Supported) return;
|
|
var host = new FailingHostViews(HostViewMemory.Create());
|
|
using var owner = new GuestSpaceOwner(host, BackingSize);
|
|
var address = AcquireRange(owner, host, HoleSize(host));
|
|
WithProtectionPaused(host, address,
|
|
() => Assert.True(owner.MapShared(address, Page, 0, HostPageProtection.ReadWrite, out _)),
|
|
() => Assert.True(owner.SetTransientAccess(address, Page, HostPageProtection.ReadOnly)),
|
|
mustCompleteWhilePaused: false, pauseMapping: true);
|
|
Assert.Contains(FailingHostViews.Op.ChangeAccess, host.Log);
|
|
Assert.True(owner.SetAccess(address, Page, HostPageProtection.ReadWrite));
|
|
*(ulong*)address = Marker;
|
|
Assert.Equal(Marker, *(ulong*)owner.AliasBase);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void ProtectionOrdersLockCollisionsAndFullLockCoverage(bool coverAllLocks)
|
|
{
|
|
if (!Supported) return;
|
|
var host = new FailingHostViews(HostViewMemory.Create());
|
|
using var owner = new GuestSpaceOwner(host, BackingSize);
|
|
var address = AcquireRange(owner, host, 258 * ProtectionBlockBytes);
|
|
var otherAddress = address + (coverAllLocks ? 128UL : 256UL) * ProtectionBlockBytes;
|
|
Assert.True(owner.MapShared(address, Page, 0, HostPageProtection.ReadWrite, out _));
|
|
Assert.True(owner.MapShared(otherAddress, Page, Page, HostPageProtection.ReadWrite, out _));
|
|
var size = coverAllLocks ? 257 * ProtectionBlockBytes : Page;
|
|
WithProtectionPaused(host, address,
|
|
() => Assert.True(owner.SetTransientAccess(address, size, HostPageProtection.ReadOnly)),
|
|
() => Assert.True(owner.SetTransientAccess(otherAddress, Page, HostPageProtection.ReadWrite)),
|
|
mustCompleteWhilePaused: false);
|
|
*(ulong*)otherAddress = Marker;
|
|
Assert.Equal(Marker, *(ulong*)(owner.AliasBase + Page));
|
|
}
|
|
|
|
[Fact]
|
|
public void TransientProtectionAllowsIndependentBlocksToProgress()
|
|
{
|
|
if (!Supported) return;
|
|
var host = new FailingHostViews(HostViewMemory.Create());
|
|
using var owner = new GuestSpaceOwner(host, BackingSize);
|
|
var address = AcquireRange(owner, host, 2 * ProtectionBlockBytes);
|
|
var otherAddress = address + ProtectionBlockBytes;
|
|
Assert.True(owner.MapShared(address, Page, 0, HostPageProtection.ReadWrite, out _));
|
|
Assert.True(owner.MapShared(otherAddress, Page, Page, HostPageProtection.ReadWrite, out _));
|
|
|
|
WithProtectionPaused(host, address,
|
|
() => Assert.True(owner.SetTransientAccess(address, Page, HostPageProtection.ReadOnly)),
|
|
() => Assert.True(owner.SetTransientAccess(otherAddress, Page, HostPageProtection.ReadOnly)),
|
|
mustCompleteWhilePaused: true);
|
|
Assert.True(owner.SetAccess(address, Page, HostPageProtection.ReadWrite));
|
|
Assert.True(owner.SetAccess(otherAddress, Page, HostPageProtection.ReadWrite));
|
|
*(ulong*)address = Marker;
|
|
*(ulong*)otherAddress = Marker + 1;
|
|
Assert.Equal(Marker, *(ulong*)owner.AliasBase);
|
|
Assert.Equal(Marker + 1, *(ulong*)(owner.AliasBase + Page));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void OverlappingProtectionWaitsForTheWholeRange(bool wrapsLockIndices)
|
|
{
|
|
if (!Supported) return;
|
|
var host = new FailingHostViews(HostViewMemory.Create());
|
|
using var owner = new GuestSpaceOwner(host, BackingSize);
|
|
var reservationSize = wrapsLockIndices ? 258 * ProtectionBlockBytes : 3 * ProtectionBlockBytes;
|
|
var reservation = AcquireRange(owner, host, reservationSize);
|
|
var boundary = wrapsLockIndices
|
|
? AlignUp(reservation + Page, 256 * ProtectionBlockBytes)
|
|
: AlignUp(reservation + Page, ProtectionBlockBytes);
|
|
var address = boundary - Page;
|
|
Assert.True(owner.MapShared(address, Page, 0, HostPageProtection.ReadWrite, out _));
|
|
Assert.True(owner.MapShared(boundary, Page, Page, HostPageProtection.ReadWrite, out _));
|
|
WithProtectionPaused(host, address,
|
|
() => Assert.True(owner.SetTransientAccess(address, 2 * Page, HostPageProtection.ReadOnly)),
|
|
() => Assert.True(owner.SetTransientAccess(boundary, Page, HostPageProtection.ReadWrite)),
|
|
mustCompleteWhilePaused: false);
|
|
*(ulong*)boundary = Marker;
|
|
Assert.Equal(Marker, *(ulong*)(owner.AliasBase + Page));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("unmap")]
|
|
[InlineData("free")]
|
|
[InlineData("release")]
|
|
[InlineData("dispose")]
|
|
[InlineData("map")]
|
|
[InlineData("allocate")]
|
|
[InlineData("access")]
|
|
public void MappingChangesWaitForActiveProtection(string operation)
|
|
{
|
|
if (!Supported) return;
|
|
var host = new FailingHostViews(HostViewMemory.Create());
|
|
using var owner = new GuestSpaceOwner(host, BackingSize);
|
|
var address = AcquireRange(owner, host, HoleSize(host));
|
|
if (operation == "free")
|
|
Assert.True(owner.AllocatePrivate(address, Page, HostPageProtection.ReadWrite));
|
|
else
|
|
Assert.True(owner.MapShared(address, Page, 0, HostPageProtection.ReadWrite, out _));
|
|
|
|
WithProtectionPaused(host, address,
|
|
() => Assert.True(owner.SetTransientAccess(address, Page, HostPageProtection.ReadOnly)),
|
|
() =>
|
|
{
|
|
switch (operation)
|
|
{
|
|
case "unmap": Assert.True(owner.UnmapShared(address, Page)); break;
|
|
case "free": Assert.True(owner.FreePrivate(address, Page)); break;
|
|
case "release": owner.ReleaseAddressRanges(); break;
|
|
case "dispose": owner.Dispose(); break;
|
|
case "map": Assert.True(owner.MapShared(address + Page, Page, Page, HostPageProtection.ReadWrite, out _)); break;
|
|
case "allocate": Assert.True(owner.AllocatePrivate(address + Page, Page, HostPageProtection.ReadWrite)); break;
|
|
case "access": Assert.True(owner.SetAccess(address, Page, HostPageProtection.ReadWrite)); break;
|
|
default: Assert.Fail("Unknown mapping operation."); break;
|
|
}
|
|
},
|
|
mustCompleteWhilePaused: false);
|
|
if (operation == "dispose")
|
|
{
|
|
Assert.False(owner.SetTransientAccess(address, Page, HostPageProtection.ReadOnly));
|
|
Assert.False(owner.SetAccess(address, Page, HostPageProtection.ReadOnly));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ThrowingProtectionReleasesAllAddressLocks()
|
|
{
|
|
if (!Supported) return;
|
|
var host = new FailingHostViews(HostViewMemory.Create());
|
|
using var owner = new GuestSpaceOwner(host, BackingSize);
|
|
var address = AcquireRange(owner, host, 2 * ProtectionBlockBytes);
|
|
Assert.True(owner.MapShared(address, Page, 0, HostPageProtection.ReadWrite, out _));
|
|
host.BeforeChangeAccess = (_, _) => throw new InvalidOperationException("Injected protection failure.");
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
owner.SetTransientAccess(address, 2 * ProtectionBlockBytes, HostPageProtection.ReadOnly));
|
|
host.BeforeChangeAccess = null;
|
|
RunOnNewThread(() =>
|
|
{
|
|
Assert.True(owner.SetTransientAccess(address, Page, HostPageProtection.ReadWrite));
|
|
Assert.True(owner.UnmapShared(address, Page));
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void TransientProtectionDoesNotAllocateAfterGuestThreadEntry(bool faultPath)
|
|
{
|
|
if (!Supported) return;
|
|
var host = 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 _));
|
|
long allocated = -1;
|
|
RunOnNewThread(() =>
|
|
{
|
|
var previous = GuestThreadExecution.EnterGuestThread(1);
|
|
try
|
|
{
|
|
var before = GC.GetAllocatedBytesForCurrentThread();
|
|
using var fault = faultPath ? GpuMemoryAccessProfile.MeasureFault(FaultKind.Write) : default;
|
|
var succeeded = true;
|
|
for (var iteration = 0; iteration < 100; iteration++)
|
|
succeeded &= owner.SetTransientAccess(address, Page, HostPageProtection.ReadWrite);
|
|
allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
|
Assert.True(succeeded);
|
|
}
|
|
finally { GuestThreadExecution.RestoreGuestThread(previous); }
|
|
});
|
|
Assert.Equal(0, allocated);
|
|
}
|
|
|
|
private static void WithProtectionPaused(FailingHostViews host, ulong address,
|
|
Action protection, Action competingOperation, bool mustCompleteWhilePaused, bool pauseMapping = false)
|
|
{
|
|
using var protectionEntered = new ManualResetEventSlim();
|
|
using var releaseProtection = new ManualResetEventSlim();
|
|
using var competitorStarted = new ManualResetEventSlim();
|
|
using var competitorFinished = new ManualResetEventSlim();
|
|
Exception? protectionFailure = null;
|
|
Exception? competitorFailure = null;
|
|
var intercepted = 0;
|
|
void Pause()
|
|
{
|
|
protectionEntered.Set();
|
|
Assert.True(releaseProtection.Wait(TimeSpan.FromSeconds(10)), "The test did not release the host operation.");
|
|
}
|
|
host.BeforeChangeAccess = (currentAddress, _) =>
|
|
{
|
|
if (pauseMapping) return;
|
|
if (currentAddress != address || Interlocked.Exchange(ref intercepted, 1) != 0) return;
|
|
Pause();
|
|
};
|
|
if (pauseMapping) host.AfterMapView = Pause;
|
|
var protectionThread = new Thread(() =>
|
|
{
|
|
try { protection(); }
|
|
catch (Exception exception) { protectionFailure = exception; }
|
|
}) { IsBackground = true };
|
|
var competitorThread = new Thread(() =>
|
|
{
|
|
competitorStarted.Set();
|
|
try { competingOperation(); }
|
|
catch (Exception exception) { competitorFailure = exception; }
|
|
finally { competitorFinished.Set(); }
|
|
}) { IsBackground = true };
|
|
protectionThread.Start();
|
|
try
|
|
{
|
|
Assert.True(protectionEntered.Wait(TimeSpan.FromSeconds(10)));
|
|
competitorThread.Start();
|
|
Assert.True(competitorStarted.Wait(TimeSpan.FromSeconds(10)));
|
|
if (mustCompleteWhilePaused)
|
|
Assert.True(competitorFinished.Wait(TimeSpan.FromSeconds(10)), "An independent range could not progress.");
|
|
else
|
|
{
|
|
Assert.True(SpinWait.SpinUntil(() => competitorFinished.IsSet ||
|
|
(competitorThread.ThreadState & ThreadState.WaitSleepJoin) != 0, TimeSpan.FromSeconds(10)));
|
|
Assert.False(competitorFinished.IsSet, "A conflicting operation bypassed active protection.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
releaseProtection.Set();
|
|
Assert.True(protectionThread.Join(TimeSpan.FromSeconds(10)));
|
|
if ((competitorThread.ThreadState & ThreadState.Unstarted) == 0)
|
|
Assert.True(competitorThread.Join(TimeSpan.FromSeconds(10)));
|
|
host.BeforeChangeAccess = null;
|
|
host.AfterMapView = null;
|
|
}
|
|
Assert.Null(protectionFailure);
|
|
Assert.Null(competitorFailure);
|
|
}
|
|
|
|
private static void RunOnNewThread(Action operation)
|
|
{
|
|
Exception? failure = null;
|
|
var thread = new Thread(() =>
|
|
{
|
|
try { operation(); }
|
|
catch (Exception exception) { failure = exception; }
|
|
}) { IsBackground = true };
|
|
thread.Start();
|
|
Assert.True(thread.Join(TimeSpan.FromSeconds(10)), "The operation retained a protection lock.");
|
|
Assert.Null(failure);
|
|
}
|
|
}
|