fix(relinker): discover required guest module identities

This commit is contained in:
eliasbruno124-dev 2026-10-09 11:30:08 +02:00
parent d70b899894
commit 98c47df9e5
6 changed files with 376 additions and 7 deletions

View File

@ -239,6 +239,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_host_libc.py $<TARGET_FILE:relinker>)
add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $<TARGET_FILE:relinker>)
add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_identity.py $<TARGET_FILE:relinker>)
add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $<TARGET_FILE:relinker>)
add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE}

View File

@ -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::map<std::u
auto name = module->second;
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) {

View File

@ -28,6 +28,7 @@ struct GuestImage {
std::filesystem::path SourcePath;
std::string OutputName;
std::string Soname;
std::vector<std::string> ModuleNames;
std::vector<std::uint8_t> Bytes;
std::vector<Codegen::TrampolineSite> Trampolines;
std::vector<Domain::ProgramHeader> Headers;
@ -44,6 +45,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;
};

View File

@ -11,6 +11,82 @@
namespace Relinker {
std::vector<std::string> GuestImageReader::ReadModuleNames(const std::vector<std::uint8_t>& 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<std::uint64_t>(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<std::uint64_t, std::uint64_t> strings;
std::vector<std::uint64_t> 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<std::string> names;
std::set<std::uint64_t> 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<std::ptrdiff_t>(offset + nameOffset);
const auto end = bytes.begin() + static_cast<std::ptrdiff_t>(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<std::uint8_t> 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<std::uint64_t, std::uint64_t> tags;
std::vector<std::uint64_t> needed;
std::vector<std::uint64_t> moduleImports;
std::vector<std::uint64_t> 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<std::uint64_t> 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<std::string> dependencies;
for (const auto offset : needed) {
const auto name = string(offset);

View File

@ -85,17 +85,60 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
}
for (const auto& [name, path] : found) paths.push_back(path);
}
Io::FileReader reader;
std::map<std::filesystem::path, GuestImage> discovered;
const auto matchesIdentity = [](const std::string& name, const std::vector<std::string>& 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<char>(character + ('a' - 'A'));
return name;
};
for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(path.string())));
std::set<std::string> 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<std::string, std::filesystem::path> 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<GuestImage> images;
std::map<std::string, std::vector<std::size_t>> exports;
std::map<std::string, std::set<std::size_t>> sharedExports;
std::set<std::string> 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<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
}
std::map<std::string, std::size_t> guestNames;
std::map<std::string, std::size_t> windowsGuestFiles;
const auto foldFilename = [](std::string name) {
for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast<char>(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<GuestArtifact> 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;

View File

@ -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('<H', image, 56, 4)
struct.pack_into('<IIQQQQQQ', image, 232, 0x61000000, 4, 0x800, 0, 0, 0x40, 0x40, 8)
struct.pack_into('<qQ', image, 0x600, 0x61000035, 0)
struct.pack_into('<q', image, 0x610, 0x61000037)
return image
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix='anyps5-module-identity-') as directory:
work = Path(directory)
def convert(label, windows, request, module_name='libGuest', providers=None, guest_request=None, symbol='shared#A#B', module_directory='sce_module', extra=None):
case = work / label
modules = case / module_directory
modules.mkdir(parents=True)
(case / 'sce_module').mkdir(exist_ok=True)
for filename, image in (providers or {'renamed.prx': declared_provider(22, ('libGuest',))}).items():
(modules / filename).write_bytes(image)
for filename, image in (extra or {}).items():
target = case / filename
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(image)
if guest_request:
(case / 'sce_module/consumer.prx').write_bytes(consumer(guest_request, symbol=symbol, module_name=module_name))
source = case / 'input.elf'
source.write_bytes(executable(request, symbol=symbol, module_name=module_name))
output = case / ('output.exe' if windows else 'output.elf')
result = subprocess.run([str(relinker), *(['--windows'] if windows else []),
'--rpath', '$ORIGIN/custom-hosts', str(source), str(output)],
capture_output=True, text=True, timeout=30)
return result, output
def run(output, expected=22, error=None):
if os.name != 'nt':
return
result = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
if error:
assert result.returncode != 0 and error in result.stderr, (result.returncode, result.stdout, result.stderr)
else:
assert result.returncode == expected, (result.returncode, result.stdout, result.stderr)
for windows in (True, False):
for directory in ('sce_module', '.', 'Media/Modules'):
result, output = convert(f'sprx-request-{windows}-{directory.replace("/", "-")}', windows,
'libGuest.sprx', module_directory=directory, guest_request='libGuest.sprx',
providers={'renamed.sprx': declared_provider(22, ('libGuest',))})
assert result.returncode == 0, result.stderr
artifact = output.parent / 'app0' / directory / 'renamed.sprx.guest.prx'
assert artifact.is_file(), list((output.parent / 'app0').rglob('*'))
assert len(list((output.parent / 'app0').rglob('*.guest.prx'))) == 2
if windows:
run(output)
else:
assert needed_libraries(output.read_bytes()) == [
'$ORIGIN/app0/' + directory + '/renamed.sprx.guest.prx',
'$ORIGIN/app0/sce_module/consumer.prx.guest.prx']
for directory in ('.', 'Media/Modules'):
for suffix in ('.prx', '.sprx', '.suprx'):
relative = Path(directory) / ('renamed' + suffix)
result, output = convert(f'outside-{windows}-{directory.replace("/", "-")}-{suffix}', windows,
'libGuest.suprx', module_directory=directory, guest_request='libGuest.suprx',
providers={relative.name: declared_provider(22, ('libGuest',))},
extra={'unused.prx': declared_provider(77, ('libUnused',)), 'opaque.sprx': b'not an ELF'})
assert result.returncode == 0, result.stderr
artifact = output.parent / 'app0' / (relative.as_posix() + '.guest.prx')
assert artifact.is_file(), list((output.parent / 'app0').rglob('*'))
assert len(list((output.parent / 'app0').rglob('*.guest.prx'))) == 2
if windows:
run(output)
else:
assert needed_libraries(output.read_bytes()) == [
'$ORIGIN/app0/' + directory + '/renamed' + suffix + '.guest.prx',
'$ORIGIN/app0/sce_module/consumer.prx.guest.prx']
result, output = convert(f'outside-ambiguous-{windows}', windows, 'libGuest.suprx', module_directory='.',
providers={'one.prx': declared_provider(22, ('libGuest',))},
extra={'other/two.prx': declared_provider(33, ('libGuest',))})
assert result.returncode == 2 and 'Ambiguous needed module identity' in result.stderr, result.stderr
assert not output.exists()
result, output = convert(f'outside-sce-strings-{windows}', windows, 'libGuest.suprx', module_directory='.',
providers={'renamed.prx': sce_strings(declared_provider(22, ('libGuest',)))})
assert result.returncode == 0, result.stderr
if windows:
run(output)
for soname in (False, True):
filename = 'physical.prx' if soname else 'libGuest.suprx'
result, output = convert(f'known-before-outside-{windows}-{soname}', windows, 'libGuest.suprx',
providers={filename: declared_provider(22, ('unrelated',), 'libGuest.suprx' if soname else None)},
extra={'renamed.prx': declared_provider(11, ('libGuest',))})
assert result.returncode == 0, result.stderr
assert len(list((output.parent / 'app0').rglob('*.guest.prx'))) == 1
if windows:
run(output)
malformed = declared_provider(22, ('libGuest',))
struct.pack_into('<Q', malformed, 0x600 + 8 * 16 + 8, (1 << 48) | 0xffffffff)
result, output = convert(f'outside-bad-name-{windows}', windows, 'libGuest.suprx',
module_directory='.', providers={'renamed.prx': malformed})
assert result.returncode == 2 and 'Module metadata string offset out of bounds' in result.stderr, result.stderr
assert not output.exists()
malformed = sce_strings(declared_provider(22, ('libGuest',)))
struct.pack_into('<Q', malformed, 0x618, 0x100)
result, output = convert(f'outside-bad-sce-range-{windows}', windows, 'libGuest.suprx',
module_directory='.', providers={'renamed.prx': malformed})
assert result.returncode == 2 and 'Module metadata strings exceed dynamic data' in result.stderr, result.stderr
assert not output.exists()
unrelated = declared_provider(77, ('libUnused',))
struct.pack_into('<qQ', unrelated, 0x600 + 5 * 16, 0x70000000, 0)
result, output = convert(f'outside-unrelated-unsupported-body-{windows}', windows, 'libGuest.suprx',
module_directory='.', extra={'unused.prx': unrelated})
assert result.returncode == 0, result.stderr
assert len(list((output.parent / 'app0').rglob('*.guest.prx'))) == 1
if windows:
run(output)
for windows in (True, False):
for tag in (0x6100000d, 0x61000043):
for suffix in ('.suprx', '.prx', '.sprx'):
request = 'libGuest' + suffix
result, output = convert(f'{windows}-{tag}-{suffix}', windows, request,
providers={'renamed.prx': declared_provider(22, ('libGuest',), tag=tag)}, guest_request=request)
assert result.returncode == 0, result.stderr
if windows:
run(output)
else:
needed = needed_libraries(output.read_bytes())
assert needed == ['$ORIGIN/app0/sce_module/renamed.prx.guest.prx',
'$ORIGIN/app0/sce_module/consumer.prx.guest.prx'], needed
child = output.parent / 'app0/sce_module/consumer.prx.guest.prx'
assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx']
providers = {'one.prx': declared_provider(22, ('libGuest',)),
'two.prx': declared_provider(33, ('libGuest',), symbol='other')}
result, output = convert(f'{windows}-ambiguous', windows, 'libGuest.suprx', providers=providers)
assert result.returncode == 2 and 'Ambiguous guest module identity' in result.stderr, result.stderr
assert not output.exists()
for request in ('LIBGUEST.suprx', 'libGuest.xyz', 'unavailable.suprx'):
result, output = convert(f'{windows}-{request}', windows, request, module_name=request.rsplit('.', 1)[0])
assert result.returncode == 0, result.stderr
if windows:
run(output, error='Failed to load module:')
else:
assert request in needed_libraries(output.read_bytes())
providers = {'wrong.prx': declared_provider(11, ('other',)),
'right.prx': declared_provider(22, ('libGuest',))}
result, output = convert('windows-owner', True, 'libGuest.suprx', providers=providers, guest_request='libGuest.suprx')
assert result.returncode == 0, result.stderr
run(output)
providers = {'LIBGUEST.suprx': declared_provider(22, ('unrelated',)),
'renamed.prx': declared_provider(11, ('libGuest',))}
result, output = convert('windows-physical-before-identity', True, 'libGuest.suprx',
providers=providers, guest_request='libGuest.suprx')
assert result.returncode == 0, result.stderr
run(output)
providers = {'LIBGUEST.suprx': declared_provider(11, ('unrelated',)),
'renamed.prx': declared_provider(22, ('libOther',), soname='libGuest.suprx')}
result, output = convert('windows-soname-before-folded-file', True, 'libGuest.suprx',
providers=providers, guest_request='libGuest.suprx')
assert result.returncode == 0, result.stderr
run(output)
for exact_soname in (False, True):
providers = {'other.prx' if exact_soname else 'libGuest.suprx':
declared_provider(22, ('unrelated',), 'libGuest.suprx' if exact_soname else None),
'alias.prx': declared_provider(11, ('libGuest',))}
result, output = convert(f'exact-{exact_soname}', True, 'libGuest.suprx', providers=providers, guest_request='libGuest.suprx')
assert result.returncode == 0, result.stderr
run(output)
result, output = convert('missing-symbol', True, 'libGuest.suprx', symbol='absent#A#B')
assert result.returncode == 0, result.stderr
run(output, error='unresolved ELF import absent')
providers = {'wrong.prx': declared_provider(11, ('other',)),
'right.prx': declared_provider(22, ('libGuest',), symbol='other')}
result, output = convert('wrong-provider', True, 'libGuest.suprx', providers=providers)
assert result.returncode == 0, result.stderr
run(output, error='unresolved ELF import shared')
result, output = convert('multiple-identities', True, 'libGuest.suprx',
providers={'renamed.prx': declared_provider(22, ('custom', 'libGuest'))}, guest_request='libGuest.suprx')
assert result.returncode == 0, result.stderr
run(output)
print('Guest module identity dependency tests passed')
if __name__ == '__main__':
main()