From 03fae5dfcbc966a96c365db7c2ba619592703dce Mon Sep 17 00:00:00 2001 From: wail-arch <116732676+wail-arch@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:06:47 +0000 Subject: [PATCH] fix(relinker): bind SDK guest module imports on Linux A bundled module that names a console file dependency (.sprx, .suprx, .debug_prx) imports by symbol name, as homebrew built with the ps5-payload-dev SDK does. Bind those imports to the NID of the name, except names another bundled module or the executable exports, and name the module's host dependencies with the .prx library of the same stem. --- core/relinker/CMakeLists.txt | 4 + .../include/relinker/guest/GuestImage.hpp | 1 + .../relinker/src/guest/GuestImageReader.cpp | 3 + .../relinker/src/guest/GuestModuleBuilder.cpp | 37 +++++- .../tests/test_linux_guest_named_imports.py | 125 ++++++++++++++++++ docs/dev/debt/relinker.md | 2 +- 6 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 core/relinker/relinker/tests/test_linux_guest_named_imports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 9b9c265029..e2070ec8e4 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -637,6 +637,10 @@ if(BUILD_TESTING) $ $) set_tests_properties(guest_zero_initializer PROPERTIES TIMEOUT 30) add_asan_test_options(guest_zero_initializer verify_asan_link_order=0) + add_test(NAME linux_guest_named_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_guest_named_imports.py + $ $) + add_asan_test_options(linux_guest_named_imports verify_asan_link_order=0) set(windows_stdio_host_arguments) if(WIN32) if(MINGW) diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index 6be45157af..8fe4507a89 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -57,6 +57,7 @@ struct GuestImage { std::uint64_t Got = 0; bool UsePlatformTlsResolver = true; bool Symbolic = false; + std::vector NamedImports; }; class GuestImageReader { diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index a630010692..c596636aa2 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -162,6 +162,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (!importModules.emplace(value >> 48, string(value & 0xffffffffu)).second) fail("Duplicate import module ID"); std::vector neededLibraries; for (const auto offset : needed) neededLibraries.push_back(string(offset)); + const bool consoleFileNames = std::any_of(neededLibraries.begin(), neededLibraries.end(), + [](const std::string& name) { return Domain::NeededLibraryName(name) != name; }); std::map exports; for (std::uint64_t offset = 0; offset < symSize; offset += 24) { auto name = string(Io::ReadU32(bytes, symOffset + offset)); @@ -195,6 +197,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) fail("String table too large"); Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast(image.Dynamic.DynStrData.size())); Io::AppendString(image.Dynamic.DynStrData, name); + if (consoleFileNames && section == 0 && !name.empty() && name == qualifiedName) image.NamedImports.push_back(image.Symbols.size()); image.Symbols.push_back(symbol); } if (tags.contains(14)) { diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 1ffd34e19d..6940d43210 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,33 @@ std::vector ReadNeededNames(const Domain::SysVDynamicSection& dynam return names; } +std::string SymbolName(const Domain::SysVDynamicSection& dynamic, const std::size_t index) { + if (dynamic.DynSymData.size() % 24 != 0 || index >= dynamic.DynSymData.size() / 24) throw Domain::RelinkerException("Invalid symbol index"); + const auto nameOffset = Io::ReadU32(dynamic.DynSymData, index * 24); + if (nameOffset >= dynamic.DynStrData.size()) throw Domain::RelinkerException("Invalid symbol name offset"); + const auto start = dynamic.DynStrData.begin() + nameOffset; + const auto end = std::find(start, dynamic.DynStrData.end(), 0); + if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated symbol name"); + return std::string(start, end); +} + +std::set DefinedNames(const Domain::SysVDynamicSection& dynamic) { + std::set names; + for (std::size_t index = 1; index < dynamic.DynSymData.size() / 24; ++index) + if (Io::ReadU16(dynamic.DynSymData, index * 24 + 6) != 0) names.insert(SymbolName(dynamic, index)); + return names; +} + +void BindNamedImportsByNid(GuestImage& image, const std::set& kept) { + for (const auto index : image.NamedImports) { + const auto name = SymbolName(image.Dynamic, index); + if (name.ends_with(GuestSymbolSuffix) || kept.contains(name)) continue; + if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) throw Domain::RelinkerException("Guest string table too large"); + Io::WriteU32(image.Dynamic.DynSymData, index * 24, static_cast(image.Dynamic.DynStrData.size())); + Io::AppendString(image.Dynamic.DynStrData, Nid::ComputeNid(name, "")); + } +} + std::string FoldFilename(std::string name) { for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); return name; @@ -292,12 +320,14 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path return directory == "." ? std::string("app0") : "app0/" + directory; }; if (!windows) { + const auto executableExports = macos ? std::set{} : DefinedNames(dynamic); for (auto& image : images) { 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 && !declaresHost(symbol.Library)) || exported) && exports.contains(symbol.Name)) rename(image.Dynamic.DynSymData, image.Dynamic.DynStrData, index, symbol.Name); } + if (!macos) BindNamedImportsByNid(image, executableExports); } } std::vector order; @@ -314,17 +344,18 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::vector hostLibraries; std::vector> moduleHostLibraries(images.size()); std::set uniqueHosts; + const auto hostLibrary = [&](const std::string& name) { return windows || macos ? name : Domain::NeededLibraryName(name); }; const auto addHost = [&](const std::string& name, const bool startup) { if (findGuest(name) != guestNames.end()) return; if (name.empty() || name.find_first_of("/\\:$") != std::string::npos) throw Domain::RelinkerException("Invalid host dependency: " + name); - if (startup && uniqueHosts.insert(name).second) hostLibraries.push_back(name); + if (const auto host = hostLibrary(name); startup && uniqueHosts.insert(host).second) hostLibraries.push_back(host); }; for (const auto& name : neededNames) addHost(name, true); for (std::size_t index = 0; index < images.size(); ++index) { auto& hosts = moduleHostLibraries[index]; for (const auto& dependency : images[index].Dependencies) { addHost(dependency, windows || !deferred(index)); - if (findGuest(dependency) == guestNames.end()) hosts.push_back(dependency); + if (findGuest(dependency) == guestNames.end()) hosts.push_back(hostLibrary(dependency)); } if (!windows && !macos && images[index].HasInit) { if (std::find(hosts.begin(), hosts.end(), "libkernel.prx") == hosts.end()) hosts.push_back("libkernel.prx"); @@ -425,7 +456,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path dynamic.GuestModules.push_back(std::move(runtime)); std::vector neededLibraries; for (const auto& name : image.Dependencies) - if (findGuest(name) == guestNames.end()) neededLibraries.push_back(name); + if (findGuest(name) == guestNames.end()) neededLibraries.push_back(hostLibrary(name)); artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath, std::move(neededLibraries)}); progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); } diff --git a/core/relinker/relinker/tests/test_linux_guest_named_imports.py b/core/relinker/relinker/tests/test_linux_guest_named_imports.py new file mode 100644 index 0000000000..86f36b1236 --- /dev/null +++ b/core/relinker/relinker/tests/test_linux_guest_named_imports.py @@ -0,0 +1,125 @@ +import os +from pathlib import Path +import platform +import shutil +import struct +import subprocess +import sys +import tempfile + +sys.path.insert(0, str(Path(__file__).resolve().parent / "macos")) + +from guest_import_fixtures import executable +from nid import nid +from test_guest_deferred_dependencies import page_aligned +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import needed_libraries +from test_guest_needed_modules import module_with_needed +from test_guest_symbol_names import guest_module, imported_symbols, module_symbols, relink +from test_linux_load_alignment import fixture +from test_windows_import_modules import consumer + +PROVIDER = "int provided(void) __asm__(\"\\\"%s\\\"\");\nint provided(void) { return 22; }\n" + + +def guest_with_needed(symbols, needed): + image = guest_module(symbols) + size, = struct.unpack_from("" in its code, and in SuperTuxKart (PPSA69421) and Mortalfin (PPSA99141) the `jae` read from "sf" points into the VEX prefix of a 256-bit store. A direct branch target inside a scanned instruction counts only where every byte before it is a legacy prefix or REX, which a real branch can skip (a `lock` prefix, for example); a target in a VEX prefix, opcode or operand is ignored, as a jump-table entry that starts no instruction already is. A real branch to such a target, overlapping instructions as hand-written code can, is not seen, and the instruction it enters can be moved. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked.