Merge pull request #431 from adriadam10/feat/relinker-e2e-fixture

test(relinker): add C++ end-to-end fixture and --to-intel Linux stub test
This commit is contained in:
Adrià 2026-10-07 16:32:24 +02:00 committed by GitHub
commit 1e5bb880bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 284 additions and 0 deletions

View File

@ -274,4 +274,11 @@ 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")
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 $<TARGET_FILE:relinker>)
endif()
endif()

View File

@ -0,0 +1,129 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <span>
#include <stdexcept>
#include <utility>
#include <vector>
#include <sys/mman.h>
#include <unistd.h>
namespace RelinkerTests {
using Bytes = std::vector<std::uint8_t>;
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<typename TValue>
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<typename TValue>
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<std::uint32_t>(image, header, type);
Write<std::uint32_t>(image, header + 4, flags);
Write<std::uint64_t>(image, header + 8, offset);
Write<std::uint64_t>(image, header + 16, vaddr);
Write<std::uint64_t>(image, header + 24, vaddr);
Write<std::uint64_t>(image, header + 32, size);
Write<std::uint64_t>(image, header + 40, size);
Write<std::uint64_t>(image, header + 48, align);
}
inline Bytes MakeExecutable(const std::span<const std::uint8_t> 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<std::uint16_t>(image, 16, 3);
Write<std::uint16_t>(image, 18, 62);
Write<std::uint32_t>(image, 20, 1);
Write<std::uint64_t>(image, 24, 0);
Write<std::uint64_t>(image, 32, 64);
Write<std::uint16_t>(image, 52, 64);
Write<std::uint16_t>(image, 54, 56);
Write<std::uint16_t>(image, 56, 2 + kSpareProgramHeaders);
Write<std::uint16_t>(image, 58, 64);
const std::pair<std::int64_t, std::uint64_t> 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<std::int64_t>(image, kCodeOffset + kDynamicVaddr + index * 16, tags[index].first);
Write<std::uint64_t>(image, kCodeOffset + kDynamicVaddr + index * 16 + 8, tags[index].second);
}
return image;
}
class MappedImage {
public:
explicit MappedImage(const Bytes& elf) {
const auto phOffset = Read<std::uint64_t>(elf, 32);
const auto phSize = Read<std::uint16_t>(elf, 54);
const auto phCount = Read<std::uint16_t>(elf, 56);
for (std::uint16_t index = 0; index < phCount; ++index) {
const auto header = phOffset + index * phSize;
if (Read<std::uint32_t>(elf, header) != 1) continue;
size = std::max<std::size_t>(size, Read<std::uint64_t>(elf, header + 16) + Read<std::uint64_t>(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<std::uint32_t>(elf, header) != 1) continue;
const auto offset = Read<std::uint64_t>(elf, header + 8);
const auto fileSize = Read<std::uint64_t>(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<std::uint8_t*>(base) + Read<std::uint64_t>(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<std::uint64_t>(sysconf(_SC_PAGESIZE));
for (std::uint16_t index = 0; index < phCount; ++index) {
const auto header = phOffset + index * phSize;
if (Read<std::uint32_t>(elf, header) != 1) continue;
const auto flags = Read<std::uint32_t>(elf, header + 4);
const auto vaddr = Read<std::uint64_t>(elf, header + 16);
const auto start = vaddr / page * page;
const auto end = (vaddr + Read<std::uint64_t>(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<std::uint8_t*>(base) + start, std::min<std::uint64_t>(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<typename TFunction>
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<TFunction*>(static_cast<std::uint8_t*>(base) + vaddr);
}
private:
void* base = nullptr;
std::size_t size = 0;
};
} // namespace RelinkerTests

View File

@ -0,0 +1,84 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <vector>
#include <fcntl.h>
#include <spawn.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
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<std::string>& arguments, const std::filesystem::path& logPath) {
std::vector<std::string> owned{relinker.string()};
owned.insert(owned.end(), arguments.begin(), arguments.end());
std::vector<char*> 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<char>(log), std::istreambuf_iterator<char>());
return run;
}
inline std::vector<std::uint8_t> 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<char>(file), std::istreambuf_iterator<char>()};
}
inline void WriteFile(const std::filesystem::path& path, const std::vector<std::uint8_t>& bytes) {
std::ofstream file(path, std::ios::binary);
file.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
if (!file) throw std::runtime_error("Cannot write " + path.string());
}
} // namespace RelinkerTests

View File

@ -0,0 +1,64 @@
#include "ElfFixture.hpp"
#include "RelinkerProcess.hpp"
#include <cstdint>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
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: to_intel_linux_stubs_tests <relinker>");
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<const std::uint8_t>(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<std::uint64_t(std::uint64_t, std::uint64_t)>(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;
}
}