fix(relinker): accept PT_DYNAMIC data after DT_NULL

The dynamic-segment parsers refused a PT_DYNAMIC whose p_filesz is not a
multiple of 16 before reading it. elfldr v0.27 from ps5-payload-dev declares
0x1468 bytes, the tail of its PT_LOAD, with DT_NULL at entry 21; FreeBSD rtld
and glibc stop at DT_NULL and never read p_filesz. Read whole 16-byte entries
within the declared size, stop at DT_NULL, and keep refusing a segment that
ends without one, in the executable reader, the code analysis, the relative
relocation index, the strict NID filter and the guest module reader.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
VingelvattenandClaude Fable 5.1 committed 2026-10-24 12:06:55 +02:00
1 parent 60465afd3a
commit 68f5474263
7 files changed
+57 -19

No files matched your search

@@ -71,11 +71,11 @@ std::set<Domain::VirtualAddress> CodeInstructionCollector::Collect(const std::ve
for (const auto& header : headers) {
if (header.Type == 0x61000000) sceBase = header.Offset;
if (header.Type != 2) continue;
if (hasDynamic || header.FileSize % 16 != 0) throw Domain::RelinkerException("Code analysis: invalid dynamic segment");
if (hasDynamic) throw Domain::RelinkerException("Code analysis: invalid dynamic segment");
hasDynamic = true;
range(header.Offset, header.FileSize);
bool terminated = false;
for (std::uint64_t offset = header.Offset; offset < header.Offset + header.FileSize; offset += 16) {
for (std::uint64_t offset = header.Offset; header.Offset + header.FileSize - offset >= 16; offset += 16) {
const auto tag = Io::ReadU64(bytes, offset);
if (tag == 0) { terminated = true; break; }
const auto value = Io::ReadU64(bytes, offset + 8);
@@ -175,7 +175,6 @@ std::unique_ptr<IRelativeRelocationIndex> BuildRelativeRelocationIndex(
std::uint64_t segOff = read64(elfBytes, phPos + 8);
std::uint64_t segSz = read64(elfBytes, phPos + 32);
if (segOff > elfBytes.size() || segSz > elfBytes.size() - segOff) throw RelinkerException("PT_DYNAMIC segment out of bounds");
if (segSz % 16 != 0) throw RelinkerException("Invalid dynamic segment size", segOff);
std::optional<VirtualAddress> relaVa;
std::optional<std::uint64_t> relaSz;
@@ -34,9 +34,8 @@ public:
throw RelinkerException("Strict filter: invalid load segment size", segment.Offset);
if (segment.Type != 2) continue;
if (!tags.empty()) throw RelinkerException("Strict filter: multiple dynamic segments");
if (segment.FileSize % 16 != 0) throw RelinkerException("Strict filter: truncated dynamic segment", segment.Offset);
bool terminated = false;
for (std::uint64_t offset = 0; offset < segment.FileSize; offset += 16) {
for (std::uint64_t offset = 0; segment.FileSize - offset >= 16; offset += 16) {
const auto tag = read<std::int64_t>(segment.Offset + offset);
if (tag == 0) {
terminated = true;
@@ -61,14 +61,14 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector
tls = &header;
}
}
if (loads.empty() || dynamic == nullptr || dynamic->FileSize % 16 != 0) fail("Missing or invalid ELF load/dynamic segments");
if (loads.empty() || dynamic == nullptr) fail("Missing or invalid ELF load/dynamic segments");
std::map<std::uint64_t, std::uint64_t> tags;
std::vector<std::uint64_t> needed;
std::vector<std::uint64_t> moduleImports;
std::vector<std::uint64_t> ps4ModuleImports;
std::vector<std::uint64_t> moduleExports;
bool terminated = false;
for (std::uint64_t offset = dynamic->Offset; offset < dynamic->Offset + dynamic->FileSize; offset += 16) {
for (std::uint64_t offset = dynamic->Offset; dynamic->Offset + dynamic->FileSize - offset >= 16; offset += 16) {
const auto tag = Io::ReadU64(bytes, offset);
const auto value = Io::ReadU64(bytes, offset + 8);
if (tag == 0) { terminated = true; break; }
@@ -270,15 +270,11 @@ std::vector<DynamicTag> ElfReader::ReadDynamicTags(const ProgramHeader& dynamicH
if (!_rangeFits(dynamicHeader.Offset, dynamicHeader.FileSize, _fileBuffer.size())) {
throw RelinkerException("Dynamic segment out of bounds", dynamicHeader.Offset);
}
if (dynamicHeader.FileSize % 16 != 0) {
throw RelinkerException("Invalid dynamic segment size", dynamicHeader.Offset);
}
std::vector<DynamicTag> tags;
FileByteOffset offset = dynamicHeader.Offset;
const FileByteOffset end = dynamicHeader.Offset + dynamicHeader.FileSize;
while (offset < end) {
while (end - offset >= 16) {
DynamicTag tag;
tag.Tag = static_cast<std::int64_t>(_readU64At(offset));
tag.Value = _readU64At(offset + 0x08);
@@ -75,7 +75,7 @@ def main():
convert(f"{index}-module-name", None, "Unsupported Windows guest filename", flags, module=guest_fixture(PLAIN_SITE))
module = guest_fixture(PLAIN_SITE)
struct.pack_into("<QQ", module, 176 + 32, 17, 17)
convert(f"{index}-module", None, "Missing or invalid ELF load/dynamic segments", flags, module=module)
convert(f"{index}-module", None, "Unterminated dynamic segment", flags, module=module)
convert(f"{index}-module-syscall", None, "Forbidden syscall instruction", flags, module=guest_fixture(b"\x0f\x05" + b"\x90" * 4), module_name="broken.prx")
if failures:
raise AssertionError(failures)
@@ -4,6 +4,8 @@ import subprocess
import sys
import tempfile
from test_guest_intel_trampolines import main_fixture
from test_guest_needed_modules import module_with_needed
from test_optional_plt import fixture as optional_plt_fixture
@@ -16,17 +18,24 @@ def fixture():
return image
def strict_fixture():
image = optional_plt_fixture([(4, 0x6a0)])
struct.pack_into("<I", image, 64 + 4, 5)
struct.pack_into("<IIII", image, 0x6a0, 1, 1, 0, 0)
return image
def main():
relinker = Path(sys.argv[1]).resolve()
failures = []
with tempfile.TemporaryDirectory(prefix="anyps5-dynamic-segment-") as directory:
work = Path(directory)
def convert(name, image, error=None):
def convert(name, image, error=None, modes=([], ["--windows"], ["--macos"], ["unused-filter=1"])):
source = work / (name + ".elf")
source.write_bytes(image)
for mode in ([], ["--windows"]):
output = work / (name + (".exe" if mode else ".out"))
for mode in modes:
output = work / (name + "." + ("-".join(flag.lstrip("-") for flag in mode) or "linux"))
result = subprocess.run([str(relinker), "--skip-sce-module", *mode, str(source), str(output)],
capture_output=True, text=True, timeout=20)
valid = result.returncode == 0 and output.exists() if error is None else (
@@ -60,9 +69,14 @@ def main():
struct.pack_into("<Q", malformed, 120 + 32, declared_size)
convert(name, malformed, "Dynamic segment out of bounds")
partial = fixture()
struct.pack_into("<Q", partial, 120 + 32, size + 1)
convert("partial-entry", partial, "Invalid dynamic segment size")
trailing = fixture()
struct.pack_into("<QQ", trailing, 120 + 32, size + 0x18, size + 0x18)
trailing[0x400 + size:0x400 + size + 0x18] = bytes(range(1, 0x19))
convert("trailing-bytes-after-null", trailing)
cut = fixture()
struct.pack_into("<QQ", cut, 120 + 32, size - 8, size - 8)
convert("partial-terminator", cut, "Unterminated dynamic segment")
missing_null = fixture()
struct.pack_into("<QQ", missing_null, 120 + 32, size - 16, size - 16)
@@ -72,6 +86,36 @@ def main():
struct.pack_into("<QQ", empty, 120 + 32, 0, 0)
convert("empty", empty, "Unterminated dynamic segment")
strict_modes = (["unused-filter=2"], ["--windows", "unused-filter=2"], ["--macos", "unused-filter=2"])
strict_size = struct.unpack_from("<Q", strict_fixture(), 120 + 32)[0]
strict = strict_fixture()
struct.pack_into("<QQ", strict, 120 + 32, strict_size + 0x18, strict_size + 0x18)
strict[0x400 + strict_size:0x400 + strict_size + 0x18] = bytes(range(1, 0x19))
convert("strict-trailing-bytes-after-null", strict, modes=strict_modes)
strict = strict_fixture()
struct.pack_into("<QQ", strict, 120 + 32, strict_size - 8, strict_size - 8)
convert("strict-partial-terminator", strict, "Unterminated dynamic segment", strict_modes)
for name, delta, error in (("module-trailing-bytes", 8, None),
("module-partial-terminator", -8, "Unterminated dynamic segment")):
module = module_with_needed((b"libkernel.prx",), (0,))
declared = struct.unpack_from("<Q", module, 176 + 32)[0] + delta
struct.pack_into("<QQ", module, 176 + 32, declared, declared)
for mode in ([], ["--windows"], ["--macos"]):
case = work / (name + "-" + (mode[0].lstrip("-") if mode else "linux"))
(case / "prx").mkdir(parents=True)
(case / "prx" / "consumer.prx").write_bytes(module)
source = case / "input.elf"
source.write_bytes(main_fixture())
output = case / "output"
result = subprocess.run([str(relinker), *mode, str(source), str(output)],
capture_output=True, text=True, timeout=30)
valid = result.returncode == 0 and output.exists() if error is None else (
result.returncode == 2 and error in result.stderr and not output.exists())
if not valid:
failures.append((name, mode, result.returncode, result.stdout, result.stderr))
if failures:
raise AssertionError(failures)
print("Dynamic segment tests passed")