diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index b2f84ae52d..fbcb56c44e 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -347,6 +347,8 @@ std::vector LinuxElfPatcher::Patch( const std::vector& trampolines) { const auto dynSection = ImportsByNid(sourceDynSection); + if (runPath.empty() || runPath.front() == ':' || runPath.back() == ':' || runPath.find("::") != std::string::npos) + throw Domain::RelinkerException("Empty Linux run path component is not supported"); if (dependencyDiagnostics) throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); if (_fixedBase && lazyBinding) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 31dc35aa0d..b5ab411899 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -378,7 +378,12 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); - if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !runPath.starts_with('/')) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + if (!windows) for (std::size_t begin = 0; begin <= runPath.size();) { + const auto end = std::min(runPath.find(':', begin), runPath.size()); + const auto component = std::string_view(runPath).substr(begin, end - begin); + if (component != "$ORIGIN" && !component.starts_with("$ORIGIN/") && !component.starts_with('/')) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + begin = end + 1; + } const auto outputDirectory = outputPath.parent_path().lexically_normal(); std::vector artifacts; for (const auto index : order) { diff --git a/core/relinker/relinker/tests/test_guest_runpath.py b/core/relinker/relinker/tests/test_guest_runpath.py index e5238ab66f..b5ad7d3701 100644 --- a/core/relinker/relinker/tests/test_guest_runpath.py +++ b/core/relinker/relinker/tests/test_guest_runpath.py @@ -52,7 +52,8 @@ def main(): if resolved != expected or (label.startswith("absolute") and actual != run_path): failures.append((str(relative), label, actual, str(resolved), str(expected))) checked += 1 - for index, run_path in enumerate(("relative/libs", "C:/host/libs", "C:\\host\\libs", "\\\\server\\share")): + for index, run_path in enumerate(("relative/libs", "C:/host/libs", "C:\\host\\libs", "\\\\server\\share", + "$ORIGIN/libs:", "$ORIGIN::/opt/libs", "$ORIGIN:relative/libs", "/opt/libs:")): output = work / f"invalid-{index}.elf" result = subprocess.run([str(relinker), "--rpath", run_path, str(source), str(output)], capture_output=True, text=True, timeout=20) diff --git a/core/relinker/relinker/tests/test_linux_dynamic_strings.py b/core/relinker/relinker/tests/test_linux_dynamic_strings.py index 82c72cbbef..4dd3e1a453 100644 --- a/core/relinker/relinker/tests/test_linux_dynamic_strings.py +++ b/core/relinker/relinker/tests/test_linux_dynamic_strings.py @@ -56,7 +56,8 @@ def main(): source = Path(directory) / "input.elf" source.write_bytes(fixture()) for name, options, expected in (("default", [], b"$ORIGIN/libs"), - ("custom", ["--rpath", "/opt/anyps5/lib"], b"/opt/anyps5/lib")): + ("custom", ["--rpath", "/opt/anyps5/lib"], b"/opt/anyps5/lib"), + ("list", ["--rpath", "/opt/anyps5/lib:$ORIGIN/libs"], b"/opt/anyps5/lib:$ORIGIN/libs")): output = Path(directory) / (name + ".elf") result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], capture_output=True, text=True, timeout=20) @@ -70,6 +71,15 @@ def main(): raise AssertionError((name, "DT_RUNPATH is outside DT_STRSZ", run_path, tags[DT_STRSZ])) if dynstr_section_size(elf) != tags[DT_STRSZ]: raise AssertionError((name, ".dynstr size differs from DT_STRSZ", dynstr_section_size(elf), tags[DT_STRSZ])) + for index, run_path in enumerate(("", ":$ORIGIN/libs", "$ORIGIN/libs:", "/opt/a::/opt/b")): + for target in ([], ["--lazy-binding"]): + output = Path(directory) / f"empty-{index}-{len(target)}.elf" + result = subprocess.run([str(relinker), "--skip-sce-module", *target, "--rpath", run_path, str(source), str(output)], + capture_output=True, text=True, timeout=20) + if result.returncode != 2 or "Empty Linux run path component" not in result.stderr: + raise AssertionError((run_path, target, result.returncode, result.stdout, result.stderr)) + if output.exists(): + raise AssertionError((run_path, target, "an empty run path component created an output")) print("Linux dynamic string tests passed") diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 83eb743254..10f79179fe 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -74,7 +74,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `unused-filter=1` | Filter unused non-PLT imports using control-flow and GOT access analysis, treating address-taken code as reachable; preserve PLT imports. | | `unused-filter=2` | Apply strict unused-import analysis and compact the PLT. Unsupported analysis cases cause an error. | | `--registry` | Write `.registry.json` beside the output executable, and `..registry.json` for each converted bundled module. | -| `--rpath ` | Set the system library search path. Quote `$ORIGIN` to prevent shell expansion, for example `--rpath '$ORIGIN/libs'` in Bash or PowerShell. Linux guest modules require an absolute path or a path beginning with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. | +| `--rpath ` | Set the system library search path. Quote `$ORIGIN` to prevent shell expansion, for example `--rpath '$ORIGIN/libs'` in Bash or PowerShell. On Linux every colon-separated entry must be nonempty, and with guest modules each must be absolute or begin with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. | | `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Adds executable permissions for Linux output. Requires the target OS and prepared runtime layout. | | `--sce-module-path ` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. | | `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. Needed modules are mapped at startup. On Linux, modules in these extra directories initialize only when explicitly loaded, with the arguments from `sceKernelLoadStartModule` (for example Unity plugins in `Media/Plugins`). A module another module needs initializes at startup on Windows and macOS; one only the executable needs initializes when explicitly loaded on Windows and at startup on macOS. Other modules initialize when loaded. Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. |