diff --git a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp index 212bdaee22..82990dd9ac 100644 --- a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp @@ -96,6 +96,66 @@ RewriteResult X64InstructionRewriter::Rewrite( const auto cutStart = static_cast(offset); const std::int64_t cutEnd = cutStart + oldLength; + if (delta != 0) { + const auto sectionSize = static_cast(codeSection.size()); + + for (const auto& match : matches) { + const auto matchOffset = static_cast(match.Offset); + if (matchOffset >= cutStart && matchOffset < cutEnd) + continue; + + const auto info = decoder.DecodeInstruction( + codeSection.data() + match.Offset, match.Length); + + if (!info.TruncatesBranchTarget) + continue; + + const auto opcodeLength = info.IsTwoByteOpcode ? 2u : 1u; + const auto displacementSize = + info.Length - info.OpcodeOffset - opcodeLength; + const auto displacementOffset = + match.Offset + info.Length - displacementSize; + + std::int64_t branchDisplacement = 0; + + if (displacementSize == 1) { + branchDisplacement = + static_cast(codeSection[displacementOffset]); + } else if (displacementSize == 2) { + const auto raw = static_cast( + static_cast(codeSection[displacementOffset]) | + (static_cast( + codeSection[displacementOffset + 1]) << 8)); + branchDisplacement = static_cast(raw); + } else { + throw CodegenException( + "Unexpected displacement size for truncated relative branch", + match.Offset); + } + + const auto rawTargetPosition = + matchOffset + static_cast(info.Length) + + branchDisplacement; + if (rawTargetPosition < 0 || rawTargetPosition > 0xFFFF) { + throw CodegenException( + "Cannot change code length while a truncated relative branch target wraps at 16 bits", + match.Offset); + } + const auto targetPosition = rawTargetPosition; + + const bool instrShifts = + matchOffset >= cutEnd && matchOffset < sectionSize; + const bool targetShifts = + targetPosition >= cutEnd && targetPosition < sectionSize; + + if (instrShifts != targetShifts) { + throw CodegenException( + "Cannot change code length while a truncated relative branch moves", + match.Offset); + } + } + } + std::vector rewritten; rewritten.reserve(codeSection.size() + static_cast(delta > 0 ? delta : 0)); rewritten.insert(rewritten.end(), codeSection.begin(), codeSection.begin() + offset); diff --git a/core/relinker/codegen/tests/RewriteBoundaryTests.cpp b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp index fa9ee00553..dd1bd9a185 100644 --- a/core/relinker/codegen/tests/RewriteBoundaryTests.cpp +++ b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp @@ -2,26 +2,114 @@ #include #include -static void Require(bool ok) { if (!ok) throw std::runtime_error("Invalid rewrite boundary accepted"); } +static void Require(bool ok) { + if (!ok) + throw std::runtime_error("Invalid rewrite boundary accepted"); +} + +static void RequireTruncatedBranchRejected(bool ok) { + if (!ok) + throw std::runtime_error( + "Length-changing rewrite accepted a section with a truncated relative branch"); +} int main() { try { const Codegen::X64InstructionRewriter rewriter; - for (const auto& code : {std::vector{0xB8, 0x90, 0x90, 0x90, 0x90, 0xC3}, - std::vector{0x48, 0x8B, 0x05, 0x90, 0x90, 0x90, 0x90, 0xC3}, - std::vector{0xE8, 0x90, 0x90, 0x90, 0x90, 0xC3}}) { + + for (const auto& code : { + std::vector{ + 0xB8, 0x90, 0x90, 0x90, 0x90, 0xC3}, + std::vector{ + 0x48, 0x8B, 0x05, 0x90, 0x90, 0x90, 0x90, 0xC3}, + std::vector{ + 0xE8, 0x90, 0x90, 0x90, 0x90, 0xC3} + }) { const auto original = code; - for (std::size_t offset = 1; offset + 1 < code.size(); ++offset) { + + for (std::size_t offset = 1; + offset + 1 < code.size(); + ++offset) { bool rejected = false; - try { (void)rewriter.Rewrite(code, {offset, {0x90}}); } - catch (const std::exception&) { rejected = true; } + + try { + (void)rewriter.Rewrite(code, {offset, {0x90}}); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected && code == original); } + const auto result = rewriter.Rewrite(code, {0, {0x90}}); - Require(result.Bytes == std::vector{0x90, 0xC3}); + + Require(result.Bytes == std::vector{ + 0x90, 0xC3}); } - const auto result = rewriter.Rewrite({0x90, 0x90, 0xC3}, {1, {0x90, 0x90}}); - Require(result.Bytes == std::vector{0x90, 0x90, 0x90, 0xC3}); - std::cout << "18 rewrite boundary cases passed\n"; - } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } + + const auto result = rewriter.Rewrite( + {0x90, 0x90, 0xC3}, + {1, {0x90, 0x90}}); + + Require(result.Bytes == std::vector{ + 0x90, 0x90, 0x90, 0xC3}); + + for (const auto& code : { + std::vector{ + 0x66, 0xEB, 0x01, 0x90, 0xC3}, + std::vector{ + 0x90, 0x66, 0xEB, 0xFC, 0xC3} + }) { + bool rejected = false; + + try { + const auto offset = + code[0] == 0x66 ? std::size_t{3} : std::size_t{0}; + + (void)rewriter.Rewrite(code, {offset, {0x66, 0x90}}); + } catch (const std::exception&) { + rejected = true; + } + + RequireTruncatedBranchRejected(rejected); + } + + const auto unaffected = rewriter.Rewrite( + {0x66, 0xEB, 0x00, 0x90, 0x90, 0xC3}, + {4, {0x66, 0x90}}); + + Require(unaffected.Bytes == std::vector{ + 0x66, 0xEB, 0x00, 0x90, 0x66, 0x90, 0xC3}); + + std::vector forwardWraparound(0x10004, 0x90); + forwardWraparound[0xFFFA] = 0x66; + forwardWraparound[0xFFFB] = 0xEB; + forwardWraparound[0xFFFC] = 0x03; + + bool forwardWraparoundRejected = false; + try { + (void)rewriter.Rewrite(forwardWraparound, {10, {0x66, 0x90}}); + } catch (const std::exception&) { + forwardWraparoundRejected = true; + } + RequireTruncatedBranchRejected(forwardWraparoundRejected); + + std::vector backwardWraparound(0x10004, 0x90); + backwardWraparound[0] = 0x66; + backwardWraparound[1] = 0xEB; + backwardWraparound[2] = 0xFC; + + bool backwardWraparoundRejected = false; + try { + (void)rewriter.Rewrite(backwardWraparound, {0xFFFE, {0x66, 0x90}}); + } catch (const std::exception&) { + backwardWraparoundRejected = true; + } + RequireTruncatedBranchRejected(backwardWraparoundRejected); + + std::cout << "21 rewrite boundary cases passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } } diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 7425d10863..2ec776579e 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -17,7 +17,7 @@ - `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified. - [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. macOS resolves these imports through the host loader by name and is unchanged. - [Unscoped imports on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) - in an executable or bundled module that names a dependency with `.sprx`, `.suprx` or `.debug_prx`, as homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) does, an import without a module that is named by symbol is bound to the NID of that name only. Unlike Windows, the name as written is not tried first, because the host loader also searches the host C library, which exports `malloc` or `pthread_create` under their names. A name that a bundled module exports still binds to that module, as does a bundled module's import of a name the executable exports, and such a dependency loads the `.prx` library of the same stem. Unqualified imports of other executables and bundled modules are still bound by name. -- 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. +- 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. The rewriter conservatively rejects length-changing edits when a surviving truncated relative branch would move relative to its target (only one of the two lies after the cut) or when its section-relative target falls outside the 16-bit range, because the runtime address base needed to resolve a wrapped destination is unavailable; truncated branches that keep their distance and stay in range are accepted. - The [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp) takes the branch targets that keep an instruction in place from a linear scan of the code segment, so data in that segment decodes as branches too: OpenSSL's x86-64 assembly keeps strings such as "SHA1 block transform for x86_64, CRYPTOGAMS by " in its code, and in SuperTuxKart (PPSA69421) and Mortalfin (PPSA99141) the `jae` read from "sf" points into the VEX prefix of a 256-bit store. A direct branch target inside a scanned instruction counts only where every byte before it is a legacy prefix or REX, which a real branch can skip (a `lock` prefix, for example); a target in a VEX prefix, opcode or operand is ignored, as a jump-table entry that starts no instruction already is. A real branch to such a target, overlapping instructions as hand-written code can, is not seen, and the instruction it enters can be moved. - 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. - In Windows output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([TLS builder](../../../core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp)): the host owns GS, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`.