fix(relinker): keep host prx references out of guest modules on Linux

On Linux every module resolves its references through one global scope,
and the guest modules are listed ahead of the host prx. When a title
bundles its own libc.prx, the host libc and the other host prx bound
their references to their own NIDs to the guest libc, so the host libc
built its standard streams on top of the guest _Stdout.

The exports of guest modules, and the guest and executable imports that
a guest module satisfies, now carry a #guest suffix. The title's imports
still reach the guest modules first, and a host prx only sees host
definitions, as on Windows. sceKernelDlsym also looks for the suffixed
name.
This commit is contained in:
Nahuel Genchi 2026-10-05 10:58:01 -03:00
parent bc24783e5b
commit 5fab03e4e5
7 changed files with 176 additions and 0 deletions

View File

@ -5,6 +5,7 @@
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#ifdef _WIN32
#define NOMINMAX
@ -47,7 +48,14 @@ void* Symbol(Module& module, const char* name) {
void* FindSymbol(Module& module, const char* name) {
if (auto* symbol = Symbol(module, name)) return symbol;
const auto nid = Nid::ComputeNid(name, "");
#ifdef _WIN32
return Symbol(module, nid.c_str());
#else
if (auto* symbol = Symbol(module, nid.c_str())) return symbol;
constexpr char guestSuffix[] = "#guest";
if (auto* symbol = Symbol(module, (std::string(name) + guestSuffix).c_str())) return symbol;
return Symbol(module, (nid + guestSuffix).c_str());
#endif
}
}

View File

@ -24,6 +24,12 @@ int main(int argc, char** argv) {
auto add = reinterpret_cast<Add>(dlsym_nid_postfix(module, "GuestModuleAdd"));
Require(add && add(17, 25) == 42);
Require(dlsym_nid_postfix(reinterpret_cast<void*>(-2), "GuestModuleAdd") == reinterpret_cast<void*>(add));
#ifndef _WIN32
auto mul = reinterpret_cast<Add>(dlsym_nid_postfix(module, "GuestModuleMul"));
Require(mul && mul(6, 7) == 42);
auto sub = reinterpret_cast<Add>(dlsym_nid_postfix(module, "GuestModuleSub"));
Require(sub && sub(50, 8) == 42);
#endif
Require(dlsym_nid_postfix(module, "missing_symbol") == nullptr);
std::thread other([] { Require(dlerror_nid_postfix() == nullptr); });
other.join();

View File

@ -1,4 +1,5 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/general/ExportMacros.hpp"
#ifdef _WIN32
#define MODULE_EXPORT __declspec(dllexport)
#else
@ -7,3 +8,13 @@
extern "C" MODULE_EXPORT int APS5_VABI GuestModuleAdd_nid_postfix(int a, int b) {
return a + b;
}
#ifndef _WIN32
extern "C" MODULE_EXPORT int APS5_VABI GuestModuleMul_nid_no_patch(int a, int b) {
return a * b;
}
APS5_EXPORT("GuestModuleMul#guest", GuestModuleMul_nid_no_patch);
extern "C" MODULE_EXPORT int APS5_VABI GuestModuleSub_nid_no_patch(int a, int b) {
return a - b;
}
APS5_EXPORT("BOyBJaKwOa8#guest", GuestModuleSub_nid_no_patch);
#endif

View File

@ -218,6 +218,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $<TARGET_FILE:relinker>)
add_test(NAME guest_module_directories COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $<TARGET_FILE:relinker>)
add_test(NAME guest_symbol_names COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_symbol_names.py $<TARGET_FILE:relinker>)
add_test(NAME guest_host_libc COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_host_libc.py $<TARGET_FILE:relinker>)
add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE}

View File

