From 721984a28f59511669fb53888cd0e8bf3b3feb5f Mon Sep 17 00:00:00 2001 From: QDarkRex Date: Fri, 2 Oct 2026 14:58:34 +0700 Subject: [PATCH 1/4] fix(relinker): load Windows guest TLS at signed displacements --- .../src/windows/WindowsTlsBuilder.cpp | 11 +- .../tests/test_tls_function_coverage.py | 116 ++++++++++++++++++ 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index cec7b18d6..bdf125dfb 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -19,6 +19,7 @@ struct TlsAccess { bool StoreImmediate; std::uint32_t Immediate; std::uint8_t Register; + std::uint32_t Displacement; }; void patchAccess(std::vector& sections, const TlsAccess& access, const std::uint32_t target) { @@ -78,11 +79,11 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co else if (value != 0x64 && !(prefix + 1 == position && value >= 0x40 && value <= 0x4f)) supportedPrefixes = false; } const auto loadRegister = info.Length - position == 7 ? static_cast(((bytes[position + 1] >> 3) & 7) | ((info.RexPrefix & 4) << 1)) : std::uint8_t{4}; - const bool loadPointer = supportedPrefixes && (info.RexPrefix == 0x48 || info.RexPrefix == 0x4c) && loadRegister != 4 && bytes[position] == 0x8b && (bytes[position + 1] & 0xc7) == 0x04 && bytes[position + 2] == 0x25 && Io::ReadU32(source, header.Offset + offset + position + 3) == 0; + const bool loadValue = supportedPrefixes && (info.RexPrefix == 0x48 || info.RexPrefix == 0x4c) && loadRegister != 4 && bytes[position] == 0x8b && (bytes[position + 1] & 0xc7) == 0x04 && bytes[position + 2] == 0x25; const bool storeImmediate = supportedPrefixes && !hasOperandSizePrefix && (info.RexPrefix == 0 || info.RexPrefix == 0x40) && info.Length - position == 11 && bytes[position] == 0xc7 && bytes[position + 1] == 0x04 && bytes[position + 2] == 0x25 && Io::ReadU32(source, header.Offset + offset + position + 3) == 0x28; - if (!loadPointer && !storeImmediate) + if (!loadValue && !storeImmediate) throw Domain::RelinkerException("Unsupported Windows guest TLS instruction", header.Offset + offset); - accesses.push_back({rva, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + position + 7) : 0, storeImmediate ? std::uint8_t{0} : loadRegister}); + accesses.push_back({rva, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + position + 7) : 0, storeImmediate ? std::uint8_t{0} : loadRegister, storeImmediate ? 0 : Io::ReadU32(source, header.Offset + offset + position + 3)}); } } } @@ -143,6 +144,10 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (preserveCounter) code.Emit({0x51}); if (preserveAccumulator) code.Emit({0x50}); loadPointer(); + if (!access.StoreImmediate && access.Displacement != 0) { + code.Emit({0x48, 0x8b, 0x80}); + code.U32(access.Displacement); + } if (access.StoreImmediate) { code.Emit({0xc7, 0x40, 0x28}); code.U32(access.Immediate); diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 0eead3d5d..25446ae2a 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -26,6 +26,107 @@ def register_load(register): return len(prefix), prefix + load + rest + restore + bytes.fromhex("31 c0 c3") +def fs_load(register, displacement): + return bytes([0x64, 0x48 | (register >> 3) << 2, 0x8b, 0x04 | (register & 7) << 3, 0x25]) + struct.pack("> 3), 0xb8 | (target & 7)]) + struct.pack("> 3) << 2, 0x89, 0x84 | (target & 7) << 3, 0x24]) + struct.pack("= 8 else b"") + bytes([0x50 | (target & 7)])) + emit(bytes.fromhex("48 8d a4 24 00 ff ff ff")) + emit(fs_load(0, 0)) + stack_store(0, 136) + positive = {8: 0x123456789abcdef0, 16: 0xfedcba9876543210, 40: 0xabcdef1278563412} + for offset, value in positive.items(): + immediate(1, value) + emit(bytes([0x48, 0x89, 0x48, offset])) + immediate(1, 0x2468ace013579bdf) + emit(bytes.fromhex("48 89 08")) + if displacement == 0: + emit(fs_load(2, 8)) + immediate(1, positive[8]) + emit(bytes.fromhex("48 39 ca")) + check() + else: + value = 0xabcdef1289abcdef if round_trip else { + **positive, -64: 0x8877665544332211, -40: 0x1122334455667788, -8: 0, + }[displacement] + immediate(1, value) + stack_store(1, 136) + markers = [0x1020304050607000 + target * 0x101 for target in range(16)] + for target in range(16): + if target != 4: + immediate(target, markers[target]) + emit(b"\x68" + struct.pack("= 8 else b"") + bytes([0x58 | (target & 7)])) + emit(b"\xc3") + for offset in failures: + struct.pack_into(" Date: Fri, 2 Oct 2026 15:45:50 +0700 Subject: [PATCH 2/4] fix(relinker): bound guest TLS loads to the thread block --- .../src/windows/WindowsTlsBuilder.cpp | 7 +++++ .../tests/test_tls_function_coverage.py | 28 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index bdf125dfb..cfee2eed7 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace Elfpatcher::Windows { @@ -107,6 +108,12 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const auto blockSize = CheckedRva((tls->MemorySize + alignment - 1) & ~(alignment - 1)); const auto templateOffset = CheckedRva((64 + alignment - 1) & ~(alignment - 1)); constexpr std::uint32_t threadControlBlockSize = 0x30; + for (const auto& access : accesses) { + if (access.StoreImmediate || access.Displacement == 0) continue; + const auto displacement = static_cast(std::bit_cast(access.Displacement)); + if (displacement < -static_cast(blockSize) || displacement + 8 > threadControlBlockSize) + throw Domain::RelinkerException("Windows guest TLS load displacement " + std::to_string(displacement) + " is outside the thread TLS block", access.Rva); + } PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector(templateOffset + blockSize + threadControlBlockSize)}; const auto indexRva = nextRva + 40; diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 25446ae2a..5f59984eb 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -127,6 +127,17 @@ def displacement_cases(): yield name, image, 0x1240 + offset +def displacement_bounds_cases(): + for register in (0, 12): + for displacement in (0x30, 0x2c, -65, -0x80000000, 0x7fffffff): + image = make_image("register", "unwind", body=fs_load(register, displacement) + b"\xc3") + struct.pack_into(" Date: Fri, 2 Oct 2026 16:14:54 +0700 Subject: [PATCH 3/4] fix(relinker): report TLS bounds failures at file offsets --- core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp | 5 +++-- core/relinker/relinker/tests/test_tls_function_coverage.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index cfee2eed7..a77ac4001 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -16,6 +16,7 @@ namespace { struct TlsAccess { std::uint32_t Rva; + Domain::FileByteOffset FileOffset; std::size_t Length; bool StoreImmediate; std::uint32_t Immediate; @@ -84,7 +85,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const bool storeImmediate = supportedPrefixes && !hasOperandSizePrefix && (info.RexPrefix == 0 || info.RexPrefix == 0x40) && info.Length - position == 11 && bytes[position] == 0xc7 && bytes[position + 1] == 0x04 && bytes[position + 2] == 0x25 && Io::ReadU32(source, header.Offset + offset + position + 3) == 0x28; if (!loadValue && !storeImmediate) throw Domain::RelinkerException("Unsupported Windows guest TLS instruction", header.Offset + offset); - accesses.push_back({rva, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + position + 7) : 0, storeImmediate ? std::uint8_t{0} : loadRegister, storeImmediate ? 0 : Io::ReadU32(source, header.Offset + offset + position + 3)}); + accesses.push_back({rva, header.Offset + offset, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + position + 7) : 0, storeImmediate ? std::uint8_t{0} : loadRegister, storeImmediate ? 0 : Io::ReadU32(source, header.Offset + offset + position + 3)}); } } } @@ -112,7 +113,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (access.StoreImmediate || access.Displacement == 0) continue; const auto displacement = static_cast(std::bit_cast(access.Displacement)); if (displacement < -static_cast(blockSize) || displacement + 8 > threadControlBlockSize) - throw Domain::RelinkerException("Windows guest TLS load displacement " + std::to_string(displacement) + " is outside the thread TLS block", access.Rva); + throw Domain::RelinkerException("Windows guest TLS load displacement " + std::to_string(displacement) + " is outside the thread TLS block", access.FileOffset); } PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector(templateOffset + blockSize + threadControlBlockSize)}; diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 5f59984eb..31b2ce8df 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -202,7 +202,7 @@ def main(): with tempfile.TemporaryDirectory(prefix="anyps5-tls-coverage-") as directory: work = Path(directory) - def convert(name, image, error=None, tls_address=0x1240, displacement=0): + def convert(name, image, error=None, tls_address=0x1240, displacement=0, error_offset=None): source = work / (name + ".elf") output = source.with_suffix(".exe") source.write_bytes(image) @@ -210,6 +210,8 @@ def main(): 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 + if error_offset is not None: + assert f"(offset {error_offset:#x})" in result.stderr, (name, result.stderr) return assert result.returncode == 0, (name, result.stdout, result.stderr) pe = output.read_bytes() @@ -248,7 +250,7 @@ def main(): displacement = struct.unpack_from(" Date: Sun, 4 Oct 2026 14:09:19 +0700 Subject: [PATCH 4/4] fix(relinker): reject guest TLS loads from unmodelled control block fields Loads at fs:1 through fs:0x27 read tcb_dtv and tcb_thread on the FreeBSD layout, which the Windows thread control block does not model, so they returned zero. Accept only the TLS block, fs:0 and the stack guard at fs:0x28, and fail the relink for the rest. --- .../elfpatcher/src/windows/WindowsTlsBuilder.cpp | 5 +++-- .../relinker/tests/test_tls_function_coverage.py | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index a77ac4001..3d111335a 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -109,11 +109,12 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const auto blockSize = CheckedRva((tls->MemorySize + alignment - 1) & ~(alignment - 1)); const auto templateOffset = CheckedRva((64 + alignment - 1) & ~(alignment - 1)); constexpr std::uint32_t threadControlBlockSize = 0x30; + constexpr std::int64_t stackGuardDisplacement = 0x28; for (const auto& access : accesses) { if (access.StoreImmediate || access.Displacement == 0) continue; const auto displacement = static_cast(std::bit_cast(access.Displacement)); - if (displacement < -static_cast(blockSize) || displacement + 8 > threadControlBlockSize) - throw Domain::RelinkerException("Windows guest TLS load displacement " + std::to_string(displacement) + " is outside the thread TLS block", access.FileOffset); + if (displacement != stackGuardDisplacement && (displacement > 0 || displacement < -static_cast(blockSize))) + throw Domain::RelinkerException("Windows guest TLS load displacement " + std::to_string(displacement) + " is not in the thread TLS block, 0 or the stack guard at 0x28", access.FileOffset); } PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector(templateOffset + blockSize + threadControlBlockSize)}; diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 31b2ce8df..31606aa11 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -60,8 +60,8 @@ def displacement_load(register, displacement, flags, round_trip=False): immediate(1, 0x2468ace013579bdf) emit(bytes.fromhex("48 89 08")) if displacement == 0: - emit(fs_load(2, 8)) - immediate(1, positive[8]) + emit(fs_load(2, 40)) + immediate(1, positive[40]) emit(bytes.fromhex("48 39 ca")) check() else: @@ -115,8 +115,8 @@ def displacement_cases(): if register == 4: continue for flags in (0x202, 0xad7): - for displacement, round_trip in ((0, False), (8, False), (16, False), (40, False), - (-64, False), (-40, False), (-8, False), (40, True)): + for displacement, round_trip in ((0, False), (40, False), (-64, False), (-40, False), + (-8, False), (40, True)): offset, body = displacement_load(register, displacement, flags, round_trip) image = make_image("register", "unwind", body=body) struct.pack_into("