fix(relinker): allow identical Windows function export aliases

This commit is contained in:
Oguzhan Ozdemir 2026-10-11 15:21:04 +03:00 committed by Adrià Franch
parent a11c8f7f70
commit 5ea9ce2e40
7 changed files with 161 additions and 7 deletions

View File

@ -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)

View File

@ -120,9 +120,16 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteWindows(const Relinker::GuestI
return false;
};
std::map<std::string, std::uint32_t> exports;
std::map<std::string, const Relinker::GuestSymbol*> 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) {

View File

@ -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<std::string> ReadModuleNames(const std::vector<std::uint8_t>& bytes) const;
GuestImage Read(const std::filesystem::path& path, std::vector<std::uint8_t> bytes) const;
GuestImage Read(const std::filesystem::path& path, std::vector<std::uint8_t> bytes, bool windows = false) const;
};
struct GuestArtifact {

View File

@ -87,7 +87,7 @@ std::vector<std::string> GuestImageReader::ReadModuleNames(const std::vector<std
return names;
}
GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector<std::uint8_t> bytes) const {
GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector<std::uint8_t> 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<std::string> neededLibraries;
for (const auto offset : needed) neededLibraries.push_back(string(offset));
std::set<std::string> exports;
std::map<std::string, std::size_t> 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<std::uint32_t>::max()) fail("String table too large");
Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast<std::uint32_t>(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));

View File

@ -122,7 +122,7 @@ std::vector<GuestArtifact> 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());

View File

@ -0,0 +1,75 @@
#include <relinker/guest/GuestImage.hpp>
#include <elfpatcher/general/GuestModuleWriter.hpp>
#include <elfpatcher/windows/WindowsLoadImage.hpp>
#include <elfpatcher/windows/WindowsPeFormat.hpp>
#include <io/BufferUtils.hpp>
#include <domain/ImportModule.hpp>
#include <array>
#include <cstring>
#include <iostream>
#include <stdexcept>
using Bytes = std::vector<std::uint8_t>;
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<std::uint64_t,std::uint64_t> 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<std::size(tags);++i){Io::WriteU64(b,0x2000+i*16,tags[i].first);Io::WriteU64(b,0x2008+i*16,tags[i].second);}
Io::WriteU32(b,0x2600,1);Io::WriteU32(b,0x2604,4);
for(unsigned i=1;i<=3;++i){const auto o=0x2400+i*24;Io::WriteU32(b,o,i==1?a:i==2?c:imp);b[o+4]=0x12;b[o+5]=i==3?0:3;Io::WriteU16(b,o+6,i==3?0:1);Io::WriteU64(b,o+8,i==3?0:0x20);Io::WriteU64(b,o+16,i==3?0:1);}
for(unsigned i=0;i<2;++i){Io::WriteU64(b,0x2700+i*24,0x2800+i*8);Io::WriteU64(b,0x2708+i*24,((i+1ull)<<32)|1);}
return b;
}
static std::size_t Offset(const Bytes& pe,std::uint32_t rva) {
const auto nt=Io::ReadU32(pe,0x3c);const auto count=Io::ReadU16(pe,nt+6);const auto sections=nt+24+Io::ReadU16(pe,nt+20);
for(unsigned i=0;i<count;++i){auto p=sections+i*40;auto start=Io::ReadU32(pe,p+12),size=Io::ReadU32(pe,p+16);if(rva>=start&&rva-start<size)return Io::ReadU32(pe,p+20)+rva-start;}
throw std::runtime_error("unmapped PE fixture RVA");
}
template<class F> 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<const char*>(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<std::pair<unsigned,std::uint64_t>,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<<e.what()<<'\n';return 1;}
}

View File

