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
This commit is contained in:
fablevoid 2026-10-11 11:22:38 +00:00
parent 0f863c1022
commit 1b865fc3ea
3 changed files with 60 additions and 16 deletions

View File

@ -424,6 +424,15 @@ std::vector<std::uint8_t> WriteMacOsImage(MacOsImageInput& input) {
metaEnd += 8;
input.Binds.push_back({symbol, metaRva + static_cast<std::uint32_t>(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<std::uint32_t>(moduleArgsSlot)});
input.Binds.push_back({"___aps5_set_module_init_result_nid_no_patch", metaRva + static_cast<std::uint32_t>(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<std::uint8_t> WriteMacOsImage(MacOsImageInput& input) {
code.Rip({0x48, 0x89, 0x05}, import.Rva);
}
if (input.InitRva != 0) {
callZeroArgs();
code.Rip({0xff, 0x15}, metaRva + static_cast<std::uint32_t>(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<std::uint32_t>(moduleResultSlot));
}
for (const auto slot : input.InitArrayRvas) callSlot(slot);
if (hasFini) {

View File

@ -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')

View File

@ -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