Repository navigation
fix(relinker): bind guest function exports at address 0 on Linux #10495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import os | ||
| from pathlib import Path | ||
| import platform | ||
| import shutil | ||
| import struct | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
|
|
||
| from test_guest_deferred_dependencies import page_aligned | ||
| from test_guest_intel_trampolines import main_fixture | ||
| from test_guest_symbol_names import dynamic_tags, file_offset, symbol_name | ||
| from test_windows_import_modules import consumer, provider | ||
|
|
||
|
|
||
| def at_address_zero(image, info=0x12): | ||
| struct.pack_into("<B", image, 0x898 + 4, info) | ||
| struct.pack_into("<Q", image, 0x898 + 8, 0) | ||
| image = page_aligned(image) | ||
| struct.pack_into("<Q", image, 24, 0) | ||
| struct.pack_into("<QQ", image, 64 + 16, 0, 0) | ||
| return image | ||
|
|
||
|
|
||
| def exports(image): | ||
| tags = dynamic_tags(image) | ||
| count, = struct.unpack_from("<I", image, file_offset(image, tags[4]) + 4) | ||
| table = file_offset(image, tags[6]) | ||
| result = {} | ||
| for index in range(1, count): | ||
| _, info, _, section, value, _ = struct.unpack_from("<IBBHQQ", image, table + index * 24) | ||
| if section: | ||
| result[symbol_name(image, tags, index)] = (info, value) | ||
| return result | ||
|
|
||
|
|
||
| def relink(relinker, case, modules): | ||
| (case / "sce_module").mkdir(parents=True) | ||
| for name, image in modules.items(): | ||
| (case / "sce_module" / name).write_bytes(image) | ||
| image = main_fixture() | ||
| code = bytes.fromhex("b83c000000bf2a0000000f05") | ||
| image[0x4000:0x4000 + len(code)] = code | ||
| source = case / "input.elf" | ||
| source.write_bytes(image) | ||
| output = case / "output.elf" | ||
| result = subprocess.run([str(relinker), "--skip-syscall-check", str(source), str(output)], | ||
| capture_output=True, text=True, timeout=30) | ||
| assert result.returncode == 0, (result.stdout, result.stderr) | ||
| return output | ||
|
|
||
|
|
||
| def main(): | ||
| relinker = Path(sys.argv[1]).resolve() | ||
| host = Path(sys.argv[2]).resolve() | ||
| runnable = sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64") | ||
| with tempfile.TemporaryDirectory(prefix="anyps5-linux-guest-zero-address-exports-") as directory: | ||
| work = Path(directory) | ||
|
|
||
| output = relink(relinker, work / "function", { | ||
| "provider.prx": at_address_zero(provider(22)), | ||
| "consumer.prx": page_aligned(consumer("provider.prx")), | ||
| }) | ||
| image = (output.parent / "app0" / "sce_module" / "provider.prx.guest.prx").read_bytes() | ||
| info, value = exports(image)["shared#guest"] | ||
| assert info == 0x12 and value != 0, (info, value) | ||
| offset = file_offset(image, value) | ||
| assert image[offset] == 0xe9, image[offset:offset + 5].hex() | ||
| displacement, = struct.unpack_from("<i", image, offset + 1) | ||
| assert value + 5 + displacement == 0, (value, displacement) | ||
|
|
||
| output = relink(relinker, work / "object", {"provider.prx": at_address_zero(provider(22), 0x11)}) | ||
| image = (output.parent / "app0" / "sce_module" / "provider.prx.guest.prx").read_bytes() | ||
| assert exports(image)["shared#guest"] == (0x11, 0), exports(image) | ||
|
|
||
| if runnable: | ||
| libraries = work / "function" / "libs" | ||
| libraries.mkdir() | ||
| for name in ("libc.prx", "libkernel.prx"): | ||
| shutil.copyfile(host, libraries / name) | ||
| output = work / "function" / "output.elf" | ||
| output.chmod(0o755) | ||
| result = subprocess.run([str(output)], capture_output=True, text=True, timeout=30, | ||
| env={**os.environ, "ANYPS5_GUEST_MODULE_ARGS": str(work / "events.txt")}) | ||
| assert result.returncode == 42, (result.returncode, result.stdout, result.stderr) | ||
| print("Linux guest zero-address export tests passed") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| ### Functional | ||
|
|
||
| - [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. | ||
| - [Guest exports at address 0 on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp) - the host loader skips a defined symbol whose value is 0 (`check_match` in glibc's `elf/dl-lookup.c`), and the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) links a shared object's code at address 0, so its first function is exported at 0, as in the SDK's `hello_so` sample. Such a function export is written as a `jmp` to it in the segment the module gains, so a pointer to it taken through the symbol differs from one the module computes itself. A data or untyped export at address 0 stays unresolvable on Linux. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: three consequences of the writer are worth a sentence here. Several function exports aliasing address 0 each get their own |
||
|
|
||
| - [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. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, optional: the filter has more arms than the two cases here.
at_address_zero(provider(22), 0x22)(weak, thunked), a protected variant (st_other3, thunked), a hidden one (st_other2, must stay 0) and a second function at 0 (two distinct thunks) would pin them; I checked all four by hand on this build and they behave as the body describes.