From 51e548004902fa3c4aa74bf21182f4c5d933cd04 Mon Sep 17 00:00:00 2001 From: Foued Attar Date: Mon, 24 Aug 2026 22:52:30 +0200 Subject: [PATCH] fix(cpu): preserve TLS instruction boundaries after rel8 branches (#846) --- .../Cpu/Native/DirectExecutionBackend.cs | 77 ++++++++++++++++++- .../Cpu/TlsLoadPatchBoundaryTests.cs | 75 ++++++++++++++++++ 2 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Cpu/TlsLoadPatchBoundaryTests.cs diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 9b458923..cf7df717 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; +using Iced.Intel; using SharpEmu.Core.Cpu; using SharpEmu.Core.Cpu.Debugging; using SharpEmu.Core.Loader; @@ -3350,10 +3351,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I 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) + var region = new ReadOnlySpan(source - regionOffset, regionOffset + availableLength); + if (IsTlsLoadCandidateInsideShortJump(region, regionOffset)) { return false; } @@ -3410,6 +3409,76 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return PatchTlsLoadInstruction(address, instructionLength, destinationRegister); } + internal static bool IsTlsLoadCandidateInsideShortJump(ReadOnlySpan region, int candidateOffset) + { + if ((uint)candidateOffset >= (uint)region.Length || + candidateOffset < 1 || + region[candidateOffset - 1] != 0xEB) + { + return false; + } + + // Accept EB when it is an aligned rel8 operand. + if (IsRel8ControlFlowInstructionEndingAtCandidate(region, candidateOffset)) + { + return false; + } + + return true; + } + + private static bool IsRel8ControlFlowInstructionEndingAtCandidate( + ReadOnlySpan region, + int candidateOffset) + { + if (candidateOffset < 2) + { + return false; + } + + var branchOffset = candidateOffset - 2; + var opcode = region[branchOffset]; + if (!((opcode >= 0x70 && opcode <= 0x7F) || + opcode is >= 0xE0 and <= 0xE3 || + opcode == 0xEB)) + { + return false; + } + + var branchTarget = candidateOffset + (sbyte)region[candidateOffset - 1]; + if (branchTarget < 0 || branchTarget >= branchOffset) + { + return false; + } + + // Require an aligned instruction stream. + var decoder = Decoder.Create( + 64, + new ByteArrayCodeReader(region[branchTarget..candidateOffset].ToArray())); + decoder.IP = (ulong)branchTarget; + while (decoder.IP < (ulong)candidateOffset) + { + var instructionOffset = (int)decoder.IP; + decoder.Decode(out var instruction); + if (instruction.Code == Code.INVALID || instruction.Length <= 0) + { + return false; + } + + if (instructionOffset == branchOffset) + { + return instruction.Length == 2 && decoder.IP == (ulong)candidateOffset; + } + + if (decoder.IP > (ulong)branchOffset) + { + return false; + } + } + + return false; + } + private unsafe bool PatchTlsLoadInstruction(nint address, int instructionLength, int destinationRegister) { uint flNewProtect = default(uint); diff --git a/tests/SharpEmu.Libs.Tests/Cpu/TlsLoadPatchBoundaryTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/TlsLoadPatchBoundaryTests.cs new file mode 100644 index 00000000..d33fc311 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/TlsLoadPatchBoundaryTests.cs @@ -0,0 +1,75 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Core.Cpu.Native; +using Xunit; + +namespace SharpEmu.Libs.Tests.Cpu; + +public sealed class TlsLoadPatchBoundaryTests +{ + [Fact] + public void RejectsGtaShortJumpDisplacementAsTlsPrefix() + { + byte[] code = + [ + 0x90, + 0xEB, 0x66, + 0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, + ]; + + Assert.True(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 2)); + Assert.False(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 3)); + } + + [Fact] + public void KeepsDreamingSarahTlsInstructionAfterBackwardJnz() + { + byte[] code = + [ + 0x48, 0x8B, 0x1C, 0xD0, + 0x4C, 0x39, 0x2B, + 0x0F, 0x84, 0x10, 0x01, 0x00, 0x00, + 0x48, 0xFF, 0xC2, + 0x48, 0x39, 0xD1, + 0x75, 0xEB, + 0x66, 0x66, 0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, + ]; + + Assert.False(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 21)); + } + + [Theory] + [InlineData(0x70)] + [InlineData(0x7F)] + [InlineData(0xE0)] + [InlineData(0xE3)] + [InlineData(0xEB)] + public void KeepsTlsInstructionAfterRel8ControlFlow(byte opcode) + { + byte[] code = + [ + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + opcode, 0xEB, + 0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, + ]; + + Assert.False(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 21)); + } + + [Fact] + public void Rel8OpcodeByteInsidePreviousInstructionDoesNotBypassGuard() + { + byte[] code = + [ + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x6A, 0x75, + 0xEB, 0x66, + 0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, + ]; + + Assert.True(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 21)); + } +}