fix: improve Direct Execution guest runtime and AGC compatibility (#880)

* test(cpu): verify VEH thunk preserves nonvolatile registers

* fix(memory): commit complete native guest mappings

* fix(cpu): treat time polling as import-loop progress

* fix(cli): preserve DLL path in mitigation relaunch

* refactor(cpu): remove obsolete inflate recovery hack

* fix(agc): accept shaders without embedded program registers
This commit is contained in:
Mathias 2026-09-11 12:37:31 +02:00 committed by GitHub
parent ca90d17541
commit 0a461f8d69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 365 additions and 203 deletions

View File

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

View File

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

View File

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

View File

@ -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.

View File

@ -26,6 +26,13 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator
/// </summary>
bool TryBackFixedRange(ulong address, ulong size, bool executable);
/// <summary>
/// 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 <c>ICpuMemory</c> on each access.
/// </summary>
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);

View File

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

View File

@ -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<byte> probe = stackalloc byte[1];
return ctx.Memory.TryRead(address, probe) &&
ctx.Memory.TryRead(address + length - 1, probe);

View File

@ -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<byte> buf = stackalloc byte[1];
Assert.True(memory.TryRead(address, buf));
return buf[0];
}
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
{
Span<byte> buf = stackalloc byte[sizeof(uint)];
Assert.True(memory.TryRead(address, buf));
return BinaryPrimitives.ReadUInt32LittleEndian(buf);
}
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
{
Span<byte> 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<byte> 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<byte> buf = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buf, value);
Assert.True(memory.TryWrite(address, buf));
}
}

View File

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

View File

@ -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<nint, nint, int>)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<int> 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<nint, int>)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<byte>(record, 0xA0).Clear();
new Span<byte>(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<byte>();
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<byte>(ptr, code.Count));
return (nint)ptr;
}
}

View File

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