420 lines
17 KiB
C#
420 lines
17 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;
|
|
|
|
[CollectionDefinition(GuestMemoryStateCollection.Name, DisableParallelization = true)]
|
|
public sealed class GuestMemoryStateCollection
|
|
{
|
|
public const string Name = "GuestMemoryState";
|
|
}
|
|
|
|
[Collection(GuestMemoryStateCollection.Name)]
|
|
public sealed class SharedBackingViewsTests
|
|
{
|
|
private static byte[] Pattern(int length, byte value) => Enumerable.Repeat(value, length).ToArray();
|
|
|
|
[Fact]
|
|
public unsafe void SingleMappingReadsDoNotAllocateTemporarySegments()
|
|
{
|
|
if (!Supported) return;
|
|
var host = HostViewMemory.Create();
|
|
using var store = new SharedBackingViews(host, BackingSize);
|
|
var hole = HoleSize(host);
|
|
var baseAddress = ReserveFreeHole(host, hole);
|
|
Assert.True(host.SplitHole(baseAddress, Segment));
|
|
Assert.True(store.TryMapReservedRange(baseAddress, Segment, Segment, HostPageProtection.ReadWrite, out _));
|
|
try
|
|
{
|
|
*(ulong*)(baseAddress + Segment - 8) = Marker;
|
|
var bytes = new byte[8];
|
|
var succeeded = true;
|
|
void ReadMarker(int count)
|
|
{
|
|
for (var index = 0; index < count; index++)
|
|
succeeded &= store.TryReadBacking(baseAddress + Segment - 8, bytes);
|
|
}
|
|
|
|
var allocatedBytes = AllocationMeasurement.SteadyState(() => ReadMarker(256), () => ReadMarker(1024));
|
|
|
|
Assert.True(succeeded);
|
|
Assert.Equal(Marker, System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(bytes));
|
|
Assert.Equal(0L, allocatedBytes);
|
|
bytes.AsSpan().Fill(0xA5);
|
|
Assert.False(store.TryReadBacking(baseAddress + Segment - 4, bytes));
|
|
Assert.True(bytes.SequenceEqual(new byte[] { 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5 }));
|
|
}
|
|
finally
|
|
{
|
|
Assert.True(store.Unmap(baseAddress, Segment, out _));
|
|
Assert.True(host.JoinHoles(baseAddress, hole));
|
|
Assert.True(host.FreeHole(baseAddress, hole));
|
|
}
|
|
}
|
|
|
|
private static ulong[] Read(SharedBackingViews store, ulong address, int count)
|
|
{
|
|
var bytes = new byte[count * 8];
|
|
Assert.True(store.TryReadBacking(address, bytes));
|
|
var values = new ulong[count];
|
|
Buffer.BlockCopy(bytes, 0, values, 0, bytes.Length);
|
|
return values;
|
|
}
|
|
|
|
[Fact]
|
|
public unsafe void CopyInAndOut_ShareBytesWithTheView()
|
|
{
|
|
if (!Supported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var host = HostViewMemory.Create();
|
|
using var store = new SharedBackingViews(host, BackingSize);
|
|
Assert.True(store.IsAvailable);
|
|
var hole = HoleSize(host);
|
|
var baseAddress = ReserveFreeHole(host, hole);
|
|
Assert.True(host.SplitHole(baseAddress, Segment));
|
|
Assert.True(store.TryMapReservedRange(baseAddress, Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
|
|
Assert.True(store.TryWriteBacking(baseAddress + 16, BitConverter.GetBytes(Marker)));
|
|
Assert.Equal(Marker, *(ulong*)(baseAddress + 16));
|
|
*(ulong*)(baseAddress + 32) = ~Marker;
|
|
Assert.Equal(new[] { ~Marker }, Read(store, baseAddress + 32, 1));
|
|
Assert.True(store.Clear(0, Segment));
|
|
Assert.Equal(0UL, *(ulong*)(baseAddress + 16));
|
|
|
|
Assert.True(store.Unmap(baseAddress, Segment, out var preserved));
|
|
Assert.True(preserved);
|
|
Assert.True(host.JoinHoles(baseAddress, hole));
|
|
Assert.True(host.FreeHole(baseAddress, hole));
|
|
}
|
|
|
|
[Fact]
|
|
public unsafe void TwoViewsOfOneOffset_ShareBytesAndTheSecondSurvives()
|
|
{
|
|
if (!Supported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var host = HostViewMemory.Create();
|
|
using var store = new SharedBackingViews(host, BackingSize);
|
|
var hole = HoleSize(host);
|
|
var first = ReserveFreeHole(host, hole);
|
|
var second = ReserveFreeHole(host, hole);
|
|
Assert.True(host.SplitHole(first, Segment));
|
|
Assert.True(host.SplitHole(second, Segment));
|
|
Assert.True(store.TryMapReservedRange(first, Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
Assert.True(store.TryMapReservedRange(second, Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
|
|
*(ulong*)first = Marker;
|
|
Assert.Equal(Marker, *(ulong*)second);
|
|
Assert.True(store.Unmap(first, Segment, out _));
|
|
Assert.False(store.Contains(first, Segment));
|
|
Assert.True(store.Contains(second, Segment));
|
|
Assert.False(store.ContainsWithoutLock(first, Segment));
|
|
Assert.True(store.ContainsWithoutLock(second, Segment));
|
|
Assert.Equal(Marker, *(ulong*)second);
|
|
|
|
Assert.True(store.Unmap(second, Segment, out _));
|
|
Assert.True(host.JoinHoles(first, hole));
|
|
Assert.True(host.JoinHoles(second, hole));
|
|
Assert.True(host.FreeHole(first, hole));
|
|
Assert.True(host.FreeHole(second, hole));
|
|
}
|
|
|
|
// Single-view transfers run without the lock against a snapshot of the views; they
|
|
// must stay exact on a stable view and fail or succeed cleanly on a view that is
|
|
// being unmapped and remapped at the same time.
|
|
[Fact]
|
|
public async Task LockFreeTransfers_StayConsistentWhileOtherViewsChange()
|
|
{
|
|
if (!Supported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var host = HostViewMemory.Create();
|
|
using var store = new SharedBackingViews(host, BackingSize);
|
|
var hole = HoleSize(host);
|
|
var stable = ReserveFreeHole(host, hole);
|
|
var churn = ReserveFreeHole(host, hole);
|
|
Assert.True(host.SplitHole(stable, Segment));
|
|
Assert.True(host.SplitHole(churn, Segment));
|
|
Assert.True(store.TryMapReservedRange(stable, Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
|
|
var stop = 0;
|
|
var failures = 0;
|
|
var workers = Enumerable.Range(0, 2).Select(worker => Task.Run(() =>
|
|
{
|
|
Span<byte> bytes = stackalloc byte[8];
|
|
var slot = stable + (ulong)(worker * 64);
|
|
for (var value = 1UL; Volatile.Read(ref stop) == 0; value++)
|
|
{
|
|
if (!store.TryWriteBacking(slot, BitConverter.GetBytes(value)) ||
|
|
!store.TryReadBacking(slot, bytes) ||
|
|
BitConverter.ToUInt64(bytes) != value)
|
|
{
|
|
Interlocked.Increment(ref failures);
|
|
}
|
|
|
|
_ = store.TryReadBacking(churn + 8, bytes);
|
|
}
|
|
})).ToArray();
|
|
|
|
for (var round = 0; round < 200; round++)
|
|
{
|
|
Assert.True(store.TryMapReservedRange(churn, Segment, Segment, HostPageProtection.ReadWrite, out _));
|
|
Assert.True(store.Unmap(churn, Segment, out _));
|
|
}
|
|
|
|
Volatile.Write(ref stop, 1);
|
|
await Task.WhenAll(workers);
|
|
Assert.Equal(0, Volatile.Read(ref failures));
|
|
|
|
Assert.True(store.Unmap(stable, Segment, out _));
|
|
Assert.True(host.JoinHoles(stable, hole));
|
|
Assert.True(host.JoinHoles(churn, hole));
|
|
Assert.True(host.FreeHole(stable, hole));
|
|
Assert.True(host.FreeHole(churn, hole));
|
|
}
|
|
|
|
[Fact]
|
|
public unsafe void Transfers_WorkWhileTheViewIsNoAccessOrReadOnly()
|
|
{
|
|
if (!Supported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var host = HostViewMemory.Create();
|
|
using var store = new SharedBackingViews(host, BackingSize);
|
|
var hole = HoleSize(host);
|
|
var baseAddress = ReserveFreeHole(host, hole);
|
|
Assert.True(host.SplitHole(baseAddress, Segment));
|
|
Assert.True(store.TryMapReservedRange(baseAddress, Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
|
|
Assert.True(host.ChangeAccess(baseAddress, Segment, HostPageProtection.NoAccess));
|
|
Assert.True(store.TryWriteBacking(baseAddress, BitConverter.GetBytes(Marker)));
|
|
Assert.Equal(new[] { Marker }, Read(store, baseAddress, 1));
|
|
Assert.True(host.ChangeAccess(baseAddress, Segment, HostPageProtection.ReadOnly));
|
|
Assert.True(store.TryWriteBacking(baseAddress, BitConverter.GetBytes(~Marker)));
|
|
Assert.Equal(~Marker, *(ulong*)baseAddress);
|
|
|
|
Assert.True(store.Unmap(baseAddress, Segment, out _));
|
|
Assert.True(host.JoinHoles(baseAddress, hole));
|
|
Assert.True(host.FreeHole(baseAddress, hole));
|
|
}
|
|
|
|
[Fact]
|
|
public unsafe void UnmapThenRemap_KeepsBackingContents()
|
|
{
|
|
if (!Supported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var host = HostViewMemory.Create();
|
|
using var store = new SharedBackingViews(host, BackingSize);
|
|
var hole = HoleSize(host);
|
|
var baseAddress = ReserveFreeHole(host, hole);
|
|
Assert.True(host.SplitHole(baseAddress, Segment));
|
|
Assert.True(store.TryMapReservedRange(baseAddress, Segment, 2 * Segment, HostPageProtection.ReadWrite, out _));
|
|
*(ulong*)baseAddress = Marker;
|
|
|
|
Assert.True(store.Unmap(baseAddress, Segment, out _));
|
|
Assert.True(store.TryMapReservedRange(baseAddress, Segment, 2 * Segment, HostPageProtection.ReadWrite, out _));
|
|
Assert.Equal(Marker, *(ulong*)baseAddress);
|
|
|
|
Assert.True(store.Unmap(baseAddress, Segment, out _));
|
|
Assert.True(host.JoinHoles(baseAddress, hole));
|
|
Assert.True(host.FreeHole(baseAddress, hole));
|
|
}
|
|
|
|
[Fact]
|
|
public unsafe void PartialUnmap_SplitsTheRecordAndTransfersNeverCopyAcrossTheGap()
|
|
{
|
|
if (!Supported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var host = HostViewMemory.Create();
|
|
using var store = new SharedBackingViews(host, BackingSize);
|
|
var hole = HoleSize(host);
|
|
var baseAddress = ReserveFreeHole(host, hole);
|
|
var middle = baseAddress + Segment;
|
|
var right = baseAddress + 2 * Segment;
|
|
Assert.True(host.SplitHole(baseAddress, 3 * Segment));
|
|
Assert.True(store.TryMapReservedRange(baseAddress, 3 * Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
*(ulong*)baseAddress = Marker;
|
|
*(ulong*)right = ~Marker;
|
|
|
|
Assert.True(store.Unmap(middle, Segment, out var preserved));
|
|
Assert.True(preserved);
|
|
Assert.True(store.Contains(baseAddress, Segment));
|
|
Assert.False(store.Contains(middle, Segment));
|
|
Assert.True(store.Contains(right, Segment));
|
|
Assert.True(store.ContainsWithoutLock(baseAddress, Segment));
|
|
Assert.False(store.ContainsWithoutLock(middle, Segment));
|
|
Assert.True(store.ContainsWithoutLock(right, Segment));
|
|
Assert.False(store.ContainsWithoutLock(baseAddress, 3 * Segment));
|
|
Assert.Equal(store.Contains(baseAddress, 3 * Segment), store.ContainsWithoutLock(baseAddress, 3 * Segment));
|
|
Assert.Equal(Marker, *(ulong*)baseAddress);
|
|
Assert.Equal(~Marker, *(ulong*)right);
|
|
|
|
var pattern = Pattern((int)(2 * Segment), 0xA5);
|
|
Assert.False(store.TryReadBacking(baseAddress, pattern));
|
|
Assert.All(pattern, value => Assert.Equal(0xA5, value));
|
|
Assert.False(store.TryWriteBacking(baseAddress, Pattern((int)(2 * Segment), 0x5A)));
|
|
Assert.Equal(Marker, *(ulong*)baseAddress);
|
|
Assert.Equal(Marker, *(ulong*)store.AliasBase);
|
|
|
|
Assert.True(store.TryMapReservedRange(middle, Segment, Segment, HostPageProtection.ReadWrite, out _));
|
|
Assert.True(store.TryReadBacking(baseAddress, new byte[3 * Segment]));
|
|
|
|
Assert.True(store.Unmap(baseAddress, 3 * Segment, out preserved));
|
|
Assert.True(preserved);
|
|
Assert.True(host.JoinHoles(baseAddress, hole));
|
|
Assert.True(host.FreeHole(baseAddress, hole));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejections_MakeNoHostCall()
|
|
{
|
|
if (!Supported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var host = new FailingHostViews(HostViewMemory.Create());
|
|
using var store = new SharedBackingViews(host, BackingSize);
|
|
var hole = HoleSize(host);
|
|
var baseAddress = ReserveFreeHole(host, hole);
|
|
host.Log.Clear();
|
|
|
|
Assert.False(store.Unmap(baseAddress, Segment, out _));
|
|
Assert.False(store.TryReadBacking(baseAddress, new byte[16]));
|
|
Assert.False(store.TryMapReservedRange(baseAddress, Segment, BackingSize - Segment / 2, HostPageProtection.ReadWrite, out var failure));
|
|
Assert.Equal(HostViewFailure.OffsetOutOfBounds, failure);
|
|
Assert.Empty(host.Log);
|
|
|
|
Assert.True(host.FreeHole(baseAddress, hole));
|
|
}
|
|
|
|
[Fact]
|
|
public unsafe void InjectedFailures_RollBackViewsAndRecordsTogether()
|
|
{
|
|
if (!Supported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var host = new FailingHostViews(HostViewMemory.Create());
|
|
using var store = new SharedBackingViews(host, BackingSize);
|
|
var hole = HoleSize(host);
|
|
var baseAddress = ReserveFreeHole(host, hole);
|
|
var middle = baseAddress + Segment;
|
|
Assert.True(host.SplitHole(baseAddress, 3 * Segment));
|
|
Assert.True(store.TryMapReservedRange(baseAddress, 3 * Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
*(ulong*)baseAddress = Marker;
|
|
|
|
host.Log.Clear();
|
|
host.FailNext(Op.MapView, afterCalls: 1);
|
|
Assert.False(store.Unmap(middle, Segment, out _));
|
|
Assert.True(store.Contains(baseAddress, 3 * Segment));
|
|
Assert.Equal(Marker, *(ulong*)baseAddress);
|
|
Assert.Equal(
|
|
new[] { Op.UnmapView, Op.SplitHole, Op.SplitHole, Op.MapView, Op.MapView, Op.UnmapView, Op.JoinHoles, Op.MapView },
|
|
host.Log);
|
|
|
|
Assert.True(store.Unmap(baseAddress, 3 * Segment, out _));
|
|
Assert.True(host.SplitHole(baseAddress, Segment));
|
|
Assert.True(host.SplitHole(middle, Segment));
|
|
Assert.True(store.TryMapReservedRange(baseAddress, Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
Assert.True(store.TryMapReservedRange(middle, Segment, Segment, HostPageProtection.ReadWrite, out _));
|
|
|
|
host.Log.Clear();
|
|
host.FailNext(Op.UnmapView, afterCalls: 1);
|
|
Assert.False(store.Unmap(baseAddress, 2 * Segment, out _));
|
|
Assert.True(store.Contains(baseAddress, 2 * Segment));
|
|
Assert.Equal(new[] { Op.UnmapView, Op.UnmapView, Op.MapView }, host.Log);
|
|
|
|
host.FailNext(Op.MapView, failure: HostViewFailure.ProtectFailed);
|
|
Assert.False(store.TryMapReservedRange(baseAddress + 2 * Segment, Segment, 0, HostPageProtection.ReadWrite, out var failure));
|
|
Assert.Equal(HostViewFailure.ProtectFailed, failure);
|
|
Assert.False(store.Contains(baseAddress + 2 * Segment, Segment));
|
|
|
|
Assert.True(store.Unmap(baseAddress, 2 * Segment, out _));
|
|
Assert.True(host.JoinHoles(baseAddress, hole));
|
|
Assert.True(host.FreeHole(baseAddress, hole));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispose_UnmapsRecordedViewsBeforeReleasingTheBacking()
|
|
{
|
|
if (!Supported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var host = HostViewMemory.Create();
|
|
var store = new SharedBackingViews(host, BackingSize);
|
|
var hole = HoleSize(host);
|
|
var baseAddress = ReserveFreeHole(host, hole);
|
|
Assert.True(host.SplitHole(baseAddress, Segment));
|
|
Assert.True(store.TryMapReservedRange(baseAddress, Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
|
|
store.Dispose();
|
|
store.Dispose();
|
|
Assert.False(store.IsAvailable);
|
|
Assert.False(store.Clear(0, Segment));
|
|
Assert.False(store.TryWriteBacking(baseAddress, new byte[8]));
|
|
Assert.False(store.TryReadBacking(baseAddress, new byte[8]));
|
|
Assert.False(store.TryCopyBacking(baseAddress, baseAddress, 8));
|
|
Assert.False(store.Unmap(baseAddress, Segment, out _));
|
|
Assert.False(store.TryMapReservedRange(baseAddress, Segment, 0, HostPageProtection.ReadWrite, out var failure));
|
|
Assert.Equal(HostViewFailure.BackingUnavailable, failure);
|
|
|
|
using var fresh = new SharedBackingViews(host, BackingSize);
|
|
Assert.True(fresh.TryMapReservedRange(baseAddress, Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
Assert.True(fresh.Unmap(baseAddress, Segment, out _));
|
|
Assert.True(host.JoinHoles(baseAddress, hole));
|
|
Assert.True(host.FreeHole(baseAddress, hole));
|
|
}
|
|
|
|
[Fact]
|
|
public void DisposeDuringMap_LeavesNoUnrecordedView()
|
|
{
|
|
if (!Supported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var host = new FailingHostViews(HostViewMemory.Create());
|
|
var store = new SharedBackingViews(host, BackingSize);
|
|
var hole = HoleSize(host);
|
|
var baseAddress = ReserveFreeHole(host, hole);
|
|
Assert.True(host.SplitHole(baseAddress, Segment));
|
|
|
|
host.AfterMapView = () => store.Dispose();
|
|
Assert.False(store.TryMapReservedRange(baseAddress, Segment, 0, HostPageProtection.ReadWrite, out var failure));
|
|
Assert.Equal(HostViewFailure.BackingUnavailable, failure);
|
|
Assert.Equal(Op.UnmapView, host.Log[^1]);
|
|
host.AfterMapView = null;
|
|
|
|
using var fresh = new SharedBackingViews(host, BackingSize);
|
|
Assert.True(fresh.TryMapReservedRange(baseAddress, Segment, 0, HostPageProtection.ReadWrite, out _));
|
|
Assert.True(fresh.Unmap(baseAddress, Segment, out _));
|
|
Assert.True(host.JoinHoles(baseAddress, hole));
|
|
Assert.True(host.FreeHole(baseAddress, hole));
|
|
}
|
|
}
|