fix(relinker): validate every entry of the Linux run path

LinuxElfPatcher wrote --rpath into DT_RUNPATH unchanged, so an empty
entry (an empty value, a leading or trailing colon, or a double colon)
reached the output, where glibc searches the working directory for it,
while the macOS and Windows outputs reject empty run paths. The guest
module rule that each entry is absolute or begins with $ORIGIN only
looked at the start of the whole value, so "$ORIGIN/libs:" or
"$ORIGIN:relative/libs" passed. The Linux patcher now rejects an empty
entry with the message the macOS patcher uses, the guest rule checks
every colon-separated entry, and USAGE.md says so.
This commit is contained in:
roboterspinne committed 2026-10-24 12:16:08 +00:00
1 parent 1b4bd4413c
commit 5362dcaaf0
5 files changed
+22 -4

No files matched your search

@@ -347,6 +347,8 @@ std::vector<std::uint8_t> LinuxElfPatcher::Patch(
const std::vector<Codegen::TrampolineSite>& 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)
@@ -378,7 +378,12 @@ std::vector<GuestArtifact> 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<GuestArtifact> artifacts;
for (const auto index : order) {
@@ -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)
@@ -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")
+1 -1
View File
@@ -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 `<output-stem>.registry.json` beside the output executable, and `<output-stem>.<module>.registry.json` for each converted bundled module. |
| `--rpath <path>` | 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 <path>` | 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 <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 <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`. |