From 00d6b33126904043ced47dcd98a99282c50fe164 Mon Sep 17 00:00:00 2001 From: Adnan Khan Date: Thu, 22 Oct 2026 12:48:19 +0530 Subject: [PATCH 1/3] feat(relinker): report needed modules shipped outside the module directories --- core/relinker/cli/include/NativeModules.hpp | 4 + core/relinker/cli/src/NativeModules.cpp | 84 +++++++++++++++---- core/relinker/main.cpp | 8 +- .../tests/test_guest_needed_modules.py | 60 +++++++++++++ docs/user/USAGE.md | 2 + 5 files changed, 141 insertions(+), 17 deletions(-) diff --git a/core/relinker/cli/include/NativeModules.hpp b/core/relinker/cli/include/NativeModules.hpp index 66139266ca..31d54f5477 100644 --- a/core/relinker/cli/include/NativeModules.hpp +++ b/core/relinker/cli/include/NativeModules.hpp @@ -1,11 +1,13 @@ #pragma once #include +#include namespace Cli { struct NativeModules { std::filesystem::path Root; + std::vector Directories; std::vector RuntimeDirectories; std::vector Modules; }; @@ -14,4 +16,6 @@ bool SameDirectory(const std::filesystem::path& first, const std::filesystem::pa NativeModules ReadModules(const std::filesystem::path& inputPath, const std::filesystem::path& sceModulePath, const std::set& excludedModules, const std::vector& moduleDirectories); +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, std::ostream& out); + } diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index f01ac374d4..a55ea22605 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -2,13 +2,46 @@ #include #include #include +#include #include +#include #include #include #include +#include +#include namespace Cli { +namespace { + +struct CandidateHeader { + bool Elf = false; + bool Self = false; + std::uint16_t Type = 0; +}; + +CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + char header[18]{}; + stream.read(header, sizeof header); + if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); + CandidateHeader result; + if (stream.gcount() < 4) return result; + const auto byte = [&](const std::size_t index) { return static_cast(header[index]); }; + result.Self = (byte(0) == 0x4f && byte(1) == 0x15 && byte(2) == 0x3d && byte(3) == 0x1d) || (byte(0) == 0x54 && byte(1) == 0x14 && byte(2) == 0xf5 && byte(3) == 0xee); + result.Elf = byte(0) == 0x7f && header[1] == 'E' && header[2] == 'L' && header[3] == 'F'; + if (result.Elf && stream.gcount() == sizeof header) result.Type = static_cast(byte(16) | (byte(17) << 8)); + return result; +} + +std::string_view ModuleStem(const std::string_view name) { + return name.substr(0, name.size() - Domain::ModuleSuffix(name).size()); +} + +} + bool SameDirectory(const std::filesystem::path& first, const std::filesystem::path& second) { if (std::filesystem::exists(first) && std::filesystem::exists(second)) return std::filesystem::equivalent(first, second); return std::filesystem::weakly_canonical(first) == std::filesystem::weakly_canonical(second); @@ -45,17 +78,6 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil } std::vector paths; std::set unmatchedExclusions = excludedModules; - const auto isElf = [](const std::filesystem::path& path, bool& self) { - std::ifstream stream(path, std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); - char magic[4]{}; - stream.read(magic, 4); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); - if (stream.gcount() != 4) return false; - const auto byte = [&](const std::size_t index) { return static_cast(magic[index]); }; - self = (byte(0) == 0x4f && byte(1) == 0x15 && byte(2) == 0x3d && byte(3) == 0x1d) || (byte(0) == 0x54 && byte(1) == 0x14 && byte(2) == 0xf5 && byte(3) == 0xee); - return byte(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: " + Io::Utf8Path(directory)); for (const auto& entry : std::filesystem::directory_iterator(directory)) { @@ -66,9 +88,9 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil } if (!entry.is_regular_file()) continue; if (Io::SameFile(entry.path(), inputPath)) continue; - bool self = false; - if (isElf(entry.path(), self)) paths.push_back(entry.path()); - else if (self) + const auto header = ReadCandidateHeader(entry.path()); + if (header.Elf) paths.push_back(entry.path()); + else if (header.Self) std::cout << "Skipping SELF container: " << entry.path().lexically_relative(root).generic_string() << " (unwrap it before relinking)\n"; } @@ -77,6 +99,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil std::sort(paths.begin(), paths.end()); NativeModules result; result.Root = root; + result.Directories = std::move(directories); result.RuntimeDirectories = std::move(runtimeDirectories); for (const auto& path : paths) result.Modules.push_back({path, Io::FileReader().Read(Io::Utf8Path(path))}); std::map sonames; @@ -107,4 +130,37 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil return result; } +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, std::ostream& out) { + std::map> stems; + for (const auto& name : neededLibraries) + if (!Domain::ModuleSuffix(name).empty()) stems[std::string(ModuleStem(name))].push_back(name); + if (stems.empty()) return; + struct Found { + std::filesystem::path Relative; + std::string Name; + bool Self; + }; + std::vector found; + for (auto entry = std::filesystem::recursive_directory_iterator(modules.Root, std::filesystem::directory_options::skip_permission_denied); entry != std::filesystem::recursive_directory_iterator(); ++entry) { + if (entry->is_directory()) { + if (std::any_of(modules.Directories.begin(), modules.Directories.end(), [&](const auto& directory) { return SameDirectory(directory, entry->path()); })) entry.disable_recursion_pending(); + continue; + } + if (!entry->is_regular_file()) continue; + const auto filename = Io::Utf8Path(entry->path().filename()); + if (Domain::ModuleSuffix(filename).empty()) continue; + const auto stem = stems.find(std::string(ModuleStem(filename))); + if (stem == stems.end()) continue; + const auto header = ReadCandidateHeader(entry->path()); + if (!header.Self && !(header.Elf && header.Type == 0xfe18)) continue; + for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header.Self}); + } + std::sort(found.begin(), found.end(), [](const auto& left, const auto& right) { return std::tie(left.Relative, left.Name) < std::tie(right.Relative, right.Name); }); + for (const auto& [relative, name, self] : found) { + const auto directory = relative.parent_path().generic_string(); + out << "Needed module not converted: " << name << " at " << relative.generic_string() << "; add --module-dir " << (directory.empty() ? "." : directory) + << (self ? " and unwrap the SELF container" : "") << '\n'; + } +} + } diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 98bca27396..49ae7b38ac 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -45,11 +45,12 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, input.Executable = fileReader.Read(inputPath); input.InputPath = std::filesystem::absolute(Io::NativePath(inputPath)).lexically_normal(); input.OutputPath = Io::NativePath(absPath); + Cli::NativeModules modules; if (!args.skipSceModule) { - auto modules = Cli::ReadModules(std::filesystem::absolute(Io::NativePath(args.inputPath)).lexically_normal(), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); - input.ModuleRoot = std::move(modules.Root); + modules = Cli::ReadModules(std::filesystem::absolute(Io::NativePath(args.inputPath)).lexically_normal(), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); + input.ModuleRoot = modules.Root; input.Modules = std::move(modules.Modules); - input.RuntimeDirectories = std::move(modules.RuntimeDirectories); + input.RuntimeDirectories = modules.RuntimeDirectories; } const auto iconPath = Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png"; if (args.toWindows && std::filesystem::exists(iconPath)) input.Icon = fileReader.Read(Io::Utf8Path(iconPath)); @@ -75,6 +76,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, progress.IncludeFile(Io::Utf8Path(input.OutputPath)); for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); + if (!args.skipSceModule) Cli::ReportUnconvertedNeeded(modules, converted.Analysis.DynamicSection.NeededLibraries, std::cout); return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase}; } diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index 6b2b18f2c8..99295fd68c 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -86,11 +86,71 @@ def check_guest_needed(relinker, work): assert not output.exists() and not (case / "app0").exists(), output +def check_needed_outside(relinker, work): + self_container = bytes.fromhex("4f153d1d") + b"\0" * 60 + sce_module = module_with_symbol(True) + struct.pack_into(" An explicit `--module-dir` also works when the module parent has no `sce_module`, `sce_modules` or `prx` directory. For a homebrew bundle with shared libraries beside its executable, use `--module-dir .`; no empty standard directory is required. Without an explicit module directory, the standard-directory requirement remains. +A module the executable needs that is not converted is looked for under the module parent directory, outside the module directories. Each SELF container or `ET_SCE_DYNAMIC` ELF found there whose name matches, up to the `.prx`, `.sprx`, `.suprx` or `.debug_prx` suffix, is reported after the conversion with the `--module-dir` that would include it; the output still refers to a host library of that name. + Linux output: ```sh From f3dcf6d8bef1cf4a27258aef2ffe11e75dec8056 Mon Sep 17 00:00:00 2001 From: Adnan Khan Date: Thu, 22 Oct 2026 14:02:37 +0530 Subject: [PATCH 2/3] fix(relinker): skip unreadable candidates and fold case for --windows in the needed-module report --- core/relinker/cli/include/NativeModules.hpp | 2 +- core/relinker/cli/src/NativeModules.cpp | 34 +++++++++++++------ core/relinker/main.cpp | 2 +- .../tests/test_guest_needed_modules.py | 18 ++++++++++ docs/user/USAGE.md | 2 +- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/core/relinker/cli/include/NativeModules.hpp b/core/relinker/cli/include/NativeModules.hpp index 31d54f5477..862acfcf0c 100644 --- a/core/relinker/cli/include/NativeModules.hpp +++ b/core/relinker/cli/include/NativeModules.hpp @@ -16,6 +16,6 @@ bool SameDirectory(const std::filesystem::path& first, const std::filesystem::pa NativeModules ReadModules(const std::filesystem::path& inputPath, const std::filesystem::path& sceModulePath, const std::set& excludedModules, const std::vector& moduleDirectories); -void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, std::ostream& out); +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, bool foldCase, std::ostream& out); } diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index a55ea22605..d10056fa28 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -21,12 +22,12 @@ struct CandidateHeader { std::uint16_t Type = 0; }; -CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { +std::optional TryReadCandidateHeader(const std::filesystem::path& path) { std::ifstream stream(path, std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + if (!stream) return std::nullopt; char header[18]{}; stream.read(header, sizeof header); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); + if (stream.bad()) return std::nullopt; CandidateHeader result; if (stream.gcount() < 4) return result; const auto byte = [&](const std::size_t index) { return static_cast(header[index]); }; @@ -36,6 +37,17 @@ CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { return result; } +CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { + const auto header = TryReadCandidateHeader(path); + if (!header) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + return *header; +} + +std::string FoldedName(std::string name, const bool foldCase) { + if (foldCase) for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + return name; +} + std::string_view ModuleStem(const std::string_view name) { return name.substr(0, name.size() - Domain::ModuleSuffix(name).size()); } @@ -130,10 +142,12 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil return result; } -void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, std::ostream& out) { +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, const bool foldCase, std::ostream& out) { std::map> stems; - for (const auto& name : neededLibraries) - if (!Domain::ModuleSuffix(name).empty()) stems[std::string(ModuleStem(name))].push_back(name); + for (const auto& name : neededLibraries) { + const auto folded = FoldedName(name, foldCase); + if (!Domain::ModuleSuffix(folded).empty()) stems[std::string(ModuleStem(folded))].push_back(name); + } if (stems.empty()) return; struct Found { std::filesystem::path Relative; @@ -147,13 +161,13 @@ void ReportUnconvertedNeeded(const NativeModules& modules, const std::vectoris_regular_file()) continue; - const auto filename = Io::Utf8Path(entry->path().filename()); + const auto filename = FoldedName(Io::Utf8Path(entry->path().filename()), foldCase); if (Domain::ModuleSuffix(filename).empty()) continue; const auto stem = stems.find(std::string(ModuleStem(filename))); if (stem == stems.end()) continue; - const auto header = ReadCandidateHeader(entry->path()); - if (!header.Self && !(header.Elf && header.Type == 0xfe18)) continue; - for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header.Self}); + const auto header = TryReadCandidateHeader(entry->path()); + if (!header || (!header->Self && !(header->Elf && header->Type == 0xfe18))) continue; + for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header->Self}); } std::sort(found.begin(), found.end(), [](const auto& left, const auto& right) { return std::tie(left.Relative, left.Name) < std::tie(right.Relative, right.Name); }); for (const auto& [relative, name, self] : found) { diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 49ae7b38ac..904f129ad8 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -76,7 +76,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, progress.IncludeFile(Io::Utf8Path(input.OutputPath)); for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); - if (!args.skipSceModule) Cli::ReportUnconvertedNeeded(modules, converted.Analysis.DynamicSection.NeededLibraries, std::cout); + if (!args.skipSceModule) Cli::ReportUnconvertedNeeded(modules, converted.Analysis.DynamicSection.NeededLibraries, args.toWindows, std::cout); return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase}; } diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index 99295fd68c..c6b04179dc 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -143,6 +143,24 @@ def check_needed_outside(relinker, work): (case / "needed.prx").write_bytes(sce_module) assert convert(case, ["--skip-sce-module"]) == [] + case = work / "outside-case" + (case / "sce_module").mkdir(parents=True) + (case / "Media").mkdir() + (case / "Media" / "NEEDED.PRX").write_bytes(sce_module) + assert convert(case) == [] + assert convert(case, ["--windows"]) == [message + "needed.prx at Media/NEEDED.PRX; add --module-dir Media"] + + if os.name != "nt" and os.geteuid() != 0: + case = work / "outside-unreadable" + (case / "sce_module").mkdir(parents=True) + (case / "Media").mkdir() + (case / "Media" / "needed.prx").write_bytes(sce_module) + (case / "needed.prx").write_bytes(self_container) + (case / "needed.prx").chmod(0) + try: + assert convert(case) == [message + "needed.prx at Media/needed.prx; add --module-dir Media"] + finally: + (case / "needed.prx").chmod(0o644) def main(): diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 20e78e83e2..c781c7ee49 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -27,7 +27,7 @@ relinker [options] An explicit `--module-dir` also works when the module parent has no `sce_module`, `sce_modules` or `prx` directory. For a homebrew bundle with shared libraries beside its executable, use `--module-dir .`; no empty standard directory is required. Without an explicit module directory, the standard-directory requirement remains. -A module the executable needs that is not converted is looked for under the module parent directory, outside the module directories. Each SELF container or `ET_SCE_DYNAMIC` ELF found there whose name matches, up to the `.prx`, `.sprx`, `.suprx` or `.debug_prx` suffix, is reported after the conversion with the `--module-dir` that would include it; the output still refers to a host library of that name. +A module the executable needs that is not converted is looked for under the module parent directory, outside the module directories. Each SELF container or `ET_SCE_DYNAMIC` ELF found there whose name matches, up to the `.prx`, `.sprx`, `.suprx` or `.debug_prx` suffix (ignoring case with `--windows`, as the guest module match does), is reported after the conversion with the `--module-dir` that would include it; the output still refers to a host library of that name. A file that cannot be read is not reported. Linux output: From 37c516ad2edc6d33f4369b7e306df34a35d41591 Mon Sep 17 00:00:00 2001 From: Adnan Khan Date: Sun, 25 Oct 2026 12:24:22 +0530 Subject: [PATCH 3/3] fix(relinker): keep the needed-module report best-effort on traversal and status errors --- core/relinker/cli/src/NativeModules.cpp | 19 +++++++++++++---- .../tests/test_guest_needed_modules.py | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index d10056fa28..6163063c69 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace Cli { @@ -155,16 +156,26 @@ void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector found; - for (auto entry = std::filesystem::recursive_directory_iterator(modules.Root, std::filesystem::directory_options::skip_permission_denied); entry != std::filesystem::recursive_directory_iterator(); ++entry) { - if (entry->is_directory()) { - if (std::any_of(modules.Directories.begin(), modules.Directories.end(), [&](const auto& directory) { return SameDirectory(directory, entry->path()); })) entry.disable_recursion_pending(); + std::error_code error; + for (auto entry = std::filesystem::recursive_directory_iterator(modules.Root, std::filesystem::directory_options::skip_permission_denied, error); !error && entry != std::filesystem::recursive_directory_iterator(); entry.increment(error)) { + const bool directory = entry->is_directory(error); + if (error) { + error.clear(); + continue; + } + if (directory) { + if (std::any_of(modules.Directories.begin(), modules.Directories.end(), [&](const auto& scanned) { return std::filesystem::equivalent(scanned, entry->path(), error) && !error; })) entry.disable_recursion_pending(); + error.clear(); continue; } - if (!entry->is_regular_file()) continue; const auto filename = FoldedName(Io::Utf8Path(entry->path().filename()), foldCase); if (Domain::ModuleSuffix(filename).empty()) continue; const auto stem = stems.find(std::string(ModuleStem(filename))); if (stem == stems.end()) continue; + if (!entry->is_regular_file(error) || error) { + error.clear(); + continue; + } const auto header = TryReadCandidateHeader(entry->path()); if (!header || (!header->Self && !(header->Elf && header->Type == 0xfe18))) continue; for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header->Self}); diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index c6b04179dc..4cd66ab6f5 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -162,6 +162,27 @@ def check_needed_outside(relinker, work): finally: (case / "needed.prx").chmod(0o644) + if os.name != "nt": + case = work / "outside-loops" + (case / "sce_module").mkdir(parents=True) + (case / "Media").mkdir() + (case / "Media" / "needed.prx").write_bytes(sce_module) + os.symlink("loop", case / "loop") + os.symlink("needed.prx", case / "needed.prx") + os.symlink("missing.prx", case / "Media" / "needed.sprx") + (case / "Media" / "Nested").mkdir() + os.symlink("..", case / "Media" / "Nested" / "needed.suprx") + expected = [message + "needed.prx at Media/needed.prx; add --module-dir Media"] + assert convert(case) == expected + if os.geteuid() != 0: + (case / "locked").mkdir() + (case / "locked" / "needed.prx").write_bytes(sce_module) + (case / "locked").chmod(0) + try: + assert convert(case) == expected + finally: + (case / "locked").chmod(0o755) + def main(): relinker = Path(sys.argv[1]).resolve()