From 1f28b37898278953299a6d1c1366f711baf2246c Mon Sep 17 00:00:00 2001 From: Agnibha Mukherjee Date: Sun, 18 Oct 2026 13:31:42 +0530 Subject: [PATCH 1/2] fix(relinker): stop the unused-filter CFG walk before trailing zero padding buildGraph fell through from the entry points into the segment's zero padding; an odd-length zero tail left a lone 0x00 the decoder needs a ModRM byte for, and the non-speculative walk let that truncation escape the pipeline, so unused-filter=1 exited 2 on input the other levels convert. Bound the walk before the trailing zeros the way StrictReachability does; mandatory roots still decode strictly. --- core/relinker/CMakeLists.txt | 2 + .../UnusedNidFilter/ControlFlowGraph.cpp | 4 +- .../test_unused_filter_fallthrough_padding.py | 72 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 core/relinker/relinker/tests/test_unused_filter_fallthrough_padding.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index a34e1d6552..7321a02917 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -686,6 +686,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_syscall_number.py $) add_test(NAME unused_filter_indirect COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_unused_filter_indirect.py $) + add_test(NAME unused_filter_fallthrough_padding COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_unused_filter_fallthrough_padding.py $) else() message(STATUS "Python3 interpreter not found; skipping Python relinker tests") endif() diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index 4ef6bdf112..4a5ac6e513 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -33,6 +33,8 @@ namespace { void buildGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, const std::vector* data, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress, const std::function& isData) { if (text.size() > std::numeric_limits::max() - textVaddr) throw RelinkerException("Text section range exceeds the address space"); + std::size_t codeSize = text.size(); + while (codeSize > 0 && text[codeSize - 1] == 0) --codeSize; const Codegen::X64InstructionDecoder decoder; std::unordered_set visited; std::queue worklist; @@ -69,7 +71,7 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi VirtualAddress va = worklist.front(); worklist.pop(); - while (va >= textVaddr && va - textVaddr < text.size() && !reachable.contains(va) && !(isData && isData(va))) { + while (va >= textVaddr && va - textVaddr < codeSize && !reachable.contains(va) && !(isData && isData(va))) { std::size_t bufOff = static_cast(va - textVaddr); std::size_t available = text.size() - bufOff; if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); diff --git a/core/relinker/relinker/tests/test_unused_filter_fallthrough_padding.py b/core/relinker/relinker/tests/test_unused_filter_fallthrough_padding.py new file mode 100644 index 0000000000..9524a62c07 --- /dev/null +++ b/core/relinker/relinker/tests/test_unused_filter_fallthrough_padding.py @@ -0,0 +1,72 @@ +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +CODE = 0x1000 +GOT_PUTS = 0x2090 +GOT_STRLEN = 0x2098 +MESSAGE = 0x2500 + + +def build(n_nops): + image = bytearray(0x3000) + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + nph = 6 + struct.pack_into(" 1; filtered=1" in result.stdout, (n_nops, result.stdout) + print("Unused filter fallthrough padding tests passed") + + +if __name__ == "__main__": + main() From 904bb59c2559253f3d1adb4fee8a26263cf093b2 Mon Sep 17 00:00:00 2001 From: Agnibha Mukherjee Date: Sun, 18 Oct 2026 18:36:06 +0530 Subject: [PATCH 2/2] fix(relinker): decode explicitly enqueued CFG targets at full bounds The padding bound in buildGraph's decoding loop skipped worklist targets inside the trailing zero region without decoding them, so malformed mandatory roots were accepted silently and complete instructions at targeted zero offsets lost their reachable marking. Decode the first instruction at each popped target against text.size() and keep the codeSize bound for sequential fall-through only. Covers the entry, extra-entry, branch-target, complete-zero-instruction and zero-immediate cases in the direct API test and in the CLI fixture. --- .../UnusedNidFilter/ControlFlowGraph.cpp | 3 +- .../relinker/tests/ControlFlowGraphTests.cpp | 39 +++++++++++++++++ .../test_unused_filter_fallthrough_padding.py | 43 ++++++++++++++++--- 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index 4a5ac6e513..443ff5dc57 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -70,8 +70,9 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi VirtualAddress va = worklist.front(); worklist.pop(); + const VirtualAddress blockEntry = va; - while (va >= textVaddr && va - textVaddr < codeSize && !reachable.contains(va) && !(isData && isData(va))) { + while (va >= textVaddr && va - textVaddr < text.size() && (va == blockEntry || va - textVaddr < codeSize) && !reachable.contains(va) && !(isData && isData(va))) { std::size_t bufOff = static_cast(va - textVaddr); std::size_t available = text.size() - bufOff; if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp index 2d0fb562ec..718e5ddb07 100644 --- a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -141,6 +141,44 @@ void CheckSpeculativeRoots() { Require(threw, "Malformed mandatory root was treated as speculative"); } +void CheckTrailingZeroRoots() { + const Pointers pointers; + const Bytes lone{0x00}; + bool threw = false; + try { + BuildControlFlowGraph(lone, TextAddress, TextAddress, {}, pointers); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "A lone zero instruction at the entry was not decoded"); + + const Bytes trailing{0xc3, 0x00}; + threw = false; + try { + BuildControlFlowGraph(trailing, TextAddress, TextAddress, {TextAddress + 1}, pointers); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "A zero instruction at an extra entry was not decoded"); + + const Bytes jump{0xeb, 0x00, 0x00}; + threw = false; + try { + BuildControlFlowGraph(jump, TextAddress, TextAddress, {}, pointers); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "A branch into a truncated zero instruction was not decoded"); + + const Bytes pair{0x00, 0x00}; + const auto complete = BuildControlFlowGraph(pair, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*complete, {0}); + + const Bytes immediate{0xb8, 0x00, 0x00, 0x00, 0x00}; + const auto zeroImmediate = BuildControlFlowGraph(immediate, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*zeroImmediate, {0}); +} + void CheckBorrowedReachability() { const Pointers pointers; const Bytes text{0x90, 0x90, 0xc3, 0x90, 0xc3}; @@ -223,6 +261,7 @@ int main() { CheckRelocatedTransfers(); CheckCodeAddresses(); CheckSpeculativeRoots(); + CheckTrailingZeroRoots(); CheckBorrowedReachability(); CheckAddressOverflow(); CheckHighVirtualAddress(); diff --git a/core/relinker/relinker/tests/test_unused_filter_fallthrough_padding.py b/core/relinker/relinker/tests/test_unused_filter_fallthrough_padding.py index 9524a62c07..e40e0d5347 100644 --- a/core/relinker/relinker/tests/test_unused_filter_fallthrough_padding.py +++ b/core/relinker/relinker/tests/test_unused_filter_fallthrough_padding.py @@ -8,9 +8,10 @@ CODE = 0x1000 GOT_PUTS = 0x2090 GOT_STRLEN = 0x2098 MESSAGE = 0x2500 +EXIT_TAIL = b"\xbf\x2a\x00\x00\x00\xb8\xe7\x00\x00\x00\x0f\x05" -def build(n_nops): +def build(n_nops, tail=EXIT_TAIL): image = bytearray(0x3000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) nph = 6 @@ -28,8 +29,7 @@ def build(n_nops): image[MESSAGE:MESSAGE + len(message)] = message lea = b"\x48\x8d\x3d" + struct.pack(" 1; filtered=1" in result.stdout, (n_nops, result.stdout) + + entry_tail = bytearray(build(0)) + struct.pack_into(" 1; filtered=1" in result.stdout, result.stdout print("Unused filter fallthrough padding tests passed")