From eb7521543c8391677f35df2130dd95ea97b80576 Mon Sep 17 00:00:00 2001 From: kuba Date: Wed, 30 Sep 2026 00:57:23 +0200 Subject: [PATCH] [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. --- .../Loader/GuestRedZonePatcher.cs | 44 ++++++++++++++++++- .../Loader/GuestRedZoneEnclosingSpanTests.cs | 23 ++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs b/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs index d541b3c..e3668ce 100644 --- a/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs +++ b/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs @@ -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. /// + // 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++) diff --git a/tests/SharpEmu.Libs.Tests/Loader/GuestRedZoneEnclosingSpanTests.cs b/tests/SharpEmu.Libs.Tests/Loader/GuestRedZoneEnclosingSpanTests.cs index a281d2f..3edc1cf 100644 --- a/tests/SharpEmu.Libs.Tests/Loader/GuestRedZoneEnclosingSpanTests.cs +++ b/tests/SharpEmu.Libs.Tests/Loader/GuestRedZoneEnclosingSpanTests.cs @@ -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); + } }