Merge pull request #3780 from MrMha/fix/unwind-record-bounds

fix(relinker): validate file-backed unwind record ranges
This commit is contained in:
Dean 2026-10-12 17:35:37 +13:00 committed by GitHub
commit 66ab8753bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 99 additions and 6 deletions

View File

@ -468,6 +468,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

View File

@ -1,6 +1,7 @@
#include <relinker/analysis/UnusedNidFilter/EhFrameReader.hpp>
#include <cstring>
#include <limits>
#include <algorithm>
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<VirtualAddress>& importSlots;
std::map<VirtualAddress, Cie> cies;
VirtualAddress checkedRangeEnd(VirtualAddress address, std::uint64_t size) const {
if (size > std::numeric_limits<VirtualAddress>::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<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;
const auto relative = address - segment.MappedAddress;
if (segment.Offset > bytes.size() || relative > bytes.size() - segment.Offset ||
sizeof(TValue) > std::numeric_limits<VirtualAddress>::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<std::uint32_t>(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<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);
@ -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<std::uint8_t>(position); break;
@ -166,7 +195,7 @@ private:
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 end = checkedRangeEnd(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);
@ -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);

View File

@ -0,0 +1,47 @@
#include <relinker/analysis/UnusedNidFilter/EhFrameReader.hpp>
#include <io/BufferUtils.hpp>
#include <iostream>
#include <limits>
#include <stdexcept>
static void Require(bool ok) { if (!ok) throw std::runtime_error("Unwind record validation failed"); }
static std::vector<std::uint8_t> Fixture() {
std::vector<std::uint8_t> 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<std::uint64_t>::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; }
}