From 9c4321166439816427a7db609505feb7b4dfef9d Mon Sep 17 00:00:00 2001 From: Ayamullah Khan Date: Thu, 15 Oct 2026 03:58:16 +0530 Subject: [PATCH 1/4] fix(relinker): run guest preinit arrays before the module initializer Converted guest modules never ran their DT_PREINIT_ARRAY entries: the reader validated the array and the unused-NID filter kept its targets, but no writer emitted or ran it. ld.so runs a shared object's preinit array only for the main executable, so the Linux output runs the entries from the DT_INIT stub before the module initializer; the macOS and Windows outputs run them before the initializer in their startup stubs, and the deferred Windows output exports a thunk that runs them before the initializer for runtime-loaded modules. A module with a preinit array and no initializer is still rejected, since the Linux stub exists only with one. --- core/relinker/CMakeLists.txt | 3 + .../domain/include/domain/GuestRuntime.hpp | 1 + .../include/elfpatcher/macos/MacOsImage.hpp | 1 + .../src/linux/LinuxGuestModuleWriter.cpp | 27 +- .../src/macos/MacOsGuestModuleWriter.cpp | 1 + .../elfpatcher/src/macos/MacOsImage.cpp | 3 +- .../src/windows/WindowsGuestModuleWriter.cpp | 34 ++- .../src/windows/WindowsGuestStartup.cpp | 1 + .../include/relinker/guest/GuestImage.hpp | 1 + .../relinker/src/guest/GuestImageReader.cpp | 1 + .../relinker/tests/test_guest_preinit.py | 289 ++++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/relinker.md | 1 + 13 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_preinit.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index c53be3987b..fad2923146 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -526,6 +526,9 @@ if(BUILD_TESTING) add_test(NAME guest_deferred_dependencies COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_deferred_dependencies.py $ $) + add_test(NAME guest_preinit COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_preinit.py + $ $) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/domain/include/domain/GuestRuntime.hpp b/core/relinker/domain/include/domain/GuestRuntime.hpp index 5f1e25c734..787c64701a 100644 --- a/core/relinker/domain/include/domain/GuestRuntime.hpp +++ b/core/relinker/domain/include/domain/GuestRuntime.hpp @@ -22,6 +22,7 @@ struct GuestRuntime { std::vector Imports; std::vector InitArrayRvas; std::vector FiniArrayRvas; + std::vector PreInitArrayRvas; std::uint32_t InitRva = 0; std::uint32_t FiniRva = 0; bool UsePlatformTlsResolver = true; diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp index 4f7ec0fee8..597e32dc60 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp @@ -50,6 +50,7 @@ struct MacOsImageInput { std::uint32_t FiniRva = 0; std::vector InitArrayRvas; std::vector FiniArrayRvas; + std::vector PreInitArrayRvas; std::vector TlsModuleSlots; std::vector TlsImports; std::vector TlsExports; diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index 1fb61430c6..820a4548a6 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -150,7 +150,31 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma Io::AppendU32(bytes, static_cast(distance)); return start; }; + const auto preinit = [&](std::uint64_t target) { + if (image.PreInitArray.empty()) return std::uint64_t{}; + if (target == 0) throw Domain::RelinkerException("Guest preinit array has no initializer"); + const auto start = address(); + std::vector stub{0x50}; + for (const auto slot : image.PreInitArray) { + const auto load = static_cast(stub.size()); + const auto distance = static_cast(slot) - static_cast(start + static_cast(load) + 7); + if (distance < std::numeric_limits::min() || distance > std::numeric_limits::max()) + throw Domain::RelinkerException("Guest preinit exceeds relative branch range"); + stub.insert(stub.end(), {0x48, 0x8b, 0x05}); + Io::AppendU32(stub, static_cast(static_cast(distance))); + stub.insert(stub.end(), {0x48, 0x85, 0xc0, 0x74, 0x08, 0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xff, 0xd0}); + } + stub.push_back(0x58); + const auto distance = static_cast(target) - static_cast(start + stub.size() + 5); + if (distance < std::numeric_limits::min() || distance > std::numeric_limits::max()) + throw Domain::RelinkerException("Guest preinit exceeds relative branch range"); + stub.push_back(0xe9); + Io::AppendU32(stub, static_cast(static_cast(distance))); + bytes.insert(bytes.end(), stub.begin(), stub.end()); + return start; + }; const auto init = lifecycle(image.Init); + const auto preinitInit = preinit(init); const auto fini = finalizer(image.Fini); Io::AlignBuffer(bytes, 8); const auto dynamicOffset = bytes.size(); @@ -177,7 +201,8 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma } if (!image.InitArray.empty()) { tag(25, image.InitArray.front()); tag(27, image.InitArray.size() * 8); } if (!image.FiniArray.empty()) { tag(26, image.FiniArray.front()); tag(28, image.FiniArray.size() * 8); } - if (init != 0) tag(12, init); + const auto entry = preinitInit != 0 ? preinitInit : init; + if (entry != 0) tag(12, entry); if (fini != 0) tag(13, fini); tag(30, 8); tag(0, 0); diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index 74e31d19d3..d251732263 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -109,6 +109,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma input.RunPaths = MachORunPaths(runPath, "@loader_path"); for (const auto slot : guest.InitArray) input.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) input.FiniArrayRvas.push_back(image.GetRva(slot, 8)); + for (const auto slot : guest.PreInitArray) input.PreInitArrayRvas.push_back(image.GetRva(slot, 8)); input.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); input.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); input.Trampolines = &guest.Trampolines; diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 838d0d6c8b..405c3d42fc 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -534,7 +534,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { input.Binds.push_back({"___cxa_atexit", metaRva + static_cast(MetaCxaAtexitSlot)}); } - const bool hasInit = hasTls || !input.TlsImports.empty() || input.InitRva != 0 || !input.InitArrayRvas.empty() || hasFini; + const bool hasInit = hasTls || !input.TlsImports.empty() || input.InitRva != 0 || !input.InitArrayRvas.empty() || !input.PreInitArrayRvas.empty() || hasFini; std::vector
dataSections{{"__meta", 0, metaSize, 0, 3}}; if (hasInit) { const auto initStub = code.GetRva(); @@ -554,6 +554,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { } code.Rip({0x48, 0x89, 0x05}, import.Rva); } + for (const auto slot : input.PreInitArrayRvas) callSlot(slot); if (input.InitRva != 0) { code.Rip({0xff, 0x15}, metaRva + static_cast(moduleArgsSlot)); code.Emit({0x48, 0x8b, 0x38, 0x48, 0x8b, 0x70, 0x08, 0x31, 0xd2}); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index ded45f6d76..59f9d874c0 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -154,7 +155,37 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI for (const auto slot : guest.InitArray) Io::AppendU32(lifecycle.Data, image.GetRva(slot, 8)); if (!exports.emplace(Relinker::GuestInitializeExport, nextRva).second) throw Domain::RelinkerException("Duplicate guest export: " + std::string(Relinker::GuestInitializeExport)); nextRva = AlignRva(nextRva + lifecycle.Data.size()); - sections.push_back(std::move(lifecycle)); + if (guest.PreInitArray.empty()) sections.push_back(std::move(lifecycle)); + else { + if (guest.Init == 0) throw Domain::RelinkerException("Guest preinit array has no initializer"); + const auto thunkRva = nextRva; + const auto codeSize = static_cast(40 + 23 * guest.PreInitArray.size()); + const auto slotsRva = static_cast(thunkRva + codeSize + (8 - codeSize % 8) % 8); + WindowsStubEmitter code(thunkRva); + code.Emit({0x41, 0x54, 0x41, 0x55, 0x41, 0x56}); + code.Emit({0x49, 0x89, 0xfc, 0x49, 0x89, 0xf5, 0x49, 0x89, 0xd6}); + for (std::size_t index = 0; index < guest.PreInitArray.size(); ++index) { + code.Rip({0x48, 0x8b, 0x05}, slotsRva + static_cast(index) * 8); + code.Emit({0x48, 0x8b, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x08}); + code.Emit({0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xff, 0xd0}); + } + code.Emit({0x4c, 0x89, 0xe7, 0x4c, 0x89, 0xee, 0x4c, 0x89, 0xf2}); + code.Rip({0x48, 0x8b, 0x05}, slotsRva + static_cast(guest.PreInitArray.size()) * 8); + code.Emit({0xff, 0xd0}); + code.Emit({0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e, 0xc3}); + auto thunk = code.TakeBytes(); + while (thunk.size() % 8 != 0) thunk.push_back(0x90); + for (const auto slot : guest.PreInitArray) { + relocations.push_back(CheckedRva(thunkRva + thunk.size())); + Io::AppendU64(thunk, ImageBase + image.GetRva(slot, 8)); + } + relocations.push_back(CheckedRva(thunkRva + thunk.size())); + Io::AppendU64(thunk, ImageBase + image.GetRva(guest.Init, 1)); + Io::WriteU32(lifecycle.Data, 0, thunkRva); + nextRva = AlignRva(nextRva + thunk.size()); + sections.push_back(std::move(lifecycle)); + sections.push_back({".ginitc", thunkRva, SectionRead | SectionExecute | 0x60u, std::move(thunk)}); + } } if (exports.size() > 65535) throw Domain::RelinkerException("Too many guest PE exports"); PeSection exportSection{".edata", nextRva, SectionRead | 0x40u, std::vector(40)}; @@ -201,6 +232,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI const auto entry = nextRva; sections.push_back({".dllmain", entry, SectionRead | SectionExecute | 0x20u, {0xb8, 1, 0, 0, 0, 0xc3}}); for (const auto slot : guest.InitArray) runtime.InitArrayRvas.push_back(image.GetRva(slot, 8)); + for (const auto slot : guest.PreInitArray) runtime.PreInitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) runtime.FiniArrayRvas.push_back(image.GetRva(slot, 8)); runtime.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); runtime.UnmatchedImportModules.assign(guest.Dynamic.UnmatchedImportModules.begin(), guest.Dynamic.UnmatchedImportModules.end()); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp index 8d241798f9..da1513a425 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp @@ -31,6 +31,7 @@ void WindowsGuestStartup::Initialize(WindowsStubEmitter& code, const std::vector for (std::size_t index = 0; index < modules.size(); ++index) { if (modules[index].DeferInitialization) continue; const auto handle = CheckedRva(handles + index * 8); + for (const auto slot : modules[index].PreInitArrayRvas) callLifecycle(code, handle, slot, true); callLifecycle(code, handle, modules[index].InitRva); for (const auto slot : modules[index].InitArrayRvas) callLifecycle(code, handle, slot, true); } diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index 31e5781a54..713a2b773d 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -47,6 +47,7 @@ struct GuestImage { std::vector Symbols; std::vector Dependencies; Domain::SysVDynamicSection Dynamic; + std::vector PreInitArray; std::vector InitArray; std::vector FiniArray; std::uint64_t Init = 0; diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index f1734eeba5..44ee6383dd 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -357,6 +357,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector entries.push_back(address + offset); } }; + readLifecycleArray(32, 33, image.PreInitArray); readLifecycleArray(25, 27, image.InitArray); readLifecycleArray(26, 28, image.FiniArray); if (tags.contains(33) && tags.at(33) != 0 && image.Init == 0) fail("Guest PREINIT_ARRAY has no module initializer"); diff --git a/core/relinker/relinker/tests/test_guest_preinit.py b/core/relinker/relinker/tests/test_guest_preinit.py new file mode 100644 index 0000000000..d6df4b5846 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_preinit.py @@ -0,0 +1,289 @@ +from pathlib import Path +import os +import platform +import shutil +import struct +import subprocess +import sys +import tempfile + +from test_guest_symbol_names import dynamic_tags, file_offset, imported_symbols, module_symbols +from test_macos_output import MachO +from test_guest_intel_trampolines import main_fixture, pe_sections +from test_windows_import_modules import executable + +ARGS = 0x1122334455667788 +ARGP = 0x99AABBCCDDEEFF00 +RETURNED = 0x5A17 + +GETTER = '__aps5_get_pending_module_args_nid_no_patch' +SETTER = '__aps5_set_module_init_result_nid_no_patch' + +SHARED = 0x1000 +PREINIT0 = 0x1010 +PREINIT1 = 0x1030 +INIT = 0x1050 +RECORD_GOT = 0x2820 +PREINIT_SLOTS = 0x2840 +FLAG = 0x2820 +IMAGE_BASE = 0x140000000 +LOAD_RVA = 0x10000 +CALL_SLOT = b'\x48\x8b\x05' + + +def elf_header(image, program_headers=4): + image[:16] = b'\x7fELF\x02\x01\x01' + bytes(9) + struct.pack_into('= 0: + if data[position + 7:position + 20] == b'\x48\x85\xc0\x74\x08\x31\xff\x31\xf6\x31\xd2\xff\xd0': + displacement, = struct.unpack_from('> 12 == 10: + found.add(page + (entry & 0xfff)) + position += size + assert set(addresses) <= found, (sorted(found), addresses) + + +def check_windows_deferred(data, slots): + sections = {name: (rva, raw_size, raw_offset, flags) for name, rva, raw_size, raw_offset, flags in pe_sections(data)} + ginit_rva, ginit_size, ginit_raw, _ = sections[b'.ginit'] + ginitc_rva, ginitc_size, ginitc_raw, _ = sections[b'.ginitc'] + entry, count = struct.unpack_from('= 0, address + return found + + initializer = position_of(LOAD_RVA + INIT) + for slot in slots: + assert position_of(LOAD_RVA + slot) < initializer, (slot, initializer) + + +def main(): + if sys.argv[1] == '--load': + import ctypes + host = ctypes.CDLL(sys.argv[2], mode=ctypes.RTLD_GLOBAL if sys.platform == 'darwin' else ctypes.DEFAULT_MODE) + host.SetPendingArgs.argtypes = [ctypes.c_size_t, ctypes.c_void_p] + host.SetPendingArgs(ARGS, ARGP) + ctypes.CDLL(sys.argv[3]) + return + relinker = Path(sys.argv[1]).resolve() + host = Path(sys.argv[2]).resolve() + linux = sys.platform == 'linux' and os.uname().machine == 'x86_64' + macos = sys.platform == 'darwin' and platform.machine() == 'x86_64' + with tempfile.TemporaryDirectory(prefix='anyps5-guest-preinit-') as directory: + work = Path(directory) + modules = work / 'sce_module' + modules.mkdir() + name = 'preinit.prx' + (modules / name).write_bytes(recording_module()) + source = work / 'input.elf' + source.write_bytes(executable(name)) + output = work / 'output.elf' + result = subprocess.run([str(relinker), '--skip-syscall-check', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + converted = output.parent / 'app0' / 'sce_module' / (name + '.guest.prx') + image = converted.read_bytes() + assert GETTER in imported_symbols(image), imported_symbols(image) + assert SETTER in imported_symbols(image), imported_symbols(image) + assert 'shared#guest' in module_symbols(image), module_symbols(image) + tags = dynamic_tags(image) + check_linux_stub(image, tags[12], [PREINIT_SLOTS, PREINIT_SLOTS + 8]) + if linux: + libraries = work / 'libs' + libraries.mkdir() + shutil.copyfile(host, libraries / 'libc.prx') + load_and_record(work, libraries / 'libc.prx', converted) + mach = work / 'macos' + mach_modules = mach / 'sce_module' + mach_modules.mkdir(parents=True) + (mach_modules / name).write_bytes(recording_module()) + mach_source = mach / 'input.elf' + mach_source.write_bytes(executable(name)) + mach_result = subprocess.run([str(relinker), '--macos', '--skip-syscall-check', str(mach_source), str(mach / 'output')], + capture_output=True, text=True, timeout=30) + assert mach_result.returncode == 0, (mach_result.stdout, mach_result.stderr) + mach_converted = mach / 'app0' / 'sce_module' / (name + '.guest.prx') + check_macos_stub(mach_converted.read_bytes(), [IMAGE_BASE + LOAD_RVA + PREINIT_SLOTS, IMAGE_BASE + LOAD_RVA + PREINIT_SLOTS + 8]) + windows = work / 'windows' + win_modules = windows / 'sce_module' + win_modules.mkdir(parents=True) + (win_modules / name).write_bytes(flag_module()) + win_source = windows / 'input.elf' + win_source.write_bytes(executable(name)) + win_result = subprocess.run([str(relinker), '--windows', '--skip-syscall-check', str(win_source), str(windows / 'output.exe')], + capture_output=True, text=True, timeout=30) + assert win_result.returncode == 0, (win_result.stdout, win_result.stderr) + win_converted = windows / 'app0' / 'sce_module' / (name + '.guest.prx') + check_windows_startup((windows / 'output.exe').read_bytes(), [PREINIT_SLOTS, PREINIT_SLOTS + 8]) + deferred = work / 'deferred' + (deferred / 'sce_module').mkdir(parents=True) + plugins = deferred / 'Media' / 'Plugins' + plugins.mkdir(parents=True) + (plugins / name).write_bytes(flag_module()) + deferred_source = deferred / 'input.elf' + deferred_source.write_bytes(main_fixture()) + deferred_result = subprocess.run([str(relinker), '--windows', '--skip-syscall-check', + '--module-dir', 'Media/Plugins', str(deferred_source), str(deferred / 'output.exe')], + capture_output=True, text=True, timeout=30) + assert deferred_result.returncode == 0, (deferred_result.stdout, deferred_result.stderr) + deferred_converted = deferred / 'app0' / 'Media' / 'Plugins' / (name + '.guest.prx') + check_windows_deferred(deferred_converted.read_bytes(), [PREINIT_SLOTS, PREINIT_SLOTS + 8]) + if os.name == 'nt': + run = subprocess.run([str(windows / 'output.exe')], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + if linux or macos or os.name == 'nt': + print('Guest preinit tests passed') + else: + print('Guest preinit tests passed: native execution skipped') + + +if __name__ == '__main__': + main() diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 9aff6ab25b..f19908894b 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -47,7 +47,7 @@ flowchart LR ## Windows deferred module initialization -Runtime-loaded Windows modules export `__aps5_guest_initialize`: the entry RVA, the initializer count and the RVAs of initializer pointer slots. [DynamicLoader.cpp](../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) consumes the first two words in the mapped image after initialization, so another reference to that image does not run it again. Unloading the image discards this state; a later load reads the original table from the file, including when Windows reuses the same base address. Native references outside the guest handle table retain the same initialized image. +Runtime-loaded Windows modules export `__aps5_guest_initialize`: the entry RVA, the initializer count and the RVAs of initializer pointer slots. [DynamicLoader.cpp](../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) consumes the first two words in the mapped image after initialization, so another reference to that image does not run it again. Unloading the image discards this state; a later load reads the original table from the file, including when Windows reuses the same base address. Native references outside the guest handle table retain the same initialized image. A module with a preinit array exports a thunk as its entry RVA that runs the preinit entries before the module initializer. While initialization runs, the count is `UINT32_MAX` and the entry word holds the initializing Windows thread ID. A different thread waits on the loader's condition variable; the owning thread can recursively open its module. Completion clears both words, restores the table's original page protection and wakes waiters. An exception leaves the count at `UINT32_MAX` with thread ID 0, so another load rejects that image until it is unloaded. The table's file format is unchanged and its initializer slots remain intact. diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 9c5f5bbaa6..370ea5f68e 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -4,6 +4,7 @@ - [Executable guest imports](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - on Linux and macOS, a relocated executable import resolved to one guest provider requires that module and its dependencies at startup, even when it was discovered through `--module-dir` without a DT_NEEDED entry. Unreferenced symbols and no-op relocations do not require an otherwise deferred module. Console module-start ordering is unmeasured. +- Guest `DT_PREINIT_ARRAY` entries are run with `(0, NULL, NULL)` before the module initializer by the [Linux](../../../core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp), [macOS](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) and [Windows](../../../core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp) outputs, since `ld.so` runs a shared object's preinit array only for the main executable; what the console passes to these entries is not measured. A module whose preinit array is non-empty and whose initializer is absent is rejected, because the Linux stub that would run the entries exists only with one. - [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle, so everything it prints is lost, including the entry stub's errors and crash reports (#3282). - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. From 36baa5fcad847c056e6261a1ae514ff25cf936cf Mon Sep 17 00:00:00 2001 From: Ayamullah Khan Date: Sat, 17 Oct 2026 20:12:31 +0530 Subject: [PATCH 2/4] test(relinker): run the guest preinit test with verify_asan_link_order=0 guest_preinit loads the instrumented guest_module_args_host.prx into Python, so in an ANYPS5_ENABLE_ASAN build it needs the same verify_asan_link_order=0 as the other module tests; without it, it fails with "ASan runtime does not come first in initial library list". add_asan_test_options does nothing outside ASan builds. Signed-off-by: Ayamullah Khan --- core/relinker/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fad2923146..fb33a63019 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -542,6 +542,7 @@ if(BUILD_TESTING) add_asan_test_options(guest_module_args verify_asan_link_order=0) add_asan_test_options(guest_deferred_dependencies verify_asan_link_order=0) add_asan_test_options(guest_module_fini verify_asan_link_order=0) + add_asan_test_options(guest_preinit verify_asan_link_order=0) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) set(windows_stdio_host_arguments) From 6574ade0334269810192e0902797750e5402eb17 Mon Sep 17 00:00:00 2001 From: Ayamullah Khan Date: Sun, 18 Oct 2026 21:13:01 +0530 Subject: [PATCH 3/4] fix(relinker): pop the guest preinit thunk registers in reverse order The Windows deferred preinit thunk pushed r12, r13 and r14 and then popped r12, r13 and r14, so the caller got r12 and r14 back swapped. InitializeDeferredModule keeps the module base, table and count live across the call to the thunk, so a compiler that keeps any of them in r12 or r14 sees corrupted loader state after opening a deferred module that has a preinit array. Pop in reverse push order instead and pin the new bytes in test_guest_preinit. --- .../elfpatcher/src/windows/WindowsGuestModuleWriter.cpp | 2 +- core/relinker/relinker/tests/test_guest_preinit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 59f9d874c0..f8a0a0d0ae 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -172,7 +172,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI code.Emit({0x4c, 0x89, 0xe7, 0x4c, 0x89, 0xee, 0x4c, 0x89, 0xf2}); code.Rip({0x48, 0x8b, 0x05}, slotsRva + static_cast(guest.PreInitArray.size()) * 8); code.Emit({0xff, 0xd0}); - code.Emit({0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e, 0xc3}); + code.Emit({0x41, 0x5e, 0x41, 0x5d, 0x41, 0x5c, 0xc3}); auto thunk = code.TakeBytes(); while (thunk.size() % 8 != 0) thunk.push_back(0x90); for (const auto slot : guest.PreInitArray) { diff --git a/core/relinker/relinker/tests/test_guest_preinit.py b/core/relinker/relinker/tests/test_guest_preinit.py index d6df4b5846..4d8ac44a63 100644 --- a/core/relinker/relinker/tests/test_guest_preinit.py +++ b/core/relinker/relinker/tests/test_guest_preinit.py @@ -173,7 +173,7 @@ def check_windows_deferred(data, slots): restore = 15 + 23 * len(slots) assert code[restore:restore + 12] == b'\x4c\x89\xe7\x4c\x89\xee\x4c\x89\xf2\x48\x8b\x05', code[restore:restore + 16] assert code[restore + 16:restore + 18] == b'\xff\xd0', code[restore:restore + 25] - assert code[restore + 18:restore + 25] == b'\x41\x5c\x41\x5d\x41\x5e\xc3', code[restore:restore + 25] + assert code[restore + 18:restore + 25] == b'\x41\x5e\x41\x5d\x41\x5c\xc3', code[restore:restore + 25] slots_offset = code_size + (8 - code_size % 8) % 8 slots_rva = ginitc_rva + slots_offset for index, slot in enumerate(slots): From e751268ad9fe45fda6dd1085df8951d434b4a505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 18 Oct 2026 19:09:58 +0200 Subject: [PATCH 4/4] test(relinker): provide libkernel.prx to the native guest preinit load Co-Authored-By: Claude Sonnet 5.5 --- core/relinker/relinker/tests/test_guest_preinit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/relinker/relinker/tests/test_guest_preinit.py b/core/relinker/relinker/tests/test_guest_preinit.py index 4d8ac44a63..c2b2bbc7b5 100644 --- a/core/relinker/relinker/tests/test_guest_preinit.py +++ b/core/relinker/relinker/tests/test_guest_preinit.py @@ -240,6 +240,7 @@ def main(): libraries = work / 'libs' libraries.mkdir() shutil.copyfile(host, libraries / 'libc.prx') + shutil.copyfile(host, libraries / 'libkernel.prx') load_and_record(work, libraries / 'libc.prx', converted) mach = work / 'macos' mach_modules = mach / 'sce_module'