[Tests] Cover kernel sync, pthread stack bounds and NpWebApi2

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
This commit is contained in:
ParantezTech 2026-10-06 13:44:55 +03:00
parent c6744d9b13
commit 9667375750
8 changed files with 650 additions and 1 deletions

View File

@ -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);
}
}

View File

@ -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<IGuestThreadScheduler, PendingExceptionScheduler>();
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<IGuestThreadScheduler, PendingExceptionScheduler>();
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<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
Assert.True(memory.TryWrite(address, bytes));
}
public class PendingExceptionScheduler : DispatchProxy
{
private int _deliverCalls;
public Action<CpuContext>? 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);
}
}
}

View File

@ -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));
}
}

View File

@ -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<IGuestThreadBlockWaiter>(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<IGuestThreadBlockWaiter>(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<IGuestThreadBlockWaiter>(waiter);
});
Thread.Sleep(10);
var resumed = Assert.IsAssignableFrom<IGuestThreadBlockWaiter>(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<byte> bytes = stackalloc byte[sizeof(int)];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadInt32LittleEndian(bytes);
}
}

View File

@ -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)
{

View File

@ -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);
}
}

View File

@ -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()
{

View File

@ -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<IGuestThreadScheduler, StackBoundsSchedulerProxy>();
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<ulong, (ulong Base, ulong Size)> 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);
}
}
}