Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions core/relinker/relinker/src/guest/GuestModuleBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,27 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
};
for (std::size_t index = 0; index < images.size(); ++index) visit(index);
std::vector<std::string> hostLibraries;
std::vector<std::vector<std::string>> moduleHostLibraries(images.size());
std::set<std::string> uniqueHosts;
const auto addHost = [&](const std::string& name) {
const auto addHost = [&](const std::string& name, const bool startup) {
if (findGuest(name) != guestNames.end()) return;
if (name.empty() || name.find_first_of("/\\:$") != std::string::npos) throw Domain::RelinkerException("Invalid host dependency: " + name);
if (uniqueHosts.insert(name).second) hostLibraries.push_back(name);
if (startup && uniqueHosts.insert(name).second) hostLibraries.push_back(name);
};
for (const auto& name : neededNames) addHost(name);
for (const auto& image : images) for (const auto& dependency : image.Dependencies) addHost(dependency);
for (const auto& name : neededNames) addHost(name, true);
for (std::size_t index = 0; index < images.size(); ++index) {
auto& hosts = moduleHostLibraries[index];
for (const auto& dependency : images[index].Dependencies) {
addHost(dependency, windows || !deferred(index));
if (findGuest(dependency) == guestNames.end()) hosts.push_back(dependency);
}
if (!windows && !macos && images[index].Init != 0) {
if (std::find(hosts.begin(), hosts.end(), "libkernel.prx") == hosts.end()) hosts.push_back("libkernel.prx");
if (!deferred(index) && uniqueHosts.insert("libkernel.prx").second) hostLibraries.push_back("libkernel.prx");
}
if (std::find(hosts.begin(), hosts.end(), "libSceLibcInternal.prx") != hosts.end() &&
std::find(hosts.begin(), hosts.end(), "libc.prx") == hosts.end()) hosts.push_back("libc.prx");
}
if (uniqueHosts.contains("libSceLibcInternal.prx") && uniqueHosts.insert("libc.prx").second) hostLibraries.push_back("libc.prx");
dynamic.NeededLibraries = hostLibraries;
dynamic.DynamicSegmentData.clear();
Expand All @@ -289,12 +302,6 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
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");
const auto outputDirectory = outputPath.parent_path().lexically_normal();
std::vector<GuestArtifact> artifacts;
std::vector<std::string> moduleHostLibraries;
std::set<std::string> uniqueModuleHosts;
for (const auto& image : images) for (const auto& dependency : image.Dependencies) {
if (findGuest(dependency) == guestNames.end() && uniqueModuleHosts.insert(dependency).second) moduleHostLibraries.push_back(dependency);
}
if (uniqueModuleHosts.contains("libSceLibcInternal.prx") && uniqueModuleHosts.insert("libc.prx").second) moduleHostLibraries.push_back("libc.prx");
for (const auto index : order) {
auto& image = images[index];
progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()});
Expand Down Expand Up @@ -339,7 +346,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
const auto dependencyPath = images[dependency].SourcePath.parent_path() / images[dependency].OutputName;
needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string());
}
needed.insert(needed.end(), moduleHostLibraries.begin(), moduleHostLibraries.end());
needed.insert(needed.end(), moduleHostLibraries[index].begin(), moduleHostLibraries[index].end());
output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath);
}
if (windows) {
Expand Down
55 changes: 55 additions & 0 deletions core/relinker/relinker/tests/test_guest_module_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from test_guest_symbol_names import dynamic_tags, imported_symbols, module_symbols
from test_macos_output import MachO
from test_windows_import_modules import executable
from test_guest_intel_trampolines import main_fixture
from test_guest_module_directories import needed_libraries
from test_guest_needed_modules import module_with_needed

ARGS = 0x1122334455667788
ARGP = 0x99AABBCCDDEEFF00
Expand Down Expand Up @@ -56,6 +59,55 @@ def no_relocation_module():
return image


def check_lifecycle_host(relinker, host, work, linux, deferred):
case = work / f'lifecycle-host-{deferred}'
modules, plugins = case / 'sce_module', case / 'Media/Plugins'
modules.mkdir(parents=True)
plugins.mkdir(parents=True)
image = no_relocation_module()
image[0x1010:0x1013] = b'\x31\xc0\xc3'
tags = []
for position in range(0x2000, 0x2200, 16):
tag, value = struct.unpack_from('<qQ', image, position)
if tag == 0:
break
if tag != 1:
tags.append((tag, value))
tags.append((0, 0))
for index, tag in enumerate(tags):
struct.pack_into('<qQ', image, 0x2000 + index * 16, *tag)
struct.pack_into('<QQ', image, 120 + 32, len(tags) * 16, len(tags) * 16)
(plugins if deferred else modules).joinpath('initializer.prx').write_bytes(image)
(plugins / 'plugin.prx').write_bytes(module_with_needed((b'libOptional.prx',), (0,)))
image = main_fixture()
image[0x4000:0x400c] = bytes.fromhex('b83c000000bf2a0000000f05')
source, output = case / 'input.elf', case / 'output.elf'
source.write_bytes(image)
result = subprocess.run([str(relinker), '--skip-syscall-check', '--module-dir', 'Media/Plugins', str(source), str(output)],
capture_output=True, text=True, timeout=30)
assert result.returncode == 0, (result.stdout, result.stderr)
relative = 'Media/Plugins' if deferred else 'sce_module'
initializer = case / 'app0' / relative / 'initializer.prx.guest.prx'
assert needed_libraries(initializer.read_bytes()) == ['libkernel.prx']
assert needed_libraries(output.read_bytes()) == ([] if deferred else ['$ORIGIN/app0/sce_module/initializer.prx.guest.prx', 'libkernel.prx'])
if linux:
libraries = case / 'libs'
libraries.mkdir()
shutil.copyfile(host, libraries / 'libkernel.prx')
events = case / 'events.txt'
output.chmod(0o755)
run = subprocess.run([str(output)], env={**os.environ, 'ANYPS5_GUEST_MODULE_ARGS': str(events)},
capture_output=True, text=True, timeout=30)
assert run.returncode == 42, (run.returncode, run.stdout, run.stderr)
if deferred:
assert not events.exists(), events.read_text()
run = subprocess.run([sys.executable, '-c', 'import ctypes,sys; ctypes.CDLL(sys.argv[1])', str(initializer)],
env={**os.environ, 'ANYPS5_GUEST_MODULE_ARGS': str(events)},
capture_output=True, text=True, timeout=30)
assert run.returncode == 0, (run.returncode, run.stdout, run.stderr)
assert events.read_text() == 'result=0\n', events.read_text()


def macho_binds(image):
linkedit = MachO(image).segment('__LINKEDIT')
return image[linkedit['fileoff']:linkedit['fileoff'] + linkedit['filesize']]
Expand Down Expand Up @@ -86,6 +138,8 @@ def main():
macos = sys.platform == 'darwin' and platform.machine() == 'x86_64'
with tempfile.TemporaryDirectory(prefix='anyps5-guest-args-') as directory:
work = Path(directory)
for deferred in (False, True):
check_lifecycle_host(relinker, host, work, linux, deferred)
modules = work / 'sce_module'
modules.mkdir()
name = 'args.prx'
Expand Down Expand Up @@ -131,6 +185,7 @@ def main():
if linux:
libraries = work / 'libs'
libraries.mkdir()
shutil.copyfile(host, libraries / 'libkernel.prx')
shutil.copyfile(host, libraries / 'libc.prx')
load_and_record(work, libraries / 'libc.prx', converted)
elif macos:
Expand Down
85 changes: 84 additions & 1 deletion core/relinker/relinker/tests/test_guest_module_dir.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pathlib import Path
import json
import os
import struct
import subprocess
Expand All @@ -7,7 +8,8 @@

from test_guest_intel_trampolines import main_fixture, pe_sections
from test_guest_module_directories import module_with_symbol, needed_libraries
from test_guest_needed_modules import executable_with_needed
from test_guest_needed_modules import executable_with_needed, module_with_needed
from test_macos_output import MachO


def pe_export_names(data):
Expand All @@ -34,10 +36,91 @@ def offset(rva):
return result


def host_module(names=()):
image = module_with_needed(names, tuple(range(len(names))))
aligned = bytearray(0x3000)
aligned[:0x400] = image[:0x400]
for index in range(3):
offset = 64 + index * 56
header = list(struct.unpack_from("<IIQQQQQQ", image, offset))
aligned[header[3]:header[3] + header[5]] = image[header[2]:header[2] + header[5]]
header[2] = header[3]
if header[0] == 1:
header[7] = 0x1000
struct.pack_into("<IIQQQQQQ", aligned, offset, *header)
struct.pack_into("<IIQQQQQQ", aligned, 64 + 3 * 56, 0x6474E551, 6, 0, 0, 0, 0, 0, 16)
struct.pack_into("<H", aligned, 56, 4)
return aligned


def check_plugin_host_dependencies(relinker, work):
for target in ("linux", "macos", "windows"):
for root in ("runtime", "startup", "required"):
case = work / f"hosts-{target}-{root}"
bundled = case / "sce_module"
plugins = case / "Media" / "Plugins"
bundled.mkdir(parents=True)
plugins.mkdir(parents=True)
(plugins / "plugin.prx").write_bytes(host_module((b"libOptional.prx",)))
if root == "startup":
(bundled / "required.prx").write_bytes(host_module((b"libPresent.prx",)))
image = executable_with_needed(b"plugin.prx") if root == "required" else main_fixture()
image[0x4000:0x400c] = bytes.fromhex("b83c000000bf2a0000000f05")
source = case / "input.elf"
source.write_bytes(image)
output = case / "output.elf"
result = subprocess.run([str(relinker), *([] if target == "linux" else [f"--{target}"]),
"--registry", "--skip-syscall-check", "--module-dir", "Media/Plugins",
str(source), str(output)], capture_output=True, text=True, timeout=30)
assert result.returncode == 0, (target, root, result.stdout, result.stderr)
plugin = case / "app0" / "Media" / "Plugins" / "plugin.prx.guest.prx"
required = case / "app0" / "sce_module" / "required.prx.guest.prx"
expected_hosts = ["libOptional.prx"] if target == "windows" or root == "required" else []
if root == "startup":
expected_hosts.append("libPresent.prx")
assert json.loads((case / "output.needed.json").read_text()) == expected_hosts, (target, root)
assert json.loads((case / "output.plugin.prx.guest.prx.needed.json").read_text()) == ["libOptional.prx"]
if target != "windows":
needed = needed_libraries(output.read_bytes()) if target == "linux" else MachO(output.read_bytes()).dylibs
guest = ("$ORIGIN/" if target == "linux" else "@executable_path/") + "app0/Media/Plugins/plugin.prx.guest.prx"
optional = "libOptional.prx" if target == "linux" else "@rpath/libOptional.prx"
present = "libPresent.prx" if target == "linux" else "@rpath/libPresent.prx"
assert (guest in needed) == (root == "required"), (target, root, needed)
assert (optional in needed) == (root == "required"), (target, root, needed)
plugin_needed = needed_libraries(plugin.read_bytes()) if target == "linux" else MachO(plugin.read_bytes()).dylibs
assert optional in plugin_needed and present not in plugin_needed, (target, root, plugin_needed)
if root == "startup":
required_needed = needed_libraries(required.read_bytes()) if target == "linux" else MachO(required.read_bytes()).dylibs
assert present in needed and present in required_needed and optional not in required_needed, (target, needed, required_needed)
if target == "linux" and sys.platform == "linux" and os.uname().machine == "x86_64":
libraries = case / "libs"
libraries.mkdir()
(libraries / "libPresent.prx").write_bytes(host_module())
output.chmod(0o755)
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
if root == "required":
assert run.returncode != 0 and "libOptional.prx" in run.stderr, (root, run.returncode, run.stderr)
else:
assert run.returncode == 42, (root, run.returncode, run.stderr)
command = [sys.executable, "-c", "import ctypes,sys; ctypes.CDLL(sys.argv[1])", str(plugin)]
load = subprocess.run(command, capture_output=True, text=True, timeout=30)
assert load.returncode != 0 and "libOptional.prx" in load.stderr, (root, load.returncode, load.stderr)
(libraries / "libOptional.prx").write_bytes(host_module())
load = subprocess.run(command, capture_output=True, text=True, timeout=30)
assert load.returncode == 0, (root, load.returncode, load.stderr)
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
assert run.returncode == 42, (root, run.returncode, run.stderr)
if root == "startup":
(libraries / "libPresent.prx").unlink()
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
assert run.returncode != 0 and "libPresent.prx" in run.stderr, (run.returncode, run.stderr)


def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-module-dir-") as directory:
work = Path(directory)
check_plugin_host_dependencies(relinker, work)

def convert(case, windows, options, executable=None, cwd=None):
(case / "sce_module").mkdir(parents=True, exist_ok=True)
Expand Down
8 changes: 4 additions & 4 deletions core/relinker/relinker/tests/test_guest_module_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def run(output, expected=22, error=None):
else:
assert needed_libraries(output.read_bytes()) == [
'$ORIGIN/app0/' + directory + '/renamed.sprx.guest.prx',
'$ORIGIN/app0/sce_module/consumer.prx.guest.prx']
'$ORIGIN/app0/sce_module/consumer.prx.guest.prx', 'libkernel.prx']

for directory in ('.', 'Media/Modules'):
for suffix in ('.prx', '.sprx', '.suprx'):
Expand Down Expand Up @@ -157,9 +157,9 @@ def run(output, expected=22, error=None):
else:
needed = needed_libraries(output.read_bytes())
assert needed == ['$ORIGIN/app0/sce_module/renamed.prx.guest.prx',
'$ORIGIN/app0/sce_module/consumer.prx.guest.prx'], needed
'$ORIGIN/app0/sce_module/consumer.prx.guest.prx', 'libkernel.prx'], needed
child = output.parent / 'app0/sce_module/consumer.prx.guest.prx'
assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx']
assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx', 'libkernel.prx']

for suffix in ('.suprx', '.prx', '.sprx', '.debug_prx'):
providers = {'renamed.prx': declared_provider(22, ('libGuest',)),
Expand All @@ -173,7 +173,7 @@ def run(output, expected=22, error=None):
run(output)
else:
child = output.parent / 'app0/sce_module/consumer.prx.guest.prx'
assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx']
assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx', 'libkernel.prx']

providers = {'one.prx': declared_provider(22, ('libGuest',)),
'two.prx': declared_provider(33, ('libGuest',), symbol='other')}
Expand Down