Fix TLS-load patcher corrupting short jumps immediately before FS:[0] reads (#838)

The linear scanner consumed leading 0x66 bytes without checking that the
candidate starts on an instruction boundary. A short jump whose disp8 is
0x66 (EB 66) directly followed by a 66-prefixed FS:[0] load made the
patcher treat the displacement as a prefix and write its call opcode over
it, rewriting 'jmp forward past the TLS access' into 'jmp backward' -- an
infinite loop. GTA V's AGC resource destructors hit exactly this shape and
leaked the entire heap inside a container drain before any frame was
presented.

Reject candidates whose preceding byte is 0xEB: a bare EB can never be the
last byte of a valid instruction, so such a position is provably
mid-instruction. The outer byte scan then retries at the next offset,
which is the true boundary, and the patch lands correctly.
This commit is contained in:
Foued Attar
2026-08-23 14:52:44 +02:00
committed by GitHub
parent 3a744c991e
commit 4a7a45d1b3
@@ -3200,7 +3200,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{ {
nint address = (nint)(ptr + i); nint address = (nint)(ptr + i);
int remainingBytes = scanBytes - i; int remainingBytes = scanBytes - i;
if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes)) if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes, i))
{ {
num3++; num3++;
} }
@@ -3343,13 +3343,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return true; return true;
} }
private unsafe bool TryPatchTlsLoadInstruction(nint address, byte* source, int availableLength) private unsafe bool TryPatchTlsLoadInstruction(nint address, byte* source, int availableLength, int regionOffset)
{ {
if (availableLength < MinTlsPatchInstructionBytes) if (availableLength < MinTlsPatchInstructionBytes)
{ {
return false; return false;
} }
// A bare 0xEB (JMP rel8) always owns a disp8 after it, so it can
// never be the last byte of a valid instruction. If it precedes our
// candidate, we're mid-instruction: reject and let the scan retry.
if (regionOffset >= 1 && source[-1] == 0xEB)
{
return false;
}
var offset = 0; var offset = 0;
while (offset < availableLength && source[offset] == 0x66) while (offset < availableLength && source[offset] == 0x66)
{ {