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:
parent
759c66564f
commit
b3fa9d8318
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue