From fcb0b962a72c9b64656a6208ccc24165f8d17174 Mon Sep 17 00:00:00 2001 From: simonbbby Date: Fri, 9 Oct 2026 10:06:11 -0400 Subject: [PATCH] fix(relinker): collect init and fini array roots through the containing load segment The unused-filter=1 entry point collector translated DT_INIT_ARRAY and DT_FINI_ARRAY through PT_DYNAMIC and skipped arrays placed before it, which is the usual linker layout, so constructors and destructors were not roots and imports used only by them were dropped. Arrays are now translated through the PT_LOAD that holds them, slot targets come from RELATIVE relocations when present, and DT_PREINIT_ARRAY is collected as in the strict filter. --- core/relinker/CMakeLists.txt | 2 + .../elfpatcher/general/ElfConstants.hpp | 4 + .../UnusedNidFilter/IEntryPointCollector.hpp | 4 +- .../relinker/src/analysis/UnusedNidFilter.cpp | 4 +- .../UnusedNidFilter/EntryPointCollector.cpp | 51 ++++++-- .../relinker/tests/test_init_array_roots.py | 110 ++++++++++++++++++ 6 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 core/relinker/relinker/tests/test_init_array_roots.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2d05bbf7d..47cc710e8 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -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 $) + add_test(NAME init_array_roots COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_init_array_roots.py $) else() message(STATUS "Python3 interpreter not found; skipping Python relinker tests") endif() diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index 51936f5dc..b0b6e5f25 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -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; diff --git a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IEntryPointCollector.hpp b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IEntryPointCollector.hpp index 0241329ee..fc9127d18 100644 --- a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IEntryPointCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IEntryPointCollector.hpp @@ -2,6 +2,7 @@ #define RELINKER_ANALYSIS_UNUSED_NID_FILTER_IENTRYPOINTCOLLECTOR_HPP #include +#include #include #include @@ -13,7 +14,8 @@ public: virtual std::vector Collect( const std::vector& elfBytes, VirtualAddress textVaddr, - std::size_t textSize + std::size_t textSize, + const IRelativeRelocationIndex& relativeRelocations ) const = 0; }; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp index e6b796348..e3396f536 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp @@ -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 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); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp index 4840a3bad..8e87a10d8 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp @@ -1,6 +1,7 @@ #include #include #include +#include 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(textSize); } @@ -41,7 +48,8 @@ public: std::vector Collect( const std::vector& 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 loads; + for (std::uint16_t i = 0; i < phCount; ++i) { + std::size_t phPos = static_cast(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(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(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(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(arrayFileOff + k * 8)); + if (target == 0 || target == std::numeric_limits::max()) continue; + addIfInText(target); } }; collectArray(initArrayVa, initArraySz); collectArray(finiArrayVa, finiArraySz); + collectArray(preinitArrayVa, preinitArraySz); break; } diff --git a/core/relinker/relinker/tests/test_init_array_roots.py b/core/relinker/relinker/tests/test_init_array_roots.py new file mode 100644 index 000000000..5f847a8eb --- /dev/null +++ b/core/relinker/relinker/tests/test_init_array_roots.py @@ -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("