Merge pull request #2330 from simonbbby/fix/relinker-init-array-roots

fix(relinker): collect init and fini array roots through the containing load segment
This commit is contained in:
Adrià 2026-10-10 08:31:38 +02:00 committed by GitHub
commit 9db3480a65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 162 additions and 13 deletions

View File

@ -293,6 +293,8 @@ if(BUILD_TESTING)
endif()
add_test(NAME entry_point_bounds COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_entry_point_bounds.py $<TARGET_FILE:relinker>)
add_test(NAME init_array_roots COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_init_array_roots.py $<TARGET_FILE:relinker>)
else()
message(STATUS "Python3 interpreter not found; skipping Python relinker tests")
endif()

View File

@ -97,6 +97,8 @@ inline constexpr std::int64_t DT_INIT_ARRAYSZ = 27;
inline constexpr std::int64_t DT_FINI_ARRAYSZ = 28;
inline constexpr std::int64_t DT_RUNPATH = 29;
inline constexpr std::int64_t DT_FLAGS = 30;
inline constexpr std::int64_t DT_PREINIT_ARRAY = 32;
inline constexpr std::int64_t DT_PREINIT_ARRAYSZ = 33;
inline constexpr std::uint64_t DF_BIND_NOW = 0x8;
inline constexpr std::int64_t DT_OS_INIT = 0x6000000c;
@ -105,6 +107,8 @@ inline constexpr std::int64_t DT_OS_INIT_ARRAY = 0x60000019;
inline constexpr std::int64_t DT_OS_FINI_ARRAY = 0x6000001a;
inline constexpr std::int64_t DT_OS_INIT_ARRAYSZ = 0x6000001b;
inline constexpr std::int64_t DT_OS_FINI_ARRAYSZ = 0x6000001c;
inline constexpr std::int64_t DT_OS_PREINIT_ARRAY = 0x60000020;
inline constexpr std::int64_t DT_OS_PREINIT_ARRAYSZ = 0x60000021;
inline constexpr std::int64_t DT_OS_PLTGOT = 0x61000027;
inline constexpr std::int64_t DT_OS_JMPREL = 0x61000029;
inline constexpr std::int64_t DT_OS_PLTREL = 0x6100002b;

View File

@ -2,6 +2,7 @@
#define RELINKER_ANALYSIS_UNUSED_NID_FILTER_IENTRYPOINTCOLLECTOR_HPP
#include <relinker/domain/Types.hpp>
#include <relinker/analysis/UnusedNidFilter/IRelativeRelocationIndex.hpp>
#include <memory>
#include <vector>
@ -13,7 +14,8 @@ public:
virtual std::vector<VirtualAddress> Collect(
const std::vector<std::uint8_t>& elfBytes,
VirtualAddress textVaddr,
std::size_t textSize
std::size_t textSize,
const IRelativeRelocationIndex& relativeRelocations
) const = 0;
};

View File

