fix(relinker): reject malformed dynamic segments

This commit is contained in:
Ethan Barr 2026-10-06 10:42:19 -07:00
parent 5895ea721a
commit cd0e534db3
4 changed files with 94 additions and 4 deletions

View File

@ -192,6 +192,8 @@ if(BUILD_TESTING)
if(Python3_Interpreter_FOUND)
add_test(NAME elf_offsets COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_offsets.py $<TARGET_FILE:relinker>)
add_test(NAME dynamic_segment COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_dynamic_segment.py $<TARGET_FILE:relinker>)
add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $<TARGET_FILE:relinker>)
add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE}

View File

@ -170,24 +170,31 @@ std::string ElfReader::_resolveShdrName(std::uint32_t nameOffset, const ElfHeade
}
std::vector<DynamicTag> ElfReader::ReadDynamicTags(const ProgramHeader& dynamicHeader) const {
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 + 16 <= end && offset + 16 <= _fileBuffer.size()) {
while (offset < end) {
DynamicTag tag;
tag.Tag = static_cast<std::int64_t>(_readU64At(offset));
tag.Value = _readU64At(offset + 0x08);
if (tag.Tag == 0) {
break;
return tags;
}
tags.push_back(tag);
offset += 16;
}
return tags;
throw RelinkerException("Unterminated dynamic segment", dynamicHeader.Offset);
}
FileByteOffset ElfReader::TranslateVirtualAddress(VirtualAddress address) const {

View File

@ -0,0 +1,81 @@
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from test_optional_plt import fixture as optional_plt_fixture
def fixture():
image = optional_plt_fixture()
struct.pack_into("<H", image, 0x38, 5)
for index in range(2, 5):
struct.pack_into("<IIQQQQQQ", image, 64 + index * 56,
0x6fffff01, 0, 0, 0, 0, 0, 0, 1)
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):
source = work / (name + ".elf")
source.write_bytes(image)
for mode in ([], ["--windows"]):
output = work / (name + (".exe" if mode else ".out"))
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 (
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))
image = fixture()
size = struct.unpack_from("<Q", image, 120 + 32)[0]
convert("valid", image)
padded = fixture()
struct.pack_into("<QQ", padded, 120 + 32, size + 16, size + 16)
struct.pack_into("<qQ", padded, 0x400 + size, 5, 0xffffffffffffffff)
convert("ignore-after-null", padded)
at_end = fixture()
at_end[-size:] = at_end[0x400:0x400 + size]
struct.pack_into("<Q", at_end, 120 + 8, len(at_end) - size)
convert("ends-at-eof", at_end)
for name, offset, declared_size in (
("offset-past-eof", len(image) + 1, size),
("offset-max", 0xffffffffffffffff, size),
("offset-wrap", 0xfffffffffffffff0, 32),
("size-wrap", 0x400, 0xfffffffffffffff0),
("truncated-after-null", 0x400, len(image) - 0x400 + 16),
):
malformed = fixture()
struct.pack_into("<Q", malformed, 120 + 8, offset)
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")
missing_null = fixture()
struct.pack_into("<QQ", missing_null, 120 + 32, size - 16, size - 16)
convert("missing-null", missing_null, "Unterminated dynamic segment")
empty = fixture()
struct.pack_into("<QQ", empty, 120 + 32, 0, 0)
convert("empty", empty, "Unterminated dynamic segment")
if failures:
raise AssertionError(failures)
print("Dynamic segment tests passed")
if __name__ == "__main__":
main()

View File

@ -12,7 +12,7 @@ def main():
cases = [
("program-header-offset", 0x20, "FileByteOffset out of bounds (offset 0xffffffffffffffff)"),
("load-offset", 64 + 8, "Segment offset out of bounds (offset 0xffffffffffffffff)"),
("dynamic-offset", 120 + 8, "FileByteOffset out of bounds (offset 0xffffffffffffffff)"),
("dynamic-offset", 120 + 8, "Dynamic segment out of bounds (offset 0xffffffffffffffff)"),
]
with tempfile.TemporaryDirectory(prefix="anyps5-elf-offsets-") as directory:
for name, field, error in cases: