Merge pull request #10495 from wail-arch/fix/linux-guest-zero-address-exports

fix(relinker): bind guest function exports at address 0 on Linux
This commit is contained in:
Dean authored and GitHub committed 2026-11-02 23:22:55 +13:00
commit 2a9de0ea82
4 files changed
+137

No files matched your search

+6
View File
@@ -807,6 +807,12 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $<TARGET_FILE:relinker>)
add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_identity.py $<TARGET_FILE:relinker>)
add_test(NAME linux_guest_zero_address_exports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_guest_zero_address_exports.py
$<TARGET_FILE:relinker> $<TARGET_FILE:guest_module_args_host>)
set_property(TEST linux_guest_zero_address_exports APPEND PROPERTY LABELS relinker)
set_tests_properties(linux_guest_zero_address_exports PROPERTIES TIMEOUT 30)
add_asan_test_options(linux_guest_zero_address_exports verify_asan_link_order=0)
add_test(NAME guest_unused_import_dependencies COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_unused_import_dependencies.py
$<TARGET_FILE:relinker> $<TARGET_FILE:guest_module_args_host>)
@@ -64,6 +64,17 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteLinux(const Relinker::GuestIma
needsTlsResolver = true;
}
}
for (std::size_t index = 1; index < image.Symbols.size(); ++index) {
const auto& symbol = image.Symbols[index];
if (symbol.Value != 0 || symbol.Section == 0 || symbol.Section >= 0xff00 || (symbol.Info >> 4) == 0 ||
(symbol.Info & 15) != 2 || symbol.Visibility == 1 || symbol.Visibility == 2) continue;
const auto thunk = address();
if (thunk > static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max()) - 5)
throw Domain::RelinkerException("Guest export at address 0 exceeds relative branch range");
bytes.push_back(0xe9);
Io::AppendU32(bytes, static_cast<std::uint32_t>(-static_cast<std::int32_t>(thunk + 5)));
Io::WriteU64(symbols, index * 24 + 8, thunk);
}
std::vector<std::uint64_t> needed;
for (const auto& dependency : dependencies) needed.push_back(addString(dependency));
if (needsTlsResolver) needed.push_back(addString("ld-linux-x86-64.so.2"));
@@ -0,0 +1,119 @@
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_export_lookup import guest_symbols
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):
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 zero_address_module(symbols):
image = guest_symbols([(name, info, other, 1) for name, info, other in symbols])
position = 0x600
while True:
tag, value = struct.unpack_from("<qQ", image, position)
if tag == 0:
break
if tag == 6:
table = value - 0x1a00
position += 16
for index in range(1, len(symbols) + 1):
struct.pack_into("<Q", image, table + index * 24 + 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 jump_target(image, value):
offset = file_offset(image, value)
assert image[offset] == 0xe9, image[offset:offset + 5].hex()
displacement, = struct.unpack_from("<i", image, offset + 1)
return value + 5 + displacement
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)
assert jump_target(image, value) == 0, value
output = relink(relinker, work / "kinds", {"provider.prx": zero_address_module([
("global", 0x12, 0), ("weak", 0x22, 0), ("protected", 0x12, 3), ("alias", 0x12, 0),
("internal", 0x12, 1), ("hidden", 0x12, 2), ("object", 0x11, 0)])})
image = (output.parent / "app0" / "sce_module" / "provider.prx.guest.prx").read_bytes()
symbols = exports(image)
jumps = [symbols[name + "#guest"][1] for name in ("global", "weak", "protected", "alias")]
assert all(value != 0 and jump_target(image, value) == 0 for value in jumps), symbols
assert len(set(jumps)) == len(jumps), symbols
assert symbols["internal"] == (0x12, 0) and symbols["hidden"] == (0x12, 0), symbols
assert symbols["object#guest"] == (0x11, 0), symbols
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()
+1
View File
@@ -9,6 +9,7 @@
- The [syscall scanner](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) discovers routine bounds from unwind ranges, direct branch targets, and address-taken `lea` targets ([#7547](https://github.com/boykopovar/AnyPS5/issues/7547)) so undecodable bytes in an inter-routine gap are not reported as an illegal instruction. Each unwind range is decoded from its start through its end, and direct branch and `lea` targets reached from it are followed until the next return, unconditional jump, indirect jump, or trap. Inside discovered routines, all syscalls are rejected, including dynamic RAX loads. In inter-routine gaps outside all discovered routines, embedded data does not trigger decode failures, and raw syscall bytes without a constant number are skipped and counted; stubs with constant syscall numbers in gaps remain detected. With no unwind ranges, discovery is not performed and every syscall byte pattern is rejected.
- [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, and two names for it get two `jmp`s at different addresses. A module whose `jmp` would lie 2 GiB or more above address 0 fails to convert. A data or untyped export at address 0 stays unresolvable on Linux, and an import of one is not reported at conversion: the host loader stops on it as an undefined symbol.
- [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. Windows defers such a module too, unless a module outside these directories or in a standard SDK folder needs it, directly or through other modules; then it starts at startup without start arguments. On both, loading a module does not start the deferred modules it needs: each starts when the title loads it, and one the title never loads gets no `module_start` and no initializers. On 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. The [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) prints `Deferred module <path>` at startup for each such module that an import of the executable names, whether or not the title loads it later; an import without a module name is not reported. macOS still starts it at startup.