Merge pull request #4340 from ArjenS-LA/fix/guest-gnu-hash

fix(relinker): convert guest modules with GNU hash tables
This commit is contained in:
Dean 2026-10-13 18:49:38 +13:00 committed by GitHub
commit ebf417a51c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 144 additions and 2 deletions

View File

@ -505,6 +505,9 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_init_array_roots.py $<TARGET_FILE:relinker>)
add_test(NAME gnu_hash_analysis COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_gnu_hash_analysis.py $<TARGET_FILE:relinker>)
add_test(NAME guest_gnu_hash COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_gnu_hash.py $<TARGET_FILE:relinker>)
set_tests_properties(guest_gnu_hash PROPERTIES TIMEOUT 20)
add_test(NAME to_rosetta COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_to_rosetta.py $<TARGET_FILE:relinker>)
add_test(NAME syscall_number COMMAND ${Python3_EXECUTABLE}

View File

@ -148,7 +148,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector
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)) {
else if (tag < 0x60000000 || tag == 0x6ffffef5 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) {
if (!tags.emplace(tag, value).second) fail("Duplicate dynamic tag " + std::to_string(tag));
}
}
@ -194,7 +194,35 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector
std::uint64_t symSize = 0;
if (tags.contains(0x6100003f)) symSize = tags.at(0x6100003f);
else if (tags.contains(4)) symSize = static_cast<std::uint64_t>(Io::ReadU32(bytes, translate(tags.at(4), 8) + 4)) * 24;
else fail("Missing dynamic symbol count");
else if (tags.contains(0x6ffffef5)) {
const auto address = tags.at(0x6ffffef5);
const auto header = translate(address, 16);
const auto bucketCount = Io::ReadU32(bytes, header);
const auto firstSymbol = Io::ReadU32(bytes, header + 4);
const auto bloomCount = Io::ReadU32(bytes, header + 8);
if (bucketCount == 0 || bloomCount == 0 || firstSymbol == 0) fail("Invalid GNU hash header");
const std::uint64_t bucketOffset = 16 + static_cast<std::uint64_t>(bloomCount) * 8;
const auto chainOffset = bucketOffset + static_cast<std::uint64_t>(bucketCount) * 4;
const auto start = translate(address, chainOffset);
std::uint64_t lastBucket = 0;
for (std::uint64_t index = 0; index < bucketCount; ++index) {
const auto bucket = Io::ReadU32(bytes, start + bucketOffset + index * 4);
if (bucket != 0 && bucket < firstSymbol) fail("Invalid GNU hash bucket");
lastBucket = std::max(lastBucket, static_cast<std::uint64_t>(bucket));
}
std::uint64_t symbolCount = firstSymbol;
if (lastBucket != 0) {
symbolCount = lastBucket;
for (;;) {
const auto offset = chainOffset + (symbolCount - firstSymbol) * 4;
if (offset > std::numeric_limits<std::uint64_t>::max() - address) fail("Invalid GNU hash chain");
const auto chain = Io::ReadU32(bytes, translate(address + offset, 4));
++symbolCount;
if (chain & 1) break;
}
}
symSize = symbolCount * 24;
} else fail("Missing dynamic symbol count");
if (symSize == 0 || symSize % 24 != 0 || symSize / 24 > std::numeric_limits<std::uint32_t>::max()) fail("Invalid dynamic symbol count");
const auto symOffset = table(6, 0x61000039, symSize);
image.Dynamic.DynSymData.assign(bytes.begin() + symOffset, bytes.begin() + symOffset + symSize);

View File

@ -0,0 +1,111 @@
from pathlib import Path
import os
import struct
import subprocess
import sys
import tempfile
from test_gnu_hash_analysis import name_hash
from test_guest_intel_trampolines import main_fixture
from test_guest_symbol_names import dynamic_tags, file_offset
from test_windows_import_modules import dynamic, executable, provider
HASH = 0xA00
SYMBOLS = 0xC00
STRINGS = 0xD00
def module(names=("alpha", "bravo", "charlie"), bucket_count=3):
image = provider(42)
image.extend(bytes(0x1000 - len(image)))
struct.pack_into("<QQ", image, 120 + 32, 0xA00, 0xA00)
names = sorted(names, key=lambda name: name_hash(name + "#A#B") % bucket_count)
strings = bytearray(b"\0")
buckets = [0] * bucket_count
chains = []
bloom = 0
for index, name in enumerate(names, 1):
code = 0x400 + index * 16
image[code:code + 6] = b"\xb8" + struct.pack("<I", 40 + index) + b"\xc3"
struct.pack_into("<IBBHQQ", image, SYMBOLS + index * 24,
len(strings), 0x12, 0, 1, 0x1000 + index * 16, 6)
qualified = name + "#A#B"
strings.extend(qualified.encode() + b"\0")
hashed = name_hash(qualified)
bucket = hashed % bucket_count
if buckets[bucket] == 0:
buckets[bucket] = index
if chains:
chains[-1] |= 1
chains.append(hashed & ~1)
bloom |= (1 << (hashed % 64)) | (1 << ((hashed >> 5) % 64))
if chains:
chains[-1] |= 1
struct.pack_into("<IIIIQ", image, HASH, bucket_count, 1, 1, 5, bloom)
struct.pack_into("<" + "I" * (len(buckets) + len(chains)), image, HASH + 24,
*buckets, *chains)
image[STRINGS:STRINGS + len(strings)] = strings
tags = [(5, STRINGS + 0x1A00), (10, len(strings)), (6, SYMBOLS + 0x1A00), (11, 24),
(0x6FFFFEF5, HASH + 0x1A00), (7, 0x2300), (8, 0), (9, 24)]
dynamic(image, 0x600, 176, tags)
return image, names
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-guest-gnu-hash-") as directory:
work = Path(directory)
def convert(label, windows, image, names):
case = work / label / str(windows)
modules = case / "sce_module"
modules.mkdir(parents=True)
guest = modules / "provider.prx"
guest.write_bytes(image)
source = case / "input.elf"
source.write_bytes(executable("provider.prx", names[-1] + "#A#B") if names else main_fixture())
output = case / ("output.exe" if windows else "output.elf")
result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(source), str(output)],
capture_output=True, text=True, timeout=10)
assert guest.read_bytes() == image
return result, output, case / "app0/sce_module/provider.prx.guest.prx"
for label, requested, bucket_count in (("multiple-buckets", ("alpha", "bravo", "charlie"), 3),
("one-chain", ("alpha", "bravo", "charlie"), 1),
("one-symbol", ("alpha",), 3),
("no-hashed-symbols", (), 3)):
image, names = module(requested, bucket_count)
for windows in (False, True):
result, output, artifact = convert(label, windows, image, names)
assert result.returncode == 0, (label, windows, result.stdout, result.stderr)
if windows and os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=10)
assert run.returncode == (40 + len(names) if names else 42), (label, run.returncode, run.stdout, run.stderr)
if not windows:
data = artifact.read_bytes()
tags = dynamic_tags(data)
symbol_count, = struct.unpack_from("<I", data, file_offset(data, tags[4]) + 4)
assert symbol_count == len(names) + 1, (label, symbol_count)
for label, offset, value, error in (("zero-buckets", 0, 0, "Invalid GNU hash header"),
("zero-bloom", 8, 0, "Invalid GNU hash header"),
("zero-first-symbol", 4, 0, "Invalid GNU hash header"),
("large-bloom", 8, 0xFFFFFFFF, "Unmapped ELF address")):
image, names = module()
struct.pack_into("<I", image, HASH + offset, value)
result, output, artifact = convert(label, False, image, names)
assert result.returncode == 2 and error in result.stderr, (label, result.stderr)
assert not output.exists() and not artifact.exists()
image, names = module()
struct.pack_into("<Q", image, 0x600 + 4 * 16 + 8, 0x29E0)
struct.pack_into("<IIIIQII", image, 0xFE0, 1, 1, 1, 5, 0, 1, 2)
result, output, artifact = convert("unterminated-chain", False, image, names)
assert result.returncode == 2 and "Unmapped ELF address" in result.stderr, result.stderr
assert not output.exists() and not artifact.exists()
print("GNU hash guest module tests passed")
if __name__ == "__main__":
main()