From 0cfdc79e59957442be8852807354841daf8a3352 Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:38:27 +0200 Subject: [PATCH] fix(relinker): preserve input binaries when writing registries --- core/relinker/main.cpp | 22 ++++++--- .../include/relinker/guest/GuestImage.hpp | 1 + .../relinker/src/guest/GuestModuleBuilder.cpp | 2 +- .../relinker/tests/test_input_preservation.py | 46 +++++++++++++++++++ 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 4d13db9f9..f1b4a5b64 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -117,13 +117,23 @@ int Run(const int argc, char* argv[]) { if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); - const std::string registryPath = Io::Utf8Path(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); - const auto registryWriter = std::make_shared(); - fileWriter.Write(registryPath, registryWriter->WriteCallRegistry(result.RegistryEntries)); - for (const auto& artifact : guestArtifacts) { - const auto modulePath = outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json"); - fileWriter.Write(Io::Utf8Path(modulePath), registryWriter->WriteModuleImports(artifact.Imports)); + std::vector registryPaths{ + 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) { + if (!std::filesystem::exists(path)) continue; + if (std::filesystem::equivalent(Io::NativePath(args.inputPath), path)) + throw Domain::RelinkerException("Registry output would overwrite the input executable: " + Io::Utf8Path(path)); + for (const auto& artifact : guestArtifacts) + if (std::filesystem::equivalent(artifact.SourcePath, path)) + throw Domain::RelinkerException("Registry output would overwrite an input module: " + Io::Utf8Path(path)); } + 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(); diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index c3d6692fa..fcc35ac65 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -56,6 +56,7 @@ struct GuestArtifact { std::filesystem::path Path; std::vector Bytes; std::vector Imports; + std::filesystem::path SourcePath; }; class GuestModuleBuilder { diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 1479710e1..689f4dafd 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -386,7 +386,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } } dynamic.GuestModules.push_back(std::move(runtime)); - artifacts.push_back({target, std::move(output), std::move(imports)}); + artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath}); std::cout << " [DONE]" << std::endl; } return artifacts; diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 2bb6b49a5..851e35fcf 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -7,12 +7,58 @@ import tempfile from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture +def check_registry_inputs(relinker, work, windows): + options = ["--windows"] if windows else [] + for module_registry in (False, True): + for bundled in (False, True): + for alias in ("direct", "hardlink", "symlink"): + if bundled and alias == "direct": + continue + case = work / f"registry-{windows}-{module_registry}-{bundled}-{alias}" + modules = case / "sce_module" + modules.mkdir(parents=True) + module = modules / "sample.prx" + module_original = bytes(guest_fixture(PLAIN_SITE)) + module.write_bytes(module_original) + output = case / ("output.exe" if windows else "output.elf") + registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json"] + registry = registries[int(module_registry)] + source = registry if alias == "direct" else case / "input.elf" + original = bytes(main_fixture()) + source.write_bytes(original) + protected = module if bundled else source + if alias == "hardlink": + os.link(protected, registry) + elif alias == "symlink": + try: + registry.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Registry symbolic-link case skipped: Windows symlink privilege unavailable") + continue + previous_registry = b"previous registry" + untouched = registries[int(not module_registry)] + untouched.write_bytes(previous_registry) + previous_output = b"previous executable" + output.write_bytes(previous_output) + result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)], + capture_output=True, text=True, timeout=20) + expected = "Registry output would overwrite an input module" if bundled else "Registry output would overwrite the input executable" + assert result.returncode == 2 and expected in result.stderr, (result.returncode, result.stdout, result.stderr) + assert source.read_bytes() == original and module.read_bytes() == module_original + assert output.read_bytes() == previous_output and untouched.read_bytes() == previous_registry + assert registry.read_bytes() == (module_original if bundled else original) + assert not (case / "app0").exists() + + def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-input-preservation-") as directory: work = Path(directory) for windows in (False, True): options = ["--windows"] if windows else [] + check_registry_inputs(relinker, work, windows) for bundled in (False, True): for alias in ("direct", "normalized", "hardlink", "symlink"): case = work / f"{windows}-{bundled}-{alias}"