fix(relinker): rebase truncated branch guard onto current main

This commit is contained in:
Yaash Singhh authored and Adrià Franch committed 2026-10-24 10:58:00 +02:00
1 parent aeeaefad98
commit f5fbf2f98d
3 files changed
+162 -13

No files matched your search

@@ -96,6 +96,66 @@ RewriteResult X64InstructionRewriter::Rewrite(
const auto cutStart = static_cast<std::int64_t>(offset);
const std::int64_t cutEnd = cutStart + oldLength;
if (delta != 0) {
const auto sectionSize = static_cast<std::int64_t>(codeSection.size());
for (const auto& match : matches) {
const auto matchOffset = static_cast<std::int64_t>(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<std::int8_t>(codeSection[displacementOffset]);
} else if (displacementSize == 2) {
const auto raw = static_cast<std::uint16_t>(
static_cast<std::uint16_t>(codeSection[displacementOffset]) |
(static_cast<std::uint16_t>(
codeSection[displacementOffset + 1]) << 8));
branchDisplacement = static_cast<std::int16_t>(raw);
} else {
throw CodegenException(
"Unexpected displacement size for truncated relative branch",
match.Offset);
}
const auto rawTargetPosition =
matchOffset + static_cast<std::int64_t>(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<std::uint8_t> rewritten;
rewritten.reserve(codeSection.size() + static_cast<std::size_t>(delta > 0 ? delta : 0));
rewritten.insert(rewritten.end(), codeSection.begin(), codeSection.begin() + offset);
@@ -1,27 +1,116 @@
#include <codegen/x86/X64InstructionRewriter.hpp>
#include <iostream>
#include <stdexcept>
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<std::uint8_t>{0xB8, 0x90, 0x90, 0x90, 0x90, 0xC3},
std::vector<std::uint8_t>{0x48, 0x8B, 0x05, 0x90, 0x90, 0x90, 0x90, 0xC3},
std::vector<std::uint8_t>{0xE8, 0x90, 0x90, 0x90, 0x90, 0xC3}}) {
for (const auto& code : {
std::vector<std::uint8_t>{
0xB8, 0x90, 0x90, 0x90, 0x90, 0xC3},
std::vector<std::uint8_t>{
0x48, 0x8B, 0x05, 0x90, 0x90, 0x90, 0x90, 0xC3},
std::vector<std::uint8_t>{
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<std::uint8_t>{0x90, 0xC3});
Require(result.Bytes == std::vector<std::uint8_t>{
0x90, 0xC3});
}
const auto result = rewriter.Rewrite({0x90, 0x90, 0xC3}, {1, {0x90, 0x90}});
Require(result.Bytes == std::vector<std::uint8_t>{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<std::uint8_t>{
0x90, 0x90, 0x90, 0xC3});
for (const auto& code : {
std::vector<std::uint8_t>{
0x66, 0xEB, 0x01, 0x90, 0xC3},
std::vector<std::uint8_t>{
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<std::uint8_t>{
0x66, 0xEB, 0x00, 0x90, 0x66, 0x90, 0xC3});
std::vector<std::uint8_t> 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<std::uint8_t> 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;
}
}
+1 -1
View File
@@ -15,7 +15,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 truncated relative branch's computed section-relative target falls outside the 16-bit range, because its runtime address base is unavailable to resolve a wrapped destination safely.
- 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 <appro@openssl.org>" 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`.