From 206aaa56cbd0710f6bfbf20b07b084207d4d51c3 Mon Sep 17 00:00:00 2001 From: oskarlgs Date: Sat, 17 Oct 2026 20:58:29 +0000 Subject: [PATCH] fix(relinker): decode near branches with an operand-size prefix as AMD does 66 E8, 66 E9 and 66 0F 80-8F without REX.W take a 16-bit displacement on AMD processors, the console included. Every near relative branch with that prefix truncates its target to 16 bits, so the decoder reports no branch target for it and stubs refuse to move it. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016TFMtUC4L4wMS4eeEqaNSR --- .../codegen/include/codegen/CodegenTypes.hpp | 1 + .../codegen/src/x86/StubBodyBuilder.cpp | 2 ++ .../codegen/src/x86/X64InstructionDecoder.cpp | 20 ++++++++++++-- .../codegen/tests/DecoderLengthCases.hpp | 8 ++++++ .../codegen/tests/DecoderLengthTests.cpp | 14 ++++++++++ .../codegen/tests/DecoderMetadataCases.hpp | 27 +++++++++++++++++++ docs/dev/debt/relinker.md | 2 +- 7 files changed, 71 insertions(+), 3 deletions(-) diff --git a/core/relinker/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index 43e5a7110b..72bace4267 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -71,6 +71,7 @@ struct DecodedInstructionInfo { std::uint8_t ModRmByte; bool HasBranchTarget; std::int64_t BranchDisp; + bool TruncatesBranchTarget; bool IsTwoByteOpcode; std::uint8_t Opcode; std::uint8_t ModRmRegField; diff --git a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp index e68805215f..f3e18ab213 100644 --- a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp +++ b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp @@ -210,6 +210,8 @@ void StubBodyBuilder::MoveControlFlow(const std::span instru const auto end = _siteOffset + static_cast(instruction.size()); if (info.FlowKind == ControlFlowKind::IndirectCall) throw CodegenException("A register or memory call cannot move into a stub"); + if (info.TruncatesBranchTarget) + throw CodegenException("A relative branch with this prefix cannot move into a stub"); if (info.FlowKind == ControlFlowKind::Call && info.HasRipRelativeDisp) { _pushReturnAddress(end); std::vector jump(instruction.begin(), instruction.end()); diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index a3f7f3962d..c6cc9de4ac 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -207,7 +207,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t } if (opcode == OneByteCallRel32 || opcode == OneByteJmpRel32) { - immediateSize = ImmSize32; + immediateSize = operandImmediateSize; } if (opcode == OneByteJmpRel8 || opcode == OneByteJrcxz || @@ -241,7 +241,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t } hasModRm = true; } else if (opcode >= TwoByteJccRel32Min && opcode <= TwoByteJccRel32Max) { - immediateSize = ImmSize32; + immediateSize = operandImmediateSize; } else if (opcode == TwoByteMovImm8ModRm || opcode == TwoByte3DNow) { hasModRm = true; immediateSize = ImmSize8; @@ -355,6 +355,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.Length = Decode(data, available); std::size_t pos = 0; + bool operandSizeOverride = false; while (pos < info.Length) { std::uint8_t b = data[pos]; @@ -371,6 +372,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.SegmentPrefix = b; if (b == PrefixAddressSize) info.AddressSize32 = true; + if (b == PrefixOperandSize) + operandSizeOverride = true; info.RexPrefix = 0; ++pos; continue; @@ -417,6 +420,19 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.Opcode = op; info.IsTwoByteOpcode = twoByteOpcode; + const bool relativeNearBranch = twoByteOpcode + ? op >= TwoByteJccRel32Min && op <= TwoByteJccRel32Max + : op == OneByteCallRel32 || op == OneByteJmpRel32 || op == OneByteJmpRel8 || + (op >= OneByteJccRel8Min && op <= OneByteJccRel8Max) || + op == OneByteJrcxz || (op >= OneByteLoop && op <= OneByteLoopMax); + if (relativeNearBranch && operandSizeOverride && (info.RexPrefix & RexWBit) == 0) { + info.TruncatesBranchTarget = true; + info.FlowKind = twoByteOpcode || (op != OneByteCallRel32 && op != OneByteJmpRel32 && op != OneByteJmpRel8) + ? ControlFlowKind::ConditionalBranch + : op == OneByteCallRel32 ? ControlFlowKind::Call : ControlFlowKind::UnconditionalJump; + return info; + } + if (!twoByteOpcode) { if (op == OneByteCallRel32) { std::int32_t disp = 0; diff --git a/core/relinker/codegen/tests/DecoderLengthCases.hpp b/core/relinker/codegen/tests/DecoderLengthCases.hpp index 10747c9a61..1de81e75ab 100644 --- a/core/relinker/codegen/tests/DecoderLengthCases.hpp +++ b/core/relinker/codegen/tests/DecoderLengthCases.hpp @@ -420,6 +420,9 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"660f79c1", 4}, {"660f7c846511223344", 9}, {"660f7d846511223344", 9}, + {"660f801122", 5}, + {"660f841122", 5}, + {"660f8f1122", 5}, {"660fd0846511223344", 9}, {"660fd6846511223344", 9}, {"660fe6846511223344", 9}, @@ -430,6 +433,7 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"66358465", 4}, {"663d8465", 4}, {"66480584651122", 7}, + {"66480f8411223344", 8}, {"66480d84651122", 7}, {"66481584651122", 7}, {"66481d84651122", 7}, @@ -450,6 +454,8 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"6648be8465112233445566", 11}, {"6648bf8465112233445566", 11}, {"6648c784651122334455667708", 13}, + {"6648e811223344", 7}, + {"6648e911223344", 7}, {"6648f784651122334455667708", 13}, {"66688465", 4}, {"66698465112233445566", 10}, @@ -464,6 +470,8 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"66be8465", 4}, {"66bf8465", 4}, {"66c78465112233445566", 10}, + {"66e81122", 4}, + {"66e91122", 4}, {"66f78465112233445566", 10}, {"676984651122334455667708", 12}, {"678184651122334455667708", 12}, diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index 54dd90df2b..63bca4bfe0 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -99,6 +99,20 @@ int main() { require(info.HasBranchTarget == testCase.HasBranchTarget, "Wrong branch target presence for " + name); require(!testCase.HasBranchTarget || info.BranchDisp == testCase.BranchDisp, "Wrong branch displacement for " + name); } + for (const auto& testCase : kDecoderTruncatedBranchCases) { + const std::string name = testCase.Bytes; + auto bytes = parse(testCase.Bytes); + require(bytes.size() == testCase.Length, "Truncated branch case " + name + " does not hold its own length"); + bytes.insert(bytes.end(), 15, 0x90); + const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); + require(info.Length == testCase.Length, "Wrong length for " + name); + require(info.FlowKind == testCase.FlowKind, "Wrong control flow kind for " + name); + require(info.TruncatesBranchTarget && !info.HasBranchTarget, "Branch target of " + name + " was reported without its 16-bit truncation"); + } + for (const auto& testCase : kDecoderBranchCases) { + const auto bytes = parse(testCase.Bytes); + require(!decoder.DecodeInstruction(bytes.data(), bytes.size()).TruncatesBranchTarget, "Branch target of " + std::string(testCase.Bytes) + " was reported as truncated"); + } const auto threeDNow = parse("0f0f0d11223344a6"); const auto threeDNowInfo = decoder.DecodeInstruction(threeDNow.data(), threeDNow.size()); require(threeDNowInfo.HasModRm && threeDNowInfo.ModRmByte == 0x0d, "3DNow! ModRM was not reported"); diff --git a/core/relinker/codegen/tests/DecoderMetadataCases.hpp b/core/relinker/codegen/tests/DecoderMetadataCases.hpp index c685b184ea..d29d0c0191 100644 --- a/core/relinker/codegen/tests/DecoderMetadataCases.hpp +++ b/core/relinker/codegen/tests/DecoderMetadataCases.hpp @@ -787,6 +787,33 @@ inline constexpr DecoderBranchCase kDecoderBranchCases[] = { {"ff28", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, {"ffd0", 2, Codegen::ControlFlowKind::IndirectCall, false, 0}, {"ffe0", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"66480f84c0111111", 8, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"6648e8c0111111", 7, Codegen::ControlFlowKind::Call, true, 286331328}, + {"6648e9c0111111", 7, Codegen::ControlFlowKind::UnconditionalJump, true, 286331328}, + {"6648ebc0", 4, Codegen::ControlFlowKind::UnconditionalJump, true, -64}, + {"664875c0", 4, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, +}; + +struct DecoderTruncatedBranchCase { + const char* Bytes; + std::size_t Length; + Codegen::ControlFlowKind FlowKind; +}; + +inline constexpr DecoderTruncatedBranchCase kDecoderTruncatedBranchCases[] = { + {"660f80c011", 5, Codegen::ControlFlowKind::ConditionalBranch}, + {"660f8fc011", 5, Codegen::ControlFlowKind::ConditionalBranch}, + {"6670c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"6675c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"667fc0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e0c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e2c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e3c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e8c011", 4, Codegen::ControlFlowKind::Call}, + {"66e9c011", 4, Codegen::ControlFlowKind::UnconditionalJump}, + {"66ebc0", 3, Codegen::ControlFlowKind::UnconditionalJump}, + {"6667e8c011", 5, Codegen::ControlFlowKind::Call}, + {"6640e8c011", 5, Codegen::ControlFlowKind::Call}, }; #endif diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index acab2b4e54..770bab7545 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -9,7 +9,7 @@ - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. -- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) decodes a near CALL, JMP or Jcc with an operand-size prefix (`66`) as Intel does, with a 32-bit or 8-bit displacement and a 64-bit target. AMD processors, the console included, read a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` and truncate the target of every near branch with that prefix, rel8 forms included, to 16 bits. Compilers do not emit these forms; the relinker's length and target for them do not match the console. +- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked.