diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs index 8b1b2f2..4e1a28a 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs @@ -103,6 +103,15 @@ public sealed partial class DirectExecutionBackend ulong rip = ReadCtxU64(contextRecord, 248); ulong rsp = ReadCtxU64(contextRecord, 152); + // Thread-mode probe: a hardware exception raised while this thread is inside + // the managed import gateway means the VEH->managed reentry happened from + // cooperative GC mode — a ReversePInvokeBadTransition candidate. + if (LogThreadMode && _threadModeGatewayDepth > 0) + { + TraceThreadMode( + $"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}"); + } + if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp)) { return -1; diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index dbec96e..5d07129 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -19,6 +19,16 @@ public sealed partial class DirectExecutionBackend private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr) { + if (LogThreadMode) + { + _threadModeGatewayCalls++; + _threadModeGatewayDepth++; + if (!_threadModeGatewayFirstLogged) + { + _threadModeGatewayFirstLogged = true; + TraceThreadMode($"gateway_first import={importIndex} total={_threadModeGatewayCalls}"); + } + } try { if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend)) @@ -36,6 +46,13 @@ public sealed partial class DirectExecutionBackend $"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}"); return 18446744071562199298uL; } + finally + { + if (LogThreadMode) + { + _threadModeGatewayDepth--; + } + } } private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo) @@ -69,6 +86,12 @@ public sealed partial class DirectExecutionBackend WriteCtxU64(contextRecord, 152, nextRsp); WriteCtxU64(contextRecord, 248, returnRip); Interlocked.Increment(ref _rawSentinelRecoveries); + if (LogThreadMode) + { + TraceThreadMode( + $"sentinel_recover rip=0x{value:X16} -> 0x{returnRip:X16} rsp=0x{rsp:X16} -> 0x{nextRsp:X16} " + + $"gateway_depth={_threadModeGatewayDepth}"); + } return -1; } return 0; @@ -204,6 +227,17 @@ public sealed partial class DirectExecutionBackend cpuContext[CpuRegister.Rax] = 1uL; return 1uL; } + // Backend teardown in progress: unwind this worker to the host instead of + // dispatching. RunGuestThread parks it as Blocked with no wake handler, its + // host thread exits, and RequestGuestThreadTeardown can finish before any + // executable memory is freed. + if (isGuestWorker && + _guestTeardownRequested && + TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "backend teardown")) + { + cpuContext[CpuRegister.Rax] = 0uL; + return 0uL; + } bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid); bool flag = num7 >= 2156221920u && num7 <= 2156225024u; bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u; diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.NativeWorker.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.NativeWorker.cs new file mode 100644 index 0000000..b77cf62 --- /dev/null +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.NativeWorker.cs @@ -0,0 +1,582 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using SharpEmu.HLE; + +namespace SharpEmu.Core.Cpu.Native; + +public sealed partial class DirectExecutionBackend +{ + // Guest entry stubs must not run above CLR-managed frames on a CLR-created thread: + // the suspension/stack-walk machinery then has to traverse a frame chain that is + // interleaved with guest stubs carrying no CLR unwind info, and any window in which + // the thread-mode bookkeeping disagrees with the actual stack (import dispatch, VEH + // redirection) fail-fasts the runtime — the audio_output_thread ~7th-cycle + // "attempted to call a UnmanagedCallersOnly method from managed code" crash. + // + // Guest execution therefore runs on dedicated raw OS threads whose run loop is + // emitted native code. While guest code executes there is not a single managed + // frame on the thread and it sits in preemptive mode, so the GC ignores it + // entirely. The only managed activity on the thread is the reverse-P/Invoke import + // dispatch and the per-run prologue/epilogue, which the CLR supports on foreign + // threads (lazy attach, preemptive between calls). The managed orchestrator + // (RunGuestThread / the main execution thread) parks in a preemptive wait for the + // duration of the run, so its own frame chain is never walked across guest frames. + private static readonly bool NativeGuestWorkersDisabled = + string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal); + + private readonly object _nativeWorkerGate = new(); + private readonly List _allNativeWorkers = new(); + private readonly Stack _idleNativeWorkers = new(); + private bool _nativeWorkersDisposed; + private int _nativeWorkerCreationFailedLogged; + + private const uint StackSizeParamIsAReservation = 0x00010000u; + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern nint CreateThread( + nint lpThreadAttributes, + nuint dwStackSize, + nint lpStartAddress, + nint lpParameter, + uint dwCreationFlags, + out uint lpThreadId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds); + + // Runs an emitted guest entry stub. Preferred path is a pooled native worker + // thread; falls back to the historical inline calli (guest frames above this + // thread's managed frames) when workers are disabled or unavailable. + // + // Callers set the Active* thread-statics before emitting the stub and read the + // yield/forced-exit flags right after this returns, so the worker outcome is + // copied back into this thread's statics before returning. + private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot) + { + var worker = RentNativeGuestExecutor(); + if (worker is null) + { + TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot); + return CallNativeEntry(entryStub); + } + try + { + var state = _activeGuestThreadState; + var nativeReturn = worker.Run( + _activeCpuContext!, + state, + GuestThreadExecution.CurrentGuestThreadHandle, + _activeEntryReturnSentinelRip, + _activeGuestReturnSlotAddress, + (nint)hostRspSlot, + (nint)entryStub, + state?.AffinityMask ?? 0, + out var yieldRequested, + out var yieldReason, + out var forcedExit); + _activeGuestThreadYieldRequested = yieldRequested; + _activeGuestThreadYieldReason = yieldReason; + _activeForcedGuestExit = forcedExit; + return nativeReturn; + } + finally + { + ReturnNativeGuestExecutor(worker); + } + } + + private NativeGuestExecutor? RentNativeGuestExecutor() + { + if (NativeGuestWorkersDisabled) + { + return null; + } + lock (_nativeWorkerGate) + { + if (_nativeWorkersDisposed) + { + return null; + } + if (_idleNativeWorkers.Count > 0) + { + return _idleNativeWorkers.Pop(); + } + } + var worker = NativeGuestExecutor.TryCreate(this); + if (worker is null) + { + if (Interlocked.Exchange(ref _nativeWorkerCreationFailedLogged, 1) == 0) + { + Console.Error.WriteLine( + "[LOADER][WARN] Failed to create a native guest worker thread; falling back to inline guest execution."); + } + return null; + } + lock (_nativeWorkerGate) + { + if (_nativeWorkersDisposed) + { + worker.Dispose(); + return null; + } + _allNativeWorkers.Add(worker); + } + return worker; + } + + private void ReturnNativeGuestExecutor(NativeGuestExecutor worker) + { + lock (_nativeWorkerGate) + { + if (!_nativeWorkersDisposed) + { + _idleNativeWorkers.Push(worker); + return; + } + } + worker.Dispose(); + } + + private void DisposeNativeGuestExecutors() + { + NativeGuestExecutor[] workers; + lock (_nativeWorkerGate) + { + if (_nativeWorkersDisposed) + { + return; + } + _nativeWorkersDisposed = true; + workers = _allNativeWorkers.ToArray(); + _allNativeWorkers.Clear(); + _idleNativeWorkers.Clear(); + } + foreach (var worker in workers) + { + worker.Dispose(); + } + } + + // A pooled raw OS thread that executes guest entry stubs. The run loop is emitted + // native code (no CLR unwind info required — nothing ever unwinds through it): + // + // loop: WaitForSingleObject(work); + // if (*stopFlag) { SetEvent(done); ExitThread(0); } + // rax = RunPrologue(self); // managed: binds per-run ambient, returns stub + // if (rax != 0) eax = rax(); // guest entry stub — zero managed frames below + // RunEpilogue(self, eax); // managed: captures outcome, restores ambient + // SetEvent(done); goto loop; + // + // Workers carry no per-guest identity of their own: the prologue rebinds guest TLS, + // the host-RSP slot, the Active* thread-statics and the GuestThreadExecution ambient + // on every run, so a worker can be reused for any guest thread. + private sealed unsafe class NativeGuestExecutor : IDisposable + { + private const uint LoopStubSize = 512u; + private const uint WorkerStackReservation = 4u * 1024u * 1024u; + + private static nint _waitForSingleObjectAddress; + private static nint _setEventAddress; + private static nint _exitThreadAddress; + + private readonly DirectExecutionBackend _backend; + private readonly AutoResetEvent _workAvailable = new(false); + private readonly AutoResetEvent _workCompleted = new(false); + private GCHandle _selfHandle; + private void* _controlBlock; + private void* _loopStub; + private nint _threadHandle; + private uint _nativeThreadId; + + // Single in-flight run; publication is ordered by the work/done event pair. + private CpuContext? _runContext; + private GuestThreadState? _runState; + private ulong _runGuestThreadHandle; + private ulong _runSentinelRip; + private ulong _runReturnSlotAddress; + private nint _runHostRspSlot; + private nint _runEntryStub; + private ulong _runAffinityMask; + private int _runNativeResult; + private bool _runYieldRequested; + private string? _runYieldReason; + private bool _runForcedExit; + private bool _runPrologueFailed; + + // Prologue -> epilogue carry, only touched on the worker thread. + private DirectExecutionBackend? _prevBackend; + private CpuContext? _prevContext; + private ulong _prevSentinel; + private ulong _prevReturnSlot; + private bool _prevForcedExit; + private bool _prevYieldRequested; + private string? _prevYieldReason; + private GuestThreadState? _prevState; + private ulong _prevGuestThreadHandle; + private nint _prevHostRspSlot; + private int _prevHostThreadId; + private bool _entered; + + private NativeGuestExecutor(DirectExecutionBackend backend) + { + _backend = backend; + } + + public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend) + { + if (!EnsureKernel32Exports()) + { + return null; + } + var executor = new NativeGuestExecutor(backend); + if (!executor.Initialize()) + { + executor.Dispose(); + return null; + } + return executor; + } + + private static bool EnsureKernel32Exports() + { + if (_exitThreadAddress != 0) + { + return _waitForSingleObjectAddress != 0 && _setEventAddress != 0; + } + nint kernel32 = GetModuleHandle("kernel32.dll"); + if (kernel32 == 0) + { + return false; + } + _waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject"); + _setEventAddress = GetProcAddress(kernel32, "SetEvent"); + _exitThreadAddress = GetProcAddress(kernel32, "ExitThread"); + return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0; + } + + private bool Initialize() + { + _selfHandle = GCHandle.Alloc(this); + _controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u); + if (_controlBlock == null) + { + return false; + } + _loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u); + if (_loopStub == null) + { + return false; + } + + var prologuePtr = (nint)(delegate* unmanaged)&RunPrologue; + var epiloguePtr = (nint)(delegate* unmanaged)&RunEpilogue; + var executorHandle = GCHandle.ToIntPtr(_selfHandle); + var workHandle = _workAvailable.SafeWaitHandle.DangerousGetHandle(); + var doneHandle = _workCompleted.SafeWaitHandle.DangerousGetHandle(); + + byte* code = (byte*)_loopStub; + int offset = 0; + void Emit(byte value) => code[offset++] = value; + void EmitMovRcxImm64(ulong value) + { + Emit(0x48); Emit(0xB9); + *(ulong*)(code + offset) = value; + offset += sizeof(ulong); + } + void EmitMovRaxImm64(ulong value) + { + Emit(0x48); Emit(0xB8); + *(ulong*)(code + offset) = value; + offset += sizeof(ulong); + } + void EmitCallRax() + { + Emit(0xFF); Emit(0xD0); + } + void EmitSetDoneEvent() + { + EmitMovRcxImm64((ulong)doneHandle); + EmitMovRaxImm64((ulong)_setEventAddress); + EmitCallRax(); + } + + // Thread entry leaves RSP ≡ 8 (mod 16); after this every call below happens + // at RSP ≡ 0 with shadow space available. + Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x28); // sub rsp, 0x28 + int loopStart = offset; + EmitMovRcxImm64((ulong)workHandle); + Emit(0xBA); // mov edx, INFINITE + *(uint*)(code + offset) = 0xFFFFFFFFu; + offset += sizeof(uint); + EmitMovRaxImm64((ulong)_waitForSingleObjectAddress); + EmitCallRax(); + EmitMovRaxImm64((ulong)_controlBlock); + Emit(0x83); Emit(0x38); Emit(0x00); // cmp dword [rax], 0 + Emit(0x0F); Emit(0x85); // jne stop + int stopJump = offset; + offset += sizeof(int); + EmitMovRcxImm64((ulong)executorHandle); + EmitMovRaxImm64((ulong)prologuePtr); + EmitCallRax(); // rax = entry stub, or 0 on failure + Emit(0x48); Emit(0x85); Emit(0xC0); // test rax, rax + Emit(0x0F); Emit(0x84); // je skipEntry + int skipJump = offset; + offset += sizeof(int); + EmitCallRax(); // guest entry stub -> eax + int skipEntryOffset = offset; + Emit(0x89); Emit(0xC2); // mov edx, eax + EmitMovRcxImm64((ulong)executorHandle); + EmitMovRaxImm64((ulong)epiloguePtr); + EmitCallRax(); + EmitSetDoneEvent(); + Emit(0xE9); // jmp loop + *(int*)(code + offset) = loopStart - (offset + sizeof(int)); + offset += sizeof(int); + int stopOffset = offset; + // Signal done so a Run() racing teardown cannot park forever; a stopped + // worker is never re-rented, so the stale signal is unobservable. + EmitSetDoneEvent(); + Emit(0x31); Emit(0xC9); // xor ecx, ecx + EmitMovRaxImm64((ulong)_exitThreadAddress); + EmitCallRax(); + Emit(0xCC); // int3 (ExitThread never returns) + *(int*)(code + stopJump) = stopOffset - (stopJump + sizeof(int)); + *(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int)); + + uint oldProtect = 0; + if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect)) + { + return false; + } + FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize); + _threadHandle = CreateThread( + 0, + WorkerStackReservation, + (nint)_loopStub, + 0, + StackSizeParamIsAReservation, + out _nativeThreadId); + if (_threadHandle == 0) + { + return false; + } + if (LogThreadMode) + { + TraceThreadMode($"worker_created native_tid={_nativeThreadId} loop=0x{(ulong)_loopStub:X16}"); + } + return true; + } + + public int Run( + CpuContext context, + GuestThreadState? state, + ulong guestThreadHandle, + ulong sentinelRip, + ulong returnSlotAddress, + nint hostRspSlot, + nint entryStub, + ulong affinityMask, + out bool yieldRequested, + out string? yieldReason, + out bool forcedExit) + { + _runContext = context; + _runState = state; + _runGuestThreadHandle = guestThreadHandle; + _runSentinelRip = sentinelRip; + _runReturnSlotAddress = returnSlotAddress; + _runHostRspSlot = hostRspSlot; + _runEntryStub = entryStub; + _runAffinityMask = affinityMask; + _runPrologueFailed = true; + _runYieldRequested = false; + _runYieldReason = null; + _runForcedExit = false; + _workAvailable.Set(); + _workCompleted.WaitOne(); + _runContext = null; + _runState = null; + yieldRequested = _runYieldRequested; + yieldReason = _runYieldReason; + forcedExit = _runForcedExit; + if (_runPrologueFailed) + { + throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)"); + } + return _runNativeResult; + } + + [UnmanagedCallersOnly] + private static nint RunPrologue(nint executorHandle) + { + try + { + var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!; + return executor.EnterRun(); + } + catch (Exception ex) + { + try + { + Console.Error.WriteLine( + $"[LOADER][ERROR] Native guest worker prologue failed: {ex.GetType().Name}: {ex.Message}"); + } + catch + { + } + return 0; + } + } + + [UnmanagedCallersOnly] + private static void RunEpilogue(nint executorHandle, int nativeResult) + { + try + { + var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!; + executor.ExitRun(nativeResult); + } + catch (Exception ex) + { + try + { + Console.Error.WriteLine( + $"[LOADER][ERROR] Native guest worker epilogue failed: {ex.GetType().Name}: {ex.Message}"); + } + catch + { + } + } + } + + private nint EnterRun() + { + var backend = _backend; + _prevBackend = _activeExecutionBackend; + _prevContext = _activeCpuContext; + _prevSentinel = _activeEntryReturnSentinelRip; + _prevReturnSlot = _activeGuestReturnSlotAddress; + _prevForcedExit = _activeForcedGuestExit; + _prevYieldRequested = _activeGuestThreadYieldRequested; + _prevYieldReason = _activeGuestThreadYieldReason; + _prevState = _activeGuestThreadState; + _prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex); + _prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle); + _entered = true; + _activeExecutionBackend = backend; + _activeCpuContext = _runContext; + _activeEntryReturnSentinelRip = _runSentinelRip; + _activeGuestReturnSlotAddress = _runReturnSlotAddress; + _activeForcedGuestExit = false; + _activeGuestThreadYieldRequested = false; + _activeGuestThreadYieldReason = null; + _activeGuestThreadState = _runState; + backend.BindTlsBase(_runContext!); + TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot); + if (_runState is { } state) + { + _prevHostThreadId = Volatile.Read(ref state.HostThreadId); + Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId())); + } + if (_runAffinityMask != 0) + { + backend.ApplyGuestThreadAffinity(_runAffinityMask); + } + _runPrologueFailed = false; + if (LogThreadMode) + { + TraceThreadMode( + $"worker_enter guest=0x{_runGuestThreadHandle:X16} stub=0x{(ulong)_runEntryStub:X16}"); + } + return _runEntryStub; + } + + private void ExitRun(int nativeResult) + { + _runNativeResult = nativeResult; + _runYieldRequested = _activeGuestThreadYieldRequested; + _runYieldReason = _activeGuestThreadYieldReason; + _runForcedExit = _activeForcedGuestExit; + if (!_entered) + { + return; + } + _entered = false; + if (_runState is { } state) + { + Volatile.Write(ref state.HostThreadId, _prevHostThreadId); + } + TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot); + GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle); + _activeExecutionBackend = _prevBackend; + _activeCpuContext = _prevContext; + _activeEntryReturnSentinelRip = _prevSentinel; + _activeGuestReturnSlotAddress = _prevReturnSlot; + _activeForcedGuestExit = _prevForcedExit; + _activeGuestThreadYieldRequested = _prevYieldRequested; + _activeGuestThreadYieldReason = _prevYieldReason; + _activeGuestThreadState = _prevState; + _prevBackend = null; + _prevContext = null; + _prevState = null; + _prevYieldReason = null; + if (LogThreadMode) + { + TraceThreadMode( + $"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X8} yield={_runYieldRequested}"); + } + } + + public void Dispose() + { + if (_controlBlock != null) + { + *(int*)_controlBlock = 1; + } + try + { + _workAvailable.Set(); + } + catch (ObjectDisposedException) + { + } + var exited = _threadHandle == 0; + if (_threadHandle != 0) + { + exited = WaitForSingleObject(_threadHandle, 1000u) == 0u; + CloseHandle(_threadHandle); + _threadHandle = 0; + } + if (!exited) + { + // The worker is still parked in guest code (teardown should have unwound + // it first). Leak the stub, control block, events and GC handle rather + // than have the thread execute freed memory. + Console.Error.WriteLine( + $"[LOADER][WARN] Native guest worker tid={_nativeThreadId} did not stop; leaking its run loop."); + return; + } + if (_loopStub != null) + { + VirtualFree(_loopStub, 0u, 32768u); + _loopStub = null; + } + if (_controlBlock != null) + { + VirtualFree(_controlBlock, 0u, 32768u); + _controlBlock = null; + } + if (_selfHandle.IsAllocated) + { + _selfHandle.Free(); + } + _workAvailable.Dispose(); + _workCompleted.Dispose(); + } + } +} diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 377d85f..f8ba82e 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -424,6 +424,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private void ThreadMain() { var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_guestThreadHandle); + if (LogThreadMode) + { + TraceThreadMode($"runner_start guest=0x{_guestThreadHandle:X16}"); + } try { while (true) @@ -434,6 +438,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return; } + if (LogThreadMode) + { + _threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter); + TraceThreadMode($"runner_run guest=0x{_guestThreadHandle:X16}"); + } try { _work?.Invoke(); @@ -446,6 +455,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } finally { + if (LogThreadMode) + { + TraceThreadMode($"runner_stop guest=0x{_guestThreadHandle:X16}"); + } GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle); } } @@ -469,6 +482,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private readonly Queue _readyGuestThreads = new Queue(); + // Once set, guest worker threads are unwound to the host at their next import + // dispatch and Pump refuses to start new ones. This must happen before the + // runtime frees trampolines or guest memory: workers that keep running native + // guest code during teardown execute freed pages (observed as an execute-AV in + // a MEM_FREE module region plus a CLR "UnmanagedCallersOnly" fatal from a Pump + // thread entering a freed stub). + private volatile bool _guestTeardownRequested; + private int _readyGuestThreadCount; private readonly Dictionary _guestThreads = new Dictionary(); @@ -567,6 +588,33 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private static int _nestedVehTraceCount; + // SHARPEMU_LOG_THREAD_MODE=1 — GC thread-mode corruption investigation. Traces + // every managed<->guest transition per guest-thread run cycle so the last event + // before a ReversePInvokeBadTransition FailFast identifies the corrupting path. + private static readonly bool LogThreadMode = + string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_THREAD_MODE"), "1", StringComparison.Ordinal); + + private static long _threadModeCycleCounter; + + [ThreadStatic] + private static long _threadModeCycleId; + + [ThreadStatic] + private static int _threadModeGatewayDepth; + + [ThreadStatic] + private static long _threadModeGatewayCalls; + + [ThreadStatic] + private static bool _threadModeGatewayFirstLogged; + + private static void TraceThreadMode(string message) + { + Console.Error.WriteLine( + $"[THREADMODE] {message} cycle={_threadModeCycleId} tid={GetCurrentThreadId()} managed={Environment.CurrentManagedThreadId}"); + Console.Error.Flush(); + } + private const uint MEM_COMMIT = 4096u; private const uint MEM_RESERVE = 8192u; @@ -625,7 +673,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private unsafe static int CallNativeEntry(void* entry) { var nativeEntry = (delegate* unmanaged[Cdecl])entry; - return nativeEntry(); + if (!LogThreadMode) + { + return nativeEntry(); + } + + _threadModeGatewayFirstLogged = false; + var dispatchesBefore = _threadModeGatewayCalls; + TraceThreadMode($"native_enter entry=0x{(ulong)entry:X16}"); + var result = nativeEntry(); + TraceThreadMode($"native_exit result=0x{result:X8} dispatches={_threadModeGatewayCalls - dispatchesBefore}"); + return result; } private unsafe static void WriteCtxU64(void* contextRecord, int offset, ulong value) @@ -824,6 +882,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I _importLoopGuardSeconds = GetImportLoopGuardSeconds(); _entryReturnSentinelRip = 0uL; _forcedGuestExit = false; + _guestTeardownRequested = false; _importLoopSignatureCount = 0; _importLoopSignatureWriteIndex = 0; _importLoopPatternHits = 0; @@ -1937,6 +1996,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I byte* code = (byte*)ptr; int offset = 0; + // Native pre-filter: these exception codes are raised while the thread can be in + // cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing + // thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed + // handler then trips the CLR's reverse-P/Invoke check and kills the process with + // "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed + // code" — this is why no managed throw (even one with a catch handler) ever + // survived inside the emulator. Continue the handler search without touching + // managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions + // (Vulkan drivers, host CRT) are excluded too: the managed handler only ever + // returned CONTINUE_SEARCH for them. + ReadOnlySpan nonManagedExceptionCodes = [0xE0434352u, MSVC_CPP_EXCEPTION, 0xC0000409u, 0xC00000FDu]; + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*) + EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode) + var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length]; + for (int i = 0; i < nonManagedExceptionCodes.Length; i++) + { + EmitByte(code, ref offset, 0x3D); // cmp eax, imm32 + EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]); + EmitByte(code, ref offset, 0x74); // je pass + passJumpOffsets[i] = offset; + EmitByte(code, ref offset, 0x00); + } + EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block + int passOffset = offset; + EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH) + EmitByte(code, ref offset, 0xC3); // ret + for (int i = 0; i < nonManagedExceptionCodes.Length; i++) + { + code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1))); + } EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12 EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13 EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp @@ -2621,6 +2710,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I public void Pump(CpuContext callerContext, string reason) { _ = callerContext; + if (_guestTeardownRequested) + { + return; + } var runSynchronously = string.Equals(reason, "entry_return", StringComparison.Ordinal); WakeExpiredBlockedGuestThreads(); if (Volatile.Read(ref _readyGuestThreadCount) == 0) @@ -3145,6 +3238,45 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I continuationThread.Join(); } + /// + /// Parks every guest worker thread before the caller starts freeing executable + /// memory. Workers unwind to the host at their next import dispatch (see the + /// teardown check in DispatchImport); this waits for their host threads to + /// finish within . Returns false when at least one + /// worker is still running — the caller must then leak its executable stubs + /// rather than free memory a live thread may still execute. + /// + private bool RequestGuestThreadTeardown(int timeoutMs) + { + _guestTeardownRequested = true; + Thread[] hostThreads; + lock (_guestThreadGate) + { + _readyGuestThreads.Clear(); + Interlocked.Exchange(ref _readyGuestThreadCount, 0); + hostThreads = _guestThreads.Values + .Select(static thread => thread.HostThread) + .Where(static host => host is not null && host != Thread.CurrentThread && host.IsAlive) + .Cast() + .ToArray(); + } + + var deadline = Environment.TickCount64 + timeoutMs; + var allStopped = true; + foreach (var host in hostThreads) + { + var remaining = (int)Math.Max(1L, deadline - Environment.TickCount64); + if (!host.Join(remaining) && host.IsAlive) + { + allStopped = false; + Console.Error.WriteLine( + $"[LOADER][WARN] Guest worker host thread '{host.Name}' still running after teardown wait."); + } + } + + return allStopped; + } + private void ClearGuestThreads() { GuestContinuationRunner[] runners; @@ -3424,6 +3556,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I ApplyGuestThreadAffinity(thread.AffinityMask); Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId())); _activeGuestThreadState = thread; + if (LogThreadMode) + { + _threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter); + TraceThreadMode( + $"cycle_start name='{thread.Name}' guest=0x{thread.ThreadHandle:X16} reason={reason} " + + $"rsp_slot=0x{(ulong)TlsGetValue(_hostRspSlotTlsIndex):X}"); + } try { LastError = null; @@ -3498,6 +3637,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } finally { + if (LogThreadMode) + { + TraceThreadMode( + $"cycle_end name='{thread.Name}' state={thread.State} " + + $"imports={Interlocked.Read(ref thread.ImportCount)} " + + $"rsp_slot=0x{(ulong)TlsGetValue(_hostRspSlotTlsIndex):X}"); + } _activeGuestThreadState = previousGuestThreadState; Volatile.Write(ref thread.HostThreadId, 0); GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle); @@ -3572,173 +3718,175 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I var previousYieldRequested = _activeGuestThreadYieldRequested; var previousYieldReason = _activeGuestThreadYieldReason; nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex); + if (LogThreadMode) + { + TraceThreadMode( + $"entry_setup name='{name}' entry=0x{entryPoint:X16} stub=0x{(ulong)ptr:X16} " + + $"guest_rsp=0x{context[CpuRegister.Rsp]:X16} rsp_slot_prev=0x{(ulong)previousHostRspSlotValue:X}"); + } try - { - _activeExecutionBackend = this; - _activeCpuContext = context; - _activeEntryReturnSentinelRip = 0; - _activeGuestReturnSlotAddress = 0; - _activeForcedGuestExit = false; - _activeGuestThreadYieldRequested = false; - _activeGuestThreadYieldReason = null; - BindTlsBase(context); - byte* ptr2 = (byte*)ptr; - ulong hostRspSlot = (ulong)ptr + stubSize - 16uL; - int offset = 0; - ptr2[offset++] = 83; - ptr2[offset++] = 85; - ptr2[offset++] = 87; - ptr2[offset++] = 86; - ptr2[offset++] = 65; - ptr2[offset++] = 84; - ptr2[offset++] = 65; - ptr2[offset++] = 85; - ptr2[offset++] = 65; - ptr2[offset++] = 86; - ptr2[offset++] = 65; - ptr2[offset++] = 87; - EmitHostNonvolatileXmmSave(ptr2, ref offset); - ptr2[offset++] = 73; - ptr2[offset++] = 186; - *(ulong*)(ptr2 + offset) = hostRspSlot; - offset += 8; - ptr2[offset++] = 73; - ptr2[offset++] = 137; - ptr2[offset++] = 34; - ptr2[offset++] = 72; - ptr2[offset++] = 184; - *(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp]; - offset += 8; - ptr2[offset++] = 72; - ptr2[offset++] = 137; - ptr2[offset++] = 196; - ptr2[offset++] = 72; - ptr2[offset++] = 131; - ptr2[offset++] = 236; - ptr2[offset++] = 8; - ptr2[offset++] = 72; - ptr2[offset++] = 189; - *(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp]; - offset += 8; - ptr2[offset++] = 72; - ptr2[offset++] = 184; - *(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi]; - offset += 8; - ptr2[offset++] = 72; - ptr2[offset++] = 137; - ptr2[offset++] = 199; - ptr2[offset++] = 72; - ptr2[offset++] = 184; - *(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi]; - offset += 8; - ptr2[offset++] = 72; - ptr2[offset++] = 137; - ptr2[offset++] = 198; - ptr2[offset++] = 72; - ptr2[offset++] = 184; - *(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx]; - offset += 8; - ptr2[offset++] = 72; - ptr2[offset++] = 137; - ptr2[offset++] = 194; - ptr2[offset++] = 72; - ptr2[offset++] = 184; - *(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx]; - offset += 8; - ptr2[offset++] = 72; - ptr2[offset++] = 137; - ptr2[offset++] = 193; - ptr2[offset++] = 72; - ptr2[offset++] = 184; - *(ulong*)(ptr2 + offset) = entryPoint; - offset += 8; - ptr2[offset++] = byte.MaxValue; - ptr2[offset++] = 208; - int sentinelOffset = offset + 4; - ptr2[offset++] = 72; - ptr2[offset++] = 131; - ptr2[offset++] = 196; - ptr2[offset++] = 8; - ptr2[offset++] = 73; - ptr2[offset++] = 186; - *(ulong*)(ptr2 + offset) = hostRspSlot; - offset += 8; - ptr2[offset++] = 73; - ptr2[offset++] = 139; - ptr2[offset++] = 34; - EmitHostNonvolatileXmmRestore(ptr2, ref offset); - ptr2[offset++] = 65; - ptr2[offset++] = 95; - ptr2[offset++] = 65; - ptr2[offset++] = 94; - ptr2[offset++] = 65; - ptr2[offset++] = 93; - ptr2[offset++] = 65; - ptr2[offset++] = 92; - ptr2[offset++] = 94; - ptr2[offset++] = 95; - ptr2[offset++] = 93; - ptr2[offset++] = 91; - ptr2[offset++] = 195; - ulong sentinel = (ulong)ptr + (ulong)sentinelOffset; - ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub; - _activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL; - if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel)) - { - reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}"; - return GuestNativeCallExitReason.Exception; - } - uint oldProtect = default(uint); - if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect)) - { - reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}"; - return GuestNativeCallExitReason.Exception; - } - FlushInstructionCache(GetCurrentProcess(), ptr, stubSize); - TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot); - ActiveGuestThreadYieldRequested = false; - ActiveGuestThreadYieldReason = null; - try - { - var nativeReturn = CallNativeEntry(ptr); - if (ActiveGuestThreadYieldRequested) - { - reason = ActiveGuestThreadYieldReason ?? "guest thread blocked"; - return GuestNativeCallExitReason.Blocked; - } - if (ActiveForcedGuestExit) - { - reason = LastError ?? "guest thread forced exit"; - return GuestNativeCallExitReason.ForcedExit; - } - reason = $"returned 0x{nativeReturn:X8}"; - return GuestNativeCallExitReason.Returned; - } - catch (AccessViolationException ex) - { - reason = "access violation: " + ex.Message; - return GuestNativeCallExitReason.Exception; - } - catch (Exception ex) - { - reason = ex.GetType().Name + ": " + ex.Message; - return GuestNativeCallExitReason.Exception; - } - } - finally - { - TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue); - RestoreActiveExecutionThread( - previousActiveBackend, - previousActiveContext, - previousSentinel, - previousReturnSlotAddress, - previousForcedExit, - previousYieldRequested, - previousYieldReason); - VirtualFree(ptr, 0u, 32768u); - } - } + { + _activeExecutionBackend = this; + _activeCpuContext = context; + _activeEntryReturnSentinelRip = 0; + _activeGuestReturnSlotAddress = 0; + _activeForcedGuestExit = false; + _activeGuestThreadYieldRequested = false; + _activeGuestThreadYieldReason = null; + BindTlsBase(context); + byte* ptr2 = (byte*)ptr; + ulong hostRspSlot = (ulong)ptr + stubSize - 16uL; + int offset = 0; + ptr2[offset++] = 83; + ptr2[offset++] = 85; + ptr2[offset++] = 87; + ptr2[offset++] = 86; + ptr2[offset++] = 65; + ptr2[offset++] = 84; + ptr2[offset++] = 65; + ptr2[offset++] = 85; + ptr2[offset++] = 65; + ptr2[offset++] = 86; + ptr2[offset++] = 65; + ptr2[offset++] = 87; + EmitHostNonvolatileXmmSave(ptr2, ref offset); + ptr2[offset++] = 73; + ptr2[offset++] = 186; + *(ulong*)(ptr2 + offset) = hostRspSlot; + offset += 8; + ptr2[offset++] = 73; + ptr2[offset++] = 137; + ptr2[offset++] = 34; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 137; + ptr2[offset++] = 196; + ptr2[offset++] = 72; + ptr2[offset++] = 131; + ptr2[offset++] = 236; + ptr2[offset++] = 8; + ptr2[offset++] = 72; + ptr2[offset++] = 189; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 137; + ptr2[offset++] = 199; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 137; + ptr2[offset++] = 198; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 137; + ptr2[offset++] = 194; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 137; + ptr2[offset++] = 193; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = entryPoint; + offset += 8; + ptr2[offset++] = byte.MaxValue; + ptr2[offset++] = 208; + int sentinelOffset = offset + 4; + ptr2[offset++] = 72; + ptr2[offset++] = 131; + ptr2[offset++] = 196; + ptr2[offset++] = 8; + ptr2[offset++] = 73; + ptr2[offset++] = 186; + *(ulong*)(ptr2 + offset) = hostRspSlot; + offset += 8; + ptr2[offset++] = 73; + ptr2[offset++] = 139; + ptr2[offset++] = 34; + EmitHostNonvolatileXmmRestore(ptr2, ref offset); + ptr2[offset++] = 65; + ptr2[offset++] = 95; + ptr2[offset++] = 65; + ptr2[offset++] = 94; + ptr2[offset++] = 65; + ptr2[offset++] = 93; + ptr2[offset++] = 65; + ptr2[offset++] = 92; + ptr2[offset++] = 94; + ptr2[offset++] = 95; + ptr2[offset++] = 93; + ptr2[offset++] = 91; + ptr2[offset++] = 195; + ulong sentinel = (ulong)ptr + (ulong)sentinelOffset; + ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub; + _activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL; + if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel)) + { + reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}"; + return GuestNativeCallExitReason.Exception; + } + uint oldProtect = default(uint); + if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect)) + { + reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}"; + return GuestNativeCallExitReason.Exception; + } + FlushInstructionCache(GetCurrentProcess(), ptr, stubSize); + ActiveGuestThreadYieldRequested = false; + ActiveGuestThreadYieldReason = null; + var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot); + if (ActiveGuestThreadYieldRequested) + { + reason = ActiveGuestThreadYieldReason ?? "guest thread blocked"; + return GuestNativeCallExitReason.Blocked; + } + if (ActiveForcedGuestExit) + { + reason = LastError ?? "guest thread forced exit"; + return GuestNativeCallExitReason.ForcedExit; + } + reason = $"returned 0x{nativeReturn:X8}"; + return GuestNativeCallExitReason.Returned; + } + catch (AccessViolationException ex) + { + reason = "access violation: " + ex.Message; + return GuestNativeCallExitReason.Exception; + } + catch (Exception ex) + { + reason = ex.GetType().Name + ": " + ex.Message; + return GuestNativeCallExitReason.Exception; + } + finally + { + TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue); + RestoreActiveExecutionThread( + previousActiveBackend, + previousActiveContext, + previousSentinel, + previousReturnSlotAddress, + previousForcedExit, + previousYieldRequested, + previousYieldReason); + VirtualFree(ptr, 0u, 32768u); + } +} private unsafe GuestNativeCallExitReason ExecuteGuestContinuationEntry( CpuContext context, @@ -3768,6 +3916,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I var previousYieldRequested = _activeGuestThreadYieldRequested; var previousYieldReason = _activeGuestThreadYieldReason; nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex); + if (LogThreadMode) + { + TraceThreadMode( + $"continuation_setup name='{name}' resume=0x{entryPoint:X16} stub=0x{(ulong)ptr:X16} " + + $"guest_rsp=0x{context[CpuRegister.Rsp]:X16} rsp_slot_prev=0x{(ulong)previousHostRspSlotValue:X}"); + } try { _activeExecutionBackend = this; @@ -3836,12 +3990,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return GuestNativeCallExitReason.Exception; } FlushInstructionCache(GetCurrentProcess(), ptr, stubSize); - TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot); ActiveGuestThreadYieldRequested = false; ActiveGuestThreadYieldReason = null; + try { - var nativeReturn = CallNativeEntry(ptr); + var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot); if (ActiveGuestThreadYieldRequested) { reason = ActiveGuestThreadYieldReason ?? "guest thread blocked"; @@ -4117,7 +4271,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I int num6 = -1; try { - num6 = CallNativeEntry(ptr); + num6 = RunGuestEntryStub(ptr, num2); Console.Error.WriteLine($"[LOADER][INFO] Guest returned: {num6}"); PumpUntilGuestThreadsIdle(context, "entry_return"); } @@ -4144,6 +4298,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I LastError = "Detected repeating import loop and forced guest unwind to host."; } Console.Error.WriteLine("[LOADER][ERROR] " + LastError); + RequestGuestThreadTeardown(3000); return false; } if (num6 == 0) @@ -4158,6 +4313,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I LastError = $"Guest entry point returned non-zero: {num6}"; } Console.Error.WriteLine("[LOADER][ERROR] " + LastError); + RequestGuestThreadTeardown(3000); return false; } finally @@ -4482,6 +4638,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I public unsafe void Dispose() { + if (!RequestGuestThreadTeardown(2000)) + { + // A guest worker is still executing native code; freeing the trampolines, + // exception-handler stubs, or GC handles under it turns process exit into + // an execute-AV / CLR fatal. Leak them — the process is going away anyway. + Console.Error.WriteLine( + "[LOADER][WARN] Skipping executable stub teardown: guest worker threads are still running."); + return; + } + // Native guest workers park idle once every guest thread has unwound; stop + // them before any executable stub or TLS index they reference is freed. + DisposeNativeGuestExecutors(); ClearImportHandlerTrampolines(); _importEntries = Array.Empty(); _runtimeSymbolsByName.Clear(); diff --git a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs index 3221d65..6a616f4 100644 --- a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs +++ b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs @@ -8,9 +8,14 @@ namespace SharpEmu.Libs.Audio; public static class AudioOut2Exports { - private const int AudioOut2ContextParamSize = 0x80; + // Sized from guest evidence, not SDK headers: Quake keeps its + // SceAudioOut2ContextParam on the stack with the frame canary at param+0x60, + // and an earlier 0x80-byte ResetParam write zeroed that canary + // (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed + // the whole audio init). Stay well below 0x60 and only write the prefix we + // populate. + private const int AudioOut2ContextParamSize = 0x30; private const int AudioOut2ContextMemorySize = 0x10000; - private const int AudioOut2ContextMemoryAlignment = 0x10000; private static long _nextContextHandle = 1; private static long _nextUserHandle = 1; private static int _nextPortId; @@ -59,20 +64,13 @@ public static class AudioOut2Exports public static int AudioOut2ContextQueryMemory(CpuContext ctx) { var paramAddress = ctx[CpuRegister.Rdi]; - var memoryInfoAddress = ctx[CpuRegister.Rsi]; - if (paramAddress == 0 || memoryInfoAddress == 0) + var outMemorySizeAddress = ctx[CpuRegister.Rsi]; + if (paramAddress == 0 || outMemorySizeAddress == 0) { return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - - Span memoryInfo = stackalloc byte[0x20]; - memoryInfo.Clear(); - BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize); - BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment); - BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize); - BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment); - - return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo) + + return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize) ? ctx.SetReturn(0) : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } diff --git a/src/SharpEmu.Libs/Ime/ImeExports.cs b/src/SharpEmu.Libs/Ime/ImeExports.cs new file mode 100644 index 0000000..7235731 --- /dev/null +++ b/src/SharpEmu.Libs/Ime/ImeExports.cs @@ -0,0 +1,46 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Ime; + +public static class ImeExports +{ + // Quake (KEX) calls this from its main loop and from the audio bring-up path with + // an event-handler pointer. No IME session ever exists here, so report success + // without invoking the handler ("no pending IME events"). This NID was previously + // misbound as an sceNgs2VoiceControl alias, which fed the game NGS2 errors. + [SysAbiExport( + Nid = "-4GCfYdNF1s", + ExportName = "sceImeUpdate", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceIme")] + public static int ImeUpdate(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "eaFXjfJv3xs", + ExportName = "sceImeKeyboardOpen", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceIme")] + public static int ImeKeyboardOpen(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "dKadqZFgKKQ", + ExportName = "sceImeKeyboardGetResourceId", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceIme")] + public static int ImeKeyboardGetResourceId(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } +} diff --git a/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs index d1e30bc..83f0261 100644 --- a/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs @@ -149,6 +149,19 @@ public static class KernelAprCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + // Success stub: the argument layout is unknown and callers tolerate the + // empty answer (Quake streams fine), so no output payload is written until + // the real signature is reversed. + [SysAbiExport( + Nid = "WvEu7yl3Ivg", + ExportName = "sceKernelAprGetFileSize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAprGetFileSize(CpuContext ctx) + { + return ctx.SetReturn(0); + } + private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress) { Span result = stackalloc byte[sizeof(ulong)]; diff --git a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs index 34b8bc9..5cd41c8 100644 --- a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs @@ -19,10 +19,15 @@ public static class KernelRuntimeCompatExports internal const int ClockProf = 2; internal const int ClockMonotonic = 4; internal const int ClockUptime = 5; + internal const int ClockUptimePrecise = 7; + internal const int ClockUptimeFast = 8; internal const int ClockRealtimePrecise = 9; - internal const int ClockMonotonicPrecise = 11; internal const int ClockRealtimeFast = 10; + internal const int ClockMonotonicPrecise = 11; internal const int ClockMonotonicFast = 12; + internal const int ClockSecond = 13; + internal const int ClockThreadCputimeId = 14; + internal const int ClockProcTime = 15; private const int Efault = 14; private const int Einval = 22; private const ulong TlsErrnoOffset = 0x40; @@ -723,10 +728,24 @@ public static class KernelRuntimeCompatExports return true; } + // CLOCK_SECOND is FreeBSD's cached whole-second realtime clock (Quake's + // audio_output_thread polls it and treated the previous EINVAL as a fatal + // init failure, exiting and getting respawned in a loop). + case ClockSecond: + seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + nanoseconds = 0; + return true; + case ClockMonotonic: case ClockMonotonicPrecise: case ClockMonotonicFast: case ClockUptime: + case ClockUptimePrecise: + case ClockUptimeFast: + // Per-thread/process CPU time approximated with the monotonic clock; games + // use these for profiling deltas where monotonicity matters, not absolutes. + case ClockThreadCputimeId: + case ClockProcTime: GetProcessMonotonicTime(out seconds, out nanoseconds); return true; diff --git a/src/SharpEmu.Libs/Mouse/MouseExports.cs b/src/SharpEmu.Libs/Mouse/MouseExports.cs new file mode 100644 index 0000000..7a74f4f --- /dev/null +++ b/src/SharpEmu.Libs/Mouse/MouseExports.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Mouse; + +public static class MouseExports +{ + // Returns 0 read entries: no mouse is connected. This NID was previously misbound + // as an sceNgs2VoiceGetState alias. + [SysAbiExport( + Nid = "x8qnXqh-tiM", + ExportName = "sceMouseRead", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceMouse")] + public static int MouseRead(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "RaqxZIf6DvE", + ExportName = "sceMouseOpen", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceMouse")] + public static int MouseOpen(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } +} diff --git a/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs b/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs index a21fced..38f92bd 100644 --- a/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs +++ b/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs @@ -203,13 +203,12 @@ public static class Ngs2Exports } } - [SysAbiExport( - Nid = "-4GCfYdNF1s", - ExportName = "sceNgs2VoiceControl", - Target = Generation.Gen4 | Generation.Gen5, - LibraryName = "libSceNgs2")] - public static int Ngs2VoiceControlAlt(CpuContext ctx) => Ngs2VoiceControl(ctx); - + // The earlier "alt NID" guesses here were wrong: an NID is the aerolib hash of one + // symbol name, and hashing scripts/ps5_names.txt shows the previous alt bindings + // actually belonged to sceImeUpdate (-4GCfYdNF1s), sceMouseRead (x8qnXqh-tiM) and + // sceSystemGestureUpdateAllTouchRecognizer (wPJGwI2RM2I) — Quake's audio thread was + // receiving an NGS2 error from what it thought was sceImeUpdate. Those now live in + // their real libraries; the NIDs below are verified against the hash. [SysAbiExport( Nid = "AbYvTOZ8Pts", ExportName = "sceNgs2VoiceRunCommands", @@ -219,25 +218,25 @@ public static class Ngs2Exports [SysAbiExport( Nid = "-TOuuAQ-buE", - ExportName = "sceNgs2VoiceRunCommands", - Target = Generation.Gen4 | Generation.Gen5, - LibraryName = "libSceNgs2")] - public static int Ngs2VoiceRunCommandsAlt(CpuContext ctx) => Ngs2VoiceControl(ctx); - - [SysAbiExport( - Nid = "x8qnXqh-tiM", ExportName = "sceNgs2VoiceGetState", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNgs2")] public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0); [SysAbiExport( - Nid = "wPJGwI2RM2I", + Nid = "rEh728kXk3w", ExportName = "sceNgs2VoiceGetStateFlags", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNgs2")] public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0); + [SysAbiExport( + Nid = "xa8oL9dmXkM", + ExportName = "sceNgs2PanInit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNgs2")] + public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0); + [SysAbiExport( Nid = "i0VnXM-C9fc", ExportName = "sceNgs2SystemRender", diff --git a/src/SharpEmu.Libs/Np/NpManagerExports.cs b/src/SharpEmu.Libs/Np/NpManagerExports.cs index 963e188..61bccb9 100644 --- a/src/SharpEmu.Libs/Np/NpManagerExports.cs +++ b/src/SharpEmu.Libs/Np/NpManagerExports.cs @@ -33,6 +33,30 @@ public static class NpManagerExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + // Offline profile: the online id payload is left untouched and the call + // reports success, matching the other offline NpManager stubs here. + [SysAbiExport( + Nid = "XDncXQIJUSk", + ExportName = "sceNpGetOnlineId", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNpManager")] + public static int NpGetOnlineId(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "e-ZuhGEoeC4", + ExportName = "sceNpGetNpReachabilityState", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNpManager")] + public static int NpGetNpReachabilityState(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "VfRSmPmj8Q8", ExportName = "sceNpRegisterStateCallback", diff --git a/src/SharpEmu.Libs/Np/NpUniversalDataSystemExports.cs b/src/SharpEmu.Libs/Np/NpUniversalDataSystemExports.cs index be66cdd..787e898 100644 --- a/src/SharpEmu.Libs/Np/NpUniversalDataSystemExports.cs +++ b/src/SharpEmu.Libs/Np/NpUniversalDataSystemExports.cs @@ -76,4 +76,14 @@ public static class NpUniversalDataSystemExports { return ctx.SetReturn(0, typeof(long)); } + + [SysAbiExport( + Nid = "AUIHb7jUX3I", + ExportName = "sceNpUniversalDataSystemDestroyHandle", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNpUniversalDataSystem")] + public static int NpUniversalDataSystemDestroyHandle(CpuContext ctx) + { + return ctx.SetReturn(0, typeof(long)); + } } diff --git a/src/SharpEmu.Libs/SystemGesture/SystemGestureExports.cs b/src/SharpEmu.Libs/SystemGesture/SystemGestureExports.cs index f3d3a48..97a4c34 100644 --- a/src/SharpEmu.Libs/SystemGesture/SystemGestureExports.cs +++ b/src/SharpEmu.Libs/SystemGesture/SystemGestureExports.cs @@ -83,4 +83,15 @@ public static class SystemGestureExports ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + + [SysAbiExport( + Nid = "wPJGwI2RM2I", + ExportName = "sceSystemGestureUpdateAllTouchRecognizer", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceSystemGesture")] + public static int SystemGestureUpdateAllTouchRecognizer(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } } diff --git a/src/SharpEmu.Libs/Ult/UltExports.cs b/src/SharpEmu.Libs/Ult/UltExports.cs new file mode 100644 index 0000000..ff8754f --- /dev/null +++ b/src/SharpEmu.Libs/Ult/UltExports.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Ult; + +public static class UltExports +{ + // Games initialize the ULT (user-level thread) runtime before spinning up + // their job systems; an unresolved import here (0x80020002) makes engines + // treat the whole task system as unavailable. The emulator schedules guest + // pthreads natively, so accepting the initialization is sufficient. + [SysAbiExport( + Nid = "hZIg1EWGsHM", + ExportName = "sceUltInitialize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceUlt")] + public static int UltInitialize(CpuContext ctx) + { + return ctx.SetReturn(0, typeof(long)); + } +}