fix(relinker): give Windows guest modules with TLS an import directory

The Windows loader sets up TLS for a dynamically loaded DLL only when it snaps
the DLL's imports. Guest modules had no import directory, so their TLS
directory was never processed: every guest module kept TLS index 0 and read
and wrote the executable's TLS block. A guest libc's thread-locals (gmtime's
buffer at +0x39c in Marvel's Wolverine) then ran past the executable's
0x150-byte block into the process heap, and corrupted a loader node that
crashed the next thread start in ntdll. Guest modules with TLS now import from
KERNEL32, as the executable does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michon 2026-09-30 22:47:50 +02:00
parent a67b644ff6
commit d29a1296cd
3 changed files with 83 additions and 0 deletions

View File

@ -167,6 +167,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_gui.py $<TARGET_FILE:relinker>)
add_test(NAME guest_intel_trampolines COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_intel_trampolines.py $<TARGET_FILE:relinker>)
add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $<TARGET_FILE:relinker>)
add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $<TARGET_FILE:relinker>)
else()

View File

@ -1,4 +1,5 @@
#include <elfpatcher/general/GuestModuleWriter.hpp>
#include <elfpatcher/windows/WindowsImportBuilder.hpp>
#include <elfpatcher/windows/WindowsLoadImage.hpp>
#include <elfpatcher/windows/WindowsTlsBuilder.hpp>
#include <elfpatcher/windows/WindowsPeWriter.hpp>
@ -148,6 +149,13 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteWindows(const Relinker::GuestI
directories[0] = {nextRva, CheckedRva(data.size())};
nextRva = AlignRva(nextRva + data.size());
sections.push_back(std::move(exportSection));
if (tlsIndex != 0) {
auto imports = WindowsImportBuilder().Build(nextRva);
directories[1] = imports.Directory;
directories[12] = imports.AddressTable;
nextRva = AlignRva(nextRva + imports.Section.Data.size());
sections.push_back(std::move(imports.Section));
}
auto relocationData = WindowsRelocationBuilder().BuildBaseRelocations(relocations);
if (!relocationData.empty()) {
directories[5] = {nextRva, CheckedRva(relocationData.size())};

View File

@ -0,0 +1,73 @@
"""Check that Windows guest modules with TLS get their TLS set up by the Windows loader."""
import ctypes
import os
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture
def guest_with_tls(tls):
image = guest_fixture(PLAIN_SITE)
if not tls:
return image
struct.pack_into("<H", image, 56, 4)
struct.pack_into("<IIQQQQQQ", image, 232,
7, 4, 0x980, 0x2380, 0x2380, 8, 0x40, 16)
image[0x980:0x988] = bytes(range(1, 9))
return image
def data_directory(data, index):
header, = struct.unpack_from("<I", data, 0x3C)
return struct.unpack_from("<II", data, header + 24 + 112 + index * 8)
def loaded_tls_index(path):
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
kernel32.LoadLibraryW.restype = ctypes.c_void_p
kernel32.LoadLibraryW.argtypes = [ctypes.c_wchar_p]
base = kernel32.LoadLibraryW(str(path))
assert base, (path, ctypes.get_last_error())
header = ctypes.c_uint32.from_address(base + 0x3C).value
tls_rva = ctypes.c_uint32.from_address(base + header + 24 + 112 + 9 * 8).value
assert tls_rva != 0, path
index_address = ctypes.c_uint64.from_address(base + tls_rva + 16).value
return base, ctypes.c_uint32.from_address(index_address).value
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-guest-tls-") as directory:
case = Path(directory)
module_dir = case / "sce_module"
module_dir.mkdir()
source = case / "input.elf"
source.write_bytes(main_fixture())
for name, tls in (("first", True), ("second", True), ("plain", False)):
(module_dir / (name + ".prx")).write_bytes(guest_with_tls(tls))
result = subprocess.run([str(relinker), "--windows", str(source), str(case / "output.exe")],
capture_output=True, text=True, timeout=30)
assert result.returncode == 0, (result.stdout, result.stderr)
modules = case / "app0" / "sce_module"
for name, tls in (("first", True), ("second", True), ("plain", False)):
data = (modules / (name + ".prx.guest.prx")).read_bytes()
assert bool(data_directory(data, 9)[0]) == tls, name
assert bool(data_directory(data, 1)[0]) == tls, name
if os.name == "nt":
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
kernel32.FreeLibrary.argtypes = [ctypes.c_void_p]
first_base, first = loaded_tls_index(modules / "first.prx.guest.prx")
second_base, second = loaded_tls_index(modules / "second.prx.guest.prx")
kernel32.FreeLibrary(first_base)
kernel32.FreeLibrary(second_base)
assert first != second, (first, second)
print("Guest TLS integration tests passed")
if __name__ == "__main__":
main()