diff --git a/src/SharpEmu.CLI/Program.cs b/src/SharpEmu.CLI/Program.cs index ceb17ee..7200085 100644 --- a/src/SharpEmu.CLI/Program.cs +++ b/src/SharpEmu.CLI/Program.cs @@ -520,7 +520,18 @@ internal static partial class Program return false; } - string[] childArgs = [MitigatedChildFlag, .. args]; + string[] childArgs; + var commandLineArgs = Environment.GetCommandLineArgs(); + var entryAssembly = commandLineArgs.Length != 0 ? commandLineArgs[0] : null; + if (Path.GetFileNameWithoutExtension(processPath).Equals("dotnet", StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrWhiteSpace(entryAssembly)) + { + childArgs = [entryAssembly, MitigatedChildFlag, .. args]; + } + else + { + childArgs = [MitigatedChildFlag, .. args]; + } var commandLine = BuildCommandLine(processPath, childArgs); var startupInfoEx = new STARTUPINFOEX(); diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs index ffa7b9a..656f53f 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs @@ -148,11 +148,6 @@ public sealed partial class DirectExecutionBackend { return -1; } - if (exceptionCode == 3221225477u && - TryRecoverInflateFastEpilogue(exceptionRecord, contextRecord, rip, rsp)) - { - return -1; - } if (exceptionCode == StatusIllegalInstruction && TryRecoverIllegalInstruction(contextRecord, rip)) { @@ -676,179 +671,6 @@ public sealed partial class DirectExecutionBackend return true; } - private unsafe bool TryRecoverInflateFastEpilogue(EXCEPTION_RECORD* exceptionRecord, void* contextRecord, ulong rip, ulong rsp) - { - // Only apply this compat recovery for Ghostrunner (PPSA03685) - var currentTitleId = CurrentTitleId ?? SharpEmu.Libs.VideoOut.VideoOutExports.GetApplicationTitleId(); - if (!string.Equals(currentTitleId, "PPSA03685", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - const ulong epilogueStoreRip = 0x0000000804A2C23EuL; - const ulong epilogueStateStoreRip = 0x0000000804A2C25AuL; - - if (rip != epilogueStoreRip && rip != epilogueStateStoreRip) - { - return false; - } - - // Check if this is the state fault at 0x804A2C25A (mov [rcx + 0x48], rax) - if (rip == epilogueStateStoreRip) - { - ulong rcxFault = ReadCtxU64(contextRecord, CTX_RCX); - if (rcxFault >= 0x10000UL && exceptionRecord->ExceptionInformation[1] != 0x48) - { - return false; - } - - Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE STATE RECOVERY TRIGGERED AT 0x804A2C25A <<<"); - - // Recover strm from rdi or caller frame - ulong strm = ReadCtxU64(contextRecord, CTX_RDI); - if (strm < 0x10000UL) - { - if (TryReadQword(rsp + 0x108, out ulong candidate) && candidate >= 0x10000) - { - strm = candidate; - } - else if (TryReadQword(rsp + 0x80, out ulong savedRbp)) - { - if (TryReadQword((ulong)((long)savedRbp - 0x58), out ulong candidate2) && candidate2 >= 0x10000) - { - strm = candidate2; - } - else if (TryReadQword(savedRbp + 0x20, out ulong candidate3) && candidate3 >= 0x10000) - { - strm = candidate3; - } - } - } - - if (strm < 0x10000UL) - { - Console.Error.WriteLine("[LOADER][ERROR] Could not locate strm for state recovery at 0x804A2C25A!"); - return false; - } - - ulong state = *(ulong*)(strm + 0x38); - if (state < 0x10000UL) - { - state = *(ulong*)(strm + 0x28); - } - - if (state < 0x10000UL) - { - Console.Error.WriteLine($"[LOADER][ERROR] Invalid state pointer from strm (0x{strm:X16}): 0x{state:X16}"); - return false; - } - - ulong raxVal = ReadCtxU64(contextRecord, CTX_RAX); - ulong r8Val = ReadCtxU64(contextRecord, CTX_R8); - - *(ulong*)(state + 0x48) = raxVal; - *(uint*)(state + 0x50) = (uint)r8Val; - - WriteCtxU64(contextRecord, CTX_RCX, state); - WriteCtxU64(contextRecord, CTX_RIP, 0x0000000804A2C262uL); - - Console.Error.WriteLine($"[LOADER][INFO] Recovered state: 0x{state:X16}"); - Console.Error.WriteLine($"[LOADER][INFO] Executed state->hold = 0x{raxVal:X16}, state->bits = {r8Val & 0xFF}"); - Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE STATE RECOVERY COMPLETED - RESUMING <<<"); - Console.Error.Flush(); - return true; - } - - // Otherwise, fault at 0x804A2C23E - ulong rdiVal = ReadCtxU64(contextRecord, CTX_RDI); - if (rdiVal != 0) - { - return false; - } - - Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE RECOVERY TRIGGERED AT 0x804A2C23E <<<"); - - // 1. Recover strm from caller frame: - // In inflate_fast frame: - // [rsp + 0x80] is caller's saved RBP - // [rsp + 0x108] holds strm (0x00007FFFF01FF470) - ulong strmVal = 0; - if (TryReadQword(rsp + 0x108, out ulong cand) && cand >= 0x10000) - { - strmVal = cand; - } - if (strmVal == 0 && TryReadQword(rsp + 0x80, out ulong savedRbpVal)) - { - if (TryReadQword((ulong)((long)savedRbpVal - 0x58), out ulong cand2) && cand2 >= 0x10000) - { - strmVal = cand2; - } - else if (TryReadQword(savedRbpVal + 0x20, out ulong cand3) && cand3 >= 0x10000) - { - strmVal = cand3; - } - } - - if (strmVal == 0) - { - Console.Error.WriteLine("[LOADER][ERROR] Could not locate strm in stack frame for inflate_fast recovery!"); - return false; - } - - Console.Error.WriteLine($"[LOADER][INFO] Recovered strm: 0x{strmVal:X16}"); - - // 2. Set rdi = strm in CPU context - WriteCtxU64(contextRecord, CTX_RDI, strmVal); - - // 3. If r10 is invalid or underflowed, recover actual input position - ulong r10Val = ReadCtxU64(contextRecord, CTX_R10); - bool isInvalidInPointer = (r10Val < 0x10000UL) || (r10Val > 0x00007FFFFFFFFFFEUL); - if (isInvalidInPointer) - { - ulong inEnd = 0; - if (TryReadQword(rsp, out inEnd) && inEnd >= 0x10000) - { - ulong raxVal = ReadCtxU64(contextRecord, CTX_RAX); - ulong unusedBytes = raxVal & 0xFFFFFFFFUL; // bits >> 3 - ulong recoveredNextIn = inEnd - unusedBytes + 1; - r10Val = recoveredNextIn - 1; - WriteCtxU64(contextRecord, CTX_R10, r10Val); - WriteCtxU64(contextRecord, CTX_RDX, recoveredNextIn); - Console.Error.WriteLine($"[LOADER][INFO] Recovered r10 (in): 0x{r10Val:X16} (in_end=0x{inEnd:X16}, unused={unusedBytes})"); - } - } - - // 4. Recover rcx (strm->state) if needed so subsequent stores at 0x804A2C25A succeed - ulong rcxVal = ReadCtxU64(contextRecord, CTX_RCX); - if (rcxVal < 0x10000UL) - { - ulong state = *(ulong*)(strmVal + 0x38); - if (state < 0x10000UL) - { - state = *(ulong*)(strmVal + 0x28); - } - if (state >= 0x10000UL) - { - WriteCtxU64(contextRecord, CTX_RCX, state); - Console.Error.WriteLine($"[LOADER][INFO] Recovered rcx (strm->state): 0x{state:X16}"); - } - } - - // 5. Perform the stores directly to ensure stream consistency - ulong rdxVal = ReadCtxU64(contextRecord, CTX_RDX); - ulong r9Val = ReadCtxU64(contextRecord, CTX_R9); - - *(ulong*)strmVal = rdxVal; - *(ulong*)(strmVal + 0x18) = r9Val + 1; - - Console.Error.WriteLine($"[LOADER][INFO] Executed strm->next_in = 0x{rdxVal:X16}"); - Console.Error.WriteLine($"[LOADER][INFO] Executed strm->next_out = 0x{r9Val + 1:X16}"); - Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE RECOVERY COMPLETED - RESUMING <<<"); - Console.Error.Flush(); - - return true; - } - private static bool IsBenignHostDebugException(uint exceptionCode) { return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION; diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index b5a4b8d..984498a 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -1880,13 +1880,17 @@ public sealed partial class DirectExecutionBackend return elapsedTicks >= (long)(_importLoopGuardSeconds * Stopwatch.Frequency); } - private static bool IsImportLoopGuardBoundary(string nid) => + internal static bool IsImportLoopGuardBoundary(string nid) => nid is "1jfXLRVzisc" or // sceKernelUsleep "WKAXJ4XBPQ4" or // scePthreadCondWait "BmMjYxmew1w" or // scePthreadCondTimedwait "Op8TBGY5KHg" or // pthread_cond_wait - "27bAgiJmOh0"; // pthread_cond_timedwait + "27bAgiJmOh0" or // pthread_cond_timedwait + "n88vx3C5nW8" or // gettimeofday + "-2IRUCO--PM" or // clock_gettime + "0V5nU-Z6t4U" or // sceKernelGetProcessTime + "aI6lQW5v57k"; // sceKernelGetProcessTimeCounter private void ResetImportLoopPattern() { diff --git a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs index 89c3cb9..f8227e9 100644 --- a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs @@ -982,6 +982,25 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA return _hostMemory.Protect(address, size, ResolveProtection(protection), out _); } + public bool TryCommitRange(ulong address, ulong size) + { + if (size == 0 || ulong.MaxValue - address < size) + { + return false; + } + + _gate.EnterReadLock(); + try + { + var region = FindRegion(address, size); + return region is not null && EnsureRangeCommitted(address, size, region); + } + finally + { + _gate.ExitReadLock(); + } + } + // Reproduces the decomposition KernelMemoryCompatExports.ResolveHostProtection // performed before this seam existed; the Windows backend maps each case back // to the identical PAGE_* value. diff --git a/src/SharpEmu.HLE/IGuestAddressSpace.cs b/src/SharpEmu.HLE/IGuestAddressSpace.cs index 53626dd..1b3d09a 100644 --- a/src/SharpEmu.HLE/IGuestAddressSpace.cs +++ b/src/SharpEmu.HLE/IGuestAddressSpace.cs @@ -26,6 +26,13 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator /// bool TryBackFixedRange(ulong address, ulong size, bool executable); + /// + /// Ensures every page in an existing guest mapping is committed for native + /// execution. This is required before returning a raw identity-mapped range + /// to guest code, which cannot pass through ICpuMemory on each access. + /// + bool TryCommitRange(ulong address, ulong size); + bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress); bool TryProtect(ulong address, ulong size, GuestPageProtection protection); diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 9278fc3..89f4976 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -14590,7 +14590,12 @@ GuestImageWriteTracker.Track( if (shRegistersAddress == 0 || registerCount < 2) { - return false; + TraceCreateShader( + 0, + headerAddress, + codeAddress, + $"skip-pgm-patch type={shaderType} shRegs=0x{shRegistersAddress:X16} regCount={registerCount}"); + return true; } // Type bytes follow the Prospero half/fused enum used by fuse-shader @@ -14619,9 +14624,10 @@ GuestImageWriteTracker.Track( _ => 0u, }; - // GTA V Enhanced hull shaders (type 5) put RSRC1/RSRC2 (0x10A/0x10B) at - // the front of the SH default table; PGM_LO/HI sit elsewhere (or are - // filled later via SetShRegisterDirect). + // Some shader headers (e.g. GTA V Enhanced hull shaders, GS front shaders, + // or UE4 mesh/task/vertex shaders where type=8 or user data registers are placed first) + // omit PGM_LO/HI from the default SH table. Still succeed: the code VA + // lives at ShaderCodeOffset and later binder paths republish it. if (!TryFindShaderProgramRegisterPair( ctx, shRegistersAddress, @@ -14634,28 +14640,12 @@ GuestImageWriteTracker.Track( out var foundHi)) { TryReadUInt32(ctx, shRegistersAddress, out var firstLo); - // GTA V Enhanced HS headers start at RSRC1/RSRC2 (0x10A/0x10B) and - // omit PGM_LO/HI from the default table. Still succeed: the code VA - // lives at ShaderCodeOffset and later binder paths republish it. - // GS front headers can likewise start at RSRC1_GS (0x8A) instead of - // PGM_LO_GS (0x88) - same deal, skip the patch here. - if ((shaderType == HsFrontShaderType && firstLo is SpiShaderPgmRsrc1Hs or SpiShaderPgmLoHs) || - (shaderType == GsFrontShaderType && firstLo is SpiShaderPgmRsrc1Gs or SpiShaderPgmLoGs)) - { - TraceCreateShader( - 0, - headerAddress, - codeAddress, - $"skip-pgm-patch type={shaderType} first_lo=0x{firstLo:X8}"); - return true; - } - TraceCreateShader( 0, headerAddress, codeAddress, - $"unexpected-registers type={shaderType} expected_lo=0x{expectedLo:X8} first_lo=0x{firstLo:X8}"); - return false; + $"skip-pgm-patch type={shaderType} expected_lo=0x{expectedLo:X8} first_lo=0x{firstLo:X8}"); + return true; } var loValue = (uint)((codeAddress >> 8) & 0xFFFF_FFFFUL); diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 24472dd..ebc6d03 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -4919,6 +4919,12 @@ public static partial class KernelMemoryCompatExports return false; } + if (KernelVirtualRangeAllocator.TryResolveAddressSpace(ctx.Memory, out var addressSpace) && + addressSpace.TryCommitRange(address, length)) + { + return true; + } + Span probe = stackalloc byte[1]; return ctx.Memory.TryRead(address, probe) && ctx.Memory.TryRead(address + length - 1, probe); diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcCreateShaderTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcCreateShaderTests.cs new file mode 100644 index 0000000..19dd59b --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcCreateShaderTests.cs @@ -0,0 +1,167 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Agc; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +public sealed class AgcCreateShaderTests +{ + private const ulong BaseAddress = 0x3_0000_0000; + private const int MemorySize = 0x4000; + + private const ulong DestinationAddress = BaseAddress + 0x0000; + private const ulong HeaderAddress = BaseAddress + 0x0100; + private const ulong CodeAddress = BaseAddress + 0x1000; + private const ulong RegistersAddress = BaseAddress + 0x0500; + + private const uint ShaderFileHeader = 0x34333231; + private const uint ShaderVersion = 0x18; + + private const ulong ShaderCodeOffset = 0x10; + private const ulong ShaderShRegistersOffset = 0x20; + private const ulong ShaderTypeOffset = 0x5A; + private const ulong ShaderNumShRegistersOffset = 0x5C; + + private const uint SpiShaderPgmLoPs = 0x8; + private const uint SpiShaderPgmHiPs = 0x9; + + [Fact] + public void CreateShader_StandardPsShader_PatchesPgmLoHi() + { + var (memory, ctx) = SetupContext(); + SetupHeader(memory, shaderType: 1, registerCount: 2); + + // Put PS PGM_LO and PGM_HI registers in the table + WriteUInt32(memory, RegistersAddress + 0, SpiShaderPgmLoPs); + WriteUInt32(memory, RegistersAddress + 4, 0); + WriteUInt32(memory, RegistersAddress + 8, SpiShaderPgmHiPs); + WriteUInt32(memory, RegistersAddress + 12, 0); + + ctx[CpuRegister.Rdi] = DestinationAddress; + ctx[CpuRegister.Rsi] = HeaderAddress; + ctx[CpuRegister.Rdx] = CodeAddress; + + var result = AgcExports.CreateShader(ctx); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(HeaderAddress, ReadUInt64(memory, DestinationAddress)); + Assert.Equal(CodeAddress, ReadUInt64(memory, HeaderAddress + ShaderCodeOffset)); + + var expectedLo = (uint)((CodeAddress >> 8) & 0xFFFF_FFFFUL); + var expectedHi = (uint)((CodeAddress >> 40) & 0xFFUL); + Assert.Equal(expectedLo, ReadUInt32(memory, RegistersAddress + 4)); + Assert.Equal(expectedHi, ReadUInt32(memory, RegistersAddress + 12)); + } + + [Fact] + public void CreateShader_ShaderType8WithoutPgmRegisters_SucceedsAndSkipsPatch() + { + var (memory, ctx) = SetupContext(); + SetupHeader(memory, shaderType: 8, registerCount: 2); + + // Registers are user data registers (e.g. 0x0 and 0x1), not PGM_LO + WriteUInt32(memory, RegistersAddress + 0, 0x0); + WriteUInt32(memory, RegistersAddress + 4, 0x1111); + WriteUInt32(memory, RegistersAddress + 8, 0x1); + WriteUInt32(memory, RegistersAddress + 12, 0x2222); + + ctx[CpuRegister.Rdi] = DestinationAddress; + ctx[CpuRegister.Rsi] = HeaderAddress; + ctx[CpuRegister.Rdx] = CodeAddress; + + var result = AgcExports.CreateShader(ctx); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(HeaderAddress, ReadUInt64(memory, DestinationAddress)); + Assert.Equal(CodeAddress, ReadUInt64(memory, HeaderAddress + ShaderCodeOffset)); + // Registers table is unchanged + Assert.Equal(0x1111u, ReadUInt32(memory, RegistersAddress + 4)); + Assert.Equal(0x2222u, ReadUInt32(memory, RegistersAddress + 12)); + } + + [Fact] + public void CreateShader_ZeroRegisters_Succeeds() + { + var (memory, ctx) = SetupContext(); + SetupHeader(memory, shaderType: 8, registerCount: 0); + + ctx[CpuRegister.Rdi] = DestinationAddress; + ctx[CpuRegister.Rsi] = HeaderAddress; + ctx[CpuRegister.Rdx] = CodeAddress; + + var result = AgcExports.CreateShader(ctx); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(HeaderAddress, ReadUInt64(memory, DestinationAddress)); + Assert.Equal(CodeAddress, ReadUInt64(memory, HeaderAddress + ShaderCodeOffset)); + } + + private static (FakeCpuMemory Memory, CpuContext Ctx) SetupContext() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + return (memory, ctx); + } + + private static void SetupHeader(FakeCpuMemory memory, byte shaderType, byte registerCount) + { + WriteUInt32(memory, HeaderAddress + 0, ShaderFileHeader); + WriteUInt32(memory, HeaderAddress + 4, ShaderVersion); + + // Pointers are relative to their field offset + if (registerCount > 0) + { + var relativeRegisters = RegistersAddress - (HeaderAddress + ShaderShRegistersOffset); + WriteUInt64(memory, HeaderAddress + ShaderShRegistersOffset, relativeRegisters); + } + else + { + WriteUInt64(memory, HeaderAddress + ShaderShRegistersOffset, 0); + } + + WriteByte(memory, HeaderAddress + ShaderTypeOffset, shaderType); + WriteByte(memory, HeaderAddress + ShaderNumShRegistersOffset, registerCount); + } + + private static byte ReadByte(FakeCpuMemory memory, ulong address) + { + Span buf = stackalloc byte[1]; + Assert.True(memory.TryRead(address, buf)); + return buf[0]; + } + + private static uint ReadUInt32(FakeCpuMemory memory, ulong address) + { + Span buf = stackalloc byte[sizeof(uint)]; + Assert.True(memory.TryRead(address, buf)); + return BinaryPrimitives.ReadUInt32LittleEndian(buf); + } + + private static ulong ReadUInt64(FakeCpuMemory memory, ulong address) + { + Span buf = stackalloc byte[sizeof(ulong)]; + Assert.True(memory.TryRead(address, buf)); + return BinaryPrimitives.ReadUInt64LittleEndian(buf); + } + + private static void WriteByte(FakeCpuMemory memory, ulong address, byte value) => + Assert.True(memory.TryWrite(address, [value])); + + private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value) + { + Span buf = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(buf, value); + Assert.True(memory.TryWrite(address, buf)); + } + + private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value) + { + Span buf = stackalloc byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64LittleEndian(buf, value); + Assert.True(memory.TryWrite(address, buf)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Cpu/ImportLoopGuardBoundaryTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/ImportLoopGuardBoundaryTests.cs new file mode 100644 index 0000000..42c4e71 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/ImportLoopGuardBoundaryTests.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Core.Cpu.Native; +using Xunit; + +namespace SharpEmu.Libs.Tests.Cpu; + +public sealed class ImportLoopGuardBoundaryTests +{ + [Theory] + [InlineData("n88vx3C5nW8")] // gettimeofday + [InlineData("-2IRUCO--PM")] // clock_gettime + [InlineData("0V5nU-Z6t4U")] // sceKernelGetProcessTime + [InlineData("aI6lQW5v57k")] // sceKernelGetProcessTimeCounter + public void TimeObservationResetsRepeatingImportPattern(string nid) + { + Assert.True(DirectExecutionBackend.IsImportLoopGuardBoundary(nid)); + } + + [Fact] + public void OrdinaryImportDoesNotResetRepeatingImportPattern() + { + Assert.False(DirectExecutionBackend.IsImportLoopGuardBoundary("ordinary-import")); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Cpu/VehTrampolineTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/VehTrampolineTests.cs index 35ca8f4..40f6db1 100644 --- a/tests/SharpEmu.Libs.Tests/Cpu/VehTrampolineTests.cs +++ b/tests/SharpEmu.Libs.Tests/Cpu/VehTrampolineTests.cs @@ -91,6 +91,47 @@ public sealed class VehTrampolineTests } } + [Fact] + public unsafe void ManagedCallbackPreservesWin64NonvolatileRegisters() + { + if (!Supported) return; + + using var backend = new DirectExecutionBackend(new ModuleManager()); + Handler handler = _ => + { + // Force ordinary managed work and volatile-register traffic while + // the generated thunk is active. The wrapper below checks the ABI + // registers after this reverse P/Invoke returns. + var text = string.Concat("VEH-", Environment.CurrentManagedThreadId, "-", DateTime.UtcNow.Ticks); + GC.KeepAlive(text); + return -1; + }; + var thunk = backend.CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(handler)); + Assert.NotEqual(0, thunk); + var wrapper = CreateRegisterProbe(thunk); + var output = stackalloc ulong[8]; + var exceptionInfo = CreateExceptionInfo(0xC000001D); + try + { + var result = ((delegate* unmanaged)wrapper)(exceptionInfo, (nint)output); + Assert.Equal(-1, result); + Assert.Equal(0x1111111111111111UL, output[0]); // rbx + Assert.Equal(0x2222222222222222UL, output[1]); // rbp + Assert.Equal(0x3333333333333333UL, output[2]); // rsi + Assert.Equal(0x4444444444444444UL, output[3]); // rdi + Assert.Equal(0x5555555555555555UL, output[4]); // r12 + Assert.Equal(0x6666666666666666UL, output[5]); // r13 + Assert.Equal(0x7777777777777777UL, output[6]); // r14 + Assert.Equal(0x8888888888888888UL, output[7]); // r15 + } + finally + { + NativeMemory.Free((void*)exceptionInfo); + HostMemory.Free((void*)wrapper, 0, HostMemory.MEM_RELEASE); + GC.KeepAlive(handler); + } + } + private static Task StartCallback(nint thunk) => Task.Factory.StartNew( () => Invoke(thunk, 0xC000001D), CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); @@ -109,4 +150,54 @@ public sealed class VehTrampolineTests pointers[1] = (nint)context; return ((delegate* unmanaged)thunk)((nint)pointers); } + + private static unsafe nint CreateExceptionInfo(uint code) + { + // The wrapper only needs a valid ExceptionCode field and ContextRecord + // pointer; the thunk's native pre-filter reads exactly those members. + var pointers = (nint*)NativeMemory.Alloc((nuint)(2 * sizeof(nint) + 0xA0 + 0x4D0)); + var record = (byte*)(pointers + 2); + var context = record + 0xA0; + new Span(record, 0xA0).Clear(); + new Span(context, 0x4D0).Clear(); + *(uint*)record = code; + pointers[0] = (nint)record; + pointers[1] = (nint)context; + return (nint)pointers; + } + + private static unsafe nint CreateRegisterProbe(nint thunk) + { + var code = new List(); + void U64(ulong value) { code.AddRange(BitConverter.GetBytes(value)); } + void MovReg(byte rex, byte opcode, ulong value) { code.Add(rex); code.Add(opcode); U64(value); } + // Preserve caller nonvolatiles, then install sentinels and call thunk. + code.AddRange(new byte[] { 0x53, 0x56, 0x57, 0x55, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57 }); + code.AddRange(new byte[] { 0x48, 0x83, 0xEC, 0x38 }); + code.AddRange(new byte[] { 0x48, 0x89, 0x54, 0x24, 0x30 }); // [rsp+30] = output + MovReg(0x49, 0xBC, 0x5555555555555555UL); + MovReg(0x49, 0xBD, 0x6666666666666666UL); + MovReg(0x49, 0xBE, 0x7777777777777777UL); + MovReg(0x49, 0xBF, 0x8888888888888888UL); + MovReg(0x48, 0xBB, 0x1111111111111111UL); + MovReg(0x48, 0xBD, 0x2222222222222222UL); + MovReg(0x48, 0xBE, 0x3333333333333333UL); + MovReg(0x48, 0xBF, 0x4444444444444444UL); + code.AddRange(new byte[] { 0x48, 0xB8 }); U64((ulong)thunk); code.AddRange(new byte[] { 0xFF, 0xD0 }); + code.AddRange(new byte[] { 0x89, 0x44, 0x24, 0x28 }); // [rsp+28] = callback result + code.AddRange(new byte[] { 0x48, 0x8B, 0x44, 0x24, 0x30 }); // rax = output + code.AddRange(new byte[] { 0x48, 0x89, 0x98, 0, 0, 0, 0 }); + code.AddRange(new byte[] { 0x48, 0x89, 0xA8, 8, 0, 0, 0 }); + code.AddRange(new byte[] { 0x48, 0x89, 0xB0, 16, 0, 0, 0 }); + code.AddRange(new byte[] { 0x48, 0x89, 0xB8, 24, 0, 0, 0 }); + code.AddRange(new byte[] { 0x4C, 0x89, 0xA0, 32, 0, 0, 0 }); + code.AddRange(new byte[] { 0x4C, 0x89, 0xA8, 40, 0, 0, 0 }); + code.AddRange(new byte[] { 0x4C, 0x89, 0xB0, 48, 0, 0, 0 }); + code.AddRange(new byte[] { 0x4C, 0x89, 0xB8, 56, 0, 0, 0 }); + code.AddRange(new byte[] { 0x8B, 0x44, 0x24, 0x28 }); // eax = callback result + code.AddRange(new byte[] { 0x48, 0x83, 0xC4, 0x38, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x5D, 0x5F, 0x5E, 0x5B, 0xC3 }); + var ptr = (byte*)HostMemory.Alloc(null, (nuint)code.Count, HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE, HostMemory.PAGE_EXECUTE_READWRITE); + code.ToArray().AsSpan().CopyTo(new Span(ptr, code.Count)); + return (nint)ptr; + } } diff --git a/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs b/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs index 1608dde..b7df756 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs @@ -59,6 +59,25 @@ public sealed class PhysicalVirtualMemoryTests Assert.Single(host.CommitCalls); } + [Fact] + public void TryCommitRangeCommitsEveryPageInReserveOnlyMapping() + { + using var host = new LazyZeroedHostMemory(); + using var memory = new PhysicalVirtualMemory(host); + + var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false); + host.CommitCalls.Clear(); + + Assert.True(memory.TryCommitRange(address + 0x1000, 0x3000)); + Assert.Equal( + [ + (address + 0x1000, 0x1000UL, HostPageProtection.ReadWrite), + (address + 0x2000, 0x1000UL, HostPageProtection.ReadWrite), + (address + 0x3000, 0x1000UL, HostPageProtection.ReadWrite), + ], + host.CommitCalls); + } + [Fact] public void TryCopyHandlesOverlappingIdentityMappedRanges() {