@ -0,0 +1,47 @@
"""Generic converter regression; emitted guest PE is inspected, never executed."""
from pathlib import Path
import hashlib,json,struct,subprocess,sys
sys.path.insert(0,str(Path(__file__).resolve().parent))
from test_windows_import_modules import executable
def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
def fixture():
b=bytearray(0x4000);b[:7]=b'\x7fELF\x02\x01\x01'
struct.pack_into('<HHI',b,16,3,62,1);struct.pack_into('<Q',b,32,64);struct.pack_into('<HHH',b,52,64,56,3)
def ph(i,t,fl,off,va,size):struct.pack_into('<IIQQQQQQ',b,64+i*56,t,fl,off,va,va,size,size,8)
ph(0,1,5,0x1000,0,0x1000);ph(1,1,6,0x2000,0x2000,0x1000)
b[0x1020:0x1026]=b'\xb8\x07\0\0\0\xc3'
strings=b'\0alias#A#A\0alias#B#A\0fixtureModule\0';b[0x2200:0x2200+len(strings)]=strings
tags=[(5,0x2200),(10,len(strings)),(6,0x2400),(11,24),(4,0x2600),(7,0x2700),(8,48),(9,24),(0x61000043,21),(0,0)]
ph(2,2,6,0x2000,0x2000,len(tags)*16)
for i,(t,v) in enumerate(tags):struct.pack_into('<QQ',b,0x2000+i*16,t,v)
struct.pack_into('<II',b,0x2600,1,3)
for i,no in [(1,1),(2,11)]:struct.pack_into('<IBBHQQ',b,0x2400+i*24,no,0x12,3,1,0x20,6)
for i in range(2):struct.pack_into('<QQq',b,0x2700+i*24,0x2800+i*8,((i+1)<<32)|1,0)
return b
def main():
stock,current,out=map(Path,sys.argv[1:]);assert not out.exists();out.mkdir()
case=out/'input';(case/'sce_module').mkdir(parents=True)
module=case/'sce_module/alias.prx';module.write_bytes(fixture());source=case/'input.elf';source.write_bytes(executable('alias.prx','alias#A#B','fixtureModule'))
pins={str(p):sha(p) for p in (module,source)};rows={}
for label,converter in [('stock',stock),('corrected',current)]:
target=out/label;target.mkdir();output=target/'app.exe'
args=[str(converter),'--windows','--sce-module-path',str(case),str(source),str(output)]
r=subprocess.run(args,cwd=target,stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=30)
rows[label]=dict(exitCode=r.returncode,stdout=r.stdout.decode(errors='replace'),stderr=r.stderr.decode(errors='replace'),converterSha256=sha(converter),outputExists=output.exists())
(out/(label+'-stdout.txt')).write_bytes(r.stdout);(out/(label+'-stderr.txt')).write_bytes(r.stderr)
if label=='stock':assert r.returncode==2 and 'Duplicate or empty export after stripping #: alias' in rows[label]['stderr'] and not output.exists()
else:assert r.returncode==0 and output.is_file(),rows[label]
guest=out/'corrected/app0/sce_module/alias.prx.guest.prx';pe=guest.read_bytes();nt=struct.unpack_from('<I',pe,0x3c)[0];opt=nt+24;sections=opt+struct.unpack_from('<H',pe,nt+20)[0]
def offset(rva):
for i in range(struct.unpack_from('<H',pe,nt+6)[0]):
_,va,sz,raw=struct.unpack_from('<IIII',pe,sections+i*40+8)
if va<=rva<va+sz:return raw+rva-va
raise AssertionError('unmapped PE RVA')
ed=offset(struct.unpack_from('<I',pe,opt+112)[0]);functions,names=struct.unpack_from('<II',pe,ed+20);assert functions==names==1
fn,ns,ords=struct.unpack_from('<III',pe,ed+28);rva=struct.unpack_from('<I',pe,offset(fn))[0];no=offset(struct.unpack_from('<I',pe,offset(ns))[0]);name=pe[no:pe.index(b'\0',no)].decode();assert name=='alias'
assert rva==0x10020 and all(struct.unpack_from('<Q',pe,offset(0x12800+i*8))[0]==0x140000000+rva for i in range(2))
assert all(sha(Path(p))==h for p,h in pins.items())
result=dict(stock=rows['stock'],corrected=rows['corrected'],fixtureHashes=pins,fixtureUnchanged=True,exportNames=[name],exportCount=functions,exportRva=rva,guestOutputSha256=sha(guest),bothRelocationTargetsVerified=True,noGuestExecution=True)
(out/'results.json').write_text(json.dumps(result,indent=2)+'\n');print('GENERIC_STOCK_FAIL_CORRECTED_PASS_SAME_FIXTURE')
if __name__=='__main__':main()