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