diff --git a/tests/SharpEmu.Libs.Tests/Cpu/GuestThreadStackBoundsTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/GuestThreadStackBoundsTests.cs new file mode 100644 index 0000000..8fe77a6 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/GuestThreadStackBoundsTests.cs @@ -0,0 +1,51 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections; +using System.Reflection; +using SharpEmu.Core.Cpu.Native; +using SharpEmu.HLE; +using Xunit; + +namespace SharpEmu.Libs.Tests.Cpu; + +public sealed class GuestThreadStackBoundsTests +{ + [NativeX64Fact] + public void SchedulerReturnsAuthoritativeBoundsAcrossTheFullSlotWindow() + { + const BindingFlags privateInstance = BindingFlags.NonPublic | BindingFlags.Instance; + const ulong stackSize = 0x20_0000; + const ulong stackStride = 0x100_0000; + var highestStack = OperatingSystem.IsWindows() + ? 0x0000_7FFF_E000_0000UL + : 0x0000_6FFF_E000_0000UL; + var backendType = typeof(DirectExecutionBackend); + var threadType = backendType.GetNestedType("GuestThreadState", BindingFlags.NonPublic)!; + var modules = new ModuleManager(); + modules.Freeze(); + using var backend = new DirectExecutionBackend(modules); + var gate = backendType.GetField("_guestThreadGate", privateInstance)!.GetValue(backend)!; + var threads = (IDictionary)backendType.GetField("_guestThreads", privateInstance)!.GetValue(backend)!; + + foreach (var slot in new[] { 0, 48, 56, 1023 }) + { + var handle = 0x1000UL + (ulong)slot; + var expectedBase = highestStack - ((ulong)slot * stackStride); + var thread = Activator.CreateInstance(threadType)!; + threadType.GetProperty("ThreadHandle")!.SetValue(thread, handle); + threadType.GetProperty("StackBase")!.SetValue(thread, expectedBase); + threadType.GetProperty("StackSize")!.SetValue(thread, stackSize); + lock (gate) + threads.Add(handle, thread); + + Assert.True(backend.TryGetGuestThreadStackBounds(handle, out var actualBase, out var actualSize)); + Assert.Equal(expectedBase, actualBase); + Assert.Equal(stackSize, actualSize); + } + + Assert.False(backend.TryGetGuestThreadStackBounds(0xDEAD, out var missingBase, out var missingSize)); + Assert.Equal(0UL, missingBase); + Assert.Equal(0UL, missingSize); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelHostWaitExceptionTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelHostWaitExceptionTests.cs new file mode 100644 index 0000000..da5218a --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelHostWaitExceptionTests.cs @@ -0,0 +1,135 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using System.Reflection; +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using Xunit; + +namespace SharpEmu.Libs.Tests.Kernel; + +[Collection("GuestThreadFlowProfile")] +public sealed class KernelHostWaitExceptionTests +{ + private const ulong MemoryBase = 0x1_3000_0000; + private const ulong SemaphoreAddress = MemoryBase + 0x100; + private const ulong NameAddress = MemoryBase + 0x200; + private const ulong ValueAddress = MemoryBase + 0x300; + private const ulong TimeoutAddress = MemoryBase + 0x400; + + [Fact] + public void SemaphoreHostWaitDeliversPendingExceptionBeforeReturning() + { + var memory = new FakeCpuMemory(MemoryBase, 0x1000); + var createContext = new CpuContext(memory, Generation.Gen5); + Assert.True(memory.TryWrite(NameAddress, "exception-sema\0"u8)); + createContext[CpuRegister.Rdi] = SemaphoreAddress; + createContext[CpuRegister.Rsi] = NameAddress; + createContext[CpuRegister.Rdx] = 1; + createContext[CpuRegister.Rcx] = 0; + createContext[CpuRegister.R8] = 1; + createContext[CpuRegister.R9] = 0; + Assert.Equal(0, KernelSemaphoreCompatExports.KernelCreateSema(createContext)); + Assert.True(createContext.TryReadUInt64(SemaphoreAddress, out var handle)); + + WriteUInt32(memory, TimeoutAddress, 1_000_000); + var waitContext = new CpuContext(memory, Generation.Gen5); + waitContext[CpuRegister.Rdi] = handle; + waitContext[CpuRegister.Rsi] = 1; + waitContext[CpuRegister.Rdx] = TimeoutAddress; + var signalContext = new CpuContext(memory, Generation.Gen5); + var scheduler = DispatchProxy.Create(); + var proxy = (PendingExceptionScheduler)(object)scheduler; + proxy.DeliverException = context => + { + Assert.Same(waitContext, context); + Assert.Equal( + 0, + KernelSemaphoreCompatExports.KernelSignalSema( + signalContext, + unchecked((uint)handle), + 1)); + }; + + var previousScheduler = GuestThreadExecution.Scheduler; + try + { + GuestThreadExecution.Scheduler = scheduler; + Assert.Equal(0, KernelSemaphoreCompatExports.KernelWaitSema(waitContext)); + Assert.Equal(1, proxy.DeliverCalls); + } + finally + { + GuestThreadExecution.Scheduler = previousScheduler; + createContext[CpuRegister.Rdi] = handle; + Assert.Equal(0, KernelSemaphoreCompatExports.KernelDeleteSema(createContext)); + } + } + + [Fact] + public void SyncOnAddressHostWaitDeliversPendingExceptionBeforeReturning() + { + var memory = new FakeCpuMemory(MemoryBase, 0x1000); + WriteUInt32(memory, ValueAddress, 7); + WriteUInt32(memory, TimeoutAddress, 1_000_000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = ValueAddress; + context[CpuRegister.Rsi] = 7; + context[CpuRegister.Rdx] = TimeoutAddress; + var scheduler = DispatchProxy.Create(); + var proxy = (PendingExceptionScheduler)(object)scheduler; + proxy.DeliverException = deliveredContext => + { + Assert.Same(context, deliveredContext); + WriteUInt32(memory, ValueAddress, 8); + }; + + var previousScheduler = GuestThreadExecution.Scheduler; + try + { + GuestThreadExecution.Scheduler = scheduler; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelSyncOnAddressCompatExports.SyncOnAddressWait32(context)); + Assert.Equal(1, proxy.DeliverCalls); + } + finally + { + GuestThreadExecution.Scheduler = previousScheduler; + } + } + + private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value) + { + Span bytes = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); + Assert.True(memory.TryWrite(address, bytes)); + } + + public class PendingExceptionScheduler : DispatchProxy + { + private int _deliverCalls; + + public Action? DeliverException { get; set; } + + public int DeliverCalls => Volatile.Read(ref _deliverCalls); + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + if (targetMethod?.Name == nameof(IGuestThreadScheduler.DeliverPendingGuestExceptionIfReady)) + { + Interlocked.Increment(ref _deliverCalls); + DeliverException!((CpuContext)args![0]!); + return null; + } + + if (targetMethod?.Name == nameof(IGuestThreadScheduler.WakeBlockedThreads)) + { + return 0; + } + + throw new NotSupportedException(targetMethod?.Name); + } + } +} diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelSemaphoreCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelSemaphoreCompatExportsTests.cs new file mode 100644 index 0000000..6377e0d --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelSemaphoreCompatExportsTests.cs @@ -0,0 +1,38 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using Xunit; + +namespace SharpEmu.Libs.Tests.Kernel; + +public sealed class KernelSemaphoreCompatExportsTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const ulong SemaphoreAddress = MemoryBase + 0x100; + private const ulong NameAddress = MemoryBase + 0x200; + + [Fact] + public void KernelCreateSemaWritesPointerSizedHandleAndClearsUpperBits() + { + var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5); + Assert.True(context.TryWriteUInt64(SemaphoreAddress, 0xC0DE_C0DE_CAFE_BA00)); + Assert.True(context.Memory.TryWrite(NameAddress, "unity-worker\0"u8)); + + context[CpuRegister.Rdi] = SemaphoreAddress; + context[CpuRegister.Rsi] = NameAddress; + context[CpuRegister.Rdx] = 1; + context[CpuRegister.Rcx] = 0; + context[CpuRegister.R8] = 1; + context[CpuRegister.R9] = 0; + + Assert.Equal(0, KernelSemaphoreCompatExports.KernelCreateSema(context)); + Assert.True(context.TryReadUInt64(SemaphoreAddress, out var handle)); + Assert.NotEqual(0UL, handle); + Assert.Equal(0UL, handle >> 32); + + context[CpuRegister.Rdi] = handle; + Assert.Equal(0, KernelSemaphoreCompatExports.KernelDeleteSema(context)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelSyncOnAddressCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelSyncOnAddressCompatExportsTests.cs index ec3224d..a1ba3a3 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/KernelSyncOnAddressCompatExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelSyncOnAddressCompatExportsTests.cs @@ -145,6 +145,166 @@ public sealed class KernelSyncOnAddressCompatExportsTests waiter.Resume()); } + [Fact] + public void UmtxWaitReturnsImmediatelyWhenValueDoesNotMatch() + { + var (memory, ctx) = CreateContext(); + WriteUInt64(memory, ValueAddress, 7); + ctx[CpuRegister.Rdi] = ValueAddress; + ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + ctx[CpuRegister.Rdx] = 9; + ctx[CpuRegister.Rcx] = 0; + ctx[CpuRegister.R8] = 0; + + RunAsGuest(0x809, () => Assert.Equal( + 0, + KernelSyncOnAddressCompatExports.UmtxOp(ctx))); + + Assert.False(GuestThreadExecution.TryConsumeCurrentThreadBlock(out _)); + Assert.Equal(0UL, ctx[CpuRegister.Rax]); + } + + [Fact] + public void UmtxWaitAndWakeShareTheAddressWaitRegistry() + { + var (memory, waitContext) = CreateContext(); + var wakeContext = new CpuContext(memory, Generation.Gen5); + WriteUInt64(memory, ValueAddress, 0); + + IGuestThreadBlockWaiter? stagedWaiter = null; + waitContext[CpuRegister.Rdi] = ValueAddress; + waitContext[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + waitContext[CpuRegister.Rdx] = 0; + waitContext[CpuRegister.Rcx] = 0; + waitContext[CpuRegister.R8] = 0; + RunAsGuest(0x80A, () => + { + Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(waitContext)); + Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock( + out var reason, + out _, + out var hasContinuation, + out _, + out var waiter, + out var deadline)); + Assert.Equal("_umtx_op.wait", reason); + Assert.True(hasContinuation); + Assert.Equal(0, deadline); + stagedWaiter = Assert.IsAssignableFrom(waiter); + }); + + wakeContext[CpuRegister.Rdi] = ValueAddress; + wakeContext[CpuRegister.Rsi] = 3; // UMTX_OP_WAKE + wakeContext[CpuRegister.Rdx] = 1; + wakeContext[CpuRegister.Rcx] = 0; + wakeContext[CpuRegister.R8] = 0; + Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(wakeContext)); + + var resumed = Assert.IsAssignableFrom(stagedWaiter); + Assert.True(resumed.TryWake()); + Assert.Equal(0, resumed.Resume()); + } + + [Fact] + public void UmtxZeroTimespecReturnsMinusOneAndTimedOutErrno() + { + var (memory, ctx) = CreateContext(); + WriteUInt64(memory, ValueAddress, 12); + WriteUInt64(memory, TimeoutAddress, 0); + WriteUInt64(memory, TimeoutAddress + sizeof(long), 0); + SetErrnoStorage(ctx); + ctx[CpuRegister.Rdi] = ValueAddress; + ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + ctx[CpuRegister.Rdx] = 12; + ctx[CpuRegister.Rcx] = 0; + ctx[CpuRegister.R8] = TimeoutAddress; + + Assert.Equal(-1, KernelSyncOnAddressCompatExports.UmtxOp(ctx)); + Assert.Equal(ulong.MaxValue, ctx[CpuRegister.Rax]); + Assert.Equal(60, ReadInt32(memory, ctx.FsBase + 0x40)); // ETIMEDOUT + Assert.False(GuestThreadExecution.TryConsumeCurrentThreadBlock(out _)); + } + + [Fact] + public void UmtxRejectsInvalidTimespecWithEinval() + { + var (memory, ctx) = CreateContext(); + WriteUInt64(memory, ValueAddress, 12); + WriteUInt64(memory, TimeoutAddress, 0); + WriteUInt64(memory, TimeoutAddress + sizeof(long), 1_000_000_000); + SetErrnoStorage(ctx); + ctx[CpuRegister.Rdi] = ValueAddress; + ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + ctx[CpuRegister.Rdx] = 12; + ctx[CpuRegister.Rcx] = 0; + ctx[CpuRegister.R8] = TimeoutAddress; + + Assert.Equal(-1, KernelSyncOnAddressCompatExports.UmtxOp(ctx)); + Assert.Equal(22, ReadInt32(memory, ctx.FsBase + 0x40)); // EINVAL + } + + [Fact] + public void UmtxLargeTimespecSaturatesWithoutOverflow() + { + var (memory, ctx) = CreateContext(); + WriteUInt64(memory, ValueAddress, 1); + WriteUInt64(memory, TimeoutAddress, unchecked((ulong)long.MaxValue)); + WriteUInt64(memory, TimeoutAddress + sizeof(long), 999_999_999); + ctx[CpuRegister.Rdi] = ValueAddress; + ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + ctx[CpuRegister.Rdx] = 2; // Already changed, so no host wait. + ctx[CpuRegister.Rcx] = 0; + ctx[CpuRegister.R8] = TimeoutAddress; + + Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(ctx)); + } + + [Fact] + public void UmtxTimedContinuationReturnsPosixTimeout() + { + var (memory, ctx) = CreateContext(); + WriteUInt64(memory, ValueAddress, 21); + WriteUInt64(memory, TimeoutAddress, 0); + WriteUInt64(memory, TimeoutAddress + sizeof(long), 1_000_000); // 1 ms + SetErrnoStorage(ctx); + ctx[CpuRegister.Rdi] = ValueAddress; + ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + ctx[CpuRegister.Rdx] = 21; + ctx[CpuRegister.Rcx] = 0; + ctx[CpuRegister.R8] = TimeoutAddress; + + IGuestThreadBlockWaiter? stagedWaiter = null; + RunAsGuest(0x80B, () => + { + Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(ctx)); + Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock( + out _, + out _, + out _, + out _, + out var waiter, + out var deadline)); + Assert.True(deadline > 0); + stagedWaiter = Assert.IsAssignableFrom(waiter); + }); + + Thread.Sleep(10); + var resumed = Assert.IsAssignableFrom(stagedWaiter); + Assert.Equal(-1, resumed.Resume()); + Assert.Equal(60, ReadInt32(memory, ctx.FsBase + 0x40)); // ETIMEDOUT + } + + [Fact] + public void UmtxExportIsRegisteredForGen5() + { + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + + Assert.True(manager.TryGetExport("04AjkP0jO9U", out var export)); + Assert.Equal("_umtx_op", export.Name); + Assert.Equal("libKernel", export.LibraryName); + } + private static (FakeCpuMemory Memory, CpuContext Context) CreateContext() { var memory = new FakeCpuMemory(BaseAddress, 0x1000); @@ -213,4 +373,16 @@ public sealed class KernelSyncOnAddressCompatExportsTests BinaryPrimitives.WriteUInt64LittleEndian(bytes, value); Assert.True(memory.TryWrite(address, bytes)); } + + private static void SetErrnoStorage(CpuContext ctx) + { + ctx.FsBase = BaseAddress + 0x700; + } + + private static int ReadInt32(FakeCpuMemory memory, ulong address) + { + Span bytes = stackalloc byte[sizeof(int)]; + Assert.True(memory.TryRead(address, bytes)); + return BinaryPrimitives.ReadInt32LittleEndian(bytes); + } } diff --git a/tests/SharpEmu.Libs.Tests/Kernel/SemaphoreSignalProfileTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/SemaphoreSignalProfileTests.cs index c92fada..b183830 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/SemaphoreSignalProfileTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/SemaphoreSignalProfileTests.cs @@ -41,7 +41,8 @@ public sealed class SemaphoreSignalProfileTests [Theory] [InlineData(0, 2, true)] - [InlineData(0, 0, false)] + [InlineData(0, 0, true)] + [InlineData(1, -1, false)] [InlineData(1, int.MaxValue, false)] public void SignalKeepsTokenSemanticsAndRecordsStagesWhenEnabled(uint initial, int count, bool succeeds) { diff --git a/tests/SharpEmu.Libs.Tests/Np/NpWebApi2ExportsTests.cs b/tests/SharpEmu.Libs.Tests/Np/NpWebApi2ExportsTests.cs new file mode 100644 index 0000000..e69ea9e --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Np/NpWebApi2ExportsTests.cs @@ -0,0 +1,78 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Np; +using Xunit; + +namespace SharpEmu.Libs.Tests.Np; + +public sealed class NpWebApi2ExportsTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const int InvalidArgument = unchecked((int)0x80553402); + + [Fact] + public void PushEventRegisterCallback_ValidatesStateAndReturnsCallbackId() + { + var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5); + NpWebApi2Exports.ResetRuntimeState(); + try + { + context[CpuRegister.Rdi] = 1; + context[CpuRegister.Rsi] = 0x1000; + var libraryContextId = NpWebApi2Exports.NpWebApi2Initialize(context); + Assert.True(libraryContextId > 0); + + context[CpuRegister.Rdi] = unchecked((ulong)libraryContextId); + context[CpuRegister.Rsi] = 0x1000_0000; + var userContextId = NpWebApi2Exports.NpWebApi2CreateUserContext(context); + Assert.True(userContextId > 0); + + context[CpuRegister.Rdi] = unchecked((ulong)libraryContextId); + var filterId = NpWebApi2Exports.NpWebApi2PushEventCreateFilter(context); + Assert.True(filterId > 0); + + context[CpuRegister.Rdi] = unchecked((ulong)userContextId); + context[CpuRegister.Rsi] = unchecked((ulong)filterId); + context[CpuRegister.Rdx] = 0x8000_1000; + context[CpuRegister.Rcx] = 0x1234; + Assert.True(NpWebApi2Exports.NpWebApi2PushEventRegisterCallback(context) > 0); + + context[CpuRegister.Rdx] = 0; + Assert.Equal( + InvalidArgument, + NpWebApi2Exports.NpWebApi2PushEventRegisterCallback(context)); + } + finally + { + NpWebApi2Exports.ResetRuntimeState(); + } + } + + [Fact] + public void PushEventRegisterCallback_RegistersForGen5() + { + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + + Assert.True(manager.TryGetExport("fY3QqeNkF8k", out var export)); + Assert.Equal("sceNpWebApi2PushEventRegisterCallback", export.Name); + Assert.Equal("libSceNpWebApi2", export.LibraryName); + } + + [Fact] + public void PushEventDeleteHandle_IsIdempotentAndRegisteredForGen5() + { + var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5); + context[CpuRegister.Rdi] = 1; + context[CpuRegister.Rsi] = 1; + Assert.Equal(0, NpWebApi2Exports.NpWebApi2PushEventDeleteHandle(context)); + + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + Assert.True(manager.TryGetExport("fIATVMo4Y1w", out var export)); + Assert.Equal("sceNpWebApi2PushEventDeleteHandle", export.Name); + Assert.Equal("libSceNpWebApi2", export.LibraryName); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs b/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs index 5dfab21..c3bf2e0 100644 --- a/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs @@ -17,6 +17,7 @@ public sealed class PthreadSemaphoreSemanticsTests [InlineData("C36iRE0F5sE", "scePthreadSemWait")] [InlineData("aishVAiFaYM", "scePthreadSemPost")] [InlineData("H2a+IN9TP0E", "scePthreadSemTrywait")] + [InlineData("fjN6NQHhK8k", "scePthreadSemTimedwait")] [InlineData("GEnUkDZoUwY", "scePthreadSemInit")] [InlineData("Vwc+L05e6oE", "scePthreadSemDestroy")] public void RegistryResolvesPthreadSemaphoreExports(string nid, string exportName) @@ -68,6 +69,41 @@ public sealed class PthreadSemaphoreSemanticsTests DestroySemaphore(context); } + [Fact] + public void PthreadSemTimedWaitTreatsSecondArgumentAsRelativeMicroseconds() + { + var context = CreateContext(); + InitializeSemaphore(context, initialCount: 1); + + context[CpuRegister.Rdi] = SemaphoreAddress; + context[CpuRegister.Rsi] = 250_000; + + Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemTimedWait(context)); + Assert.Equal(0UL, context[CpuRegister.Rax]); + Assert.Equal(0, ReadSemaphoreValue(context)); + + DestroySemaphore(context); + } + + [Fact] + public void PthreadSemTimedWaitWithZeroTimeoutReturnsTimedOutWhenEmpty() + { + var context = CreateContext(); + InitializeSemaphore(context, initialCount: 0); + + context[CpuRegister.Rdi] = SemaphoreAddress; + context[CpuRegister.Rsi] = 0; + + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT, + KernelSemaphoreCompatExports.PthreadSemTimedWait(context)); + Assert.Equal( + unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT), + context[CpuRegister.Rax]); + + DestroySemaphore(context); + } + [Fact] public void PthreadSemInitRejectsNonPrivateFlag() { diff --git a/tests/SharpEmu.Libs.Tests/Pthread/PthreadStackBoundsTests.cs b/tests/SharpEmu.Libs.Tests/Pthread/PthreadStackBoundsTests.cs new file mode 100644 index 0000000..2399637 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Pthread/PthreadStackBoundsTests.cs @@ -0,0 +1,138 @@ +// 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 PthreadStackBoundsCollection +{ + public const string Name = "PthreadStackBounds"; +} + +[Collection(PthreadStackBoundsCollection.Name)] +public sealed class PthreadStackBoundsTests : IDisposable +{ + private const ulong MemoryBase = 0x6_2000_0000; + private const ulong ThreadHandle = 0x1234_5000; + private const ulong AttrAddress = MemoryBase + 0x100; + private const ulong OutStackAddress = MemoryBase + 0x200; + private const ulong OutStackSizeAddress = MemoryBase + 0x208; + private readonly IGuestThreadScheduler? _previousScheduler = GuestThreadExecution.Scheduler; + + [Fact] + public void PosixGetAttrUsesSchedulerBoundsForRemoteHighSlotThread() + { + const ulong stackBase = 0x0000_7FFF_A800_0000; + const ulong stackSize = 0x20_0000; + var scheduler = CreateScheduler(ThreadHandle, stackBase, stackSize); + GuestThreadExecution.Scheduler = scheduler; + + var context = CreateContext(); + context[CpuRegister.Rsp] = stackBase + stackSize - 0xEA0; + context[CpuRegister.Rdi] = ThreadHandle; + context[CpuRegister.Rsi] = AttrAddress; + + Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGetNpPOSIX(context)); + AssertStackBounds(context, stackBase, stackSize); + } + + [Fact] + public void CurrentPrimaryThreadRetainsBoundedStackPointerFallback() + { + const ulong primaryThreadHandle = ThreadHandle + 1; + var stackBase = OperatingSystem.IsWindows() + ? 0x0000_7FFF_F000_0000UL + : 0x0000_6FFF_F000_0000UL; + GuestThreadExecution.Scheduler = CreateScheduler(); + var previousThread = GuestThreadExecution.EnterGuestThread(primaryThreadHandle); + try + { + var context = CreateContext(); + context[CpuRegister.Rsp] = stackBase + 0x1F_F160; + context[CpuRegister.Rdi] = primaryThreadHandle; + context[CpuRegister.Rsi] = AttrAddress + 0x20; + + Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGet(context)); + AssertStackBounds(context, stackBase, 0x20_0000, AttrAddress + 0x20); + } + finally + { + GuestThreadExecution.RestoreGuestThread(previousThread); + } + } + + [Fact] + public void InvalidSchedulerBoundsDoNotReplaceDefaultAttributes() + { + const ulong invalidThreadHandle = ThreadHandle + 2; + GuestThreadExecution.Scheduler = CreateScheduler(invalidThreadHandle, 0, 0x20_0000); + var context = CreateContext(); + context[CpuRegister.Rdi] = invalidThreadHandle; + context[CpuRegister.Rsi] = AttrAddress + 0x40; + + Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGet(context)); + AssertStackBounds(context, 0, 0x10_0000, AttrAddress + 0x40); + } + + public void Dispose() + { + GuestThreadExecution.Scheduler = _previousScheduler; + } + + private static CpuContext CreateContext() => + new(new FakeCpuMemory(MemoryBase, 0x4000), Generation.Gen5); + + private static IGuestThreadScheduler CreateScheduler( + ulong threadHandle = 0, + ulong stackBase = 0, + ulong stackSize = 0) + { + var scheduler = DispatchProxy.Create(); + var proxy = (StackBoundsSchedulerProxy)(object)scheduler; + if (threadHandle != 0) + { + proxy.Bounds[threadHandle] = (stackBase, stackSize); + } + + return scheduler; + } + + private static void AssertStackBounds( + CpuContext context, + ulong expectedBase, + ulong expectedSize, + ulong attrAddress = AttrAddress) + { + context[CpuRegister.Rdi] = attrAddress; + context[CpuRegister.Rsi] = OutStackAddress; + context[CpuRegister.Rdx] = OutStackSizeAddress; + Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGetstackPOSIX(context)); + Assert.True(context.TryReadUInt64(OutStackAddress, out var actualBase)); + Assert.True(context.TryReadUInt64(OutStackSizeAddress, out var actualSize)); + Assert.Equal(expectedBase, actualBase); + Assert.Equal(expectedSize, actualSize); + } + + public class StackBoundsSchedulerProxy : DispatchProxy + { + public Dictionary Bounds { get; } = []; + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + if (targetMethod?.Name == nameof(IGuestThreadScheduler.TryGetGuestThreadStackBounds)) + { + var found = Bounds.TryGetValue((ulong)args![0]!, out var bounds); + args[1] = found ? bounds.Base : 0UL; + args[2] = found ? bounds.Size : 0UL; + return found; + } + + throw new NotSupportedException(targetMethod?.Name); + } + } +}