feat(relinker): strict unused nids filtering mode

This commit is contained in:
boykopovar 2026-09-09 19:29:25 +03:00
parent 77cfe65b9c
commit 5bf78ab8b4
14 changed files with 1128 additions and 12 deletions

View File

@ -28,6 +28,10 @@ add_executable(relinker
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp
@ -46,3 +50,29 @@ target_include_directories(relinker PRIVATE
)
add_subdirectory(libs)
if(BUILD_TESTING)
add_executable(strict_nid_filter_tests
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/output/SysVDynamicSectionBuilder.cpp
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp
)
target_sources(strict_nid_filter_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp
)
target_include_directories(strict_nid_filter_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/domain/include
${CMAKE_CURRENT_SOURCE_DIR}/codegen/include
${CMAKE_CURRENT_SOURCE_DIR}/relinker/include
)
add_test(NAME strict_nid_filter COMMAND strict_nid_filter_tests)
endif()

View File

@ -68,7 +68,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t
throw CodegenException("Instruction truncated after VEX2 prefix");
}
vexPresent = true;
vexMap = TwoByteOpcodeEscape;
vexMap = 1;
pos += 1;
} else if (opcode == OneByteVex3) {
if (pos >= available) {
@ -117,18 +117,20 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t
bool hasModRm = false;
std::size_t immediateSize = ImmSizeNone;
const bool vectorImmediate = vexMap == 1 && ((opcode >= 0x70 && opcode <= 0x73) || opcode == 0xC2 || opcode == 0xC4 || opcode == 0xC5 || opcode == 0xC6);
if (vexPresent) {
if (vexMap == TwoByteOpcodeEscape && opcode >= VexNoModRmMin && opcode <= VexNoModRmMax) {
if (vexMap == 1 && opcode >= VexNoModRmMin && opcode <= VexNoModRmMax) {
hasModRm = false;
} else {
hasModRm = true;
if (vexMap == Vex3Map0F3A) {
if (vexMap == Vex3Map0F3A || vectorImmediate) {
immediateSize = ImmSize8;
}
}
} else if (evexPresent) {
hasModRm = true;
if (vexMap == EvexMap0F3A) {
if (vexMap == EvexMap0F3A || vectorImmediate) {
immediateSize = ImmSize8;
}
} else if (threeByteEscape) {

View File

@ -106,12 +106,17 @@ int main(const int argc, char* argv[]) {
Relinker::MakeCallSiteResolver(),
std::make_shared<Relinker::ValidationPolicy>(),
std::make_shared<Relinker::SysVDynamicSectionBuilder>(),
Relinker::MakeUnusedNidFilter(),
unusedFilterLevel == 2 ? Relinker::MakeStrictUnusedNidFilter() : Relinker::MakeUnusedNidFilter(),
unusedFilterLevel
);
std::cout << "System: " << (toWindows ? "Windows" : "Linux") << "; unused-filter=" << unusedFilterLevel << "\n";
auto result = pipeline->Relink(sourceBytes);
for (const auto& patch : result.Patches) {
if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset)
throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset);
for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index];
}
if (writeRegistry) {
const std::filesystem::path outFsPath(outputPath);

View File

@ -7,6 +7,7 @@
namespace Relinker {
std::shared_ptr<IUnusedNidFilter> MakeUnusedNidFilter();
std::shared_ptr<IUnusedNidFilter> MakeStrictUnusedNidFilter();
}

View File

@ -0,0 +1,12 @@
#ifndef RELINKER_ANALYSIS_UNUSED_NID_FILTER_EHFRAMEREADER_HPP
#define RELINKER_ANALYSIS_UNUSED_NID_FILTER_EHFRAMEREADER_HPP
#include <relinker/analysis/UnusedNidFilter/StrictReachability.hpp>
namespace Relinker::UnusedNidFilter {
std::vector<StrictCodeRegion> ReadExceptionFunctions(const std::vector<std::uint8_t>& bytes, const std::vector<ProgramHeader>& headers, const std::map<VirtualAddress, VirtualAddress>& pointers, const std::set<VirtualAddress>& importSlots);
}
#endif

View File

@ -0,0 +1,18 @@
#ifndef RELINKER_ANALYSIS_UNUSED_NID_FILTER_PLTCOMPACTOR_HPP
#define RELINKER_ANALYSIS_UNUSED_NID_FILTER_PLTCOMPACTOR_HPP
#include <relinker/domain/RelinkResult.hpp>
namespace Relinker::UnusedNidFilter {
struct CompactedPlt {
std::vector<NidReference> References;
std::vector<RelinkPatch> Patches;
std::uint32_t SlotCount = 0;
};
CompactedPlt CompactPlt(const std::vector<NidReference>& originalReferences, const std::vector<NidReference>& keptReferences, const std::vector<std::uint8_t>& text, VirtualAddress textVaddr, FileByteOffset textOffset, FileByteOffset tableOffset);
}
#endif

View File

@ -0,0 +1,44 @@
#ifndef RELINKER_ANALYSIS_UNUSED_NID_FILTER_STRICTREACHABILITY_HPP
#define RELINKER_ANALYSIS_UNUSED_NID_FILTER_STRICTREACHABILITY_HPP
#include <relinker/domain/Types.hpp>
#include <map>
#include <set>
namespace Relinker::UnusedNidFilter {
struct StrictCodeRegion {
VirtualAddress Begin;
VirtualAddress End;
std::vector<VirtualAddress> ExtraTargets;
};
struct StrictDataRegion {
VirtualAddress Address;
std::vector<std::uint8_t> Bytes;
};
struct StrictReachabilityInput {
std::vector<std::uint8_t> Text;
VirtualAddress TextVaddr = 0;
std::vector<VirtualAddress> Entries;
std::map<VirtualAddress, VirtualAddress> Pointers;
std::set<VirtualAddress> ImportSlots;
std::vector<StrictCodeRegion> Functions;
std::vector<StrictDataRegion> Data;
};
struct StrictReachabilityResult {
std::set<VirtualAddress> Instructions;
std::set<VirtualAddress> ImportSlots;
std::size_t IndirectTransfers = 0;
std::size_t LiveRegions = 0;
std::size_t TotalRegions = 0;
std::size_t AddressTakenRoots = 0;
};
StrictReachabilityResult AnalyzeStrictReachability(const StrictReachabilityInput& input);
}
#endif

View File

@ -6,11 +6,17 @@
namespace Relinker {
struct RelinkPatch {
FileByteOffset Offset;
std::vector<std::uint8_t> Bytes;
};
struct RelinkResult {
std::vector<CallRegistryEntry> RegistryEntries;
std::vector<ProgramHeader> OriginalHeaders;
SysVDynamicSection DynamicSection;
VirtualAddress OriginalPltGotVaddr;
std::vector<RelinkPatch> Patches;
};
}

View File

@ -0,0 +1,219 @@
#include <relinker/analysis/UnusedNidFilter/EhFrameReader.hpp>
#include <cstring>
#include <limits>
namespace Relinker::UnusedNidFilter {
namespace {
struct Cie {
std::uint8_t PointerEncoding = 0;
std::uint8_t LsdaEncoding = 0xFF;
bool Augmented = false;
VirtualAddress Personality = 0;
};
class Reader {
public:
Reader(const std::vector<std::uint8_t>& bytes, const std::vector<ProgramHeader>& headers, const std::map<VirtualAddress, VirtualAddress>& pointers, const std::set<VirtualAddress>& importSlots) : bytes(bytes), headers(headers), pointers(pointers), importSlots(importSlots) {}
std::vector<StrictCodeRegion> Read() {
std::vector<StrictCodeRegion> result;
for (const auto& segment : headers) {
if (segment.Type != 0x6474E550 || segment.FileSize == 0) continue;
auto position = segment.MappedAddress;
const auto version = read<std::uint8_t>(position);
const auto frameEncoding = read<std::uint8_t>(position);
const auto countEncoding = read<std::uint8_t>(position);
const auto tableEncoding = read<std::uint8_t>(position);
if (version != 1 || frameEncoding == 0xFF || countEncoding == 0xFF || tableEncoding == 0xFF)
throw RelinkerException("Strict filter: unsupported EH frame header", segment.MappedAddress);
encoded(position, frameEncoding, segment.MappedAddress);
const auto count = encoded(position, countEncoding, segment.MappedAddress);
if (count > segment.FileSize / 2) throw RelinkerException("Strict filter: invalid EH frame count", position);
for (std::uint64_t index = 0; index < count; ++index) {
const auto begin = encoded(position, tableEncoding, segment.MappedAddress);
const auto fde = encoded(position, tableEncoding, segment.MappedAddress);
if (position > segment.MappedAddress + segment.FileSize) throw RelinkerException("Strict filter: EH frame table exceeds segment", position);
result.push_back(readFde(begin, fde));
}
}
return result;
}
private:
const std::vector<std::uint8_t>& bytes;
const std::vector<ProgramHeader>& headers;
const std::map<VirtualAddress, VirtualAddress>& pointers;
const std::set<VirtualAddress>& importSlots;
std::map<VirtualAddress, Cie> cies;
template<typename TValue>
TValue read(VirtualAddress& address) const {
for (const auto& segment : headers) {
if (segment.Type != 1 || address < segment.MappedAddress || address - segment.MappedAddress > segment.FileSize || sizeof(TValue) > segment.FileSize - (address - segment.MappedAddress)) continue;
const auto offset = segment.Offset + address - segment.MappedAddress;
if (offset > bytes.size() || sizeof(TValue) > bytes.size() - offset) throw RelinkerException("Strict filter: unwind data exceeds file", address);
TValue value;
std::memcpy(&value, bytes.data() + offset, sizeof(value));
address += sizeof(value);
return value;
}
throw RelinkerException("Strict filter: unwind address is not file-backed", address);
}
std::uint64_t leb(VirtualAddress& position, bool signedValue = false) const {
std::uint64_t value = 0;
unsigned shift = 0;
for (;;) {
const auto byte = read<std::uint8_t>(position);
if (shift == 63 && ((byte & 0x7F) != 0 && (byte & 0x7F) != 1 && (!signedValue || (byte & 0x7F) != 0x7F)))
throw RelinkerException("Strict filter: overflowing LEB128 value", position);
value |= static_cast<std::uint64_t>(byte & 0x7F) << shift;
shift += 7;
if ((byte & 0x80) == 0) {
if (signedValue && (byte & 0x40) != 0 && shift < 64) value |= (~std::uint64_t{0}) << shift;
return value;
}
if (shift >= 64) throw RelinkerException("Strict filter: unterminated LEB128 value", position);
}
}
std::uint64_t encoded(VirtualAddress& position, std::uint8_t encoding, VirtualAddress dataBase = 0, VirtualAddress functionBase = 0) const {
const auto field = position;
std::uint64_t value;
switch (encoding & 0x0F) {
case 0: value = read<std::uint64_t>(position); break;
case 1: value = leb(position); break;
case 2: value = read<std::uint16_t>(position); break;
case 3: value = read<std::uint32_t>(position); break;
case 4: value = read<std::uint64_t>(position); break;
case 9: value = leb(position, true); break;
case 10: value = static_cast<std::uint64_t>(static_cast<std::int64_t>(read<std::int16_t>(position))); break;
case 11: value = static_cast<std::uint64_t>(static_cast<std::int64_t>(read<std::int32_t>(position))); break;
case 12: value = static_cast<std::uint64_t>(read<std::int64_t>(position)); break;
default: throw RelinkerException("Strict filter: unsupported unwind pointer format", field);
}
if (value == 0) return 0;
switch (encoding & 0x70) {
case 0: break;
case 0x10: value += field; break;
case 0x30: value += dataBase; break;
case 0x40: value += functionBase; break;
default: throw RelinkerException("Strict filter: unsupported unwind pointer base", field);
}
if ((encoding & 0x80) != 0 && !importSlots.contains(value)) {
const auto pointer = pointers.find(value);
if (pointer != pointers.end()) value = pointer->second;
else value = read<std::uint64_t>(value);
}
return value;
}
Cie readCie(VirtualAddress address) {
if (const auto found = cies.find(address); found != cies.end()) return found->second;
auto position = address;
const auto length = read<std::uint32_t>(position);
if (length == 0xFFFFFFFF || length < 8) throw RelinkerException("Strict filter: unsupported CIE record size", address);
const auto end = position + length;
if (read<std::uint32_t>(position) != 0) throw RelinkerException("Strict filter: invalid CIE identifier", address);
const auto version = read<std::uint8_t>(position);
if (version != 1 && version != 3 && version != 4) throw RelinkerException("Strict filter: unsupported CIE version", address);
std::string augmentation;
for (;;) {
if (position >= end) throw RelinkerException("Strict filter: unterminated CIE augmentation", address);
const auto character = read<std::uint8_t>(position);
if (character == 0) break;
augmentation.push_back(static_cast<char>(character));
}
if (version == 4 && (read<std::uint8_t>(position) != 8 || read<std::uint8_t>(position) != 0))
throw RelinkerException("Strict filter: unsupported CIE address size", address);
leb(position);
leb(position, true);
if (version == 1) read<std::uint8_t>(position);
else leb(position);
Cie cie;
if (!augmentation.empty()) {
if (augmentation.front() != 'z') throw RelinkerException("Strict filter: unsupported CIE augmentation", address);
cie.Augmented = true;
const auto size = leb(position);
const auto augmentationEnd = position + size;
if (augmentationEnd > end) throw RelinkerException("Strict filter: CIE augmentation exceeds record", address);
for (std::size_t index = 1; index < augmentation.size(); ++index) {
switch (augmentation[index]) {
case 'L': cie.LsdaEncoding = read<std::uint8_t>(position); break;
case 'R': cie.PointerEncoding = read<std::uint8_t>(position); break;
case 'P': {
const auto encoding = read<std::uint8_t>(position);
cie.Personality = encoded(position, encoding);
break;
}
case 'S': break;
default: throw RelinkerException("Strict filter: unknown CIE augmentation field", position);
}
}
if (position != augmentationEnd) throw RelinkerException("Strict filter: CIE augmentation size mismatch", address);
}
if (position > end) throw RelinkerException("Strict filter: CIE fields exceed record", address);
cies.emplace(address, cie);
return cie;
}
StrictCodeRegion readFde(VirtualAddress expectedBegin, VirtualAddress address) {
auto position = address;
const auto length = read<std::uint32_t>(position);
if (length == 0xFFFFFFFF || length < 8) throw RelinkerException("Strict filter: unsupported FDE record size", address);
const auto end = position + length;
const auto cieField = position;
const auto cieOffset = read<std::uint32_t>(position);
if (cieOffset > cieField) throw RelinkerException("Strict filter: invalid FDE CIE offset", address);
const auto cie = readCie(cieField - cieOffset);
const auto begin = encoded(position, cie.PointerEncoding);
const auto size = encoded(position, cie.PointerEncoding & 0x0F);
if (begin != expectedBegin || size == 0 || size > std::numeric_limits<VirtualAddress>::max() - begin)
throw RelinkerException("Strict filter: inconsistent FDE function range", address);
StrictCodeRegion region{begin, begin + size, {}};
if (cie.Personality != 0) region.ExtraTargets.push_back(cie.Personality);
if (cie.Augmented) {
const auto augmentationSize = leb(position);
const auto augmentationEnd = position + augmentationSize;
if (augmentationEnd > end) throw RelinkerException("Strict filter: FDE augmentation exceeds record", address);
if (cie.LsdaEncoding != 0xFF) {
const auto lsda = encoded(position, cie.LsdaEncoding, 0, begin);
if (lsda != 0) readLsda(lsda, region);
}
if (position > augmentationEnd) throw RelinkerException("Strict filter: FDE augmentation size mismatch", address);
}
if (position > end) throw RelinkerException("Strict filter: FDE fields exceed record", address);
return region;
}
void readLsda(VirtualAddress address, StrictCodeRegion& region) const {
auto position = address;
const auto landingEncoding = read<std::uint8_t>(position);
const auto landingBase = landingEncoding == 0xFF ? region.Begin : encoded(position, landingEncoding, 0, region.Begin);
const auto typeEncoding = read<std::uint8_t>(position);
if (typeEncoding != 0xFF) leb(position);
const auto callEncoding = read<std::uint8_t>(position);
if ((callEncoding & 0xF0) != 0) throw RelinkerException("Strict filter: unsupported LSDA call-site encoding", address);
const auto tableSize = leb(position);
if (tableSize > bytes.size()) throw RelinkerException("Strict filter: invalid LSDA table size", address);
const auto tableEnd = position + tableSize;
while (position < tableEnd) {
encoded(position, callEncoding);
encoded(position, callEncoding);
const auto landing = encoded(position, callEncoding);
leb(position);
if (landing != 0) region.ExtraTargets.push_back(landingBase + landing);
}
if (position != tableEnd) throw RelinkerException("Strict filter: LSDA call-site table size mismatch", address);
}
};
}
std::vector<StrictCodeRegion> ReadExceptionFunctions(const std::vector<std::uint8_t>& bytes, const std::vector<ProgramHeader>& headers, const std::map<VirtualAddress, VirtualAddress>& pointers, const std::set<VirtualAddress>& importSlots) {
return Reader(bytes, headers, pointers, importSlots).Read();
}
}

View File

@ -0,0 +1,55 @@
#include <relinker/analysis/UnusedNidFilter/PltCompactor.hpp>
#include <cstring>
#include <limits>
#include <map>
#include <set>
namespace Relinker::UnusedNidFilter {
CompactedPlt CompactPlt(const std::vector<NidReference>& originalReferences, const std::vector<NidReference>& keptReferences, const std::vector<std::uint8_t>& text, VirtualAddress textVaddr, FileByteOffset textOffset, FileByteOffset tableOffset) {
CompactedPlt result;
result.References = keptReferences;
std::map<VirtualAddress, std::uint32_t> originalSlots;
for (const auto& reference : originalReferences) {
if (reference.RelocationTypeValue != 7) continue;
if (reference.RelocationTableOffset < tableOffset || (reference.RelocationTableOffset - tableOffset) % 24 != 0 || (reference.RelocationTableOffset - tableOffset) / 24 > std::numeric_limits<std::uint32_t>::max())
throw RelinkerException("Strict filter: invalid original PLT relocation index", reference.RelocationTableOffset);
if (!originalSlots.emplace(reference.RelocationAddress, static_cast<std::uint32_t>((reference.RelocationTableOffset - tableOffset) / 24)).second)
throw RelinkerException("Strict filter: duplicate original PLT slot", reference.RelocationAddress);
}
std::map<VirtualAddress, std::uint32_t> newSlots;
for (auto& reference : result.References) {
if (reference.RelocationTypeValue != 7) continue;
if (!originalSlots.contains(reference.RelocationAddress) || !newSlots.emplace(reference.RelocationAddress, result.SlotCount).second)
throw RelinkerException("Strict filter: invalid retained PLT slot", reference.RelocationAddress);
reference.RelocationTableOffset = tableOffset + static_cast<std::uint64_t>(result.SlotCount++) * 24;
}
std::set<VirtualAddress> found;
for (std::size_t offset = 0; offset + 16 <= text.size(); ++offset) {
if (text[offset] != 0xFF || text[offset + 1] != 0x25) continue;
std::int32_t displacement;
std::memcpy(&displacement, text.data() + offset + 2, sizeof(displacement));
const auto slot = textVaddr + offset + 6 + static_cast<std::uint64_t>(static_cast<std::int64_t>(displacement));
const auto original = originalSlots.find(slot);
if (original == originalSlots.end()) continue;
if (text[offset + 6] != 0x68 || text[offset + 11] != 0xE9) continue;
std::uint32_t index;
std::memcpy(&index, text.data() + offset + 7, sizeof(index));
if (index != original->second) throw RelinkerException("Strict filter: PLT thunk index disagrees with its relocation", textVaddr + offset);
found.insert(slot);
if (const auto kept = newSlots.find(slot); kept != newSlots.end()) {
std::vector<std::uint8_t> bytes(sizeof(std::uint32_t));
std::memcpy(bytes.data(), &kept->second, bytes.size());
result.Patches.push_back({textOffset + offset + 7, std::move(bytes)});
} else {
result.Patches.push_back({textOffset + offset, {0x0F, 0x0B}});
result.Patches.push_back({textOffset + offset + 6, {0x0F, 0x0B}});
}
offset += 15;
}
for (const auto& [slot, index] : originalSlots)
if (!found.contains(slot)) throw RelinkerException("Strict filter: canonical PLT thunk was not found", slot);
return result;
}
}

View File

@ -0,0 +1,205 @@
#include <relinker/analysis/UnusedNidFilter.hpp>
#include <relinker/analysis/UnusedNidFilter/StrictReachability.hpp>
#include <relinker/analysis/UnusedNidFilter/EhFrameReader.hpp>
#include <relinker/parsing/ElfReader.hpp>
#include <cstring>
#include <iostream>
#include <limits>
#include <optional>
namespace Relinker {
namespace {
class StrictImage {
public:
explicit StrictImage(const std::vector<std::uint8_t>& bytes) : bytes(bytes), reader(bytes) {
if (bytes.size() < 64 || bytes[4] != 2 || bytes[5] != 1 || bytes[6] != 1)
throw RelinkerException("Strict filter: expected a little-endian ELF64 image");
const auto header = reader.ReadHeader();
if (header.Machine != 62 || header.ProgramHeaderEntrySize != 56)
throw RelinkerException("Strict filter: unsupported ELF machine or program header format");
requireRange(header.ProgramHeaderOffset, static_cast<std::uint64_t>(header.ProgramHeaderCount) * 56);
headers = reader.ReadProgramHeaders();
for (const auto& segment : headers) {
requireRange(segment.Offset, segment.FileSize);
if (segment.MemorySize > std::numeric_limits<VirtualAddress>::max() - segment.MappedAddress)
throw RelinkerException("Strict filter: segment address overflow", segment.MappedAddress);
if (segment.Type == 1 && segment.FileSize > segment.MemorySize)
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) {
const auto tag = read<std::int64_t>(segment.Offset + offset);
if (tag == 0) {
terminated = true;
break;
}
if (tag == 1 || (tag >= 0x61000000 && tag != 0x6100002F && tag != 0x61000031 && tag != 0x61000033 && tag != 0x61000039 && tag != 0x6100003B && tag != 0x6100003F)) continue;
if (!tags.emplace(tag, read<std::uint64_t>(segment.Offset + offset + 8)).second)
throw RelinkerException("Strict filter: duplicate dynamic tag", segment.Offset + offset);
}
if (!terminated) throw RelinkerException("Strict filter: unterminated dynamic segment", segment.Offset);
}
}
UnusedNidFilter::StrictReachabilityInput Build(const std::vector<NidReference>& references, const std::vector<std::uint8_t>& text, VirtualAddress textVaddr) const {
UnusedNidFilter::StrictReachabilityInput input{text, textVaddr, {reader.ReadHeader().EntryPoint}, {}, {}, {}, {}};
std::size_t codeSegments = 0;
for (const auto& segment : headers) {
if (segment.Type == 1 && (segment.Flags & 1) != 0) {
++codeSegments;
if (segment.MappedAddress != textVaddr || segment.FileSize != text.size() || segment.MemorySize != segment.FileSize || (segment.Flags & 2) != 0)
throw RelinkerException("Strict filter: unsupported executable segment layout", segment.MappedAddress);
}
}
if (codeSegments != 1) throw RelinkerException("Strict filter: requires exactly one immutable code segment");
for (const auto& reference : references) {
if (reference.RelocationTypeValue != 1 && reference.RelocationTypeValue != 6 && reference.RelocationTypeValue != 7)
throw RelinkerException("Strict filter: unsupported imported relocation", reference.RelocationAddress);
if (!input.ImportSlots.insert(reference.RelocationAddress).second)
throw RelinkerException("Strict filter: duplicate imported slot", reference.RelocationAddress);
}
const auto rela = table(7, 0x6100002F);
const auto relaSize = value(8, 0x61000031);
if (rela.has_value() != relaSize.has_value()) throw RelinkerException("Strict filter: incomplete relocation table metadata");
if (rela) {
if (value(9, 0x61000033) != 24 || *relaSize % 24 != 0)
throw RelinkerException("Strict filter: invalid RELA entry size");
requireRange(*rela, *relaSize);
for (std::uint64_t offset = 0; offset < *relaSize; offset += 24) {
const auto slot = read<std::uint64_t>(*rela + offset);
const auto info = read<std::uint64_t>(*rela + offset + 8);
if (static_cast<std::uint32_t>(info) != 8) continue;
if (info >> 32) throw RelinkerException("Strict filter: relative relocation has a symbol", slot);
const auto target = read<std::uint64_t>(*rela + offset + 16);
if (input.ImportSlots.contains(slot) || !input.Pointers.emplace(slot, target).second)
throw RelinkerException("Strict filter: overlapping pointer relocations", slot);
}
}
for (const auto tag : {12, 13}) {
if (const auto entry = value(tag, 0x60000000 + tag); entry && *entry != 0) input.Entries.push_back(*entry);
}
for (const auto& [addressTag, sizeTag] : {std::pair{25, 27}, std::pair{26, 28}, std::pair{32, 33}}) {
const auto array = value(addressTag, 0x60000000 + addressTag);
const auto size = value(sizeTag, 0x60000000 + sizeTag);
if (array.has_value() != size.has_value()) throw RelinkerException("Strict filter: incomplete constructor array metadata");
if (!array) continue;
if (*size % 8 != 0) throw RelinkerException("Strict filter: invalid constructor array size", *array);
for (std::uint64_t offset = 0; offset < *size; offset += 8) {
const auto slot = *array + offset;
const auto position = input.Pointers.find(slot);
const auto entry = position == input.Pointers.end() ? read<std::uint64_t>(fileOffset(slot, 8)) : position->second;
if (entry != 0 && entry != std::numeric_limits<std::uint64_t>::max()) input.Entries.push_back(entry);
}
}
const auto symbols = table(6, 0x61000039);
if (symbols) {
if (value(11, 0x6100003B) != 24) throw RelinkerException("Strict filter: invalid symbol entry size");
std::uint64_t count = 0;
if (const auto size = tags.find(0x6100003F); size != tags.end()) {
if (size->second % 24 != 0) throw RelinkerException("Strict filter: invalid symbol table size");
count = size->second / 24;
} else if (const auto hash = tags.find(4); hash != tags.end()) {
count = read<std::uint32_t>(fileOffset(hash->second, 8) + 4);
} else {
throw RelinkerException("Strict filter: cannot establish dynamic symbol table bounds");
}
if (count > bytes.size() / 24) throw RelinkerException("Strict filter: symbol count exceeds image size");
requireRange(*symbols, count * 24);
for (std::uint64_t index = 0; index < count; ++index) {
const auto position = *symbols + index * 24;
const auto section = read<std::uint16_t>(position + 6);
const auto info = read<std::uint8_t>(position + 4);
const auto type = info & 15;
if (type == 10) throw RelinkerException("Strict filter: IFUNC resolver is not modeled", position);
if (section == 0 || (info >> 4) == 0 || type != 2) continue;
input.Entries.push_back(read<std::uint64_t>(position + 8));
}
}
input.Functions = UnusedNidFilter::ReadExceptionFunctions(bytes, headers, input.Pointers, input.ImportSlots);
for (const auto& segment : headers) {
if (segment.Type != 1 || (segment.Flags & 1) != 0 || (segment.Flags & 6) == 0 || segment.FileSize == 0) continue;
input.Data.push_back({segment.MappedAddress, reader.ReadSegment(segment)});
}
return input;
}
private:
const std::vector<std::uint8_t>& bytes;
ElfReader reader;
std::vector<ProgramHeader> headers;
std::map<std::int64_t, std::uint64_t> tags;
void requireRange(std::uint64_t offset, std::uint64_t size) const {
if (offset > bytes.size() || size > bytes.size() - offset) throw RelinkerException("Strict filter: file range out of bounds", offset);
}
template<typename TValue>
TValue read(std::uint64_t offset) const {
requireRange(offset, sizeof(TValue));
TValue result;
std::memcpy(&result, bytes.data() + offset, sizeof(result));
return result;
}
std::uint64_t fileOffset(VirtualAddress address, std::uint64_t size) const {
for (const auto& segment : headers)
if (segment.Type == 1 && address >= segment.MappedAddress && address - segment.MappedAddress <= segment.FileSize && size <= segment.FileSize - (address - segment.MappedAddress))
return segment.Offset + address - segment.MappedAddress;
throw RelinkerException("Strict filter: virtual range is not file-backed", address);
}
std::optional<std::uint64_t> value(std::int64_t standardTag, std::int64_t osTag) const {
const auto standard = tags.find(standardTag);
const auto os = tags.find(osTag);
if (standard != tags.end() && os != tags.end()) throw RelinkerException("Strict filter: conflicting standard and OS dynamic tags");
if (standard != tags.end()) return standard->second;
if (os != tags.end()) return os->second;
return std::nullopt;
}
std::optional<std::uint64_t> table(std::int64_t standardTag, std::int64_t osTag) const {
const auto address = value(standardTag, osTag);
if (!address) return std::nullopt;
return tags.contains(osTag) ? *address : fileOffset(*address, 0);
}
};
class StrictNidFilter : public IUnusedNidFilter {
public:
std::vector<NidReference> Filter(const std::vector<NidReference>& nidRefs, const std::vector<std::uint8_t>& elfBytes, const std::vector<std::uint8_t>& textSection, VirtualAddress textVAddr) override {
const StrictImage image(elfBytes);
const auto input = image.Build(nidRefs, textSection, textVAddr);
const auto analysis = UnusedNidFilter::AnalyzeStrictReachability(input);
std::vector<NidReference> nonPlt;
for (const auto& reference : nidRefs)
if (reference.RelocationTypeValue != 7) nonPlt.push_back(reference);
const auto originalNonPltCount = nonPlt.size();
if (!nonPlt.empty()) nonPlt = MakeUnusedNidFilter()->Filter(nonPlt, elfBytes, textSection, textVAddr);
std::set<VirtualAddress> nonPltSlots;
for (const auto& reference : nonPlt) nonPltSlots.insert(reference.RelocationAddress);
std::cout << "CFG/GOT filtering: " << originalNonPltCount << " -> " << nonPlt.size() << "; filtered=" << originalNonPltCount - nonPlt.size() << "\n";
std::vector<NidReference> result;
for (const auto& reference : nidRefs) {
if (reference.RelocationTypeValue == 7 ? analysis.ImportSlots.contains(reference.RelocationAddress) : nonPltSlots.contains(reference.RelocationAddress)) result.push_back(reference);
}
std::cout << "Strict reachability: " << analysis.Instructions.size() << " instructions; conservative indirect transfers=" << analysis.IndirectTransfers << "\n";
std::cout << "Function graph: " << analysis.LiveRegions << "/" << analysis.TotalRegions << " live regions; address-taken roots=" << analysis.AddressTakenRoots << "; unwind functions=" << input.Functions.size() << "\n";
return result;
}
};
}
std::shared_ptr<IUnusedNidFilter> MakeStrictUnusedNidFilter() {
return std::make_shared<StrictNidFilter>();
}
}

View File

@ -0,0 +1,224 @@
#include <relinker/analysis/UnusedNidFilter/StrictReachability.hpp>
#include <codegen/x86/X64InstructionDecoder.hpp>
#include <codegen/CodegenException.hpp>
#include <algorithm>
#include <cstring>
#include <deque>
#include <limits>
#include <iterator>
namespace Relinker::UnusedNidFilter {
namespace {
struct Instruction {
VirtualAddress Address;
Codegen::DecodedInstructionInfo Info;
};
struct Region {
VirtualAddress Begin;
VirtualAddress End;
std::set<VirtualAddress> Edges;
std::set<VirtualAddress> Imports;
std::vector<VirtualAddress> Instructions;
std::size_t IndirectTransfers = 0;
};
bool endsFlow(Codegen::ControlFlowKind kind) {
using enum Codegen::ControlFlowKind;
return kind == Return || kind == Trap || kind == UnconditionalJump || kind == IndirectJump;
}
class Analyzer {
public:
explicit Analyzer(const StrictReachabilityInput& input) : input(input) {}
StrictReachabilityResult Run() {
if (input.Text.empty() || input.Text.size() > std::numeric_limits<VirtualAddress>::max() - input.TextVaddr)
throw RelinkerException("Strict filter: invalid text range");
if (input.Entries.empty()) throw RelinkerException("Strict filter: no entry points");
buildRegions();
buildEdges();
for (const auto& [slot, target] : input.Pointers) addAddressTaken(target);
collectDataPointers();
collectRelativeTables();
std::deque<VirtualAddress> pending;
std::set<VirtualAddress> live;
const auto enqueue = [&](VirtualAddress target) {
if (input.ImportSlots.contains(target)) result.ImportSlots.insert(target);
if (!isCode(target)) return;
const auto begin = owner(target).Begin;
if (live.insert(begin).second) pending.push_back(begin);
};
for (const auto entry : input.Entries) {
if (!isCode(entry)) throw RelinkerException("Strict filter: entry point lies outside code", entry);
enqueue(entry);
}
for (const auto root : addressTaken) enqueue(root);
while (!pending.empty()) {
const auto begin = pending.front();
pending.pop_front();
const auto& region = regions.at(begin);
result.Instructions.insert(region.Instructions.begin(), region.Instructions.end());
result.ImportSlots.insert(region.Imports.begin(), region.Imports.end());
result.IndirectTransfers += region.IndirectTransfers;
for (const auto target : region.Edges) enqueue(target);
}
result.LiveRegions = live.size();
result.TotalRegions = regions.size();
result.AddressTakenRoots = addressTaken.size();
return result;
}
private:
const StrictReachabilityInput& input;
Codegen::X64InstructionDecoder decoder;
std::map<VirtualAddress, Region> regions;
std::set<VirtualAddress> addressTaken;
std::set<VirtualAddress> tableBases;
StrictReachabilityResult result;
bool isCode(VirtualAddress address) const {
return address >= input.TextVaddr && address - input.TextVaddr < input.Text.size();
}
Region& owner(VirtualAddress address) {
auto position = regions.upper_bound(address);
if (position == regions.begin() || std::prev(position)->second.End <= address)
throw RelinkerException("Strict filter: code address has no region", address);
return std::prev(position)->second;
}
Instruction decode(VirtualAddress address, VirtualAddress end) const {
const auto offset = static_cast<std::size_t>(address - input.TextVaddr);
Codegen::DecodedInstructionInfo info;
try {
info = decoder.DecodeInstruction(input.Text.data() + offset, static_cast<std::size_t>(end - address));
} catch (const Codegen::CodegenException& error) {
throw RelinkerException(std::string("Strict filter: ") + error.what(), address);
}
if (info.Length == 0 || info.Length > end - address) throw RelinkerException("Strict filter: instruction crosses a region boundary", address);
return {address, info};
}
bool isZeroPadding(VirtualAddress begin, VirtualAddress end) const {
const auto first = input.Text.begin() + static_cast<std::ptrdiff_t>(begin - input.TextVaddr);
const auto last = input.Text.begin() + static_cast<std::ptrdiff_t>(end - input.TextVaddr);
return std::all_of(first, last, [](std::uint8_t byte) { return byte == 0; });
}
void addGap(VirtualAddress begin, VirtualAddress end) {
VirtualAddress regionBegin = begin;
for (auto address = begin; address < end;) {
if (isZeroPadding(address, end)) break;
const auto instruction = decode(address, end);
address += instruction.Info.Length;
if (endsFlow(instruction.Info.FlowKind)) {
regions.emplace(regionBegin, Region{regionBegin, address, {}, {}, {}});
regionBegin = address;
}
}
if (regionBegin != end) regions.emplace(regionBegin, Region{regionBegin, end, {}, {}, {}});
}
void buildRegions() {
auto functions = input.Functions;
std::sort(functions.begin(), functions.end(), [](const auto& left, const auto& right) { return left.Begin < right.Begin; });
auto previousEnd = input.TextVaddr;
const auto textEnd = input.TextVaddr + input.Text.size();
for (const auto& function : functions) {
if (function.Begin < previousEnd || function.End <= function.Begin || function.End > textEnd)
throw RelinkerException("Strict filter: invalid or overlapping function range", function.Begin);
addGap(previousEnd, function.Begin);
Region region{function.Begin, function.End, {}, {}, {}};
region.Edges.insert(function.ExtraTargets.begin(), function.ExtraTargets.end());
regions.emplace(function.Begin, std::move(region));
previousEnd = function.End;
}
addGap(previousEnd, textEnd);
}
void addAddressTaken(VirtualAddress target) {
if (isCode(target) || input.ImportSlots.contains(target)) addressTaken.insert(target);
}
void buildEdges() {
for (auto& [begin, region] : regions) {
Codegen::ControlFlowKind lastFlow = Codegen::ControlFlowKind::Sequential;
for (auto address = begin; address < region.End;) {
if (isZeroPadding(address, region.End)) {
lastFlow = Codegen::ControlFlowKind::Sequential;
break;
}
const auto instruction = decode(address, region.End);
const auto& info = instruction.Info;
const auto next = address + info.Length;
const auto offset = static_cast<std::size_t>(address - input.TextVaddr);
region.Instructions.push_back(address);
if (info.HasBranchTarget && !info.HasRipRelativeDisp) {
const auto target = next + static_cast<VirtualAddress>(info.BranchDisp);
if (!isCode(target)) throw RelinkerException("Strict filter: direct branch leaves code", address);
region.Edges.insert(target);
}
if (info.HasRipRelativeDisp) {
std::int32_t displacement;
std::memcpy(&displacement, input.Text.data() + offset + info.RipRelativeDispOffset, sizeof(displacement));
const auto target = next + static_cast<VirtualAddress>(static_cast<std::int64_t>(displacement));
if (input.ImportSlots.contains(target)) region.Imports.insert(target);
if (!info.IsTwoByteOpcode && info.Opcode == 0x8D) {
addAddressTaken(target);
tableBases.insert(target);
}
if (const auto pointer = input.Pointers.find(target); pointer != input.Pointers.end()) region.Edges.insert(pointer->second);
}
using enum Codegen::ControlFlowKind;
if (info.FlowKind == IndirectCall || info.FlowKind == IndirectJump || ((info.FlowKind == Call || info.FlowKind == UnconditionalJump) && info.HasRipRelativeDisp)) ++region.IndirectTransfers;
if (!info.IsTwoByteOpcode && ((info.Opcode >= 0xB8 && info.Opcode <= 0xBF) || info.Opcode == 0x68 || info.Opcode == 0xC7)) {
for (const auto width : {4u, 8u}) {
if (info.Length <= width) continue;
std::uint64_t value = 0;
std::memcpy(&value, input.Text.data() + offset + info.Length - width, width);
addAddressTaken(value);
}
}
lastFlow = info.FlowKind;
address = next;
}
if (!endsFlow(lastFlow) && isCode(region.End)) region.Edges.insert(region.End);
}
}
void collectDataPointers() {
for (const auto& data : input.Data) {
for (std::size_t offset = 0; offset + 8 <= data.Bytes.size(); ++offset) {
std::uint64_t target;
std::memcpy(&target, data.Bytes.data() + offset, sizeof(target));
addAddressTaken(target);
}
}
}
void collectRelativeTables() {
for (const auto base : tableBases) {
for (const auto& data : input.Data) {
if (base < data.Address || base - data.Address >= data.Bytes.size()) continue;
for (auto offset = static_cast<std::size_t>(base - data.Address); offset + 4 <= data.Bytes.size(); offset += 4) {
std::int32_t displacement;
std::memcpy(&displacement, data.Bytes.data() + offset, sizeof(displacement));
const auto target = base + static_cast<std::uint64_t>(static_cast<std::int64_t>(displacement));
if (!isCode(target)) break;
addAddressTaken(target);
}
}
}
}
};
}
StrictReachabilityResult AnalyzeStrictReachability(const StrictReachabilityInput& input) {
return Analyzer(input).Run();
}
}

View File

@ -1,5 +1,6 @@
#include <relinker/pipeline/RelinkerPipeline.hpp>
#include <relinker/analysis/ValidationPolicy.hpp>
#include <relinker/analysis/UnusedNidFilter/PltCompactor.hpp>
#include <sstream>
#include <iostream>
#include <cstring>
@ -197,9 +198,14 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
static constexpr std::uint32_t R_X86_64_JUMP_SLOT = 7;
const std::size_t originalNidCount = nidRefs.size();
const auto originalNidRefs = nidRefs;
std::cout << "NID input: " << originalNidCount << " references\n";
if (unusedFilterLevel != 0) {
if (unusedFilterLevel == 2) {
nidRefs = _unusedNidFilter->Filter(nidRefs, raw, textSection, textVAddr);
if (nidRefs.size() > originalNidCount) throw RelinkerException("Strict NID filter increased the reference count");
std::cout << "Strict filtering total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n";
} else if (unusedFilterLevel == 1) {
std::vector<NidReference> pltRefs;
std::vector<NidReference> nonPltRefs;
for (const auto& ref : nidRefs) {
@ -215,9 +221,6 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
if (nonPltRefs.size() > nonPltCount) throw RelinkerException("Unused NID filter increased the reference count");
std::cout << "CFG/GOT filtering: " << nonPltCount << " -> " << nonPltRefs.size() << "; filtered=" << nonPltCount - nonPltRefs.size() << "\n";
if (unusedFilterLevel == 2)
std::cout << "Strict filtering: " << nonPltRefs.size() << " -> " << nonPltRefs.size() << "; filtered=0 (reserved; currently identical to level 1)\n";
nidRefs.clear();
nidRefs.reserve(pltRefs.size() + nonPltRefs.size());
for (auto& ref : pltRefs) nidRefs.push_back(std::move(ref));
@ -228,8 +231,17 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
std::cout << "NID total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n";
auto dynSection = _dynamicSectionBuilder->BuildDynamicSection(
nidRefs, neededLibraries, dynJmpRelOffset, static_cast<std::uint32_t>(dynJmpRelSize / relaEntSize));
auto dynamicRefs = nidRefs;
std::vector<RelinkPatch> patches;
auto pltCount = static_cast<std::uint32_t>(dynJmpRelSize / relaEntSize);
if (unusedFilterLevel == 2) {
auto compacted = UnusedNidFilter::CompactPlt(originalNidRefs, nidRefs, textSection, textVAddr, _elfReader->TranslateVirtualAddress(textVAddr), dynJmpRelOffset);
dynamicRefs = std::move(compacted.References);
patches = std::move(compacted.Patches);
std::cout << "PLT compaction: " << pltCount << " -> " << compacted.SlotCount << "; lazy binding preserved\n";
pltCount = compacted.SlotCount;
}
auto dynSection = _dynamicSectionBuilder->BuildDynamicSection(dynamicRefs, neededLibraries, dynJmpRelOffset, pltCount);
static constexpr std::uint32_t R_X86_64_RELATIVE = 8;
@ -280,7 +292,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
entries.push_back(std::move(entry));
}
return RelinkResult{std::move(entries), std::move(programHeaders), std::move(dynSection), gotVAddr};
return RelinkResult{std::move(entries), std::move(programHeaders), std::move(dynSection), gotVAddr, std::move(patches)};
}
}

View File

@ -0,0 +1,283 @@
#include <relinker/analysis/UnusedNidFilter.hpp>
#include <relinker/analysis/UnusedNidFilter/StrictReachability.hpp>
#include <relinker/output/SysVDynamicSectionBuilder.hpp>
#include <relinker/analysis/UnusedNidFilter/EhFrameReader.hpp>
#include <relinker/analysis/UnusedNidFilter/PltCompactor.hpp>
#include <codegen/x86/X64InstructionDecoder.hpp>
#include <algorithm>
#include <cstring>
#include <functional>
#include <iostream>
#include <stdexcept>
namespace {
using Relinker::UnusedNidFilter::AnalyzeStrictReachability;
using Relinker::UnusedNidFilter::StrictReachabilityInput;
void require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void requireFailure(const std::function<void()>& operation, const char* message) {
try {
operation();
} catch (const Relinker::RelinkerException&) {
return;
}
throw std::runtime_error(message);
}
template<typename TValue>
void write(std::vector<std::uint8_t>& bytes, std::size_t offset, TValue value) {
if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Test fixture write is out of bounds");
std::memcpy(bytes.data() + offset, &value, sizeof(value));
}
StrictReachabilityInput fixture() {
StrictReachabilityInput input;
input.Text.assign(128, 0xCC);
input.TextVaddr = 0x1000;
input.Entries = {0x1000};
input.ImportSlots = {0x2000, 0x2008};
return input;
}
void emit(StrictReachabilityInput& input, std::size_t offset, std::initializer_list<std::uint8_t> bytes) {
for (const auto byte : bytes) input.Text.at(offset++) = byte;
}
void ripOperand(StrictReachabilityInput& input, std::size_t offset, std::initializer_list<std::uint8_t> opcode, std::uint64_t target) {
emit(input, offset, opcode);
const auto next = input.TextVaddr + offset + opcode.size() + 4;
write(input.Text, offset + opcode.size(), static_cast<std::int32_t>(target - next));
}
void importThunk(StrictReachabilityInput& input, std::size_t offset, std::uint64_t slot) {
ripOperand(input, offset, {0xFF, 0x25}, slot);
}
void deadTailAndExplicitEntry() {
auto input = fixture();
emit(input, 0, {0x0F, 0x0B});
importThunk(input, 16, 0x2000);
const auto dead = AnalyzeStrictReachability(input);
require(dead.ImportSlots.empty(), "UD2 fallthrough retained a dead import");
input.Entries.push_back(0x1010);
const auto live = AnalyzeStrictReachability(input);
require(live.ImportSlots == std::set<std::uint64_t>{0x2000}, "Explicit entry after UD2 lost its import");
}
void conditionalTailCall() {
auto input = fixture();
emit(input, 0, {0x75, 0x0E, 0xC3});
importThunk(input, 16, 0x2000);
require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Conditional tail call was removed");
}
void callbackAndRelocationRoots() {
auto input = fixture();
ripOperand(input, 0, {0x48, 0x8D, 0x3D}, 0x1020);
emit(input, 7, {0xC3});
importThunk(input, 32, 0x2000);
importThunk(input, 48, 0x2008);
input.Pointers.emplace(0x3000, 0x1030);
require(AnalyzeStrictReachability(input).ImportSlots == input.ImportSlots, "Address-taken callback or relocation root was removed");
}
void registerImportCall() {
auto input = fixture();
ripOperand(input, 0, {0x48, 0x8B, 0x05}, 0x2000);
emit(input, 7, {0xFF, 0xD0, 0xC3});
const auto result = AnalyzeStrictReachability(input);
require(result.ImportSlots.contains(0x2000) && result.IndirectTransfers == 1, "Imported register call was not resolved");
}
void mergedJumpTable() {
auto input = fixture();
emit(input, 0, {0x75, 0x07, 0xB9, 0, 0, 0, 0, 0xEB, 0x05, 0xB9, 1, 0, 0, 0});
ripOperand(input, 14, {0x48, 0x8D, 0x05}, 0x3000);
emit(input, 21, {0xFF, 0x24, 0xC8});
importThunk(input, 48, 0x2000);
importThunk(input, 64, 0x2008);
input.Pointers = {{0x3000, 0x1030}, {0x3008, 0x1040}};
require(AnalyzeStrictReachability(input).ImportSlots == input.ImportSlots, "Merged indexed jump table lost a target");
}
void conservativeIndirectTargets() {
auto input = fixture();
emit(input, 0, {0xFF, 0xE0});
importThunk(input, 32, 0x2000);
importThunk(input, 64, 0x2008);
input.Pointers.emplace(0x3000, 0x1020);
const auto result = AnalyzeStrictReachability(input);
require(result.ImportSlots == std::set<std::uint64_t>{0x2000}, "Unknown indirect transfer lost its address-taken target");
input.Functions = {{0x1000, 0x1040, {}}, {0x1020, 0x1050, {}}};
requireFailure([&] { AnalyzeStrictReachability(input); }, "Overlapping unwind functions were accepted");
}
void isolatedFunctionCycle() {
auto input = fixture();
emit(input, 0, {0xC3});
ripOperand(input, 32, {0xE8}, 0x1030);
emit(input, 37, {0xC3});
ripOperand(input, 48, {0xE8}, 0x1020);
importThunk(input, 53, 0x2000);
input.Functions = {{0x1000, 0x1001, {}}, {0x1020, 0x1026, {}}, {0x1030, 0x103B, {}}};
require(AnalyzeStrictReachability(input).ImportSlots.empty(), "Unrooted function cycle kept its import");
input.Pointers.emplace(0x3000, 0x1020);
require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Rooted function cycle lost its import");
}
void relativeTableAndWholeFunction() {
auto input = fixture();
ripOperand(input, 0, {0x48, 0x8D, 0x05}, 0x3000);
emit(input, 7, {0xFF, 0xE0});
importThunk(input, 32, 0x2000);
importThunk(input, 64, 0x2008);
Relinker::UnusedNidFilter::StrictDataRegion data{0x3000, std::vector<std::uint8_t>(8)};
write<std::int32_t>(data.Bytes, 0, 0x1020 - 0x3000);
input.Data.push_back(data);
require(AnalyzeStrictReachability(input).ImportSlots == std::set<std::uint64_t>{0x2000}, "Relative jump-table target was removed");
input = fixture();
emit(input, 0, {0xFF, 0xE0});
importThunk(input, 32, 0x2000);
input.Functions = {{0x1000, 0x1026, {}}};
require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Unknown switch target inside a live function was removed");
}
void vectorInstructionLengths() {
const Codegen::X64InstructionDecoder decoder;
const std::vector<std::vector<std::uint8_t>> instructions = {
{0xC5, 0xD9, 0x73, 0xD4, 0x20},
{0xC4, 0xE1, 0x79, 0x70, 0xC0, 0x1B},
{0xC4, 0xE1, 0x78, 0x77},
{0x62, 0xF1, 0x7D, 0x48, 0x72, 0xD0, 0x04}
};
for (const auto& instruction : instructions)
require(decoder.Decode(instruction.data(), instruction.size()) == instruction.size(), "Vector immediate or VZEROUPPER was decoded with the wrong length");
}
void exceptionLandingPads() {
std::vector<std::uint8_t> bytes(0x400);
write<std::uint32_t>(bytes, 0x200, 15);
write<std::uint8_t>(bytes, 0x208, 1);
const std::vector<std::uint8_t> augmentation = {'z', 'L', 'R', 0, 1, 0x78, 16, 2, 0, 0};
std::copy(augmentation.begin(), augmentation.end(), bytes.begin() + 0x209);
write<std::uint32_t>(bytes, 0x220, 29);
write<std::uint32_t>(bytes, 0x224, 0x24);
write<std::uint64_t>(bytes, 0x228, 0x1000);
write<std::uint64_t>(bytes, 0x230, 0x20);
write<std::uint8_t>(bytes, 0x238, 8);
write<std::uint64_t>(bytes, 0x239, 0x280);
const std::vector<std::uint8_t> lsda = {0xFF, 0xFF, 1, 4, 0, 1, 0x40, 0};
std::copy(lsda.begin(), lsda.end(), bytes.begin() + 0x280);
write<std::uint8_t>(bytes, 0x300, 1);
write<std::uint8_t>(bytes, 0x302, 3);
write<std::uint64_t>(bytes, 0x304, 0x200);
write<std::uint32_t>(bytes, 0x30C, 1);
write<std::uint64_t>(bytes, 0x310, 0x1000);
write<std::uint64_t>(bytes, 0x318, 0x220);
const std::vector<Relinker::ProgramHeader> headers = {{1, 4, 0, 0, 0, bytes.size(), bytes.size(), 8}, {0x6474E550, 4, 0x300, 0x300, 0, 32, 32, 4}};
auto input = fixture();
input.Functions = Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {});
require(input.Functions.size() == 1 && input.Functions[0].ExtraTargets == std::vector<std::uint64_t>{0x1040}, "LSDA landing pad outside the function was not recovered");
emit(input, 0, {0x0F, 0x0B});
emit(input, 31, {0xC3});
importThunk(input, 64, 0x2000);
require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Exception-only import was removed");
write<std::uint8_t>(bytes, 0x282, 0xFF);
requireFailure([&] { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); }, "Malformed LSDA was accepted");
}
std::vector<std::uint8_t> elfFixture(const StrictReachabilityInput& input) {
std::vector<std::uint8_t> bytes(0x400);
bytes[0] = 0x7F;
bytes[1] = 'E';
bytes[2] = 'L';
bytes[3] = 'F';
bytes[4] = 2;
bytes[5] = 1;
bytes[6] = 1;
write<std::uint16_t>(bytes, 16, 3);
write<std::uint16_t>(bytes, 18, 62);
write<std::uint64_t>(bytes, 24, input.Entries.at(0));
write<std::uint64_t>(bytes, 32, 64);
write<std::uint16_t>(bytes, 54, 56);
write<std::uint16_t>(bytes, 56, 2);
write<std::uint32_t>(bytes, 64, 1);
write<std::uint32_t>(bytes, 68, 5);
write<std::uint64_t>(bytes, 72, 0x200);
write<std::uint64_t>(bytes, 80, input.TextVaddr);
write<std::uint64_t>(bytes, 96, input.Text.size());
write<std::uint64_t>(bytes, 104, input.Text.size());
write<std::uint32_t>(bytes, 120, 1);
write<std::uint32_t>(bytes, 124, 6);
write<std::uint64_t>(bytes, 128, 0x300);
write<std::uint64_t>(bytes, 136, 0x2000);
write<std::uint64_t>(bytes, 152, 0x100);
write<std::uint64_t>(bytes, 160, 0x100);
std::copy(input.Text.begin(), input.Text.end(), bytes.begin() + 0x200);
return bytes;
}
void filterAndPltCompaction() {
auto input = fixture();
importThunk(input, 0, 0x2000);
importThunk(input, 16, 0x2008);
const auto bytes = elfFixture(input);
const std::vector<Relinker::NidReference> references = {{"live", {}, 7, 0x300, 0x2000, 0}, {"dead", {}, 7, 0x318, 0x2008, 0}};
const auto filtered = Relinker::MakeStrictUnusedNidFilter()->Filter(references, bytes, input.Text, input.TextVaddr);
require(filtered.size() == 1 && filtered[0].Nid == references[0].Nid, "Strict ELF filter did not remove only the dead PLT import");
for (std::size_t index = 0; index < references.size(); ++index) {
const auto offset = index * 16;
emit(input, offset + 6, {0x68, 0, 0, 0, 0, 0xE9, 0, 0, 0, 0});
write<std::uint32_t>(input.Text, offset + 7, static_cast<std::uint32_t>(index));
}
const std::vector<Relinker::NidReference> kept = {references[1]};
auto compacted = Relinker::UnusedNidFilter::CompactPlt(references, kept, input.Text, input.TextVaddr, 0, 0x300);
require(compacted.SlotCount == 1 && compacted.References[0].RelocationTableOffset == 0x300, "PLT relocation indices were not compacted");
auto invalidThunks = input.Text;
write<std::uint32_t>(invalidThunks, 23, 99);
requireFailure([&] { Relinker::UnusedNidFilter::CompactPlt(references, kept, invalidThunks, input.TextVaddr, 0, 0x300); }, "Mismatched original PLT index was accepted");
for (const auto& patch : compacted.Patches)
std::copy(patch.Bytes.begin(), patch.Bytes.end(), input.Text.begin() + static_cast<std::ptrdiff_t>(patch.Offset));
std::uint32_t newIndex;
std::memcpy(&newIndex, input.Text.data() + 23, sizeof(newIndex));
require(newIndex == 0 && input.Text[0] == 0x0F && input.Text[1] == 0x0B && input.Text[6] == 0x0F && input.Text[7] == 0x0B, "PLT thunk index or removed thunk traps are incorrect");
Relinker::SysVDynamicSectionBuilder builder;
const auto section = builder.BuildDynamicSection(compacted.References, {}, 0x300, compacted.SlotCount);
require(section.RelaPltData.size() == 24 && section.DynSymData.size() == 48, "Compacted PLT retained a dead dynamic symbol");
auto truncated = bytes;
truncated.resize(70);
requireFailure([&] { Relinker::MakeStrictUnusedNidFilter()->Filter(references, truncated, input.Text, input.TextVaddr); }, "Truncated ELF was accepted");
auto exceptional = bytes;
write<std::uint16_t>(exceptional, 56, 3);
write<std::uint32_t>(exceptional, 176, 0x6474E550);
write<std::uint64_t>(exceptional, 184, 0x380);
write<std::uint64_t>(exceptional, 208, 8);
requireFailure([&] { Relinker::MakeStrictUnusedNidFilter()->Filter(references, exceptional, input.Text, input.TextVaddr); }, "Exception metadata was ignored");
}
}
int main() {
try {
deadTailAndExplicitEntry();
conditionalTailCall();
callbackAndRelocationRoots();
registerImportCall();
mergedJumpTable();
conservativeIndirectTargets();
isolatedFunctionCycle();
relativeTableAndWholeFunction();
vectorInstructionLengths();
exceptionLandingPads();
filterAndPltCompaction();
std::cout << "Strict NID filter tests passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}