[Loader] Rewrite SHA instructions in spans that touch no guest memory

The red-zone patcher computes a shifted core for every span: the run of
faultable guest memory accesses that executes with RSP moved below the
red zone. A span over register-only instructions has no such access, so
the core could not be computed and the span was refused as too short.
SHA rewrite sites are exactly that shape (sha256msg1/msg2/rnds2 on
registers), so no SHA instruction was rewritten any more: on hosts
without the SHA extensions every one trapped and was emulated one at a
time. Poppy Playtime logged sha_rewrites=0 with 144 too-short refusals,
hit hundreds of thousands of illegal-instruction traps and stopped making
progress within a minute.

A span with no guest memory access has nothing that must run outside the
shift, and the span builder has already refused any instruction that
touches RSP, so the whole span is the core.
This commit is contained in:
kuba 2026-09-30 00:57:23 +02:00
parent 50198a694b
commit eb7521543c
2 changed files with 65 additions and 2 deletions

View File

@ -349,6 +349,41 @@ internal static class GuestRedZonePatcher
/// behaviour can be pinned on the byte sequences that occur in real guest
/// code rather than on a mocked decode.
/// </summary>
// Test hook: the forward span a site at siteAddress gets, as the patcher builds it.
internal static bool TryBuildForwardSpan(
byte[] code,
ulong baseAddress,
ulong siteAddress,
out int spanLength,
out int coreStart,
out int coreCount)
{
spanLength = 0;
coreStart = 0;
coreCount = 0;
var decoded = DecodeFunction(code, baseAddress, baseAddress, baseAddress + (ulong)code.Length);
var siteIndex = -1;
for (var index = 0; index < decoded.Count; index++)
{
if (decoded[index].Instruction.IP == siteAddress)
{
siteIndex = index;
break;
}
}
if (siteIndex < 0 ||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), out var site, out _))
{
return false;
}
spanLength = site.ByteLength;
coreStart = site.CoreStart;
coreCount = site.CoreCount;
return true;
}
internal static bool TryBuildEnclosingSpan(
byte[] code,
ulong baseAddress,
@ -417,8 +452,13 @@ internal static class GuestRedZonePatcher
if (coreStart < 0)
{
coreCount = 0;
return false;
// Nothing in the span touches guest memory: it is a SHA or vector-store rewrite over
// register-only instructions. The span builder already refused anything that touches
// RSP, so the whole span can run inside the shift. Refusing it here left every SHA
// instruction unrewritten, trapping on each execution on hosts without SHA.
coreStart = 0;
coreCount = instructions.Count;
return true;
}
for (var index = coreStart; index <= coreEnd; index++)

View File

@ -127,4 +127,27 @@ public sealed class GuestRedZoneEnclosingSpanTests
Assert.False(GuestRedZonePatcher.TryBuildEnclosingSpan(
code, Base, Base + 2, out _, out _, out _, out _));
}
[Fact]
public void BuildsASpanOverRegisterOnlyShaInstructions()
{
// sha256msg1 xmm1, xmm2 -> 4 bytes, no memory operand: the rewrite site
// sha256msg2 xmm1, xmm2 -> 4 bytes, taken to reach the 5-byte jump
// ret
byte[] code =
[
0x0F, 0x38, 0xCC, 0xCA,
0x0F, 0x38, 0xCD, 0xCA,
0xC3,
];
// A span with no guest memory access has nothing to keep outside the RSP shift, so
// the whole span is the core. Refusing it left SHA instructions unrewritten.
Assert.True(GuestRedZonePatcher.TryBuildForwardSpan(
code, Base, Base, out var length, out var coreStart, out var coreCount));
Assert.Equal(8, length);
Assert.Equal(0, coreStart);
Assert.Equal(2, coreCount);
}
}