From b443760ca41c2853938a0833a095681fa97d4a68 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:55:09 +0400 Subject: [PATCH] fix(relinker): validate file-backed unwind record ranges --- core/relinker/CMakeLists.txt | 15 ++++++ .../UnusedNidFilter/EhFrameReader.cpp | 43 ++++++++++++++--- .../tests/UnwindRecordBoundsTests.cpp | 47 +++++++++++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..bad6afa79 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -370,6 +370,21 @@ if(BUILD_TESTING) else() message(STATUS "Python3 interpreter not found; skipping Python relinker tests") endif() + add_test_executable(unwind_record_bounds_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/UnwindRecordBoundsTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ) + target_include_directories(unwind_record_bounds_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME unwind_record_bounds COMMAND unwind_record_bounds_tests) + set_tests_properties(unwind_record_bounds PROPERTIES TIMEOUT 20) + add_test_executable(windows_icon_resource_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsIconResourceTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index 84d8b64d2..8ce7c297b 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -1,6 +1,7 @@ #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -37,7 +38,7 @@ public: 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); + if (position < segment.MappedAddress || position - segment.MappedAddress > segment.FileSize) throw RelinkerException("Strict filter: EH frame table exceeds segment", position); result.push_back(readFde(begin, fde)); } } @@ -51,11 +52,38 @@ private: const std::set& importSlots; std::map cies; + VirtualAddress checkedRangeEnd(VirtualAddress address, std::uint64_t size) const { + if (size > std::numeric_limits::max() - address) + throw RelinkerException("Strict filter: unwind range overflowed", address); + const auto end = address + size; + while (address < end) { + bool found = false; + for (const auto& segment : headers) { + if (segment.Type != 1 || address < segment.MappedAddress) continue; + const auto relative = address - segment.MappedAddress; + if (relative >= segment.FileSize) continue; + const auto chunk = std::min(end - address, segment.FileSize - relative); + if (segment.Offset > bytes.size() || relative > bytes.size() - segment.Offset || + chunk > bytes.size() - segment.Offset - relative) + throw RelinkerException("Strict filter: unwind data exceeds file", address); + address += chunk; + found = true; + break; + } + if (!found) throw RelinkerException("Strict filter: unwind range is not file-backed", address); + } + return end; + } + 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; + const auto relative = address - segment.MappedAddress; + if (segment.Offset > bytes.size() || relative > bytes.size() - segment.Offset || + sizeof(TValue) > std::numeric_limits::max() - address) + throw RelinkerException("Strict filter: unwind data range overflowed", address); + const auto offset = segment.Offset + relative; 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)); @@ -118,7 +146,7 @@ private: 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; + const auto end = checkedRangeEnd(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); @@ -140,8 +168,9 @@ private: if (augmentation.front() != 'z') throw RelinkerException("Strict filter: unsupported CIE augmentation", address); cie.Augmented = true; const auto size = leb(position); + if (position > end || size > end - position) + throw RelinkerException("Strict filter: CIE augmentation exceeds record", address); 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; @@ -166,7 +195,7 @@ private: 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 end = checkedRangeEnd(position, length); const auto cieField = position; const auto cieOffset = read(position); if (cieOffset > cieField) throw RelinkerException("Strict filter: invalid FDE CIE offset", address); @@ -179,6 +208,8 @@ private: if (cie.Personality != 0) region.ExtraTargets.push_back(cie.Personality); if (cie.Augmented) { const auto augmentationSize = leb(position); + if (position > end || augmentationSize > end - position) + throw RelinkerException("Strict filter: FDE augmentation exceeds record", address); const auto augmentationEnd = position + augmentationSize; if (augmentationEnd > end) throw RelinkerException("Strict filter: FDE augmentation exceeds record", address); if (cie.LsdaEncoding != 0xFF) { @@ -201,7 +232,7 @@ private: 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; + const auto tableEnd = checkedRangeEnd(position, tableSize); while (position < tableEnd) { encoded(position, callEncoding); encoded(position, callEncoding); diff --git a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp new file mode 100644 index 000000000..0210073af --- /dev/null +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Unwind record validation failed"); } + +static std::vector Fixture() { + std::vector bytes(128, 0); + bytes[0] = 1; bytes[1] = 0; bytes[2] = 3; bytes[3] = 0; + Io::WriteU64(bytes, 4, 0x1040); Io::WriteU32(bytes, 12, 1); + Io::WriteU64(bytes, 16, 0x2000); Io::WriteU64(bytes, 24, 0x1060); + Io::WriteU32(bytes, 64, 9); bytes[72] = 1; bytes[73] = 0; bytes[74] = 1; bytes[75] = 0x78; bytes[76] = 16; + Io::WriteU32(bytes, 96, 20); Io::WriteU32(bytes, 100, 36); + Io::WriteU64(bytes, 104, 0x2000); Io::WriteU64(bytes, 112, 0x10); + return bytes; +} + +int main() { + try { + Domain::ProgramHeader load{}; load.Type = 1; load.MappedAddress = 0x1000; load.FileSize = 128; + auto eh = load; eh.Type = 0x6474E550; eh.FileSize = 32; + const auto regions = Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {load, eh}, {}, {}); + Require(regions.size() == 1 && regions[0].Begin == 0x2000 && regions[0].End == 0x2010); + auto second = load; second.MappedAddress = 0x1048; second.Offset = 72; second.FileSize = 56; + auto first = load; first.FileSize = 72; + Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {first, second, eh}, {}, {}).size() == 1); + for (int kind = 0; kind < 7; ++kind) { + auto bytes = Fixture(); auto segment = load; + switch (kind) { + case 0: Io::WriteU32(bytes, 64, 0x1000); break; + case 1: Io::WriteU32(bytes, 96, 0x1000); break; + case 2: Io::WriteU32(bytes, 64, 100); break; + case 3: Io::WriteU32(bytes, 96, 32); bytes.resize(120); break; + case 4: segment.Offset = std::numeric_limits::max() - 4095; segment.MappedAddress = 0; segment.FileSize = 0x1100; break; + case 5: segment.FileSize = 119; break; + case 6: Io::WriteU32(bytes, 64, 8); break; + } + bool rejected = false; + try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, {segment, eh}, {}, {}); } + catch (const std::exception&) { rejected = true; } + Require(rejected); + } + std::cout << "9 unwind record cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +}