Merge pull request #1102 from jeffrey12cali/fix/module-args-on-linux

fix(libkernel): pass module args and argp to runtime-loaded modules on Linux
This commit is contained in:
Adrià 2026-10-10 01:27:59 +02:00 committed by GitHub
commit ff8961dfbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 281 additions and 11 deletions

View File

@ -153,14 +153,41 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn
#endif
}
namespace {
struct PendingModuleArgs {
std::size_t args = 0;
const void* argp = nullptr;
};
thread_local PendingModuleArgs pendingModuleArgs;
thread_local int pendingModuleInitResult = 0;
}
extern "C" {
const void* __aps5_get_pending_module_args_nid_no_patch() {
return &pendingModuleArgs;
}
void __aps5_set_module_init_result_nid_no_patch(int result) {
pendingModuleInitResult = result;
}
}
KernelModule APS5_VABI sceKernelLoadStartModule(const char* module_file_name, size_t args, const void* argp, uint32_t flags, const KernelLoadModuleOpt* opt, int* res) {
(void)args;
(void)argp;
(void)flags;
(void)opt;
if (res) *res = 0;
if (!module_file_name) return static_cast<KernelModule>(SCE_KERNEL_ERROR_EFAULT);
pendingModuleArgs = {args, argp};
pendingModuleInitResult = 0;
void* handle = dlopen_nid_postfix(module_file_name, kRtldNow);
const int started = pendingModuleInitResult;
pendingModuleArgs = {};
pendingModuleInitResult = 0;
if (res) *res = started;
if (!handle) return static_cast<KernelModule>(SCE_KERNEL_ERROR_ENOENT);
return static_cast<KernelModule>(reinterpret_cast<intptr_t>(handle));
}

View File

@ -239,6 +239,13 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_registry.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_library(guest_module_args_host SHARED elfpatcher/tests/GuestModuleArgsHost.cpp)
target_include_directories(guest_module_args_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs)
set_target_properties(guest_module_args_host PROPERTIES
PREFIX "" SUFFIX ".prx" WINDOWS_EXPORT_ALL_SYMBOLS ON)
add_test(NAME guest_module_args COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_args.py
$<TARGET_FILE:relinker> $<TARGET_FILE:guest_module_args_host>)
add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $<TARGET_FILE:relinker>)
add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE}

View File

