fix(relinker): reject executable DT_NEEDED names the guest reader rejects

The pipeline read an executable's DT_NEEDED names with readCStr and
checked only bounds and the NUL terminator, so without bundled modules
an empty name, $LIB.prx, a:b.prx or a name with CR or LF relinked with
exit 0 and reached the Linux DT_NEEDED unchanged, while the Windows and
macOS outputs each rejected a different subset. The DT_NEEDED loop now
applies the rule GuestImageReader applies to a module's DT_NEEDED: a
name is not empty and contains no backslash, colon, dollar, CR or LF; a
slash stays allowed. Such an executable now fails on every target with
"Invalid dependency: <name>" at the name's file offset, and outputs for
valid names do not change.
This commit is contained in:
roboterspinne committed 2026-10-24 20:20:26 +02:00
1 parent befe264ffb
commit 0ba2ca7982
2 files changed
+51 -3

No files matched your search

@@ -277,6 +277,8 @@ RelinkResult RelinkerPipeline::Relink() {
for (auto& [fst, snd] : neededLibraryNamesByStrOffset) {
snd = readCStr(fst);
if (snd.empty() || snd.find_first_of("\\:$\r\n") != std::string::npos)
throw RelinkerException("Invalid dependency: " + snd, dynStrTabOffset + fst);
neededLibraries.push_back(snd);
}
@@ -6,15 +6,15 @@ import sys
import tempfile
from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture
from test_guest_symbol_names import importing_executable
from test_guest_symbol_names import DYNAMIC, RELOCATIONS, STRINGS, SYMBOLS, importing_executable
sys.path.insert(0, str(Path(__file__).resolve().parents[4] / "tools"))
import import_audit
def named_module(tag):
def named_module(tag, name="Party.prx"):
image = guest_fixture(PLAIN_SITE)
strings = b"\0Party.prx\0"
strings = b"\0" + name.encode() + b"\0"
image[0x800:0x800 + len(strings)] = strings
struct.pack_into("<Q", image, 0x618, len(strings))
struct.pack_into("<qQqQ", image, 0x680, tag, 1, 0, 0)
@@ -30,10 +30,56 @@ def executable_with_dependencies(libraries):
return image
def executable_needing(name):
image = importing_executable([])
strings = b"\0" + name.encode() + b"\0"
image[STRINGS:STRINGS + len(strings)] = strings
tags = [(1, 1), (5, STRINGS), (10, len(strings)), (6, SYMBOLS), (11, 24),
(7, RELOCATIONS), (8, 0), (9, 24), (0, 0)]
for index, tag in enumerate(tags):
struct.pack_into("<qQ", image, DYNAMIC + index * 16, *tag)
struct.pack_into("<QQ", image, 152, len(tags) * 16, len(tags) * 16)
return image
def dependency_names(relinker, work):
names = ("Party.prx", "", "$LIB.prx", "$ORIGIN/x.prx", "a:b.prx", "a\\b.prx", "a\nb.prx", "a\rb.prx")
for index, name in enumerate(names):
source = work / f"dependency-{index}.elf"
source.write_bytes(executable_needing(name))
for target, flags in enumerate(([], ["--lazy-binding"], ["--windows"], ["--macos"])):
output = work / f"dependency-{index}-{target}.out"
result = subprocess.run([str(relinker), "--skip-sce-module", *flags, str(source), str(output)],
capture_output=True, timeout=20)
stderr = result.stderr.replace(b"\r\n", b"\n")
if index == 0:
assert result.returncode == 0 and output.is_file(), (flags, result.stdout, stderr)
continue
expected = f"FAIL: Invalid dependency: {name} (offset {STRINGS + 1:#x})\n".encode()
assert result.returncode == 2 and stderr.startswith(expected) and not output.exists(), (
name, flags, result.returncode, stderr)
def slash_dependency(relinker, work):
for target, flags in enumerate(([], ["--windows"], ["--macos"])):
case = work / f"slash-{target}"
modules = case / "sce_module"
modules.mkdir(parents=True)
(modules / "engine.prx").write_bytes(named_module(14, "dir/Party.prx"))
source = case / "input.elf"
source.write_bytes(executable_needing("dir/Party.prx"))
output = case / "output.out"
result = subprocess.run([str(relinker), *flags, str(source), str(output)],
capture_output=True, timeout=20)
assert result.returncode == 0 and output.is_file(), (flags, result.stdout, result.stderr)
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-needed-libraries-") as directory:
work = Path(directory)
dependency_names(relinker, work)
slash_dependency(relinker, work)
for windows in (False, True):
for bundled in (False, True):
case = work / f"{windows}-{bundled}"