diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index ee69f44a7..7d5820269 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -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() diff --git a/core/codegen/src/x86/X64InstructionDecoder.cpp b/core/codegen/src/x86/X64InstructionDecoder.cpp index c78851947..d38b35d6f 100644 --- a/core/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/codegen/src/x86/X64InstructionDecoder.cpp @@ -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) { diff --git a/core/main.cpp b/core/main.cpp index 0f56a3ab1..05b4129be 100644 --- a/core/main.cpp +++ b/core/main.cpp @@ -106,12 +106,17 @@ int main(const int argc, char* argv[]) { Relinker::MakeCallSiteResolver(), std::make_shared(), std::make_shared(), - 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); diff --git a/core/relinker/include/relinker/analysis/UnusedNidFilter.hpp b/core/relinker/include/relinker/analysis/UnusedNidFilter.hpp index 445d6972f..91ca7f306 100644 --- a/core/relinker/include/relinker/analysis/UnusedNidFilter.hpp +++ b/core/relinker/include/relinker/analysis/UnusedNidFilter.hpp @@ -7,6 +7,7 @@ namespace Relinker { std::shared_ptr MakeUnusedNidFilter(); +std::shared_ptr MakeStrictUnusedNidFilter(); } diff --git a/core/relinker/include/relinker/analysis/UnusedNidFilter/EhFrameReader.hpp b/core/relinker/include/relinker/analysis/UnusedNidFilter/EhFrameReader.hpp new file mode 100644 index 000000000..82d4028f4 --- /dev/null +++ b/core/relinker/include/relinker/analysis/UnusedNidFilter/EhFrameReader.hpp @@ -0,0 +1,12 @@ +#ifndef RELINKER_ANALYSIS_UNUSED_NID_FILTER_EHFRAMEREADER_HPP +#define RELINKER_ANALYSIS_UNUSED_NID_FILTER_EHFRAMEREADER_HPP + +#include + +namespace Relinker::UnusedNidFilter { + +std::vector ReadExceptionFunctions(const std::vector& bytes, const std::vector& headers, const std::map& pointers, const std::set& importSlots); + +} + +#endif diff --git a/core/relinker/include/relinker/analysis/UnusedNidFilter/PltCompactor.hpp b/core/relinker/include/relinker/analysis/UnusedNidFilter/PltCompactor.hpp new file mode 100644 index 000000000..167b1c014 --- /dev/null +++ b/core/relinker/include/relinker/analysis/UnusedNidFilter/PltCompactor.hpp @@ -0,0 +1,18 @@ +#ifndef RELINKER_ANALYSIS_UNUSED_NID_FILTER_PLTCOMPACTOR_HPP +#define RELINKER_ANALYSIS_UNUSED_NID_FILTER_PLTCOMPACTOR_HPP + +#include + +namespace Relinker::UnusedNidFilter { + +struct CompactedPlt { + std::vector References; + std::vector Patches; + std::uint32_t SlotCount = 0; +}; + +CompactedPlt CompactPlt(const std::vector& originalReferences, const std::vector& keptReferences, const std::vector& text, VirtualAddress textVaddr, FileByteOffset textOffset, FileByteOffset tableOffset); + +} + +#endif diff --git a/core/relinker/include/relinker/analysis/UnusedNidFilter/StrictReachability.hpp b/core/relinker/include/relinker/analysis/UnusedNidFilter/StrictReachability.hpp new file mode 100644 index 000000000..5c0d663c7 --- /dev/null +++ b/core/relinker/include/relinker/analysis/UnusedNidFilter/StrictReachability.hpp @@ -0,0 +1,44 @@ +#ifndef RELINKER_ANALYSIS_UNUSED_NID_FILTER_STRICTREACHABILITY_HPP +#define RELINKER_ANALYSIS_UNUSED_NID_FILTER_STRICTREACHABILITY_HPP + +#include +#include +#include + +namespace Relinker::UnusedNidFilter { + +struct StrictCodeRegion { + VirtualAddress Begin; + VirtualAddress End; + std::vector ExtraTargets; +}; + +struct StrictDataRegion { + VirtualAddress Address; + std::vector Bytes; +}; + +struct StrictReachabilityInput { + std::vector Text; + VirtualAddress TextVaddr = 0; + std::vector Entries; + std::map Pointers; + std::set ImportSlots; + std::vector Functions; + std::vector Data; +}; + +struct StrictReachabilityResult { + std::set Instructions; + std::set 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 diff --git a/core/relinker/include/relinker/domain/RelinkResult.hpp b/core/relinker/include/relinker/domain/RelinkResult.hpp index b7942a1ca..142f8f228 100644 --- a/core/relinker/include/relinker/domain/RelinkResult.hpp +++ b/core/relinker/include/relinker/domain/RelinkResult.hpp @@ -6,11 +6,17 @@ namespace Relinker { +struct RelinkPatch { + FileByteOffset Offset; + std::vector Bytes; +}; + struct RelinkResult { std::vector RegistryEntries; std::vector OriginalHeaders; SysVDynamicSection DynamicSection; VirtualAddress OriginalPltGotVaddr; + std::vector Patches; }; } diff --git a/core/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp new file mode 100644 index 000000000..50cbf3ee2 --- /dev/null +++ b/core/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -0,0 +1,219 @@ +#include +#include +#include + +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& bytes, const std::vector& headers, const std::map& pointers, const std::set& importSlots) : bytes(bytes), headers(headers), pointers(pointers), importSlots(importSlots) {} + + std::vector Read() { + std::vector result; + for (const auto& segment : headers) { + if (segment.Type != 0x6474E550 || segment.FileSize == 0) continue; + auto position = segment.MappedAddress; + const auto version = read(position); + const auto frameEncoding = read(position); + const auto countEncoding = read(position); + const auto tableEncoding = read(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& bytes; + const std::vector& headers; + const std::map& pointers; + const std::set& importSlots; + std::map cies; + + template + 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(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(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(position); break; + case 1: value = leb(position); break; + case 2: value = read(position); break; + case 3: value = read(position); break; + case 4: value = read(position); break; + case 9: value = leb(position, true); break; + case 10: value = static_cast(static_cast(read(position))); break; + case 11: value = static_cast(static_cast(read(position))); break; + case 12: value = static_cast(read(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(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(position); + if (length == 0xFFFFFFFF || length < 8) throw RelinkerException("Strict filter: unsupported CIE record size", address); + const auto end = position + length; + if (read(position) != 0) throw RelinkerException("Strict filter: invalid CIE identifier", address); + const auto version = read(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(position); + if (character == 0) break; + augmentation.push_back(static_cast(character)); + } + if (version == 4 && (read(position) != 8 || read(position) != 0)) + throw RelinkerException("Strict filter: unsupported CIE address size", address); + leb(position); + leb(position, true); + if (version == 1) read(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(position); break; + case 'R': cie.PointerEncoding = read(position); break; + case 'P': { + const auto encoding = read(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(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(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::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(position); + const auto landingBase = landingEncoding == 0xFF ? region.Begin : encoded(position, landingEncoding, 0, region.Begin); + const auto typeEncoding = read(position); + if (typeEncoding != 0xFF) leb(position); + const auto callEncoding = read(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 ReadExceptionFunctions(const std::vector& bytes, const std::vector& headers, const std::map& pointers, const std::set& importSlots) { + return Reader(bytes, headers, pointers, importSlots).Read(); +} + +} diff --git a/core/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp b/core/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp new file mode 100644 index 000000000..832de8d4f --- /dev/null +++ b/core/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include + +namespace Relinker::UnusedNidFilter { + +CompactedPlt CompactPlt(const std::vector& originalReferences, const std::vector& keptReferences, const std::vector& text, VirtualAddress textVaddr, FileByteOffset textOffset, FileByteOffset tableOffset) { + CompactedPlt result; + result.References = keptReferences; + std::map 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::max()) + throw RelinkerException("Strict filter: invalid original PLT relocation index", reference.RelocationTableOffset); + if (!originalSlots.emplace(reference.RelocationAddress, static_cast((reference.RelocationTableOffset - tableOffset) / 24)).second) + throw RelinkerException("Strict filter: duplicate original PLT slot", reference.RelocationAddress); + } + std::map 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(result.SlotCount++) * 24; + } + std::set 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(static_cast(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 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; +} + +} diff --git a/core/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp new file mode 100644 index 000000000..56a586e82 --- /dev/null +++ b/core/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -0,0 +1,205 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Relinker { + +namespace { + +class StrictImage { +public: + explicit StrictImage(const std::vector& 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(header.ProgramHeaderCount) * 56); + headers = reader.ReadProgramHeaders(); + for (const auto& segment : headers) { + requireRange(segment.Offset, segment.FileSize); + if (segment.MemorySize > std::numeric_limits::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(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(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& references, const std::vector& 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(*rela + offset); + const auto info = read(*rela + offset + 8); + if (static_cast(info) != 8) continue; + if (info >> 32) throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + const auto target = read(*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(fileOffset(slot, 8)) : position->second; + if (entry != 0 && entry != std::numeric_limits::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(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(position + 6); + const auto info = read(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(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& bytes; + ElfReader reader; + std::vector headers; + std::map 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 + 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 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 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 Filter(const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, VirtualAddress textVAddr) override { + const StrictImage image(elfBytes); + const auto input = image.Build(nidRefs, textSection, textVAddr); + const auto analysis = UnusedNidFilter::AnalyzeStrictReachability(input); + std::vector 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 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 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 MakeStrictUnusedNidFilter() { + return std::make_shared(); +} + +} diff --git a/core/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp b/core/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp new file mode 100644 index 000000000..a7d7cd658 --- /dev/null +++ b/core/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp @@ -0,0 +1,224 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Relinker::UnusedNidFilter { + +namespace { + +struct Instruction { + VirtualAddress Address; + Codegen::DecodedInstructionInfo Info; +}; + +struct Region { + VirtualAddress Begin; + VirtualAddress End; + std::set Edges; + std::set Imports; + std::vector 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::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 pending; + std::set 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 regions; + std::set addressTaken; + std::set 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(address - input.TextVaddr); + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(input.Text.data() + offset, static_cast(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(begin - input.TextVaddr); + const auto last = input.Text.begin() + static_cast(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(address - input.TextVaddr); + region.Instructions.push_back(address); + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = next + static_cast(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(static_cast(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(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(static_cast(displacement)); + if (!isCode(target)) break; + addAddressTaken(target); + } + } + } + } +}; + +} + +StrictReachabilityResult AnalyzeStrictReachability(const StrictReachabilityInput& input) { + return Analyzer(input).Run(); +} + +} diff --git a/core/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/src/pipeline/RelinkerPipeline.cpp index ff0bdefdb..314b07586 100644 --- a/core/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/src/pipeline/RelinkerPipeline.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -197,9 +198,14 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& 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 pltRefs; std::vector nonPltRefs; for (const auto& ref : nidRefs) { @@ -215,9 +221,6 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& 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& sourceElf std::cout << "NID total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n"; - auto dynSection = _dynamicSectionBuilder->BuildDynamicSection( - nidRefs, neededLibraries, dynJmpRelOffset, static_cast(dynJmpRelSize / relaEntSize)); + auto dynamicRefs = nidRefs; + std::vector patches; + auto pltCount = static_cast(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& 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)}; } } diff --git a/core/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/tests/StrictNidFilterTests.cpp new file mode 100644 index 000000000..53daae0d4 --- /dev/null +++ b/core/relinker/tests/StrictNidFilterTests.cpp @@ -0,0 +1,283 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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& operation, const char* message) { + try { + operation(); + } catch (const Relinker::RelinkerException&) { + return; + } + throw std::runtime_error(message); +} + +template +void write(std::vector& 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 bytes) { + for (const auto byte : bytes) input.Text.at(offset++) = byte; +} + +void ripOperand(StrictReachabilityInput& input, std::size_t offset, std::initializer_list 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(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{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{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(8)}; + write(data.Bytes, 0, 0x1020 - 0x3000); + input.Data.push_back(data); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{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> 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 bytes(0x400); + write(bytes, 0x200, 15); + write(bytes, 0x208, 1); + const std::vector augmentation = {'z', 'L', 'R', 0, 1, 0x78, 16, 2, 0, 0}; + std::copy(augmentation.begin(), augmentation.end(), bytes.begin() + 0x209); + write(bytes, 0x220, 29); + write(bytes, 0x224, 0x24); + write(bytes, 0x228, 0x1000); + write(bytes, 0x230, 0x20); + write(bytes, 0x238, 8); + write(bytes, 0x239, 0x280); + const std::vector lsda = {0xFF, 0xFF, 1, 4, 0, 1, 0x40, 0}; + std::copy(lsda.begin(), lsda.end(), bytes.begin() + 0x280); + write(bytes, 0x300, 1); + write(bytes, 0x302, 3); + write(bytes, 0x304, 0x200); + write(bytes, 0x30C, 1); + write(bytes, 0x310, 0x1000); + write(bytes, 0x318, 0x220); + const std::vector 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{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(bytes, 0x282, 0xFF); + requireFailure([&] { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); }, "Malformed LSDA was accepted"); +} + +std::vector elfFixture(const StrictReachabilityInput& input) { + std::vector bytes(0x400); + bytes[0] = 0x7F; + bytes[1] = 'E'; + bytes[2] = 'L'; + bytes[3] = 'F'; + bytes[4] = 2; + bytes[5] = 1; + bytes[6] = 1; + write(bytes, 16, 3); + write(bytes, 18, 62); + write(bytes, 24, input.Entries.at(0)); + write(bytes, 32, 64); + write(bytes, 54, 56); + write(bytes, 56, 2); + write(bytes, 64, 1); + write(bytes, 68, 5); + write(bytes, 72, 0x200); + write(bytes, 80, input.TextVaddr); + write(bytes, 96, input.Text.size()); + write(bytes, 104, input.Text.size()); + write(bytes, 120, 1); + write(bytes, 124, 6); + write(bytes, 128, 0x300); + write(bytes, 136, 0x2000); + write(bytes, 152, 0x100); + write(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 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(input.Text, offset + 7, static_cast(index)); + } + const std::vector 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(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(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(exceptional, 56, 3); + write(exceptional, 176, 0x6474E550); + write(exceptional, 184, 0x380); + write(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; + } +}