@ -10,6 +10,7 @@
namespace Relinker {
inline constexpr char GuestSymbolSuffix[] = "#guest";
inline constexpr char GuestModuleSuffix[] = ".guest.prx";
struct GuestSymbol {

View File

@ -7,6 +7,7 @@
#include <fstream>
#include <functional>
#include <iostream>
#include <limits>
#include <set>
namespace Relinker {
@ -100,6 +101,11 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
for (const auto provider : shared->second) providers += " " + images[provider].SourcePath.string();
throw Domain::RelinkerException("Ambiguous guest import " + name + " in " + importer + ": exported by" + providers);
};
const auto rename = [](std::vector<std::uint8_t>& symbols, std::vector<std::uint8_t>& strings, std::size_t index, const std::string& name) {
if (strings.size() > std::numeric_limits<std::uint32_t>::max()) throw Domain::RelinkerException("Guest string table too large");
Io::WriteU32(symbols, index * 24, static_cast<std::uint32_t>(strings.size()));
Io::AppendString(strings, name + GuestSymbolSuffix);
};
if (dynamic.DynSymData.size() % 24 != 0) throw Domain::RelinkerException("Invalid executable symbol table");
for (std::size_t offset = 0; offset < dynamic.DynSymData.size(); offset += 24) {
if (Io::ReadU16(dynamic.DynSymData, offset + 6) != 0) continue;
@ -110,6 +116,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated executable symbol name");
const std::string name(start, end);
rejectSharedImport(name.substr(0, name.find('#')), inputPath.string());
if (!windows && exports.contains(name)) rename(dynamic.DynSymData, dynamic.DynStrData, offset / 24, name);
}
std::vector<std::set<std::size_t>> dependencies(images.size());
for (auto& image : images) image.UsePlatformTlsResolver = !exports.contains("vNe1w4diLCs");
@ -130,6 +137,15 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
} else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name);
}
}
if (!windows) {
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 || exported) && exports.contains(symbol.Name)) rename(image.Dynamic.DynSymData, image.Dynamic.DynStrData, index, symbol.Name);
}
}
}
std::vector<std::size_t> order;
std::vector<unsigned char> states(images.size());
const std::function<void(std::size_t)> visit = [&](std::size_t index) {

View File

@ -0,0 +1,132 @@
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from test_guest_intel_trampolines import PLAIN_SITE, elf_loads, guest_fixture, main_fixture
DYNAMIC = 0x4800
STRINGS = 0x4A00
SYMBOLS = 0x4B00
RELOCATIONS = 0x4C00
GOT = 0x4E00
def string_table(names):
strings = b"\0"
offsets = []
for name in names:
offsets.append(len(strings))
strings += name.encode() + b"\0"
return strings, offsets
def guest_module(symbols):
image = guest_fixture(PLAIN_SITE)
strings, offsets = string_table(name for name, _ in symbols)
assert len(strings) <= 0x40
image[0x800:0x800 + len(strings)] = strings
struct.pack_into("<Q", image, 0x618, len(strings))
struct.pack_into("<Q", image, 0x628, 0x2280)
struct.pack_into("<II", image, 0x840, 1, len(symbols) + 1)
for index, (offset, (_, exported)) in enumerate(zip(offsets, symbols), 1):
struct.pack_into("<IBBHQQ", image, 0x880 + index * 24, offset, 0x12, 0,
1 if exported else 0, 0x1000 if exported else 0, 1 if exported else 0)
return image
def importing_executable(names):
image = main_fixture()
strings, offsets = string_table(names)
image[STRINGS:STRINGS + len(strings)] = strings
for index, offset in enumerate(offsets, 1):
struct.pack_into("<IBBHQQ", image, SYMBOLS + index * 24, offset, 0x12, 0, 0, 0, 0)
struct.pack_into("<QQq", image, RELOCATIONS + (index - 1) * 24, GOT + (index - 1) * 8, (index << 32) | 6, 0)
tags = [(5, STRINGS), (10, len(strings)), (6, SYMBOLS), (11, 24),
(7, RELOCATIONS), (8, len(names) * 24), (9, 24), (0, 0)]
for index, tag in enumerate(tags):
struct.pack_into("<qQ", image, DYNAMIC + index * 16, *tag)
struct.pack_into("<QQQQQ", image, 120 + 8, DYNAMIC, DYNAMIC, DYNAMIC, len(tags) * 16, len(tags) * 16)
return image
def dynamic_tags(image):
phoff, = struct.unpack_from("<Q", image, 32)
phsize, phcount = struct.unpack_from("<HH", image, 54)
dynamic = next(struct.unpack_from("<IIQQQQQQ", image, phoff + index * phsize)
for index in range(phcount)
if struct.unpack_from("<I", image, phoff + index * phsize)[0] == 2)
tags = {}
for position in range(dynamic[2], dynamic[2] + dynamic[5], 16):
tag, value = struct.unpack_from("<qQ", image, position)
if tag == 0:
break
tags.setdefault(tag, value)
return tags
def file_offset(image, address):
for header in elf_loads(image):
if header[3] <= address < header[3] + header[5]:
return header[2] + address - header[3]
raise AssertionError(f"Unmapped address: {address:#x}")
def symbol_name(image, tags, index):
strings = file_offset(image, tags[5])
name, = struct.unpack_from("<I", image, file_offset(image, tags[6]) + index * 24)
return image[strings + name:image.index(0, strings + name)].decode()
def module_symbols(image):
tags = dynamic_tags(image)
count, = struct.unpack_from("<I", image, file_offset(image, tags[4]) + 4)
return [symbol_name(image, tags, index) for index in range(1, count)]
def imported_symbols(image):
tags = dynamic_tags(image)
names = set()
for address, size in ((7, 8), (23, 2)):
if address not in tags or tags.get(size, 0) == 0:
continue
table = file_offset(image, tags[address])
for position in range(table, table + tags[size], 24):
info, = struct.unpack_from("<Q", image, position + 8)
if info >> 32:
names.add(symbol_name(image, tags, info >> 32))
return names
def convert(relinker, case):
(case / "sce_module").mkdir(parents=True)
source = case / "input.elf"
source.write_bytes(importing_executable(["AAAAAAAAAAA#A#A", "DDDDDDDDDDD#D#D"]))
(case / "sce_module" / "libc.prx").write_bytes(
guest_module([("AAAAAAAAAAA#A#A", True), ("BBBBBBBBBBB#B#B", False), ("CCCCCCCCCCC#C#C", False), ("EEEEEEEEEEE", True)]))
(case / "sce_module" / "other.prx").write_bytes(guest_module([("BBBBBBBBBBB#B#B", True), ("EEEEEEEEEEE", True)]))
output = case / "output.elf"
result = subprocess.run([str(relinker), str(source), str(output)], capture_output=True, text=True, timeout=30)
assert result.returncode == 0, (result.stdout, result.stderr)
return output
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-guest-symbol-names-") as directory:
work = Path(directory)
output = convert(relinker, work)
modules = output.parent / "app0" / "sce_module"
libc = module_symbols((modules / "libc.prx.guest.prx").read_bytes())
assert libc == ["AAAAAAAAAAA#guest", "BBBBBBBBBBB#guest", "CCCCCCCCCCC", "EEEEEEEEEEE#guest"], libc
other = module_symbols((modules / "other.prx.guest.prx").read_bytes())
assert other == ["BBBBBBBBBBB#guest", "EEEEEEEEEEE#guest"], other
imports = imported_symbols(output.read_bytes())
assert imports == {"AAAAAAAAAAA#guest", "DDDDDDDDDDD"}, imports
print("Guest symbol name tests passed")
if __name__ == "__main__":
main()