Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/relinker/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,8 @@ if(BUILD_TESTING)
set_tests_properties(macos_tls_alu PROPERTIES TIMEOUT 60)
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_load_bounds COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_load_bounds.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}
Expand Down
21 changes: 21 additions & 0 deletions core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ namespace Elfpatcher::Linux {
namespace {

constexpr std::uint64_t kLinuxUserAddressEnd = 0x800000000000;
constexpr std::uint64_t kLazyPltGotBytes = 24;
constexpr std::uint64_t kPltGotEntryBytes = 8;

std::string Hex(const std::uint64_t value) {
std::ostringstream text;
Expand Down Expand Up @@ -101,6 +103,19 @@ void RequireFileRange(const std::vector<std::uint8_t>& buf, const Domain::Progra
throw Domain::RelinkerException("PT_LOAD at " + Hex(load.MappedAddress) + " has file bytes outside the input");
}

void RequireLoaded(const std::vector<Domain::ProgramHeader>& headers, const std::uint64_t address, const std::uint64_t size, const std::uint32_t flags, const std::string& what) {
for (const auto& header : headers)
if (header.Type == PT_LOAD && (header.Flags == 0 || (header.Flags & flags) == flags) && address >= header.MappedAddress && address - header.MappedAddress < header.MemorySize && size <= header.MemorySize - (address - header.MappedAddress))
return;
const auto range = size > 1 ? " (" + std::to_string(size) + " bytes) does not fit inside " : std::string(" is not inside ");
throw Domain::RelinkerException(what + " " + Hex(address) + range + ((flags & PF_X) != 0 ? "an executable" : "a") + " PT_LOAD segment");
}

std::uint64_t RelocationWidth(const std::vector<std::uint8_t>& relocations, const std::size_t entry) {
const auto type = static_cast<std::uint32_t>(Io::ReadU64(relocations, entry + 8));
return type == R_X86_64_PC32 || type == R_X86_64_32 ? 4 : 8;
}

void SeparateSharedLoadPages(std::vector<std::uint8_t>& buf, std::vector<Domain::ProgramHeader>& headers) {
std::vector<std::size_t> loads;
for (std::size_t index = 0; index < headers.size(); ++index) {
Expand Down Expand Up @@ -356,6 +371,12 @@ std::vector<std::uint8_t> LinuxElfPatcher::Patch(
throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries");
if (_fixedBase)
RequireOnlyDependencies(dynSection.DynamicSegmentData);
RequireLoaded(originalHeaders, Io::ReadU64(sourceElf, kEhdrEntryOffset), 1, PF_X, "e_entry");
for (const auto& [relocationTable, name] : {std::pair{&dynSection.RelaData, "RELA r_offset"}, std::pair{&dynSection.RelaPltData, "JMPREL r_offset"}})
for (std::size_t entry = 0; entry < relocationTable->size(); entry += kRelaEntrySize)
RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry), 0, name);
if (!dynSection.RelaPltData.empty())
RequireLoaded(originalHeaders, originalPltGotVaddr, lazyBinding ? kLazyPltGotBytes : kPltGotEntryBytes, 0, "DT_PLTGOT");
const auto traced = _traceImports ? TracedImports(dynSection) : std::vector<TracedImport>{};
const std::uint64_t base = _fixedBase.value_or(0);
const auto address = [this, base](const std::uint64_t vaddr) {
Expand Down
2 changes: 1 addition & 1 deletion core/relinker/relinker/tests/test_linux_eh_frame_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def fixture(header_count, with_frame=True):
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)
3, 62, 1, 0, 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,
Expand Down
2 changes: 1 addition & 1 deletion core/relinker/relinker/tests/test_linux_load_alignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def fixture():
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, 6, 64, 0, 0)
3, 62, 1, 0, 64, 0, 0, 64, 56, 6, 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,
Expand Down
122 changes: 122 additions & 0 deletions core/relinker/relinker/tests/test_linux_load_bounds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Check that a Linux relink rejects an entry point, relocation offset or DT_PLTGOT outside PT_LOAD memory."""

from pathlib import Path
import struct
import subprocess
import sys
import tempfile

sys.path.insert(0, str(Path(__file__).resolve().parent))
from test_jmprel_table_bounds import fixture

PT_LOAD = 1
PT_SCE_RELRO = 0x61000010
PF_X = 1
PF_W = 2
PF_R = 4
R_X86_64_PC32 = 2
R_X86_64_32 = 10
E_ENTRY = 24
FIRST_LOAD_FLAGS = 64 + 4
SPARE_HEADER = 64 + 3 * 56
PLTGOT_VALUE = 0x400 + 9 * 16 + 8
OUTSIDE = 0x2000
ENTRY = 0x200
FIXED_BASE = 0x400000


def with_bss(image):
struct.pack_into("<IIQQQQQQ", image, SPARE_HEADER, PT_LOAD, PF_R | PF_W, 0x1000, 0x1000, 0x1000, 0, 0x1000, 0x1000)
return image


def with_relro(image):
struct.pack_into("<IIQQQQQQ", image, SPARE_HEADER, PT_SCE_RELRO, PF_R, 0, 0x1000, 0x1000, 0, 0x1000, 1)
return with_pltgot(image, 0x1800)


def with_entry(image, entry):
struct.pack_into("<Q", image, E_ENTRY, entry)
return image


def with_pltgot(image, value):
struct.pack_into("<Q", image, PLTGOT_VALUE, value)
return image


def without_load_flags(image):
struct.pack_into("<I", image, FIRST_LOAD_FLAGS, 0)
return image


def load_flags_at(elf, address):
phoff, = struct.unpack_from("<Q", elf, 0x20)
phentsize, phnum = struct.unpack_from("<HH", elf, 0x36)
headers = [struct.unpack_from("<IIQQQQQQ", elf, phoff + index * phentsize) for index in range(phnum)]
return [header[1] for header in headers if header[0] == PT_LOAD and header[3] <= address < header[3] + header[6]]


def relink(relinker, work, name, image, options, error):
source = work / (name + ".elf")
output = work / (name + ".out")
source.write_bytes(image)
result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)],
capture_output=True, text=True, timeout=20)
if error is None:
assert result.returncode == 0 and output.exists(), (name, options, result.returncode, result.stdout, result.stderr)
elf = output.read_bytes()
output.unlink()
return elf
assert result.returncode == 2 and error in result.stderr and not output.exists(), (
name, options, result.returncode, result.stdout, result.stderr)
return None


def main():
relinker = Path(sys.argv[1]).resolve()
cases = [
("plt-valid", fixture(linux=True), None),
("rela-valid", fixture(linux=True, data_relocations=True), None),
("rela-in-bss", with_bss(fixture(linux=True, data_relocations=True, target=0x1ff8)), None),
("rela-in-relro", with_relro(fixture(linux=True, data_relocations=True, target=0x1ff8)), None),
("jmprel-in-relro", with_relro(fixture(linux=True, target=0x1ff8)), None),
("pc32-load-tail", fixture(linux=True, data_relocations=True, relocation_type=R_X86_64_PC32, target=0xFFC), None),
("abs32-load-tail", fixture(linux=True, data_relocations=True, relocation_type=R_X86_64_32, target=0xFFC), None),
("entry-outside", with_entry(fixture(linux=True), 0x7FFFF000),
"e_entry 0x7ffff000 is not inside an executable PT_LOAD segment"),
("entry-not-executable", with_entry(with_bss(fixture(linux=True)), 0x1000),
"e_entry 0x1000 is not inside an executable PT_LOAD segment"),
("rela-outside", fixture(linux=True, data_relocations=True, target=OUTSIDE),
"RELA r_offset 0x2000 (8 bytes) does not fit inside a PT_LOAD segment"),
("rela-past-load-end", fixture(linux=True, data_relocations=True, target=0xFFC),
"RELA r_offset 0xffc (8 bytes) does not fit inside a PT_LOAD segment"),
("abs32-past-load-end", fixture(linux=True, data_relocations=True, relocation_type=R_X86_64_32, target=0xFFE),
"RELA r_offset 0xffe (4 bytes) does not fit inside a PT_LOAD segment"),
("jmprel-outside", fixture(linux=True, target=OUTSIDE),
"JMPREL r_offset 0x2000 (8 bytes) does not fit inside a PT_LOAD segment"),
]
with tempfile.TemporaryDirectory(prefix="anyps5-linux-load-bounds-") as directory:
work = Path(directory)
for name, image, error in cases:
for options in ([], ["--lazy-binding"]):
relink(relinker, work, name, image, options, error)
for options, base in (([], FIXED_BASE), (["--lazy-binding"], 0)):
elf = relink(relinker, work, "entry-in-flagless-load", without_load_flags(fixture(linux=True)), options, None)
flags = load_flags_at(elf, base + ENTRY)
assert flags == [PF_R | PF_W | PF_X], ("entry-in-flagless-load", options, flags)
pltgot_cases = [
("pltgot-outside", OUTSIDE, "DT_PLTGOT 0x2000 (8 bytes) does not fit inside a PT_LOAD segment",
"DT_PLTGOT 0x2000 (24 bytes) does not fit inside a PT_LOAD segment"),
("pltgot-load-tail", 0xFF0, None, "DT_PLTGOT 0xff0 (24 bytes) does not fit inside a PT_LOAD segment"),
("pltgot-lazy-fits", 0xFE8, None, None),
]
for name, pltgot, eager_error, lazy_error in pltgot_cases:
image = with_pltgot(fixture(linux=True), pltgot)
relink(relinker, work, name, image, [], eager_error)
relink(relinker, work, name, image, ["--lazy-binding"], lazy_error)
print("Linux load bounds tests passed")


if __name__ == "__main__":
main()
7 changes: 7 additions & 0 deletions core/relinker/relinker/tests/test_unused_filter_indirect.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
TABLE = 0x820
FUNCTION = CODE + 0x40
SECOND_FUNCTION = CODE + 0x60
REJECTED = {"got-slot-address-size-underflow":
"RELA r_offset 0xfffff008 (8 bytes) does not fit inside a PT_LOAD segment"}


def rel(next_address, target):
Expand Down Expand Up @@ -62,6 +64,11 @@ def run(relinker, work, name, image, level=1):
source.write_bytes(image)
result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", f"unused-filter={level}",
str(source), str(output)], capture_output=True, text=True, timeout=20)
if name in REJECTED:
assert result.returncode == 2 and REJECTED[name] in result.stderr and not output.exists(), (
name, result.returncode, result.stdout, result.stderr)
assert "CFG/GOT filtering: 1 -> 1;" in result.stdout, (name, result.stdout)
return True
assert result.returncode == 0 and output.exists(), (name, result.returncode, result.stdout, result.stderr)
registry = json.loads(output.with_suffix(".registry.json").read_text())
kept = len(registry)
Expand Down
Loading