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 <noreply@anthropic.com>
This commit is contained in:
saxopa 2026-10-12 17:26:44 +02:00 committed by Adrià Franch
parent 759c66564f
commit b3fa9d8318
7 changed files with 74 additions and 36 deletions

View File

@ -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;

View File

@ -13,6 +13,8 @@ namespace Elfpatcher {
virtual std::uint64_t ComputeExtraBlockVaddr(const std::vector<Domain::ProgramHeader>& originalHeaders, std::uint64_t extraBlockOffset) const = 0;
virtual std::size_t RequiredProgramHeaderCount(const std::vector<Domain::ProgramHeader>& originalHeaders) const = 0;
virtual std::uint16_t WriteLayout(std::vector<std::uint8_t>& buf, const ProgramHeaderLayoutRequest& request) const = 0;
};

View File

@ -17,6 +17,8 @@ namespace Elfpatcher {
std::uint64_t ComputeExtraBlockVaddr(const std::vector<Domain::ProgramHeader>& originalHeaders, std::uint64_t extraBlockOffset) const override;
std::size_t RequiredProgramHeaderCount(const std::vector<Domain::ProgramHeader>& originalHeaders) const override;
std::uint16_t WriteLayout(
std::vector<std::uint8_t>& 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<Domain::ProgramHeader> _keptHeaders(const std::vector<Domain::ProgramHeader>& originalHeaders) const;
void _sortLoadHeaders(std::vector<Domain::ProgramHeader>& headers) const;
void _writeProgramHeader(std::vector<std::uint8_t>& buf, std::size_t offset, const Domain::ProgramHeader& ph) const;
};

View File

@ -132,19 +132,25 @@ std::uint64_t ProgramHeaderLayoutBuilder::ComputeExtraBlockVaddr(
return vaddr;
}
std::vector<Domain::ProgramHeader> ProgramHeaderLayoutBuilder::_keptHeaders(const std::vector<Domain::ProgramHeader>& originalHeaders) const {
std::vector<Domain::ProgramHeader> kept;
for (const auto& ph : originalHeaders) {
if (!_segmentFilter->ShouldSkip(ph))
kept.push_back(ph);
}
return kept;
}
std::size_t ProgramHeaderLayoutBuilder::RequiredProgramHeaderCount(const std::vector<Domain::ProgramHeader>& originalHeaders) const {
return _keptHeaders(originalHeaders).size() + kSyntheticProgramHeaderCount;
}
std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout(
std::vector<std::uint8_t>& buf,
const ProgramHeaderLayoutRequest& request
) const {
std::vector<Domain::ProgramHeader> 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<std::uint64_t>(neededPh) * request.PhEntSize);
if (headerBlockSize > buf.size())
const std::uint64_t tableEnd = checkedEnd(request.PhOff, static_cast<std::uint64_t>(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<Domain::ProgramHeader> layout;
layout.push_back(_makePhdrHeader(request.PhOff, headerBlockVaddr + request.PhOff, static_cast<std::uint64_t>(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<std::uint64_t>(neededPh) * request.PhEntSize));
layout.push_back(_makeInterpHeader(request.InterpOffset, interpVaddr, request.InterpSize));
for (auto ph : kept) {
if (ph.Type == PT_LOAD)

View File

@ -187,12 +187,22 @@ std::vector<std::uint8_t> LinuxElfPatcher::Patch(
for (char i : kInterp)
buf.push_back(static_cast<std::uint8_t>(i));
std::uint64_t tableOff = phOff;
std::uint16_t tableSlots = phNum;
if (const auto required = _programHeaderLayoutBuilder->RequiredProgramHeaderCount(originalHeaders); required > phNum && required <= std::numeric_limits<std::uint16_t>::max()) {
alignBuf(buf, kPhdrHeaderAlignment);
tableOff = static_cast<std::uint64_t>(buf.size());
tableSlots = static_cast<std::uint16_t>(required);
buf.resize(buf.size() + required * phEntSize);
_byteWriter->WriteU64(buf, kEhdrPhOffOffset, tableOff);
}
const std::uint64_t extraBlockSize = static_cast<std::uint64_t>(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;

View File

@ -38,8 +38,9 @@ def program_headers(elf):
headers = [struct.unpack_from("<IIQQQQQQ", elf, phoff + index * phentsize) for index in range(phnum)]
phdr = next(header for header in headers if header[0] == 6)
assert phdr[5:7] == (phnum * phentsize, phnum * phentsize), ("PT_PHDR must cover every output header", phdr, phnum)
header_load = next(header for header in headers if header[0] == PT_LOAD and header[2] == 0)
assert header_load[5:7] == (phoff + phnum * phentsize, phoff + phnum * phentsize), ("Header PT_LOAD must cover every output header", header_load, phnum)
assert phdr[2] == phoff, ("PT_PHDR must name the program header table", phdr, phoff)
table_load = [header for header in headers if header[0] == PT_LOAD and header[2] <= phoff and phoff + phnum * phentsize <= header[2] + header[5]]
assert len(table_load) == 1 and phdr[3] == table_load[0][3] + phoff - table_load[0][2], ("A PT_LOAD must map every output header at PT_PHDR", table_load, phdr)
return headers
@ -96,13 +97,16 @@ def main():
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
source = Path(directory) / "full-slots.elf"
output = Path(directory) / "full-slots.out"
source.write_bytes(fixture(6))
result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], capture_output=True, text=True, timeout=20)
assert result.returncode == 2, result
assert "No free program header slot for PT_GNU_EH_FRAME" in result.stderr, result.stderr
assert not output.exists(), "Executable written without exception metadata"
image = fixture(6)
table_end = 64 + 6 * 56
image[table_end:table_end + 56] = bytes([0xa5]) * 56
result, elf = relink(relinker, directory, "full-slots", image)
headers = program_headers(elf)
assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == [EH_FRAME_HEADER], headers
assert len(headers) == 7, headers
assert struct.unpack_from("<Q", elf, 0x20)[0] >= 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("<Q", elf, 0x20)[0] >= 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]

View File

@ -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("<H", image, 0x38, 2)
relocated_source.write_bytes(image)
result = subprocess.run([str(relinker), "--skip-sce-module", str(relocated_source), str(relocated)],
capture_output=True, text=True, timeout=20)
assert result.returncode == 0, (result.stdout, result.stderr)
assert struct.unpack_from("<Q", relocated.read_bytes(), 0x20)[0] >= 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)