sharpemu/tests/SharpEmu.Libs.Tests/Pthread/PthreadMutexSemanticsTests.cs

580 lines
23 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using Xunit;
namespace SharpEmu.Libs.Tests.Pthread;
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class PthreadExceptionStateCollection
{
public const string Name = "PthreadExceptionState";
}
[Collection(PthreadExceptionStateCollection.Name)]
public sealed class PthreadMutexExceptionTests
{
[Fact]
public void HostMutexWaitDeliversExceptionWithoutGrantingOwnership()
{
const ulong mutexAddress = 0x6_0000_0100;
var memory = new PthreadMutexSemanticsTests.AllocatingCpuMemory(0x6_0000_0000, 0x4000);
var ownerContext = new CpuContext(memory, Generation.Gen5);
ownerContext[CpuRegister.Rdi] = mutexAddress;
Assert.True(ownerContext.TryWriteUInt64(mutexAddress, 1));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(ownerContext));
using var exceptionDelivered = new ManualResetEventSlim();
using var acquired = new ManualResetEventSlim();
var scheduler = DispatchProxy.Create<IGuestThreadScheduler, ExceptionWaitScheduler>();
var proxy = (ExceptionWaitScheduler)(object)scheduler;
var waiterThreadId = 0;
var deliveryThreadId = 0;
proxy.DeliverException = context =>
{
deliveryThreadId = Environment.CurrentManagedThreadId;
Assert.Equal(mutexAddress, context[CpuRegister.Rdi]);
exceptionDelivered.Set();
};
var previousScheduler = GuestThreadExecution.Scheduler;
var lockResult = -1;
var unlockResult = -1;
Exception? failure = null;
var waiter = new Thread(() =>
{
try
{
waiterThreadId = Environment.CurrentManagedThreadId;
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = mutexAddress;
lockResult = KernelPthreadCompatExports.PthreadMutexLock(context);
acquired.Set();
unlockResult = KernelPthreadCompatExports.PthreadMutexUnlock(context);
}
catch (Exception error)
{
failure = error;
}
}) { IsBackground = true };
try
{
GuestThreadExecution.Scheduler = scheduler;
waiter.Start();
Assert.True(exceptionDelivered.Wait(TimeSpan.FromSeconds(5)));
Assert.False(acquired.IsSet);
Assert.Equal(waiterThreadId, deliveryThreadId);
}
finally
{
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(ownerContext));
var joined = waiter.Join(TimeSpan.FromSeconds(5));
GuestThreadExecution.Scheduler = previousScheduler;
Assert.True(joined);
}
Assert.Null(failure);
Assert.Equal(0, lockResult);
Assert.Equal(0, unlockResult);
}
public class ExceptionWaitScheduler : DispatchProxy
{
public Action<CpuContext>? DeliverException { get; set; }
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
if (targetMethod?.Name == nameof(IGuestThreadScheduler.DeliverPendingGuestExceptionIfReady))
{
DeliverException!((CpuContext)args![0]!);
return null;
}
throw new NotSupportedException(targetMethod?.Name);
}
}
}
public sealed class PthreadMutexSemanticsTests
{
[Fact]
public void AdaptiveMutex_SelfLockIsIdempotent()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong mutexAddress = memoryBase + 0x100;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(context.TryWriteUInt64(mutexAddress, 1)); // Static adaptive initializer.
context[CpuRegister.Rdi] = mutexAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public void AdaptiveMutex_GuestTrackedSelfLockReturnsDeadlockAndSingleUnlockReleases()
{
const ulong memoryBase = 0x1_0001_0000;
const ulong mutexAddress = memoryBase + 0x100;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(context.TryWriteUInt64(mutexAddress, 1)); // Static adaptive initializer.
context[CpuRegister.Rdi] = mutexAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
Assert.True(context.TryWriteUInt64(mutexAddress + 8, currentThreadHandle));
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK,
KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.True(context.TryWriteUInt64(mutexAddress + 8, 0));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexTrylock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public void RecursiveMutex_GuestTrackedSelfLockKeepsRecursiveSemantics()
{
const ulong memoryBase = 0x1_0002_0000;
const ulong attrAddress = memoryBase + 0x100;
const ulong mutexAddress = memoryBase + 0x200;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = attrAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrInit(context));
context[CpuRegister.Rsi] = 2;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrSettype(context));
context[CpuRegister.Rdi] = mutexAddress;
context[CpuRegister.Rsi] = attrAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexInit(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
Assert.True(context.TryWriteUInt64(mutexAddress + 8, currentThreadHandle));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public async Task ContendedMutex_HandsOffOneHostWaiterAtATime()
{
const ulong memoryBase = 0x2_0000_0000;
const ulong mutexAddress = memoryBase + 0x100;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var ownerContext = new CpuContext(memory, Generation.Gen5);
Assert.True(ownerContext.TryWriteUInt64(mutexAddress, 1));
ownerContext[CpuRegister.Rdi] = mutexAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(ownerContext));
using var waitersStarted = new CountdownEvent(2);
using var firstAcquired = new ManualResetEventSlim(false);
using var secondAcquired = new ManualResetEventSlim(false);
using var releaseFirst = new ManualResetEventSlim(false);
var acquisitionCount = 0;
Task<(int LockResult, int UnlockResult)> StartWaiter() =>
Task.Factory.StartNew(
() =>
{
var waiterContext = new CpuContext(memory, Generation.Gen5);
waiterContext[CpuRegister.Rdi] = mutexAddress;
waitersStarted.Signal();
var lockResult = KernelPthreadCompatExports.PthreadMutexLock(waiterContext);
if (lockResult != 0)
{
return (lockResult, int.MinValue);
}
if (Interlocked.Increment(ref acquisitionCount) == 1)
{
firstAcquired.Set();
releaseFirst.Wait(TimeSpan.FromSeconds(5));
}
else
{
secondAcquired.Set();
}
var unlockResult = KernelPthreadCompatExports.PthreadMutexUnlock(waiterContext);
return (lockResult, unlockResult);
},
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
var firstWaiter = StartWaiter();
var secondWaiter = StartWaiter();
Assert.True(waitersStarted.Wait(TimeSpan.FromSeconds(5)));
Thread.Sleep(50);
Assert.Equal(0, Volatile.Read(ref acquisitionCount));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(ownerContext));
Assert.True(firstAcquired.Wait(TimeSpan.FromSeconds(5)));
Assert.Equal(1, Volatile.Read(ref acquisitionCount));
releaseFirst.Set();
Assert.True(secondAcquired.Wait(TimeSpan.FromSeconds(5)));
var results = await Task.WhenAll(firstWaiter, secondWaiter).WaitAsync(TimeSpan.FromSeconds(5));
Assert.All(results, result => Assert.Equal((0, 0), result));
Assert.Equal(2, Volatile.Read(ref acquisitionCount));
}
[Fact]
public async Task ContendedMutex_PreservesMutualExclusionUnderLoad()
{
const ulong memoryBase = 0x3_0000_0000;
const ulong mutexAddress = memoryBase + 0x100;
const int workerCount = 4;
const int iterationsPerWorker = 250;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var initializationContext = new CpuContext(memory, Generation.Gen5);
Assert.True(initializationContext.TryWriteUInt64(mutexAddress, 1));
initializationContext[CpuRegister.Rdi] = mutexAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(initializationContext));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(initializationContext));
using var start = new ManualResetEventSlim(false);
var insideCriticalSection = 0;
var mutualExclusionViolations = 0;
var protectedCounter = 0;
var workers = Enumerable.Range(0, workerCount)
.Select(_ => Task.Factory.StartNew(
() =>
{
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = mutexAddress;
start.Wait();
for (var iteration = 0; iteration < iterationsPerWorker; iteration++)
{
if (KernelPthreadCompatExports.PthreadMutexLock(context) != 0)
{
throw new InvalidOperationException("pthread mutex lock failed during contention stress.");
}
if (Interlocked.Increment(ref insideCriticalSection) != 1)
{
Interlocked.Increment(ref mutualExclusionViolations);
}
protectedCounter++;
Thread.SpinWait(20);
Interlocked.Decrement(ref insideCriticalSection);
if (KernelPthreadCompatExports.PthreadMutexUnlock(context) != 0)
{
throw new InvalidOperationException("pthread mutex unlock failed during contention stress.");
}
}
},
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default))
.ToArray();
start.Set();
await Task.WhenAll(workers).WaitAsync(TimeSpan.FromSeconds(10));
Assert.Equal(0, Volatile.Read(ref mutualExclusionViolations));
Assert.Equal(workerCount * iterationsPerWorker, protectedCounter);
}
/// <summary>
/// A ScePthreadMutex variable is storage the guest owns and reuses — stack
/// frames recycle the slot, and a slot can be reassigned to another mutex —
/// so the handle the slot currently holds outranks anything cached under the
/// slot's own address. Resolving to the stale entry silently released the
/// wrong mutex and left the real one owned forever.
/// </summary>
[Fact]
public void ReusedHandleSlot_UnlockReleasesTheMutexTheSlotNowNames()
{
const ulong memoryBase = 0x1_0010_0000;
const ulong slotAddress = memoryBase + 0x100;
const ulong realMutexAddress = memoryBase + 0x200;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
// First use of the slot happens while it still reads as zero, which
// implicitly registers a mutex keyed by the slot address itself.
Assert.True(context.TryWriteUInt64(slotAddress, 0));
context[CpuRegister.Rdi] = slotAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
// The frame is reused: the slot now holds a different, real handle.
context[CpuRegister.Rdi] = realMutexAddress;
context[CpuRegister.Rsi] = 0;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexInit(context));
Assert.True(context.TryReadUInt64(realMutexAddress, out var realHandle));
Assert.NotEqual(0ul, realHandle);
Assert.True(context.TryWriteUInt64(slotAddress, realHandle));
// Locking by handle and releasing through the slot must hit one mutex.
context[CpuRegister.Rdi] = realHandle;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
context[CpuRegister.Rdi] = slotAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
// Released for real: an unrelated acquisition now succeeds.
context[CpuRegister.Rdi] = realHandle;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexTrylock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public void ReusedHandleSlot_RecursiveMutexUnwindsThroughEitherAlias()
{
const ulong memoryBase = 0x1_0011_0000;
const ulong attrAddress = memoryBase + 0x100;
const ulong slotAddress = memoryBase + 0x200;
const ulong realMutexAddress = memoryBase + 0x300;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(context.TryWriteUInt64(slotAddress, 0));
context[CpuRegister.Rdi] = slotAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
context[CpuRegister.Rdi] = attrAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrInit(context));
context[CpuRegister.Rsi] = 2; // Recursive.
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrSettype(context));
context[CpuRegister.Rdi] = realMutexAddress;
context[CpuRegister.Rsi] = attrAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexInit(context));
Assert.True(context.TryReadUInt64(realMutexAddress, out var realHandle));
Assert.True(context.TryWriteUInt64(slotAddress, realHandle));
context[CpuRegister.Rdi] = realHandle;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
context[CpuRegister.Rdi] = slotAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
context[CpuRegister.Rdi] = realHandle;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexTrylock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public void OpaqueObjects_AreReleasedAfterDestroy()
{
const ulong memoryBase = 0x1_0012_0000;
const ulong objectAddress = memoryBase + 0x100;
var memory = new RecyclingCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
for (var iteration = 0; iteration < 4; iteration++)
{
context[CpuRegister.Rdi] = objectAddress;
context[CpuRegister.Rsi] = 0;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexInit(context));
Assert.Equal(1, memory.OutstandingAllocations);
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexDestroy(context));
Assert.Equal(0, memory.OutstandingAllocations);
context[CpuRegister.Rdi] = objectAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadCondInit(context));
Assert.Equal(1, memory.OutstandingAllocations);
Assert.Equal(0, KernelPthreadCompatExports.PthreadCondDestroy(context));
Assert.Equal(0, memory.OutstandingAllocations);
context[CpuRegister.Rdi] = objectAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrInit(context));
Assert.Equal(1, memory.OutstandingAllocations);
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrDestroy(context));
Assert.Equal(0, memory.OutstandingAllocations);
}
}
[Fact]
public void OpaqueObjects_AreReleasedWhenCallerHandleWriteFails()
{
const ulong memoryBase = 0x1_0013_0000;
const ulong invalidObjectAddress = memoryBase + 0x2000;
var memory = new RecyclingCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
var memoryFault = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
for (var iteration = 0; iteration < 4; iteration++)
{
context[CpuRegister.Rdi] = invalidObjectAddress;
context[CpuRegister.Rsi] = 0;
Assert.Equal(memoryFault, KernelPthreadCompatExports.PthreadMutexInit(context));
Assert.Equal(0, memory.OutstandingAllocations);
Assert.Equal(memoryFault, KernelPthreadCompatExports.PthreadCondInit(context));
Assert.Equal(0, memory.OutstandingAllocations);
Assert.Equal(memoryFault, KernelPthreadCompatExports.PthreadMutexattrInit(context));
Assert.Equal(0, memory.OutstandingAllocations);
}
}
internal sealed class AllocatingCpuMemory : ICpuMemory, IGuestMemoryAllocator
{
private readonly ulong _baseAddress;
private readonly byte[] _storage;
private ulong _nextAllocation;
public AllocatingCpuMemory(ulong baseAddress, int size)
{
_baseAddress = baseAddress;
_storage = new byte[size];
_nextAllocation = baseAddress + 0x1000;
}
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
if (!TryResolve(virtualAddress, destination.Length, out var offset))
{
return false;
}
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
if (!TryResolve(virtualAddress, source.Length, out var offset))
{
return false;
}
source.CopyTo(_storage.AsSpan(offset, source.Length));
return true;
}
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
var mask = alignment - 1;
var aligned = (_nextAllocation + mask) & ~mask;
if (!TryResolve(aligned, checked((int)size), out _))
{
address = 0;
return false;
}
address = aligned;
_nextAllocation = aligned + size;
return true;
}
public bool TryFreeGuestMemory(ulong address) =>
address >= _baseAddress && address < _baseAddress + (ulong)_storage.Length;
private bool TryResolve(ulong virtualAddress, int length, out int offset)
{
offset = 0;
if (virtualAddress < _baseAddress)
{
return false;
}
var relative = virtualAddress - _baseAddress;
if (relative + (ulong)length > (ulong)_storage.Length)
{
return false;
}
offset = (int)relative;
return true;
}
}
private sealed class RecyclingCpuMemory : ICpuMemory, IGuestMemoryAllocator
{
private readonly ulong _baseAddress;
private readonly byte[] _storage;
private readonly ulong _allocationAddress;
private bool _allocated;
public RecyclingCpuMemory(ulong baseAddress, int size)
{
_baseAddress = baseAddress;
_storage = new byte[size];
_allocationAddress = baseAddress + 0x800;
}
public int OutstandingAllocations => _allocated ? 1 : 0;
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
if (!TryResolve(virtualAddress, destination.Length, out var offset))
{
return false;
}
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
if (!TryResolve(virtualAddress, source.Length, out var offset))
{
return false;
}
source.CopyTo(_storage.AsSpan(offset, source.Length));
return true;
}
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
if (_allocated || size > 0x100 || alignment > 0x10)
{
address = 0;
return false;
}
_allocated = true;
address = _allocationAddress;
return true;
}
public bool TryFreeGuestMemory(ulong address)
{
if (!_allocated || address != _allocationAddress)
{
return false;
}
_allocated = false;
return true;
}
private bool TryResolve(ulong virtualAddress, int length, out int offset)
{
offset = 0;
if (virtualAddress < _baseAddress)
{
return false;
}
var relative = virtualAddress - _baseAddress;
if (relative + (ulong)length > (ulong)_storage.Length)
{
return false;
}
offset = (int)relative;
return true;
}
}
}