From 1a9ef2f25d45385988913fd2da98308f4601369c Mon Sep 17 00:00:00 2001 From: Lexroid Date: Thu, 8 Oct 2026 23:42:06 +0300 Subject: [PATCH 1/2] fix(relinker): match needed module names that differ only in the extension A title's executable can name a bundled module differently from the file it ships: Dying Light 2 needs EOSSDK-PS5-Shipping.debug_prx and ships EOSSDK-PS5-Shipping.prx. Needed names were matched by exact filename, so the module was skipped without an error and start-up failed to load it. Renaming the file does not help either: the title later loads the module at run time by its shipped name (/app0//EOSSDK-PS5-Shipping.prx), and dlopen looks for that name's .guest.prx (#1794). When no file has the needed name, a module whose name differs only in its .prx, .sprx or .debug_prx extension now satisfies it, in the module directories or elsewhere under the input. The output keeps the shipped file name, and the needed name is added as another name of that guest module. Two such candidates for one needed name are ambiguous, as before. Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/guest/GuestModuleBuilder.cpp | 49 +++++++++++++++++-- .../tests/test_guest_needed_modules.py | 31 ++++++++++-- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 051c506f4..69b66183a 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -8,7 +8,11 @@ #include #include #include +#include #include +#include +#include +#include namespace Relinker { @@ -29,6 +33,13 @@ std::vector ReadNeededNames(const Domain::SysVDynamicSection& dynam return names; } +std::string ModuleStem(const std::string& name) { + for (const std::string_view suffix : {".debug_prx", ".sprx", ".prx"}) { + if (name.size() > suffix.size() && name.ends_with(suffix)) return name.substr(0, name.size() - suffix.size()); + } + return {}; +} + } std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool toIntel, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set& excludedModules) const { @@ -68,22 +79,45 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } const auto neededNames = ReadNeededNames(dynamic); std::set missingNeeded; + std::map neededAliases; for (const auto& name : neededNames) { 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 (std::any_of(paths.begin(), paths.end(), [&](const auto& path) { return path.filename().string() == name; })) continue; + const auto stem = ModuleStem(name); + const auto alias = stem.empty() ? paths.end() : std::find_if(paths.begin(), paths.end(), [&](const auto& path) { return ModuleStem(path.filename().string()) == stem; }); + if (alias != paths.end()) neededAliases.emplace(name, *alias); + else missingNeeded.insert(name); } if (!missingNeeded.empty()) { std::map found; + std::map foundByStem; + const auto record = [](std::map& matches, const std::string& name, const std::filesystem::path& path) { + if (!matches.emplace(name, path).second) throw Domain::RelinkerException("Ambiguous needed module: " + matches.at(name).string() + " and " + path.string()); + }; 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; } + if (!it->is_regular_file()) 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()); + const auto stem = ModuleStem(name); + std::vector stemMatches; + if (!stem.empty()) { + for (const auto& needed : missingNeeded) if (needed != name && ModuleStem(needed) == stem) stemMatches.push_back(needed); + } + if ((!missingNeeded.contains(name) && stemMatches.empty()) || !isElf(it->path())) continue; + if (missingNeeded.contains(name)) record(found, name, it->path()); + for (const auto& needed : stemMatches) record(foundByStem, needed, it->path()); + } + for (const auto& name : missingNeeded) { + const auto exact = found.find(name); + const auto byStem = foundByStem.find(name); + if (exact == found.end() && byStem == foundByStem.end()) continue; + const auto& path = exact != found.end() ? exact->second : byStem->second; + if (exact == found.end()) neededAliases.emplace(name, path); + if (std::find(paths.begin(), paths.end(), path) == paths.end()) paths.push_back(path); } - 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()); @@ -140,6 +174,13 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } if (windows) windowsGuestFiles.emplace(foldFilename(images[index].SourcePath.filename().string()), index); } + for (const auto& [name, path] : neededAliases) { + const auto image = std::find_if(images.begin(), images.end(), [&](const auto& candidate) { return candidate.SourcePath == path; }); + if (image == images.end()) throw Domain::RelinkerException("Needed module alias has no guest image: " + name); + const auto index = static_cast(image - images.begin()); + const auto [found, inserted] = guestNames.emplace(name, index); + if (!inserted && found->second != index) throw Domain::RelinkerException("Ambiguous guest dependency name: " + name); + } const auto findGuest = [&](const std::string& name) { const auto exact = guestNames.find(name); if (exact != guestNames.end() || !windows) return exact; diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index 5d3fd88dd..bc15f23e3 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -12,9 +12,9 @@ from test_guest_module_directories import module_with_symbol, needed_libraries NEEDED = b"needed.prx" -def executable_with_needed(): +def executable_with_needed(needed=NEEDED): image = main_fixture() - strings = b"\0" + NEEDED + b"\0" + strings = b"\0" + needed + b"\0" image[0x4800:0x4800 + len(strings)] = strings tags = [] for position in range(0x4600, 0x4600 + 0x200, 16): @@ -35,10 +35,10 @@ def main(): with tempfile.TemporaryDirectory(prefix="anyps5-needed-modules-") as directory: work = Path(directory) - def convert(case, windows): + def convert(case, windows, needed=NEEDED): (case / "sce_module").mkdir(parents=True, exist_ok=True) source = case / "input.elf" - source.write_bytes(executable_with_needed()) + source.write_bytes(executable_with_needed(needed)) output = case / ("output.exe" if windows else "output.elf") result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(source), str(output)], capture_output=True, text=True, timeout=30) @@ -63,6 +63,29 @@ def main(): needed = needed_libraries(output.read_bytes()) assert needed == ["$ORIGIN/app0/Media/Modules/needed.prx.guest.prx"], needed + for directory in ("Media/Modules", "sce_module"): + case = work / f"{windows}-debug-name-{directory.replace('/', '-')}" + (case / directory).mkdir(parents=True) + (case / directory / "needed.prx").write_bytes(module_with_symbol(True)) + result, output = convert(case, windows, b"needed.debug_prx") + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = case / "app0" / directory / "needed.prx.guest.prx" + assert list((case / "app0").rglob("*.guest.prx")) == [artifact], list((case / "app0").rglob("*")) + if windows and os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + if not windows: + needed = needed_libraries(output.read_bytes()) + assert needed == [f"$ORIGIN/app0/{directory}/needed.prx.guest.prx"], needed + + case = work / f"{windows}-debug-name-ambiguous" + (case / "first").mkdir(parents=True) + (case / "first" / "needed.prx").write_bytes(module_with_symbol(True)) + (case / "first" / "needed.sprx").write_bytes(module_with_symbol(True)) + result, output = convert(case, windows, b"needed.debug_prx") + assert result.returncode == 2 and "Ambiguous needed module" in result.stderr, result.stderr + assert not output.exists(), output + case = work / f"{windows}-ambiguous" for name in ("first", "second"): (case / name).mkdir(parents=True) From 0745d803e5736fa8498baf0c9f883da62bb6b23f Mon Sep 17 00:00:00 2001 From: Lexroid Date: Sat, 10 Oct 2026 00:29:45 +0300 Subject: [PATCH 2/2] fix(relinker): fold case when matching needed module stems on Windows Palworld's eboot needs EOSSDK-PS5-Shipping.debug_prx while the dump ships prx/eossdk-ps5-shipping.prx. On Windows, compare module stems without ASCII case, and let a name that already resolves to a guest image (exact or case-folded filename, or soname) win over a stem alias. Reported and diagnosed by @Rohan03x in #1830. Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/guest/GuestModuleBuilder.cpp | 28 +++++++++++-------- .../tests/test_guest_needed_modules.py | 12 ++++++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 69b66183a..a874188d9 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace Relinker { @@ -33,7 +34,13 @@ std::vector ReadNeededNames(const Domain::SysVDynamicSection& dynam return names; } -std::string ModuleStem(const std::string& name) { +std::string FoldFilename(std::string name) { + for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + return name; +} + +std::string ModuleStem(std::string name, const bool windows) { + if (windows) name = FoldFilename(std::move(name)); for (const std::string_view suffix : {".debug_prx", ".sprx", ".prx"}) { if (name.size() > suffix.size() && name.ends_with(suffix)) return name.substr(0, name.size() - suffix.size()); } @@ -82,9 +89,9 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::map neededAliases; for (const auto& name : neededNames) { if (excludedModules.contains(name)) continue; - if (std::any_of(paths.begin(), paths.end(), [&](const auto& path) { return path.filename().string() == name; })) continue; - const auto stem = ModuleStem(name); - const auto alias = stem.empty() ? paths.end() : std::find_if(paths.begin(), paths.end(), [&](const auto& path) { return ModuleStem(path.filename().string()) == stem; }); + if (std::any_of(paths.begin(), paths.end(), [&](const auto& path) { return path.filename().string() == name || (windows && FoldFilename(path.filename().string()) == FoldFilename(name)); })) continue; + const auto stem = ModuleStem(name, windows); + const auto alias = stem.empty() ? paths.end() : std::find_if(paths.begin(), paths.end(), [&](const auto& path) { return ModuleStem(path.filename().string(), windows) == stem; }); if (alias != paths.end()) neededAliases.emplace(name, *alias); else missingNeeded.insert(name); } @@ -101,10 +108,10 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } if (!it->is_regular_file()) continue; const auto name = it->path().filename().string(); - const auto stem = ModuleStem(name); + const auto stem = ModuleStem(name, windows); std::vector stemMatches; if (!stem.empty()) { - for (const auto& needed : missingNeeded) if (needed != name && ModuleStem(needed) == stem) stemMatches.push_back(needed); + for (const auto& needed : missingNeeded) if (needed != name && ModuleStem(needed, windows) == stem) stemMatches.push_back(needed); } if ((!missingNeeded.contains(name) && stemMatches.empty()) || !isElf(it->path())) continue; if (missingNeeded.contains(name)) record(found, name, it->path()); @@ -162,19 +169,16 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } std::map guestNames; std::map windowsGuestFiles; - const auto foldFilename = [](std::string name) { - for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); - return name; - }; for (std::size_t index = 0; index < images.size(); ++index) { for (const auto& name : {images[index].SourcePath.filename().string(), images[index].Soname}) { if (name.empty()) continue; const auto [found, inserted] = guestNames.emplace(name, index); if (!inserted && found->second != index) throw Domain::RelinkerException("Ambiguous guest dependency name: " + name); } - if (windows) windowsGuestFiles.emplace(foldFilename(images[index].SourcePath.filename().string()), index); + if (windows) windowsGuestFiles.emplace(FoldFilename(images[index].SourcePath.filename().string()), index); } for (const auto& [name, path] : neededAliases) { + if (guestNames.contains(name)) continue; const auto image = std::find_if(images.begin(), images.end(), [&](const auto& candidate) { return candidate.SourcePath == path; }); if (image == images.end()) throw Domain::RelinkerException("Needed module alias has no guest image: " + name); const auto index = static_cast(image - images.begin()); @@ -184,7 +188,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto findGuest = [&](const std::string& name) { const auto exact = guestNames.find(name); if (exact != guestNames.end() || !windows) return exact; - const auto file = windowsGuestFiles.find(foldFilename(name)); + const auto file = windowsGuestFiles.find(FoldFilename(name)); return file == windowsGuestFiles.end() ? guestNames.end() : guestNames.emplace(name, file->second).first; }; const auto rejectSharedImport = [&](const std::string& name, const std::string& importer) { diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index bc15f23e3..e25310eb5 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -78,6 +78,18 @@ def main(): needed = needed_libraries(output.read_bytes()) assert needed == [f"$ORIGIN/app0/{directory}/needed.prx.guest.prx"], needed + if windows: + case = work / "debug-name-case" + (case / "prx").mkdir(parents=True) + (case / "prx" / "foo-bar.prx").write_bytes(module_with_symbol(True)) + result, output = convert(case, windows, b"Foo-Bar.debug_prx") + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = case / "app0" / "prx" / "foo-bar.prx.guest.prx" + assert list((case / "app0").rglob("*.guest.prx")) == [artifact], list((case / "app0").rglob("*")) + if os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + case = work / f"{windows}-debug-name-ambiguous" (case / "first").mkdir(parents=True) (case / "first" / "needed.prx").write_bytes(module_with_symbol(True))