@ -16,15 +16,15 @@ public:
) override {
if (textSection.empty()) throw RelinkerException("Cannot filter NIDs: text section is empty");
auto relativeRelocations = UnusedNidFilter::BuildRelativeRelocationIndex(elfBytes);
auto collector = UnusedNidFilter::MakeEntryPointCollector();
auto entries = collector->Collect(elfBytes, textVAddr, textSection.size());
auto entries = collector->Collect(elfBytes, textVAddr, textSection.size(), *relativeRelocations);
if (entries.empty()) throw RelinkerException("Cannot filter NIDs: no entry points found");
VirtualAddress primary = entries[0];
std::vector<VirtualAddress> extra(entries.begin() + 1, entries.end());
auto relativeRelocations = UnusedNidFilter::BuildRelativeRelocationIndex(elfBytes);
auto cfg = UnusedNidFilter::BuildControlFlowGraph(textSection, textVAddr, primary, extra, *relativeRelocations);
auto index = UnusedNidFilter::BuildGotAccessIndex(*cfg, textSection, textVAddr);

View File

@ -1,6 +1,7 @@
#include <relinker/analysis/UnusedNidFilter/IEntryPointCollector.hpp>
#include <elfpatcher/general/ElfConstants.hpp>
#include <cstring>
#include <limits>
namespace Relinker::UnusedNidFilter {
@ -30,6 +31,12 @@ bool fitsAt(std::uint64_t base, std::uint64_t offset, std::uint64_t length, std:
return base <= size && offset <= size - base && length <= size - base - offset;
}
struct LoadSegment {
VirtualAddress vaddr;
std::uint64_t fileOffset;
std::uint64_t fileSize;
};
bool inText(VirtualAddress va, VirtualAddress textVaddr, std::size_t textSize) {
return va >= textVaddr && va < textVaddr + static_cast<VirtualAddress>(textSize);
}
@ -41,7 +48,8 @@ public:
std::vector<VirtualAddress> Collect(
const std::vector<std::uint8_t>& elfBytes,
VirtualAddress textVaddr,
std::size_t textSize
std::size_t textSize,
const IRelativeRelocationIndex& relativeRelocations
) const override {
if (elfBytes.size() < 64) throw RelinkerException("ELF too small for header");
if (elfBytes[0] != 0x7f || elfBytes[1] != 'E' ||
@ -66,6 +74,14 @@ public:
if (phEntSize < 56) throw RelinkerException("ELF program header entry too small");
std::vector<LoadSegment> loads;
for (std::uint16_t i = 0; i < phCount; ++i) {
std::size_t phPos = static_cast<std::size_t>(phOff) + i * phEntSize;
if (phPos + 56 > elfBytes.size()) throw RelinkerException("Program header out of bounds");
if (read32(elfBytes, phPos) != PT_LOAD) continue;
loads.push_back({read64(elfBytes, phPos + 16), read64(elfBytes, phPos + 8), read64(elfBytes, phPos + 32)});
}
for (std::uint16_t i = 0; i < phCount; ++i) {
const std::uint64_t phIndexOff = static_cast<std::uint64_t>(i) * phEntSize;
if (!fitsAt(phOff, phIndexOff, 56, elfBytes.size())) throw RelinkerException("Program header out of bounds");
@ -84,6 +100,8 @@ public:
std::uint64_t initArraySz = 0;
VirtualAddress finiArrayVa = 0;
std::uint64_t finiArraySz = 0;
VirtualAddress preinitArrayVa = 0;
std::uint64_t preinitArraySz = 0;
for (std::uint64_t off = 0; off + 16 <= segSz; off += 16) {
std::size_t pos = static_cast<std::size_t>(segOff + off);
@ -97,24 +115,37 @@ public:
if (tag == DT_INIT_ARRAYSZ || tag == DT_OS_INIT_ARRAYSZ) initArraySz = val;
if (tag == DT_FINI_ARRAY || tag == DT_OS_FINI_ARRAY) finiArrayVa = val;
if (tag == DT_FINI_ARRAYSZ || tag == DT_OS_FINI_ARRAYSZ) finiArraySz = val;
if (tag == DT_PREINIT_ARRAY || tag == DT_OS_PREINIT_ARRAY) preinitArrayVa = val;
if (tag == DT_PREINIT_ARRAYSZ || tag == DT_OS_PREINIT_ARRAYSZ) preinitArraySz = val;
}
auto collectArray = [&](VirtualAddress arrayVa, std::uint64_t arraySz) {
if (arrayVa == 0 || arraySz == 0) return;
std::uint64_t phFileOff = read64(elfBytes, phPos + 8);
std::uint64_t phVaddr = read64(elfBytes, phPos + 16);
if (arrayVa < phVaddr || !fitsAt(phFileOff, arrayVa - phVaddr, 0, elfBytes.size())) return;
std::uint64_t arrayFileOff = phFileOff + (arrayVa - phVaddr);
std::uint64_t count = arraySz / 8;
for (std::uint64_t k = 0; k < count; ++k) {
if (!fitsAt(arrayFileOff, k * 8, 8, elfBytes.size())) break;
std::size_t entPos = static_cast<std::size_t>(arrayFileOff + k * 8);
addIfInText(read64(elfBytes, entPos));
if (arraySz % 8 != 0) throw RelinkerException("Init/fini array size is not a multiple of 8", arrayVa);
const LoadSegment* container = nullptr;
for (const auto& load : loads) {
if (arrayVa >= load.vaddr && arrayVa - load.vaddr <= load.fileSize &&
arraySz <= load.fileSize - (arrayVa - load.vaddr)) {
container = &load;
break;
}
}
if (!container) throw RelinkerException("Init/fini array is not file-backed", arrayVa);
std::uint64_t arrayFileOff = container->fileOffset + (arrayVa - container->vaddr);
if (arrayFileOff > elfBytes.size() || arraySz > elfBytes.size() - arrayFileOff)
throw RelinkerException("Init/fini array is out of bounds", arrayVa);
for (std::uint64_t k = 0; k < arraySz / 8; ++k) {
VirtualAddress slot = arrayVa + k * 8;
auto relocated = relativeRelocations.TargetOfSlot(slot);
std::uint64_t target = relocated ? *relocated : read64(elfBytes, static_cast<std::size_t>(arrayFileOff + k * 8));
if (target == 0 || target == std::numeric_limits<std::uint64_t>::max()) continue;
addIfInText(target);
}
};
collectArray(initArrayVa, initArraySz);
collectArray(finiArrayVa, finiArraySz);
collectArray(preinitArrayVa, preinitArraySz);
break;
}

View File

@ -0,0 +1,110 @@
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, DT_RELA, DT_RELASZ
DT_INIT_ARRAY = 25
DT_FINI_ARRAY = 26
DT_INIT_ARRAYSZ = 27
DT_FINI_ARRAYSZ = 28
DT_PREINIT_ARRAY = 32
DT_PREINIT_ARRAYSZ = 33
DT_OS_INIT_ARRAY = 0x60000019
DT_OS_INIT_ARRAYSZ = 0x6000001B
DT_OS_PREINIT_ARRAY = 0x60000020
DT_OS_PREINIT_ARRAYSZ = 0x60000021
R_X86_64_GLOB_DAT = 6
R_X86_64_RELATIVE = 8
DYNAMIC = 0x400
RELA = 0x7A0
GOT_SLOT = 0x308
FUNCTION = 0x280
BEFORE_DYNAMIC = 0x380
AFTER_DYNAMIC = 0x540
LOAD_END = 0x800
KEPT = "CFG/GOT filtering: 1 -> 1; filtered=0"
DROPPED = "CFG/GOT filtering: 1 -> 0; filtered=1"
def image(array_tag=None, array_size_tag=None, array_va=0, slots=(), relative=(), load_size=None):
elf = fixture()
for index in range(16):
tag, _ = struct.unpack_from("<qQ", elf, DYNAMIC + index * 16)
if tag == DT_RELASZ:
struct.pack_into("<qQ", elf, DYNAMIC + index * 16, DT_RELASZ, 24 * (1 + len(relative)))
elf[0x200] = 0xC3
elf[FUNCTION:FUNCTION + 8] = b"\x48\x8b\x05" + struct.pack("<i", GOT_SLOT - (FUNCTION + 7)) + b"\xc3"
if array_tag is not None:
tags = [(array_tag, array_va), (array_size_tag, 8 * len(slots)), (0, 0)]
for index, tag in enumerate(tags):
struct.pack_into("<qQ", elf, DYNAMIC + 13 * 16 + index * 16, *tag)
struct.pack_into("<Q", elf, 120 + 32, 16 * 16)
struct.pack_into("<Q", elf, 120 + 40, 16 * 16)
for index, word in enumerate(slots):
struct.pack_into("<Q", elf, array_va + index * 8, word)
struct.pack_into("<QQq", elf, RELA, GOT_SLOT, (1 << 32) | R_X86_64_GLOB_DAT, 0)
for index, (slot, addend) in enumerate(relative, 1):
struct.pack_into("<QQq", elf, RELA + index * 24, slot, R_X86_64_RELATIVE, addend)
if load_size is not None:
struct.pack_into("<Q", elf, 64 + 32, load_size)
struct.pack_into("<Q", elf, 64 + 40, load_size)
return elf
def run(relinker, work, name, elf):
source = work / (name + ".elf")
output = work / (name + ".exe")
source.write_bytes(elf)
return subprocess.run([str(relinker), "--skip-sce-module", "--windows", "unused-filter=1", str(source), str(output)],
capture_output=True, text=True, timeout=20), output
def filtered(relinker, work, name, elf):
result, output = run(relinker, work, name, elf)
assert result.returncode == 0 and output.exists(), (name, result.stdout, result.stderr)
lines = [line for line in result.stdout.splitlines() if line.startswith("CFG/GOT filtering:")]
assert len(lines) == 1, (name, result.stdout)
return lines[0]
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-init-array-") as directory:
work = Path(directory)
cases = [
("no-array", image(), DROPPED),
("init-before-dynamic", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION]), KEPT),
("init-after-dynamic", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, AFTER_DYNAMIC, [FUNCTION]), KEPT),
("init-os-tags", image(DT_OS_INIT_ARRAY, DT_OS_INIT_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION]), KEPT),
("init-second-slot", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [0, FUNCTION]), KEPT),
("init-terminators", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [(1 << 64) - 1, 0]), DROPPED),
("init-relative-addend", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [0],
[(BEFORE_DYNAMIC, FUNCTION)]), KEPT),
("init-relative-overrides-word", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [0x200],
[(BEFORE_DYNAMIC, FUNCTION)]), KEPT),
("init-relative-terminator", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION],
[(BEFORE_DYNAMIC, 0)]), DROPPED),
("fini-before-dynamic", image(DT_FINI_ARRAY, DT_FINI_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION]), KEPT),
("fini-relative-addend", image(DT_FINI_ARRAY, DT_FINI_ARRAYSZ, AFTER_DYNAMIC, [0],
[(AFTER_DYNAMIC, FUNCTION)]), KEPT),
("preinit-before-dynamic", image(DT_PREINIT_ARRAY, DT_PREINIT_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION]), KEPT),
("preinit-os-tags", image(DT_OS_PREINIT_ARRAY, DT_OS_PREINIT_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION]), KEPT),
]
for name, elf, expected in cases:
line = filtered(relinker, work, name, elf)
assert line == expected, (name, line, expected)
result, output = run(relinker, work, "unmapped-array",
image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, LOAD_END + 0x100, [FUNCTION], load_size=LOAD_END))
assert result.returncode == 2 and "array is not file-backed" in result.stderr and not output.exists(), (
result.returncode, result.stdout, result.stderr)
print("Init array root tests passed")
if __name__ == "__main__":
main()