Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/relinker/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,10 @@ 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>)
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>)
Expand Down
11 changes: 11 additions & 0 deletions core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,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"));
Expand Down
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)

Copy link
Copy Markdown
Contributor

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_other 3, thunked), a hidden one (st_other 2, 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.


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 change: 1 addition & 0 deletions docs/dev/debt/relinker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 jmp (verified: two GLOBAL FUNC symbols at 0 come out at 0x4000 and 0x4005), so two names for one function have different addresses on Linux where the console gives them the same; a module whose gained segment lies above INT32_MAX - 5 throws Guest export at address 0 exceeds relative branch range; and an import of a data or untyped export at 0 is not reported at conversion time (an executable importing a GLOBAL OBJECT at 0 converts with exit 0 and fails only at load; pre-existing on main, narrowed by this PR), in the style of the neighbouring entry's "and nothing reports it".


- [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.
Expand Down
Loading