From 4a7a45d1b36384f4f36bd72837edcbe501eb06e9 Mon Sep 17 00:00:00 2001 From: Foued Attar Date: Sun, 23 Aug 2026 14:52:44 +0200 Subject: [PATCH] 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. --- .../Cpu/Native/DirectExecutionBackend.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 35d797e2..9b458923 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -3200,7 +3200,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { nint address = (nint)(ptr + i); int remainingBytes = scanBytes - i; - if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes)) + if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes, i)) { num3++; } @@ -3343,13 +3343,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I 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) { 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; while (offset < availableLength && source[offset] == 0x66) {