From b3fa9d8318d89a2cd8a9e617526d726a7c9cac2d Mon Sep 17 00:00:00 2001 From: saxopa <99569665+saxopa@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:26:44 +0200 Subject: [PATCH] fix(relinker): move the Linux program header table when it has no free slot Linux output reused the input program header table in place, so it could hold no more entries than the input declared. A title whose kept segments and the five synthetic headers did not fit lost PT_GNU_EH_FRAME with a warning, or failed with "Not enough program header slots". When more headers are needed, the table is now written at the end of the extra block, e_phoff points there and PT_PHDR maps it through the extra block PT_LOAD; the header block then maps only the ELF header. Bytes after the input table are left as they were. Co-Authored-By: Claude Opus 5.5 --- .../elfpatcher/general/ElfConstants.hpp | 1 + .../general/IProgramHeaderLayoutBuilder.hpp | 2 + .../general/ProgramHeaderLayoutBuilder.hpp | 3 ++ .../general/ProgramHeaderLayoutBuilder.cpp | 35 ++++++++++++------ .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 14 ++++++- .../tests/test_linux_eh_frame_header.py | 37 ++++++++++--------- .../relinker/tests/test_linux_entry_argv.py | 18 +++++++-- 7 files changed, 74 insertions(+), 36 deletions(-) diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index 40802530e..dc0099f1d 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -51,6 +51,7 @@ inline constexpr std::size_t kRelaPltAlignment = 8; inline constexpr std::size_t kGotAlignment = 8; inline constexpr std::size_t kGotReservedSlots = 3; +inline constexpr std::uint64_t kElfHeaderSize = 64; inline constexpr std::uint64_t kDefaultLoadAlignment = 0x1000; inline constexpr std::uint64_t kPhdrHeaderAlignment = 8; inline constexpr std::uint64_t kDynamicHeaderAlignment = 8; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp index 75b2ae1e1..fcdb2eca0 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp @@ -13,6 +13,8 @@ namespace Elfpatcher { virtual std::uint64_t ComputeExtraBlockVaddr(const std::vector& originalHeaders, std::uint64_t extraBlockOffset) const = 0; + virtual std::size_t RequiredProgramHeaderCount(const std::vector& originalHeaders) const = 0; + virtual std::uint16_t WriteLayout(std::vector& buf, const ProgramHeaderLayoutRequest& request) const = 0; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp index 6c86cf4cf..189256c86 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp @@ -17,6 +17,8 @@ namespace Elfpatcher { std::uint64_t ComputeExtraBlockVaddr(const std::vector& originalHeaders, std::uint64_t extraBlockOffset) const override; + std::size_t RequiredProgramHeaderCount(const std::vector& originalHeaders) const override; + std::uint16_t WriteLayout( std::vector& buf, const ProgramHeaderLayoutRequest& request @@ -32,6 +34,7 @@ namespace Elfpatcher { [[nodiscard]] Domain::ProgramHeader _makeDynamicHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size) const; [[nodiscard]] Domain::ProgramHeader _makeInterpHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size) const; [[nodiscard]] std::uint32_t _fixLoadFlags(std::uint32_t originalFlags) const; + [[nodiscard]] std::vector _keptHeaders(const std::vector& originalHeaders) const; void _sortLoadHeaders(std::vector& headers) const; void _writeProgramHeader(std::vector& buf, std::size_t offset, const Domain::ProgramHeader& ph) const; }; diff --git a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp index a5dd1d79f..c02856a8f 100644 --- a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp @@ -132,19 +132,25 @@ std::uint64_t ProgramHeaderLayoutBuilder::ComputeExtraBlockVaddr( return vaddr; } +std::vector ProgramHeaderLayoutBuilder::_keptHeaders(const std::vector& originalHeaders) const { + std::vector kept; + for (const auto& ph : originalHeaders) { + if (!_segmentFilter->ShouldSkip(ph)) + kept.push_back(ph); + } + return kept; +} + +std::size_t ProgramHeaderLayoutBuilder::RequiredProgramHeaderCount(const std::vector& originalHeaders) const { + return _keptHeaders(originalHeaders).size() + kSyntheticProgramHeaderCount; +} + std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( std::vector& buf, const ProgramHeaderLayoutRequest& request ) const { - std::vector kept; - std::size_t frameCount = 0; - for (const auto& ph : request.OriginalHeaders) { - if (_segmentFilter->ShouldSkip(ph)) - continue; - kept.push_back(ph); - if (ph.Type == PT_GNU_EH_FRAME) - frameCount++; - } + auto kept = _keptHeaders(request.OriginalHeaders); + const auto frameCount = std::count_if(kept.begin(), kept.end(), [](const Domain::ProgramHeader& ph) { return ph.Type == PT_GNU_EH_FRAME; }); const bool keepFrames = kept.size() + kSyntheticProgramHeaderCount <= request.PhNum; if (!keepFrames && frameCount > 0) { @@ -165,9 +171,13 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( }; if (request.PhEntSize < 56) throw Domain::RelinkerException("Program header entry is too small"); - const std::uint64_t headerBlockSize = checkedEnd(request.PhOff, static_cast(neededPh) * request.PhEntSize); - if (headerBlockSize > buf.size()) + const std::uint64_t tableEnd = checkedEnd(request.PhOff, static_cast(neededPh) * request.PhEntSize); + if (tableEnd > buf.size()) throw Domain::RelinkerException("Program header table exceeds the output file"); + const bool tableInExtraBlock = request.PhOff >= request.ExtraBlockOffset; + if (tableInExtraBlock && tableEnd - request.ExtraBlockOffset > request.ExtraBlockSize) + throw Domain::RelinkerException("Program header table does not fit within the extra block"); + const std::uint64_t headerBlockSize = tableInExtraBlock ? kElfHeaderSize : tableEnd; if (request.ExtraBlockOffset > buf.size() || request.ExtraBlockSize > buf.size() - request.ExtraBlockOffset) throw Domain::RelinkerException("Extra block exceeds the output file"); std::uint64_t headerBlockAlign = kDefaultLoadAlignment; @@ -194,7 +204,8 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( const std::uint64_t interpVaddr = request.ExtraBlockVaddr + (request.InterpOffset - request.ExtraBlockOffset); std::vector layout; - layout.push_back(_makePhdrHeader(request.PhOff, headerBlockVaddr + request.PhOff, static_cast(neededPh) * request.PhEntSize)); + const std::uint64_t phdrVaddr = tableInExtraBlock ? request.ExtraBlockVaddr + (request.PhOff - request.ExtraBlockOffset) : headerBlockVaddr + request.PhOff; + layout.push_back(_makePhdrHeader(request.PhOff, phdrVaddr, static_cast(neededPh) * request.PhEntSize)); layout.push_back(_makeInterpHeader(request.InterpOffset, interpVaddr, request.InterpSize)); for (auto ph : kept) { if (ph.Type == PT_LOAD) diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 1db4b2741..4fd051eba 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -187,12 +187,22 @@ std::vector LinuxElfPatcher::Patch( for (char i : kInterp) buf.push_back(static_cast(i)); + std::uint64_t tableOff = phOff; + std::uint16_t tableSlots = phNum; + if (const auto required = _programHeaderLayoutBuilder->RequiredProgramHeaderCount(originalHeaders); required > phNum && required <= std::numeric_limits::max()) { + alignBuf(buf, kPhdrHeaderAlignment); + tableOff = static_cast(buf.size()); + tableSlots = static_cast(required); + buf.resize(buf.size() + required * phEntSize); + _byteWriter->WriteU64(buf, kEhdrPhOffOffset, tableOff); + } + const std::uint64_t extraBlockSize = static_cast(buf.size()) - extraBlockOff; ProgramHeaderLayoutRequest layoutRequest{}; - layoutRequest.PhOff = phOff; + layoutRequest.PhOff = tableOff; layoutRequest.PhEntSize = phEntSize; - layoutRequest.PhNum = phNum; + layoutRequest.PhNum = tableSlots; layoutRequest.OriginalHeaders = originalHeaders; layoutRequest.ExtraBlockOffset = extraBlockOff; layoutRequest.ExtraBlockVaddr = extraBlockVaddr; 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 10abad32d..ee1635a0d 100644 --- a/core/relinker/relinker/tests/test_linux_eh_frame_header.py +++ b/core/relinker/relinker/tests/test_linux_eh_frame_header.py @@ -38,8 +38,9 @@ def program_headers(elf): headers = [struct.unpack_from("= len(image), "The grown program header table must be moved past the input" + assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr + assert elf[table_end:table_end + 56] == image[table_end:table_end + 56], "Bytes after the input program header table changed" image = fixture(6, with_frame=False) table_end = 64 + 6 * 56 @@ -114,17 +118,14 @@ def main(): assert elf[table_end:table_end + 56] == image[table_end:table_end + 56], "Bytes after the program header table changed" for with_frame in (False, True): - source = Path(directory) / ("insufficient-slots-" + str(with_frame) + ".elf") - output = source.with_suffix(".out") image = fixture(5, with_frame=with_frame) image[64 + 5 * 56:64 + 6 * 56] = bytes([0xa5]) * 56 - source.write_bytes(image) - result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], capture_output=True, text=True, timeout=20) - assert result.returncode == 2, (result.returncode, result.stdout, result.stderr) - expected = "No free program header slot for PT_GNU_EH_FRAME" if with_frame else "Not enough program header slots: need 6, available 5" - assert expected in result.stderr, result.stderr - assert not output.exists(), "Insufficient program header slots must not produce an executable" - assert source.read_bytes() == image, "Input changed while rejecting insufficient program header slots" + result, elf = relink(relinker, directory, "few-slots-" + str(with_frame), image) + headers = program_headers(elf) + assert len(headers) == (7 if with_frame else 6), headers + assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_HEADER] if with_frame else []), headers + assert struct.unpack_from("= len(image), "The grown program header table must be moved past the input" + assert elf[64 + 5 * 56:64 + 6 * 56] == image[64 + 5 * 56:64 + 6 * 56], "Bytes after the input program header table changed" image = fixture(5, with_frame=False) headers = image[64:64 + 4 * 56] diff --git a/core/relinker/relinker/tests/test_linux_entry_argv.py b/core/relinker/relinker/tests/test_linux_entry_argv.py index 61c042681..82c043c19 100644 --- a/core/relinker/relinker/tests/test_linux_entry_argv.py +++ b/core/relinker/relinker/tests/test_linux_entry_argv.py @@ -56,11 +56,21 @@ def main(): result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], capture_output=True, text=True, timeout=20) assert result.returncode == 0, (result.stdout, result.stderr) + relocated_source = Path(directory) / "relocated-table.elf" + relocated = Path(directory) / "relocated-table.out" + image = argv_fixture() + struct.pack_into("= len(image), "The program header table was not moved" if sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64"): - output.chmod(0o755) - for arguments, expected in ((["Z"], -signal.SIGTRAP), (["Z", "extra"], -signal.SIGILL)): - executed = subprocess.run([str(output), *arguments], capture_output=True, timeout=20) - assert executed.returncode == expected, (arguments, executed.returncode) + for executable in (output, relocated): + executable.chmod(0o755) + for arguments, expected in ((["Z"], -signal.SIGTRAP), (["Z", "extra"], -signal.SIGILL)): + executed = subprocess.run([str(executable), *arguments], capture_output=True, timeout=20) + assert executed.returncode == expected, (executable.name, arguments, executed.returncode) for address, error in ((0x7FF00000, None), (0x80000000, "Entry stub call exceeds rel32 range")): source.write_bytes(far_load_fixture(address)) far = Path(directory) / ("far-%x.elf" % address)