From 14dc9b31ab633bfe62a6392c09bac91eb7232aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 3 Oct 2026 07:57:08 +0200 Subject: [PATCH 1/2] test(relinker): add C++ end-to-end fixture and --to-intel Linux stub test Add header-only support for end-to-end relinker tests: a minimal PS5 executable builder, a PT_LOAD mapper that applies segment permissions, and a posix_spawn runner for the relinker binary. Every core/relinker/tests/e2e/*.cpp becomes its own test, so new tests do not touch CMake. The first test relinks an executable with EXTRQ and INSERTQ sites using --to-intel, maps the output and calls the patched code, which runs both stubs from their Linux segment and returns through their branches. Co-Authored-By: Claude --- core/relinker/CMakeLists.txt | 10 ++ core/relinker/tests/e2e/ToIntelLinuxStubs.cpp | 64 +++++++++ core/relinker/tests/support/ElfFixture.hpp | 129 ++++++++++++++++++ .../tests/support/RelinkerProcess.hpp | 84 ++++++++++++ 4 files changed, 287 insertions(+) create mode 100644 core/relinker/tests/e2e/ToIntelLinuxStubs.cpp create mode 100644 core/relinker/tests/support/ElfFixture.hpp create mode 100644 core/relinker/tests/support/RelinkerProcess.hpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 0df884a49..1a5b1fd38 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -274,4 +274,14 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include ) add_test(NAME strict_nid_filter COMMAND strict_nid_filter_tests) + + if(UNIX AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + file(GLOB relinker_e2e_sources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/e2e/*.cpp) + foreach(source ${relinker_e2e_sources}) + get_filename_component(name ${source} NAME_WE) + add_test_executable(relinker_e2e_${name} ${source}) + target_include_directories(relinker_e2e_${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests/support) + add_test(NAME relinker_e2e_${name} COMMAND relinker_e2e_${name} $) + endforeach() + endif() endif() diff --git a/core/relinker/tests/e2e/ToIntelLinuxStubs.cpp b/core/relinker/tests/e2e/ToIntelLinuxStubs.cpp new file mode 100644 index 000000000..a9fceb1e4 --- /dev/null +++ b/core/relinker/tests/e2e/ToIntelLinuxStubs.cpp @@ -0,0 +1,64 @@ +#include "ElfFixture.hpp" +#include "RelinkerProcess.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace RelinkerTests; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +const Bytes kCode = { + 0x66, 0x48, 0x0F, 0x6E, 0xCF, + 0x66, 0x48, 0x0F, 0x6E, 0xD6, + 0x66, 0x0F, 0x78, 0xC1, 0x08, 0x08, + 0xF2, 0x0F, 0x78, 0xD1, 0x08, 0x10, + 0x66, 0x48, 0x0F, 0x7E, 0xD0, + 0xC3, +}; +constexpr std::uint64_t kSites[] = {10, 16}; +constexpr std::size_t kSiteLength = 6; + +std::uint64_t expected(const std::uint64_t value, const std::uint64_t destination) { + return (destination & ~0xFF0000ull) | (((value >> 8) & 0xFF) << 16); +} + +} + +int main(const int argc, char** argv) { + try { + require(argc == 2, "usage: ToIntelLinuxStubs "); + const TempDirectory directory; + const auto input = directory.Path() / "input.elf"; + const auto output = directory.Path() / "output.elf"; + WriteFile(input, MakeExecutable(kCode)); + const auto run = RunRelinker(argv[1], {"--skip-sce-module", "--to-intel", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode == 0, "Relinker failed:\n" + run.Output); + require(run.Output.find("Intel conversion: 0 in place, 2 stubs") != std::string::npos, "Relinker did not report two stubs:\n" + run.Output); + + const MappedImage image(ReadFile(output)); + for (const auto site : kSites) { + const auto* bytes = image.At(site); + require(bytes[0] == 0xE9, "SSE4a site was not replaced by a jump"); + for (std::size_t index = 5; index < kSiteLength; ++index) require(bytes[index] == 0x90, "SSE4a site tail is not padded with NOPs"); + std::int32_t displacement; + std::memcpy(&displacement, bytes + 1, sizeof(displacement)); + require(site + 5 + displacement >= kCodeSize, "SSE4a stub is inside the original code segment"); + } + + const auto function = image.At(0); + for (const auto [value, destination] : {std::pair{0x1122334455667788ull, 0x0123456789ABCDEFull}, {0ull, ~0ull}, {~0ull, 0ull}, {0x9E3779B97F4A7C15ull, 0x0F1E2D3C4B5A6978ull}}) + require(function(value, destination) == expected(value, destination), "Relinked SSE4a stubs computed the wrong value"); + std::cout << "--to-intel Linux stub tests passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/tests/support/ElfFixture.hpp b/core/relinker/tests/support/ElfFixture.hpp new file mode 100644 index 000000000..9962b9d85 --- /dev/null +++ b/core/relinker/tests/support/ElfFixture.hpp @@ -0,0 +1,129 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RelinkerTests { + +using Bytes = std::vector; + +inline constexpr std::uint64_t kCodeOffset = 0x4000; +inline constexpr std::uint64_t kCodeSize = 0x1000; +inline constexpr std::uint64_t kDynamicVaddr = 0x600; +inline constexpr std::uint16_t kSpareProgramHeaders = 3; + +template +void Write(Bytes& bytes, const std::size_t offset, const TValue value) { + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("ELF fixture write is out of bounds"); + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +template +TValue Read(const Bytes& bytes, const std::size_t offset) { + TValue value; + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("ELF read is out of bounds"); + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +inline void WriteProgramHeader(Bytes& image, const std::uint16_t index, const std::uint32_t type, const std::uint32_t flags, const std::uint64_t offset, const std::uint64_t vaddr, const std::uint64_t size, const std::uint64_t align) { + const std::size_t header = 64 + index * 56; + Write(image, header, type); + Write(image, header + 4, flags); + Write(image, header + 8, offset); + Write(image, header + 16, vaddr); + Write(image, header + 24, vaddr); + Write(image, header + 32, size); + Write(image, header + 40, size); + Write(image, header + 48, align); +} + +inline Bytes MakeExecutable(const std::span code) { + if (code.size() > kDynamicVaddr) throw std::runtime_error("ELF fixture code overlaps the dynamic section"); + Bytes image(0x8000); + const std::uint8_t ident[16] = {0x7F, 'E', 'L', 'F', 2, 1, 1}; + std::memcpy(image.data(), ident, sizeof(ident)); + Write(image, 16, 3); + Write(image, 18, 62); + Write(image, 20, 1); + Write(image, 24, 0); + Write(image, 32, 64); + Write(image, 52, 64); + Write(image, 54, 56); + Write(image, 56, 2 + kSpareProgramHeaders); + Write(image, 58, 64); + const std::pair tags[] = {{5, 0x600}, {10, 1}, {6, 0x620}, {11, 24}, {7, 0x700}, {8, 0}, {9, 24}, {0, 0}}; + WriteProgramHeader(image, 0, 1, 5, kCodeOffset, 0, kCodeSize, 0x4000); + WriteProgramHeader(image, 1, 2, 6, kCodeOffset + kDynamicVaddr, kDynamicVaddr, sizeof(tags) / sizeof(tags[0]) * 16, 8); + for (std::uint16_t index = 2; index < 2 + kSpareProgramHeaders; ++index) WriteProgramHeader(image, index, 0x6FFFFF01, 0, 0, 0, 0, 1); + std::fill(image.begin() + kCodeOffset, image.begin() + kCodeOffset + kDynamicVaddr, 0xCC); + std::copy(code.begin(), code.end(), image.begin() + kCodeOffset); + for (std::size_t index = 0; index < sizeof(tags) / sizeof(tags[0]); ++index) { + Write(image, kCodeOffset + kDynamicVaddr + index * 16, tags[index].first); + Write(image, kCodeOffset + kDynamicVaddr + index * 16 + 8, tags[index].second); + } + return image; +} + +class MappedImage { +public: + explicit MappedImage(const Bytes& elf) { + const auto phOffset = Read(elf, 32); + const auto phSize = Read(elf, 54); + const auto phCount = Read(elf, 56); + for (std::uint16_t index = 0; index < phCount; ++index) { + const auto header = phOffset + index * phSize; + if (Read(elf, header) != 1) continue; + size = std::max(size, Read(elf, header + 16) + Read(elf, header + 40)); + } + if (size == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + base = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (base == MAP_FAILED) throw std::runtime_error("Cannot map executable memory for the ELF image"); + for (std::uint16_t index = 0; index < phCount; ++index) { + const auto header = phOffset + index * phSize; + if (Read(elf, header) != 1) continue; + const auto offset = Read(elf, header + 8); + const auto fileSize = Read(elf, header + 32); + if (offset > elf.size() || fileSize > elf.size() - offset) throw std::runtime_error("PT_LOAD exceeds the ELF file"); + std::memcpy(static_cast(base) + Read(elf, header + 16), elf.data() + offset, fileSize); + } + if (mprotect(base, size, PROT_NONE) != 0) throw std::runtime_error("Cannot protect the ELF image"); + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + for (std::uint16_t index = 0; index < phCount; ++index) { + const auto header = phOffset + index * phSize; + if (Read(elf, header) != 1) continue; + const auto flags = Read(elf, header + 4); + const auto vaddr = Read(elf, header + 16); + const auto start = vaddr / page * page; + const auto end = (vaddr + Read(elf, header + 40) + page - 1) / page * page; + const int protection = ((flags & 4) != 0 ? PROT_READ : 0) | ((flags & 2) != 0 ? PROT_WRITE : 0) | ((flags & 1) != 0 ? PROT_EXEC : 0); + if (mprotect(static_cast(base) + start, std::min(end, size) - start, protection) != 0) throw std::runtime_error("Cannot apply PT_LOAD permissions"); + } + } + + MappedImage(const MappedImage&) = delete; + MappedImage& operator=(const MappedImage&) = delete; + + ~MappedImage() { + munmap(base, size); + } + + template + TFunction* At(const std::uint64_t vaddr) const { + if (vaddr >= size) throw std::runtime_error("Address is outside the mapped ELF image"); + return reinterpret_cast(static_cast(base) + vaddr); + } + +private: + void* base = nullptr; + std::size_t size = 0; +}; + +} // namespace RelinkerTests diff --git a/core/relinker/tests/support/RelinkerProcess.hpp b/core/relinker/tests/support/RelinkerProcess.hpp new file mode 100644 index 000000000..b642783fe --- /dev/null +++ b/core/relinker/tests/support/RelinkerProcess.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char** environ; + +namespace RelinkerTests { + +class TempDirectory { +public: + TempDirectory() { + auto pattern = (std::filesystem::temp_directory_path() / "anyps5-relinker-XXXXXX").string(); + if (mkdtemp(pattern.data()) == nullptr) throw std::runtime_error("Cannot create a temporary directory"); + path = pattern; + } + + TempDirectory(const TempDirectory&) = delete; + TempDirectory& operator=(const TempDirectory&) = delete; + + ~TempDirectory() { + std::error_code error; + std::filesystem::remove_all(path, error); + } + + const std::filesystem::path& Path() const { + return path; + } + +private: + std::filesystem::path path; +}; + +struct RelinkerRun { + int ExitCode = -1; + std::string Output; +}; + +inline RelinkerRun RunRelinker(const std::filesystem::path& relinker, const std::vector& arguments, const std::filesystem::path& logPath) { + std::vector owned{relinker.string()}; + owned.insert(owned.end(), arguments.begin(), arguments.end()); + std::vector argv; + for (auto& argument : owned) argv.push_back(argument.data()); + argv.push_back(nullptr); + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, logPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, STDERR_FILENO); + pid_t pid = 0; + const auto spawned = posix_spawn(&pid, argv[0], &actions, nullptr, argv.data(), environ); + posix_spawn_file_actions_destroy(&actions); + if (spawned != 0) throw std::runtime_error("Cannot start the relinker"); + int status = 0; + if (waitpid(pid, &status, 0) != pid) throw std::runtime_error("Cannot wait for the relinker"); + RelinkerRun run; + if (WIFEXITED(status)) run.ExitCode = WEXITSTATUS(status); + std::ifstream log(logPath, std::ios::binary); + run.Output.assign(std::istreambuf_iterator(log), std::istreambuf_iterator()); + return run; +} + +inline std::vector ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) throw std::runtime_error("Cannot read " + path.string()); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +inline void WriteFile(const std::filesystem::path& path, const std::vector& bytes) { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (!file) throw std::runtime_error("Cannot write " + path.string()); +} + +} // namespace RelinkerTests From 9f3ac9a5bd3fb764bd2740a53bc567046a79b54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 7 Oct 2026 16:03:41 +0200 Subject: [PATCH 2/2] test(relinker): register the --to-intel stub test next to the relinker tests Co-Authored-By: Claude --- core/relinker/CMakeLists.txt | 11 ++++------- .../{tests/support => relinker/tests}/ElfFixture.hpp | 0 .../support => relinker/tests}/RelinkerProcess.hpp | 0 .../tests/ToIntelLinuxStubsTests.cpp} | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) rename core/relinker/{tests/support => relinker/tests}/ElfFixture.hpp (100%) rename core/relinker/{tests/support => relinker/tests}/RelinkerProcess.hpp (100%) rename core/relinker/{tests/e2e/ToIntelLinuxStubs.cpp => relinker/tests/ToIntelLinuxStubsTests.cpp} (97%) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 1a5b1fd38..fddfe88ff 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -276,12 +276,9 @@ if(BUILD_TESTING) add_test(NAME strict_nid_filter COMMAND strict_nid_filter_tests) if(UNIX AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") - file(GLOB relinker_e2e_sources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/e2e/*.cpp) - foreach(source ${relinker_e2e_sources}) - get_filename_component(name ${source} NAME_WE) - add_test_executable(relinker_e2e_${name} ${source}) - target_include_directories(relinker_e2e_${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests/support) - add_test(NAME relinker_e2e_${name} COMMAND relinker_e2e_${name} $) - endforeach() + add_test_executable(to_intel_linux_stubs_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ToIntelLinuxStubsTests.cpp + ) + add_test(NAME to_intel_linux_stubs COMMAND to_intel_linux_stubs_tests $) endif() endif() diff --git a/core/relinker/tests/support/ElfFixture.hpp b/core/relinker/relinker/tests/ElfFixture.hpp similarity index 100% rename from core/relinker/tests/support/ElfFixture.hpp rename to core/relinker/relinker/tests/ElfFixture.hpp diff --git a/core/relinker/tests/support/RelinkerProcess.hpp b/core/relinker/relinker/tests/RelinkerProcess.hpp similarity index 100% rename from core/relinker/tests/support/RelinkerProcess.hpp rename to core/relinker/relinker/tests/RelinkerProcess.hpp diff --git a/core/relinker/tests/e2e/ToIntelLinuxStubs.cpp b/core/relinker/relinker/tests/ToIntelLinuxStubsTests.cpp similarity index 97% rename from core/relinker/tests/e2e/ToIntelLinuxStubs.cpp rename to core/relinker/relinker/tests/ToIntelLinuxStubsTests.cpp index a9fceb1e4..04a57131f 100644 --- a/core/relinker/tests/e2e/ToIntelLinuxStubs.cpp +++ b/core/relinker/relinker/tests/ToIntelLinuxStubsTests.cpp @@ -34,7 +34,7 @@ std::uint64_t expected(const std::uint64_t value, const std::uint64_t destinatio int main(const int argc, char** argv) { try { - require(argc == 2, "usage: ToIntelLinuxStubs "); + require(argc == 2, "usage: to_intel_linux_stubs_tests "); const TempDirectory directory; const auto input = directory.Path() / "input.elf"; const auto output = directory.Path() / "output.elf";