From 98c47df9e5fc8eb2cf6c2abfa71c024b46821d19 Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Fri, 9 Oct 2026 11:30:08 +0200 Subject: [PATCH] fix(relinker): discover required guest module identities --- core/relinker/CMakeLists.txt | 2 + .../domain/include/domain/ImportModule.hpp | 4 +- .../include/relinker/guest/GuestImage.hpp | 2 + .../relinker/src/guest/GuestImageReader.cpp | 85 +++++++ .../relinker/src/guest/GuestModuleBuilder.cpp | 64 ++++- .../tests/test_guest_module_identity.py | 226 ++++++++++++++++++ 6 files changed, 376 insertions(+), 7 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_module_identity.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index f1d7705e2..f9ebb2acd 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -239,6 +239,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_host_libc.py $) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) + add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_identity.py $) add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $) add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index 81b0c3b2b..a2bd98e33 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -10,6 +10,8 @@ namespace Domain { inline std::string ImportModuleName(std::string name) { if (name.ends_with(".prx")) name.resize(name.size() - 4); + else if (name.ends_with(".sprx")) name.resize(name.size() - 5); + else if (name.ends_with(".suprx")) name.resize(name.size() - 6); if (name.ends_with("-module")) name.resize(name.size() - 7); std::replace(name.begin(), name.end(), '.', '_'); return name; @@ -36,7 +38,7 @@ inline std::string ImportModule(const std::string& symbol, const std::mapsecond; if (name.empty() || name.find_first_of("/\\:$\r\n") != std::string::npos) throw RelinkerException("Invalid import module name: " + name); - if (!name.ends_with(".prx")) name += ".prx"; + if (!name.ends_with(".prx") && !name.ends_with(".sprx") && !name.ends_with(".suprx")) name += ".prx"; if (std::find(dependencies.begin(), dependencies.end(), name) != dependencies.end()) return name; std::string matched; for (const auto& dependency : dependencies) { diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index fe34092b5..1cf350007 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -28,6 +28,7 @@ struct GuestImage { std::filesystem::path SourcePath; std::string OutputName; std::string Soname; + std::vector ModuleNames; std::vector Bytes; std::vector Trampolines; std::vector Headers; @@ -44,6 +45,7 @@ struct GuestImage { class GuestImageReader { public: + std::vector ReadModuleNames(const std::vector& bytes) const; GuestImage Read(const std::filesystem::path& path, std::vector bytes) const; }; diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 56551a48d..3e9d18c32 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -11,6 +11,82 @@ namespace Relinker { +std::vector GuestImageReader::ReadModuleNames(const std::vector& bytes) const { + if (bytes.size() < 64 || Io::ReadU32(bytes, 0) != 0x464c457f || bytes[4] != 2 || bytes[5] != 1 || bytes[6] != 1 || Io::ReadU16(bytes, 18) != 62) + throw Domain::RelinkerException("Expected little-endian ELF64 x86-64 module metadata"); + const auto type = Io::ReadU16(bytes, 16); + if (type != 3 && type != 0xfe18) return {}; + if (Io::ReadU16(bytes, 52) != 64 || Io::ReadU16(bytes, 54) != 56 || Io::ReadU16(bytes, 56) == 0) + throw Domain::RelinkerException("Invalid module metadata program headers"); + const auto range = [&](std::uint64_t offset, std::uint64_t size) { + if (offset > bytes.size() || size > bytes.size() - offset) throw Domain::RelinkerException("Module metadata exceeds file"); + }; + range(Io::ReadU64(bytes, 32), static_cast(Io::ReadU16(bytes, 56)) * 56); + ElfReader reader(bytes); + const auto headers = reader.ReadProgramHeaders(); + const Domain::ProgramHeader* dynamic = nullptr; + const Domain::ProgramHeader* dynlib = nullptr; + for (const auto& header : headers) { + if (header.Type == 2) { + if (dynamic) throw Domain::RelinkerException("Duplicate module metadata dynamic segment"); + dynamic = &header; + } + if (header.Type == 0x61000000) { + if (dynlib) throw Domain::RelinkerException("Duplicate module metadata dynamic data"); + dynlib = &header; + } + } + if (!dynamic) throw Domain::RelinkerException("Missing module metadata dynamic segment"); + std::map strings; + std::vector modules; + for (const auto& tag : reader.ReadDynamicTags(*dynamic)) { + if (tag.Tag == 0x6100000d || tag.Tag == 0x61000043) modules.push_back(tag.Value); + else if (tag.Tag == 5 || tag.Tag == 10 || tag.Tag == 0x61000035 || tag.Tag == 0x61000037) { + if (!strings.emplace(tag.Tag, tag.Value).second) throw Domain::RelinkerException("Duplicate module metadata string tag"); + } + } + if (modules.empty()) return {}; + const auto get = [&](std::uint64_t standard, std::uint64_t sce) { + if (strings.contains(standard) == strings.contains(sce)) throw Domain::RelinkerException("Missing or ambiguous module metadata string tag"); + return strings.at(strings.contains(standard) ? standard : sce); + }; + const auto size = get(10, 0x61000037); + const auto address = get(5, 0x61000035); + std::uint64_t offset = 0; + if (strings.contains(5)) { + bool found = false; + for (const auto& header : headers) { + if (header.Type != 1 || address < header.MappedAddress || address - header.MappedAddress > header.FileSize || size > header.FileSize - (address - header.MappedAddress)) continue; + range(header.Offset, header.FileSize); + if (found) throw Domain::RelinkerException("Ambiguous module metadata string mapping"); + offset = header.Offset + address - header.MappedAddress; + found = true; + } + if (!found) throw Domain::RelinkerException("Unmapped module metadata strings"); + } else { + if (!dynlib || address > dynlib->FileSize || size > dynlib->FileSize - address) throw Domain::RelinkerException("Module metadata strings exceed dynamic data"); + range(dynlib->Offset, dynlib->FileSize); + offset = dynlib->Offset + address; + } + range(offset, size); + if (size == 0 || bytes[offset] != 0) throw Domain::RelinkerException("Invalid module metadata strings"); + std::vector names; + std::set ids; + for (const auto value : modules) { + const auto nameOffset = value & 0xffffffffu; + if (nameOffset >= size) throw Domain::RelinkerException("Module metadata string offset out of bounds"); + const auto start = bytes.begin() + static_cast(offset + nameOffset); + const auto end = bytes.begin() + static_cast(offset + size); + const auto zero = std::find(start, end, 0); + if (zero == end) throw Domain::RelinkerException("Unterminated module metadata string"); + const std::string name(start, zero); + if (name.empty() || name.find_first_of("/\\:$\r\n") != std::string::npos) throw Domain::RelinkerException("Invalid module metadata name"); + if (!ids.insert(value >> 48).second) throw Domain::RelinkerException("Duplicate module metadata export ID"); + names.push_back(name); + } + return names; +} + GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector bytes) const { const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(path.string() + ": " + message); }; const auto range = [&](std::uint64_t offset, std::uint64_t size) { @@ -63,6 +139,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector std::map tags; std::vector needed; std::vector moduleImports; + std::vector moduleExports; bool terminated = false; for (std::uint64_t offset = dynamic->Offset; offset < dynamic->Offset + dynamic->FileSize; offset += 16) { const auto tag = Io::ReadU64(bytes, offset); @@ -70,6 +147,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (tag == 0) { terminated = true; break; } if (tag == 1) needed.push_back(value); else if (tag == 0x61000045) moduleImports.push_back(value); + else if (tag == 0x6100000d || tag == 0x61000043) moduleExports.push_back(value); else if (tag < 0x60000000 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { if (!tags.emplace(tag, value).second) fail("Duplicate dynamic tag " + std::to_string(tag)); } @@ -159,6 +237,13 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector image.Soname = string(tags.at(14)); if (image.Soname.empty() || image.Soname.find_first_of("\\:$\r\n") != std::string::npos) fail("Invalid guest SONAME: " + image.Soname); } + std::set moduleIds; + for (const auto value : moduleExports) { + const auto name = string(value & 0xffffffffu); + if (name.empty() || name.find_first_of("/\\:$\r\n") != std::string::npos) fail("Invalid guest module name: " + name); + if (!moduleIds.insert(value >> 48).second) fail("Duplicate export module ID"); + image.ModuleNames.push_back(name); + } std::set dependencies; for (const auto offset : needed) { const auto name = string(offset); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index b2cd06d9a..2b641a5fd 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -85,17 +85,60 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } for (const auto& [name, path] : found) paths.push_back(path); } + Io::FileReader reader; + std::map discovered; + const auto matchesIdentity = [](const std::string& name, const std::vector& identities) { + return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { + return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx"; + }); + }; + const auto foldFilename = [](std::string name) { + for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + return name; + }; + for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(path.string()))); + std::set unresolved; + for (const auto& name : missingNeeded) { + if (std::none_of(discovered.begin(), discovered.end(), [&](const auto& entry) { + const auto& [path, image] = entry; + return path.filename().string() == name || image.Soname == name || + (windows && foldFilename(path.filename().string()) == foldFilename(name)) || matchesIdentity(name, image.ModuleNames); + })) unresolved.insert(name); + } + if (!unresolved.empty()) { + std::map identities; + for (auto it = std::filesystem::recursive_directory_iterator(root); it != std::filesystem::recursive_directory_iterator(); ++it) { + if (it->is_directory() && std::find(directories.begin(), directories.end(), it->path()) != directories.end()) { + it.disable_recursion_pending(); + continue; + } + const auto& path = it->path(); + const auto extension = path.extension().string(); + if (!it->is_regular_file() || (extension != ".prx" && extension != ".sprx" && extension != ".suprx") || + path.filename().string().ends_with(GuestModuleSuffix) || excludedModules.contains(path.filename().string()) || discovered.contains(path) || !isElf(path)) continue; + const auto names = GuestImageReader().ReadModuleNames(reader.Read(path.string())); + for (const auto& name : unresolved) { + if (!matchesIdentity(name, names)) continue; + if (!identities.emplace(name, path).second) throw Domain::RelinkerException("Ambiguous needed module identity: " + name); + } + } + for (const auto& [name, path] : identities) { + if (discovered.contains(path)) continue; + discovered.emplace(path, GuestImageReader().Read(path, reader.Read(path.string()))); + paths.push_back(path); + } + } if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); std::sort(paths.begin(), paths.end()); + paths.erase(std::unique(paths.begin(), paths.end()), paths.end()); if (paths.empty()) return {}; if (lazyBinding) throw Domain::RelinkerException("Guest modules require eager binding; --lazy-binding is incompatible"); std::vector images; std::map> exports; std::map> sharedExports; std::set outputNames; - Io::FileReader reader; for (const auto& path : paths) { - auto image = GuestImageReader().Read(path, reader.Read(path.string())); + auto image = std::move(discovered.at(path)); if (image.OutputName.find_first_of("$\r\n") != std::string::npos) throw Domain::RelinkerException("Unsupported guest filename: " + image.OutputName); std::string folded = image.OutputName; if (windows) { @@ -128,10 +171,6 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } std::map guestNames; std::map windowsGuestFiles; - const auto foldFilename = [](std::string name) { - for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); - return name; - }; for (std::size_t index = 0; index < images.size(); ++index) { for (const auto& name : {images[index].SourcePath.filename().string(), images[index].Soname}) { if (name.empty()) continue; @@ -146,6 +185,19 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto file = windowsGuestFiles.find(foldFilename(name)); return file == windowsGuestFiles.end() ? guestNames.end() : guestNames.emplace(name, file->second).first; }; + const auto resolveIdentity = [&](const std::string& name) { + if (findGuest(name) != guestNames.end()) return; + std::size_t match = images.size(); + for (std::size_t index = 0; index < images.size(); ++index) { + const auto& identities = images[index].ModuleNames; + if (!matchesIdentity(name, identities)) continue; + if (match != images.size()) throw Domain::RelinkerException("Ambiguous guest module identity: " + name); + match = index; + } + if (match != images.size()) guestNames.emplace(name, match); + }; + for (const auto& name : missingNeeded) resolveIdentity(name); + for (const auto& image : images) for (const auto& name : image.Dependencies) resolveIdentity(name); const auto rejectSharedImport = [&](const std::string& name, const std::string& importer) { const auto shared = sharedExports.find(name); if (shared == sharedExports.end()) return; diff --git a/core/relinker/relinker/tests/test_guest_module_identity.py b/core/relinker/relinker/tests/test_guest_module_identity.py new file mode 100644 index 000000000..8e542e72b --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_module_identity.py @@ -0,0 +1,226 @@ +from pathlib import Path +import os +import struct +import subprocess +import sys +import tempfile + +from test_windows_import_modules import consumer, dynamic, executable, provider +from test_guest_module_directories import needed_libraries + + +def declared_provider(value, identities, soname=None, tag=0x61000043, symbol='shared'): + image = provider(value) + strings = b'\0' + symbol.encode() + b'#A#B\0' + tags = [(5, 0x2200), (10, 0), (6, 0x2280), (11, 24), + (4, 0x2240), (7, 0x2300), (8, 0), (9, 24)] + if soname: + tags.append((14, len(strings))) + strings += soname.encode() + b'\0' + for index, identity in enumerate(identities, 1): + tags.append((tag, (index << 48) | (0x101 << 32) | len(strings))) + strings += identity.encode() + b'\0' + assert len(strings) <= 0x40 + tags[1] = (10, len(strings)) + image[0x800:0x800 + len(strings)] = strings + dynamic(image, 0x600, 176, tags) + return image + + +def sce_strings(image): + struct.pack_into('