From d52a5f0169f5257e48023cbab35a3136b49e4ac3 Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 19:38:51 +0200 Subject: [PATCH] feat(relinker): relink needed modules outside the module directories Unity titles keep modules the executable needs in Media/Modules (PPSA12544: Il2cppUserAssemblies.prx and PS5Util.prx), outside sce_module and prx, so they were left as host libraries and startup failed to load them. A DT_NEEDED module of the executable that is not in a module directory is now searched for under the input directory and relinked like the others; two ELF files with the name are ambiguous. Guest outputs keep their directory relative to the input instead of only its last component. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 2 + core/relinker/main.cpp | 2 +- .../relinker/src/guest/GuestModuleBuilder.cpp | 45 +++++++++-- .../tests/test_guest_needed_modules.py | 77 +++++++++++++++++++ 4 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_needed_modules.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index a388ec7a3..edfe5ec1b 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -208,6 +208,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_intel_trampolines.py $) add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $) + add_test(NAME guest_needed_modules COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_modules.py $) add_test(NAME guest_module_directories COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $) add_test(NAME guest_host_libc COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 2e3b6553e..1b4db2e0d 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -135,7 +135,7 @@ int main(const int argc, char* argv[]) { << std::filesystem::path(absPath).filename().string() << "\n" << "libs/\n *.prx\napp0/\n \n"; for (const auto& artifact : guestArtifacts) - std::cout << " " << artifact.Path.parent_path().filename().string() << "/" << artifact.Path.filename().string() << '\n'; + std::cout << " " << artifact.Path.lexically_relative(std::filesystem::path(absPath).parent_path() / "app0").generic_string() << '\n'; std::cout << "Game resources and system libraries must be placed in this layout separately.\n"; if (args.runPath != "$ORIGIN/libs") std::cout << "Custom library search path (--rpath): " << args.runPath << '\n'; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index eebb504ca..b9d9f8c10 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -26,6 +26,14 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (hasPrx) directories.push_back(prx); std::vector paths; std::set unmatchedExclusions = excludedModules; + const auto isElf = [](const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + path.string()); + char magic[4]{}; + stream.read(magic, 4); + if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + path.string()); + return stream.gcount() == 4 && static_cast(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; + }; for (const auto& directory : directories) { if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string()); for (const auto& entry : std::filesystem::directory_iterator(directory)) { @@ -35,14 +43,35 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path continue; } if (!entry.is_regular_file()) continue; - std::ifstream stream(entry.path(), std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + entry.path().string()); - char magic[4]{}; - stream.read(magic, 4); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + entry.path().string()); - if (stream.gcount() == 4 && static_cast(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') paths.push_back(entry.path()); + if (isElf(entry.path())) paths.push_back(entry.path()); } } + if (dynamic.DynamicSegmentData.size() % 16 != 0) throw Domain::RelinkerException("Invalid executable dependency table"); + std::set missingNeeded; + for (std::size_t offset = 0; offset < dynamic.DynamicSegmentData.size(); offset += 16) { + if (Io::ReadU64(dynamic.DynamicSegmentData, offset) != 1) continue; + const auto nameOffset = Io::ReadU64(dynamic.DynamicSegmentData, offset + 8); + if (nameOffset >= dynamic.DynStrData.size()) throw Domain::RelinkerException("Invalid dependency string offset"); + const auto start = dynamic.DynStrData.begin() + static_cast(nameOffset); + const auto end = std::find(start, dynamic.DynStrData.end(), 0); + if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated dependency string"); + const std::string name(start, end); + if (excludedModules.contains(name)) continue; + if (std::none_of(paths.begin(), paths.end(), [&](const auto& path) { return path.filename().string() == name; })) missingNeeded.insert(name); + } + if (!missingNeeded.empty()) { + std::map found; + for (auto it = std::filesystem::recursive_directory_iterator(root); it != std::filesystem::recursive_directory_iterator(); ++it) { + if (it->is_directory() && std::find(directories.begin(), directories.end(), it->path()) != directories.end()) { + it.disable_recursion_pending(); + continue; + } + const auto name = it->path().filename().string(); + if (!it->is_regular_file() || !missingNeeded.contains(name) || !isElf(it->path())) continue; + if (!found.emplace(name, it->path()).second) throw Domain::RelinkerException("Ambiguous needed module: " + found.at(name).string() + " and " + it->path().string()); + } + for (const auto& [name, path] : found) paths.push_back(path); + } if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); std::sort(paths.begin(), paths.end()); if (paths.empty()) return {}; @@ -142,7 +171,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size()); Io::AppendString(dynamic.DynStrData, name); }; - for (const auto index : order) if (!windows) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().filename().generic_string() + "/" + images[index].OutputName); + for (const auto index : order) if (!windows) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().lexically_relative(root).generic_string() + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); std::string guestRunPath = runPath; if (!windows) { @@ -153,7 +182,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::vector artifacts; for (const auto index : order) { const auto& image = images[index]; - const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().filename().generic_string(); + const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().lexically_relative(root).generic_string(); const auto destination = std::filesystem::absolute(outputPath).parent_path() / relativeDirectory; const auto target = destination / image.OutputName; if (target.lexically_normal() == std::filesystem::absolute(outputPath).lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output"); diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py new file mode 100644 index 000000000..5d3fd88dd --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -0,0 +1,77 @@ +from pathlib import Path +import os +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import module_with_symbol, needed_libraries + + +NEEDED = b"needed.prx" + + +def executable_with_needed(): + image = main_fixture() + strings = b"\0" + NEEDED + b"\0" + image[0x4800:0x4800 + len(strings)] = strings + tags = [] + for position in range(0x4600, 0x4600 + 0x200, 16): + tag, value = struct.unpack_from("