From ea61baec62aff0d1ebd4d75ac2edc30215396611 Mon Sep 17 00:00:00 2001 From: Karim Ibrahim <40270714+karimibrahim07@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:51:13 -0400 Subject: [PATCH] fix(relinker): resolve symbol-name imports through NIDs on Windows Homebrew built with the ps5-payload-dev SDK imports by symbol name and names its system libraries with .sprx, while the libraries in libs/ are .prx files that export NIDs. Quake from the websrv launcher stopped with 457 unresolved imports such as strlen and malloc. An import without a module that no searched library exports under its name is now looked up again under the NID of that name. A dependency named with .sprx, .suprx or .debug_prx loads the .prx library of the same stem, and libkernel_web also loads libkernel.prx, as libSceLibcInternal already loads libc.prx. --- core/relinker/CMakeLists.txt | 5 ++ .../domain/include/domain/ImportModule.hpp | 6 ++ .../src/windows/WindowsEntryStubBuilder.cpp | 37 +++++++-- .../src/windows/WindowsImportBuilder.cpp | 5 +- .../tests/test_windows_named_imports.py | 78 +++++++++++++++++++ docs/dev/debt/relinker.md | 1 + 6 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 core/relinker/relinker/tests/test_windows_named_imports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 11c7ea959a..9f148e1897 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -71,7 +71,10 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/output/CallRegistryWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/pipeline/Conversion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/pipeline/RelinkerPipeline.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/NidCompute.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/Sha1.cpp ) +target_include_directories(relinker_core PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/include) target_include_directories(relinker_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include @@ -512,6 +515,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_output.py $) add_test(NAME guest_host_libc COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_host_libc.py $) + add_test(NAME windows_named_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_named_imports.py $) add_library(guest_module_args_host SHARED elfpatcher/tests/GuestModuleArgsHost.cpp) target_include_directories(guest_module_args_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs) set_target_properties(guest_module_args_host PROPERTIES diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index 37bab4c73e..18b018f770 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -16,6 +16,12 @@ inline std::string_view ModuleSuffix(const std::string_view name) { return suffix == ModuleSuffixes.end() ? std::string_view{} : *suffix; } +inline std::string NeededLibraryName(std::string name) { + const auto suffix = ModuleSuffix(name); + if (!suffix.empty() && suffix != ".prx") name.replace(name.size() - suffix.size(), suffix.size(), ".prx"); + return name; +} + inline std::string ImportModuleName(std::string name) { name.resize(name.size() - ModuleSuffix(name).size()); if (name.ends_with("-module")) name.resize(name.size() - 7); diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 6506f2a745..fb4ea13dfa 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -132,8 +134,12 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con } std::vector symbolNames; - for (const auto& import : imports) + std::vector> nidNames; + for (const auto& import : imports) { symbolNames.push_back(addString(import.Name)); + const bool named = import.Library.empty() && !import.Name.empty() && import.Name.find('#') == std::string::npos; + nidNames.push_back(named ? std::optional(addString(Nid::ComputeNid(import.Name, ""))) : std::nullopt); + } std::vector searchTables; std::vector searchCounts; @@ -141,22 +147,23 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con std::vector imported(libraries.size()); const bool unscopedImports = std::any_of(imports.begin(), imports.end(), [](const auto& import) { return import.Library.empty(); }); for (const auto& import : imports) { + const auto library = Domain::NeededLibraryName(import.Library); std::vector order; for (std::size_t index = 0; index < libraries.size(); ++index) { auto name = std::filesystem::path(libraries[index]).filename().string(); if (name.ends_with(".guest.prx")) name.resize(name.size() - 10); const bool guestName = index < guestModules.size() && std::find(guestModules[index].Names.begin(), guestModules[index].Names.end(), import.Library) != guestModules[index].Names.end(); - if (import.Library.empty() || name == import.Library || guestName) order.push_back(CheckedRva(index)); + if (import.Library.empty() || name == library || guestName) order.push_back(CheckedRva(index)); } if (order.empty()) throw Domain::RelinkerException("Import module is not a dependency: " + import.Library); const bool hostModule = !import.Library.empty() && std::any_of(order.begin(), order.end(), [&](auto index) { return index >= guestModules.size(); }); if (hostModule) { for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { - if (libraries[index] == "libc.prx" && import.Library != "libc.prx") order.push_back(CheckedRva(index)); + if (libraries[index] == "libc.prx" && library != "libc.prx") order.push_back(CheckedRva(index)); } - if (import.Library == "libSceLibcInternal.prx") { + if (library == "libSceLibcInternal.prx") { for (std::size_t index = 0; index < guestModules.size(); ++index) { const auto& names = guestModules[index].Names; if (std::find(names.begin(), names.end(), "libc.prx") != names.end() && !guestModules[index].ZeroReturnExports.contains(import.Name)) @@ -596,9 +603,23 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.Emit({0x8b, 0x0f, 0x48, 0x8b, 0x0c, 0xcb, 0x48, 0x89, 0xf2}); call("GetProcAddress"); code.Emit({0x48, 0x85, 0xc0}); - const auto resolved = code.Branch({0x0f, 0x85}); + std::vector resolved = {code.Branch({0x0f, 0x85})}; code.Emit({0x48, 0x83, 0xc7, 4, 0xff, 0xcd}); code.Rip({0x0f, 0x85}, search); + if (nidNames[index]) { + code.Rip({0x48, 0x8d, 0x35}, *nidNames[index]); + code.Rip({0x48, 0x8d, 0x3d}, searchTables[index]); + code.Emit({0xbd}); + code.U32(searchCounts[index]); + const auto nidSearch = code.GetRva(); + code.Emit({0x8b, 0x0f, 0x48, 0x8b, 0x0c, 0xcb, 0x48, 0x89, 0xf2}); + call("GetProcAddress"); + code.Emit({0x48, 0x85, 0xc0}); + resolved.push_back(code.Branch({0x0f, 0x85})); + code.Emit({0x48, 0x83, 0xc7, 4, 0xff, 0xcd}); + code.Rip({0x0f, 0x85}, nidSearch); + } + const auto patchResolved = [&] { for (const auto branch : resolved) code.PatchBranch(branch, code.GetRva()); }; if (imports[index].Weak) { code.Emit({0x31, 0xc0}); const auto writeWeak = code.GetRva(); @@ -609,7 +630,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con } guestStartup.WriteImport(code, imports[index], handles); const auto weakDone = code.Branch({0xe9}); - code.PatchBranch(resolved, code.GetRva()); + patchResolved(); code.Rip({0xe9}, writeWeak); code.PatchBranch(weakDone, code.GetRva()); continue; @@ -617,7 +638,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con if (lazyBinding) { lazyUnresolvedImports.push_back(index); const auto skipGotWrite = code.Branch({0xe9}); - code.PatchBranch(resolved, code.GetRva()); + patchResolved(); if (imports[index].Addend != 0) { code.Emit({0x48, 0xba}); code.U64(imports[index].Addend); @@ -631,7 +652,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con writeString(errorRvas.at(3 + index), true); code.Rip({0xff, 0x05}, unresolvedCount); const auto nextImport = code.Branch({0xe9}); - code.PatchBranch(resolved, code.GetRva()); + patchResolved(); if (imports[index].RelocationType == 16) code.Emit({0x48, 0x8b, 0x00}); if (imports[index].RelocationType == 17) code.Emit({0x48, 0x8b, 0x40, 8}); if (imports[index].Addend != 0) { diff --git a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp index ed4f72c88e..e7153da217 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -43,7 +44,7 @@ std::vector WindowsImportBuilder::ReadLibraries(const Domain::SysVD const auto nameOffset = Io::ReadU64(bytes, offset + 8); if (nameOffset >= dynamicSection.DynStrData.size()) throw Domain::RelinkerException("DT_NEEDED string offset is out of bounds", nameOffset); - auto name = ReadString(dynamicSection.DynStrData, static_cast(nameOffset)); + auto name = Domain::NeededLibraryName(ReadString(dynamicSection.DynStrData, static_cast(nameOffset))); if (name.empty() || name.find_first_of("/\\:") != std::string::npos) throw Domain::RelinkerException("Invalid DT_NEEDED library: " + name); if (unique.insert(name).second) @@ -54,6 +55,8 @@ std::vector WindowsImportBuilder::ReadLibraries(const Domain::SysVD // Keep the requested module first, then make its shared runtime visible. if ((unique.contains("libSceLibcInternal.prx") || unique.contains("libkernel.prx")) && !unique.contains("libc.prx")) result.push_back("libc.prx"); + if (unique.contains("libkernel_web.prx") && !unique.contains("libkernel.prx")) + result.push_back("libkernel.prx"); return result; } diff --git a/core/relinker/relinker/tests/test_windows_named_imports.py b/core/relinker/relinker/tests/test_windows_named_imports.py new file mode 100644 index 0000000000..52df34c392 --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_named_imports.py @@ -0,0 +1,78 @@ +import os +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent / "macos")) + +from guest_import_fixtures import executable +from nid import nid +from test_guest_tls import guest_with_tls +from test_windows_import_modules import provider + + +def native_library(relinker, case, exported, value): + modules = case / "prx" + modules.mkdir(parents=True) + image = provider(value) + name = b"\0" + exported.encode() + b"\0" + image[0x800:0x800 + len(name)] = name + struct.pack_into("