From 1b865fc3ea4b3300dcdfdc6ecfcd84b18197dfe7 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:22:38 +0000 Subject: [PATCH] fix(relinker): pass module start arguments to macOS guest initializers The macOS image initializer called a guest module's DT_INIT with no arguments, so a module started with sceKernelLoadStartModule never saw its args and argp, and its result was dropped. The Linux writer already calls DT_INIT through libkernel's pending argument handoff. On the macOS port, the PSN plugin of PPSA12544 refuses to start without its argument block ("PSN is an old version that cannot be used by the current player runtime") and the title then crashes. A guest module with DT_INIT now gets two slots bound to __aps5_get_pending_module_args_nid_no_patch and __aps5_set_module_init_result_nid_no_patch, and its initializer calls DT_INIT with the args and argp that sceKernelLoadStartModule records, as on Linux, then hands the result back. The TechnicalDebt sentence about the missing arguments on macOS is removed. guest_module_args also relinks its fixture with --macos and checks the two binds; on x86-64 macOS it loads the module and checks that DT_INIT receives the arguments and that its result is returned. AI-assisted: yes --- .../elfpatcher/src/macos/MacOsImage.cpp | 14 ++++- .../relinker/tests/test_guest_module_args.py | 60 ++++++++++++++----- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 098505bf8..c8db078cd 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -424,6 +424,15 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { metaEnd += 8; input.Binds.push_back({symbol, metaRva + static_cast(slot)}); } + std::uint64_t moduleArgsSlot = 0; + std::uint64_t moduleResultSlot = 0; + if (input.InitRva != 0) { + moduleArgsSlot = metaEnd; + moduleResultSlot = metaEnd + 8; + metaEnd += 16; + input.Binds.push_back({"___aps5_get_pending_module_args_nid_no_patch", metaRva + static_cast(moduleArgsSlot)}); + input.Binds.push_back({"___aps5_set_module_init_result_nid_no_patch", metaRva + static_cast(moduleResultSlot)}); + } meta.resize(metaEnd, 0); for (std::size_t index = 0; index < input.TlsExports.size(); ++index) { const auto& variable = input.TlsExports[index]; @@ -545,8 +554,11 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { code.Rip({0x48, 0x89, 0x05}, import.Rva); } if (input.InitRva != 0) { - callZeroArgs(); + code.Rip({0xff, 0x15}, metaRva + static_cast(moduleArgsSlot)); + code.Emit({0x48, 0x8b, 0x38, 0x48, 0x8b, 0x70, 0x08, 0x31, 0xd2}); code.Rip({0xe8}, input.InitRva); + code.Emit({0x89, 0xc7}); + code.Rip({0xff, 0x15}, metaRva + static_cast(moduleResultSlot)); } for (const auto slot : input.InitArrayRvas) callSlot(slot); if (hasFini) { diff --git a/core/relinker/relinker/tests/test_guest_module_args.py b/core/relinker/relinker/tests/test_guest_module_args.py index 54ec3622a..509c98c23 100644 --- a/core/relinker/relinker/tests/test_guest_module_args.py +++ b/core/relinker/relinker/tests/test_guest_module_args.py @@ -1,5 +1,6 @@ from pathlib import Path import os +import platform import shutil import struct import subprocess @@ -7,6 +8,7 @@ import sys import tempfile from test_guest_symbol_names import dynamic_tags, imported_symbols, module_symbols +from test_macos_output import MachO from test_windows_import_modules import executable ARGS = 0x1122334455667788 @@ -53,17 +55,34 @@ def no_relocation_module(): return image +def macho_binds(image): + linkedit = MachO(image).segment('__LINKEDIT') + return image[linkedit['fileoff']:linkedit['fileoff'] + linkedit['filesize']] + + +def load_and_record(work, provider, module): + events = work / 'events.txt' + run = subprocess.run([sys.executable, __file__, '--load', str(provider), str(module)], + 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 + + def main(): if sys.argv[1] == '--load': import ctypes - host = ctypes.CDLL(sys.argv[2]) + 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]) return relinker = Path(sys.argv[1]).resolve() host = Path(sys.argv[2]).resolve() - native = sys.platform == 'linux' and os.uname().machine == 'x86_64' + linux = sys.platform == 'linux' and os.uname().machine == 'x86_64' + macos = sys.platform == 'darwin' and platform.machine() == 'x86_64' with tempfile.TemporaryDirectory(prefix='anyps5-guest-args-') as directory: work = Path(directory) modules = work / 'sce_module' @@ -95,20 +114,33 @@ def main(): 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: + mach = work / 'macos' + mach_modules = mach / 'sce_module' + mach_modules.mkdir(parents=True) + (mach_modules / name).write_bytes(args_module()) + mach_source = mach / 'input.elf' + mach_source.write_bytes(executable(name)) + mach_result = subprocess.run([str(relinker), '--macos', '--skip-syscall-check', str(mach_source), str(mach / 'output')], + capture_output=True, text=True, timeout=30) + assert mach_result.returncode == 0, (mach_result.stdout, mach_result.stderr) + mach_converted = mach / 'app0' / 'sce_module' / (name + '.guest.prx') + binds = macho_binds(mach_converted.read_bytes()) + assert ('_' + GETTER).encode() in binds, f'the macOS module does not bind {GETTER}' + assert ('_' + SETTER).encode() in binds, f'the macOS module does not bind {SETTER}' + if linux: + libraries = work / 'libs' + libraries.mkdir() + shutil.copyfile(host, libraries / 'libc.prx') + load_and_record(work, libraries / 'libc.prx', converted) + elif macos: + libraries = mach / 'libs' + libraries.mkdir() + shutil.copyfile(host, libraries / 'libkernel.prx') + (libraries / 'libc.prx').symlink_to('libkernel.prx') + load_and_record(mach, libraries / 'libkernel.prx', mach_converted) + else: 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') diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b72e13228..b99ca5a99 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -379,7 +379,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries -- [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. On macOS, `sceKernelLoadStartModule` does not pass start arguments to a module's initializer. +- [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. - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console - [sceKernelGetFsSandboxRandomWord](../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist