Merge pull request #6586 from JJenkx/fix/linux-runtime-module-start

fix(loader): defer Linux runtime module starts until requested
This commit is contained in:
Dean authored and GitHub committed 2026-10-24 19:54:29 +13:00
commit ec3ea2f558
15 files changed
+437 -19

No files matched your search

+11 -1
View File
@@ -214,6 +214,17 @@ if(BUILD_TESTING)
set_tests_properties(guest_process_arguments PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
find_package(Python3 QUIET COMPONENTS Interpreter)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND Python3_Interpreter_FOUND)
add_test_executable(guest_runtime_lifecycle_tests ${LIBS_INCLUDE_DIR}/tests/GuestRuntimeLifecycle.cpp)
target_include_directories(guest_runtime_lifecycle_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_runtime_lifecycle_tests PRIVATE libkernel libc ${CMAKE_DL_LIBS})
set_target_properties(guest_runtime_lifecycle_tests PROPERTIES ENABLE_EXPORTS ON)
add_dependencies(guest_runtime_lifecycle_tests relinker)
add_test(NAME guest_runtime_lifecycle COMMAND ${Python3_EXECUTABLE}
${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests/test_guest_runtime_lifecycle.py
$<TARGET_FILE:relinker> $<TARGET_FILE:guest_runtime_lifecycle_tests> $<TARGET_FILE_DIR:libkernel>)
endif()
add_library(guest_module_fixture SHARED ${LIBS_INCLUDE_DIR}/tests/GuestModuleFixture.cpp)
target_include_directories(guest_module_fixture PRIVATE ${LIBS_INCLUDE_DIR})
add_custom_command(TARGET guest_module_fixture POST_BUILD
@@ -222,7 +233,6 @@ if(BUILD_TESTING)
add_dependencies(guest_module_fixture nid_patcher)
configure_windows_unwind(guest_module_fixture)
set_target_properties(guest_module_fixture PROPERTIES BUILD_WITH_INSTALL_RPATH ON INSTALL_RPATH "$ORIGIN")
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND AND NOT WIN32)
target_link_options(guest_module_fixture PRIVATE -Wl,--no-as-needed)
add_test(NAME nid_patcher_dynamic_strings COMMAND ${Python3_EXECUTABLE}
@@ -24,6 +24,9 @@
#else
#include <dlfcn.h>
#include <link.h>
#ifdef __linux__
#include <unistd.h>
#endif
#endif
#ifdef _WIN32
@@ -137,6 +140,56 @@ char* APS5_VABI dlerror_nid_postfix() {
pendingError = false;
return loaderError.data();
}
#ifdef __linux__
static void InitializeLinuxGuestModule(void* native) {
auto* table = static_cast<std::uint64_t*>(::dlsym(native, "__aps5_guest_lifecycle_nid_no_patch"));
if (!table) return;
link_map* requested = nullptr;
link_map* owner = nullptr;
Dl_info info{};
if (::dlinfo(native, RTLD_DI_LINKMAP, &requested) != 0 || requested == nullptr ||
!::dladdr1(table, &info, reinterpret_cast<void**>(&owner), RTLD_DL_LINKMAP) || owner == nullptr)
throw std::runtime_error("dlopen: cannot identify guest lifecycle metadata owner");
if (owner != requested) return;
if (table[0] != 0x415053354c494658ull) throw std::runtime_error("dlopen: unsupported guest lifecycle metadata");
using Entry = int (APS5_VABI *)(std::size_t, const void*, void*);
using Initializer = void (APS5_VABI *)(int, char**, char**);
static std::mutex initializedLock;
static std::condition_variable initializedChanged;
const auto thread = static_cast<std::uint64_t>(::gettid());
std::unique_lock lock(initializedLock);
while (table[6] == 1) {
if (table[7] == thread) return;
initializedChanged.wait(lock);
}
if (table[6] == 2) return;
if (table[6] != 0) throw std::runtime_error("dlopen: deferred guest initialization failed or finalized");
table[6] = 1;
table[7] = thread;
lock.unlock();
try {
const auto pending = *static_cast<const PendingModuleArgs*>(__aps5_get_pending_module_args_nid_no_patch());
const int result = table[2] == 0 ? 0 : reinterpret_cast<Entry>(table[2])(pending.args, pending.argp, nullptr);
for (std::uint64_t index = 0; index < table[4]; ++index) {
const auto initializer = *reinterpret_cast<Initializer*>(table[8 + index]);
if (initializer) initializer(0, nullptr, nullptr);
}
__aps5_set_module_init_result_nid_no_patch(result);
lock.lock();
table[6] = 2;
table[7] = 0;
lock.unlock();
initializedChanged.notify_all();
} catch (...) {
lock.lock();
table[6] = 3;
table[7] = 0;
lock.unlock();
initializedChanged.notify_all();
throw;
}
}
#endif
#ifdef _WIN32
static void InitializeDeferredModule(HMODULE native) {
using Entry = int (APS5_VABI *)(std::size_t, const void*, void*);
@@ -228,6 +281,9 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) {
if (path) LoadFailure(source, resolved, ::dlerror()); else Error(::dlerror());
return nullptr;
}
#ifdef __linux__
if (path) InitializeLinuxGuestModule(module->native);
#endif
#endif
std::lock_guard lock(modulesMutex);
const auto handle = nextHandle++;
+140
View File
@@ -0,0 +1,140 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dlfcn.h>
#include <thread>
#include <source_location>
#include <pthread.h>
extern "C" {
char* APS5_VABI dlerror_nid_postfix();
std::int32_t APS5_VABI sceKernelLoadStartModule(const char*, std::size_t, const void*, std::uint32_t, const void*, int*);
int APS5_VABI sceKernelStopUnloadModule(std::int32_t, std::size_t, const void*, std::uint32_t, const void*, int*);
}
namespace {
struct Event { std::size_t args; const void* argp; };
std::array<Event, 8> events{};
std::atomic<unsigned> count{0};
std::atomic<bool> entered{false}, releaseEntry{false};
bool concurrent = false, reentrant = false;
thread_local bool isLifecycleWaiter = false;
std::atomic<bool> waiterObserved{false};
std::atomic<unsigned> waiterReturns{0};
const char* modulePath = nullptr;
std::int32_t reentrantHandle = -1;
void Require(bool ok, const std::source_location& where = std::source_location::current()) {
if (ok) return;
std::fprintf(stderr, "guest_runtime_lifecycle: failed at %u\n", where.line());
std::abort();
}
}
extern "C" int pthread_cond_wait(pthread_cond_t* condition, pthread_mutex_t* mutex) {
using Wait = int (*)(pthread_cond_t*, pthread_mutex_t*);
static const auto actual = reinterpret_cast<Wait>(::dlsym(RTLD_NEXT, "pthread_cond_wait"));
Require(actual != nullptr);
if (isLifecycleWaiter) waiterObserved.store(true, std::memory_order_release);
const int result = actual(condition, mutex);
if (isLifecycleWaiter) waiterReturns.fetch_add(1, std::memory_order_relaxed);
return result;
}
extern "C" void APS5_VABI RecordArgs(std::size_t args, const void* argp) {
const auto index = count.fetch_add(1);
Require(index < events.size());
events[index] = {args, argp};
if (index == 0 && reentrant) {
reentrantHandle = sceKernelLoadStartModule(modulePath, 0, nullptr, 0, nullptr, nullptr);
Require(reentrantHandle > 0 && count.load() == 1);
}
if (index == 0 && concurrent) {
entered.store(true, std::memory_order_release);
const auto limit = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (!releaseEntry.load(std::memory_order_acquire)) {
Require(std::chrono::steady_clock::now() < limit);
std::this_thread::yield();
}
}
}
int main(int argc, char** argv) {
Require(argc == 3);
concurrent = std::strcmp(argv[2], "concurrent") == 0;
reentrant = std::strcmp(argv[2], "reentrant") == 0;
modulePath = argv[1];
void* preload = ::dlopen(argv[1], RTLD_NOW | RTLD_LOCAL);
if (!preload) std::fprintf(stderr, "%s\n", ::dlerror());
Require(preload && count.load() == 0);
if (std::strcmp(argv[2], "never-start") == 0) {
Require(::dlclose(preload) == 0 && count.load() == 0);
return 0;
}
const std::uint64_t payload = 0xfeed0123;
const bool zeroStart = std::strcmp(argv[2], "zero-start") == 0;
const std::size_t requestedSize = zeroStart ? 0 : sizeof(payload);
const void* requestedPointer = zeroStart ? nullptr : &payload;
int result = -1;
std::int32_t first = -1, second = -1;
if (concurrent) {
std::thread one([&] { first = sceKernelLoadStartModule(argv[1], requestedSize, requestedPointer, 0, nullptr, &result); });
const auto limit = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (!entered.load(std::memory_order_acquire)) {
Require(std::chrono::steady_clock::now() < limit);
std::this_thread::yield();
}
std::thread two([&] {
isLifecycleWaiter = true;
second = sceKernelLoadStartModule(argv[1], 0, nullptr, 0, nullptr, nullptr);
isLifecycleWaiter = false;
});
while (!waiterObserved.load(std::memory_order_acquire)) {
Require(std::chrono::steady_clock::now() < limit);
std::this_thread::yield();
}
Require(count.load() == 1);
releaseEntry.store(true, std::memory_order_release);
one.join();
two.join();
Require(waiterReturns.load() > 0);
std::fprintf(stderr, "Observed lifecycle waiter before initializer release; actual condition-wait returns=%u\n", waiterReturns.load());
} else {
first = sceKernelLoadStartModule(argv[1], requestedSize, requestedPointer, 0, nullptr, &result);
second = sceKernelLoadStartModule(argv[1], 0, nullptr, 0, nullptr, nullptr);
}
std::fprintf(stderr, "first=%d second=%d result=%x count=%u\n", first, second, result, count.load());
if (first < 0) std::fprintf(stderr, "loader error: %s\n", dlerror_nid_postfix());
Require(first > 0 && second > 0 && first != second && result == 0x5a17);
Require(count.load() == 2 && events[0].args == requestedSize && events[0].argp == requestedPointer);
Require(events[1].args == 0xa1 && events[1].argp == nullptr);
Require(sceKernelStopUnloadModule(first, 0, nullptr, 0, nullptr, nullptr) == 0 && count.load() == 2);
Require(sceKernelStopUnloadModule(second, 0, nullptr, 0, nullptr, nullptr) == 0 && count.load() == 2);
if (reentrant) {
Require(reentrantHandle != first && reentrantHandle != second);
Require(::dlclose(preload) == 0 && count.load() == 2);
Require(sceKernelStopUnloadModule(reentrantHandle, 0, nullptr, 0, nullptr, nullptr) == 0 && count.load() == 4);
} else {
Require(::dlclose(preload) == 0 && count.load() == 4);
}
Require(events[2].args == 0xf1 && events[2].argp == nullptr);
Require(events[3].args == 0xfe && events[3].argp == nullptr);
if (std::strcmp(argv[2], "reload") == 0) {
void* resident = ::dlopen(argv[1], RTLD_NOW | RTLD_NOLOAD);
Require(resident == nullptr);
result = -1;
const auto reload = sceKernelLoadStartModule(argv[1], sizeof(payload), &payload, 0, nullptr, &result);
Require(reload > 0 && reload != first && reload != second && result == 0x5a17);
Require(count.load() == 6 && events[4].args == sizeof(payload) && events[4].argp == &payload);
Require(events[5].args == 0xa1 && events[5].argp == nullptr);
Require(sceKernelStopUnloadModule(reload, 0, nullptr, 0, nullptr, nullptr) == 0 && count.load() == 8);
Require(events[6].args == 0xf1 && events[6].argp == nullptr);
Require(events[7].args == 0xfe && events[7].argp == nullptr);
Require(::dlopen(argv[1], RTLD_NOW | RTLD_NOLOAD) == nullptr);
}
}
@@ -9,7 +9,7 @@ namespace Elfpatcher {
class GuestModuleWriter {
public:
std::vector<std::uint8_t> WriteLinux(const Relinker::GuestImage& image, const std::vector<std::string>& dependencies, const std::string& runPath) const;
std::vector<std::uint8_t> WriteLinux(const Relinker::GuestImage& image, const std::vector<std::string>& dependencies, const std::string& runPath, bool deferInitialization = false) const;
std::vector<std::uint8_t> WriteWindows(const Relinker::GuestImage& image, Domain::GuestRuntime& runtime, const std::function<void(std::string_view)>& message = {}) const;
std::vector<std::uint8_t> WriteMacOs(const Relinker::GuestImage& image, const std::vector<std::string>& dependencies, const std::string& runPath, const std::function<void(std::string_view)>& message = {}) const;
};
@@ -9,7 +9,7 @@
namespace Elfpatcher {
std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestImage& image, const std::vector<std::string>& dependencies, const std::string& runPath) const {
std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestImage& image, const std::vector<std::string>& dependencies, const std::string& runPath, const bool deferInitialization) const {
auto bytes = image.Bytes;
std::vector<Domain::ProgramHeader> headers;
std::uint64_t end = 0;
@@ -35,7 +35,7 @@ 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.HasInit;
const bool passesModuleArgs = image.HasInit && !deferInitialization;
std::uint64_t argsSlot = 0;
std::uint64_t resultSlot = 0;
if (passesModuleArgs) {
@@ -83,6 +83,39 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestIma
Io::WriteU64(relaData, entry, slot);
Io::WriteU64(relaData, entry + 8, (symbol << 32) | kGlobDat);
};
std::uint64_t lifecycleTable = 0;
if (deferInitialization) {
constexpr char exportName[] = "__aps5_guest_lifecycle_nid_no_patch";
if (std::any_of(image.Symbols.begin(), image.Symbols.end(), [&](const auto& symbol) { return symbol.Name == exportName; }))
throw Domain::RelinkerException("Reserved guest lifecycle export is already present");
Io::AlignBuffer(bytes, 8);
lifecycleTable = address();
const auto pointer = [&](std::uint64_t target, bool relocate = true) {
const auto slot = address();
Io::AppendU64(bytes, 0);
if (!relocate) return;
Io::AppendU64(relaData, slot);
Io::AppendU64(relaData, 8);
Io::AppendU64(relaData, target);
};
Io::AppendU64(bytes, 0x415053354c494658ull);
pointer(0);
pointer(image.Init, image.HasInit);
pointer(image.Fini, image.Fini != 0);
Io::AppendU64(bytes, image.InitArray.size());
Io::AppendU64(bytes, image.FiniArray.size());
Io::AppendU64(bytes, 0);
Io::AppendU64(bytes, 0);
for (const auto slot : image.InitArray) pointer(slot);
for (const auto slot : image.FiniArray) pointer(slot);
const auto entry = symbols.size();
symbols.resize(entry + 24, 0);
Io::WriteU32(symbols, entry, addString(exportName));
symbols[entry + 4] = 0x11;
Io::WriteU16(symbols, entry + 6, 1);
Io::WriteU64(symbols, entry + 8, lifecycleTable);
Io::WriteU64(symbols, entry + 16, address() - lifecycleTable);
}
if (passesModuleArgs) {
argsImport = addUndefinedImport("__aps5_get_pending_module_args_nid_no_patch");
resultImport = addUndefinedImport("__aps5_set_module_init_result_nid_no_patch");
@@ -156,8 +189,33 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestIma
Io::AppendU32(bytes, static_cast<std::uint32_t>(distance));
return start;
};
const auto init = lifecycle(image.Init);
const auto fini = finalizer(image.Fini);
const auto init = deferInitialization ? 0 : lifecycle(image.Init);
auto fini = deferInitialization ? 0 : finalizer(image.Fini);
if (deferInitialization && (image.Fini != 0 || !image.FiniArray.empty())) {
fini = address();
bytes.insert(bytes.end(), {0x48, 0x83, 0x3d});
Io::AppendU32(bytes, stubDisplacement(lifecycleTable + 48, address() + 5));
bytes.push_back(2);
bytes.insert(bytes.end(), {0x0f, 0x85});
const auto skip = bytes.size();
Io::AppendU32(bytes, 0);
bytes.insert(bytes.end(), {0x48, 0xc7, 0x05});
Io::AppendU32(bytes, stubDisplacement(lifecycleTable + 48, address() + 8));
Io::AppendU32(bytes, 4);
bytes.insert(bytes.end(), {0x48, 0x83, 0xec, 8});
for (auto slot = image.FiniArray.rbegin(); slot != image.FiniArray.rend(); ++slot) {
bytes.insert(bytes.end(), {0x48, 0x8b, 0x05});
Io::AppendU32(bytes, stubDisplacement(*slot, address() + 4));
bytes.insert(bytes.end(), {0x48, 0x85, 0xc0, 0x74, 8, 0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xff, 0xd0});
}
if (image.Fini != 0) {
bytes.insert(bytes.end(), {0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xe8});
Io::AppendU32(bytes, stubDisplacement(image.Fini, address() + 4));
}
bytes.insert(bytes.end(), {0x48, 0x83, 0xc4, 8});
Io::WriteU32(bytes, skip, static_cast<std::uint32_t>(bytes.size() - skip - 4));
bytes.push_back(0xc3);
}
Io::AlignBuffer(bytes, 8);
const auto dynamicOffset = bytes.size();
const auto dynamicAddress = address();
@@ -181,8 +239,8 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestIma
tag(20, 7);
tag(3, image.Got);
}
if (!image.InitArray.empty()) { tag(25, image.InitArray.front()); tag(27, image.InitArray.size() * 8); }
if (!image.FiniArray.empty()) { tag(26, image.FiniArray.front()); tag(28, image.FiniArray.size() * 8); }
if (!deferInitialization && !image.InitArray.empty()) { tag(25, image.InitArray.front()); tag(27, image.InitArray.size() * 8); }
if (!deferInitialization && !image.FiniArray.empty()) { tag(26, image.FiniArray.front()); tag(28, image.FiniArray.size() * 8); }
if (init != 0) tag(12, init);
if (fini != 0) tag(13, fini);
tag(30, DF_BIND_NOW | (image.Symbolic ? DF_SYMBOLIC : 0));
@@ -439,7 +439,10 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string());
}
needed.insert(needed.end(), moduleHostLibraries[index].begin(), moduleHostLibraries[index].end());
output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath, message) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath);
const auto directory = image.SourcePath.parent_path();
const bool deferLinuxStart = isRuntime(index) && directory != root / "sce_module" &&
directory != root / "sce_modules" && directory != root / "prx";
output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath, message) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath, deferLinuxStart);
}
if (windows) {
for (const auto& [name, provider] : guestNames) {
@@ -127,6 +127,35 @@ void convert() {
require(progress.Events.back().Completed != 1 || progress.Events.back().Stage != Domain::ProgressStage::Executable, "Failed conversion reported completion");
}
void runtimeLifecyclePolicy() {
TestProgress progress;
const Relinker::Conversion converter(progress);
for (const auto* directory : {"sce_module", "sce_modules", "prx", "Media/Plugins"}) {
auto source = input();
const auto parent = source.ModuleRoot / directory;
auto module = fixture();
Io::WriteU64(module, 0x2080, 12);
Io::WriteU64(module, 0x2088, 0x1000);
Io::WriteU64(module, 208, 160);
Io::WriteU64(module, 216, 160);
source.Modules.push_back({parent / "plugin.prx", std::move(module)});
source.RuntimeDirectories.push_back(parent);
const auto output = converter.Convert(std::move(source), {});
const auto& bytes = output.Modules.at(0).Bytes;
const auto headers = Io::ReadU64(bytes, 32);
bool initializesOnMapping = false;
for (std::uint16_t index = 0; index < Io::ReadU16(bytes, 56); ++index) {
const auto header = headers + index * 56;
if (Io::ReadU32(bytes, header) != 2) continue;
const auto dynamic = Io::ReadU64(bytes, header + 8);
const auto end = dynamic + Io::ReadU64(bytes, header + 32);
for (auto offset = dynamic; offset < end; offset += 16)
if (Io::ReadU64(bytes, offset) == 12) initializesOnMapping = Io::ReadU64(bytes, offset + 8) != 0;
}
require(initializesOnMapping == (std::string_view(directory) != "Media/Plugins"), "Default SDK directories lost their mapping initializer");
}
}
void fixedBase() {
TestProgress progress;
const Relinker::Conversion converter(progress);
@@ -348,6 +377,7 @@ void relocationValidation() {
int main() {
try {
convert();
runtimeLifecyclePolicy();
fixedBase();
scanning();
diagnosticContext();
@@ -124,8 +124,8 @@ def main():
env={**os.environ, "ANYPS5_GUEST_MODULE_ARGS": str(events)})
assert run.returncode == 42, (root, run.returncode, run.stdout, run.stderr)
recorded = events.read_text() if events.exists() else ""
assert recorded.count("args=") == (1 if root in ("executable", "bundled", "imported") else 0), (root, recorded)
load_and_record(case, libraries / "libc.prx", runtime / "plugin.prx.guest.prx")
assert recorded.count("args=") == 0, (root, recorded)
load_and_record(case, libraries / "libc.prx", runtime / "provider.prx.guest.prx")
print("Guest deferred dependency tests passed")
@@ -105,6 +105,11 @@ def check_lifecycle_host(relinker, host, work, linux, deferred):
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 not events.exists(), events.read_text()
run = subprocess.run([sys.executable, __file__, '--load', str(libraries / 'libkernel.prx'), 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()
@@ -161,7 +166,20 @@ def main():
host = ctypes.CDLL(sys.argv[2], mode=ctypes.RTLD_GLOBAL if sys.platform == 'darwin' else ctypes.DEFAULT_MODE)
host.SetPendingArgs.argtypes = [ctypes.c_size_t, ctypes.c_void_p]
host.SetPendingArgs(ARGS, ARGP)
ctypes.CDLL(sys.argv[3])
module = ctypes.CDLL(sys.argv[3])
try:
table = (ctypes.c_uint64 * 8).in_dll(module, '__aps5_guest_lifecycle_nid_no_patch')
except ValueError:
return
assert table[0] == 0x415053354c494658 and table[6] == 0
table[6] = 1
returned = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_void_p)(table[2])(ARGS, ARGP, None) if table[2] else 0
for index in range(table[4]):
callback = ctypes.c_void_p.from_address(ctypes.c_uint64.from_address(ctypes.addressof(table) + 64 + index * 8).value).value
if callback:
ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)(callback)(0, None, None)
host.__aps5_set_module_init_result_nid_no_patch(returned)
table[6] = 2
return
relinker = Path(sys.argv[1]).resolve()
host = Path(sys.argv[2]).resolve()
@@ -0,0 +1,57 @@
from pathlib import Path
import os
import struct
import subprocess
import sys
import tempfile
from test_guest_module_args import args_module
def lifecycle_module():
image = args_module()
tags = []
for offset in range(0x2000, 0x2300, 16):
tag, value = struct.unpack_from("<qQ", image, offset)
if tag == 0:
break
tags.append((tag, 72 if tag == 8 else value))
tags += [(13, 0x1080), (25, 0x2880), (27, 8), (26, 0x2888), (28, 8), (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)
struct.pack_into("<QQq", image, 0x2718, 0x2880, 8, 0x10c0)
struct.pack_into("<QQq", image, 0x2730, 0x2888, 8, 0x1100)
for address, marker in ((0x1080, 0xfe), (0x10c0, 0xa1), (0x1100, 0xf1)):
code = b"\x48\x83\xec\x08\xbf" + struct.pack("<I", marker) + b"\x31\xf6\xff\x15"
code += struct.pack("<i", 0x2820 - address - len(code) - 4)
code += b"\x48\x83\xc4\x08\xc3"
image[address:address + len(code)] = code
return image
def main():
relinker, executable, libraries = map(lambda p: Path(p).resolve(), sys.argv[1:4])
with tempfile.TemporaryDirectory(prefix="anyps5-runtime-lifecycle-") as directory:
root = Path(directory)
plugins = root / "Media" / "Plugins"
plugins.mkdir(parents=True)
(plugins / "args.prx").write_bytes(lifecycle_module())
from test_guest_module_directories import module_with_symbol
bundled = root / "sce_module"
bundled.mkdir()
(bundled / "dummy.prx").write_bytes(module_with_symbol(False))
source = root / "main.elf"
from test_guest_intel_trampolines import main_fixture
source.write_bytes(main_fixture())
run = subprocess.run([str(relinker), "--skip-syscall-check", "--module-dir", "Media/Plugins", "--rpath", str(libraries), str(source), str(root / "app.elf")], capture_output=True, text=True, timeout=30)
assert run.returncode == 0, (run.stdout, run.stderr)
module = root / "app0/Media/Plugins/args.prx.guest.prx"
for mode in ("never-start", "start", "zero-start", "concurrent", "reentrant", "reload"):
run = subprocess.run([str(executable), "./" + module.name, mode], cwd=module.parent, env=os.environ, capture_output=True, text=True, timeout=10)
assert run.returncode == 0, (mode, run.returncode, run.stdout, run.stderr)
print("Runtime module initialization and finalization passed")
if __name__ == "__main__":
main()
@@ -9,7 +9,7 @@ import tempfile
from guest_import_fixtures import dynamic
from test_guest_deferred_dependencies import page_aligned
from test_guest_intel_trampolines import main_fixture
from test_guest_module_args import args_module
from test_guest_module_args import args_module, load_and_record
from test_guest_module_directories import module_with_symbol, needed_libraries
from test_guest_module_dir import pe_export_names
from test_guest_needed_modules import module_with_needed
@@ -98,7 +98,8 @@ def main():
else:
assert run.returncode == 42, (kind, run.returncode, run.stdout, run.stderr)
recorded = events.read_text() if events.exists() else ''
assert recorded.count('args=') == (1 if kind in ('referenced', 'weak-referenced', 'plt', 'needed') else 0), (kind, recorded)
assert recorded.count('args=') == 0, (kind, recorded)
load_and_record(case, libraries / 'libc.prx', case / provider)
cases += 1
cycle = Path(directory) / f'{system}-cycle'
(cycle / 'sce_module').mkdir(parents=True)
@@ -8,10 +8,10 @@ import tempfile
from guest_import_fixtures import executable
from test_guest_intel_trampolines import main_fixture, pe_sections
from test_guest_module_args import ARGP, ARGS, GETTER, RETURNED, SETTER, macho_binds
from test_guest_module_args import ARGP, ARGS, GETTER, RETURNED, SETTER, load_and_record, macho_binds
from test_guest_module_directories import needed_libraries
from test_guest_module_dir import pe_export_names
from test_guest_symbol_names import dynamic_tags, imported_symbols
from test_guest_symbol_names import dynamic_tags, file_offset, imported_symbols, module_symbols
PREINIT_SLOT = 0x2900
SHARED = 0x100
@@ -85,6 +85,46 @@ def convert(relinker, work, preinit, options, base=0):
return output, work / 'app0' / 'sce_module' / (name + '.guest.prx')
def check_deferred_linux(relinker, work, host, preinit, has_init, native):
(work / 'sce_module').mkdir(parents=True)
plugins = work / 'plugins'
plugins.mkdir()
source = work / 'input.elf'
source.write_bytes(main_fixture())
image = zero_init_module(preinit)
if not has_init:
for position in range(0x2000, 0x2300, 16):
if struct.unpack_from('<q', image, position)[0] == 12:
struct.pack_into('<q', image, position, 21)
break
else:
raise AssertionError('Fixture has no DT_INIT tag to remove')
(plugins / 'zero.prx').write_bytes(image)
libraries = work / 'libs'
libraries.mkdir()
provider = libraries / 'libc.prx'
shutil.copyfile(host, provider)
shutil.copyfile(host, libraries / 'libkernel.prx')
result = subprocess.run([str(relinker), '--skip-syscall-check', '--module-dir', 'plugins',
'--rpath', '$ORIGIN/libs', str(source), str(work / 'output.elf')],
capture_output=True, text=True, timeout=30)
assert result.returncode == 0, (preinit, has_init, result.stdout, result.stderr)
converted = work / 'app0/plugins/zero.prx.guest.prx'
image = converted.read_bytes()
assert ('libkernel.prx' in needed_libraries(image)) == has_init, 'Deferred initializer dependency does not match DT_INIT presence'
tags = dynamic_tags(image)
assert tags.get(12, 0) == 0 and tags.get(25, 0) == 0, 'Deferred Linux module has an eager initializer'
index = module_symbols(image).index('__aps5_guest_lifecycle_nid_no_patch') + 1
table, = struct.unpack_from('<Q', image, file_offset(image, tags[6]) + index * 24 + 8)
relocations = [struct.unpack_from('<QQq', image, file_offset(image, tags[7]) + offset)
for offset in range(0, tags[8], tags[9])]
init_relocations = [entry for entry in relocations if entry[0] == table + 16]
expected = [(table + 16, 8, 0)] if has_init else []
assert init_relocations == expected, (preinit, has_init, init_relocations, expected)
if native and has_init:
load_and_record(work, provider, converted)
def main():
if sys.argv[1] == '--load':
import ctypes
@@ -138,6 +178,9 @@ def main():
assert run.returncode == 0, (preinit, run.returncode, run.stdout, run.stderr)
recorded = events.read_text()
assert f'args={ARGS:016x} argp={ARGP:016x}\nresult={RETURNED}\n' == recorded, (preinit, recorded)
for preinit, has_init in ((False, True), (True, True), (False, False)):
check_deferred_linux(relinker, work / f'linux-runtime-{preinit}-{has_init}',
host, preinit, has_init, linux)
for index, options in enumerate(([], ['--macos'], ['--windows'])):
convert(relinker, work / f'outside-{index}', False, options, 0x1000)
print('Guest zero initializer tests passed' if linux else 'Guest zero initializer tests passed: native execution skipped')
+2
View File
@@ -125,6 +125,8 @@
- [scePthreadYield](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) on Windows calls Sleep(1) after 512 consecutive yields that find no other thread ready on the current processor and arrive less than ~10,000 TSC ticks apart (so the span varies with host CPU TSC rate); FreeBSD sched_yield never sleeps.
- [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling
- [Linux runtime module initialization](../../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) (libkernel) - modules in explicit runtime directories can be mapped for bindings before an explicit guest load runs their entry and init array once with the caller arguments. Finalizers run only after initialization completed. Console dependency-start and finalization ordering are unmeasured. Windows and macOS use their existing lifecycle paths.
- [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console.
- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute
- [JIT shared memory on macOS](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - the object is an unlinked `mkstemp` file in the temporary directory, as `shm_open` makes its anonymous objects. macOS refuses an executable shared file mapping (`EPERM`), so an executable view is mapped without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view after it ran from another keeps its old translation until the executable view's protection changes; first writes and a single read-write-execute view, as XPSemu PPSA97358 and VLC PPSA85300 map, are seen. `mmap` of a JIT descriptor is not supported, as file-backed guest `mmap` throws on macOS.
+2 -2
View File
@@ -4,8 +4,8 @@
- [Executable guest imports](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - on Linux and macOS, a relocated executable import resolved to one guest provider requires that module and its dependencies at startup, even when it was discovered through `--module-dir` without a DT_NEEDED entry. Unreferenced symbols and no-op relocations do not require an otherwise deferred module. Console module-start ordering is unmeasured.
- [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded.
- [Runtime-loaded modules the executable needs](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) are mapped at startup on Windows and started when the title loads them with `sceKernelLoadStartModule`, with their start arguments, as a Unity player loads `mono-ps4.sprx`. A title that calls such a module before loading it finds its initializers not run; whether the console starts it earlier is not measured. A module the executable only imports from and the title never loads itself gets no `module_start` and no initializers at all under this deferral, and nothing reports it. Linux and macOS still start it at startup.
- [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) can be mapped early for bindings. Linux defers their initialization to an explicit load of each module; standard SDK folders keep their mapping initializer, including through the browser conversion API. On Windows and macOS, a module another module needs is initialized at startup without start arguments; console dependency-start ordering is unmeasured.
- [Runtime-loaded modules the executable needs](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) are mapped at startup on Windows and started when the title loads them with `sceKernelLoadStartModule`, with their start arguments, as a Unity player loads `mono-ps4.sprx`. A title that calls such a module before loading it finds its initializers not run; whether the console starts it earlier is not measured. A module the executable only imports from and the title never loads itself gets no `module_start` and no initializers at all under this deferral, and nothing reports it. macOS still starts it at startup.
- The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost.
- [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability.
- Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10.
+1 -1
View File
@@ -77,7 +77,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath`
| `--rpath <path>` | Set the system library search path. Quote `$ORIGIN` to prevent shell expansion, for example `--rpath '$ORIGIN/libs'` in Bash or PowerShell. Linux guest modules require an absolute path or a path beginning with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. |
| `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Adds executable permissions for Linux output. Requires the target OS and prepared runtime layout. |
| `--sce-module-path <path>` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. |
| `--module-dir <dir>` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module another module needs is initialized at startup, and on Linux and macOS also one the executable needs; any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`, or `mono-ps4.sprx` on Windows). Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. |
| `--module-dir <dir>` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. Needed modules are mapped at startup. On Linux, modules in these extra directories initialize only when explicitly loaded, with the arguments from `sceKernelLoadStartModule` (for example Unity plugins in `Media/Plugins`). A module another module needs initializes at startup on Windows and macOS; one only the executable needs initializes when explicitly loaded on Windows and at startup on macOS. Other modules initialize when loaded. Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. |
| `--executable <path>` | Also convert another executable of the title, which it starts with `sceSystemServiceLoadExec`; the path is relative to the module parent directory, for example `picker/picker.bin`. It is written beside the output, named after the path with `/` replaced by `.` and given the output's extension (`picker.picker.bin.exe`), and uses the same `libs/`, `app0/` and guest modules. It is an error if a guest module converts differently for it. Repeatable. Conflicts with `--skip-sce-module`. |
| `--patches <file>` | Apply the byte patches of this file during conversion, see [game patches](#game-patches). Overrides `relinker-patches.toml` discovery. Conflicts with `--no-patches`. |
| `--no-patches` | Do not apply patches, and do not load `relinker-patches.toml` from the input directory. |