fix(relinker): keep PT_GNU_EH_FRAME in the relinked executable

The segment filter dropped every header in the PT_LOOS..PT_HIOS range,
which includes PT_GNU_EH_FRAME. The unwinders find the executable's
unwind tables through that header, so no C++ exception thrown in the
main executable could be caught on Linux.

The header is kept only when the output program header table still fits
in the input's table; otherwise it is dropped as before, with a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8
This commit is contained in:
clem-gb 2026-10-08 15:23:31 +02:00
parent cba8d86da8
commit d16cac0ab2
5 changed files with 87 additions and 1 deletions

View File

@ -206,6 +206,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_diagnostics.py $<TARGET_FILE:relinker>)
add_test(NAME linux_load_alignment COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_load_alignment.py $<TARGET_FILE:relinker>)
add_test(NAME linux_eh_frame_header COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_eh_frame_header.py $<TARGET_FILE:relinker>)
add_test(NAME linux_dynamic_strings COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_dynamic_strings.py $<TARGET_FILE:relinker>)
add_test(NAME executable_segments COMMAND ${Python3_EXECUTABLE}

View File

@ -62,6 +62,7 @@ inline constexpr std::uint32_t PT_DYNAMIC = 2;
inline constexpr std::uint32_t PT_INTERP = 3;
inline constexpr std::uint32_t PT_NOTE = 4;
inline constexpr std::uint32_t PT_PHDR = 6;
inline constexpr std::uint32_t PT_GNU_EH_FRAME = 0x6474e550;
inline constexpr std::uint32_t PT_GNU_RELRO = 0x6474e552;
inline constexpr std::uint32_t PF_X = 0x1;
inline constexpr std::uint32_t PF_W = 0x2;

View File

@ -2,6 +2,7 @@
#include <elfpatcher/general/ElfConstants.hpp>
#include <domain/Types.hpp>
#include <cstdint>
#include <iostream>
#include <string>
namespace Elfpatcher {
@ -131,10 +132,19 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout(
const ProgramHeaderLayoutRequest& request
) const {
std::uint16_t keptCount = 0;
std::uint16_t frameCount = 0;
for (const auto& ph : request.OriginalHeaders) {
if (_segmentFilter->ShouldSkip(ph))
continue;
keptCount++;
if (ph.Type == PT_GNU_EH_FRAME)
frameCount++;
}
const bool keepFrames = keptCount + kSyntheticProgramHeaderCount < request.PhNum;
if (!keepFrames && frameCount > 0) {
keptCount -= frameCount;
std::cerr << "WARNING: No free program header slot for PT_GNU_EH_FRAME; C++ exceptions thrown in the executable cannot be caught.\n";
}
const std::uint16_t neededPh = keptCount + kSyntheticProgramHeaderCount;
@ -175,7 +185,7 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout(
writtenPh++;
for (const auto& ph : request.OriginalHeaders) {
if (_segmentFilter->ShouldSkip(ph))
if (_segmentFilter->ShouldSkip(ph) || (!keepFrames && ph.Type == PT_GNU_EH_FRAME))
continue;
const std::size_t phEntOff = static_cast<std::size_t>(request.PhOff) + writtenPh * request.PhEntSize;
if (ph.Type == PT_LOAD) {

View File

@ -12,6 +12,7 @@ bool SegmentFilter::_isSceSpecificSegment(const std::uint32_t type) const {
bool SegmentFilter::ShouldSkip(const Domain::ProgramHeader& ph) const {
if (ph.Type == PT_OS_PROCPARAM) return false;
if (ph.Type == PT_GNU_EH_FRAME) return false;
if (_isSceSpecificSegment(ph.Type)) return true;
if (ph.Type == PT_DYNAMIC) return true;
if (ph.Type == PT_NOTE && ph.MappedAddress == 0 && ph.FileSize > 0) return true;

View File

@ -0,0 +1,72 @@
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
PT_LOAD = 1
PT_DYNAMIC = 2
PT_GNU_EH_FRAME = 0x6474E550
PT_SCE_VERSION = 0x6FFFFF01
EH_FRAME_HEADER = (PT_GNU_EH_FRAME, 4, 0x4800, 0x800, 0x800, 16, 16, 4)
def fixture(header_count):
image = bytearray(0x8000)
image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9)
struct.pack_into("<HHIQQQIHHHHHH", image, 16,
3, 62, 1, 0x4000, 64, 0, 0, 64, 56, header_count, 64, 0, 0)
image[0x4000:0x4006] = b"\xb8\x2a\x00\x00\x00\xc3"
tags = [(5, 0x600), (10, 1), (6, 0x620), (11, 24), (7, 0x700), (8, 0), (9, 24), (0, 0)]
struct.pack_into("<IIQQQQQQ", image, 64,
PT_LOAD, 5, 0x4000, 0, 0, 0x1000, 0x1000, 0x4000)
struct.pack_into("<IIQQQQQQ", image, 120,
PT_DYNAMIC, 6, 0x600 + 0x4000, 0x600, 0x600, len(tags) * 16, len(tags) * 16, 8)
struct.pack_into("<IIQQQQQQ", image, 176, *EH_FRAME_HEADER)
for index in range(3, header_count):
struct.pack_into("<IIQQQQQQ", image, 64 + index * 56, PT_SCE_VERSION, 0, 0, 0, 0, 0, 0, 1)
for index, tag in enumerate(tags):
struct.pack_into("<qQ", image, 0x4600 + index * 16, *tag)
struct.pack_into("<BBBBQI", image, 0x4800, 1, 0, 3, 0, 0x900, 0)
return image
def program_headers(elf):
phoff, = struct.unpack_from("<Q", elf, 0x20)
phentsize, phnum = struct.unpack_from("<HH", elf, 0x36)
return [struct.unpack_from("<IIQQQQQQ", elf, phoff + index * phentsize) for index in range(phnum)]
def relink(relinker, directory, name, image):
source = Path(directory) / (name + ".elf")
output = Path(directory) / (name + ".out")
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 == 0, (name, result.returncode, result.stdout, result.stderr)
return result, output.read_bytes()
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-eh-frame-") as directory:
image = fixture(7)
result, elf = relink(relinker, directory, "spare-slot", image)
assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr
headers = program_headers(elf)
frames = [header for header in headers if header[0] == PT_GNU_EH_FRAME]
assert frames == [EH_FRAME_HEADER], ("PT_GNU_EH_FRAME must be kept unchanged", headers)
offset, size = EH_FRAME_HEADER[2], EH_FRAME_HEADER[5]
assert elf[offset:offset + size] == image[offset:offset + size], "eh_frame_hdr bytes changed"
assert all(header[0] != PT_SCE_VERSION for header in headers), headers
for header_count in (5, 6):
result, elf = relink(relinker, directory, "full-slots-" + str(header_count), fixture(header_count))
headers = program_headers(elf)
assert all(header[0] != PT_GNU_EH_FRAME for header in headers), (header_count, headers)
assert len(headers) == 6, (header_count, headers)
assert "No free program header slot for PT_GNU_EH_FRAME" in result.stderr, (header_count, result.stderr)
print("Linux eh_frame header test passed")
if __name__ == "__main__":
main()