From 5f4a28c2bcc48c2e87bc944d68b806b9752ad15e Mon Sep 17 00:00:00 2001 From: "john.maia" Date: Thu, 8 Oct 2026 11:05:52 +0100 Subject: [PATCH 1/2] fix(relinker): keep Linux imports declared from host modules off guest exports On Linux every executable or guest-module import whose NID a guest module exports was renamed to the guest-suffixed name, whatever module the import declares. A title that bundles a module exporting system NIDs then had those imports bound to it: the libc.prx loader shim of ps5-native-app-boilerplate exports placeholder functions under the libSceLibcInternal NIDs, so DOOM PPSA99666's snprintf, vsnprintf, memcpy and malloc (declared from libSceLibcInternal) returned 0 and the title never found its WAD (#1497). An import now binds to a guest export only when the module it declares is a guest module, as on Windows. Imports without import-module metadata keep the previous behaviour, and a title's own libc.prx imported from libc still binds to it. Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/guest/GuestModuleBuilder.cpp | 15 ++++++- .../relinker/tests/test_guest_symbol_names.py | 45 ++++++++++++++----- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 4f8bb12bd..f885f10ed 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -206,7 +206,17 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::WriteU32(symbols, index * 24, static_cast(strings.size())); Io::AppendString(strings, name + GuestSymbolSuffix); }; + const auto declaresHost = [&](const std::string& library) { return !windows && !library.empty() && findGuest(library) == guestNames.end(); }; if (dynamic.DynSymData.size() % 24 != 0) throw Domain::RelinkerException("Invalid executable symbol table"); + std::vector executableModules(dynamic.DynSymData.size() / 24); + for (const auto* table : {&dynamic.RelaData, &dynamic.RelaPltData}) { + if (table->size() % 24 != 0) throw Domain::RelinkerException("Invalid executable relocation table"); + for (std::size_t offset = 0; offset < table->size(); offset += 24) { + const auto symbol = Io::ReadU64(*table, offset + 8) >> 32; + const auto module = dynamic.ImportModules.find(Io::ReadU64(*table, offset)); + if (symbol != 0 && symbol < executableModules.size() && module != dynamic.ImportModules.end()) executableModules[symbol] = module->second; + } + } for (std::size_t offset = 0; offset < dynamic.DynSymData.size(); offset += 24) { if (Io::ReadU16(dynamic.DynSymData, offset + 6) != 0) continue; const auto nameOffset = Io::ReadU32(dynamic.DynSymData, offset); @@ -215,6 +225,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto end = std::find(start, dynamic.DynStrData.end(), 0); if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated executable symbol name"); const std::string name(start, end); + if (declaresHost(executableModules[offset / 24])) continue; rejectSharedImport(name.substr(0, name.find('#')), Io::Utf8Path(inputPath)); if (!windows && exports.contains(name)) rename(dynamic.DynSymData, dynamic.DynStrData, offset / 24, name); } @@ -232,7 +243,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } for (std::size_t symbolIndex = 0; symbolIndex < images[index].Symbols.size(); ++symbolIndex) { const auto& symbol = images[index].Symbols[symbolIndex]; - if (symbol.Section != 0 || symbol.Name.empty()) continue; + if (symbol.Section != 0 || symbol.Name.empty() || declaresHost(symbol.Library)) continue; rejectSharedImport(symbol.Name, Io::Utf8Path(images[index].SourcePath)); const auto found = exports.find(symbol.Name); std::vector providers; @@ -257,7 +268,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path for (std::size_t index = 1; index < image.Symbols.size(); ++index) { const auto& symbol = image.Symbols[index]; const bool exported = symbol.Section != 0 && (symbol.Info >> 4) != 0 && symbol.Visibility != 1 && symbol.Visibility != 2; - if ((symbol.Section == 0 || exported) && exports.contains(symbol.Name)) rename(image.Dynamic.DynSymData, image.Dynamic.DynStrData, index, symbol.Name); + if (((symbol.Section == 0 && !declaresHost(symbol.Library)) || exported) && exports.contains(symbol.Name)) rename(image.Dynamic.DynSymData, image.Dynamic.DynStrData, index, symbol.Name); } } } diff --git a/core/relinker/relinker/tests/test_guest_symbol_names.py b/core/relinker/relinker/tests/test_guest_symbol_names.py index a99180830..dc01e84e3 100644 --- a/core/relinker/relinker/tests/test_guest_symbol_names.py +++ b/core/relinker/relinker/tests/test_guest_symbol_names.py @@ -5,6 +5,7 @@ import sys import tempfile from test_guest_intel_trampolines import PLAIN_SITE, elf_loads, guest_fixture, main_fixture +from test_windows_import_modules import consumer, provider DYNAMIC = 0x4800 STRINGS = 0x4A00 @@ -36,15 +37,18 @@ def guest_module(symbols): return image -def importing_executable(names): +def importing_executable(names, modules=()): image = main_fixture() - strings, offsets = string_table(names) + needed = [module + ".prx" for module in modules] + strings, offsets = string_table([*names, *needed, *modules]) image[STRINGS:STRINGS + len(strings)] = strings - for index, offset in enumerate(offsets, 1): + for index, offset in enumerate(offsets[:len(names)], 1): struct.pack_into(" Date: Sun, 11 Oct 2026 00:14:26 +0200 Subject: [PATCH 2/2] test(relinker): check the declared-host import by membership, not as the module's only import --- core/relinker/relinker/tests/test_guest_symbol_names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/relinker/relinker/tests/test_guest_symbol_names.py b/core/relinker/relinker/tests/test_guest_symbol_names.py index dc01e84e3..e37ca64ca 100644 --- a/core/relinker/relinker/tests/test_guest_symbol_names.py +++ b/core/relinker/relinker/tests/test_guest_symbol_names.py @@ -172,7 +172,7 @@ def main(): exporter = module_symbols((guests / "b.prx.guest.prx").read_bytes()) assert exporter == ["shared#guest"], exporter importer = module_symbols((guests / "c.prx.guest.prx").read_bytes()) - assert importer == ["shared"], importer + assert "shared" in importer and "shared#guest" not in importer, importer print("Guest symbol name tests passed")