From fd6f716cb6f47c1e9fe8cdadc0c776a053cfc3c4 Mon Sep 17 00:00:00 2001 From: RINNECODER <93304526+RINNECODER@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:05:49 -0500 Subject: [PATCH 1/4] Port upstream #4340 (GNU hash tables) by @ArjenS-LA Ported from boykopovar/AnyPS5 PR #4340 (author @ArjenS-LA), applied as a patch. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 3 + .../relinker/src/guest/GuestImageReader.cpp | 32 ++++- .../relinker/tests/test_guest_gnu_hash.py | 111 ++++++++++++++++++ 3 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_gnu_hash.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index ab124744fb3..6f199b2cf74 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -452,6 +452,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_init_array_roots.py $) add_test(NAME gnu_hash_analysis COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_gnu_hash_analysis.py $) + add_test(NAME guest_gnu_hash COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_gnu_hash.py $) + set_tests_properties(guest_gnu_hash PROPERTIES TIMEOUT 20) add_test(NAME to_rosetta COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_to_rosetta.py $) add_test(NAME syscall_number COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index e6c3545a7c3..4ebc792f49a 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -148,7 +148,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (tag == 1) needed.push_back(value); else if (tag == 0x61000045) moduleImports.push_back(value); else if (tag == 0x6100000d || tag == 0x61000043) moduleExports.push_back(value); - else if (tag < 0x60000000 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { + else if (tag < 0x60000000 || tag == 0x6ffffef5 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { if (!tags.emplace(tag, value).second) fail("Duplicate dynamic tag " + std::to_string(tag)); } } @@ -194,7 +194,35 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector std::uint64_t symSize = 0; if (tags.contains(0x6100003f)) symSize = tags.at(0x6100003f); else if (tags.contains(4)) symSize = static_cast(Io::ReadU32(bytes, translate(tags.at(4), 8) + 4)) * 24; - else fail("Missing dynamic symbol count"); + else if (tags.contains(0x6ffffef5)) { + const auto address = tags.at(0x6ffffef5); + const auto header = translate(address, 16); + const auto bucketCount = Io::ReadU32(bytes, header); + const auto firstSymbol = Io::ReadU32(bytes, header + 4); + const auto bloomCount = Io::ReadU32(bytes, header + 8); + if (bucketCount == 0 || bloomCount == 0 || firstSymbol == 0) fail("Invalid GNU hash header"); + const std::uint64_t bucketOffset = 16 + static_cast(bloomCount) * 8; + const auto chainOffset = bucketOffset + static_cast(bucketCount) * 4; + const auto start = translate(address, chainOffset); + std::uint64_t lastBucket = 0; + for (std::uint64_t index = 0; index < bucketCount; ++index) { + const auto bucket = Io::ReadU32(bytes, start + bucketOffset + index * 4); + if (bucket != 0 && bucket < firstSymbol) fail("Invalid GNU hash bucket"); + lastBucket = std::max(lastBucket, static_cast(bucket)); + } + std::uint64_t symbolCount = firstSymbol; + if (lastBucket != 0) { + symbolCount = lastBucket; + for (;;) { + const auto offset = chainOffset + (symbolCount - firstSymbol) * 4; + if (offset > std::numeric_limits::max() - address) fail("Invalid GNU hash chain"); + const auto chain = Io::ReadU32(bytes, translate(address + offset, 4)); + ++symbolCount; + if (chain & 1) break; + } + } + symSize = symbolCount * 24; + } else fail("Missing dynamic symbol count"); if (symSize == 0 || symSize % 24 != 0 || symSize / 24 > std::numeric_limits::max()) fail("Invalid dynamic symbol count"); const auto symOffset = table(6, 0x61000039, symSize); image.Dynamic.DynSymData.assign(bytes.begin() + symOffset, bytes.begin() + symOffset + symSize); diff --git a/core/relinker/relinker/tests/test_guest_gnu_hash.py b/core/relinker/relinker/tests/test_guest_gnu_hash.py new file mode 100644 index 00000000000..7b03aca665a --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_gnu_hash.py @@ -0,0 +1,111 @@ +from pathlib import Path +import os +import struct +import subprocess +import sys +import tempfile + +from test_gnu_hash_analysis import name_hash +from test_guest_intel_trampolines import main_fixture +from test_guest_symbol_names import dynamic_tags, file_offset +from test_windows_import_modules import dynamic, executable, provider + + +HASH = 0xA00 +SYMBOLS = 0xC00 +STRINGS = 0xD00 + + +def module(names=("alpha", "bravo", "charlie"), bucket_count=3): + image = provider(42) + image.extend(bytes(0x1000 - len(image))) + struct.pack_into("> 5) % 64)) + if chains: + chains[-1] |= 1 + struct.pack_into(" Date: Mon, 12 Oct 2026 20:05:49 -0500 Subject: [PATCH 2/4] Port upstream #4802 (output collisions) by @ArjenS-LA Ported from boykopovar/AnyPS5 PR #4802 (author @ArjenS-LA), applied as a patch. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 2 + core/relinker/main.cpp | 112 +++++++++++++ .../relinker/tests/test_output_collisions.py | 153 ++++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 core/relinker/relinker/tests/test_output_collisions.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 6f199b2cf74..094328b7b46 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -429,6 +429,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_magic.py $) add_test(NAME input_preservation COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $) + add_test(NAME output_collisions COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_output_collisions.py $) add_test(NAME input_file COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_file.py $) add_test(NAME input_nonascii COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 0e747853d8e..38f5e547912 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -38,6 +39,100 @@ namespace { +#ifdef _WIN32 + +class OutputIdentityDirectory { +public: + OutputIdentityDirectory(const OutputIdentityDirectory&) = delete; + OutputIdentityDirectory& operator=(const OutputIdentityDirectory&) = delete; + + explicit OutputIdentityDirectory(const std::filesystem::path& parent) { + std::random_device random; + for (unsigned attempt = 0; attempt < 64; ++attempt) { + const auto candidate = parent / (".anyps5-output-identity-" + std::to_string(random()) + "-" + std::to_string(random())); + std::error_code error; + if (std::filesystem::create_directory(candidate, error)) { + directory = candidate; + return; + } + if (error && error != std::errc::file_exists) + throw std::filesystem::filesystem_error("Cannot create output identity directory", candidate, error); + } + throw Domain::RelinkerException("Cannot reserve an output identity directory in " + Io::Utf8Path(parent)); + } + + ~OutputIdentityDirectory() { + if (directory.empty()) return; + try { + Remove(); + } catch (const std::exception& error) { + std::cerr << "FAIL: Output identity cleanup: " << error.what() << '\n'; + } + } + + std::filesystem::path GetPath() const { return directory / "file"; } + const std::filesystem::path& GetDirectory() const { return directory; } + + void Remove() { + std::filesystem::remove(GetPath()); + std::filesystem::remove(directory); + directory.clear(); + } + +private: + std::filesystem::path directory; +}; + +#endif + +bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::path& second) { + if (std::filesystem::equivalent(first, second)) return true; +#ifdef _WIN32 + if (std::filesystem::status(first).permissions() != std::filesystem::status(second).permissions() && + std::filesystem::hard_link_count(first) > 1 && std::filesystem::hard_link_count(second) > 1) { + for (const auto& path : {first, second}) { + if ((std::filesystem::status(path).permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none) + throw Domain::RelinkerException("Cannot compare read-only output identities: " + Io::Utf8Path(path)); + } + OutputIdentityDirectory firstDirectory(first.parent_path()); + OutputIdentityDirectory secondDirectory(second.parent_path()); + std::filesystem::create_hard_link(first, firstDirectory.GetPath()); + std::filesystem::create_hard_link(second, secondDirectory.GetPath()); + const bool same = std::filesystem::equivalent(firstDirectory.GetPath(), secondDirectory.GetPath()); + secondDirectory.Remove(); + firstDirectory.Remove(); + return same; + } +#endif + return false; +} + +bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { + const auto firstPath = std::filesystem::weakly_canonical(first); + const auto secondPath = std::filesystem::weakly_canonical(second); + if (firstPath == secondPath) return true; + if (std::filesystem::exists(first) && std::filesystem::exists(second)) return SameOutputFile(first, second); +#ifdef _WIN32 + auto firstName = Io::Utf8Path(firstPath.filename()); + auto secondName = Io::Utf8Path(secondPath.filename()); + for (auto* name : {&firstName, &secondName}) + for (auto& character : *name) + if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + if (firstName == secondName && std::filesystem::exists(firstPath.parent_path()) && + std::filesystem::exists(secondPath.parent_path()) && std::filesystem::equivalent(firstPath.parent_path(), secondPath.parent_path())) { + OutputIdentityDirectory probe(firstPath.parent_path()); + auto alternateName = Io::Utf8Path(probe.GetDirectory().filename()); + for (auto& character : alternateName) + if (character >= 'a' && character <= 'z') character = static_cast(character - ('a' - 'A')); + const auto alternate = firstPath.parent_path() / Io::NativePath(alternateName); + const bool same = std::filesystem::exists(alternate) && std::filesystem::equivalent(probe.GetDirectory(), alternate); + probe.Remove(); + return same; + } +#endif + return false; +} + int Run(const int argc, char* argv[]) { Cli::Args args; try { @@ -142,6 +237,23 @@ int Run(const int argc, char* argv[]) { fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); } + std::vector outputPaths{Io::NativePath(absPath)}; + for (const auto& artifact : guestArtifacts) outputPaths.push_back(artifact.Path); + outputPaths.insert(outputPaths.end(), registryPaths.begin(), registryPaths.end()); + if (desktopEntry) { + outputPaths.push_back(desktopEntry->IconPath); + outputPaths.push_back(desktopEntry->EntryPath); + } + if (args.windowsDiagnostics) outputPaths.push_back(std::filesystem::absolute("windows-diagnostics-imports.txt")); + for (std::size_t index = 0; index < outputPaths.size(); ++index) { + for (std::size_t previous = 0; previous < index; ++previous) { + const auto& first = outputPaths[index]; + const auto& second = outputPaths[previous]; + if (SameOutputDestination(first, second)) + throw Domain::RelinkerException("Output paths collide: " + Io::Utf8Path(first) + " and " + Io::Utf8Path(second)); + } + } + auto byteWriter = std::make_shared(); std::shared_ptr patcher; diff --git a/core/relinker/relinker/tests/test_output_collisions.py b/core/relinker/relinker/tests/test_output_collisions.py new file mode 100644 index 00000000000..2026412310a --- /dev/null +++ b/core/relinker/relinker/tests/test_output_collisions.py @@ -0,0 +1,153 @@ +import itertools +import json +import os +from pathlib import Path +import stat +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture +from test_linux_desktop_entry_output import png + + +def prepare(case, windows): + case.mkdir() + source = case / "input.elf" + source.write_bytes(main_fixture()) + modules = case / "sce_module" + modules.mkdir() + module = modules / "sample.prx" + module.write_bytes(guest_fixture(PLAIN_SITE)) + output = case / ("output.exe" if windows else "output.elf") + paths = { + "executable": output, + "module": case / "app0/sce_module/sample.prx.guest.prx", + "registry": case / "output.registry.json", + "module-registry": case / "output.sample.prx.guest.prx.registry.json", + } + if windows: + paths["diagnostics"] = case / "windows-diagnostics-imports.txt" + else: + (case / "sce_sys").mkdir() + (case / "sce_sys/icon0.png").write_bytes(png(1, 1)) + paths["icon"] = Path(f"{output}.png") + paths["desktop"] = Path(f"{output}.desktop") + for path in paths.values(): + path.parent.mkdir(parents=True, exist_ok=True) + options = ["--windows", "--windows-diagnostics"] if windows else [] + command = [*options, "--registry", str(source), str(output)] + return command, paths, {source: source.read_bytes(), module: module.read_bytes()} + + +def case_sensitive(directory): + probe = directory / "CaseProbe" + probe.write_bytes(b"probe") + sensitive = not (directory / "caseprobe").exists() + probe.unlink() + return sensitive + + +def main(): + relinker = Path(sys.argv[1]).resolve() + checked = 0 + with tempfile.TemporaryDirectory(prefix="anyps5-output-collisions-") as directory: + work = Path(directory) + for windows in (False, True): + command, paths, inputs = prepare(work / f"{windows}-control", windows) + links = paths["executable"].parent / "separate-links" + links.mkdir() + for name, path in paths.items(): + path.write_bytes(b"previous output") + os.link(path, links / name) + result = subprocess.run([str(relinker), *command], cwd=paths["executable"].parent, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + for name in ("executable", "module"): + assert paths[name].read_bytes().startswith(b"MZ" if windows else b"\x7fELF") + for name in ("registry", "module-registry"): + json.loads(paths[name].read_text()) + for path, original in inputs.items(): + assert path.read_bytes() == original + assert not list(paths["executable"].parent.rglob(".anyps5-output-identity-*")) + for first, second in itertools.combinations(paths, 2): + case = work / f"{windows}-{first}-{second}" + command, outputs, inputs = prepare(case, windows) + previous = b"previous output" + outputs[first].write_bytes(previous) + os.link(outputs[first], outputs[second]) + for name, path in outputs.items(): + if name not in (first, second): + path.write_bytes(previous) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, ( + windows, first, second, result.returncode, result.stdout, result.stderr) + for path in outputs.values(): + assert path.read_bytes() == previous, path + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert not list(case.rglob(".anyps5-output-identity-*")) + checked += 1 + names = ["windows-diagnostics-imports.txt"] + if os.name == "nt" and not case_sensitive(work): + names.append("WINDOWS-DIAGNOSTICS-IMPORTS.TXT") + for index, name in enumerate(names): + for existing in (False, True): + case = work / f"diagnostics-executable-{index}-{existing}" + case.mkdir() + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / name + if existing: + output.write_bytes(b"previous output") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", "--skip-sce-module", + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, (result.stdout, result.stderr) + assert output.read_bytes() == b"previous output" if existing else not output.exists() + assert source.read_bytes() == main_fixture() + assert not list(case.rglob(".anyps5-output-identity-*")) + for existing in (False, True): + case = work / f"case-sensitive-{existing}" + case.mkdir() + if not case_sensitive(case): + print("Distinct case-variant outputs skipped: case-sensitive directory unavailable") + break + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / "WINDOWS-DIAGNOSTICS-IMPORTS.TXT" + diagnostics = case / "windows-diagnostics-imports.txt" + if existing: + output.write_bytes(b"previous executable") + diagnostics.write_bytes(b"previous diagnostics") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", "--skip-sce-module", + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + assert output.read_bytes().startswith(b"MZ") and diagnostics.exists() + assert not os.path.samefile(output, diagnostics) + assert not list(case.rglob(".anyps5-output-identity-*")) + if os.name == "nt": + case = work / "readonly-identity" + command, paths, inputs = prepare(case, True) + output = paths["executable"] + output.write_bytes(b"previous output") + os.link(output, paths["module"]) + output.chmod(stat.S_IREAD) + try: + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Cannot compare read-only output identities" in result.stderr, ( + result.stdout, result.stderr) + assert output.read_bytes() == b"previous output" + assert paths["module"].read_bytes() == b"previous output" + assert not output.stat().st_mode & stat.S_IWRITE + assert not list(case.rglob(".anyps5-output-identity-*")) + finally: + output.chmod(stat.S_IWRITE) + print(f"Output collision tests passed: {checked} hard-link pairs, direct paths, and identity controls") + + +if __name__ == "__main__": + main() From 6e4ea3e1ac54b1d229122d899f5d4a67695f6cdd Mon Sep 17 00:00:00 2001 From: RINNECODER <93304526+RINNECODER@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:05:50 -0500 Subject: [PATCH 3/4] Port upstream #4501 (SELF skip warning) by @devavijitsingh Ported from boykopovar/AnyPS5 PR #4501 (author @devavijitsingh), applied as a patch. Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/guest/GuestModuleBuilder.cpp | 20 ++++++++++++++----- .../tests/test_guest_module_directories.py | 3 +++ docs/user/USAGE.md | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index f6c8f8a4958..908966f239b 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -50,6 +50,8 @@ std::string ModuleStem(std::string name, const bool windows) { return {}; } +enum class GuestModuleFormat { Elf, Self, Other }; + } std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool macos, const bool toIntel, const bool toRosetta, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& sceModulePath, const std::vector& moduleDirectories) const { @@ -82,16 +84,20 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } std::vector paths; std::set unmatchedExclusions = excludedModules; - const auto isElf = [](const std::filesystem::path& path) { + const auto classifyGuestModule = [](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 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; + if (stream.gcount() != 4) return GuestModuleFormat::Other; const auto byte = [&](const std::size_t index) { return static_cast(magic[index]); }; - const bool 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'; + if ((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 GuestModuleFormat::Self; + if (byte(0) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') + return GuestModuleFormat::Elf; + return GuestModuleFormat::Other; }; for (const auto& directory : directories) { if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); @@ -103,7 +109,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } if (!entry.is_regular_file()) continue; if (std::filesystem::equivalent(entry.path(), inputPath)) continue; - if (isElf(entry.path())) paths.push_back(entry.path()); + const auto format = classifyGuestModule(entry.path()); + if (format == GuestModuleFormat::Elf) paths.push_back(entry.path()); + else if (format == GuestModuleFormat::Self) + std::cout << "Skipping SELF container: " << entry.path().lexically_relative(root).generic_string() + << " (unwrap it before relinking)\n"; } } const auto neededNames = ReadNeededNames(dynamic); diff --git a/core/relinker/relinker/tests/test_guest_module_directories.py b/core/relinker/relinker/tests/test_guest_module_directories.py index 278a968f0f8..31f7ebaaac7 100644 --- a/core/relinker/relinker/tests/test_guest_module_directories.py +++ b/core/relinker/relinker/tests/test_guest_module_directories.py @@ -118,9 +118,12 @@ def convert(case, windows, options=()): (case / "prx" / "provider.prx").write_bytes(module_with_symbol(True)) result, output = convert(case, windows) assert result.returncode == 0 and output.exists(), result.stderr + warning = "Skipping SELF container: sce_module/libc.prx (unwrap it before relinking)" + assert result.stdout.count(warning) == 1, result.stdout assert {path.name for path in (case / "app0").rglob("*.guest.prx")} == {"provider.prx.guest.prx"} result, output = convert(case, windows, ["--exclude-sce-module", "libc.prx"]) assert result.returncode == 0 and output.exists(), result.stderr + assert warning not in result.stdout, result.stdout assert {path.name for path in (case / "app0").rglob("*.guest.prx")} == {"provider.prx.guest.prx"} case = work / f"{windows}-ambiguous" diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa4daa4a03..fa7892945ce 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -2,7 +2,7 @@ ## Input and conversion -Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. A bundled module in a SELF container is an error, as the executable is. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. +Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. ```text source/ From 4f846654c10c62c322282ec1cb4373c2f1d76c8c Mon Sep 17 00:00:00 2001 From: RINNECODER <93304526+RINNECODER@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:06:16 -0500 Subject: [PATCH 4/4] Adapt #4802 port: hoist registry paths so the collision check runs before registries are written Co-Authored-By: Claude Opus 5.5 --- core/relinker/main.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 38f5e547912..22bc04ec2fa 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -223,18 +223,13 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); } + std::vector registryPaths; if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); - std::vector registryPaths{ - outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json") - }; + registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); - const auto registryWriter = std::make_shared(); - fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); - for (std::size_t index = 0; index < guestArtifacts.size(); ++index) - fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); } std::vector outputPaths{Io::NativePath(absPath)}; @@ -254,6 +249,13 @@ int Run(const int argc, char* argv[]) { } } + if (args.writeRegistry) { + const auto registryWriter = std::make_shared(); + fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); + for (std::size_t index = 0; index < guestArtifacts.size(); ++index) + fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); + } + auto byteWriter = std::make_shared(); std::shared_ptr patcher;