From b10424a47bf3efe4a07972d031879579999a687e Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:43:37 +0000 Subject: [PATCH 1/4] fix(relinker): require the Linux entry, relocation offsets and DT_PLTGOT in PT_LOAD memory The Windows and macOS patchers reject an entry point outside executable PT_LOAD memory and a relocation offset or DT_PLTGOT outside PT_LOAD memory through WindowsLoadImage::GetRva. The Linux patcher copied these values unchecked and reported success; with a bad entry or relocation offset the output crashed with SIGSEGV at load. LinuxElfPatcher::Patch now requires e_entry in an executable load and every non-NONE RELA and JMPREL r_offset and DT_PLTGOT to fit inside one PT_LOAD's MemorySize, in the fixed-base and --lazy-binding paths. A relocation needs the bytes ld.so writes: 4 for PC32 and 32, the output symbol's st_size (at least 1) for COPY, and 8 otherwise. DT_PLTGOT needs 8 bytes with eager binding and 24 with --lazy-binding, where ld.so writes GOT[1] and GOT[2]. Two test fixtures that set e_entry to a file offset outside their only PT_LOAD now use the matching virtual address. --- core/relinker/CMakeLists.txt | 2 + .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 34 ++++++ .../tests/test_linux_eh_frame_header.py | 2 +- .../tests/test_linux_load_alignment.py | 2 +- .../relinker/tests/test_linux_load_bounds.py | 114 ++++++++++++++++++ 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 core/relinker/relinker/tests/test_linux_load_bounds.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 5c4b89dfa89..7c091a9c5ae 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -527,6 +527,8 @@ if(BUILD_TESTING) set_tests_properties(macos_tls_alu PROPERTIES TIMEOUT 60) add_test(NAME linux_load_alignment COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_load_alignment.py $) + add_test(NAME linux_load_bounds COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_load_bounds.py $) add_test(NAME linux_eh_frame_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_eh_frame_header.py $) add_test(NAME linux_dynamic_strings COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 7453d82acaa..883abd7e464 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -22,6 +22,8 @@ namespace Elfpatcher::Linux { namespace { constexpr std::uint64_t kLinuxUserAddressEnd = 0x800000000000; +constexpr std::uint64_t kLazyPltGotBytes = 24; +constexpr std::uint64_t kPltGotEntryBytes = 8; std::string Hex(const std::uint64_t value) { std::ostringstream text; @@ -103,6 +105,31 @@ void RequireFileRange(const std::vector& buf, const Domain::Progra throw Domain::RelinkerException("PT_LOAD at " + Hex(load.MappedAddress) + " has file bytes outside the input"); } +void RequireLoaded(const std::vector& headers, const std::uint64_t address, const std::uint64_t size, const std::uint32_t flags, const std::string& what) { + for (const auto& header : headers) + if (header.Type == PT_LOAD && (header.Flags & flags) == flags && address >= header.MappedAddress && address - header.MappedAddress < header.MemorySize && size <= header.MemorySize - (address - header.MappedAddress)) + return; + const auto range = size > 1 ? " (" + std::to_string(size) + " bytes) does not fit inside " : std::string(" is not inside "); + throw Domain::RelinkerException(what + " " + Hex(address) + range + ((flags & PF_X) != 0 ? "an executable" : "a") + " PT_LOAD segment"); +} + +std::uint64_t RelocationWidth(const std::vector& relocations, const std::size_t entry, const std::vector& symbols) { + const auto info = Io::ReadU64(relocations, entry + 8); + switch (static_cast(info)) { + case R_X86_64_PC32: + case R_X86_64_32: + return 4; + case R_X86_64_COPY: { + const auto symbol = info >> 32; + if (symbol >= symbols.size() / kSymEntrySize) + throw Domain::RelinkerException("COPY relocation symbol index " + std::to_string(symbol) + " is outside the dynamic symbol table"); + return std::max(Io::ReadU64(symbols, static_cast(symbol * kSymEntrySize + 16)), 1); + } + default: + return 8; + } +} + void SeparateSharedLoadPages(std::vector& buf, std::vector& headers) { std::vector loads; for (std::size_t index = 0; index < headers.size(); ++index) { @@ -358,6 +385,13 @@ std::vector LinuxElfPatcher::Patch( throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); if (_fixedBase) RequireOnlyDependencies(dynSection.DynamicSegmentData); + RequireLoaded(originalHeaders, Io::ReadU64(sourceElf, kEhdrEntryOffset), 1, PF_X, "e_entry"); + for (const auto& [relocationTable, name] : {std::pair{&dynSection.RelaData, "RELA r_offset"}, std::pair{&dynSection.RelaPltData, "JMPREL r_offset"}}) + for (std::size_t entry = 0; entry < relocationTable->size(); entry += kRelaEntrySize) + if (static_cast(Io::ReadU64(*relocationTable, entry + 8)) != R_X86_64_NONE) + RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry, dynSection.DynSymData), 0, name); + if (!dynSection.RelaPltData.empty()) + RequireLoaded(originalHeaders, originalPltGotVaddr, lazyBinding ? kLazyPltGotBytes : kPltGotEntryBytes, 0, "DT_PLTGOT"); const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; const std::uint64_t base = _fixedBase.value_or(0); const auto address = [this, base](const std::uint64_t vaddr) { diff --git a/core/relinker/relinker/tests/test_linux_eh_frame_header.py b/core/relinker/relinker/tests/test_linux_eh_frame_header.py index a80a1eb1ed5..88d77abe531 100644 --- a/core/relinker/relinker/tests/test_linux_eh_frame_header.py +++ b/core/relinker/relinker/tests/test_linux_eh_frame_header.py @@ -18,7 +18,7 @@ def fixture(header_count, with_frame=True): image = bytearray(0x8000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) struct.pack_into(" Date: Sat, 24 Oct 2026 17:44:53 +0200 Subject: [PATCH 2/4] fix(relinker): drop the COPY width from the Linux load bounds check RelocationWidth took the width of a COPY relocation from the output symbol table, where every import has st_size 0, so the width was always one byte and the two copy-* boundary cases passed for that reason alone. #10658 rejects R_X86_64_COPY in validation, so the type no longer reaches this check; until it merges, COPY takes the 8-byte default like every other type. The three copy-* cases go with it. --- .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 20 ++++--------------- .../relinker/tests/test_linux_load_bounds.py | 13 ------------ 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 883abd7e464..3e9f0c0d272 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -113,21 +113,9 @@ void RequireLoaded(const std::vector& headers, const std: throw Domain::RelinkerException(what + " " + Hex(address) + range + ((flags & PF_X) != 0 ? "an executable" : "a") + " PT_LOAD segment"); } -std::uint64_t RelocationWidth(const std::vector& relocations, const std::size_t entry, const std::vector& symbols) { - const auto info = Io::ReadU64(relocations, entry + 8); - switch (static_cast(info)) { - case R_X86_64_PC32: - case R_X86_64_32: - return 4; - case R_X86_64_COPY: { - const auto symbol = info >> 32; - if (symbol >= symbols.size() / kSymEntrySize) - throw Domain::RelinkerException("COPY relocation symbol index " + std::to_string(symbol) + " is outside the dynamic symbol table"); - return std::max(Io::ReadU64(symbols, static_cast(symbol * kSymEntrySize + 16)), 1); - } - default: - return 8; - } +std::uint64_t RelocationWidth(const std::vector& relocations, const std::size_t entry) { + const auto type = static_cast(Io::ReadU64(relocations, entry + 8)); + return type == R_X86_64_PC32 || type == R_X86_64_32 ? 4 : 8; } void SeparateSharedLoadPages(std::vector& buf, std::vector& headers) { @@ -389,7 +377,7 @@ std::vector LinuxElfPatcher::Patch( for (const auto& [relocationTable, name] : {std::pair{&dynSection.RelaData, "RELA r_offset"}, std::pair{&dynSection.RelaPltData, "JMPREL r_offset"}}) for (std::size_t entry = 0; entry < relocationTable->size(); entry += kRelaEntrySize) if (static_cast(Io::ReadU64(*relocationTable, entry + 8)) != R_X86_64_NONE) - RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry, dynSection.DynSymData), 0, name); + RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry), 0, name); if (!dynSection.RelaPltData.empty()) RequireLoaded(originalHeaders, originalPltGotVaddr, lazyBinding ? kLazyPltGotBytes : kPltGotEntryBytes, 0, "DT_PLTGOT"); const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; diff --git a/core/relinker/relinker/tests/test_linux_load_bounds.py b/core/relinker/relinker/tests/test_linux_load_bounds.py index d75d394cdf0..49e9a994c49 100644 --- a/core/relinker/relinker/tests/test_linux_load_bounds.py +++ b/core/relinker/relinker/tests/test_linux_load_bounds.py @@ -14,12 +14,10 @@ PF_W = 2 PF_R = 4 R_X86_64_PC32 = 2 -R_X86_64_COPY = 5 R_X86_64_32 = 10 E_ENTRY = 24 SPARE_HEADER = 64 + 3 * 56 PLTGOT_VALUE = 0x400 + 9 * 16 + 8 -SYMBOL_SIZE = 0x620 + 24 + 16 OUTSIDE = 0x2000 @@ -43,11 +41,6 @@ def with_pltgot(image, value): return image -def with_symbol_size(image, size): - struct.pack_into(" Date: Sat, 24 Oct 2026 18:09:58 +0200 Subject: [PATCH 3/4] test(relinker): expect the load bounds error for the wrapped GOT slot The got-slot-address-size-underflow case puts its GOT slot at the 32-bit wrapped address 0xfffff008, outside every PT_LOAD. The unused filter still keeps the import, which the case keeps checking through the CFG/GOT filtering line, and the Linux patcher now rejects the relocation, so the case expects exit 2 with that message instead of an output file. --- .../relinker/relinker/tests/test_unused_filter_indirect.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/relinker/relinker/tests/test_unused_filter_indirect.py b/core/relinker/relinker/tests/test_unused_filter_indirect.py index 7b0197ee6e3..69e6e2f8649 100644 --- a/core/relinker/relinker/tests/test_unused_filter_indirect.py +++ b/core/relinker/relinker/tests/test_unused_filter_indirect.py @@ -11,6 +11,8 @@ TABLE = 0x820 FUNCTION = CODE + 0x40 SECOND_FUNCTION = CODE + 0x60 +REJECTED = {"got-slot-address-size-underflow": + "RELA r_offset 0xfffff008 (8 bytes) does not fit inside a PT_LOAD segment"} def rel(next_address, target): @@ -62,6 +64,11 @@ def run(relinker, work, name, image, level=1): source.write_bytes(image) result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", f"unused-filter={level}", str(source), str(output)], capture_output=True, text=True, timeout=20) + if name in REJECTED: + assert result.returncode == 2 and REJECTED[name] in result.stderr and not output.exists(), ( + name, result.returncode, result.stdout, result.stderr) + assert "CFG/GOT filtering: 1 -> 1;" in result.stdout, (name, result.stdout) + return True assert result.returncode == 0 and output.exists(), (name, result.returncode, result.stdout, result.stderr) registry = json.loads(output.with_suffix(".registry.json").read_text()) kept = len(registry) From 66473085c2e7a82a76de6a5938abaae471f18ec0 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:21:16 +0100 Subject: [PATCH 4/4] fix(relinker): accept a Linux entry in a PT_LOAD without flags The Linux output maps a PT_LOAD whose p_flags is 0 as RWX, so the e_entry check now accepts an entry inside such a segment instead of rejecting it. A new test case relinks that input in both Linux modes and checks that the output maps the entry's segment RWX. The R_X86_64_NONE skip in the relocation offset loop is removed, together with its none-outside case, because extractRela drops NONE before the relocation tables are rebuilt. --- .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 5 ++-- .../relinker/tests/test_linux_load_bounds.py | 29 ++++++++++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 2817ea4271d..32a7cf27ba7 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -106,7 +106,7 @@ void RequireFileRange(const std::vector& buf, const Domain::Progra void RequireLoaded(const std::vector& headers, const std::uint64_t address, const std::uint64_t size, const std::uint32_t flags, const std::string& what) { for (const auto& header : headers) - if (header.Type == PT_LOAD && (header.Flags & flags) == flags && address >= header.MappedAddress && address - header.MappedAddress < header.MemorySize && size <= header.MemorySize - (address - header.MappedAddress)) + if (header.Type == PT_LOAD && (header.Flags == 0 || (header.Flags & flags) == flags) && address >= header.MappedAddress && address - header.MappedAddress < header.MemorySize && size <= header.MemorySize - (address - header.MappedAddress)) return; const auto range = size > 1 ? " (" + std::to_string(size) + " bytes) does not fit inside " : std::string(" is not inside "); throw Domain::RelinkerException(what + " " + Hex(address) + range + ((flags & PF_X) != 0 ? "an executable" : "a") + " PT_LOAD segment"); @@ -375,8 +375,7 @@ std::vector LinuxElfPatcher::Patch( RequireLoaded(originalHeaders, Io::ReadU64(sourceElf, kEhdrEntryOffset), 1, PF_X, "e_entry"); for (const auto& [relocationTable, name] : {std::pair{&dynSection.RelaData, "RELA r_offset"}, std::pair{&dynSection.RelaPltData, "JMPREL r_offset"}}) for (std::size_t entry = 0; entry < relocationTable->size(); entry += kRelaEntrySize) - if (static_cast(Io::ReadU64(*relocationTable, entry + 8)) != R_X86_64_NONE) - RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry), 0, name); + RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry), 0, name); if (!dynSection.RelaPltData.empty()) RequireLoaded(originalHeaders, originalPltGotVaddr, lazyBinding ? kLazyPltGotBytes : kPltGotEntryBytes, 0, "DT_PLTGOT"); const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; diff --git a/core/relinker/relinker/tests/test_linux_load_bounds.py b/core/relinker/relinker/tests/test_linux_load_bounds.py index 49e9a994c49..687e95ae2b1 100644 --- a/core/relinker/relinker/tests/test_linux_load_bounds.py +++ b/core/relinker/relinker/tests/test_linux_load_bounds.py @@ -11,14 +11,18 @@ PT_LOAD = 1 PT_SCE_RELRO = 0x61000010 +PF_X = 1 PF_W = 2 PF_R = 4 R_X86_64_PC32 = 2 R_X86_64_32 = 10 E_ENTRY = 24 +FIRST_LOAD_FLAGS = 64 + 4 SPARE_HEADER = 64 + 3 * 56 PLTGOT_VALUE = 0x400 + 9 * 16 + 8 OUTSIDE = 0x2000 +ENTRY = 0x200 +FIXED_BASE = 0x400000 def with_bss(image): @@ -41,6 +45,18 @@ def with_pltgot(image, value): return image +def without_load_flags(image): + struct.pack_into("