From 5ea9ce2e40df9eeacf4e60702e45b256f3cabb96 Mon Sep 17 00:00:00 2001 From: Oguzhan Ozdemir <48760055+oguzhanozfe@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:21:04 +0300 Subject: [PATCH] fix(relinker): allow identical Windows function export aliases --- core/relinker/CMakeLists.txt | 14 ++++ .../src/windows/WindowsGuestModuleWriter.cpp | 9 ++- .../include/relinker/guest/GuestImage.hpp | 10 ++- .../relinker/src/guest/GuestImageReader.cpp | 11 ++- .../relinker/src/guest/GuestModuleBuilder.cpp | 2 +- .../tests/WindowsExportAliasTests.cpp | 75 +++++++++++++++++++ .../tests/test_windows_export_aliases.py | 47 ++++++++++++ 7 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 core/relinker/relinker/tests/WindowsExportAliasTests.cpp create mode 100644 core/relinker/relinker/tests/test_windows_export_aliases.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..8a0ed0d66 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -81,6 +81,20 @@ target_include_directories(relinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs ) +if(BUILD_TESTING AND WIN32) + get_target_property(windowsAliasTestSources relinker SOURCES) + list(REMOVE_ITEM windowsAliasTestSources + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp) + add_test_executable(windows_guest_export_alias_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/WindowsExportAliasTests.cpp + ${windowsAliasTestSources}) + get_target_property(windowsAliasIncludes relinker INCLUDE_DIRECTORIES) + target_include_directories(windows_guest_export_alias_tests PRIVATE ${windowsAliasIncludes}) + add_test(NAME windows_guest_export_aliases COMMAND windows_guest_export_alias_tests) +endif() + if(WIN32) target_compile_options(relinker PRIVATE -municode) target_link_options(relinker PRIVATE -municode) diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index f7ca50fa2..bcc3469dd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -120,9 +120,16 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI return false; }; std::map exports; + std::map exportSymbols; PeSection tlsExports{".tlsrefs", nextRva, SectionRead | 0x40u, {}}; for (const auto& symbol : guest.Symbols) { - if (symbol.Section == 0 || symbol.Section == Relinker::AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + if (symbol.Section == 0 || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + const auto [previous, inserted] = exportSymbols.emplace(symbol.Name, &symbol); + if (!inserted) { + if (!Relinker::IdenticalOrdinaryFunctionExport(*previous->second, symbol)) throw Domain::RelinkerException("Duplicate guest export: " + symbol.Name); + continue; + } + if (symbol.Section == Relinker::AbsoluteSection) continue; if (symbol.Section >= 0xff00) throw Domain::RelinkerException("Unsupported guest export section: " + symbol.Name); std::uint32_t rva = 0; if ((symbol.Info & 15) == 6) { diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index fcc35ac65..d566a152c 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -25,8 +25,16 @@ struct GuestSymbol { std::uint64_t Value; std::uint64_t Size; std::string Library; + std::string QualifiedName; }; +inline bool IdenticalOrdinaryFunctionExport(const GuestSymbol& left, const GuestSymbol& right) { + return left.Section != 0 && left.Section < 0xff00 && (left.Info >> 4) != 0 && + left.Visibility != 1 && left.Visibility != 2 && (left.Info & 15) == 2 && + left.Info == right.Info && left.Visibility == right.Visibility && + left.Section == right.Section && left.Value == right.Value && left.Size == right.Size; +} + struct GuestImage { std::filesystem::path SourcePath; std::string OutputName; @@ -49,7 +57,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; + GuestImage Read(const std::filesystem::path& path, std::vector bytes, bool windows = false) const; }; struct GuestArtifact { diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index ac7caaf6b..e6c3545a7 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -87,7 +87,7 @@ std::vector GuestImageReader::ReadModuleNames(const std::vector bytes) const { +GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector bytes, const bool windows) 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) { if (offset > bytes.size() || size > bytes.size() - offset) { @@ -204,19 +204,21 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (!importModules.emplace(value >> 48, string(value & 0xffffffffu)).second) fail("Duplicate import module ID"); std::vector neededLibraries; for (const auto offset : needed) neededLibraries.push_back(string(offset)); - std::set exports; + std::map exports; for (std::uint64_t offset = 0; offset < symSize; offset += 24) { auto name = string(Io::ReadU32(bytes, symOffset + offset)); const auto library = Io::ReadU16(bytes, symOffset + offset + 6) == 0 ? Domain::ImportModule(name, importModules, neededLibraries) : std::string{}; if (Io::ReadU16(bytes, symOffset + offset + 6) == 0 && library.empty()) { if (auto declared = Domain::DeclaredImportModule(name, importModules); !declared.empty()) image.Dynamic.UnmatchedImportModules.insert(std::move(declared)); } + const auto qualifiedName = name; name = name.substr(0, name.find('#')); const auto info = bytes[symOffset + offset + 4]; const auto visibility = bytes[symOffset + offset + 5]; const auto section = Io::ReadU16(bytes, symOffset + offset + 6); const auto value = Io::ReadU64(bytes, symOffset + offset + 8); const auto size = Io::ReadU64(bytes, symOffset + offset + 16); + const GuestSymbol symbol{name, info, visibility, section, value, size, library, qualifiedName}; if (offset == 0 && (info != 0 || visibility != 0 || section != 0 || value != 0 || size != 0 || !name.empty())) fail("Invalid null symbol"); if (offset == 0) { image.Symbols.push_back({{}, 0, 0, 0, 0, 0}); @@ -224,7 +226,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector } if (offset != 0 && ((info >> 4) > 2 || (visibility & ~3u) != 0 || ((info & 15) != 0 && (info & 15) != 1 && (info & 15) != 2 && (info & 15) != 6))) fail("Unsupported symbol attributes: " + name); if (section != 0 && (info >> 4) != 0 && visibility != 1 && visibility != 2) { - if (name.empty() || !exports.insert(name).second) fail("Duplicate or empty export after stripping #: " + name); + const auto [found, inserted] = exports.emplace(name, image.Symbols.size()); + if (name.empty() || (!inserted && (!windows || !IdenticalOrdinaryFunctionExport(image.Symbols[found->second], symbol)))) fail("Duplicate or empty export after stripping #: " + name); } if ((info & 15) == 6 && section != 0 && (tls == nullptr || value > tls->MemorySize || size > tls->MemorySize - value)) fail("TLS symbol exceeds TLS segment: " + name); if (section != 0 && section != AbsoluteSection && (info & 15) != 6) { @@ -234,7 +237,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) fail("String table too large"); Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast(image.Dynamic.DynStrData.size())); Io::AppendString(image.Dynamic.DynStrData, name); - image.Symbols.push_back({name, info, visibility, section, value, size, library}); + image.Symbols.push_back(symbol); } if (tags.contains(14)) { image.Soname = string(tags.at(14)); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 689f4dafd..e2c63cd97 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -122,7 +122,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx"; }); }; - for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)))); + for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)), windows)); 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()); diff --git a/core/relinker/relinker/tests/WindowsExportAliasTests.cpp b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp new file mode 100644 index 000000000..78cc146c8 --- /dev/null +++ b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Bytes = std::vector; +static void Check(bool ok, const char* message) { if (!ok) throw std::runtime_error(message); } +static void Ph(Bytes& b, int i, unsigned type, unsigned flags, unsigned off, unsigned va, unsigned size) { + const auto p=64+i*56; Io::WriteU32(b,p,type); Io::WriteU32(b,p+4,flags); + Io::WriteU64(b,p+8,off);Io::WriteU64(b,p+16,va);Io::WriteU64(b,p+24,va); + Io::WriteU64(b,p+32,size);Io::WriteU64(b,p+40,size);Io::WriteU64(b,p+48,8); +} +static Bytes Fixture(bool reverse=false) { + Bytes b(0x4000);std::memcpy(b.data(),"\x7f" "ELF\x02\x01\x01",7); + Io::WriteU16(b,16,3);Io::WriteU16(b,18,62);Io::WriteU32(b,20,1); + Io::WriteU64(b,32,64);Io::WriteU16(b,52,64);Io::WriteU16(b,54,56);Io::WriteU16(b,56,3); + Ph(b,0,1,5,0x1000,0,0x1000);Ph(b,1,1,6,0x2000,0x2000,0x1000); + b[0x1020]=0xc3;b[0x1028]=0xc3; + std::string strings(1,'\0');auto str=[&](const char* s){auto o=strings.size();strings+=s;strings+='\0';return o;}; + const auto a=str(reverse?"alias#B#A":"alias#A#A"),c=str(reverse?"alias#A#A":"alias#B#A"); + const auto imp=str("imported#A#B"),host=str("fixtureHost.prx"),module=str("fixtureModule"); + std::copy(strings.begin(),strings.end(),b.begin()+0x2200); + const std::pair tags[]={{5,0x2200},{10,strings.size()},{6,0x2400},{11,24},{4,0x2600},{7,0x2700},{8,48},{9,24},{1,host},{0x61000043,module},{0x61000045,(1ull<<48)|host},{0,0}}; + Ph(b,2,2,6,0x2000,0x2000,sizeof(tags)); + for(unsigned i=0;i=start&&rva-start static void Reject(F&& f,const char* expected) { + bool rejected=false;try{f();}catch(const Domain::RelinkerException& e){rejected=std::string(e.what()).find(expected)!=std::string::npos;} + Check(rejected,expected); +} +static void Positive(bool reverse) { + const auto bytes=Fixture(reverse);const auto g=Relinker::GuestImageReader().Read("generic.prx",bytes,true); + Check(g.Symbols.size()==4&&g.Bytes==bytes,"input symbols/bytes changed"); + Check(g.Symbols[1].Name=="alias"&&g.Symbols[2].Name=="alias","stripped lookup names"); + Check(g.Symbols[1].QualifiedName==(reverse?"alias#B#A":"alias#A#A")&&g.Symbols[2].QualifiedName==(reverse?"alias#A#A":"alias#B#A"),"qualified names lost"); + Check(g.Symbols[1].Info==18&&g.Symbols[2].Info==18&&g.Symbols[1].Visibility==3&&g.Symbols[2].Visibility==3&&g.Symbols[1].Section==1&&g.Symbols[2].Section==1&&g.Symbols[1].Value==0x20&&g.Symbols[2].Value==0x20&&g.Symbols[1].Size==1&&g.Symbols[2].Size==1,"raw tuple changed"); + Check((Io::ReadU64(g.Dynamic.RelaData,8)>>32)==1&&(Io::ReadU64(g.Dynamic.RelaData,32)>>32)==2,"alias relocation indices changed"); + Check(g.Symbols[3].QualifiedName=="imported#A#B"&&g.Symbols[3].Library=="fixtureHost.prx","qualified import changed"); + Domain::GuestRuntime runtime;const auto pe=Elfpatcher::GuestModuleWriter().WriteWindows(g,runtime); + const auto directory=Offset(pe,Io::ReadU32(pe,Io::ReadU32(pe,0x3c)+24+112)); + Check(Io::ReadU32(pe,directory+20)==1&&Io::ReadU32(pe,directory+24)==1,"multiple PE exports"); + const auto function=Io::ReadU32(pe,Offset(pe,Io::ReadU32(pe,directory+28))); + const auto nameRva=Io::ReadU32(pe,Offset(pe,Io::ReadU32(pe,directory+32))); + Check(std::strcmp(reinterpret_cast(pe.data()+Offset(pe,nameRva)),"alias")==0,"wrong export name"); + Elfpatcher::Windows::WindowsLoadImage image(g.Bytes,g.Headers,false); + Check(function==image.GetRva(0x20,1),"wrong export RVA"); + for(auto target:{0x2800,0x2808})Check(Io::ReadU64(pe,Offset(pe,image.GetRva(target,8)))==Elfpatcher::Windows::ImageBase+function,"wrong alias relocation value"); +} +int main() { + try { + Positive(false);Positive(true); + Reject([]{Relinker::GuestImageReader().Read("generic.prx",Fixture());},"Duplicate or empty export"); + const std::array,7> changes={{{4,0x22},{4,0x11},{5,0},{6,2},{8,0x28},{16,2},{6,Relinker::AbsoluteSection}}}; + for(const auto& [field,value]:changes){auto b=Fixture();auto off=0x2400+48+field;if(field<6)b[off]=value;else if(field==6)Io::WriteU16(b,off,value);else Io::WriteU64(b,off,value);Reject([&]{Relinker::GuestImageReader().Read("generic.prx",b,true);},"Duplicate or empty export");} + auto good=Relinker::GuestImageReader().Read("generic.prx",Fixture(),true); + for(unsigned kind=0;kind<10;++kind){auto g=good;auto& s=g.Symbols[2];switch(kind){case 0:s.Value+=8;break;case 1:s.Size+=1;break;case 2:s.Info=0x22;break;case 3:s.Info=0x11;break;case 4:s.Visibility=0;break;case 5:s.Section=2;break;case 6:g.Symbols[1].Info=s.Info=0x11;break;case 7:g.Symbols[1].Section=s.Section=Relinker::AbsoluteSection;break;case 8:g.Symbols[1].Info=s.Info=0x16;g.Dynamic.RelaData.clear();break;case 9:g.Symbols[1].Info=s.Info=0x10;break;}Check(!Relinker::IdenticalOrdinaryFunctionExport(g.Symbols[1],s),"nonordinary/conflicting tuple accepted");Domain::GuestRuntime r;Reject([&]{Elfpatcher::GuestModuleWriter().WriteWindows(g,r);},kind==8?"TLS":"Duplicate guest export");} + Check(Domain::DeclaredImportModule("same#A#B",{{1,"one"},{2,"two"}})=="one"&&Domain::DeclaredImportModule("same#A#C",{{1,"one"},{2,"two"}})=="two","module-qualified IDs merged"); + std::cout<<"WINDOWS_EXPORT_ALIAS_CASES_PASSED\npreserved-indices reversed-order conflicting-fields strict-nonwindows qualified-import writer-guard\n"; + } catch(const std::exception& e){std::cerr<