From 14e1079336fdf66471eb229ad55131364e0bddc1 Mon Sep 17 00:00:00 2001 From: aayushmishraaa Date: Sun, 11 Oct 2026 22:26:47 +0530 Subject: [PATCH 01/28] fix(relinker): reject input executables that are not x86-64 Co-Authored-By: Claude Opus 5.5 (cherry picked from commit f0bca23deb9ec9ae598b5c12967db34a8b92f2e9) --- .../relinker/src/parsing/ElfReader.cpp | 2 ++ .../relinker/relinker/tests/test_elf_header.py | 18 +++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index c6af1ec10b5..4b0e8560dca 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -86,6 +86,8 @@ ElfHeader ElfReader::ReadHeader() const { } if (_fileBuffer[4] != 2 || _fileBuffer[5] != 1 || _fileBuffer[6] != 1) throw RelinkerException("Expected little-endian ELF64 version 1"); + if (_readU16At(0x12) != 62) + throw RelinkerException("Expected an x86-64 ELF"); ElfHeader header{}; header.Machine = _readU16At(0x12); diff --git a/core/relinker/relinker/tests/test_elf_header.py b/core/relinker/relinker/tests/test_elf_header.py index 553665bbde4..d314868311a 100644 --- a/core/relinker/relinker/tests/test_elf_header.py +++ b/core/relinker/relinker/tests/test_elf_header.py @@ -8,17 +8,21 @@ def identification_cases(relinker, directory): image = main_fixture() - cases = [("sysv", image, False)] + cases = [("sysv", image, None)] freebsd = bytearray(image) freebsd[7] = 9 - cases.append(("freebsd", freebsd, False)) + cases.append(("freebsd", freebsd, None)) for field, values in ((4, (0, 1, 255)), (5, (0, 2, 255)), (6, (0, 2, 255))): for value in values: data = bytearray(image) data[field] = value - cases.append((f"ident-{field}-{value}", data, True)) + cases.append((f"ident-{field}-{value}", data, "Expected little-endian ELF64 version 1")) + for value in (0, 3, 183): + data = bytearray(image) + data[18:20] = value.to_bytes(2, "little") + cases.append((f"machine-{value}", data, "Expected an x86-64 ELF")) for mode, flags in enumerate(([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"])): - for name, data, invalid in cases: + for name, data, error in cases: source = Path(directory) / f"{mode}-{name}.elf" output = source.with_suffix(".out") registry = output.with_name(output.stem + ".registry.json") @@ -26,8 +30,8 @@ def identification_cases(relinker, directory): result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", *flags, str(source), str(output)], capture_output=True, text=True, timeout=20) assert source.read_bytes() == data, (mode, name, "source changed") - if invalid: - assert result.returncode == 2 and "Expected little-endian ELF64 version 1" in result.stderr, (mode, name, result) + if error: + assert result.returncode == 2 and error in result.stderr, (mode, name, result) assert not output.exists() and not registry.exists(), (mode, name, "output written for unsupported ELF identification") else: assert result.returncode == 0 and registry.is_file(), (mode, name, result) @@ -39,7 +43,7 @@ def main(): cases = [ ("tiny", bytes(10), "File too small for ELF header"), ("truncated", b"\x7fELF\x02\x01\x01" + bytes(25), "File too small for ELF header"), - ("header-only", b"\x7fELF\x02\x01\x01" + bytes(57), "No PT_DYNAMIC segment found"), + ("header-only", b"\x7fELF\x02\x01\x01" + bytes(11) + b"\x3e" + bytes(45), "No PT_DYNAMIC segment found"), ("bad-magic", bytes(64), "Invalid ELF magic number"), ] with tempfile.TemporaryDirectory(prefix="anyps5-elf-header-") as directory: From 79e452a2c688d5f957eb3e42a7ff4fb94515dab7 Mon Sep 17 00:00:00 2001 From: Ayan <108534604+excited-raccoon@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:55:39 +0000 Subject: [PATCH 02/28] perf(relinker): walk fallthrough instructions without queueing (cherry picked from commit d70056700837178d30548ddc224c037acb954119) --- core/relinker/CMakeLists.txt | 14 ++ .../UnusedNidFilter/ControlFlowGraph.cpp | 151 ++++++++-------- .../relinker/tests/ControlFlowGraphTests.cpp | 171 ++++++++++++++++++ 3 files changed, 261 insertions(+), 75 deletions(-) create mode 100644 core/relinker/relinker/tests/ControlFlowGraphTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index ab124744fb3..cf04b681b3b 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -499,6 +499,20 @@ if(BUILD_TESTING) ) add_test(NAME entry_stub COMMAND entry_stub_tests) + add_test_executable(control_flow_graph_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ControlFlowGraphTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(control_flow_graph_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ) + add_test(NAME control_flow_graph COMMAND control_flow_graph_tests) + set_tests_properties(control_flow_graph PROPERTIES TIMEOUT 20) + add_test_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index 10629ddefb6..7e74855fa2f 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -80,87 +80,88 @@ void buildGraph( VirtualAddress va = worklist.front(); worklist.pop(); - if (va < textVaddr || va >= textVaddr + static_cast(text.size())) - throw RelinkerException("CFG: jump target outside text segment", va); - - std::size_t bufOff = static_cast(va - textVaddr); - std::size_t available = text.size() - bufOff; - if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); - - Codegen::DecodedInstructionInfo info; - if (speculative) { - try { + while (va >= textVaddr && va - textVaddr < text.size() && !reachable.contains(va)) { + std::size_t bufOff = static_cast(va - textVaddr); + std::size_t available = text.size() - bufOff; + if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); + + Codegen::DecodedInstructionInfo info; + if (speculative) { + try { + info = decoder.DecodeInstruction(text.data() + bufOff, available); + } catch (const Codegen::CodegenException&) { + break; + } + } else { info = decoder.DecodeInstruction(text.data() + bufOff, available); - } catch (const Codegen::CodegenException&) { - continue; } - } else { - info = decoder.DecodeInstruction(text.data() + bufOff, available); - } - reachable.insert(va); + reachable.insert(va); + visited.erase(va); - VirtualAddress nextVaddr = va + static_cast(info.Length); + VirtualAddress nextVaddr = va + static_cast(info.Length); - if (followCodeAddresses && info.HasRipRelativeDisp && text[bufOff + info.OpcodeOffset] == 0x8d && (info.RexPrefix & 8)) { - std::int32_t displacement = 0; - std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - enqueue(static_cast(static_cast(nextVaddr) + displacement)); - } + if (followCodeAddresses && info.HasRipRelativeDisp && text[bufOff + info.OpcodeOffset] == 0x8d && (info.RexPrefix & 8)) { + std::int32_t displacement = 0; + std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); + enqueue(static_cast(static_cast(nextVaddr) + displacement)); + } - if (data != nullptr && info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.Opcode == 0x8D) { - std::int32_t displacement = 0; - std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - const auto base = static_cast(static_cast(nextVaddr) + displacement); - takeAddress(base); - for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); - } + if (data != nullptr && info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.Opcode == 0x8D) { + std::int32_t displacement = 0; + std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); + const auto base = static_cast(static_cast(nextVaddr) + displacement); + takeAddress(base); + for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); + } - switch (info.FlowKind) { - using enum Codegen::ControlFlowKind; - case Sequential: - enqueue(nextVaddr); - break; - case ConditionalBranch: - enqueue(nextVaddr); - if (info.HasBranchTarget) - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - break; - case UnconditionalJump: - if (info.HasRipRelativeDisp) { - std::int32_t disp = 0; - std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); - auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); - if (resolved.has_value()) - enqueue(*resolved); - } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - } - break; - case Call: - if (info.HasRipRelativeDisp) { - std::int32_t disp = 0; - std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); - auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); - if (resolved.has_value()) - enqueue(*resolved); - } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - } - enqueue(nextVaddr); - break; - case IndirectCall: - enqueue(nextVaddr); - break; - case Return: - case IndirectJump: - case Trap: - break; + bool fallThrough = true; + switch (info.FlowKind) { + using enum Codegen::ControlFlowKind; + case Sequential: + break; + case ConditionalBranch: + if (info.HasBranchTarget) + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + break; + case UnconditionalJump: + fallThrough = false; + if (info.HasRipRelativeDisp) { + std::int32_t disp = 0; + std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); + VirtualAddress slotVaddr = static_cast( + static_cast(nextVaddr) + disp + ); + auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); + if (resolved.has_value()) + enqueue(*resolved); + } else if (info.HasBranchTarget) { + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + } + break; + case Call: + if (info.HasRipRelativeDisp) { + std::int32_t disp = 0; + std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); + VirtualAddress slotVaddr = static_cast( + static_cast(nextVaddr) + disp + ); + auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); + if (resolved.has_value()) + enqueue(*resolved); + } else if (info.HasBranchTarget) { + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + } + break; + case IndirectCall: + break; + case Return: + case IndirectJump: + case Trap: + fallThrough = false; + break; + } + if (!fallThrough) break; + va = nextVaddr; } } } diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp new file mode 100644 index 00000000000..5870e3c5e7a --- /dev/null +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Relinker::UnusedNidFilter; +using Relinker::VirtualAddress; +using Bytes = std::vector; +constexpr VirtualAddress TextAddress = 0xec00001000; + +class Pointers : public IRelativeRelocationIndex { +public: + std::map Slots; + + std::optional TargetOfSlot(VirtualAddress slot) const override { + const auto position = Slots.find(slot); + if (position == Slots.end()) return std::nullopt; + return position->second; + } + + std::vector Targets() const override { + std::vector targets; + for (const auto& [slot, target] : Slots) targets.push_back(target); + return targets; + } +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void RequireOffsets(const IControlFlowGraph& graph, std::initializer_list offsets) { + std::unordered_set expected; + for (const auto offset : offsets) expected.insert(TextAddress + offset); + Require(graph.ReachableVaddrs() == expected, "Unexpected reachable instruction set"); + for (const auto address : expected) Require(graph.IsReachable(address), "Missing reachable instruction"); + Require(!graph.IsReachable(TextAddress - 1), "Address before code is reachable"); +} + +void SetDisplacement(Bytes& text, std::size_t offset, std::size_t next, VirtualAddress target) { + const auto displacement = static_cast(target - (TextAddress + next)); + std::memcpy(text.data() + offset, &displacement, sizeof(displacement)); +} + +void CheckFallthrough() { + const Pointers pointers; + const Bytes text{0x90, 0x48, 0x89, 0xc0, 0xc3, 0x90}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 1, 4}); + const auto end = BuildControlFlowGraph(Bytes{0x90, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*end, {0, 1}); + const auto empty = BuildControlFlowGraph(Bytes{}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*empty, {}); + const auto outside = BuildControlFlowGraph(text, TextAddress, TextAddress - 1, {TextAddress + text.size()}, pointers); + RequireOffsets(*outside, {}); +} + +void CheckBranches() { + const Pointers pointers; + const Bytes text{0x75, 0x03, 0x90, 0xeb, 0x03, 0x90, 0xeb, 0xfa, 0xc3, 0x90}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {TextAddress, TextAddress + 2}, pointers); + RequireOffsets(*graph, {0, 2, 3, 5, 6, 8}); + const auto self = BuildControlFlowGraph(Bytes{0xeb, 0xfe, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*self, {0}); + const auto outside = BuildControlFlowGraph(Bytes{0xeb, 0x7f, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*outside, {0}); + const auto trap = BuildControlFlowGraph(Bytes{0x0f, 0x0b, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*trap, {0}); +} + +void CheckCalls() { + const Pointers pointers; + const Bytes text{0xe8, 0x03, 0, 0, 0, 0x90, 0xc3, 0x90, 0xe8, 0xf3, 0xff, 0xff, 0xff, 0xc3}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 5, 6, 8, 13}); + const auto indirect = BuildControlFlowGraph(Bytes{0xff, 0xd0, 0x90, 0xff, 0xe0, 0xc3}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*indirect, {0, 2, 3}); +} + +void CheckRelocatedTransfers() { + constexpr auto slot = TextAddress + 0x1000; + Bytes text{0xff, 0x15, 0, 0, 0, 0, 0x90, 0xc3, 0xff, 0x25, 0, 0, 0, 0, 0x90, 0x90, 0xc3}; + SetDisplacement(text, 2, 6, slot); + SetDisplacement(text, 10, 14, slot + 8); + Pointers pointers; + pointers.Slots = {{slot, TextAddress + 8}, {slot + 8, TextAddress + 16}}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 6, 7, 8, 16}); + const Pointers missing; + const auto unresolved = BuildControlFlowGraph(text, TextAddress, TextAddress, {TextAddress + 8}, missing); + RequireOffsets(*unresolved, {0, 6, 7, 8}); +} + +void CheckCodeAddresses() { + Bytes text(17, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 16); + text[7] = text[16] = 0xc3; + const Pointers pointers; + const auto ordinary = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*ordinary, {0, 7}); + const auto addresses = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers, true); + RequireOffsets(*addresses, {0, 7, 16}); +} + +void CheckSpeculativeRoots() { + constexpr auto table = TextAddress + 0x1000; + Bytes text(32, 0xc3); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, table); + text[16] = 0x90; + text[31] = 0x0f; + Bytes entries(12); + const std::int32_t targets[]{static_cast(TextAddress + 16 - table), + static_cast(TextAddress + 31 - table), 0}; + std::memcpy(entries.data(), targets, sizeof(targets)); + Pointers pointers; + pointers.Slots = {{table + 16, TextAddress + 24}, {table + 24, TextAddress + 31}}; + const std::vector data{{table, entries}}; + const auto graph = BuildAddressTakenControlFlowGraph(text, TextAddress, TextAddress, {}, pointers, data); + RequireOffsets(*graph, {0, 7, 16, 17, 24}); + bool threw = false; + try { + BuildAddressTakenControlFlowGraph(text, TextAddress, TextAddress, {TextAddress + 31}, pointers, data); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "Malformed mandatory root was treated as speculative"); +} + +void CheckBorrowedReachability() { + const Pointers pointers; + const Bytes text{0x90, 0x90, 0xc3, 0x90, 0xc3}; + std::unordered_set reachable{TextAddress + 1}; + const auto first = BuildControlFlowGraph(std::span(text), TextAddress, TextAddress, {}, pointers, false, reachable); + RequireOffsets(*first, {0, 1}); + Require(&first->ReachableVaddrs() == &reachable, "Borrowed graph copied the result set"); + const auto second = BuildControlFlowGraph(std::span(text), TextAddress, TextAddress + 3, + {TextAddress + 3, TextAddress + 4}, pointers, false, reachable); + RequireOffsets(*second, {0, 1, 3, 4}); + RequireOffsets(*first, {0, 1, 3, 4}); +} + +} + +int main() { + try { + CheckFallthrough(); + CheckBranches(); + CheckCalls(); + CheckRelocatedTransfers(); + CheckCodeAddresses(); + CheckSpeculativeRoots(); + CheckBorrowedReachability(); + std::cout << "Control-flow graph tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "control_flow_graph_tests: " << error.what() << '\n'; + return 1; + } +} From 1a4bdb70ec3cbba328f948e830a4938ac17edb4c Mon Sep 17 00:00:00 2001 From: Aryan singh Date: Sun, 11 Oct 2026 23:26:35 +0530 Subject: [PATCH 03/28] fix(relinker): resolve debug PRX dependencies by module identity (cherry picked from commit d3fc14a50c389373b5a0a174352644a25c19ef74) --- core/relinker/relinker/src/guest/GuestModuleBuilder.cpp | 2 +- core/relinker/relinker/tests/test_guest_module_identity.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index f6c8f8a4958..e815362c352 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -121,7 +121,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::map discovered; const auto matchesIdentity = [](const std::string& name, const std::vector& identities) { return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { - return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx"; + return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx" || name == identity + ".debug_prx"; }); }; for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)), windows)); diff --git a/core/relinker/relinker/tests/test_guest_module_identity.py b/core/relinker/relinker/tests/test_guest_module_identity.py index 211b7a6cca4..2c53e77feb0 100644 --- a/core/relinker/relinker/tests/test_guest_module_identity.py +++ b/core/relinker/relinker/tests/test_guest_module_identity.py @@ -147,7 +147,7 @@ def run(output, expected=22, error=None): for windows in (True, False): for tag in (0x6100000d, 0x61000043): - for suffix in ('.suprx', '.prx', '.sprx'): + for suffix in ('.suprx', '.prx', '.sprx', '.debug_prx'): request = 'libGuest' + suffix result, output = convert(f'{windows}-{tag}-{suffix}', windows, request, providers={'renamed.prx': declared_provider(22, ('libGuest',), tag=tag)}, guest_request=request) From 301cb1da1f4c8123177dc05a0ce52dd5fb28c694 Mon Sep 17 00:00:00 2001 From: Aryan singh Date: Mon, 12 Oct 2026 03:38:07 +0530 Subject: [PATCH 04/28] fix(relinker): share module suffixes between identity and import matching The guest identity predicate accepted .debug_prx while Domain::ImportModule did not: a dependency named libGuest.debug_prx normalised to libGuest_debug_prx, so a qualified import declared in module libGuest was left unscoped. When another guest module exported the same symbol, the conversion failed with an ambiguous guest import, while .prx, .sprx and .suprx resolved to the declaring module. Module suffixes are now one list in the domain layer, used by the identity predicate, import module normalisation and the default .prx suffix. Needed-name stem aliasing keeps its own list. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 7c1b64b0ca273c9e9f6a2812aca11f97a7b3d238) --- .../domain/include/domain/ImportModule.hpp | 14 ++++++++++---- .../relinker/src/guest/GuestModuleBuilder.cpp | 6 +++--- .../relinker/tests/test_guest_module_identity.py | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index ecc49bd3999..37bab4c73e1 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -3,15 +3,21 @@ #include #include +#include #include #include namespace Domain { +inline constexpr std::array ModuleSuffixes = {".prx", ".sprx", ".suprx", ".debug_prx"}; + +inline std::string_view ModuleSuffix(const std::string_view name) { + const auto suffix = std::find_if(ModuleSuffixes.begin(), ModuleSuffixes.end(), [&](const auto candidate) { return name.ends_with(candidate); }); + return suffix == ModuleSuffixes.end() ? std::string_view{} : *suffix; +} + inline std::string ImportModuleName(std::string name) { - if (name.ends_with(".prx")) name.resize(name.size() - 4); - else if (name.ends_with(".sprx")) name.resize(name.size() - 5); - else if (name.ends_with(".suprx")) name.resize(name.size() - 6); + name.resize(name.size() - ModuleSuffix(name).size()); if (name.ends_with("-module")) name.resize(name.size() - 7); std::replace(name.begin(), name.end(), '.', '_'); return name; @@ -44,7 +50,7 @@ inline std::string ImportModule(const std::string& symbol, const std::map dependencies = {}) { auto name = DeclaredImportModule(symbol, modules); if (name.empty()) return {}; - if (!name.ends_with(".prx") && !name.ends_with(".sprx") && !name.ends_with(".suprx")) name += ".prx"; + if (ModuleSuffix(name).empty()) name += ".prx"; if (std::find(dependencies.begin(), dependencies.end(), name) != dependencies.end()) return name; std::string matched; for (const auto& dependency : dependencies) { diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index e815362c352..01047372f45 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -120,9 +121,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::FileReader reader; std::map discovered; const auto matchesIdentity = [](const std::string& name, const std::vector& identities) { - return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { - return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx" || name == identity + ".debug_prx"; - }); + const auto stem = std::string_view(name).substr(0, name.size() - Domain::ModuleSuffix(name).size()); + return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { return name == identity || stem == identity; }); }; for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)), windows)); if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); diff --git a/core/relinker/relinker/tests/test_guest_module_identity.py b/core/relinker/relinker/tests/test_guest_module_identity.py index 2c53e77feb0..2a03efd3fc4 100644 --- a/core/relinker/relinker/tests/test_guest_module_identity.py +++ b/core/relinker/relinker/tests/test_guest_module_identity.py @@ -161,6 +161,20 @@ def run(output, expected=22, error=None): child = output.parent / 'app0/sce_module/consumer.prx.guest.prx' assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx'] + for suffix in ('.suprx', '.prx', '.sprx', '.debug_prx'): + providers = {'renamed.prx': declared_provider(22, ('libGuest',)), + 'other.prx': declared_provider(33, ('libOther',)), + 'own.prx': declared_provider(22, ('libOwn',), symbol='own'), + 'consumer.prx': consumer('libGuest' + suffix, module_name='libGuest')} + result, output = convert(f'{windows}-shared-{suffix}', windows, 'libOwn.prx', module_name='libOwn', + symbol='own#A#B', providers=providers) + assert result.returncode == 0, result.stderr + if windows: + run(output) + else: + child = output.parent / 'app0/sce_module/consumer.prx.guest.prx' + assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx'] + providers = {'one.prx': declared_provider(22, ('libGuest',)), 'two.prx': declared_provider(33, ('libGuest',), symbol='other')} result, output = convert(f'{windows}-ambiguous', windows, 'libGuest.suprx', providers=providers) From c1dd887a49af31b17bf60c14949a650a481732c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 22:21:32 +0000 Subject: [PATCH 05/28] fix(relinker): decode 3DNow! instructions with their ModRM and opcode suffix 0F 0F was decoded as a two-byte instruction. 3DNow! instructions are 0F 0F ModRM [SIB] [displacement] imm8, the trailing byte selecting the operation (AMD64 Architecture Programmer's Manual Volume 3, Appendix A, 3DNow! opcodes). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VZWLdTjf1S57iLgn2rcaFb (cherry picked from commit feb19a844d14fe77c68c1c71c95ce8508c2dc053) --- .../codegen/include/codegen/x86/X64OpcodeConstants.hpp | 1 + core/relinker/codegen/src/x86/X64InstructionDecoder.cpp | 2 +- core/relinker/codegen/tests/DecoderLengthCases.hpp | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 889d0c447f8..7a9a14a364e 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -170,6 +170,7 @@ inline constexpr std::uint8_t TwoByteGrp6 = 0x00; inline constexpr std::uint8_t TwoByteLar = 0x02; inline constexpr std::uint8_t TwoByteLsl = 0x03; inline constexpr std::uint8_t TwoBytePrefetchw = 0x0D; +inline constexpr std::uint8_t TwoByte3DNow = 0x0F; inline constexpr std::uint8_t TwoByteMovCrDrMin = 0x20; inline constexpr std::uint8_t TwoByteMovCrDrMax = 0x23; inline constexpr std::uint8_t TwoBytePopcnt = 0xB8; diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index b894332bdf0..ab5785a01a6 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -242,7 +242,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t hasModRm = true; } else if (opcode >= TwoByteJccRel32Min && opcode <= TwoByteJccRel32Max) { immediateSize = ImmSize32; - } else if (opcode == TwoByteMovImm8ModRm) { + } else if (opcode == TwoByteMovImm8ModRm || opcode == TwoByte3DNow) { hasModRm = true; immediateSize = ImmSize8; } else if (opcode >= TwoByteShiftImm8Min && opcode <= TwoByteShiftImm8Max) { diff --git a/core/relinker/codegen/tests/DecoderLengthCases.hpp b/core/relinker/codegen/tests/DecoderLengthCases.hpp index 561f3a64e08..10747c9a61b 100644 --- a/core/relinker/codegen/tests/DecoderLengthCases.hpp +++ b/core/relinker/codegen/tests/DecoderLengthCases.hpp @@ -33,6 +33,12 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"0f0b", 2}, {"0f0d846511223344", 8}, {"0f0e", 2}, + {"0f0fc196", 4}, + {"0f0f08b7", 4}, + {"0f0f4f80b4", 5}, + {"0f0f846511223344b4", 9}, + {"0f0f0d11223344a6", 8}, + {"410f0f1c24bf", 6}, {"0f10846511223344", 8}, {"0f11846511223344", 8}, {"0f12846511223344", 8}, From 92405592bd58e1e4c84249a3011559699fedc874 Mon Sep 17 00:00:00 2001 From: ethan Date: Sun, 11 Oct 2026 15:24:53 -0700 Subject: [PATCH 06/28] fix(relinker): filter non-reference RIP-relative FF operations (cherry picked from commit 409f53eaed31b12761b58c158d4a75e69389c1f9) --- core/relinker/CMakeLists.txt | 12 ++++++ .../src/analysis/CallSiteResolver.cpp | 3 +- .../relinker/tests/CallSiteResolverTests.cpp | 41 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 core/relinker/relinker/tests/CallSiteResolverTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index cf04b681b3b..35a39326e6b 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -312,6 +312,18 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/io/include ) add_test(NAME amd64_only_converter COMMAND amd64_only_converter_tests) + add_test_executable(call_site_resolver_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteResolverTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(call_site_resolver_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ) + add_test(NAME call_site_resolver COMMAND call_site_resolver_tests) add_test_executable(decoder_length_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/DecoderLengthTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 0c698568e61..fbd29c405f8 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -28,7 +28,8 @@ bool isRipRelativeFF(const std::vector& text, const Codegen::Instr std::uint8_t modrm = text[cur + 1]; std::uint8_t mod = (modrm >> ModRmModShift) & ModRmModMask; std::uint8_t rm = modrm & ModRmRmMask; - return mod == ModRmModIndirect && rm == ModRmRmRipRelative; + const auto operation = (modrm >> ModRmRegShift) & ModRmRegMask; + return mod == ModRmModIndirect && rm == ModRmRmRipRelative && (operation >= 2 && operation <= 6); } break; } diff --git a/core/relinker/relinker/tests/CallSiteResolverTests.cpp b/core/relinker/relinker/tests/CallSiteResolverTests.cpp new file mode 100644 index 00000000000..f780307eb3f --- /dev/null +++ b/core/relinker/relinker/tests/CallSiteResolverTests.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const std::string& name) { + if (!condition) throw std::runtime_error(name); +} + +void check(const std::string& name, std::initializer_list instruction, bool expected) { + auto resolver = Relinker::MakeCallSiteResolver(); + const std::vector text(instruction); + const auto sites = resolver->ResolveCallSites(text, 0x1000, 0x1100, 8); + require(sites == (expected ? std::vector{0x1000} : std::vector{}), name); + require(resolver->ResolveCallSites(text, 0x1000, 0x1200, 8).empty(), name + " unexpected address"); +} + +} + +int main() { + try { + check("indirect call", {0xff, 0x15, 0xfa, 0, 0, 0}, true); + check("indirect tail jump", {0xff, 0x25, 0xfa, 0, 0, 0}, true); + check("far indirect call", {0xff, 0x1d, 0xfa, 0, 0, 0}, true); + check("far indirect jump", {0xff, 0x2d, 0xfa, 0, 0, 0}, true); + check("RIP-relative function pointer load", {0x48, 0x8b, 0x05, 0xf9, 0, 0, 0}, true); + check("increment memory", {0xff, 0x05, 0xfa, 0, 0, 0}, false); + check("decrement memory", {0xff, 0x0d, 0xfa, 0, 0, 0}, false); + check("push memory", {0xff, 0x35, 0xfa, 0, 0, 0}, true); + std::cout << "Call-site resolver tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "call_site_resolver_tests: " << error.what() << '\n'; + return 1; + } +} From 9b89aaf2e0d86487628ed7cd0fbcff60dfd95180 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 22:34:39 +0000 Subject: [PATCH 07/28] fix(relinker): report the ModRM of 3DNow! instructions in decoded instruction info DecodeInstruction did not treat 0F 0F as having a ModRM byte, so a RIP-relative 3DNow! operand was not reported to the analyses and the --to-intel stub relocation that read HasRipRelativeDisp. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VZWLdTjf1S57iLgn2rcaFb (cherry picked from commit 36c78ba1433e467d777595441067916cc38873b5) --- core/relinker/codegen/src/x86/X64InstructionDecoder.cpp | 2 +- core/relinker/codegen/tests/DecoderLengthTests.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index ab5785a01a6..4f9ff5b3b6f 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -556,7 +556,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( op == TwoByteImulRmModRm || op == TwoByteGrp7 || op == TwoByteGrp15 || op == TwoByteXadd || op == TwoByteGrp9 || op == TwoByteNopModRm || - op == TwoByteEndbr || op == TwoByteMovImm8ModRm || + op == TwoByteEndbr || op == TwoByteMovImm8ModRm || op == TwoByte3DNow || op == ThreeByteEscape38 || op == ThreeByteEscape3A || (op >= TwoByteShiftImm8Min && op <= TwoByteShiftImm8Max) || op == TwoByteShldImm8 || op == TwoByteShrdImm8 || diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index d119b3b3200..e141e1539e6 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -77,6 +77,10 @@ int main() { } catch (const Codegen::CodegenException&) { } } + 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"); + require(threeDNowInfo.HasRipRelativeDisp && threeDNowInfo.RipRelativeDispOffset == 3, "3DNow! RIP-relative displacement was not reported"); std::cout << "Decoder length tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 9896bbb0025ed0449db8c2f5b0aa16cbf2bd432f Mon Sep 17 00:00:00 2001 From: ethan Date: Sun, 11 Oct 2026 15:45:26 -0700 Subject: [PATCH 08/28] fix(relinker): resolve .suprx guest module aliases (cherry picked from commit 0a324d8ccb47f79b3f513318aef1d67f2a1aa4d0) --- .../relinker/src/guest/GuestModuleBuilder.cpp | 2 +- .../tests/test_guest_needed_modules.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 01047372f45..08aec4772e1 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -45,7 +45,7 @@ std::string FoldFilename(std::string name) { std::string ModuleStem(std::string name, const bool windows) { if (windows) name = FoldFilename(std::move(name)); - for (const std::string_view suffix : {".debug_prx", ".sprx", ".prx"}) { + for (const std::string_view suffix : {".debug_prx", ".suprx", ".sprx", ".prx"}) { if (name.size() > suffix.size() && name.ends_with(suffix)) return name.substr(0, name.size() - suffix.size()); } return {}; diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index adc9b97da16..d15b96ac99a 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -142,6 +142,24 @@ def convert(case, windows, needed=NEEDED, repeats=1): needed = needed_libraries(output.read_bytes()) assert needed == [f"$ORIGIN/app0/{directory}/needed.prx.guest.prx"], needed + for requested, physical in ((b"needed.suprx", "needed.prx"), + (b"needed.prx", "needed.suprx"), + (b"needed.sprx", "needed.suprx"), + (b"needed.debug_prx", "needed.suprx")): + case = work / f"{windows}-suprx-alias-{requested.decode()}-{physical}" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / physical).write_bytes(module_with_symbol(True)) + result, output = convert(case, windows, requested) + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = case / "app0" / "sce_module" / (physical + ".guest.prx") + assert artifact.is_file(), (requested, physical, result.stdout) + if not windows: + needed = needed_libraries(output.read_bytes()) + assert needed == [f"$ORIGIN/app0/sce_module/{physical}.guest.prx"], needed + elif os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + if windows: case = work / "debug-name-case" (case / "prx").mkdir(parents=True) From ec101662ec656a020c4d02bdfd74c5e640de588c Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:02:06 -0400 Subject: [PATCH 09/28] fix(relinker): ignore no-op relocations in guest modules (cherry picked from commit 72d5b8ce9007a83b1721e445284f348f0f9e79d4) --- core/relinker/CMakeLists.txt | 3 + .../relinker/src/guest/GuestImageReader.cpp | 12 +- .../tests/test_guest_none_relocations.py | 189 ++++++++++++++++++ 3 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_none_relocations.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 35a39326e6b..5d3eb5f3fce 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -346,6 +346,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_rela_size.py $) add_test(NAME none_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_none_relocations.py $) + add_test(NAME guest_none_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_none_relocations.py $) + set_tests_properties(guest_none_relocations PROPERTIES TIMEOUT 20) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index e6c3545a7c3..52cf793e561 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -262,13 +262,15 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto size = get(sizeTag, sceSizeTag); if (size % 24 != 0) fail("Invalid relocation table size"); const auto offset = table(addressTag, sceAddressTag, size); - output.assign(bytes.begin() + offset, bytes.begin() + offset + size); + if (plt && size != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + output.reserve(size); for (std::uint64_t index = 0; index < size; index += 24) { - const auto info = Io::ReadU64(output, index + 8); + const auto info = Io::ReadU64(bytes, offset + index + 8); const auto relocation = static_cast(info); + if (relocation == 0) continue; if ((info >> 32) >= image.Symbols.size() || (plt && relocation != 7) || (relocation != 1 && relocation != 6 && relocation != 7 && relocation != 8 && relocation != 16 && relocation != 17 && relocation != 18)) fail("Unsupported relocation or symbol index"); - const auto target = Io::ReadU64(output, index); - const auto addend = Io::ReadU64(output, index + 16); + const auto target = Io::ReadU64(bytes, offset + index); + const auto addend = Io::ReadU64(bytes, offset + index + 16); mapped(target, 8, 2); const auto next = relocationTargets.lower_bound(target); if ((next != relocationTargets.end() && next->first < target + 8) || (next != relocationTargets.begin() && std::prev(next)->second > target)) fail("Overlapping guest relocations"); @@ -283,6 +285,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto& symbol = image.Symbols[info >> 32]; if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); } + output.insert(output.end(), bytes.begin() + offset + index, bytes.begin() + offset + index + 24); } }; if (tags.contains(7) || tags.contains(0x6100002f)) { @@ -290,7 +293,6 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector } copyRelocations(7, 0x6100002f, 8, 0x61000031, image.Dynamic.RelaData, false); copyRelocations(23, 0x61000029, 2, 0x6100002d, image.Dynamic.RelaPltData, true); - if (!image.Dynamic.RelaPltData.empty() && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); if (tags.contains(3) || tags.contains(0x61000027)) image.Got = get(3, 0x61000027); if (tags.contains(12)) image.Init = tags.at(12); if (tags.contains(13)) image.Fini = tags.at(13); diff --git a/core/relinker/relinker/tests/test_guest_none_relocations.py b/core/relinker/relinker/tests/test_guest_none_relocations.py new file mode 100644 index 00000000000..f11f7966933 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_none_relocations.py @@ -0,0 +1,189 @@ +from pathlib import Path +import platform +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture, pe_sections +from test_guest_module_directories import module_with_symbol +from test_guest_symbol_names import dynamic_tags, file_offset +from test_macos_output import MachO + + +TARGET = 0x2500 +FUNCTION = 0x1000 +UNCHANGED = 0x8877665544332211 +NOOP = (0, 0, 0) +IGNORED = (0xffffffffffffffff, 0xffffffff00000000, -1) +OVERLAP = (TARGET + 1, 0xffffffff00000000, -1) +RELATIVE = (TARGET, 8, FUNCTION) +JUMP_SLOT = (TARGET, (1 << 32) | 7, 0) + + +def module(rela=(), plt=(), sce=False, lifecycle=False, initial=UNCHANGED): + image = module_with_symbol(True) + image.extend(bytes(0xE00 - len(image))) + struct.pack_into(" Date: Sun, 11 Oct 2026 16:24:06 -0700 Subject: [PATCH 10/28] fix(relinker): resolve EIP-relative imports in strict filter (cherry picked from commit 63a27648a83d2d6e20420d3352363f63cdfcc9b9) --- .../codegen/include/codegen/CodegenTypes.hpp | 1 + .../codegen/src/x86/X64InstructionDecoder.cpp | 2 ++ .../UnusedNidFilter/StrictReachability.cpp | 3 ++- .../relinker/tests/StrictNidFilterTests.cpp | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index e41f6cd538f..43e5a7110b6 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -65,6 +65,7 @@ struct DecodedInstructionInfo { std::uint8_t RexPrefix; ControlFlowKind FlowKind; bool HasRipRelativeDisp; + bool AddressSize32; std::size_t RipRelativeDispOffset; bool HasModRm; std::uint8_t ModRmByte; diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index 4f9ff5b3b6f..f905d2df437 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -369,6 +369,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( b == PrefixOperandSize || b == PrefixAddressSize) { if (b >= PrefixSegFs && b <= PrefixSegGs) info.SegmentPrefix = b; + if (b == PrefixAddressSize) + info.AddressSize32 = true; info.RexPrefix = 0; ++pos; continue; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp index 71fd7342ef3..21229806dda 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp @@ -163,7 +163,8 @@ class Analyzer { if (info.HasRipRelativeDisp) { std::int32_t displacement; std::memcpy(&displacement, input.Text.data() + offset + info.RipRelativeDispOffset, sizeof(displacement)); - const auto target = next + static_cast(static_cast(displacement)); + auto target = next + static_cast(static_cast(displacement)); + if (info.AddressSize32) target = static_cast(target); if (input.ImportSlots.contains(target)) region.Imports.insert(target); if (!info.IsTwoByteOpcode && info.Opcode == 0x8D) { addAddressTaken(target); diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index f62ec3f0943..d7d1477ac90 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -98,6 +98,22 @@ void fallthroughAfterZeroPadding() { require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Zero padding after a region's last jump dropped its fall-through edge"); } +void addressSizeOverrideImport() { + auto input = fixture(); + input.Text.assign(32, 0); + input.TextVaddr = 0x100001000ull; + input.Entries = {input.TextVaddr}; + input.ImportSlots = {0x2000, 0x100002000ull}; + emit(input, 0, {0x67, 0xFF, 0x15, 0xF9, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x2000}, "EIP-relative import was not resolved with 32-bit address wrapping"); + input.Text.assign(32, 0); + emit(input, 0, {0x67, 0x48, 0x8B, 0x05, 0xF8, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x2000}, "REX.W EIP-relative load did not resolve its import"); + input.Text.assign(32, 0); + emit(input, 0, {0xFF, 0x15, 0xFA, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x100002000ull}, "RIP-relative import lost its full 64-bit address"); +} + void callbackAndRelocationRoots() { auto input = fixture(); ripOperand(input, 0, {0x48, 0x8D, 0x3D}, 0x1020); @@ -586,6 +602,7 @@ int main() { ud1DeadTail(); conditionalTailCall(); fallthroughAfterZeroPadding(); + addressSizeOverrideImport(); callbackAndRelocationRoots(); registerImportCall(); mergedJumpTable(); From 1ce820593d8a617ca83d3b55e9df3522cc080640 Mon Sep 17 00:00:00 2001 From: Archit Dewan <108434204+ArchitDewan@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:28:56 -0400 Subject: [PATCH 11/28] fix(relinker): reject empty positional paths (cherry picked from commit 5eb22de16d8fee5b1bb2f1a3db77711707240b22) --- core/relinker/cli/src/CliArgs.cpp | 2 ++ core/relinker/cli/tests/CliArgsTests.cpp | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index eef10daad7b..21d055d6b93 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -71,6 +71,8 @@ Args ParseArgs(int argc, char* argv[]) { args.windowsGui = true; } else if (arg.rfind("--", 0) == 0 || arg == "unused-filter") { throw std::runtime_error("unknown option: " + arg); + } else if (arg.empty()) { + throw std::runtime_error("input and output paths must be nonempty"); } else if (args.inputPath.empty()) { args.inputPath = arg; } else if (args.outputPath.empty()) { diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index ec27e771aa6..3cc339e55d3 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -163,6 +163,13 @@ void unknownOption() { expectThrows({"--", "input.elf", "output.elf"}, "unknown option: --"); } +void emptyPositionals() { + for (const auto& arguments : std::vector>{ + {"", "input.elf", "output.elf"}, {"input.elf", "", "output.elf"}, + {"", "output.elf"}, {"input.elf", ""}}) + expectThrows(arguments, "input and output paths must be nonempty"); +} + void tooManyPositionals() { expectThrows({"input.elf", "output.elf", "extra.elf"}, "unexpected argument: extra.elf"); } @@ -211,6 +218,7 @@ int main() { unusedFilterRejectsDuplicates(); unusedFilterBareWordIsUnknown(); unknownOption(); + emptyPositionals(); tooManyPositionals(); windowsDiagnosticsRequiresWindows(); windowsGuiRequiresWindows(); From d9ef3c0a2e0689f1b0d8236b268d3984c85a2fe4 Mon Sep 17 00:00:00 2001 From: saxopa <99569665+saxopa@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:12:02 +0200 Subject: [PATCH 12/28] fix(relinker): accept RELATIVE relocations against a zero-value section symbol OpenOrbis create-fself adds one to the symbol index of every relocation it copies from .rela.dyn, so each R_X86_64_RELATIVE entry of a homebrew built with the OpenOrbis toolchain names symbol 1, a local STT_SECTION symbol with value 0. FreeBSD rtld computes RELATIVE as base + addend without reading the symbol. The pipeline, the code analysis and the strict filter now accept index 0 or a STT_SECTION symbol with value 0, and still throw for any other symbol. The relocation is written with symbol index 0, where the pipeline previously dropped RELATIVE entries with a nonzero index. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit cded9ba8c373f0b149fd335e2ac8f73267531aeb) --- .../src/analysis/CodeInstructionCollector.cpp | 3 +- .../UnusedNidFilter/StrictNidFilter.cpp | 7 +++- .../src/pipeline/RelinkerPipeline.cpp | 14 +++++-- .../relinker/tests/StrictNidFilterTests.cpp | 26 +++++++++++++ .../relinker/tests/test_none_relocations.py | 38 +++++++++++++++---- 5 files changed, 76 insertions(+), 12 deletions(-) diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 49b4a86b2dc..5222f8a409d 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -236,7 +236,8 @@ CodeInstructionSet CodeInstructionCollector::Collect(const std::vector> 32; const auto addend = Io::ReadU64(bytes, offset + 16); if (type == 8) { - if (index != 0 || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); + const auto zeroSectionSymbol = index != 0 && index < symbolCount && (bytes[symbols + index * 24 + 4] & 15) == 3 && Io::ReadU64(bytes, symbols + index * 24 + 8) == 0; + if ((index != 0 && !zeroSectionSymbol) || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); addRoot(addend); } else if (type == 1 || type == 6 || type == 7) { if (index == 0 || index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index cf163ac5553..3eede671ebc 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -79,7 +79,12 @@ class StrictImage { const auto slot = read(*rela + offset); const auto info = read(*rela + offset + 8); if (static_cast(info) != 8) continue; - if (info >> 32) throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + if (const auto symbol = info >> 32; symbol != 0) { + const auto symbols = table(6, 0x61000039); + const auto size = tags.find(0x6100003F); + if (!symbols || size == tags.end() || symbol >= size->second / 24 || (read(*symbols + symbol * 24 + 4) & 15) != 3 || read(*symbols + symbol * 24 + 8) != 0) + throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + } const auto target = read(*rela + offset + 16); if (input.ImportSlots.contains(slot) || !input.Pointers.emplace(slot, target).second) throw RelinkerException("Strict filter: overlapping pointer relocations", slot); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index a486d3d6465..d1ff243f4de 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -219,6 +219,15 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return relaOff + off; }; + auto isZeroSectionSymbol = [&](const std::uint32_t symIdx) { + const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize) + throw RelinkerException("Symbol table entry out of bounds", symOff); + std::uint64_t value = 0; + std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); + return (raw[symOff + 4] & 0xf) == 3 && value == 0; + }; + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); @@ -236,7 +245,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf continue; if (relType == 8) { - if (symIdx != 0) + if (symIdx != 0 && !isZeroSectionSymbol(symIdx)) throw RelinkerException("RELATIVE relocation has a nonzero symbol index", pos); continue; } @@ -405,9 +414,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(&rOffset, raw.data() + pos, 8); std::memcpy(&rInfo, raw.data() + pos + 8, 8); std::memcpy(&rAddend, raw.data() + pos + 16, 8); - const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); - if (symIdx == 0 && relType == R_X86_64_RELATIVE) + if (relType == R_X86_64_RELATIVE) appendRela(dynSection.RelaData, rOffset, static_cast(R_X86_64_RELATIVE), rAddend); } }; diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index d7d1477ac90..bb3646c3610 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -592,6 +592,32 @@ void sceDynlibDataOffsets() { "Duplicate PT_SCE_DYNLIBDATA segments were accepted"); requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, duplicate, input.Text, input.TextVaddr); }, "Strict filter accepted duplicate PT_SCE_DYNLIBDATA segments"); + + auto sectionRelative = bytes; + write(sectionRelative, 208, 0x70); + write(sectionRelative, 216, 0x70); + write(sectionRelative, 0x330, 0x61000039); + write(sectionRelative, 0x338, 0x100); + write(sectionRelative, 0x340, 0x6100003B); + write(sectionRelative, 0x348, 24); + write(sectionRelative, 0x350, 0x6100003F); + write(sectionRelative, 0x358, 48); + write(sectionRelative, 0x61C, 3); + write(sectionRelative, 0x908, (std::uint64_t{1} << 32) | 8); + require(Relinker::MakeStrictUnusedNidFilter()->Filter(references, sectionRelative, input.Text, input.TextVaddr).size() == 1, + "Strict filter rejected a RELATIVE relocation against a zero-value section symbol"); + auto valuedSection = sectionRelative; + write(valuedSection, 0x620, 0x10); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, valuedSection, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation against a section symbol with a value"); + auto untyped = sectionRelative; + write(untyped, 0x61C, 0); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, untyped, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation against a non-section symbol"); + auto outside = sectionRelative; + write(outside, 0x908, (std::uint64_t{2} << 32) | 8); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, outside, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation symbol outside the symbol table"); } } diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 5a0d06fc3a9..cbf3d4cb366 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -79,13 +79,37 @@ def main(): if target == "windows" and os.name == "nt" and not has_import: run = subprocess.run([str(output)], capture_output=True, timeout=20) assert run.returncode == 42, (name, level, run.returncode, run.stderr) - source = work / "invalid-relative.elf" - source.write_bytes(image_with_relocations([noop, (0x300, (1 << 32) | 8, 0x210)], False, False)) - output = work / "invalid-relative.exe" - result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], - capture_output=True, text=True, timeout=20) - assert result.returncode == 2 and "RELATIVE relocation has a nonzero symbol index" in result.stderr - assert not output.exists() + for sce in (False, True): + image = image_with_relocations([noop, (0x300, (1 << 32) | 8, 0x210)], sce, False) + struct.pack_into(" Date: Sun, 11 Oct 2026 22:29:23 +0000 Subject: [PATCH 13/28] feat(relinker): split unaligned 256-bit stores for Rosetta Rosetta 2 ends the process when an unaligned 256-bit store crosses into a page the store may not write ("rosetta error: unexpectedly need to EmulateForward on a synchronous exception") instead of raising the fault. A store inside such a page, and a 128-bit store across one, fault normally. The macOS write watch keeps watched pages read-only between collections, so a guest that stores 32 bytes across the start of a watched page ends the process. --to-rosetta now lowers VMOVUPS, VMOVUPD and VMOVDQU with a 256-bit memory destination to a stub that stores the low half with the same instruction at VEX.L=0 and the high half with VEXTRACTF128 at the operand plus 16, so each half faults like any other store. RIP-relative operands are relocated from the instruction's place in the site. A store whose upper half has no 32-bit displacement fails the conversion. Aligned stores (VMOVAPS, VMOVAPD, VMOVDQA and the non-temporal forms) cannot cross a page and are left alone. Every store is lowered. A 4-byte store has no room for the jump to its stub, so its stub also takes one neighbouring instruction. The following one: a return ends the stub, a call pushes its original return address and jumps, a jump or conditional branch is re-encoded with a 32-bit displacement, and another 256-bit store is lowered in the same stub. Otherwise the preceding one, unless it is a call, a jump, a return or a trap. Register calls, LOOP, JRCXZ and branches with a prefix that changes them do not move. When neither neighbour can move, the store becomes a short jump to an island: a run of movable instructions within reach is moved to its own stub, and the bytes it frees hold the jump to the store's stub. A store with neither fails the conversion. The converter tests compare the halves with clang's encodings for base, SIB, disp8 overflowing to disp32, disp32, RSP-relative, FS-absolute, r13-based and RIP-relative operands, and check that aligned stores, loads, register forms and masked stores are not split. They check the stub of a short store followed by a return, a call (direct, with an address-size prefix, and through a RIP-relative pointer), short, near and RIP-relative jumps, short and near conditional branches and another store; that a following register call, LOOP, JRCXZ, prefixed branch or CLWB makes the store take the instruction before it; islands past instructions already moved, a store that needs its own stub, a branch target, a register call and the short-jump reach; and that a store with no room fails. PPSA12544: 55295 stores split in the executable, 278 of them in the stub of the store before them; every module converts with none kept. AI-assisted: yes (cherry picked from commit c763a78b6778ffcb83b5ea76d43ef634c1826103) --- core/relinker/CMakeLists.txt | 6 + .../x86/Amd64OnlySubstitutionTable.hpp | 1 + .../x86/Amd64OnlySubstitutionTypes.hpp | 1 + .../x86/IAmd64OnlyInstructionMatcher.hpp | 6 + .../include/codegen/x86/StubBodyBuilder.hpp | 10 +- .../include/codegen/x86/WideStoreLowering.hpp | 16 ++ .../include/codegen/x86/WideStoreOperands.hpp | 26 +++ .../codegen/src/Amd64OnlyConverter.cpp | 199 ++++++++++++++++ .../src/x86/Amd64OnlyInstructionMatcher.cpp | 98 +++++--- .../codegen/src/x86/StubBodyBuilder.cpp | 76 +++++- .../codegen/src/x86/WideStoreLowering.cpp | 21 ++ .../codegen/src/x86/WideStoreOperands.cpp | 133 +++++++++++ .../codegen/tests/Amd64OnlyConverterTests.cpp | 220 +++++++++++++++++- core/relinker/main.cpp | 3 +- 14 files changed, 774 insertions(+), 42 deletions(-) create mode 100644 core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp create mode 100644 core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp create mode 100644 core/relinker/codegen/src/x86/WideStoreLowering.cpp create mode 100644 core/relinker/codegen/src/x86/WideStoreOperands.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 5d3eb5f3fce..fc9e4bcf5f8 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -53,6 +53,8 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp @@ -141,6 +143,8 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ) target_include_directories(amd64_only_windows_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libc/src/specifics/windows) if(BUILD_TESTING) @@ -297,6 +301,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SegmentFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/EntryStubBuilder.cpp diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index 67e81c11a7d..7c94063bc7b 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -42,6 +42,7 @@ inline constexpr Entry kRdseed = {"RDSEED", nullptr, 0}; inline constexpr Entry kRdpid = {"RDPID", nullptr, 0}; inline constexpr Entry kClwb = {"CLWB", nullptr, 0}; inline constexpr Entry kCpuid = {"CPUID", nullptr, 0}; +inline constexpr Entry kWideStore = {"256-bit store", nullptr, 0}; inline constexpr Entry kExtrq = {"EXTRQ", nullptr, 0}; inline constexpr Entry kInsertq = {"INSERTQ", nullptr, 0}; inline constexpr Entry kExtrqRegisterForm = {"EXTRQ register form", nullptr, 0}; diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp index bcb5df81629..e2f59464242 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp @@ -35,6 +35,7 @@ struct Amd64OnlyMatch { std::size_t ReturnBranchOffset; bool Optional = false; std::vector Relocations = {}; + bool RelocatableWindow = false; }; struct Amd64OnlySubstitutionReport { diff --git a/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp b/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp index df17eb1753e..9986e79286a 100644 --- a/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp +++ b/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp @@ -2,6 +2,7 @@ #define CODEGEN_X86_IAMD64ONLYINSTRUCTIONMATCHER_HPP #include +#include #include #include #include @@ -23,6 +24,11 @@ class IAmd64OnlyInstructionMatcher { std::span> instructions, std::span trailing ) const = 0; + + virtual void EmitOutOfLine( + StubBodyBuilder& body, + std::span instruction + ) const = 0; }; std::unique_ptr MakeAmd64OnlyInstructionMatcher(Amd64OnlyTarget target = Amd64OnlyTarget::Intel); diff --git a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp index 212eeea546c..f2ff3c3652c 100644 --- a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp +++ b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp @@ -1,6 +1,7 @@ #ifndef CODEGEN_X86_STUBBODYBUILDER_HPP #define CODEGEN_X86_STUBBODYBUILDER_HPP +#include #include #include #include @@ -53,6 +54,10 @@ class StubBodyBuilder { void Restore(std::uint8_t reg); void Raw(std::span bytes); void Move(std::span instruction, std::optional ripDisplacementOffset); + void RipRelative(std::span instruction, std::size_t displacementOffset, std::int64_t target); + void MoveControlFlow(std::span instruction, const DecodedInstructionInfo& info); + void JumpTo(std::int64_t siteTarget); + void Seek(std::int64_t siteOffset); void Advance(std::size_t length); [[nodiscard]] LoweredBody Finish(); @@ -63,12 +68,15 @@ class StubBodyBuilder { std::size_t ConstantIndex; }; + void _rel32(std::initializer_list opcode, std::int64_t siteTarget); + void _pushReturnAddress(std::int64_t siteTarget); + std::vector _bytes; std::vector _fixups; std::vector _constants; std::vector _relocations; std::size_t _stackDepth = 0; - std::size_t _siteOffset = 0; + std::int64_t _siteOffset = 0; }; } diff --git a/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp b/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp new file mode 100644 index 00000000000..7e27e279669 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp @@ -0,0 +1,16 @@ +#ifndef CODEGEN_X86_WIDESTORELOWERING_HPP +#define CODEGEN_X86_WIDESTORELOWERING_HPP + +#include +#include + +namespace Codegen { + +class WideStoreLowering { +public: + void EmitOutOfLine(StubBodyBuilder& body, const WideStoreOperands& operands) const; +}; + +} + +#endif diff --git a/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp b/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp new file mode 100644 index 00000000000..1716af37be5 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp @@ -0,0 +1,26 @@ +#ifndef CODEGEN_X86_WIDESTOREOPERANDS_HPP +#define CODEGEN_X86_WIDESTOREOPERANDS_HPP + +#include +#include +#include +#include + +namespace Codegen { + +struct WideStoreHalf { + std::vector Bytes; + std::optional RipDisplacementOffset; + std::int64_t RipTarget = 0; +}; + +struct WideStoreOperands { + WideStoreHalf Low; + WideStoreHalf High; +}; + +[[nodiscard]] std::optional DecodeVexWideStore(const std::uint8_t* data, std::size_t length); + +} + +#endif diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index 20163046468..2f57bd9fd60 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -3,9 +3,13 @@ #include #include #include +#include #include +#include #include +#include #include +#include #include #include #include @@ -24,6 +28,181 @@ auto _atFileOffset(const Domain::FileByteOffset base, const TOperation& operatio } } +constexpr std::int64_t kShortJumpSize = 2; +constexpr std::uint64_t kIslandSearchBytes = 0x100; + +struct Placement { + std::vector Sites; + std::vector Claimed; + std::vector Consumed; + std::size_t ReplacementLength = 0; +}; + +class WindowPlacer { +public: + WindowPlacer( + std::vector& seg, + const std::vector& matches, + const Domain::ProgramHeader& ph, + const std::set& branchTargets, + const std::set& pending, + const std::set& claimed, + const IAmd64OnlyInstructionMatcher& matcher + ) : _seg(seg), _matches(matches), _ph(ph), _branchTargets(branchTargets), _pending(pending), _claimed(claimed), _matcher(matcher) {} + + [[nodiscard]] std::optional Window(const std::size_t index) const { + if (index + 1 < _matches.size() && (_pending.contains(index + 1) || _movable(index + 1, true))) { + if (auto placement = _relocate(index, index, index + 1)) + return placement; + } + if (index > 0 && _movable(index - 1, false)) + return _relocate(index, index - 1, index); + return std::nullopt; + } + + [[nodiscard]] std::optional Island(const std::size_t index) { + const auto store = _address(index); + for (std::size_t distance = 1;; ++distance) { + bool inReach = false; + for (const bool before : {true, false}) { + if (before ? distance > index : index + distance >= _matches.size()) + continue; + const auto start = before ? index - distance : index + distance; + if (_address(start) + kIslandSearchBytes < store || _address(start) > store + kIslandSearchBytes) + continue; + inReach = true; + if (auto placement = _islandAt(index, start)) + return placement; + } + if (!inReach) + return std::nullopt; + } + } + +private: + std::vector& _seg; + const std::vector& _matches; + const Domain::ProgramHeader& _ph; + const std::set& _branchTargets; + const std::set& _pending; + const std::set& _claimed; + const IAmd64OnlyInstructionMatcher& _matcher; + X64InstructionDecoder _decoder; + + [[nodiscard]] std::uint64_t _address(const std::size_t index) const { return _ph.MappedAddress + _matches[index].Offset; } + [[nodiscard]] std::uint64_t _end(const std::size_t index) const { return _address(index) + _matches[index].Length; } + [[nodiscard]] std::span _bytes(const std::size_t index) const { return {_seg.data() + _matches[index].Offset, _matches[index].Length}; } + [[nodiscard]] DecodedInstructionInfo _info(const std::size_t index) const { return _decoder.DecodeInstruction(_seg.data() + _matches[index].Offset, _matches[index].Length); } + + [[nodiscard]] bool _entered(const std::uint64_t start, const std::uint64_t end) const { + const auto hit = _branchTargets.upper_bound(start); + return hit != _branchTargets.end() && *hit < end; + } + + [[nodiscard]] bool _blocksFollowing(const std::size_t index) const { + const auto kind = _info(index).FlowKind; + return kind != ControlFlowKind::Sequential && kind != ControlFlowKind::ConditionalBranch; + } + + [[nodiscard]] bool _movable(const std::size_t index, const bool last) const { + return !_claimed.contains(index) && !_pending.contains(index) && (last || !_blocksFollowing(index)); + } + + [[nodiscard]] TrampolineSite _site(const std::uint64_t start, const std::size_t length, LoweredBody body) const { + const auto offset = static_cast(start - _ph.MappedAddress); + return { + static_cast(_ph.Offset + offset), + static_cast(start), + length, + std::vector(_seg.begin() + static_cast(offset), _seg.begin() + static_cast(offset + length)), + std::move(body.Bytes), + body.ReturnBranchOffset, + std::move(body.Relocations) + }; + } + + [[nodiscard]] std::optional _relocate(const std::size_t index, const std::size_t first, const std::size_t last) const { + if (_entered(_address(first), _end(last))) + return std::nullopt; + StubBodyBuilder body; + Placement placement; + try { + for (auto k = first; k <= last; ++k) { + if (_pending.contains(k)) { + _matcher.EmitOutOfLine(body, _bytes(k)); + if (k != index) + placement.Consumed.push_back(k); + } else { + body.MoveControlFlow(_bytes(k), _info(k)); + } + placement.Claimed.push_back(k); + } + } catch (const CodegenException&) { + return std::nullopt; + } + placement.Sites.push_back(_site(_address(first), static_cast(_end(last) - _address(first)), body.Finish())); + placement.ReplacementLength = placement.Sites.back().Body.size(); + return placement; + } + + [[nodiscard]] std::optional _islandAt(const std::size_t index, const std::size_t start) { + using Amd64OnlySubstitutionTable::kJmpRel32; + std::size_t head = 0; + for (auto last = start; last < _matches.size(); ++last) { + if (!_movable(last, true) || _entered(_address(start), _end(last))) + return std::nullopt; + if (head == 0 && _end(last) - _address(start) >= kJmpRel32.Size) + head = static_cast(_end(last) - _address(start)); + const auto island = _address(start) + head; + const auto reach = static_cast(island) - static_cast(_address(index) + kShortJumpSize); + if (head != 0 && (reach < std::numeric_limits::min() || reach > std::numeric_limits::max())) + return std::nullopt; + if (head != 0 && _end(last) >= island + kJmpRel32.Size) { + try { + return _buildIsland(index, start, last, head); + } catch (const CodegenException&) { + return std::nullopt; + } + } + if (_blocksFollowing(last)) + return std::nullopt; + } + return std::nullopt; + } + + [[nodiscard]] Placement _buildIsland(const std::size_t index, const std::size_t start, const std::size_t last, const std::size_t head) { + using Amd64OnlySubstitutionTable::kJmpRel32; + const auto windowStart = _address(start); + const auto windowEnd = _end(last); + const auto island = windowStart + head; + const auto store = _address(index); + Placement placement; + + StubBodyBuilder moved; + for (auto k = start; k <= last; ++k) { + moved.MoveControlFlow(_bytes(k), _info(k)); + placement.Claimed.push_back(k); + } + if (!_blocksFollowing(last)) + moved.JumpTo(static_cast(windowEnd - windowStart)); + placement.Sites.push_back(_site(windowStart, head, moved.Finish())); + + StubBodyBuilder lowered; + lowered.Seek(static_cast(store) - static_cast(island)); + _matcher.EmitOutOfLine(lowered, _bytes(index)); + lowered.JumpTo(static_cast(_end(index)) - static_cast(island)); + placement.Sites.push_back(_site(island, kJmpRel32.Size, lowered.Finish())); + placement.ReplacementLength = placement.Sites.back().Body.size(); + placement.Claimed.push_back(index); + + const auto offset = _matches[index].Offset; + std::fill_n(_seg.begin() + static_cast(offset), _matches[index].Length, Amd64OnlySubstitutionTable::kTrapFill); + _seg[offset] = X64OpcodeConstants::OneByteJmpRel8; + _seg[offset + 1] = static_cast(static_cast(static_cast(island) - static_cast(store + kShortJumpSize))); + return placement; + } +}; + class Amd64OnlyConverter : public IAmd64OnlyConverter { public: explicit Amd64OnlyConverter(const Amd64OnlyTarget target) : _matcher(MakeAmd64OnlyInstructionMatcher(target)) {} @@ -106,6 +285,10 @@ void Amd64OnlyConverter::_convertSegment( }); const auto branchTargets = needsBranchTargets ? _collectBranchTargets(seg, matches, ph) : std::set{}; + std::set pendingIndices; + for (const auto& item : pending) + pendingIndices.insert(item.Index); + std::set claimed; std::set consumed; for (const auto& item : pending) { if (consumed.contains(item.Index)) @@ -126,6 +309,20 @@ void Amd64OnlyConverter::_convertSegment( break; } case Amd64OnlyLowering::Trampoline: { + if (substitution.RelocatableWindow && match.Length < Amd64OnlySubstitutionTable::kJmpRel32.Size) { + WindowPlacer placer(seg, matches, ph, branchTargets, pendingIndices, claimed, *_matcher); + auto placement = placer.Window(item.Index); + if (!placement) + placement = placer.Island(item.Index); + if (!placement) + throw CodegenException("No room for a jump to the stub of " + substitution.InstructionName + " within its neighbours or a nearby island", fileOffset); + for (auto& site : placement->Sites) + result.Trampolines.push_back(std::move(site)); + claimed.insert(placement->Claimed.begin(), placement->Claimed.end()); + consumed.insert(placement->Consumed.begin(), placement->Consumed.end()); + replacementLength = placement->ReplacementLength; + break; + } std::vector taken; try { std::size_t siteLength = match.Length; @@ -172,6 +369,8 @@ void Amd64OnlyConverter::_convertSegment( }); replacementLength = stub.StubBody.size(); consumed.insert(taken.begin(), taken.end()); + for (auto k = item.Index; k < matches.size() && matches[k].Offset < match.Offset + siteLength; ++k) + claimed.insert(k); } catch (const CodegenException&) { if (!substitution.Optional) throw; diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 3c549f7b458..e0306bc068d 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -73,8 +75,8 @@ const Entry& _sha1Entry(const Sha1Operands& operands) { return kSha1rnds4; } -Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false) { - return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations)}; +Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false, const bool window = false) { + return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations), window}; } void _moveTrailing(StubBodyBuilder& body, std::span trailing) { @@ -143,6 +145,11 @@ class Amd64OnlyInstructionMatcher : public IAmd64OnlyInstructionMatcher { std::span trailing ) const override; + void EmitOutOfLine( + StubBodyBuilder& body, + std::span instruction + ) const override; + private: Amd64OnlyTarget _target; Sse4aLowering _lowering; @@ -150,6 +157,7 @@ class Amd64OnlyInstructionMatcher : public IAmd64OnlyInstructionMatcher { Sha1Lowering _sha1Lowering; ClzeroLowering _clzeroLowering; ReciprocalLowering _reciprocalLowering; + WideStoreLowering _wideStoreLowering; [[nodiscard]] Amd64OnlyMatch _matchMovnts(const DecodedInstruction& instr, const Entry& entry) const; [[nodiscard]] Amd64OnlyMatch _matchSse4a(const DecodedInstruction& instr, const Entry& entry, const Entry& registerFormEntry, std::span trailing) const; @@ -157,6 +165,7 @@ class Amd64OnlyInstructionMatcher : public IAmd64OnlyInstructionMatcher { [[nodiscard]] Amd64OnlyMatch _matchSha1(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] Amd64OnlyMatch _matchClzero(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] std::optional _matchRosetta(const DecodedInstruction& instr, std::span trailing) const; + [[nodiscard]] const char* _emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const; }; Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { @@ -211,9 +220,56 @@ std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const D return _trampoline(kRdpid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdpidReplacement(instr)); }), true); if (instr.IsCpuid()) return _trampoline(kCpuid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(RosettaCpuid::StubBody()); }), true); + if (const auto wide = DecodeVexWideStore(instr.Data, instr.Length)) + return _trampoline(kWideStore.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _wideStoreLowering.EmitOutOfLine(body, *wide); }), false, true); return std::nullopt; } +const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const { + if (instr.IsExtrq() || instr.IsInsertq()) { + const auto operands = DecodeSse4a(instr.Data, instr.Length); + _lowering.EmitOutOfLine(body, operands); + return _sse4aEntry(operands).Name; + } + if (instr.IsSha256()) { + const auto operands = DecodeSha256(instr.Data, instr.Length); + _sha256Lowering.EmitOutOfLine(body, operands); + return _sha256Entry(operands).Name; + } + if (instr.IsSha1()) { + const auto operands = DecodeSha1(instr.Data, instr.Length); + _sha1Lowering.EmitOutOfLine(body, operands); + return _sha1Entry(operands).Name; + } + if (instr.IsClzero()) { + _clzeroLowering.EmitOutOfLine(body, DecodeClzero(instr.Data, instr.Length)); + return kClzero.Name; + } + if (const auto reciprocal = DecodeReciprocal(instr.Data, instr.Length)) { + _reciprocalLowering.EmitOutOfLine(body, *reciprocal); + return _reciprocalEntry(*reciprocal).Name; + } + if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { + body.Raw(_rdpidReplacement(instr)); + return kRdpid.Name; + } + if (_target == Amd64OnlyTarget::Rosetta && instr.IsCpuid()) { + body.Raw(RosettaCpuid::StubBody()); + return kCpuid.Name; + } + if (const auto wide = _target == Amd64OnlyTarget::Rosetta ? DecodeVexWideStore(instr.Data, instr.Length) : std::nullopt) { + _wideStoreLowering.EmitOutOfLine(body, *wide); + return kWideStore.Name; + } + return nullptr; +} + +void Amd64OnlyInstructionMatcher::EmitOutOfLine(StubBodyBuilder& body, const std::span instruction) const { + if (_emitOutOfLine(body, DecodedInstruction{instruction.data(), instruction.size()}) == nullptr) + throw CodegenException("AMD-only instruction has no out-of-line lowering"); + body.Advance(instruction.size()); +} + std::optional Amd64OnlyInstructionMatcher::MatchSequence( std::span> instructions, std::span trailing @@ -224,41 +280,11 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( const char* name = nullptr; for (const auto& instruction : instructions) { const DecodedInstruction instr{instruction.data(), instruction.size()}; - if (instr.IsExtrq() || instr.IsInsertq()) { - const auto operands = DecodeSse4a(instr.Data, instr.Length); - if (name == nullptr) - name = _sse4aEntry(operands).Name; - _lowering.EmitOutOfLine(body, operands); - } else if (instr.IsSha256()) { - const auto operands = DecodeSha256(instr.Data, instr.Length); - if (name == nullptr) - name = _sha256Entry(operands).Name; - _sha256Lowering.EmitOutOfLine(body, operands); - } else if (instr.IsSha1()) { - const auto operands = DecodeSha1(instr.Data, instr.Length); - if (name == nullptr) - name = _sha1Entry(operands).Name; - _sha1Lowering.EmitOutOfLine(body, operands); - } else if (instr.IsClzero()) { - const auto operands = DecodeClzero(instr.Data, instr.Length); - if (name == nullptr) - name = kClzero.Name; - _clzeroLowering.EmitOutOfLine(body, operands); - } else if (const auto reciprocal = DecodeReciprocal(instr.Data, instr.Length)) { - if (name == nullptr) - name = _reciprocalEntry(*reciprocal).Name; - _reciprocalLowering.EmitOutOfLine(body, *reciprocal); - } else if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { - if (name == nullptr) - name = kRdpid.Name; - body.Raw(_rdpidReplacement(instr)); - } else if (_target == Amd64OnlyTarget::Rosetta && instr.IsCpuid()) { - if (name == nullptr) - name = kCpuid.Name; - body.Raw(RosettaCpuid::StubBody()); - } else { + const auto emitted = _emitOutOfLine(body, instr); + if (emitted == nullptr) return std::nullopt; - } + if (name == nullptr) + name = emitted; body.Advance(instr.Length); } _moveTrailing(body, trailing); diff --git a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp index a1b6e44b617..e68805215ff 100644 --- a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp +++ b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace Codegen { @@ -150,7 +151,7 @@ void StubBodyBuilder::Load(const std::uint8_t reg, const MemoryOperand& operand) if (operand.Rm == ModRmRmSibPresent) _bytes.push_back(operand.Sib); if (operand.RipRelative) { - _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, static_cast(_siteOffset + operand.InstructionLength) + operand.Displacement}); + _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, _siteOffset + static_cast(operand.InstructionLength) + operand.Displacement}); _bytes.insert(_bytes.end(), Disp32Size, 0); return; } @@ -189,13 +190,82 @@ void StubBodyBuilder::Move(const std::span instruction, cons value |= static_cast(instruction[*ripDisplacementOffset + index]) << (index * 8); _bytes[start + *ripDisplacementOffset + index] = 0; } - _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), static_cast(_siteOffset + instruction.size()) + static_cast(value)}); + _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), _siteOffset + static_cast(instruction.size()) + static_cast(value)}); } Advance(instruction.size()); } +void StubBodyBuilder::RipRelative(const std::span instruction, const std::size_t displacementOffset, const std::int64_t target) { + using namespace X64OpcodeConstants; + if (displacementOffset > instruction.size() || instruction.size() - displacementOffset < Disp32Size) + throw CodegenException("RIP-relative displacement lies outside its instruction"); + const auto start = _bytes.size(); + Raw(instruction); + std::fill_n(_bytes.begin() + static_cast(start + displacementOffset), Disp32Size, std::uint8_t{0}); + _relocations.push_back({start + displacementOffset, start + instruction.size(), _siteOffset + target}); +} + +void StubBodyBuilder::MoveControlFlow(const std::span instruction, const DecodedInstructionInfo& info) { + using namespace X64OpcodeConstants; + 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.FlowKind == ControlFlowKind::Call && info.HasRipRelativeDisp) { + _pushReturnAddress(end); + std::vector jump(instruction.begin(), instruction.end()); + auto& modrm = jump[info.RipRelativeDispOffset - 1]; + modrm = static_cast((modrm & ~(ModRmRegMask << ModRmRegShift)) | (4u << ModRmRegShift)); + Move(jump, info.RipRelativeDispOffset); + return; + } + if (!info.HasBranchTarget || info.HasRipRelativeDisp) { + Move(instruction, info.HasRipRelativeDisp ? std::optional(info.RipRelativeDispOffset) : std::nullopt); + return; + } + for (std::size_t index = 0; index < info.OpcodeOffset; ++index) { + const auto prefix = instruction[index]; + if (prefix != PrefixSegCs && prefix != PrefixSegDs && prefix != PrefixRepne && prefix != PrefixAddressSize && (prefix & 0xF0) != kRexBase) + throw CodegenException("A relative branch with this prefix cannot move into a stub"); + } + const auto target = end + info.BranchDisp; + if (info.IsTwoByteOpcode) { + _rel32({TwoByteOpcodeEscape, info.Opcode}, target); + } else if (info.Opcode == OneByteCallRel32) { + _pushReturnAddress(end); + _rel32({OneByteJmpRel32}, target); + } else if (info.Opcode == OneByteJmpRel32 || info.Opcode == OneByteJmpRel8) { + _rel32({OneByteJmpRel32}, target); + } else if (info.Opcode >= OneByteJccRel8Min && info.Opcode <= OneByteJccRel8Max) { + _rel32({TwoByteOpcodeEscape, static_cast(TwoByteJccRel32Min | (info.Opcode & 0x0F))}, target); + } else { + throw CodegenException("A LOOP or JRCXZ branch cannot move into a stub"); + } + Advance(instruction.size()); +} + +void StubBodyBuilder::JumpTo(const std::int64_t siteTarget) { + _rel32({X64OpcodeConstants::OneByteJmpRel32}, siteTarget); +} + +void StubBodyBuilder::Seek(const std::int64_t siteOffset) { + _siteOffset = siteOffset; +} + void StubBodyBuilder::Advance(const std::size_t length) { - _siteOffset += length; + _siteOffset += static_cast(length); +} + +void StubBodyBuilder::_rel32(const std::initializer_list opcode, const std::int64_t siteTarget) { + _bytes.insert(_bytes.end(), opcode); + const auto displacement = _bytes.size(); + _bytes.insert(_bytes.end(), X64OpcodeConstants::Disp32Size, std::uint8_t{0}); + _relocations.push_back({displacement, _bytes.size(), siteTarget}); +} + +void StubBodyBuilder::_pushReturnAddress(const std::int64_t siteTarget) { + _bytes.insert(_bytes.end(), {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50}); + _rel32({0x48, 0x8D, 0x05}, siteTarget); + _bytes.insert(_bytes.end(), {0x48, 0x89, 0x44, 0x24, 0x08, 0x58}); } LoweredBody StubBodyBuilder::Finish() { diff --git a/core/relinker/codegen/src/x86/WideStoreLowering.cpp b/core/relinker/codegen/src/x86/WideStoreLowering.cpp new file mode 100644 index 00000000000..40e22c9a376 --- /dev/null +++ b/core/relinker/codegen/src/x86/WideStoreLowering.cpp @@ -0,0 +1,21 @@ +#include + +namespace Codegen { + +namespace { + +void _emitHalf(StubBodyBuilder& body, const WideStoreHalf& half) { + if (half.RipDisplacementOffset) + body.RipRelative(half.Bytes, *half.RipDisplacementOffset, half.RipTarget); + else + body.Raw(half.Bytes); +} + +} + +void WideStoreLowering::EmitOutOfLine(StubBodyBuilder& body, const WideStoreOperands& operands) const { + _emitHalf(body, operands.Low); + _emitHalf(body, operands.High); +} + +} diff --git a/core/relinker/codegen/src/x86/WideStoreOperands.cpp b/core/relinker/codegen/src/x86/WideStoreOperands.cpp new file mode 100644 index 00000000000..478c4108edb --- /dev/null +++ b/core/relinker/codegen/src/x86/WideStoreOperands.cpp @@ -0,0 +1,133 @@ +#include +#include +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::uint8_t kVexMap0F = 0x01; +constexpr std::uint8_t kVexLength256 = 0x04; +constexpr std::uint8_t kVexUnusedRegister = 0x0F; +constexpr std::uint8_t kPpNone = 0x00; +constexpr std::uint8_t kPp66 = 0x01; +constexpr std::uint8_t kPpF3 = 0x02; +constexpr std::uint8_t kVextractf128 = 0x19; +constexpr std::uint8_t kVextractf128Payload = 0x7D; +constexpr std::uint8_t kUpperHalf = 0x01; +constexpr std::int64_t kHalfBytes = 16; + +bool _unalignedStore(const std::uint8_t pp, const std::uint8_t opcode) { + return ((pp == kPpNone || pp == kPp66) && opcode == 0x11) || (pp == kPpF3 && opcode == 0x7F); +} + +bool _legacyPrefixAllowed(const std::uint8_t prefix) { + using namespace X64OpcodeConstants; + return prefix == PrefixSegCs || prefix == PrefixSegSs || prefix == PrefixSegDs || prefix == PrefixSegEs || prefix == PrefixSegFs || prefix == PrefixSegGs || prefix == PrefixAddressSize; +} + +void _appendU32(std::vector& bytes, const std::uint32_t value) { + for (std::size_t index = 0; index < X64OpcodeConstants::Disp32Size; ++index) + bytes.push_back(static_cast(value >> (index * 8))); +} + +} + +std::optional DecodeVexWideStore(const std::uint8_t* data, const std::size_t length) { + using namespace X64OpcodeConstants; + std::size_t pos = 0; + while (pos < length && _legacyPrefixAllowed(data[pos])) + ++pos; + const auto vex = pos; + if (vex + 2 > length) + return std::nullopt; + std::uint8_t inverted = 0; + std::uint8_t payload = 0; + if (data[vex] == OneByteVex2) { + inverted = static_cast((data[vex + 1] & 0x80) | 0x60); + payload = data[vex + 1]; + pos = vex + Vex2PrefixLength; + } else if (data[vex] == OneByteVex3) { + if (vex + Vex3PrefixLength > length || (data[vex + 1] & Vex3MapMask) != kVexMap0F) + return std::nullopt; + inverted = static_cast(data[vex + 1] & 0xE0); + payload = data[vex + 2]; + pos = vex + Vex3PrefixLength; + } else { + return std::nullopt; + } + if ((payload & kVexLength256) == 0 || ((payload >> 3) & 0x0F) != kVexUnusedRegister) + return std::nullopt; + if (pos + 2 > length || !_unalignedStore(static_cast(payload & 0x03), data[pos])) + return std::nullopt; + const auto modRmOffset = pos + 1; + const auto modrm = data[modRmOffset]; + const auto mod = static_cast((modrm >> ModRmModShift) & ModRmModMask); + const auto rm = static_cast(modrm & ModRmRmMask); + if (mod == ModRmModRegister) + return std::nullopt; + auto cursor = modRmOffset + 1; + std::optional sib; + if (rm == ModRmRmSibPresent) { + if (cursor >= length) + return std::nullopt; + sib = data[cursor++]; + } + const bool ripRelative = mod == ModRmModIndirect && rm == ModRmRmRipRelative; + const bool absolute = mod == ModRmModIndirect && sib.has_value() && (*sib & SibBaseMask) == SibBaseDisp32; + const std::size_t displacementSize = mod == ModRmModDisp8 ? Disp8Size : (mod == ModRmModDisp32 || ripRelative || absolute) ? Disp32Size : 0; + if (cursor + displacementSize != length) + return std::nullopt; + std::int64_t displacement = 0; + if (displacementSize == Disp8Size) { + displacement = static_cast(data[cursor]); + } else if (displacementSize == Disp32Size) { + std::uint32_t value = 0; + for (std::size_t index = 0; index < Disp32Size; ++index) + value |= static_cast(data[cursor + index]) << (index * 8); + displacement = static_cast(value); + } + + WideStoreOperands operands; + operands.Low.Bytes.assign(data, data + length); + operands.Low.Bytes[data[vex] == OneByteVex2 ? vex + 1 : vex + 2] &= static_cast(~kVexLength256); + if (ripRelative) { + operands.Low.RipDisplacementOffset = cursor; + operands.Low.RipTarget = static_cast(length) + displacement; + } + + auto& high = operands.High.Bytes; + high.assign(data, data + vex); + high.insert(high.end(), {OneByteVex3, static_cast(inverted | Vex3Map0F3A), kVextractf128Payload, kVextractf128}); + const auto reg = static_cast(modrm & (ModRmRegMask << ModRmRegShift)); + std::int64_t highDisplacement = displacement + kHalfBytes; + std::uint8_t highMod = mod; + std::size_t highDisplacementSize = displacementSize; + if (!ripRelative && !absolute) { + if (mod == ModRmModIndirect || (mod == ModRmModDisp8 && highDisplacement > std::numeric_limits::max())) { + highMod = highDisplacement > std::numeric_limits::max() ? ModRmModDisp32 : ModRmModDisp8; + highDisplacementSize = highMod == ModRmModDisp8 ? Disp8Size : Disp32Size; + } + } + if (!ripRelative && highDisplacement > std::numeric_limits::max()) + throw CodegenException("256-bit store displacement leaves no room for its upper half"); + high.push_back(static_cast((highMod << ModRmModShift) | reg | rm)); + if (sib) + high.push_back(*sib); + const auto highDisplacementOffset = high.size(); + if (highDisplacementSize == Disp8Size) + high.push_back(static_cast(static_cast(highDisplacement))); + else if (ripRelative) + _appendU32(high, 0); + else if (highDisplacementSize == Disp32Size) + _appendU32(high, static_cast(static_cast(highDisplacement))); + high.push_back(kUpperHalf); + if (ripRelative) { + operands.High.RipDisplacementOffset = highDisplacementOffset; + operands.High.RipTarget = static_cast(length) + displacement + kHalfBytes; + } + return operands; +} + +} diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 719788893ef..16e891044ab 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -670,6 +671,221 @@ void rosettaSubstitutions() { require(rdpid->ReturnBranchOffset == 6 && Bytes(rdpid->StubBody.begin(), rdpid->StubBody.begin() + 7) == Bytes{0x41, 0xBB, 0, 0, 0, 0, 0xE9}, "RDPID r11 stub does not load 0 before returning"); } +void wideStoreSplit() { + struct Case { + Bytes Original; + Bytes Low; + Bytes High; + const char* Name; + }; + const std::vector cases = { + {{0xC5, 0xFC, 0x11, 0x07}, {0xC5, 0xF8, 0x11, 0x07}, {0xC4, 0xE3, 0x7D, 0x19, 0x47, 0x10, 0x01}, "vmovups (%rdi)"}, + {{0xC4, 0x41, 0x7E, 0x7F, 0x4C, 0x87, 0x70}, {0xC4, 0x41, 0x7A, 0x7F, 0x4C, 0x87, 0x70}, {0xC4, 0x43, 0x7D, 0x19, 0x8C, 0x87, 0x80, 0x00, 0x00, 0x00, 0x01}, "vmovdqu 0x70(%r15,%rax,4)"}, + {{0xC5, 0xFC, 0x11, 0x4C, 0x24, 0xF8}, {0xC5, 0xF8, 0x11, 0x4C, 0x24, 0xF8}, {0xC4, 0xE3, 0x7D, 0x19, 0x4C, 0x24, 0x08, 0x01}, "vmovups -8(%rsp)"}, + {{0xC5, 0xFE, 0x7F, 0x93, 0x78, 0x56, 0x34, 0x12}, {0xC5, 0xFA, 0x7F, 0x93, 0x78, 0x56, 0x34, 0x12}, {0xC4, 0xE3, 0x7D, 0x19, 0x93, 0x88, 0x56, 0x34, 0x12, 0x01}, "vmovdqu 0x12345678(%rbx)"}, + {{0x64, 0xC5, 0xFC, 0x11, 0x04, 0x25, 0x40, 0x00, 0x00, 0x00}, {0x64, 0xC5, 0xF8, 0x11, 0x04, 0x25, 0x40, 0x00, 0x00, 0x00}, {0x64, 0xC4, 0xE3, 0x7D, 0x19, 0x04, 0x25, 0x50, 0x00, 0x00, 0x00, 0x01}, "vmovups %fs:0x40"}, + {{0xC4, 0x41, 0x7C, 0x11, 0x65, 0x00}, {0xC4, 0x41, 0x78, 0x11, 0x65, 0x00}, {0xC4, 0x43, 0x7D, 0x19, 0x65, 0x10, 0x01}, "vmovups (%r13)"}, + {{0xC4, 0xC1, 0x7C, 0x11, 0x47, 0x68}, {0xC4, 0xC1, 0x78, 0x11, 0x47, 0x68}, {0xC4, 0xC3, 0x7D, 0x19, 0x47, 0x78, 0x01}, "vmovups 0x68(%r15)"}}; + for (const auto& item : cases) { + const auto split = Codegen::DecodeVexWideStore(item.Original.data(), item.Original.size()); + require(split && split->Low.Bytes == item.Low && split->High.Bytes == item.High && !split->Low.RipDisplacementOffset && !split->High.RipDisplacementOffset, (std::string("256-bit store was not split into its 128-bit halves: ") + item.Name).c_str()); + } + const Bytes rip = {0xC5, 0xFD, 0x11, 0x1D, 0x00, 0x01, 0x00, 0x00}; + const auto ripSplit = Codegen::DecodeVexWideStore(rip.data(), rip.size()); + require(ripSplit && ripSplit->Low.Bytes == Bytes{0xC5, 0xF9, 0x11, 0x1D, 0x00, 0x01, 0x00, 0x00} && ripSplit->High.Bytes == Bytes{0xC4, 0xE3, 0x7D, 0x19, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x01}, "RIP-relative 256-bit store was not split"); + require(ripSplit->Low.RipDisplacementOffset == 4 && ripSplit->Low.RipTarget == 8 + 0x100 && ripSplit->High.RipDisplacementOffset == 5 && ripSplit->High.RipTarget == 8 + 0x110, "RIP-relative halves do not address the original operand and the one 16 bytes above it"); + for (const auto& other : {Bytes{0xC5, 0xF8, 0x11, 0x07}, Bytes{0xC5, 0xFC, 0x11, 0xC1}, Bytes{0xC5, 0xFC, 0x10, 0x07}, Bytes{0xC4, 0xE2, 0x75, 0x2E, 0x07}, Bytes{0xC5, 0xF4, 0x11, 0x07}, Bytes{0xC4, 0xE3, 0x7D, 0x19, 0x07, 0x01}, Bytes{0xC5, 0xFC, 0x11, 0x04}, Bytes{0xC5, 0xFC, 0x11, 0x47}}) + require(!Codegen::DecodeVexWideStore(other.data(), other.size()).has_value(), "An instruction other than a plain 256-bit store was split"); + for (const auto& aligned : {Bytes{0xC5, 0xFC, 0x29, 0x4C, 0x24, 0xF8}, Bytes{0xC5, 0xFD, 0xE7, 0x93, 0x78, 0x56, 0x34, 0x12}, Bytes{0xC5, 0xFD, 0x7F, 0x1F}, Bytes{0xC5, 0xFC, 0x2B, 0x27}}) + require(!Codegen::DecodeVexWideStore(aligned.data(), aligned.size()).has_value(), "An aligned 256-bit store, which cannot cross a page, was split"); + const Bytes farRip = {0xC5, 0xFC, 0x11, 0x05, 0xF8, 0xFF, 0xFF, 0x7F}; + const auto farRipSplit = Codegen::DecodeVexWideStore(farRip.data(), farRip.size()); + require(farRipSplit && farRipSplit->High.RipTarget == std::int64_t{8} + 0x7FFFFFF8 + 0x10, "A RIP-relative 256-bit store near the end of its displacement range was not split"); + const Bytes farBase = {0xC5, 0xFC, 0x11, 0x80, 0xF8, 0xFF, 0xFF, 0x7F}; + requireFailure([&] { (void)Codegen::DecodeVexWideStore(farBase.data(), farBase.size()); }, "A 256-bit store whose upper half has no 32-bit displacement was kept"); + requireFailure([] { Codegen::StubBodyBuilder body; body.RipRelative(Bytes{0x90}, 0, 0); }, "A RIP-relative displacement outside its instruction was relocated"); + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes store = {0xC4, 0xC1, 0x7C, 0x11, 0x47, 0x68}; + const auto lowered = rosetta->Match(store.data(), store.size()); + require(lowered && lowered->Lowering == Codegen::Amd64OnlyLowering::Trampoline && !lowered->Optional && lowered->RelocatableWindow && lowered->InstructionName == "256-bit store", "A 256-bit store was not lowered through a required stub that may move its neighbours for Rosetta"); + Bytes expectedBody = cases.back().Low; + expectedBody.insert(expectedBody.end(), cases.back().High.begin(), cases.back().High.end()); + require(Bytes(lowered->StubBody.begin(), lowered->StubBody.begin() + static_cast(lowered->ReturnBranchOffset)) == expectedBody, "The 256-bit store stub is not the two halves"); + const std::vector> pair = {std::span(cases[0].Original), std::span(rip)}; + const auto sequence = rosetta->MatchSequence(pair, {}); + require(sequence && sequence->Relocations.size() == 2 && sequence->Relocations[0].SiteTarget == 4 + 8 + 0x100 && sequence->Relocations[1].SiteTarget == 4 + 8 + 0x110, "A RIP-relative 256-bit store after another is not relocated from its place in the site"); + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->Match(store.data(), store.size()).has_value(), "A 256-bit store was split for an Intel host"); + const std::vector> single = {std::span(store)}; + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->MatchSequence(single, {}).has_value(), "A 256-bit store was split in a sequence for an Intel host"); +} + +void converterWideStoreWindows() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes store = {0xC5, 0xFC, 0x11, 0x07}; + Bytes halves = {0xC5, 0xF8, 0x11, 0x07, 0xC4, 0xE3, 0x7D, 0x19, 0x47, 0x10, 0x01}; + const auto convert = [&](const Bytes& text) { + Bytes file(0x300 + text.size(), 0xCC); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return converter->Convert(file, {segmentHeader(text.size())}); + }; + const auto body = [](const Codegen::TrampolineSite& site) { + return Bytes(site.Body.begin(), site.Body.begin() + static_cast(site.ReturnBranchOffset)); + }; + const auto withStore = [&](Bytes before, const Bytes& after) { + before.insert(before.end(), store.begin(), store.end()); + before.insert(before.end(), after.begin(), after.end()); + return before; + }; + + const auto returned = convert(withStore({}, {0xC3})); + Bytes expected = halves; + expected.push_back(0xC3); + require(returned.Trampolines.size() == 1 && returned.KeptCount == 0 && returned.Reports.size() == 1 && returned.Reports[0].InstructionName == "256-bit store", "A 256-bit store followed by a return was not moved"); + require(returned.Trampolines[0].Offset == 0x200 && returned.Trampolines[0].Length == 5 && body(returned.Trampolines[0]) == expected, "A 256-bit store followed by a return did not end its stub with the return"); + + const auto called = convert(withStore({}, {0xE8, 0x10, 0x00, 0x00, 0x00})); + require(called.Trampolines.size() == 1 && called.Trampolines[0].Offset == 0x200 && called.Trampolines[0].Length == 9, "A 256-bit store followed by a call did not take the call"); + const auto& call = called.Trampolines[0]; + expected = halves; + expected.insert(expected.end(), {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50, 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x08, 0x58, 0xE9, 0, 0, 0, 0}); + require(body(call) == expected, "A moved call does not push its original return address and jump"); + require(call.Relocations.size() == 2 && call.Relocations[0].DisplacementOffset == halves.size() + 9 && call.Relocations[0].SiteTarget == 9 && call.Relocations[1].DisplacementOffset == halves.size() + 20 && call.Relocations[1].SiteTarget == 9 + 0x10, "A moved call returns to or calls the wrong address"); + + const auto branched = convert(withStore({}, {0x75, 0x10, 0x90})); + require(branched.Trampolines.size() == 1 && branched.Trampolines[0].Length == 6, "A 256-bit store followed by a short conditional branch did not take the branch"); + const auto& jcc = branched.Trampolines[0]; + expected = halves; + expected.insert(expected.end(), {0x0F, 0x85, 0, 0, 0, 0}); + require(body(jcc) == expected && jcc.Relocations.size() == 1 && jcc.Relocations[0].SiteTarget == 6 + 0x10, "A moved short conditional branch was not re-encoded with a 32-bit displacement to its target"); + + const auto jumped = convert(withStore({}, {0xEB, 0x10, 0x90})); + expected = halves; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(jumped.Trampolines.size() == 1 && body(jumped.Trampolines[0]) == expected && jumped.Trampolines[0].Relocations.size() == 1 && jumped.Trampolines[0].Relocations[0].SiteTarget == 6 + 0x10, "A moved short jump was not re-encoded with a 32-bit displacement to its target"); + + const auto longBranched = convert(withStore({}, {0x0F, 0x85, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), {0x0F, 0x85, 0, 0, 0, 0}); + require(longBranched.Trampolines.size() == 1 && longBranched.Trampolines[0].Length == 10 && body(longBranched.Trampolines[0]) == expected && longBranched.Trampolines[0].Relocations.size() == 1 && longBranched.Trampolines[0].Relocations[0].SiteTarget == 10 + 0x10, "A moved conditional branch with a 32-bit displacement does not reach its target"); + + const auto entered = convert(withStore({0x48, 0x83, 0xC1, 0x01}, {0xC3, 0xEB, 0xFD})); + expected = {0x48, 0x83, 0xC1, 0x01}; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(entered.Trampolines.size() == 1 && entered.Trampolines[0].Offset == 0x200 && entered.Trampolines[0].Length == 8 && body(entered.Trampolines[0]) == expected, "A 256-bit store before a branch target did not take room from the instruction before it"); + + Bytes adds; + for (int index = 0; index < 4; ++index) + adds.insert(adds.end(), {0x48, 0x83, 0xC1, 0x01}); + Bytes after = {0xC3, 0xEB, 0xFD}; + after.insert(after.end(), adds.begin(), adds.end()); + const auto island = convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, after)); + require(island.Trampolines.size() == 2 && island.KeptCount == 0, "A 256-bit store with no movable neighbours was not lowered through an island"); + const auto& moved = island.Trampolines[0]; + const auto& lowered = island.Trampolines[1]; + expected = adds; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(moved.Offset == 0x20C && moved.Length == 8 && body(moved) == expected && moved.Relocations.size() == 1 && moved.Relocations[0].SiteTarget == 16, "The instructions under the island were not moved and continued after it"); + expected = halves; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(lowered.Offset == 0x214 && lowered.Length == 5 && body(lowered) == expected && lowered.Relocations.size() == 1 && lowered.Relocations[0].SiteTarget == -11, "The island does not store both halves and return after the original store"); + require(Bytes(island.Bytes.begin() + 0x205, island.Bytes.begin() + 0x209) == Bytes{0xEB, 0x0D, 0xCC, 0xCC}, "The 256-bit store was not replaced by a short jump to its island"); + + requireFailure([&] { (void)convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, {0xC3, 0xEB, 0xFD})); }, "A 256-bit store with no room for a stub jump was kept"); + + const auto paired = convert(withStore({}, {0xC5, 0xFC, 0x11, 0x0E, 0xC3})); + expected = halves; + expected.insert(expected.end(), {0xC5, 0xF8, 0x11, 0x0E, 0xC4, 0xE3, 0x7D, 0x19, 0x4E, 0x10, 0x01}); + require(paired.Trampolines.size() == 1 && paired.Trampolines[0].Length == 8 && body(paired.Trampolines[0]) == expected && paired.Reports.size() == 1, "A 256-bit store followed by another was not lowered with it in one stub"); + + const Bytes push = {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50, 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x08, 0x58}; + const auto ripCalled = convert(withStore({}, {0xFF, 0x15, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), push.begin(), push.end()); + expected.insert(expected.end(), {0xFF, 0x25, 0, 0, 0, 0}); + const auto& ripCall = ripCalled.Trampolines[0]; + require(ripCalled.Trampolines.size() == 1 && ripCall.Length == 10 && body(ripCall) == expected, "A moved call through a RIP-relative pointer does not push its original return address and jump through the pointer"); + require(ripCall.Relocations.size() == 2 && ripCall.Relocations[0].SiteTarget == 10 && ripCall.Relocations[1].DisplacementOffset == halves.size() + 21 && ripCall.Relocations[1].SiteTarget == 10 + 0x10, "A moved call through a RIP-relative pointer returns to or reads the wrong address"); + + const auto ripJumped = convert(withStore({}, {0xFF, 0x25, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), {0xFF, 0x25, 0, 0, 0, 0}); + require(ripJumped.Trampolines.size() == 1 && body(ripJumped.Trampolines[0]) == expected && ripJumped.Trampolines[0].Relocations.size() == 1 && ripJumped.Trampolines[0].Relocations[0].SiteTarget == 10 + 0x10, "A moved jump through a RIP-relative pointer reads the wrong address"); + + const auto relaxed = convert(withStore({}, {0x67, 0xE8, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), push.begin(), push.end()); + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(relaxed.Trampolines.size() == 1 && relaxed.Trampolines[0].Length == 10 && body(relaxed.Trampolines[0]) == expected && relaxed.Trampolines[0].Relocations.size() == 2 && relaxed.Trampolines[0].Relocations[1].SiteTarget == 10 + 0x10, "A call with an address-size prefix was not moved like a call"); + + const Bytes add = {0x48, 0x83, 0xC1, 0x01}; + for (const auto& follower : {Bytes{0xFF, 0xD0}, Bytes{0xE3, 0x10}, Bytes{0x66, 0x75, 0x10}, Bytes{0x66, 0x0F, 0xAE, 0x37}}) { + const auto preceded = convert(withStore(add, follower)); + expected = add; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(!preceded.Trampolines.empty() && preceded.Trampolines[0].Offset == 0x200 && preceded.Trampolines[0].Length == 8 && body(preceded.Trampolines[0]) == expected, "A 256-bit store moved a following instruction that cannot leave its place instead of the one before it"); + } + + Bytes throwing = {0xC3, 0xEB, 0xFD}; + for (int index = 0; index < 3; ++index) + throwing.insert(throwing.end(), add.begin(), add.end()); + throwing.insert(throwing.end(), {0xFF, 0xD0}); + throwing.insert(throwing.end(), adds.begin(), adds.end()); + const auto farther = convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, throwing)); + require(farther.Trampolines.size() == 2 && farther.Trampolines[0].Offset == 0x21A && farther.Trampolines[1].Offset == 0x222, "An island that would move a register call was not passed over for the next one"); + require(Bytes(farther.Bytes.begin() + 0x205, farther.Bytes.begin() + 0x207) == Bytes{0xEB, 0x1B}, "The 256-bit store does not jump to the island after the register call"); + + Bytes reentered = {0xFF, 0xD0, 0xEB, 0xF8}; + reentered.insert(reentered.end(), adds.begin(), adds.end()); + const auto beyondStore = convert(withStore(Bytes(adds.begin(), adds.begin() + 8), reentered)); + require(beyondStore.Trampolines.size() == 2 && beyondStore.Trampolines[0].Offset == 0x210 && beyondStore.Trampolines[1].Offset == 0x218, "An island before a 256-bit store that is a branch target took the store with it"); + require(Bytes(beyondStore.Bytes.begin() + 0x208, beyondStore.Bytes.begin() + 0x20A) == Bytes{0xEB, 0x0E}, "The 256-bit store that is a branch target does not jump to the island after it"); + + const Bytes farCall = {0xE8, 0x40, 0x00, 0x00, 0x00}; + Bytes claimedFirst = {0xFF, 0xD0}; + claimedFirst.insert(claimedFirst.end(), adds.begin(), adds.end()); + const auto afterSequence = convert(withStore({0xC5, 0xF8, 0x53, 0xC1, 0x48, 0x83, 0xC1, 0x01}, claimedFirst)); + require(afterSequence.Trampolines.size() == 3 && afterSequence.Trampolines[0].Offset == 0x200 && afterSequence.Trampolines[0].Length == 8 && afterSequence.Trampolines[1].Offset == 0x20E && afterSequence.Trampolines[2].Offset == 0x216, "A 256-bit store moved an instruction already moved into the stub of the instruction before it"); + + Bytes pendingRun = {0xFF, 0xD0, 0x48, 0x83, 0xC1, 0x01, 0xC5, 0xFC, 0x11, 0x47, 0x10}; + pendingRun.insert(pendingRun.end(), adds.begin(), adds.end()); + const auto pendingIsland = convert(withStore(farCall, pendingRun)); + require(pendingIsland.Trampolines.size() == 3 && pendingIsland.Trampolines[0].Offset == 0x214 && pendingIsland.Trampolines[1].Offset == 0x21C && pendingIsland.Trampolines[2].Offset == 0x20F && pendingIsland.Trampolines[2].Length == 5, "An island moved the call before a 256-bit store or another store that still needs its own stub"); + + Bytes enteredRun = {0xFF, 0xD0}; + for (int index = 0; index < 6; ++index) + enteredRun.insert(enteredRun.end(), add.begin(), add.end()); + enteredRun.insert(enteredRun.end(), {0xEB, 0xEE}); + const auto enteredIsland = convert(withStore(farCall, enteredRun)); + require(enteredIsland.Trampolines.size() == 2 && enteredIsland.Trampolines[0].Offset == 0x213 && enteredIsland.Trampolines[1].Offset == 0x21B, "An island was placed over a branch target"); + + Bytes returning = {0xFF, 0xD0}; + for (int index = 0; index < 3; ++index) + returning.insert(returning.end(), add.begin(), add.end()); + returning.push_back(0xC3); + const auto returnIsland = convert(withStore(farCall, returning)); + expected = Bytes(adds.begin(), adds.begin() + 12); + expected.push_back(0xC3); + require(returnIsland.Trampolines.size() == 2 && returnIsland.Trampolines[0].Offset == 0x20B && body(returnIsland.Trampolines[0]) == expected, "Instructions moved for an island that end in a return jump back after it"); + + const auto afterBranch = convert(withStore({0x75, 0x10}, {0xFF, 0xD0})); + expected = {0x0F, 0x85, 0, 0, 0, 0}; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(afterBranch.Trampolines.size() == 1 && afterBranch.Trampolines[0].Offset == 0x200 && afterBranch.Trampolines[0].Length == 6 && body(afterBranch.Trampolines[0]) == expected && afterBranch.Trampolines[0].Relocations[0].SiteTarget == 2 + 0x10, "A 256-bit store after a conditional branch did not take the branch"); + + Bytes far = adds; + far.resize(8); + for (int index = 0; index < 60; ++index) + far.insert(far.end(), {0xFF, 0xD0}); + far.insert(far.end(), adds.begin(), adds.end()); + for (int index = 0; index < 70; ++index) + far.insert(far.end(), {0xFF, 0xD0}); + Bytes tail; + for (int index = 0; index < 11; ++index) + tail.insert(tail.end(), {0xFF, 0xD0}); + tail.insert(tail.end(), {0x90, 0x90}); + requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); +} + void converterRosetta() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); Bytes file(0x300, 0xCC); @@ -776,7 +992,7 @@ void converterReciprocal() { void converterStrayRex() { const auto converter = Codegen::MakeAmd64OnlyConverter(); const auto convert = [&](const Bytes& text) { - Bytes file(0x300, 0xCC); + Bytes file(0x300 + text.size(), 0xCC); std::copy(text.begin(), text.end(), file.begin() + 0x200); return std::pair{file, converter->Convert(file, {segmentHeader(text.size())})}; }; @@ -1419,6 +1635,8 @@ int main() { converterClzero(); rosettaSubstitutions(); converterRosetta(); + wideStoreSplit(); + converterWideStoreWindows(); converterReciprocal(); converterStrayRex(); rewriterStrayRex(); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 0e747853d8e..1734ede1b30 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -71,10 +71,11 @@ int Run(const int argc, char* argv[]) { sourceBytes = std::move(converted.Bytes); trampolines = std::move(converted.Trampolines); std::map stubsByName; + const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; for (const auto& report : converted.Reports) { if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; - else if (report.InstructionName == "VRSQRTPS" || report.InstructionName == "VRCPPS") + else if (counted(report.InstructionName)) ++stubsByName[report.InstructionName]; else std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; From 4ed5eb2dcd1648305c3e6bd6c915c72805ad2056 Mon Sep 17 00:00:00 2001 From: poonasor Date: Sat, 10 Oct 2026 21:50:23 -0400 Subject: [PATCH 14/28] fix(relinker): move a short guest TLS access's follower branch into its stub A short fs-prefixed TLS access is patched with a 5-byte jump by relocating the following instructions into the stub, but any branch there was rejected outright. NHL 27 hits this on every stack-protector epilogue: fs:xor rax,[rbx] immediately followed by jne __stack_chk_fail. Allow relative conditional branches and short jumps: re-encode rel8 forms as their rel32 equivalents, rebase the displacement when the stub is emitted, and keep rejecting loop/jrcxz, calls, returns, and segment-prefixed followers. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit c6f934a3d05934b9f41f7346f3fcebd07d062a48) --- core/relinker/CMakeLists.txt | 2 +- .../src/windows/WindowsTlsBuilder.cpp | 31 +++++++++++++++++-- .../tests/test_tls_function_coverage.py | 25 ++++++++++++++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fc9e4bcf5f8..c5cbca3c590 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -359,7 +359,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) - set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads rejected) + set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads moved_branch rejected) foreach(register IN ITEMS 0 1 2 3 5 6 7 8 9 10 11 12 13 14 15) list(APPEND tlsCoverageGroups displacement_r${register}) endforeach() diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index df1ce0fea79..e37c631dccb 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -23,6 +23,8 @@ struct MovedInstruction { std::uint32_t Rva; std::vector Bytes; std::optional RipDisplacement; + std::optional BranchTarget; + std::size_t BranchDisplacementOffset = 0; }; struct TlsAccess { @@ -145,9 +147,28 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (length >= header.FileSize - offset) throw Domain::RelinkerException("Short guest TLS instruction ends its segment", header.Offset + offset); const auto next = decoder.DecodeInstruction(bytes + length, header.FileSize - offset - length); - if (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0) + const bool relativeBranch = + (next.FlowKind == Codegen::ControlFlowKind::ConditionalBranch && (next.IsTwoByteOpcode ? next.Opcode >= 0x80 && next.Opcode <= 0x8f : next.Opcode >= 0x70 && next.Opcode <= 0x7f)) || + (next.FlowKind == Codegen::ControlFlowKind::UnconditionalJump && !next.HasRipRelativeDisp && !next.IsTwoByteOpcode && (next.Opcode == 0xe9 || next.Opcode == 0xeb)); + if (!relativeBranch && (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0)) throw Domain::RelinkerException("Short guest TLS instruction is followed by an instruction that cannot move", header.Offset + offset + length); - moved.push_back({CheckedRva(rva + length), std::vector(bytes + length, bytes + length + next.Length), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt}); + std::vector movedBytes(bytes + length, bytes + length + next.Length); + std::optional branchTarget; + std::size_t branchDisplacementOffset = 0; + if (relativeBranch) { + const auto target = static_cast(rva) + static_cast(length + next.Length) + next.BranchDisp; + if (target < 0 || target > std::numeric_limits::max()) + throw Domain::RelinkerException("Short guest TLS instruction is followed by a branch beyond the address space", header.Offset + offset + length); + branchTarget = static_cast(target); + if (!next.IsTwoByteOpcode && next.Opcode != 0xe9) { + movedBytes = next.Opcode == 0xeb ? std::vector{0xe9} : std::vector{0x0f, static_cast(0x80 | (next.Opcode & 0xf))}; + branchDisplacementOffset = movedBytes.size(); + movedBytes.insert(movedBytes.end(), 4, 0); + } else { + branchDisplacementOffset = next.Length - 4; + } + } + moved.push_back({CheckedRva(rva + length), std::move(movedBytes), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt, branchTarget, branchDisplacementOffset}); length += next.Length; } accesses.push_back({rva, header.Offset + offset, length, false, 0, addressRegister, 0, isAlu ? bytes[position] : std::uint8_t{0}, *address, wide, std::move(moved)}); @@ -357,6 +378,12 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co throw Domain::RelinkerException("Moved RIP-relative instruction exceeds rel32 range from its TLS stub", access.FileOffset); Io::WriteU32(bytes, *instruction.RipDisplacement, static_cast(static_cast(displacement))); } + if (instruction.BranchTarget) { + const auto displacement = static_cast(*instruction.BranchTarget) - (static_cast(code.GetRva()) + static_cast(bytes.size())); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Moved branch exceeds rel32 range from its TLS stub", access.FileOffset); + Io::WriteU32(bytes, instruction.BranchDisplacementOffset, static_cast(static_cast(displacement))); + } for (const auto byte : bytes) code.Emit({byte}); } diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 5abf3e195f9..8061ee16387 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -511,7 +511,7 @@ def add_alias(image, size, begin=0x1200): def main(): groups = ("metadata", "padding", "register_loads", "displacement_bounds", "alu_execution", - "register_address_loads", "rejected") + "register_address_loads", "moved_branch", "rejected") groups += tuple(f"displacement_r{register}" for register in range(16) if register != 4) groups += tuple(f"alu_{name}" for name in ALU_READ) parser = argparse.ArgumentParser(description="Run Windows guest TLS function coverage fixtures") @@ -699,12 +699,35 @@ def convert_padded(name, image): stub = pe_bytes_at(pe, 0x10000 + address + 5 + struct.unpack_from("= 0, (name, stub.hex()) + displacement = struct.unpack_from(" Date: Mon, 12 Oct 2026 00:00:52 -0700 Subject: [PATCH 15/28] fix(relinker): rebase each origin in guest runpath lists (cherry picked from commit 341dafb7cadb6dfc18a716f0546a5504a93f57c1) --- .../relinker/src/guest/GuestModuleBuilder.cpp | 11 +++++++---- core/relinker/relinker/tests/test_guest_runpath.py | 10 +++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 08aec4772e1..bbcecbca4f0 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -331,7 +331,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); - if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); const auto outputDirectory = std::filesystem::absolute(outputPath).parent_path().lexically_normal(); std::vector artifacts; for (const auto index : order) { @@ -370,9 +370,12 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); else { std::string guestRunPath = runPath; - if (runPath == "$ORIGIN" || runPath.starts_with("$ORIGIN/")) { - const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); - guestRunPath = "$ORIGIN/" + relativeRoot + runPath.substr(7); + const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); + for (std::size_t position = 0; (position = guestRunPath.find("$ORIGIN", position)) != std::string::npos;) { + position += 7; + if (position != guestRunPath.size() && guestRunPath[position] != '/' && guestRunPath[position] != ':') continue; + guestRunPath.insert(position, "/" + relativeRoot); + position += relativeRoot.size() + 1; } std::vector needed; for (const auto dependency : dependencies[index]) { diff --git a/core/relinker/relinker/tests/test_guest_runpath.py b/core/relinker/relinker/tests/test_guest_runpath.py index 421199eda12..754fee90a6c 100644 --- a/core/relinker/relinker/tests/test_guest_runpath.py +++ b/core/relinker/relinker/tests/test_guest_runpath.py @@ -17,7 +17,9 @@ def main(): checked = 0 for index, module_directory in enumerate(("sce_module", "prx")): for label, run_path in (("default", None), ("origin", "$ORIGIN"), - ("custom", "$ORIGIN/host libs"), ("absolute", str(work / "absolute libs"))): + ("custom", "$ORIGIN/host libs"), ("absolute", str(work / "absolute libs")), + ("list", "$ORIGIN/first:$ORIGIN/second"), + ("list-bare", "$ORIGIN:$ORIGIN/libs:$ORIGIN")): case = work / f"{index}-{label}" source = case / "input" / "input.elf" source.parent.mkdir(parents=True) @@ -41,8 +43,10 @@ def main(): assert 0 <= offset < len(strings) and end >= 0, (tags, strings) actual = strings[offset:end].decode() configured = run_path if run_path is not None else "$ORIGIN/libs" - expected = Path(configured.replace("$ORIGIN", str(output.parent))).resolve() - resolved = Path(actual.replace("$ORIGIN", str(artifact.parent))).resolve() + configured_paths = configured.split(":") if label.startswith("list") else [configured] + actual_paths = actual.split(":") if label.startswith("list") else [actual] + expected = [Path(path.replace("$ORIGIN", str(output.parent))).resolve() for path in configured_paths] + resolved = [Path(path.replace("$ORIGIN", str(artifact.parent))).resolve() for path in actual_paths] if resolved != expected or (label == "absolute" and actual != run_path): failures.append((str(relative), label, actual, str(resolved), str(expected))) checked += 1 From 9e4807a6df645575190a2212af411c551f56bbd6 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 12 Oct 2026 00:01:09 -0700 Subject: [PATCH 16/28] fix(relinker): accept trailing separators on the Windows origin path (cherry picked from commit e2c24cf038b9bb5f200a666b2b5fedf2fdf10ff9) --- core/relinker/CMakeLists.txt | 3 ++ .../src/windows/WindowsEntryStubBuilder.cpp | 2 + .../relinker/tests/test_windows_runpath.py | 42 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 core/relinker/relinker/tests/test_windows_runpath.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index c5cbca3c590..e770b1da96f 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -395,6 +395,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_gui.py $) add_test(NAME windows_working_directory COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_working_directory.py $) + add_test(NAME windows_runpath COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_runpath.py $) + set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 83281042fdf..a5ea5b7e3af 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -26,6 +26,8 @@ std::string normalizeRunPath(std::string path) { path = "."; if (path.starts_with("$ORIGIN\\")) path.erase(0, 8); + if (path.empty()) + path = "."; if (path.find('$') != std::string::npos) throw Domain::RelinkerException("Unsupported variable in Windows run path"); if (path.back() != '\\') diff --git a/core/relinker/relinker/tests/test_windows_runpath.py b/core/relinker/relinker/tests/test_windows_runpath.py new file mode 100644 index 00000000000..44db3f9bd22 --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_runpath.py @@ -0,0 +1,42 @@ +from pathlib import Path +import os +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_windows_import_modules import executable, provider + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-windows-runpath-") as directory: + work = Path(directory) + seed = work / "seed" + (seed / "prx").mkdir(parents=True) + (seed / "prx" / "a.prx").write_bytes(provider(77)) + source = seed / "input.elf" + source.write_bytes(main_fixture()) + result = subprocess.run([str(relinker), "--windows", str(source), str(seed / "output.exe")], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + library = (seed / "app0" / "prx" / "a.prx.guest.prx").read_bytes() + + for index, runpath in enumerate(("$ORIGIN", "$ORIGIN/", "$ORIGIN\\", "$ORIGIN/.", ".")): + case = work / str(index) + case.mkdir() + (case / "a.prx").write_bytes(library) + source = case / "input.elf" + source.write_bytes(executable("a.prx")) + output = case / "output.exe" + result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", "--rpath", runpath, + str(source), str(output)], capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (runpath, result.stdout, result.stderr) + if os.name == "nt": + executed = subprocess.run([str(output)], cwd=work, capture_output=True, text=True, timeout=20) + assert executed.returncode == 77, (runpath, executed.returncode, executed.stdout, executed.stderr) + print("Windows runpath tests passed") + + +if __name__ == "__main__": + main() From 6ca17adc88144329f62cfdf9b8031330496173a5 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 12 Oct 2026 00:01:25 -0700 Subject: [PATCH 17/28] fix(relinker): preserve inputs when writing Windows diagnostics (cherry picked from commit 4b6bb2f05928d8026d89459b22d3cba0a532ac6d) --- core/relinker/main.cpp | 2 + .../relinker/tests/test_input_preservation.py | 67 +++++++++++++++++++ docs/user/USAGE.md | 2 +- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 1734ede1b30..dfbbc5d779c 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -128,6 +128,8 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(desktopEntry->IconPath, "Icon"); checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); } + if (args.windowsDiagnostics) + checkOutputInputs(std::filesystem::absolute("windows-diagnostics-imports.txt"), "Windows import diagnostics"); if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 2114abbe35d..7863f55a755 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -8,6 +8,72 @@ from test_linux_desktop_entry_output import png +def check_diagnostic_inputs(relinker, work): + filename = "windows-diagnostics-imports.txt" + for bundled in (False, True): + for alias in ("direct", "hardlink", "symlink"): + for registry in (False, True): + case = work / f"diagnostics-{bundled}-{alias}-{registry}" + modules = case / "sce_module" + modules.mkdir(parents=True) + diagnostics = (modules if bundled and alias == "direct" else case) / filename + source = diagnostics if not bundled and alias == "direct" else case / "input.elf" + original = bytes(main_fixture()) + source.write_bytes(original) + module = diagnostics if bundled and alias == "direct" else modules / "sample.prx" + module_original = bytes(guest_fixture(PLAIN_SITE)) + module.write_bytes(module_original) + protected = module if bundled else source + if alias == "hardlink": + os.link(protected, diagnostics) + elif alias == "symlink": + try: + diagnostics.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Diagnostic symbolic-link case skipped: Windows symlink privilege unavailable") + continue + output = case / "output.exe" + previous_output = b"previous executable" + output.write_bytes(previous_output) + registry_path = case / "output.registry.json" + previous_registry = b"previous registry" + registry_path.write_bytes(previous_registry) + options = [] if bundled else ["--skip-sce-module"] + if registry: + options.append("--registry") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", *options, + str(source), str(output)], cwd=diagnostics.parent, + capture_output=True, text=True, timeout=20) + owner = "an input module" if bundled else "the input executable" + assert result.returncode == 2 and f"Windows import diagnostics output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) + assert str(diagnostics) in result.stderr, result.stderr + assert source.read_bytes() == original and module.read_bytes() == module_original + assert diagnostics.read_bytes() == (module_original if bundled else original) + assert output.read_bytes() == previous_output and registry_path.read_bytes() == previous_registry + assert not (case / "app0").exists() + + for enabled in (False, True): + for existing in (False, True): + case = work / f"diagnostics-valid-{enabled}-{existing}" + case.mkdir() + diagnostics = case / filename + source = case / "input.elf" if enabled else diagnostics + original = bytes(main_fixture()) + source.write_bytes(original) + if enabled and existing: + diagnostics.write_bytes(b"previous diagnostics") + output = case / "output.exe" + options = ["--windows-diagnostics"] if enabled else [] + result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", *options, + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0 and output.read_bytes().startswith(b"MZ"), (result.stdout, result.stderr) + assert source.read_bytes() == original + assert diagnostics.read_bytes() == (b"" if enabled else original) + + def check_registry_inputs(relinker, work, windows): options = ["--windows"] if windows else [] for module_registry in (False, True): @@ -112,6 +178,7 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-input-preservation-") as directory: work = Path(directory) + check_diagnostic_inputs(relinker, work) check_desktop_inputs(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa4daa4a03..0928dee45d5 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -35,7 +35,7 @@ relinker --macos source/input.elf eboot Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. -The executable output must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. An existing output file can be replaced if it is separate from those inputs. +The executable, registry, Windows import diagnostics, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. ## Options From a8dc377dcc62c2166a711a3d24d562ee88d08bee Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Mon, 12 Oct 2026 12:48:01 +0530 Subject: [PATCH 18/28] fix(relinker): write registries after the executable Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 70f03dc5e51ffaaa0e2e0c05d91a1354269e423e) --- core/relinker/main.cpp | 15 ++++++------ .../relinker/tests/test_input_preservation.py | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index dfbbc5d779c..824d8d6561e 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -131,18 +131,13 @@ int Run(const int argc, char* argv[]) { if (args.windowsDiagnostics) checkOutputInputs(std::filesystem::absolute("windows-diagnostics-imports.txt"), "Windows import diagnostics"); + std::vector registryPaths; if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); - std::vector registryPaths{ - outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json") - }; + registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); - const auto registryWriter = std::make_shared(); - fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); - for (std::size_t index = 0; index < guestArtifacts.size(); ++index) - fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); } auto byteWriter = std::make_shared(); @@ -177,6 +172,12 @@ int Run(const int argc, char* argv[]) { std::cout << "Guest module: " << Io::Utf8Path(artifact.Path) << '\n'; } fileWriter.Write(absPath, executableBytes); + if (args.writeRegistry) { + const auto registryWriter = std::make_shared(); + fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); + for (std::size_t index = 0; index < guestArtifacts.size(); ++index) + fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); + } if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\n'; std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; std::cout << "Expected runtime layout (relative to the output executable):\n" diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 7863f55a755..3ec4114b305 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -119,6 +119,29 @@ def check_registry_inputs(relinker, work, windows): assert not (case / "app0").exists() +def check_registry_after_failed_patch(relinker, work): + case = work / "registry-failed-patch" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / "sample.prx").write_bytes(guest_fixture(PLAIN_SITE)) + (case / "sce_sys").mkdir() + (case / "sce_sys" / "icon0.png").write_bytes(b"\x89PNG\r\n\x1a\n" + bytes(40)) + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / "output.exe" + previous_output = b"previous executable" + output.write_bytes(previous_output) + registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json"] + previous_registry = b"previous registry" + for registry in registries: + registry.write_bytes(previous_registry) + result = subprocess.run([str(relinker), "--windows", "--registry", str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Invalid Windows icon PNG" in result.stderr, (result.stdout, result.stderr) + assert output.read_bytes() == previous_output + assert all(registry.read_bytes() == previous_registry for registry in registries) + assert not (case / "app0").exists() + + def check_desktop_inputs(relinker, work): for desktop in (False, True): for bundled in (False, True): @@ -180,6 +203,7 @@ def main(): work = Path(directory) check_diagnostic_inputs(relinker, work) check_desktop_inputs(relinker, work) + check_registry_after_failed_patch(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] check_registry_inputs(relinker, work, windows) From 1195d5f5cf60d37ab1f457a692f5355bb5e16bbb Mon Sep 17 00:00:00 2001 From: Ori Shamir Date: Mon, 12 Oct 2026 14:53:02 +0300 Subject: [PATCH 19/28] fix(relinker): resolve relocations against null symbol entries to 0 A relocation whose dynamic symbol entry is all zero, like the null symbol, now takes the value 0 plus its addend, as the gABI defines for STN_UNDEF. The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0. The Windows and macOS outputs write the value into the slot instead of failing with "Empty import symbol name". Such references are left out of the call registry. An unnamed symbol entry that is not all zero still fails the relink. OliSe Player (PPSA01153) has seven of these entries and calls through the slots only when they are not 0 (#4437). Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 1205d03daa089a654cc1cdfda3416122d069ea98) --- core/relinker/CMakeLists.txt | 2 + .../elfpatcher/general/ElfConstants.hpp | 1 + .../src/windows/WindowsRelocationBuilder.cpp | 7 +- .../src/output/SysVDynamicSectionBuilder.cpp | 11 ++- .../src/pipeline/RelinkerPipeline.cpp | 5 ++ .../tests/test_null_symbol_relocations.py | 87 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 7 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 core/relinker/relinker/tests/test_null_symbol_relocations.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index e770b1da96f..23efb9d5bef 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -455,6 +455,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $) add_test(NAME input_file COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_file.py $) + add_test(NAME null_symbol_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_null_symbol_relocations.py $) add_test(NAME input_nonascii COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_nonascii.py $) add_test(NAME string_table_bounds COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index 615c7c7742d..40802530ee0 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -141,6 +141,7 @@ inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; inline constexpr std::uint8_t STB_GLOBAL = 1; inline constexpr std::uint8_t STB_WEAK = 2; +inline constexpr std::uint8_t STT_NOTYPE = 0; inline constexpr std::uint8_t STT_FUNC = 2; inline constexpr std::uint8_t STV_DEFAULT = 0; inline constexpr std::uint16_t SHN_UNDEF = 0; diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index b1f71e3f3de..9706bfd1b73 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -38,11 +39,15 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom if (Io::ReadU16(dynamicSection.DynSymData, symbolOffset + 6) != 0) throw Domain::RelinkerException("Defined symbols require explicit relocation support", target); const auto name = ReadString(dynamicSection.DynStrData, Io::ReadU32(dynamicSection.DynSymData, symbolOffset)); - if (name.empty()) + if (name.empty() && dynamicSection.DynSymData[symbolOffset + 4] >> 4 != STB_WEAK) throw Domain::RelinkerException("Empty import symbol name", target); if (type != 1 && addend != 0) throw Domain::RelinkerException("GLOB_DAT/JUMP_SLOT relocation has a nonzero addend", target); image.RequireWritable(target, 8); + if (name.empty()) { + image.WritePointer(target, addend); + continue; + } image.WritePointer(target, 0); result.Imports.push_back({name, rva, addend}); if (const auto module = dynamicSection.ImportModules.find(target); module != dynamicSection.ImportModules.end()) diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index d3ad5fee490..b639608662b 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -82,6 +82,11 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( return value.substr(0, hashPos); }; + auto symbolInfo = [](const NidReference& ref) -> std::uint8_t { + if (ref.Nid.empty()) return static_cast((STB_WEAK << 4) | STT_NOTYPE); + return static_cast((STB_GLOBAL << 4) | STT_FUNC); + }; + static constexpr std::size_t kRelaEntSize = 24; std::vector pltSlots(originalJmprelCount, nullptr); @@ -132,8 +137,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( const NidReference& ref = *slot; const std::uint32_t nameOff = _appendStr(result.DynStrData, stripHashSuffix(ref.Nid)); - const auto info = static_cast((STB_GLOBAL << 4) | STT_FUNC); - _appendElfSym(result.DynSymData, nameOff, info, STV_DEFAULT, 0, 0, 0); + _appendElfSym(result.DynSymData, nameOff, symbolInfo(ref), STV_DEFAULT, 0, 0, 0); const std::uint64_t relaInfo = (static_cast(symIdx) << 32) | R_X86_64_JUMP_SLOT; _appendRela(result.RelaPltData, ref.RelocationAddress, relaInfo, ref.Addend); @@ -143,8 +147,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( for (const NidReference* slotPtr : nonPltRefs) { const NidReference& ref = *slotPtr; const std::uint32_t nameOff = _appendStr(result.DynStrData, stripHashSuffix(ref.Nid)); - const auto info = static_cast((STB_GLOBAL << 4) | STT_FUNC); - _appendElfSym(result.DynSymData, nameOff, info, STV_DEFAULT, 0, 0, 0); + _appendElfSym(result.DynSymData, nameOff, symbolInfo(ref), STV_DEFAULT, 0, 0, 0); const std::uint64_t relaInfo = (static_cast(symIdx) << 32) | ref.RelocationTypeValue; _appendRela(result.RelaData, ref.RelocationAddress, relaInfo, ref.Addend); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index d1ff243f4de..64aa5b865d0 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -258,6 +258,10 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(&nameOff, raw.data() + symOff, 4); const auto name = readCStr(nameOff); + if (name.empty() && (raw.size() - symOff < symEntSize + || std::any_of(raw.begin() + static_cast(symOff), raw.begin() + static_cast(symOff + symEntSize), + [](const std::uint8_t byte) { return byte != 0; }))) + throw RelinkerException("Unnamed import symbol is not a null symbol entry", symOff); nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend}); } }; @@ -426,6 +430,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::vector entries; entries.reserve(nidRefs.size()); for (const auto& ref : nidRefs) { + if (ref.Nid.empty()) continue; CallRegistryEntry entry; entry.Nid = ref.Nid; entry.Library = ref.Library; diff --git a/core/relinker/relinker/tests/test_null_symbol_relocations.py b/core/relinker/relinker/tests/test_null_symbol_relocations.py new file mode 100644 index 00000000000..23aa8c8d413 --- /dev/null +++ b/core/relinker/relinker/tests/test_null_symbol_relocations.py @@ -0,0 +1,87 @@ +import json +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_dynamic_segment import fixture +from test_linux_dynamic_strings import dynamic_strings, file_offset +from test_linux_load_alignment import loads + + +GLOB_DAT_SLOT = 0x308 +ABSOLUTE_SLOT = 0x310 +ADDEND = 0x1234 +CHECK_SLOTS = bytes.fromhex( + "488b0501010000" "4885c0" "751a" + "488b0dfd000000" "4881f934120000" "750a" + "b82a000000c3" "cccccccc" "b807000000c3") +TARGETS = (("linux", [], b"\x7fELF"), ("windows", ["--windows"], b"MZ"), ("macos", ["--macos"], b"\xcf\xfa\xed\xfe")) + + +def image_with(symbol=b""): + image = fixture() + relocations = [(GLOB_DAT_SLOT, (1 << 32) | 6, 0), (ABSOLUTE_SLOT, (1 << 32) | 1, ADDEND)] + struct.pack_into("> 32) * 24) + imports.append((target, info & 0xffffffff, addend, strings[name:strings.index(b"\0", name)], bind_type, section, value, size)) + return sorted(imports) + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-null-symbol-") as directory: + work = Path(directory) + source = work / "null-entry.elf" + source.write_bytes(image_with()) + for target, options, magic in TARGETS: + for level in (0, 1): + output = work / f"null-entry-{target}-{level}.out" + result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", f"unused-filter={level}", + *options, str(source), str(output)], capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (target, level, result.stdout, result.stderr) + data = output.read_bytes() + assert data.startswith(magic), target + registry = json.loads(output.with_suffix(".registry.json").read_text(encoding="utf-8")) + assert registry == [], (target, level, registry) + if target == "linux": + assert linux_imports(data) == [(GLOB_DAT_SLOT, 6, 0, b"", 0x20, 0, 0, 0), + (ABSOLUTE_SLOT, 1, ADDEND, b"", 0x20, 0, 0, 0)], level + if target == "windows" and os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, timeout=20) + assert run.returncode == 42, (level, run.returncode, run.stderr) + source = work / "named-unnamed.elf" + source.write_bytes(image_with(struct.pack(" Date: Mon, 12 Oct 2026 13:06:21 +0000 Subject: [PATCH 20/28] fix(relinker): treat jump table targets as branch targets when moving instructions The AMD-only converter moves the instructions next to a short AMD-only instruction into its stub, and refuses to move one that a branch enters. It knew only direct jump and call targets, so a switch case entered through a jump table could land inside the jump written over the moved instructions. The converter now also reads relative jump tables: a RIP-relative LEA whose address lies in a loaded segment starts a table of 32-bit offsets from that address, read while each target starts an instruction of the segment being converted. To read them, Convert takes the program headers and converts their executable load segments; the relinker passes all headers, and ElfReader::ReadCodeSegments, now unused, is removed. The converter test builds a dispatch through such a table whose second case enters the instruction after a 4-byte 256-bit store, and checks that the store takes the instruction before it instead. It also checks that reading stops at an entry that does not start an instruction, that only a LEA forms a table address, that tables are read only inside loaded segments, that only executable load segments are converted, and that a segment past the end of the file fails the conversion. PPSA12544: one 256-bit store in Il2cppUserAssemblies moved the instruction after it, which is case 1 of a switch; it now goes through an island. The other relinked files are unchanged. AI-assisted: yes (cherry picked from commit 1541244b784734db508b390681072729e5c5de21) --- .../include/codegen/IAmd64OnlyConverter.hpp | 2 +- .../codegen/src/Amd64OnlyConverter.cpp | 60 +++++++++++++++---- .../codegen/tests/Amd64OnlyConverterTests.cpp | 38 ++++++++++++ core/relinker/main.cpp | 4 +- .../include/relinker/domain/IElfReader.hpp | 1 - .../include/relinker/parsing/ElfReader.hpp | 1 - .../relinker/src/guest/GuestModuleBuilder.cpp | 2 +- .../relinker/src/parsing/ElfReader.cpp | 12 ---- 8 files changed, 90 insertions(+), 30 deletions(-) diff --git a/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp b/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp index ca82119d748..e133c533f14 100644 --- a/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp +++ b/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp @@ -14,7 +14,7 @@ class IAmd64OnlyConverter { [[nodiscard]] virtual ConvertResult Convert( std::vector fileBytes, - const std::vector& codeSegments + const std::vector& segments ) const = 0; }; diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index 2f57bd9fd60..6990cf40fb1 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,9 @@ auto _atFileOffset(const Domain::FileByteOffset base, const TOperation& operatio } constexpr std::int64_t kShortJumpSize = 2; +constexpr std::uint32_t kLoadSegment = 1; +constexpr std::uint32_t kExecutableSegment = 1; +constexpr std::uint8_t kLea = 0x8D; constexpr std::uint64_t kIslandSearchBytes = 0x100; struct Placement { @@ -209,7 +213,7 @@ class Amd64OnlyConverter : public IAmd64OnlyConverter { [[nodiscard]] ConvertResult Convert( std::vector fileBytes, - const std::vector& codeSegments + const std::vector& segments ) const override; private: @@ -225,30 +229,59 @@ class Amd64OnlyConverter : public IAmd64OnlyConverter { void _convertSegment( std::vector& fileBytes, const Domain::ProgramHeader& ph, + const std::vector& segments, ConvertResult& result ) const; [[nodiscard]] static std::set _collectBranchTargets( const std::vector& seg, const std::vector& matches, - const Domain::ProgramHeader& ph + const Domain::ProgramHeader& ph, + const std::vector& fileBytes, + const std::vector& segments ); }; std::set Amd64OnlyConverter::_collectBranchTargets( const std::vector& seg, const std::vector& matches, - const Domain::ProgramHeader& ph + const Domain::ProgramHeader& ph, + const std::vector& fileBytes, + const std::vector& segments ) { + const auto startsInstruction = [&](const std::uint64_t address) { + const auto offset = address - ph.MappedAddress; + const auto match = std::lower_bound(matches.begin(), matches.end(), offset, [](const InstructionMatch& item, const std::uint64_t value) { return item.Offset < value; }); + return match != matches.end() && match->Offset == offset; + }; const X64InstructionDecoder decoder; std::set targets; for (const auto& match : matches) { const auto info = decoder.DecodeInstruction(seg.data() + match.Offset, match.Length); - if (!info.HasBranchTarget || info.HasRipRelativeDisp) - continue; - const auto target = static_cast(ph.MappedAddress + match.Offset + info.Length) + info.BranchDisp; - if (target >= 0) - targets.insert(static_cast(target)); + const auto next = static_cast(ph.MappedAddress + match.Offset + info.Length); + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = next + info.BranchDisp; + if (target >= 0) + targets.insert(static_cast(target)); + } else if (info.HasRipRelativeDisp && info.Opcode == kLea) { + std::int32_t displacement; + std::memcpy(&displacement, seg.data() + match.Offset + info.RipRelativeDispOffset, sizeof(displacement)); + const auto table = static_cast(next + displacement); + for (const auto& data : segments) { + if (data.Type != kLoadSegment || table < data.MappedAddress || table - data.MappedAddress >= data.FileSize) + continue; + if (data.Offset > fileBytes.size() || data.FileSize > fileBytes.size() - data.Offset) + throw CodegenException("Segment exceeds the file", data.Offset); + for (auto entry = table; entry - data.MappedAddress + sizeof(std::int32_t) <= data.FileSize; entry += sizeof(std::int32_t)) { + std::int32_t relative; + std::memcpy(&relative, fileBytes.data() + data.Offset + (entry - data.MappedAddress), sizeof(relative)); + const auto target = table + static_cast(static_cast(relative)); + if (!startsInstruction(target)) + break; + targets.insert(target); + } + } + } } return targets; } @@ -256,6 +289,7 @@ std::set Amd64OnlyConverter::_collectBranchTargets( void Amd64OnlyConverter::_convertSegment( std::vector& fileBytes, const Domain::ProgramHeader& ph, + const std::vector& segments, ConvertResult& result ) const { const auto segOffset = static_cast(ph.Offset); @@ -283,7 +317,7 @@ void Amd64OnlyConverter::_convertSegment( const bool needsBranchTargets = std::any_of(pending.begin(), pending.end(), [](const Pending& item) { return item.Substitution.Lowering == Amd64OnlyLowering::Trampoline; }); - const auto branchTargets = needsBranchTargets ? _collectBranchTargets(seg, matches, ph) : std::set{}; + const auto branchTargets = needsBranchTargets ? _collectBranchTargets(seg, matches, ph, fileBytes, segments) : std::set{}; std::set pendingIndices; for (const auto& item : pending) @@ -395,11 +429,13 @@ void Amd64OnlyConverter::_convertSegment( ConvertResult Amd64OnlyConverter::Convert( std::vector fileBytes, - const std::vector& codeSegments + const std::vector& segments ) const { ConvertResult result{}; - for (const auto& ph : codeSegments) - _convertSegment(fileBytes, ph, result); + for (const auto& ph : segments) { + if (ph.Type == kLoadSegment && (ph.Flags & kExecutableSegment) != 0) + _convertSegment(fileBytes, ph, segments, result); + } result.Bytes = std::move(fileBytes); return result; } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 16e891044ab..8c4e4e00907 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -886,6 +886,43 @@ void converterWideStoreWindows() { requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); } +void converterJumpTableTargets() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes leaTable = {0x48, 0x8D, 0x0D, 0xF9, 0x1F, 0x00, 0x00}; + const Bytes dispatch = {0x48, 0x63, 0x04, 0x81, 0x48, 0x01, 0xC8, 0xFF, 0xE0, 0x48, 0x83, 0xC1, 0x01, 0xC5, 0xFC, 0x11, 0x07, 0x48, 0x83, 0xC2, 0x01, 0xC3}; + const Bytes store = {0xC5, 0xFC, 0x11, 0x07}; + const Domain::ProgramHeader code{1, 5, 0x200, 0x1000, 0, 29, 29, 16}; + const Domain::ProgramHeader data{1, 4, 0x300, 0x3000, 0, 0x40, 0x40, 16}; + const auto table = [](const std::uint64_t base, const std::vector& targets) { + Bytes bytes; + for (const auto target : targets) { + const auto relative = static_cast(target - base); + for (std::size_t byte = 0; byte < 4; ++byte) + bytes.push_back(static_cast(relative >> (byte * 8))); + } + return bytes; + }; + const auto convert = [&](const Bytes& first, const std::vector>& placed, const std::vector& headers) { + Bytes file(0x400, 0xCC); + std::copy(first.begin(), first.end(), file.begin() + 0x200); + std::copy(dispatch.begin(), dispatch.end(), file.begin() + 0x207); + for (const auto& [offset, bytes] : placed) + std::copy(bytes.begin(), bytes.end(), file.begin() + static_cast(offset)); + return converter->Convert(file, headers); + }; + const auto placedAt = [](const Codegen::ConvertResult& result, const Domain::FileByteOffset offset) { + return result.Trampolines.size() == 1 && result.Trampolines[0].Offset == offset && result.Trampolines[0].Length == 8; + }; + + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1018})}, {0x330, store}}, {code, data}), 0x210), "A 256-bit store moved the instruction after it although a jump table enters that instruction"); + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1019, 0x1018})}}, {code, data}), 0x214), "A jump table was read past an entry that does not start an instruction"); + require(placedAt(convert({0x48, 0x8B, 0x0D, 0xF9, 0x1F, 0x00, 0x00}, {{0x300, table(0x3000, {0x1010, 0x1018})}}, {code, data}), 0x214), "A jump table was read at an address loaded rather than formed by LEA"); + require(placedAt(convert({0x48, 0x8D, 0x0D, 0xF7, 0x1F, 0x00, 0x00}, {{0x2FE, table(0x2FFE, {0x1018})}}, {code, data}), 0x214), "A jump table was read at an address outside every segment"); + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010})}, {0x380, table(0x3000, {0x1018})}}, {code, data, {0x61000000, 4, 0x380, 0x3000, 0, 0x10, 0x10, 16}}), 0x214), "A jump table was read from a segment that is not loaded"); + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1018})}, {0x3A0, store}}, {code, data, {2, 5, 0x3A0, 0x2000, 0, 4, 4, 16}}), 0x210), "A segment that is not loaded was converted"); + requireFailure([&] { (void)convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1018})}}, {code, {1, 4, 0x300, 0x3000, 0, 0x1000, 0x1000, 16}}); }, "A jump table was read from a segment that exceeds the file"); +} + void converterRosetta() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); Bytes file(0x300, 0xCC); @@ -1637,6 +1674,7 @@ int main() { converterRosetta(); wideStoreSplit(); converterWideStoreWindows(); + converterJumpTableTargets(); converterReciprocal(); converterStrayRex(); rewriterStrayRex(); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 824d8d6561e..f67db4aa08c 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -66,8 +66,8 @@ int Run(const int argc, char* argv[]) { std::vector trampolines; if (args.toIntel) { - const auto codeSegments = Relinker::ElfReader(sourceBytes).ReadCodeSegments(); - auto converted = Codegen::MakeAmd64OnlyConverter(args.toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(sourceBytes), codeSegments); + const auto segments = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); + auto converted = Codegen::MakeAmd64OnlyConverter(args.toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(sourceBytes), segments); sourceBytes = std::move(converted.Bytes); trampolines = std::move(converted.Trampolines); std::map stubsByName; diff --git a/core/relinker/relinker/include/relinker/domain/IElfReader.hpp b/core/relinker/relinker/include/relinker/domain/IElfReader.hpp index 90ce34de307..f528b394511 100644 --- a/core/relinker/relinker/include/relinker/domain/IElfReader.hpp +++ b/core/relinker/relinker/include/relinker/domain/IElfReader.hpp @@ -18,7 +18,6 @@ class IElfReader { [[nodiscard]] virtual FileByteOffset TranslateVirtualAddress(VirtualAddress address) const = 0; [[nodiscard]] virtual std::vector ReadSection(const SectionHeader& header) const = 0; [[nodiscard]] virtual std::vector ReadSegment(const ProgramHeader& header) const = 0; - [[nodiscard]] virtual std::vector ReadCodeSegments() const = 0; [[nodiscard]] virtual std::uint64_t GetFileSize() const = 0; [[nodiscard]] virtual const std::vector& GetRawBytes() const = 0; }; diff --git a/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp b/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp index 1ed4ca8895a..b0cb4d0f761 100644 --- a/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp +++ b/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp @@ -17,7 +17,6 @@ class ElfReader : public IElfReader { FileByteOffset TranslateVirtualAddress(VirtualAddress address) const override; std::vector ReadSection(const SectionHeader& header) const override; std::vector ReadSegment(const ProgramHeader& header) const override; - std::vector ReadCodeSegments() const override; std::uint64_t GetFileSize() const override; const std::vector& GetRawBytes() const override; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index bbcecbca4f0..c16d9ed6841 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -342,7 +342,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::vector codeHeaders; for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); if (toIntel) { - auto converted = Codegen::MakeAmd64OnlyConverter(toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(image.Bytes), codeHeaders); + auto converted = Codegen::MakeAmd64OnlyConverter(toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(image.Bytes), image.Headers); image.Trampolines = std::move(converted.Trampolines); image.Bytes = std::move(converted.Bytes); } diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index 4b0e8560dca..1ea86114752 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -263,18 +263,6 @@ std::vector ElfReader::ReadSegment(const ProgramHeader& header) co _fileBuffer.begin() + header.Offset + header.FileSize); } -std::vector ElfReader::ReadCodeSegments() const { - static constexpr std::uint32_t kPtLoad = 1; - static constexpr std::uint32_t kPfX = 0x1; - std::vector result; - for (const auto& ph : ReadProgramHeaders()) { - if (ph.Type == kPtLoad && (ph.Flags & kPfX)) { - result.push_back(ph); - } - } - return result; -} - std::uint64_t ElfReader::GetFileSize() const { return _fileBuffer.size(); } From 8af94f9889d0f23660a6318843110d006ef6a121 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Mon, 12 Oct 2026 16:15:06 +0300 Subject: [PATCH 21/28] perf(relinker): index guest exports for dependency resolution (cherry picked from commit 33c955a8e3f90e2cee8a37d5b0fb90d172c9af58) --- core/relinker/CMakeLists.txt | 3 + .../relinker/src/guest/GuestModuleBuilder.cpp | 36 +++-- .../tests/test_guest_export_lookup.py | 128 ++++++++++++++++++ 3 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_export_lookup.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 23efb9d5bef..a11cc50b55e 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -443,6 +443,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_identity.py $) + add_test(NAME guest_export_lookup COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_export_lookup.py $) + set_tests_properties(guest_export_lookup PROPERTIES TIMEOUT 30) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index c16d9ed6841..3b33b9a01aa 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,11 @@ namespace Relinker { namespace { +struct GuestExport { + std::size_t Provider; + std::uint8_t Type; +}; + std::vector ReadNeededNames(const Domain::SysVDynamicSection& dynamic) { if (dynamic.DynamicSegmentData.size() % 16 != 0) throw Domain::RelinkerException("Invalid executable dependency table"); std::vector names; @@ -136,7 +142,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (std::filesystem::equivalent(path, outputPath)) throw Domain::RelinkerException("Executable output would overwrite an input module: " + Io::Utf8Path(outputPath)); } - std::map> exports; + std::unordered_map> exports; std::map> sharedExports; std::set outputNames; for (const auto& path : paths) { @@ -153,10 +159,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || symbol.Section == AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; auto& providers = exports[symbol.Name]; - const bool repeated = std::find(providers.begin(), providers.end(), images.size()) != providers.end(); - if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front()].SourcePath)); - if (!repeated) providers.push_back(images.size()); - if (!windows && providers.size() > 1) sharedExports[symbol.Name].insert(providers.begin(), providers.end()); + const bool repeated = !providers.empty() && providers.back().Provider == images.size(); + if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath)); + if (!repeated) providers.push_back({images.size(), static_cast(symbol.Info & 15)}); + if (!windows && providers.size() > 1) + for (const auto& provider : providers) sharedExports[symbol.Name].insert(provider.Provider); } images.push_back(std::move(image)); } @@ -264,20 +271,19 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (shared == sharedExports.end() || declared == guestNames.end() || !shared->second.contains(declared->second)) rejectSharedImport(symbol.Name, Io::Utf8Path(images[index].SourcePath)); const auto found = exports.find(symbol.Name); - std::vector providers; + const GuestExport* provider = nullptr; if (found != exports.end()) { - for (const auto provider : found->second) { - if (symbol.Library.empty() || (declared != guestNames.end() && declared->second == provider)) providers.push_back(provider); + for (const auto& candidate : found->second) { + if (!symbol.Library.empty() && (declared == guestNames.end() || declared->second != candidate.Provider)) continue; + if (provider != nullptr) throw Domain::RelinkerException("Ambiguous guest import after stripping #: " + symbol.Name); + provider = &candidate; } } - if (providers.size() > 1) throw Domain::RelinkerException("Ambiguous guest import after stripping #: " + symbol.Name); - if (!providers.empty()) { - const auto& provider = images[providers.front()]; - const auto exported = std::find_if(provider.Symbols.begin(), provider.Symbols.end(), [&](const auto& candidate) { return candidate.Section != 0 && candidate.Section != AbsoluteSection && candidate.Name == symbol.Name && (candidate.Info >> 4) != 0 && candidate.Visibility != 1 && candidate.Visibility != 2; }); - if (exported == provider.Symbols.end() || ((symbol.Info & 15) != 0 && (symbol.Info & 15) != (exported->Info & 15))) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); - if (providers.front() != index) dependencies[index].insert(providers.front()); + if (provider != nullptr) { + if ((symbol.Info & 15) != 0 && (symbol.Info & 15) != provider->Type) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); + if (provider->Provider != index) dependencies[index].insert(provider->Provider); } else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name); - if (providers.empty()) systemImports[index].insert(symbolIndex); + if (provider == nullptr) systemImports[index].insert(symbolIndex); } } std::vector required(images.size()); diff --git a/core/relinker/relinker/tests/test_guest_export_lookup.py b/core/relinker/relinker/tests/test_guest_export_lookup.py new file mode 100644 index 00000000000..0204d163304 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_export_lookup.py @@ -0,0 +1,128 @@ +from pathlib import Path +import os +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture +from test_guest_module_directories import needed_libraries +from test_guest_symbol_names import imported_symbols, module_symbols +from test_windows_import_modules import executable + + +def guest_symbols(symbols, dependency=None): + image = guest_fixture(PLAIN_SITE) + image[0x400:0x406] = b'\xb8\x2a\x00\x00\x00\xc3' + strings = bytearray(b'\0') + table = bytearray(24) + for name, info, visibility, section in symbols: + offset = len(strings) + strings.extend(name.encode() + b'\0') + table.extend(struct.pack(' Date: Mon, 12 Oct 2026 18:21:01 +0500 Subject: [PATCH 22/28] fix(relinker): recognize SDK memory syscall trap patterns (cherry picked from commit c0967216bfe04be0f9f1a9299f56bfd937a89997) --- core/relinker/CMakeLists.txt | 13 +++ .../relinker/src/analysis/SyscallScanner.cpp | 42 ++++++++- .../relinker/tests/SdkMmanSyscallTests.cpp | 87 +++++++++++++++++++ .../relinker/tests/test_guest_syscall_trap.py | 62 ++++++++----- docs/dev/TechnicalDebt.md | 1 + docs/user/USAGE.md | 1 + 6 files changed, 185 insertions(+), 21 deletions(-) create mode 100644 core/relinker/relinker/tests/SdkMmanSyscallTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index a11cc50b55e..fa6d4e37194 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -181,6 +181,19 @@ if(BUILD_TESTING) add_test(NAME call_site_index COMMAND call_site_index_tests) set_tests_properties(call_site_index PROPERTIES TIMEOUT 20) + add_test_executable(sdk_mman_syscall_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/SdkMmanSyscallTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/DecodedInstruction.cpp) + target_include_directories(sdk_mman_syscall_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include) + add_test(NAME sdk_mman_syscall COMMAND sdk_mman_syscall_tests) + set_tests_properties(sdk_mman_syscall PROPERTIES TIMEOUT 20) + add_test_executable(autorun_exit_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/AutorunExitTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index a7e80bcf0c8..f83eb1eead4 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -50,6 +50,45 @@ enum class ForbiddenInstruction { Return, }; +static constexpr std::array SDK_MPROTECT_INLINE{ + 0x55, 0x48, 0x89, 0xE5, 0x53, 0x50, 0x89, 0xD1, 0x49, 0x89, 0xF0, 0x48, 0x89, 0xFE, + 0xF6, 0xC1, 0x04, 0x75, 0x31, 0x48, 0x63, 0xD1, 0xB8, 0x4A, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xF7, 0x4C, 0x89, 0xC6, 0x45, 0x31, 0xD2, 0x45, 0x31, 0xC0, 0x45, 0x31, 0xC9, + 0x0F, 0x05, 0x0F, 0x93, 0xC1, 0x48, 0x89, 0xC3, 0x48, 0xF7, 0xDB, 0x84, 0xC9, + 0x48, 0x0F, 0x45, 0xD8, 0x48, 0x85, 0xDB, +}; + +static constexpr std::array SDK_MMAP_INLINE{ + 0x49, 0x63, 0xD6, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, 0xB8, 0xDD, 0x01, 0x00, 0x00, + 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x48, 0x89, 0xC1, 0x0F, 0x93, 0xC2, 0x48, 0xF7, 0xD8, + 0x84, 0xD2, 0x48, 0x0F, 0x45, 0xC1, 0x48, 0x85, 0xC0, +}; + +static constexpr std::array SDK_MMAP_EXEC_INLINE{ + 0x44, 0x89, 0xF0, 0x83, 0xE0, 0xFB, 0x48, 0x63, 0xD0, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, + 0xB8, 0xDD, 0x01, 0x00, 0x00, 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x0F, 0x93, 0xC1, + 0x49, 0x89, 0xC7, 0x49, 0xF7, 0xDF, 0x84, 0xC9, 0x4C, 0x0F, 0x45, 0xF8, 0x4D, 0x85, 0xFF, +}; + +template +static bool MatchesSdkInlineStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, + const std::array& pattern, std::size_t beforeSyscall, + const std::vector& matches, FileByteOffset codeOffset) { + if (syscallOffset < beforeSyscall || syscallOffset > limit) return false; + const auto start = syscallOffset - beforeSyscall; + if (pattern.size() > limit - start || !std::equal(pattern.begin(), pattern.end(), code.begin() + static_cast(start))) return false; + const auto instruction = std::lower_bound(matches.begin(), matches.end(), codeOffset + start, + [](const Codegen::InstructionMatch& match, FileByteOffset offset) { return match.Offset < offset; }); + return instruction != matches.end() && instruction->Offset == codeOffset + start; +} + +static bool IsSdkMmanSyscallStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, + const std::vector& matches, FileByteOffset codeOffset) { + return MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MPROTECT_INLINE, 42, matches, codeOffset) + || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_INLINE, 17, matches, codeOffset) + || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_EXEC_INLINE, 23, matches, codeOffset); +} + static ForbiddenInstruction ClassifyForbiddenInstruction( const std::vector& codeSection, const std::size_t opcodeOffset, @@ -158,7 +197,8 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls( if (kind == ForbiddenInstruction::None) continue; - if (_trapSdkStubs && kind == ForbiddenInstruction::Invocation && IsSdkSyscallStub(codeSection, opcode)) { + if (_trapSdkStubs && kind == ForbiddenInstruction::Invocation && + (IsSdkSyscallStub(codeSection, opcode) || IsSdkMmanSyscallStub(codeSection, opcode, limit, matches, codeSectionOffset))) { trapped.push_back(opcode); std::cout << "Trapping SDK syscall instruction at code offset 0x" << std::hex << match.Offset << std::dec << '\n'; continue; diff --git a/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp new file mode 100644 index 00000000000..264cb6057c7 --- /dev/null +++ b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; +struct Fixture { Bytes code; std::size_t syscall; }; + +const std::vector Fixtures = { + {{0x55, 0x48, 0x89, 0xE5, 0x53, 0x50, 0x89, 0xD1, 0x49, 0x89, 0xF0, 0x48, 0x89, 0xFE, + 0xF6, 0xC1, 0x04, 0x75, 0x31, 0x48, 0x63, 0xD1, 0xB8, 0x4A, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xF7, 0x4C, 0x89, 0xC6, 0x45, 0x31, 0xD2, 0x45, 0x31, 0xC0, 0x45, 0x31, 0xC9, + 0x0F, 0x05, 0x0F, 0x93, 0xC1, 0x48, 0x89, 0xC3, 0x48, 0xF7, 0xDB, 0x84, 0xC9, + 0x48, 0x0F, 0x45, 0xD8, 0x48, 0x85, 0xDB}, 42}, + {{0x49, 0x63, 0xD6, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, 0xB8, 0xDD, 0x01, 0x00, 0x00, + 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x48, 0x89, 0xC1, 0x0F, 0x93, 0xC2, 0x48, 0xF7, 0xD8, + 0x84, 0xD2, 0x48, 0x0F, 0x45, 0xC1, 0x48, 0x85, 0xC0}, 17}, + {{0x44, 0x89, 0xF0, 0x83, 0xE0, 0xFB, 0x48, 0x63, 0xD0, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, + 0xB8, 0xDD, 0x01, 0x00, 0x00, 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x0F, 0x93, 0xC1, + 0x49, 0x89, 0xC7, 0x49, 0xF7, 0xDF, 0x84, 0xC9, 0x4C, 0x0F, 0x45, 0xF8, 0x4D, 0x85, 0xFF}, 23}, +}; + +void Require(bool value, const std::string& message) { + if (!value) throw std::runtime_error(message); +} + +bool Rejected(const Bytes& code, bool trap, std::size_t size) { + try { + Relinker::MakeSyscallScanner(trap)->ScanCodeSectionForSyscalls(code, 0, size); + return false; + } catch (const std::runtime_error&) { + return true; + } +} + +void CheckFixtures() { + std::size_t rejectedMutations = 0; + for (const auto& fixture : Fixtures) { + Require(Rejected(fixture.code, false, fixture.code.size()), "default mode accepted an SDK inline syscall"); + for (const auto padding : {std::size_t{0}, std::size_t{1}, std::size_t{7}, std::size_t{64}}) { + Bytes code(padding, 0x90); + code.insert(code.end(), fixture.code.begin(), fixture.code.end()); + const auto trapped = Relinker::MakeSyscallScanner(true)->ScanCodeSectionForSyscalls(code, 0x123400, code.size()); + Require(trapped == std::vector{padding + fixture.syscall}, "SDK inline syscall returned the wrong patch offset"); + } + for (std::size_t length = fixture.syscall + 2; length < fixture.code.size(); ++length) + Require(Rejected(fixture.code, true, length), "SDK matching read its suffix beyond the code section"); + for (std::size_t removed = 1; removed <= fixture.syscall; ++removed) { + const Bytes fragment(fixture.code.begin() + static_cast(removed), fixture.code.end()); + if (Rejected(fragment, false, fragment.size())) + Require(Rejected(fragment, true, fragment.size()), "SDK matching accepted a truncated prefix"); + } + for (std::size_t offset = 0; offset < fixture.code.size(); ++offset) { + if (offset == fixture.syscall || offset == fixture.syscall + 1) continue; + for (unsigned bit = 0; bit < 8; ++bit) { + auto code = fixture.code; + code[offset] ^= static_cast(1u << bit); + if (!Rejected(code, false, code.size())) continue; + Require(Rejected(code, true, code.size()), "SDK matching accepted a changed instruction sequence"); + ++rejectedMutations; + } + } + } + Require(rejectedMutations > 1000, "too few near-match rejection cases ran"); + Bytes embedded = {0x48, 0xB8}; + embedded.insert(embedded.end(), Fixtures.front().code.begin(), Fixtures.front().code.end()); + Require(Rejected(embedded, false, embedded.size()), "embedded fixture lost its syscall instruction"); + Require(Rejected(embedded, true, embedded.size()), "SDK matching accepted a prefix that starts inside an immediate"); + for (const auto& code : std::vector{{0xB8, 0xDD, 0x01, 0x00, 0x00, 0x0F, 0x05, 0xC3}, {0xCD, 0x80}, {0x0F, 0x34}, {0x0F, 0x07}}) + Require(Rejected(code, true, code.size()), "SDK trap mode accepted an unrelated privileged instruction"); + std::cout << "SDK inline syscall checks passed, " << rejectedMutations << " near-match rejections\n"; +} + +} + +int main() { + try { + CheckFixtures(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/test_guest_syscall_trap.py b/core/relinker/relinker/tests/test_guest_syscall_trap.py index 8a94d30f398..6971a52b6be 100644 --- a/core/relinker/relinker/tests/test_guest_syscall_trap.py +++ b/core/relinker/relinker/tests/test_guest_syscall_trap.py @@ -4,38 +4,60 @@ import sys import tempfile -from test_guest_intel_trampolines import elf_bytes_at, elf_loads, guest_fixture, main_fixture +from test_guest_intel_trampolines import elf_bytes_at, elf_loads, guest_fixture, main_fixture, pe_bytes_at, pe_sections GENERIC_STUB = bytes.fromhex( "48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 " "4c 8b 4c 24 08 0f 05 c3") NUMBERED_STUB = bytes.fromhex("48 c7 c0 09 00 00 00 49 89 ca 0f 05 c3") +MPROTECT_INLINE = bytes.fromhex( + "55 48 89 e5 53 50 89 d1 49 89 f0 48 89 fe f6 c1 04 75 31 48 63 d1 " + "b8 4a 00 00 00 48 89 f7 4c 89 c6 45 31 d2 45 31 c0 45 31 c9 " + "0f 05 0f 93 c1 48 89 c3 48 f7 db 84 c9 48 0f 45 d8 48 85 db") +MMAP_INLINE = bytes.fromhex( + "49 63 d6 4c 63 d1 4d 63 c0 b8 dd 01 00 00 48 89 de " + "0f 05 48 89 c1 0f 93 c2 48 f7 d8 84 d2 48 0f 45 c1 48 85 c0") +MMAP_EXEC_INLINE = bytes.fromhex( + "44 89 f0 83 e0 fb 48 63 d0 4c 63 d1 4d 63 c0 b8 dd 01 00 00 48 89 de " + "0f 05 0f 93 c1 49 89 c7 49 f7 df 84 c9 4c 0f 45 f8 4d 85 ff") def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-guest-syscall-trap-") as directory: work = Path(directory) - modules = work / "sce_module" - modules.mkdir() - module = bytearray(guest_fixture(b"\x90" * 6)) - module[0x400:0x400 + len(GENERIC_STUB)] = GENERIC_STUB - module[0x440:0x440 + len(NUMBERED_STUB)] = NUMBERED_STUB - (modules / "sample.prx").write_bytes(module) - source = work / "input.elf" - source.write_bytes(main_fixture()) - output = work / "output.elf" - result = subprocess.run( - [str(relinker), "--trap-sdk-syscall-stubs", str(source), str(output)], - capture_output=True, text=True, timeout=30) - assert result.returncode == 0, (result.stdout, result.stderr) - - converted = output.parent / "app0" / "sce_module" / "sample.prx.guest.prx" - image = converted.read_bytes() - loads = elf_loads(image) - assert elf_bytes_at(image, loads, 0x1017, 3) == b"\x0f\x0b\xc3" - assert elf_bytes_at(image, loads, 0x104a, 3) == b"\x0f\x0b\xc3" + for windows in (False, True): + case = work / ("windows" if windows else "linux") + modules = case / "sce_module" + modules.mkdir(parents=True) + module = bytearray(guest_fixture(b"\x90" * 6)) + for offset, stub in ((0x400, GENERIC_STUB), (0x440, NUMBERED_STUB), + (0x450, MPROTECT_INLINE), (0x490, MMAP_INLINE), (0x4b8, MMAP_EXEC_INLINE)): + module[offset:offset + len(stub)] = stub + (modules / "sample.prx").write_bytes(module) + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / ("output.exe" if windows else "output.elf") + options = ["--windows"] if windows else [] + result = subprocess.run( + [str(relinker), *options, "--trap-sdk-syscall-stubs", str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (windows, result.stdout, result.stderr) + + converted = output.parent / "app0" / "sce_module" / "sample.prx.guest.prx" + image = converted.read_bytes() + if windows: + sections = pe_sections(image) + base = next(section[1] for section in sections if section[0] == b".elf0") + converted_code = pe_bytes_at(image, sections, base, 0x100) + else: + converted_code = elf_bytes_at(image, elf_loads(image), 0x1000, 0x100) + expected = bytearray(module[0x400:0x500]) + for offset in (0x17, 0x4a, 0x7a, 0xa1, 0xcf): + assert expected[offset:offset + 2] == b"\x0f\x05" + expected[offset:offset + 2] = b"\x0f\x0b" + assert converted_code == expected, (windows, converted_code.hex(), expected.hex()) if __name__ == "__main__": diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b5ca609ba0b..803c8dd1a68 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -484,6 +484,7 @@ Throughout the project, every function at every stage either **does exactly what - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_log_f32 and v_log_f16](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Dual-source blending](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. +- [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. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - [Ordered not-equal compares](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual` with both operands checked for NaN on their bits instead of `OpFOrdNotEqual`; the two agree for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of the compare feeds a ballot on a 32-wide subgroup (#825) - [sce::Job::JobManager](../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Only CPU sequences exist: `initialize` and `calculateRequiredMemorySize` with compute sequences, and both compute dispatches, are not implemented, and `createCompute` and `destroyCompute` return the errors the original returns when no compute sequence was configured. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 0928dee45d5..cddf9fde72f 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -60,6 +60,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. | | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | +| `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs and the recognized inline mmap/mprotect sequences from SDK libc/mman.c with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | | `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. There is no `--help` flag; invoking `relinker` without arguments prints the usage syntax and exits with an error. From e390acb860df818225b5a7a5142675525a13671c Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 12 Oct 2026 15:48:50 +0200 Subject: [PATCH 23/28] fix(relinker): keep weak undefined imports weak Weak undefined imports in the input ELF became STB_GLOBAL in the relinked output, so a title that weak-imports a function no module exports stopped at startup instead of binding the import to null. Keep STB_WEAK on the relinked dynamic symbols. On Windows, write 0 plus the addend to the slot of a weak import that no searched module exports. Fixes #4497 Co-Authored-By: Claude Opus 5.5 (cherry picked from commit cb1c18d552fbd03b860ee02aae109a24155fe791) --- core/relinker/CMakeLists.txt | 2 + core/relinker/domain/include/domain/Types.hpp | 1 + .../elfpatcher/windows/WindowsPeFormat.hpp | 1 + .../src/windows/WindowsEntryStubBuilder.cpp | 15 ++++ .../src/windows/WindowsRelocationBuilder.cpp | 1 + .../src/output/SysVDynamicSectionBuilder.cpp | 2 +- .../src/pipeline/RelinkerPipeline.cpp | 4 +- .../relinker/tests/test_weak_imports.py | 75 +++++++++++++++++++ 8 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 core/relinker/relinker/tests/test_weak_imports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fa6d4e37194..16c8fa6bbcb 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -463,6 +463,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $) + add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_weak_imports.py $) add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $) add_test(NAME input_magic COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index 35ec257472f..4e4cad6e5de 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -61,6 +61,7 @@ struct NidReference { FileByteOffset RelocationTableOffset; VirtualAddress RelocationAddress; std::int64_t Addend; + bool Weak = false; }; struct CallSiteInfo { diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index bef348f4a98..e20f23b1dcc 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -35,6 +35,7 @@ struct PeImport { std::int32_t TargetModule = -1; std::uint32_t RelocationType = 1; std::string Library; + bool Weak = false; }; struct PeRelocations { diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index a5ea5b7e3af..dcd704baba8 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -449,6 +449,21 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto resolved = code.Branch({0x0f, 0x85}); code.Emit({0x48, 0x83, 0xc7, 4, 0xff, 0xcd}); code.Rip({0x0f, 0x85}, search); + if (imports[index].Weak) { + code.Emit({0x31, 0xc0}); + const auto writeWeak = code.GetRva(); + if (imports[index].Addend != 0) { + code.Emit({0x48, 0xba}); + code.U64(imports[index].Addend); + code.Emit({0x48, 0x01, 0xd0}); + } + guestStartup.WriteImport(code, imports[index], handles); + const auto weakDone = code.Branch({0xe9}); + code.PatchBranch(resolved, code.GetRva()); + code.Rip({0xe9}, writeWeak); + code.PatchBranch(weakDone, code.GetRva()); + continue; + } if (lazyBinding) { lazyUnresolvedImports.push_back(index); const auto skipGotWrite = code.Branch({0xe9}); diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index 9706bfd1b73..0d4efd25b1b 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -50,6 +50,7 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom } image.WritePointer(target, 0); result.Imports.push_back({name, rva, addend}); + result.Imports.back().Weak = (dynamicSection.DynSymData[symbolOffset + 4] >> 4) == STB_WEAK; if (const auto module = dynamicSection.ImportModules.find(target); module != dynamicSection.ImportModules.end()) result.Imports.back().Library = module->second; } diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index b639608662b..c65cfeac1af 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -84,7 +84,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( auto symbolInfo = [](const NidReference& ref) -> std::uint8_t { if (ref.Nid.empty()) return static_cast((STB_WEAK << 4) | STT_NOTYPE); - return static_cast((STB_GLOBAL << 4) | STT_FUNC); + return static_cast(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | STT_FUNC); }; static constexpr std::size_t kRelaEntSize = 24; diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 64aa5b865d0..f0ad0772aa2 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -251,7 +251,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; - if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 4) + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 5) throw RelinkerException("Symbol table entry out of bounds", symOff); std::uint32_t nameOff = 0; @@ -262,7 +262,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf || std::any_of(raw.begin() + static_cast(symOff), raw.begin() + static_cast(symOff + symEntSize), [](const std::uint8_t byte) { return byte != 0; }))) throw RelinkerException("Unnamed import symbol is not a null symbol entry", symOff); - nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend}); + nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend, (raw[symOff + 4] >> 4) == STB_WEAK}); } }; diff --git a/core/relinker/relinker/tests/test_weak_imports.py b/core/relinker/relinker/tests/test_weak_imports.py new file mode 100644 index 00000000000..1141a74c670 --- /dev/null +++ b/core/relinker/relinker/tests/test_weak_imports.py @@ -0,0 +1,75 @@ +"""Check that weak undefined imports stay weak on Linux and bind to null on Windows when no module exports them.""" + +import os +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +from test_windows_import_modules import executable, provider + +SHT_DYNSYM = 11 +STB_GLOBAL = 1 +STB_WEAK = 2 + + +def weak_executable(owner, symbol): + image = executable(owner, symbol) + image[0x4898 + 4] = (STB_WEAK << 4) | 2 + code = (b'\x48\x8b\x05' + struct.pack('> 4 + return bindings + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-weak-imports-') as directory: + work = Path(directory) + + for name, image, symbol, expected in (('linux-weak', weak_executable('a.prx', 'absent#A#B'), 'absent', STB_WEAK), + ('linux-global', executable('a.prx', 'absent#A#B'), 'absent', STB_GLOBAL)): + source = work / (name + '.elf') + source.write_bytes(image) + output = work / (name + '.out') + result = subprocess.run([str(relinker), '--skip-sce-module', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + bindings = import_bindings(output.read_bytes()) + assert bindings.get(symbol) == expected, (name, bindings) + + for name, symbol, expected in (('windows-weak-missing', 'absent#A#B', 33), + ('windows-weak-present', 'shared#A#B', 11)): + case = work / name + (case / 'prx').mkdir(parents=True) + (case / 'prx' / 'a.prx').write_bytes(provider(11)) + source = case / 'input.elf' + source.write_bytes(weak_executable('a.prx', symbol)) + output = case / 'output.exe' + result = subprocess.run([str(relinker), '--windows', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + if os.name == 'nt': + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + + print('Weak import tests passed') + + +if __name__ == '__main__': + main() From 7f6f5c99abba7cc9d212b08fec018a24c8beb5f2 Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 12 Oct 2026 16:31:10 +0200 Subject: [PATCH 24/28] test(relinker): register weak_imports next to elf_header Co-Authored-By: Claude Opus 5.5 (cherry picked from commit bdf003a5e80abf5e8f292f452d389016e7b7c4d2) --- core/relinker/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 16c8fa6bbcb..628466dbc49 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -463,10 +463,10 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $) - add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_weak_imports.py $) add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $) + add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_weak_imports.py $) add_test(NAME input_magic COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_magic.py $) add_test(NAME input_preservation COMMAND ${Python3_EXECUTABLE} From 21e62412137df6254aa03bdbb4328a90dc1d79ba Mon Sep 17 00:00:00 2001 From: saxopa <99569665+saxopa@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:54:13 +0200 Subject: [PATCH 25/28] fix(relinker): load a PT_SCE_RELRO segment that no PT_LOAD covers PS4 executables keep their relocated read-only data, GOT and init/fini arrays in a PT_SCE_RELRO segment outside every PT_LOAD. The segment filter dropped it, so Windows and macOS output stopped at the first relocation into it, and Linux output placed the relinker's extra block over its addresses without an error. The ELF reader now reports such a segment as a read-write PT_LOAD. A PT_SCE_RELRO inside a PT_LOAD keeps being skipped, and one that partially overlaps a PT_LOAD throws. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit d0502b449d076428b63d2d78606a6af87ea40b1b) --- core/relinker/CMakeLists.txt | 2 + .../relinker/src/parsing/ElfReader.cpp | 17 +++++ .../relinker/relinker/tests/test_sce_relro.py | 74 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 94 insertions(+) create mode 100644 core/relinker/relinker/tests/test_sce_relro.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 628466dbc49..a9dd1b8138e 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -481,6 +481,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_string_table_bounds.py $) add_test(NAME sce_code_extent COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_extent.py $) + add_test(NAME sce_relro COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $) add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index 1ea86114752..47f2cd05832 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -130,6 +130,23 @@ std::vector ElfReader::ReadProgramHeaders() const { offset += header.ProgramHeaderEntrySize; } + for (auto& relro : headers) { + if (relro.Type != PT_OS_RELRO || relro.MemorySize == 0) continue; + bool covered = false; + for (const auto& load : headers) { + if (load.Type != PT_LOAD || load.MemorySize == 0) continue; + const bool overlaps = relro.MappedAddress < load.MappedAddress ? load.MappedAddress - relro.MappedAddress < relro.MemorySize : relro.MappedAddress - load.MappedAddress < load.MemorySize; + if (!overlaps) continue; + if (relro.MappedAddress < load.MappedAddress || relro.MemorySize > load.MemorySize - (relro.MappedAddress - load.MappedAddress)) + throw RelinkerException("PT_SCE_RELRO partially overlaps a PT_LOAD segment", relro.MappedAddress); + covered = true; + } + if (!covered) { + relro.Type = PT_LOAD; + relro.Flags = PF_R | PF_W; + } + } + return headers; } diff --git a/core/relinker/relinker/tests/test_sce_relro.py b/core/relinker/relinker/tests/test_sce_relro.py new file mode 100644 index 00000000000..bcb87833755 --- /dev/null +++ b/core/relinker/relinker/tests/test_sce_relro.py @@ -0,0 +1,74 @@ +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_linux_dynamic_strings import dynamic_strings, file_offset +from test_linux_load_alignment import loads + +PT_LOAD = 1 +PT_DYNAMIC = 2 +PT_SCE_RELRO = 0x61000010 +PT_SCE_VERSION = 0x6FFFFF01 + + +def fixture(extra): + image = bytearray(0x3000) + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + headers = [(PT_LOAD, 5, 0, 0, 0x1000, 0x1000, 0x1000), *extra] + tags = [(5, 0x600), (10, 1), (6, 0x620), (11, 24), (7, 0x700), (8, 24), (9, 24), (0, 0)] + headers.append((PT_DYNAMIC, 6, 0x400, 0x400, len(tags) * 16, len(tags) * 16, 8)) + headers += [(PT_SCE_VERSION, 0, 0, 0, 0, 0, 1)] * (7 - len(headers)) + struct.pack_into(" Date: Sun, 11 Oct 2026 16:28:41 +0300 Subject: [PATCH 26/28] fix(relinker): validate the ELF object version and header size (cherry picked from commit faf1cd77c6e3f30ccb1467787119619f9fa18611) --- .../relinker/src/parsing/ElfReader.cpp | 4 ++ .../relinker/tests/StrictNidFilterTests.cpp | 2 + .../relinker/tests/test_elf_header.py | 48 +++++++++++++++++-- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index 47f2cd05832..a214a56f8d4 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -88,6 +88,10 @@ ElfHeader ElfReader::ReadHeader() const { throw RelinkerException("Expected little-endian ELF64 version 1"); if (_readU16At(0x12) != 62) throw RelinkerException("Expected an x86-64 ELF"); + if (_readU32At(0x14) != 1) + throw RelinkerException("Unsupported ELF object version: expected 1", 0x14); + if (_readU16At(0x34) != 64) + throw RelinkerException("Invalid ELF header size: expected 64 bytes", 0x34); ElfHeader header{}; header.Machine = _readU16At(0x12); diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index bb3646c3610..4878395603b 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -310,8 +310,10 @@ std::vector elfFixture(const StrictReachabilityInput& input) { bytes[6] = 1; write(bytes, 16, 3); write(bytes, 18, 62); + write(bytes, 20, 1); write(bytes, 24, input.Entries.at(0)); write(bytes, 32, 64); + write(bytes, 52, 64); write(bytes, 54, 56); write(bytes, 56, 2); write(bytes, 64, 1); diff --git a/core/relinker/relinker/tests/test_elf_header.py b/core/relinker/relinker/tests/test_elf_header.py index d314868311a..171c44e1c03 100644 --- a/core/relinker/relinker/tests/test_elf_header.py +++ b/core/relinker/relinker/tests/test_elf_header.py @@ -1,4 +1,5 @@ from pathlib import Path +import struct import subprocess import sys import tempfile @@ -6,6 +7,10 @@ from test_guest_intel_trampolines import main_fixture +MODES = ([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"], + ["--macos"], ["--macos", "--to-rosetta"]) + + def identification_cases(relinker, directory): image = main_fixture() cases = [("sysv", image, None)] @@ -21,7 +26,7 @@ def identification_cases(relinker, directory): data = bytearray(image) data[18:20] = value.to_bytes(2, "little") cases.append((f"machine-{value}", data, "Expected an x86-64 ELF")) - for mode, flags in enumerate(([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"])): + for mode, flags in enumerate(MODES): for name, data, error in cases: source = Path(directory) / f"{mode}-{name}.elf" output = source.with_suffix(".out") @@ -35,15 +40,51 @@ def identification_cases(relinker, directory): assert not output.exists() and not registry.exists(), (mode, name, "output written for unsupported ELF identification") else: assert result.returncode == 0 and registry.is_file(), (mode, name, result) - assert output.read_bytes().startswith(b"MZ" if "--windows" in flags else b"\x7fELF"), (mode, name) + magic = b"\x7fELF" + if "--windows" in flags: + magic = b"MZ" + elif "--macos" in flags: + magic = b"\xcf\xfa\xed\xfe" + assert output.read_bytes().startswith(magic), (mode, name) + + +def structural_cases(relinker, directory): + fields = ( + (20, "I", (0, 2, 0xFFFFFFFF), "Unsupported ELF object version"), + (52, "H", (0, 63, 65, 65535), "Invalid ELF header size"), + ) + for mode, flags in enumerate(MODES): + for filter_level in (0, 1, 2): + for offset, encoding, values, error in fields: + for value in values: + case = Path(directory) / f"header-{mode}-{filter_level}-{offset}-{value}" + case.mkdir() + source = case / "input.elf" + output = case / "output.out" + registry = case / "output.registry.json" + data = main_fixture() + struct.pack_into("<" + encoding, data, offset, value) + source.write_bytes(data) + result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", + f"unused-filter={filter_level}", *flags, str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and error in result.stderr, (case.name, result) + assert source.read_bytes() == data, (case.name, "source changed") + assert not output.exists() and not registry.exists(), (case.name, "output written") def main(): relinker = Path(sys.argv[1]).resolve() + headerOnly = bytearray(64) + headerOnly[0:7] = b"\x7fELF\x02\x01\x01" + headerOnly[18:20] = (62).to_bytes(2, "little") + headerOnly[20:24] = (1).to_bytes(4, "little") + headerOnly[52:54] = (64).to_bytes(2, "little") cases = [ ("tiny", bytes(10), "File too small for ELF header"), ("truncated", b"\x7fELF\x02\x01\x01" + bytes(25), "File too small for ELF header"), - ("header-only", b"\x7fELF\x02\x01\x01" + bytes(11) + b"\x3e" + bytes(45), "No PT_DYNAMIC segment found"), + ("header-only", bytes(headerOnly), "No PT_DYNAMIC segment found"), + ("header-only-sections", main_fixture()[:64], "FileByteOffset out of bounds"), ("bad-magic", bytes(64), "Invalid ELF magic number"), ] with tempfile.TemporaryDirectory(prefix="anyps5-elf-header-") as directory: @@ -55,6 +96,7 @@ def main(): capture_output=True, text=True, timeout=20) assert result.returncode == 2 and error in result.stderr and not output.exists(), (name, result) identification_cases(relinker, directory) + structural_cases(relinker, directory) print("ELF header tests passed") From 789b715124847404de9c1092bc843d546d667e80 Mon Sep 17 00:00:00 2001 From: RINNECODER <93304526+RINNECODER@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:57:53 -0500 Subject: [PATCH 27/28] fix(relinker): adapt fallthrough walk to the bitmap visited set The upstream change erases the address from a hash set; the fork's visited set is a per-byte bitmap and enqueue() already checks reachable, so the erase is unnecessary. Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index 7e74855fa2f..1c64337a30f 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -96,7 +96,6 @@ void buildGraph( info = decoder.DecodeInstruction(text.data() + bufOff, available); } reachable.insert(va); - visited.erase(va); VirtualAddress nextVaddr = va + static_cast(info.Length); From 9de9aa79069c0becfa5116079cc28187db8210c4 Mon Sep 17 00:00:00 2001 From: RINNECODER <93304526+RINNECODER@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:03:48 -0500 Subject: [PATCH 28/28] Link wide-store codegen sources into rosetta_cpuid_tests Amd64OnlyInstructionMatcher now references DecodeVexWideStore and WideStoreLowering; add WideStoreOperands.cpp and WideStoreLowering.cpp. Co-Authored-By: Claude Opus 5.5 --- tools/rosetta/tests/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/rosetta/tests/CMakeLists.txt b/tools/rosetta/tests/CMakeLists.txt index 2b78486292a..df0e9698254 100644 --- a/tools/rosetta/tests/CMakeLists.txt +++ b/tools/rosetta/tests/CMakeLists.txt @@ -27,7 +27,9 @@ add_test_executable(rosetta_cpuid_tests ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/ClzeroOperands.cpp ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/ClzeroLowering.cpp ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/ReciprocalOperands.cpp - ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/ReciprocalLowering.cpp) + ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/ReciprocalLowering.cpp + ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/WideStoreOperands.cpp + ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/WideStoreLowering.cpp) target_include_directories(rosetta_cpuid_tests PRIVATE ${ANYPS5_RELINKER_ROOT}/domain/include ${ANYPS5_RELINKER_ROOT}/codegen/include) target_compile_features(rosetta_cpuid_tests PRIVATE cxx_std_20) add_test(NAME rosetta_cpuid COMMAND rosetta_cpuid_tests)