fix(relinker): scan complete function ranges for windows tls accesses

This commit is contained in:
boykopovar 2026-09-27 09:00:11 +03:00
parent 9cc36a02fe
commit bfa1ac0751
3 changed files with 166 additions and 3 deletions

View File

@ -86,8 +86,10 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $<TARGET_FILE:relinker>)
add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $<TARGET_FILE:relinker>)
add_test(NAME tls_function_coverage COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_function_coverage.py $<TARGET_FILE:relinker>)
else()
message(STATUS "Python3 interpreter not found; skipping optional_plt and empty_tls tests")
message(STATUS "Python3 interpreter not found; skipping Python relinker tests")
endif()
add_executable(strict_nid_filter_tests
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp

View File

@ -2,8 +2,11 @@
#include <relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp>
#include <relinker/analysis/UnusedNidFilter/EhFrameReader.hpp>
#include <codegen/x86/X64InstructionDecoder.hpp>
#include <codegen/CodegenException.hpp>
#include <io/BufferUtils.hpp>
#include <limits>
#include <map>
#include <string>
namespace Relinker {
@ -73,7 +76,27 @@ std::set<Domain::VirtualAddress> CodeInstructionCollector::Collect(const std::ve
return offset;
};
std::set<std::uint64_t> roots;
std::map<std::uint64_t, std::uint64_t> functions;
const auto addRoot = [&](std::uint64_t address) { if (isCode(address)) roots.insert(address); };
const auto addFunction = [&](std::uint64_t begin, std::uint64_t size) {
if (size == 0) return;
if (!isCode(begin) || size > std::numeric_limits<std::uint64_t>::max() - begin)
throw Domain::RelinkerException("Code analysis: invalid function range", begin);
bool mapped = false;
for (const auto& header : headers) {
if (header.Type != 1 || (header.Flags & 1) == 0 || begin < header.MappedAddress || begin - header.MappedAddress >= header.FileSize) continue;
if (size > header.FileSize - (begin - header.MappedAddress))
throw Domain::RelinkerException("Code analysis: function exceeds executable segment", begin);
range(header.Offset + begin - header.MappedAddress, size);
mapped = true;
break;
}
if (!mapped) throw Domain::RelinkerException("Code analysis: function is not file-backed", begin);
const auto [position, inserted] = functions.emplace(begin, begin + size);
if (!inserted && position->second != begin + size)
throw Domain::RelinkerException("Code analysis: conflicting function ranges", begin);
roots.insert(begin);
};
range(0, 64);
if (const auto entry = Io::ReadU64(bytes, 24); entry != 0) addRoot(entry);
for (const auto tag : {12u, 13u}) if (const auto address = value(tag, 0x60000000 + tag); address && *address != 0) addRoot(*address);
@ -89,7 +112,11 @@ std::set<Domain::VirtualAddress> CodeInstructionCollector::Collect(const std::ve
symbols = table(6, 0x61000039, symbolCount * 24);
for (std::uint64_t index = 0; index < symbolCount; ++index) {
const auto offset = symbols + index * 24;
if ((bytes[offset + 4] & 15) == 2 && Io::ReadU16(bytes, offset + 6) != 0) addRoot(Io::ReadU64(bytes, offset + 8));
if ((bytes[offset + 4] & 15) == 2 && Io::ReadU16(bytes, offset + 6) != 0) {
const auto address = Io::ReadU64(bytes, offset + 8);
addRoot(address);
addFunction(address, Io::ReadU64(bytes, offset + 16));
}
}
}
CodePointers pointers;
@ -139,12 +166,30 @@ std::set<Domain::VirtualAddress> CodeInstructionCollector::Collect(const std::ve
}
}
for (const auto& function : UnusedNidFilter::ReadExceptionFunctions(bytes, headers, pointers.Values, importSlots)) {
addRoot(function.Begin);
if (function.End <= function.Begin) throw Domain::RelinkerException("Code analysis: invalid unwind function range", function.Begin);
addFunction(function.Begin, function.End - function.Begin);
for (const auto target : function.ExtraTargets) addRoot(target);
}
if (roots.empty()) throw Domain::RelinkerException("Code analysis: no code entry points");
std::set<std::uint64_t> instructions;
const Codegen::X64InstructionDecoder decoder;
for (const auto& [begin, end] : functions) {
const auto offset = fileOffset(begin, end - begin);
for (auto address = begin; address < end;) {
Codegen::DecodedInstructionInfo info;
try {
info = decoder.DecodeInstruction(bytes.data() + offset + address - begin, end - address);
} catch (const Codegen::CodegenException& error) {
throw Domain::RelinkerException(std::string("Code analysis: ") + error.what(), address);
}
if (info.Length == 0 || info.Length > end - address)
throw Domain::RelinkerException("Code analysis: instruction crosses function boundary", address);
instructions.insert(address);
if (info.HasBranchTarget && !info.HasRipRelativeDisp)
addRoot(address + info.Length + static_cast<std::uint64_t>(info.BranchDisp));
address += info.Length;
}
}
std::size_t previousRoots = 0;
do {
previousRoots = roots.size();

View File

@ -0,0 +1,116 @@
import os
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from test_optional_plt import fixture
TLS_LOAD = bytes.fromhex("66 66 66 64 48 8b 04 25 00 00 00 00")
def make_image(transfer, metadata, extent=None):
image = fixture()
image.extend(b"\x90" * 0x1000)
struct.pack_into("<Q", image, 24, 0x1200)
struct.pack_into("<I", image, 68, 6)
image[0x1200:0x1300] = b"\x90" * 0x100
target = 0x1240
if transfer == "table":
code = bytes.fromhex("31 c0 48 8d 0d") + struct.pack("<i", 0x780 - 0x1209)
code += bytes.fromhex("48 63 04 81 48 01 c8 ff e0")
struct.pack_into("<i", image, 0x780, target - 0x780)
elif transfer == "register":
code = bytes.fromhex("48 8d 05") + struct.pack("<i", target - 0x1207)
code += bytes.fromhex("ff e0")
elif transfer == "memory":
code = bytes.fromhex("48 8d 0d") + struct.pack("<i", target - 0x1207)
code += bytes.fromhex("48 89 4c 24 f8 ff 64 24 f8")
else:
raise ValueError(transfer)
image[0x1200:0x1200 + len(code)] = code
body = TLS_LOAD + bytes.fromhex("8b 40 f0 c3")
image[target:target + len(body)] = body
image[0x1850:0x1850 + len(TLS_LOAD)] = TLS_LOAD
struct.pack_into("<QQq", image, 0x700, 0x300, 8, 0x1200)
struct.pack_into("<Q", image, 0x800, 42)
struct.pack_into("<H", image, 56, 4 if metadata == "symbol" else 5)
struct.pack_into("<IIQQQQQQ", image, 176, 7, 4, 0x800, 0x800, 0x800, 8, 16, 16)
struct.pack_into("<IIQQQQQQ", image, 232 if metadata == "symbol" else 288, 1, 5, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000)
function_size = target + len(body) - 0x1200 if extent is None else extent
if metadata == "unwind":
struct.pack_into("<IIQQQQQQ", image, 232, 0x6474e550, 4, 0x980, 0x980, 0x980, 32, 32, 8)
struct.pack_into("<II", image, 0x900, 12, 0)
image[0x908:0x910] = bytes.fromhex("01 00 01 78 10 00 00 00")
struct.pack_into("<IIQQ", image, 0x910, 20, 0x14, 0x1200, function_size)
struct.pack_into("<BBBBQIQQ", image, 0x980, 1, 0, 3, 0, 0x900, 1, 0x1200, 0x910)
elif metadata == "symbol":
struct.pack_into("<IBBHQQ", image, 0x638, 0, 0x12, 0, 1, 0x1200, function_size)
struct.pack_into("<IIII", image, 0x680, 1, 2, 1, 0)
struct.pack_into("<qQqQ", image, 0x470, 4, 0x680, 0, 0)
struct.pack_into("<QQ", image, 160, 0x90, 8)
struct.pack_into("<Q", image, 152, 0x90)
else:
raise ValueError(metadata)
return image
def pe_bytes_at(pe, rva, size):
header = struct.unpack_from("<I", pe, 0x3c)[0]
count = struct.unpack_from("<H", pe, header + 6)[0]
sections = header + 24 + struct.unpack_from("<H", pe, header + 20)[0]
for index in range(count):
offset = sections + index * 40
address, length, position = struct.unpack_from("<III", pe, offset + 12)
if address <= rva and rva + size <= address + length:
return pe[position + rva - address:position + rva - address + size]
raise AssertionError(f"Unmapped PE RVA {rva:#x}")
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-tls-coverage-") as directory:
work = Path(directory)
def convert(name, image, error=None, tls_address=0x1240):
source = work / (name + ".elf")
output = source.with_suffix(".exe")
source.write_bytes(image)
result = subprocess.run([str(relinker), "--windows", str(source), str(output)],
capture_output=True, text=True, timeout=30)
if error is not None:
assert result.returncode == 2 and error in result.stderr and not output.exists(), result
return
assert result.returncode == 0, (name, result.stdout, result.stderr)
pe = output.read_bytes()
assert pe_bytes_at(pe, 0x10000 + tls_address, 1) == b"\xe9", name
assert pe_bytes_at(pe, 0x11850, len(TLS_LOAD)) == TLS_LOAD, name
if os.name == "nt":
executed = subprocess.run([str(output)], capture_output=True, timeout=30)
assert executed.returncode == 42, (name, executed.returncode, executed.stderr)
for metadata in ("unwind", "symbol"):
for transfer in ("table", "register", "memory"):
convert(metadata + "-" + transfer, make_image(transfer, metadata))
convert(metadata + "-truncated", make_image("register", metadata, 0x46), "Code analysis:")
convert(metadata + "-outside", make_image("register", metadata, 0x1000), "Code analysis: function exceeds executable segment")
overlapping = make_image("register", "unwind")
overlapping[0x1200:0x1205] = b"\xe9" + struct.pack("<i", 0x1245 - 0x1205)
convert("overlapping-entry", overlapping, "Code analysis: overlapping instruction boundaries")
external = make_image("register", "unwind")
external[0x1300:0x1310] = external[0x1240:0x1250]
external[0x1240:0x1250] = b"\xe8" + struct.pack("<i", 0x1300 - 0x1245) + b"\xc3" + b"\x90" * 10
convert("direct-call-from-indirect-block", external, tls_address=0x1300)
conflicting = make_image("register", "symbol")
unwind = make_image("register", "unwind", 0x51)
conflicting[0x900:0x9a0] = unwind[0x900:0x9a0]
conflicting[232:344] = unwind[232:344]
struct.pack_into("<H", conflicting, 56, 5)
convert("conflicting-function-extents", conflicting, "Code analysis: conflicting function ranges")
print("TLS function coverage integration tests passed")
if __name__ == "__main__":
main()