[CPU] Reuse import trampolines and leave patched stubs alone on module reloads

Every runtime module load sets up the import stubs of all loaded modules
again, and each setup built a fresh trampoline and rewrote the stub's 16 bytes
-- under guest threads that may be executing exactly those bytes.

A trampoline only encodes its import index, so the trampolines are now cached
by index and reused, and a stub that already jumps to the trampoline it would
be given is left untouched, with no unprotect/write/restore cycle at all. A
reload of a module whose stubs are already patched therefore writes no guest
code.

The trampoline ABI test builds its backend with GetUninitializedObject, which
skips field initializers, so it has to supply the new cache; both fields now
go through one helper that names a field it cannot find.
This commit is contained in:
kuba 2026-09-30 00:11:15 +02:00
parent 829dc815d1
commit ae7ca8dfbe
2 changed files with 53 additions and 5 deletions

View File

@ -2329,7 +2329,31 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
}
// A trampoline only encodes its import index, so a runtime module load that sets up every
// module's stubs again reuses them: the stubs then keep their bytes and are not rewritten
// under guest threads that may be executing them.
private readonly Dictionary<int, nint> _importHandlerTrampolineByIndex = new();
private unsafe nint CreateImportHandlerTrampoline(int importIndex)
{
lock (ImportStubPatchGate)
{
if (_importHandlerTrampolineByIndex.TryGetValue(importIndex, out var existing))
{
return existing;
}
var created = CreateImportHandlerTrampolineCore(importIndex);
if (created != 0)
{
_importHandlerTrampolineByIndex[importIndex] = created;
}
return created;
}
}
private unsafe nint CreateImportHandlerTrampolineCore(int importIndex)
{
const uint stubSize = 1024u;
void* ptr = VirtualAlloc(null, stubSize, 12288u, 64u);
@ -2520,6 +2544,20 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private unsafe bool PatchImportStubLocked(nint address, nint trampoline)
{
// Every runtime module load sets up the stubs of all loaded modules again; a stub that
// already jumps to this trampoline needs no unprotect/write/restore cycle.
Span<byte> patch = stackalloc byte[16];
patch[0] = 0x48;
patch[1] = 0xB8;
System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(patch[2..], trampoline);
patch[10] = 0xFF;
patch[11] = 0xE0;
patch[12..].Fill(0x90);
if (new ReadOnlySpan<byte>((void*)address, 16).SequenceEqual(patch))
{
return true;
}
uint flNewProtect = default(uint);
if (!VirtualProtect((void*)address, 16u, 64u, &flNewProtect))
{
@ -2556,6 +2594,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
}
_importHandlerTrampolines.Clear();
_importHandlerTrampolineByIndex.Clear();
}
private unsafe void CreateTlsHandler()

View File

@ -81,13 +81,13 @@ public sealed class ImportTrampolineAbiTests
private static unsafe byte[] CreateTrampolineBytes()
{
// GetUninitializedObject skips the field initializers, so every reference
// field the emitter touches has to be supplied here, including the
// per-index trampoline cache.
var backend = (DirectExecutionBackend)RuntimeHelpers.GetUninitializedObject(
typeof(DirectExecutionBackend));
var trampolineList = typeof(DirectExecutionBackend).GetField(
"_importHandlerTrampolines",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(trampolineList);
trampolineList.SetValue(backend, new List<nint>());
SetPrivateField(backend, "_importHandlerTrampolines", new List<nint>());
SetPrivateField(backend, "_importHandlerTrampolineByIndex", new Dictionary<int, nint>());
var createTrampoline = typeof(DirectExecutionBackend).GetMethod(
"CreateImportHandlerTrampoline",
@ -106,6 +106,15 @@ public sealed class ImportTrampolineAbiTests
}
}
private static void SetPrivateField(DirectExecutionBackend backend, string name, object value)
{
var field = typeof(DirectExecutionBackend).GetField(
name,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.True(field is not null, $"DirectExecutionBackend has no field '{name}'.");
field!.SetValue(backend, value);
}
private static void AssertContains(ReadOnlySpan<byte> code, ReadOnlySpan<byte> expected)
{
Assert.True(