mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
fix(relinker): bind SDK guest module imports on Linux
A bundled module that names a console file dependency (.sprx, .suprx, .debug_prx) imports by symbol name, as homebrew built with the ps5-payload-dev SDK does. Bind those imports to the NID of the name, except names another bundled module or the executable exports, and name the module's host dependencies with the .prx library of the same stem.
This commit is contained in:
1 parent
be51ab7bae
commit
03fae5dfcb
6 files changed
+168
-4
No files matched your search
@@ -637,6 +637,10 @@ if(BUILD_TESTING)
|
||||
$<TARGET_FILE:relinker> $<TARGET_FILE:guest_module_args_host>)
|
||||
set_tests_properties(guest_zero_initializer PROPERTIES TIMEOUT 30)
|
||||
add_asan_test_options(guest_zero_initializer verify_asan_link_order=0)
|
||||
add_test(NAME linux_guest_named_imports COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_guest_named_imports.py
|
||||
$<TARGET_FILE:relinker> $<TARGET_FILE:guest_module_args_host>)
|
||||
add_asan_test_options(linux_guest_named_imports verify_asan_link_order=0)
|
||||
set(windows_stdio_host_arguments)
|
||||
if(WIN32)
|
||||
if(MINGW)
|
||||
|
||||
@@ -57,6 +57,7 @@ struct GuestImage {
|
||||
std::uint64_t Got = 0;
|
||||
bool UsePlatformTlsResolver = true;
|
||||
bool Symbolic = false;
|
||||
std::vector<std::size_t> NamedImports;
|
||||
};
|
||||
|
||||
class GuestImageReader {
|
||||
|
||||
@@ -162,6 +162,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector
|
||||
if (!importModules.emplace(value >> 48, string(value & 0xffffffffu)).second) fail("Duplicate import module ID");
|
||||
std::vector<std::string> neededLibraries;
|
||||
for (const auto offset : needed) neededLibraries.push_back(string(offset));
|
||||
const bool consoleFileNames = std::any_of(neededLibraries.begin(), neededLibraries.end(),
|
||||
[](const std::string& name) { return Domain::NeededLibraryName(name) != name; });
|
||||
std::map<std::string, std::size_t> exports;
|
||||
for (std::uint64_t offset = 0; offset < symSize; offset += 24) {
|
||||
auto name = string(Io::ReadU32(bytes, symOffset + offset));
|
||||
@@ -195,6 +197,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector
|
||||
if (image.Dynamic.DynStrData.size() > std::numeric_limits<std::uint32_t>::max()) fail("String table too large");
|
||||
Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast<std::uint32_t>(image.Dynamic.DynStrData.size()));
|
||||
Io::AppendString(image.Dynamic.DynStrData, name);
|
||||
if (consoleFileNames && section == 0 && !name.empty() && name == qualifiedName) image.NamedImports.push_back(image.Symbols.size());
|
||||
image.Symbols.push_back(symbol);
|
||||
}
|
||||
if (tags.contains(14)) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <domain/ImportModule.hpp>
|
||||
#include <io/NativePath.hpp>
|
||||
#include <io/BufferUtils.hpp>
|
||||
#include <nid/NidCompute.hpp>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
@@ -42,6 +43,33 @@ std::vector<std::string> ReadNeededNames(const Domain::SysVDynamicSection& dynam
|
||||
return names;
|
||||
}
|
||||
|
||||
std::string SymbolName(const Domain::SysVDynamicSection& dynamic, const std::size_t index) {
|
||||
if (dynamic.DynSymData.size() % 24 != 0 || index >= dynamic.DynSymData.size() / 24) throw Domain::RelinkerException("Invalid symbol index");
|
||||
const auto nameOffset = Io::ReadU32(dynamic.DynSymData, index * 24);
|
||||
if (nameOffset >= dynamic.DynStrData.size()) throw Domain::RelinkerException("Invalid symbol name offset");
|
||||
const auto start = dynamic.DynStrData.begin() + nameOffset;
|
||||
const auto end = std::find(start, dynamic.DynStrData.end(), 0);
|
||||
if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated symbol name");
|
||||
return std::string(start, end);
|
||||
}
|
||||
|
||||
std::set<std::string> DefinedNames(const Domain::SysVDynamicSection& dynamic) {
|
||||
std::set<std::string> names;
|
||||
for (std::size_t index = 1; index < dynamic.DynSymData.size() / 24; ++index)
|
||||
if (Io::ReadU16(dynamic.DynSymData, index * 24 + 6) != 0) names.insert(SymbolName(dynamic, index));
|
||||
return names;
|
||||
}
|
||||
|
||||
void BindNamedImportsByNid(GuestImage& image, const std::set<std::string>& kept) {
|
||||
for (const auto index : image.NamedImports) {
|
||||
const auto name = SymbolName(image.Dynamic, index);
|
||||
if (name.ends_with(GuestSymbolSuffix) || kept.contains(name)) continue;
|
||||
if (image.Dynamic.DynStrData.size() > std::numeric_limits<std::uint32_t>::max()) throw Domain::RelinkerException("Guest string table too large");
|
||||
Io::WriteU32(image.Dynamic.DynSymData, index * 24, static_cast<std::uint32_t>(image.Dynamic.DynStrData.size()));
|
||||
Io::AppendString(image.Dynamic.DynStrData, Nid::ComputeNid(name, ""));
|
||||
}
|
||||
}
|
||||
|
||||
std::string FoldFilename(std::string name) {
|
||||
for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast<char>(character + ('a' - 'A'));
|
||||
return name;
|
||||
@@ -292,12 +320,14 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
|
||||
return directory == "." ? std::string("app0") : "app0/" + directory;
|
||||
};
|
||||
if (!windows) {
|
||||
const auto executableExports = macos ? std::set<std::string>{} : DefinedNames(dynamic);
|
||||
for (auto& image : images) {
|
||||
for (std::size_t index = 1; index < image.Symbols.size(); ++index) {
|
||||
const auto& symbol = image.Symbols[index];
|
||||
const bool exported = symbol.Section != 0 && (symbol.Info >> 4) != 0 && symbol.Visibility != 1 && symbol.Visibility != 2;
|
||||
if (((symbol.Section == 0 && !declaresHost(symbol.Library)) || exported) && exports.contains(symbol.Name)) rename(image.Dynamic.DynSymData, image.Dynamic.DynStrData, index, symbol.Name);
|
||||
}
|
||||
if (!macos) BindNamedImportsByNid(image, executableExports);
|
||||
}
|
||||
}
|
||||
std::vector<std::size_t> order;
|
||||
@@ -314,17 +344,18 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
|
||||
std::vector<std::string> hostLibraries;
|
||||
std::vector<std::vector<std::string>> moduleHostLibraries(images.size());
|
||||
std::set<std::string> uniqueHosts;
|
||||
const auto hostLibrary = [&](const std::string& name) { return windows || macos ? name : Domain::NeededLibraryName(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 (startup && uniqueHosts.insert(name).second) hostLibraries.push_back(name);
|
||||
if (const auto host = hostLibrary(name); startup && uniqueHosts.insert(host).second) hostLibraries.push_back(host);
|
||||
};
|
||||
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 (findGuest(dependency) == guestNames.end()) hosts.push_back(hostLibrary(dependency));
|
||||
}
|
||||
if (!windows && !macos && images[index].HasInit) {
|
||||
if (std::find(hosts.begin(), hosts.end(), "libkernel.prx") == hosts.end()) hosts.push_back("libkernel.prx");
|
||||
@@ -425,7 +456,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
|
||||
dynamic.GuestModules.push_back(std::move(runtime));
|
||||
std::vector<std::string> neededLibraries;
|
||||
for (const auto& name : image.Dependencies)
|
||||
if (findGuest(name) == guestNames.end()) neededLibraries.push_back(name);
|
||||
if (findGuest(name) == guestNames.end()) neededLibraries.push_back(hostLibrary(name));
|
||||
artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath, std::move(neededLibraries)});
|
||||
progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent / "macos"))
|
||||
|
||||
from guest_import_fixtures import executable
|
||||
from nid import nid
|
||||
from test_guest_deferred_dependencies import page_aligned
|
||||
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
|
||||
from test_guest_symbol_names import guest_module, imported_symbols, module_symbols, relink
|
||||
from test_linux_load_alignment import fixture
|
||||
from test_windows_import_modules import consumer
|
||||
|
||||
PROVIDER = "int provided(void) __asm__(\"\\\"%s\\\"\");\nint provided(void) { return 22; }\n"
|
||||
|
||||
|
||||
def guest_with_needed(symbols, needed):
|
||||
image = guest_module(symbols)
|
||||
size, = struct.unpack_from("<Q", image, 0x618)
|
||||
name = needed.encode() + b"\0"
|
||||
assert size + len(name) <= 0x40
|
||||
image[0x800 + size:0x800 + size + len(name)] = name
|
||||
struct.pack_into("<Q", image, 0x618, size + len(name))
|
||||
struct.pack_into("<qQqQ", image, 0x680, 1, size, 0, 0)
|
||||
struct.pack_into("<QQ", image, 176 + 32, 10 * 16, 10 * 16)
|
||||
return image
|
||||
|
||||
|
||||
def exporting_executable(name):
|
||||
image = fixture()
|
||||
strings = b"\0" + name.encode() + b"\0"
|
||||
image[0x4600:0x4600 + len(strings)] = strings
|
||||
image[0x4210:0x4216] = bytes.fromhex("b82a000000c3")
|
||||
struct.pack_into("<IBBHQQ", image, 0x4900, 0, 0, 0, 0, 0, 0)
|
||||
struct.pack_into("<IBBHQQ", image, 0x4918, 1, 0x12, 0, 1, 0x210, 6)
|
||||
tags = [(5, 0x600), (10, len(strings)), (6, 0x900), (11, 24), (7, 0x700), (8, 0), (9, 24), (0x6100003f, 48), (0, 0)]
|
||||
struct.pack_into("<IIQQQQQQ", image, 120, 2, 6, 0x4800, 0x800, 0x800, len(tags) * 16, len(tags) * 16, 8)
|
||||
for index, tag in enumerate(tags):
|
||||
struct.pack_into("<qQ", image, 0x4800 + index * 16, *tag)
|
||||
return image
|
||||
|
||||
|
||||
def provider(work, name, symbol):
|
||||
source = work / (name + ".c")
|
||||
source.write_text(PROVIDER % symbol)
|
||||
library = work / (name + ".so")
|
||||
subprocess.run([os.environ.get("CC", "cc"), "-shared", "-fPIC", str(source), "-o", str(library)], check=True)
|
||||
return library
|
||||
|
||||
|
||||
def run_consumer(relinker, host, case, library):
|
||||
(case / "sce_module").mkdir(parents=True)
|
||||
(case / "sce_module" / "consumer.prx").write_bytes(page_aligned(consumer("available.sprx", symbol="shared")))
|
||||
image = main_fixture()
|
||||
code = bytes.fromhex("b83c000000bf2a0000000f05")
|
||||
image[0x4000:0x4000 + len(code)] = code
|
||||
source = case / "input.elf"
|
||||
source.write_bytes(image)
|
||||
output = case / "output.elf"
|
||||
result = subprocess.run([str(relinker), "--skip-syscall-check", str(source), str(output)],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
module = case / "app0" / "sce_module" / "consumer.prx.guest.prx"
|
||||
assert "available.prx" in needed_libraries(module.read_bytes()), needed_libraries(module.read_bytes())
|
||||
assert nid("shared") in module_symbols(module.read_bytes()), module_symbols(module.read_bytes())
|
||||
libraries = case / "libs"
|
||||
libraries.mkdir()
|
||||
for name in ("libc.prx", "libkernel.prx"):
|
||||
shutil.copyfile(host, libraries / name)
|
||||
shutil.copyfile(library, libraries / "available.prx")
|
||||
output.chmod(0o755)
|
||||
return subprocess.run([str(output)], capture_output=True, text=True, timeout=30,
|
||||
env={**os.environ, "ANYPS5_GUEST_MODULE_ARGS": str(case / "events.txt")})
|
||||
|
||||
|
||||
def main():
|
||||
relinker = Path(sys.argv[1]).resolve()
|
||||
host = Path(sys.argv[2]).resolve()
|
||||
runnable = sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64")
|
||||
with tempfile.TemporaryDirectory(prefix="anyps5-linux-guest-named-imports-") as directory:
|
||||
work = Path(directory)
|
||||
|
||||
guest = relink(relinker, work / "guest", executable("libkernel_web.sprx", "shared"), {
|
||||
"user.sprx": guest_with_needed([("shared", True), ("BBBBBBBBBBB", False), ("CCCCCCCCCCC#C#C", False)], "libkernel_web.sprx"),
|
||||
"plain.prx": guest_module([("DDDDDDDDDDD", False)]),
|
||||
})
|
||||
assert imported_symbols(guest.read_bytes()) == {"shared#guest"}, imported_symbols(guest.read_bytes())
|
||||
modules = guest.parent / "app0" / "sce_module"
|
||||
symbols = module_symbols((modules / "user.sprx.guest.prx").read_bytes())
|
||||
assert symbols == ["shared#guest", nid("BBBBBBBBBBB"), "CCCCCCCCCCC"], symbols
|
||||
symbols = module_symbols((modules / "plain.prx.guest.prx").read_bytes())
|
||||
assert symbols == ["DDDDDDDDDDD"], symbols
|
||||
|
||||
dependencies = relink(relinker, work / "dependencies", executable("available.prx"), {
|
||||
"user.prx": module_with_needed((b"libSceLibcInternal.sprx", b"libkernel_web.sprx"), (0, 1)),
|
||||
})
|
||||
needed = needed_libraries(dependencies.read_bytes())
|
||||
assert needed == ["$ORIGIN/app0/sce_module/user.prx.guest.prx", "available.prx", "libSceLibcInternal.prx", "libkernel_web.prx", "libc.prx"], needed
|
||||
needed = needed_libraries((dependencies.parent / "app0" / "sce_module" / "user.prx.guest.prx").read_bytes())
|
||||
assert needed == ["libSceLibcInternal.prx", "libkernel_web.prx", "libc.prx"], needed
|
||||
|
||||
exported = relink(relinker, work / "executable-export", exporting_executable("FixtureExport#A#B"), {
|
||||
"user.prx": guest_with_needed([("FixtureExport", False), ("BBBBBBBBBBB", False)], "libkernel_web.sprx"),
|
||||
})
|
||||
symbols = module_symbols((exported.parent / "app0" / "sce_module" / "user.prx.guest.prx").read_bytes())
|
||||
assert symbols == ["FixtureExport", nid("BBBBBBBBBBB")], symbols
|
||||
|
||||
if runnable:
|
||||
result = run_consumer(relinker, host, work / "by-nid", provider(work, "by-nid", nid("shared")))
|
||||
assert result.returncode == 42, (result.returncode, result.stdout, result.stderr)
|
||||
result = run_consumer(relinker, host, work / "by-name", provider(work, "by-name", "shared"))
|
||||
assert result.returncode != 0 and nid("shared") in result.stderr, (result.returncode, result.stdout, result.stderr)
|
||||
print("Linux guest named import tests passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -13,7 +13,7 @@
|
||||
- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms.
|
||||
- `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified.
|
||||
- [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. macOS resolves these imports through the host loader by name and is unchanged.
|
||||
- [Unscoped imports on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) - in an executable that names a dependency with `.sprx`, `.suprx` or `.debug_prx`, as homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) does, an import without a module that is named by symbol is bound to the NID of that name only. Unlike Windows, the name as written is not tried first, because the host loader also searches the host C library, which exports `malloc` or `pthread_create` under their names. A name that a bundled module exports still binds to that module, and such a dependency loads the `.prx` library of the same stem. Unqualified imports of other executables, and imports of bundled modules, are still bound by name.
|
||||
- [Unscoped imports on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) - in an executable or bundled module that names a dependency with `.sprx`, `.suprx` or `.debug_prx`, as homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) does, an import without a module that is named by symbol is bound to the NID of that name only. Unlike Windows, the name as written is not tried first, because the host loader also searches the host C library, which exports `malloc` or `pthread_create` under their names. A name that a bundled module exports still binds to that module, as does a bundled module's import of a name the executable exports, and such a dependency loads the `.prx` library of the same stem. Unqualified imports of other executables and bundled modules are still bound by name.
|
||||
- The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.
|
||||
- The [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp) takes the branch targets that keep an instruction in place from a linear scan of the code segment, so data in that segment decodes as branches too: OpenSSL's x86-64 assembly keeps strings such as "SHA1 block transform for x86_64, CRYPTOGAMS by <appro@openssl.org>" in its code, and in SuperTuxKart (PPSA69421) and Mortalfin (PPSA99141) the `jae` read from "sf" points into the VEX prefix of a 256-bit store. A direct branch target inside a scanned instruction counts only where every byte before it is a legacy prefix or REX, which a real branch can skip (a `lock` prefix, for example); a target in a VEX prefix, opcode or operand is ignored, as a jump-table entry that starts no instruction already is. A real branch to such a target, overlapping instructions as hand-written code can, is not seen, and the instruction it enters can be moved.
|
||||
- A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked.
|
||||
|
||||
Reference in new issue
Block a user