@ -26,8 +26,19 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestIma
const auto extraOffset = bytes.size();
const auto extraAddress = Io::AlignUp64(end, 0x4000);
const auto address = [&] { return extraAddress + bytes.size() - extraOffset; };
const bool passesModuleArgs = image.Init != 0;
std::uint64_t argsSlot = 0;
std::uint64_t resultSlot = 0;
if (passesModuleArgs) {
Io::AlignBuffer(bytes, 8);
argsSlot = address();
bytes.insert(bytes.end(), 8, 0);
resultSlot = address();
bytes.insert(bytes.end(), 8, 0);
}
auto strings = image.Dynamic.DynStrData;
auto symbols = image.Dynamic.DynSymData;
constexpr std::uint64_t kGlobDat = 6;
const auto addString = [&](const std::string& value) {
if (strings.size() > std::numeric_limits<std::uint32_t>::max()) throw Domain::RelinkerException("Guest string table too large");
const auto offset = static_cast<std::uint32_t>(strings.size());
@ -46,6 +57,30 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestIma
std::vector<std::uint64_t> needed;
for (const auto& dependency : dependencies) needed.push_back(addString(dependency));
if (needsTlsResolver) needed.push_back(addString("ld-linux-x86-64.so.2"));
const auto addUndefinedImport = [&](const std::string& name) {
const auto nameOffset = addString(name);
const auto entry = symbols.size();
symbols.resize(entry + 24, 0);
Io::WriteU32(symbols, entry, nameOffset);
symbols[entry + 4] = 0x12;
Io::WriteU16(symbols, entry + 6, 0);
return entry / 24;
};
auto relaData = image.Dynamic.RelaData;
std::uint64_t argsImport = 0;
std::uint64_t resultImport = 0;
const auto addGlobDat = [&](std::uint64_t slot, std::uint64_t symbol) {
const auto entry = relaData.size();
relaData.resize(entry + 24, 0);
Io::WriteU64(relaData, entry, slot);
Io::WriteU64(relaData, entry + 8, (symbol << 32) | kGlobDat);
};
if (passesModuleArgs) {
argsImport = addUndefinedImport("__aps5_get_pending_module_args_nid_no_patch");
resultImport = addUndefinedImport("__aps5_set_module_init_result_nid_no_patch");
addGlobDat(argsSlot, argsImport);
addGlobDat(resultSlot, resultImport);
}
const auto soname = addString(image.OutputName);
const auto search = addString(runPath);
const auto strAddress = address();
@ -54,7 +89,7 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestIma
const auto symAddress = address();
bytes.insert(bytes.end(), symbols.begin(), symbols.end());
const auto hashAddress = address();
const auto count = static_cast<std::uint32_t>(image.Symbols.size());
const auto count = static_cast<std::uint32_t>(symbols.size() / 24);
const auto bucketCount = std::max<std::uint32_t>(count, 1);
std::vector<std::uint32_t> buckets(bucketCount), chains(count);
for (std::uint32_t index = count; index-- > 1;) {
@ -72,20 +107,49 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestIma
for (const auto chain : chains) Io::AppendU32(bytes, chain);
Io::AlignBuffer(bytes, 8);
const auto relaAddress = address();
bytes.insert(bytes.end(), image.Dynamic.RelaData.begin(), image.Dynamic.RelaData.end());
bytes.insert(bytes.end(), relaData.begin(), relaData.end());
const auto pltAddress = address();
bytes.insert(bytes.end(), image.Dynamic.RelaPltData.begin(), image.Dynamic.RelaPltData.end());
constexpr std::size_t kArgsDisplacement = 3, kTargetDisplacement = 0x11, kResultDisplacement = 0x19;
constexpr std::size_t kStubSize = 0x1f;
const auto stubDisplacement = [](std::uint64_t target, std::uint64_t next) {
const auto distance = static_cast<std::int64_t>(target) - static_cast<std::int64_t>(next);
if (distance < std::numeric_limits<std::int32_t>::min() || distance > std::numeric_limits<std::int32_t>::max())
throw Domain::RelinkerException("Guest initializer exceeds relative branch range");
return static_cast<std::uint32_t>(static_cast<std::int32_t>(distance));
};
const auto lifecycle = [&](std::uint64_t target) {
if (target == 0) return std::uint64_t{};
const auto start = address();
bytes.insert(bytes.end(), {0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xe9});
const auto displacement = static_cast<std::int64_t>(target) - static_cast<std::int64_t>(address() + 4);
if (displacement < std::numeric_limits<std::int32_t>::min() || displacement > std::numeric_limits<std::int32_t>::max()) throw Domain::RelinkerException("Guest initializer exceeds relative branch range");
Io::AppendU32(bytes, static_cast<std::uint32_t>(displacement));
std::vector<std::uint8_t> stub(kStubSize, 0xc3);
stub[0x00] = 0x50;
stub[0x01] = 0xff; stub[0x02] = 0x15;
Io::WriteU32(stub, kArgsDisplacement, stubDisplacement(argsSlot, start + 7));
stub[0x07] = 0x48; stub[0x08] = 0x8b; stub[0x09] = 0x38;
stub[0x0a] = 0x48; stub[0x0b] = 0x8b; stub[0x0c] = 0x70; stub[0x0d] = 0x08;
stub[0x0e] = 0x31; stub[0x0f] = 0xd2;
stub[0x10] = 0xe8;
Io::WriteU32(stub, kTargetDisplacement, stubDisplacement(target, start + 0x15));
stub[0x15] = 0x89; stub[0x16] = 0xc7;
stub[0x17] = 0xff; stub[0x18] = 0x15;
Io::WriteU32(stub, kResultDisplacement, stubDisplacement(resultSlot, start + 0x1d));
stub[0x1d] = 0x58;
stub[0x1e] = 0xc3;
bytes.insert(bytes.end(), stub.begin(), stub.end());
return start;
};
const auto callThrough = [&](std::uint64_t target) {
if (target == 0) return std::uint64_t{};
const auto start = address();
const auto distance = static_cast<std::int64_t>(target) - static_cast<std::int64_t>(start + 5);
if (distance < std::numeric_limits<std::int32_t>::min() || distance > std::numeric_limits<std::int32_t>::max())
throw Domain::RelinkerException("Guest finaliser exceeds relative branch range");
bytes.push_back(0xe9);
Io::AppendU32(bytes, static_cast<std::uint32_t>(distance));
return start;
};
const auto init = lifecycle(image.Init);
const auto fini = lifecycle(image.Fini);
const auto fini = callThrough(image.Fini);
Io::AlignBuffer(bytes, 8);
const auto dynamicOffset = bytes.size();
const auto dynamicAddress = address();
@ -98,9 +162,9 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestIma
tag(10, strings.size());
tag(6, symAddress);
tag(11, 24);
if (!image.Dynamic.RelaData.empty()) {
if (!relaData.empty()) {
tag(7, relaAddress);
tag(8, image.Dynamic.RelaData.size());
tag(8, relaData.size());
tag(9, 24);
}
if (!image.Dynamic.RelaPltData.empty()) {

View File

@ -0,0 +1,56 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
namespace {
struct Pending {
std::size_t args = 0;
const void* argp = nullptr;
} pending;
[[noreturn]] void Fail(const char* message) noexcept {
std::fprintf(stderr, "Guest module argument fixture: %s\n", message);
std::fflush(stderr);
std::_Exit(EXIT_FAILURE);
}
FILE* Events() {
const auto* path = std::getenv("ANYPS5_GUEST_MODULE_ARGS");
if (!path) Fail("Missing ANYPS5_GUEST_MODULE_ARGS");
auto* file = std::fopen(path, "ab");
if (!file) Fail("Cannot open argument event file");
return file;
}
void Write(const char* text) {
auto* file = Events();
const auto written = std::fputs(text, file);
const auto closed = std::fclose(file);
if (written == EOF || closed != 0) Fail("Cannot write argument event");
}
}
extern "C" void APS5_VABI RecordArgs(std::size_t args, const void* argp) {
char line[80];
std::snprintf(line, sizeof(line), "args=%016zx argp=%016zx\n", args,
static_cast<std::size_t>(reinterpret_cast<std::uintptr_t>(argp)));
Write(line);
}
extern "C" void SetPendingArgs(std::size_t args, const void* argp) {
pending.args = args;
pending.argp = argp;
}
extern "C" const void* __aps5_get_pending_module_args_nid_no_patch() {
return &pending;
}
extern "C" void __aps5_set_module_init_result_nid_no_patch(int result) {
char line[40];
std::snprintf(line, sizeof(line), "result=%d\n", result);
Write(line);
}

View File

@ -0,0 +1,116 @@
from pathlib import Path
import os
import shutil
import struct
import subprocess
import sys
import tempfile
from test_guest_symbol_names import dynamic_tags, imported_symbols, module_symbols
from test_windows_import_modules import executable
ARGS = 0x1122334455667788
ARGP = 0x99AABBCCDDEEFF00
RETURNED = 0x5A17
GETTER = '__aps5_get_pending_module_args_nid_no_patch'
SETTER = '__aps5_set_module_init_result_nid_no_patch'
def args_module():
image = bytearray(0x3000)
image[:16] = b'\x7fELF\x02\x01\x01' + bytes(9)
struct.pack_into('<HHIQQQIHHHHHH', image, 16,
3, 62, 1, 0, 64, 0, 0, 64, 56, 3, 0, 0, 0)
struct.pack_into('<IIQQQQQQ', image, 64, 1, 7, 0, 0, 0, len(image), len(image), 0x1000)
struct.pack_into('<IIQQQQQQ', image, 176, 0x6474e551, 6, 0, 0, 0, 0, 0, 16)
strings = b'\0RecordArgs\0libc.prx\0shared\0'
tags = [(5, 0x2600), (10, len(strings)), (6, 0x2400), (11, 24), (4, 0x2500),
(7, 0x2700), (8, 24), (9, 24), (1, 12), (12, 0x1010), (0, 0)]
struct.pack_into('<IIQQQQQQ', image, 120, 2, 6, 0x2000, 0x2000, 0x2000,
len(tags) * 16, len(tags) * 16, 8)
for index, tag in enumerate(tags):
struct.pack_into('<qQ', image, 0x2000 + index * 16, *tag)
image[0x2600:0x2600 + len(strings)] = strings
struct.pack_into('<IBBHQQ', image, 0x2418, 1, 0x12, 0, 0, 0, 0)
struct.pack_into('<IBBHQQ', image, 0x2430, 21, 0x12, 0, 1, 0x1000, 6)
struct.pack_into('<II', image, 0x2500, 1, 3)
struct.pack_into('<QQq', image, 0x2700, 0x2820, (1 << 32) | 6, 0)
image[0x1000:0x1006] = b'\xb8\x2a\0\0\0\xc3'
code = b'\x48\x83\xec\x08\xff\x15' + struct.pack('<i', 0x2820 - 0x101a)
code += b'\x48\x83\xc4\x08\xb8' + struct.pack('<I', RETURNED) + b'\xc3'
image[0x1010:0x1010 + len(code)] = code
return image
def no_relocation_module():
image = args_module()
for position in range(0x2000, 0x2200, 16):
tag, _ = struct.unpack_from('<qQ', image, position)
if tag == 0:
break
if tag == 8:
struct.pack_into('<Q', image, position + 8, 0)
return image
def main():
if sys.argv[1] == '--load':
import ctypes
host = ctypes.CDLL(sys.argv[2])
host.SetPendingArgs.argtypes = [ctypes.c_size_t, ctypes.c_void_p]
host.SetPendingArgs(ARGS, ARGP)
ctypes.CDLL(sys.argv[3])
return
relinker = Path(sys.argv[1]).resolve()
host = Path(sys.argv[2]).resolve()
native = sys.platform == 'linux' and os.uname().machine == 'x86_64'
with tempfile.TemporaryDirectory(prefix='anyps5-guest-args-') as directory:
work = Path(directory)
modules = work / 'sce_module'
modules.mkdir()
name = 'args.prx'
(modules / name).write_bytes(args_module())
source = work / 'input.elf'
source.write_bytes(executable(name))
output = work / '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)
converted = output.parent / 'app0' / 'sce_module' / (name + '.guest.prx')
image = converted.read_bytes()
assert GETTER in imported_symbols(image), imported_symbols(image)
assert SETTER in imported_symbols(image), imported_symbols(image)
assert 'shared#guest' in module_symbols(image), module_symbols(image)
bare = work / 'bare'
bare_modules = bare / 'sce_module'
bare_modules.mkdir(parents=True)
(bare_modules / name).write_bytes(no_relocation_module())
bare_source = bare / 'input.elf'
bare_source.write_bytes(executable(name))
bare_result = subprocess.run([str(relinker), '--skip-syscall-check', str(bare_source), str(bare / 'output.elf')],
capture_output=True, text=True, timeout=30)
assert bare_result.returncode == 0, (bare_result.stdout, bare_result.stderr)
bare_image = (bare / 'app0' / 'sce_module' / (name + '.guest.prx')).read_bytes()
bare_imports = imported_symbols(bare_image)
assert GETTER in bare_imports, bare_imports
assert SETTER in bare_imports, bare_imports
assert dynamic_tags(bare_image).get(8) == 48, dynamic_tags(bare_image)
if not native:
print('Guest module argument tests passed: native execution skipped')
return
libraries = work / 'libs'
libraries.mkdir()
shutil.copyfile(host, libraries / 'libc.prx')
events = work / 'events.txt'
run = subprocess.run([sys.executable, __file__, '--load', str(libraries / 'libc.prx'), str(converted)],
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)
recorded = events.read_text()
assert f'args={ARGS:016x} argp={ARGP:016x}' in recorded, recorded
assert f'result={RETURNED}' in recorded, recorded
print('Guest module argument tests passed')
if __name__ == '__main__':
main()