From 36c979b369e3c1516c000408d2633a5e2365b2e7 Mon Sep 17 00:00:00 2001 From: jeffrey12cali <27741664+jeffrey12cali@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:37:43 +0200 Subject: [PATCH 1/4] fix(libkernel): pass module args and argp to runtime-loaded modules on Linux sceKernelLoadStartModule discarded its arguments, and the relinker's Linux guest writer synthesized an initialisation trampoline that also cleared the argument registers before jumping to the module entry point, so a converted module always reached its own start-up code with args = 0 and argp = NULL. On PPSA28997 two of the title's own modules validate that parameter block as their first act and dereference it unconditionally: SaveData faults on `mov (%rsi),%eax` with rsi = 0 beside its `cmp $0x10` size checks, and PSN faults the same way at PSN+0x3ee7 with rbx = 0. The Windows branch delivers the block, so identical code reports its parameter-version error and continues instead of faulting. sceKernelLoadStartModule now records the pair before dlopen and clears it afterwards, and the emitted DT_INIT stub reads it through two accessors exported by libkernel. The initialiser is called rather than tail-jumped so its return value can be reported through *res as the PS5 API specifies. The state is thread_local, which makes the handoff per-call: ld.so runs a module's initialiser on the thread that called dlopen. DT_FINI keeps a plain call-through, so unloading neither reads stale arguments nor reports a result. The accessor names use the _nid_no_patch suffix so the NID resolver leaves them alone and a guest import can name them exactly. --- core/libs/prx/libkernel/Module/src/Module.cpp | 31 ++++++- .../src/linux/LinuxGuestModuleWriter.cpp | 80 +++++++++++++++++-- 2 files changed, 101 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 886503c99..6636e0dfc 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -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(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(SCE_KERNEL_ERROR_ENOENT); return static_cast(reinterpret_cast(handle)); } diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index 9653c8b9f..3ee50ad1f 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -26,8 +26,19 @@ std::vector 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::max()) throw Domain::RelinkerException("Guest string table too large"); const auto offset = static_cast(strings.size()); @@ -46,6 +57,30 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma std::vector 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 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(image.Symbols.size()); + const auto count = static_cast(symbols.size() / 24); const auto bucketCount = std::max(count, 1); std::vector buckets(bucketCount), chains(count); for (std::uint32_t index = count; index-- > 1;) { @@ -72,20 +107,49 @@ std::vector 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, std::uint64_t start) { + const auto distance = static_cast(target) - static_cast(next); + if (distance < std::numeric_limits::min() || distance > std::numeric_limits::max()) + throw Domain::RelinkerException("Guest initializer exceeds relative branch range"); + return static_cast(static_cast(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(target) - static_cast(address() + 4); - if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) throw Domain::RelinkerException("Guest initializer exceeds relative branch range"); - Io::AppendU32(bytes, static_cast(displacement)); + std::vector stub(kStubSize, 0xc3); + stub[0x00] = 0x50; + stub[0x01] = 0xff; stub[0x02] = 0x15; + Io::WriteU32(stub, kArgsDisplacement, stubDisplacement(argsSlot, start + 7, start)); + 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, start)); + stub[0x15] = 0x89; stub[0x16] = 0xc7; + stub[0x17] = 0xff; stub[0x18] = 0x15; + Io::WriteU32(stub, kResultDisplacement, stubDisplacement(resultSlot, start + 0x1d, start)); + 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(target) - static_cast(start + 5); + if (distance < std::numeric_limits::min() || distance > std::numeric_limits::max()) + throw Domain::RelinkerException("Guest finaliser exceeds relative branch range"); + bytes.push_back(0xe9); + Io::AppendU32(bytes, static_cast(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(); @@ -100,7 +164,7 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma tag(11, 24); if (!image.Dynamic.RelaData.empty()) { tag(7, relaAddress); - tag(8, image.Dynamic.RelaData.size()); + tag(8, relaData.size()); tag(9, 24); } if (!image.Dynamic.RelaPltData.empty()) { From 7b8b2047b82ea2463e3c62141264182c9c59e0da Mon Sep 17 00:00:00 2001 From: jeffrey12cali <27741664+jeffrey12cali@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:17:25 +0200 Subject: [PATCH 2/4] test(relinker): assert converted modules receive the module argument block Add a hand-built guest module with an explicit DT_INIT that forwards rdi and rsi to a host recorder without touching them and returns a known value. The test sets a known argument count and argument pointer, loads the converted module, and asserts that the initialiser received both values and that the stub forwarded its return value. The host recorder also provides the two accessor symbols, so the test resolves them without loading libkernel. The converted module must import both accessors, which is what distinguishes the new behaviour from the previous stub that zeroed the arguments. --- core/relinker/CMakeLists.txt | 7 ++ .../elfpatcher/tests/GuestModuleArgsHost.cpp | 56 ++++++++++++ .../relinker/tests/test_guest_module_args.py | 91 +++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp create mode 100644 core/relinker/relinker/tests/test_guest_module_args.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d93145995..6ed3c7b25 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -239,6 +239,13 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_registry.py $) add_test(NAME guest_host_libc COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_host_libc.py $) + 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 + $ $) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp new file mode 100644 index 000000000..708d39dc7 --- /dev/null +++ b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp @@ -0,0 +1,56 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +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(reinterpret_cast(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); +} diff --git a/core/relinker/relinker/tests/test_guest_module_args.py b/core/relinker/relinker/tests/test_guest_module_args.py new file mode 100644 index 000000000..1697be0be --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_module_args.py @@ -0,0 +1,91 @@ +from pathlib import Path +import os +import shutil +import struct +import subprocess +import sys +import tempfile + +from test_guest_symbol_names import 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(' Date: Thu, 8 Oct 2026 17:26:43 +0200 Subject: [PATCH 3/4] fix(relinker): emit the relocation table when the guest module has none The DT_RELA tags were still guarded on the input module's relocation table, while the new accessor relocations were appended to a copy of it. A module with an initialiser and no imports therefore kept the argument stub but lost both relocations that fill its GOT slots, so the stub called through a null pointer at load time. Guard on the extended table instead, and drop the unused start parameter from the displacement helper. The test now converts a module whose DT_RELASZ is zero and asserts that the output carries both accessor imports and a 48-byte relocation table. --- .../src/linux/LinuxGuestModuleWriter.cpp | 10 +++---- .../relinker/tests/test_guest_module_args.py | 27 ++++++++++++++++++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index 3ee50ad1f..834c21021 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -112,7 +112,7 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma 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, std::uint64_t start) { + const auto stubDisplacement = [](std::uint64_t target, std::uint64_t next) { const auto distance = static_cast(target) - static_cast(next); if (distance < std::numeric_limits::min() || distance > std::numeric_limits::max()) throw Domain::RelinkerException("Guest initializer exceeds relative branch range"); @@ -124,15 +124,15 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma std::vector stub(kStubSize, 0xc3); stub[0x00] = 0x50; stub[0x01] = 0xff; stub[0x02] = 0x15; - Io::WriteU32(stub, kArgsDisplacement, stubDisplacement(argsSlot, start + 7, start)); + 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, start)); + 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, start)); + Io::WriteU32(stub, kResultDisplacement, stubDisplacement(resultSlot, start + 0x1d)); stub[0x1d] = 0x58; stub[0x1e] = 0xc3; bytes.insert(bytes.end(), stub.begin(), stub.end()); @@ -162,7 +162,7 @@ std::vector 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, relaData.size()); tag(9, 24); diff --git a/core/relinker/relinker/tests/test_guest_module_args.py b/core/relinker/relinker/tests/test_guest_module_args.py index 1697be0be..992eaf8a0 100644 --- a/core/relinker/relinker/tests/test_guest_module_args.py +++ b/core/relinker/relinker/tests/test_guest_module_args.py @@ -6,7 +6,7 @@ import subprocess import sys import tempfile -from test_guest_symbol_names import imported_symbols, module_symbols +from test_guest_symbol_names import dynamic_tags, imported_symbols, module_symbols from test_windows_import_modules import executable ARGS = 0x1122334455667788 @@ -42,6 +42,17 @@ def args_module(): return image +def no_relocation_module(): + image = args_module() + for position in range(0x2000, 0x2200, 16): + tag, _ = struct.unpack_from(' Date: Sat, 10 Oct 2026 00:23:46 +0200 Subject: [PATCH 4/4] test(relinker): make guest_module_args run its native path The machine check compared against 'x86-64' (uname reports 'x86_64'), so the native execution was always skipped. With it enabled the fixture itself was wrong: the initialiser's rip-relative call used the wrong displacement, and the pending block was set on a different copy of the host library than the one the module binds to. Co-Authored-By: Claude --- core/relinker/relinker/tests/test_guest_module_args.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/relinker/relinker/tests/test_guest_module_args.py b/core/relinker/relinker/tests/test_guest_module_args.py index 992eaf8a0..54ec3622a 100644 --- a/core/relinker/relinker/tests/test_guest_module_args.py +++ b/core/relinker/relinker/tests/test_guest_module_args.py @@ -36,7 +36,7 @@ def args_module(): struct.pack_into('