Merge pull request #10560 from Bram910/fix/audit-gnu-hash-chain-walks

fix(tools): avoid repeated GNU hash chain walks
This commit is contained in:
Dean authored and GitHub committed 2026-10-27 09:01:01 +13:00
commit 0dbed7b5b5
2 files changed
+64 -4

No files matched your search

+3 -1
View File
@@ -127,16 +127,18 @@ def elf_program_dynamic_symbols(path, data):
bucket_data = take(data, bucket_offset, buckets * 4, path, "GNU hash buckets")
chain_offset = bucket_offset + buckets * 4
maximum = first_symbol - 1
visited_chain_entries = set()
for index in struct.unpack(f"<{buckets}I", bucket_data) if buckets else ():
if index == 0:
continue
if index < first_symbol:
raise AuditError(f"{path}: GNU hash bucket index {index} precedes symbol offset {first_symbol}")
symbol_index = index
while True:
while symbol_index not in visited_chain_entries:
chain_address = dynamic[0x6ffffef5] + (chain_offset - hash_offset) + (symbol_index - first_symbol) * 4
chain_file_offset = file_offset(chain_address, 4, "GNU hash chain")
chain_value = unpack("<I", data, chain_file_offset, path, "GNU hash chain entry")[0]
visited_chain_entries.add(symbol_index)
maximum = max(maximum, symbol_index)
if chain_value & 1:
break
+61 -3
View File
@@ -56,7 +56,7 @@ def build_elf(symbols, needed=()):
return bytes(image)
def build_sectionless_elf(symbols, needed=(), gnu_hash=False):
def build_sectionless_elf(symbols, needed=(), gnu_hash=False, gnu_buckets=(1,)):
strings = b"\0"
entries = [struct.pack("<IBBHQQ", 0, 0, 0, 0, 0, 0)]
for name, index, info in symbols:
@@ -73,8 +73,9 @@ def build_sectionless_elf(symbols, needed=(), gnu_hash=False):
symbol_table = b"".join(entries)
hash_offset = (symbols_offset + len(symbol_table) + 3) & ~3
if gnu_hash:
hash_table = struct.pack("<IIII", 1, 1, 1, 0) + bytes(8)
hash_table += struct.pack("<I", 1) + b"".join(struct.pack("<I", 1 if index == len(entries) - 1 else 0) for index in range(1, len(entries)))
hash_table = struct.pack("<IIII", len(gnu_buckets), 1, 1, 0) + bytes(8)
hash_table += b"".join(struct.pack("<I", index) for index in gnu_buckets)
hash_table += b"".join(struct.pack("<I", 1 if index == len(entries) - 1 else 0) for index in range(1, len(entries)))
hash_tag = 0x6ffffef5
else:
hash_table = struct.pack("<II", 1, len(entries)) + struct.pack("<I", 0) + bytes(4 * len(entries))
@@ -100,6 +101,21 @@ def build_sectionless_elf(symbols, needed=(), gnu_hash=False):
return bytes(image)
def gnu_hash_chain_offset(image):
header = struct.unpack_from("<16sHHIQQQIHHHHHH", image)
segments = [struct.unpack_from("<IIQQQQQQ", image, header[5] + index * header[9])
for index in range(header[10])]
dynamic = next(segment for segment in segments if segment[0] == 2)
tags = dict(struct.unpack_from("<qQ", image, position)
for position in range(dynamic[2], dynamic[2] + dynamic[5], 16))
address = tags[0x6ffffef5]
load = next(segment for segment in segments
if segment[0] == 1 and segment[3] <= address < segment[3] + segment[5])
hash_offset = address - load[3] + load[2]
bucket_count, _, bloom_count, _ = struct.unpack_from("<IIII", image, hash_offset)
return hash_offset + struct.calcsize("<IIII") + bloom_count * struct.calcsize("<Q") + bucket_count * struct.calcsize("<I")
def exporting(*names):
return build_elf([(name, 1, GLOBAL_FUNCTION) for name in names])
@@ -1129,6 +1145,48 @@ class ImportAuditTests(unittest.TestCase):
self.assertEqual(import_audit.elf_program_dynamic_symbols(Path("module.prx"), image),
({"AAAAAAAAAAA": False}, {"BBBBBBBBBBB"}, {"libSceA.prx"}))
def test_sectionless_elf_gnu_hash_reads_each_chain_entry_once(self):
symbols = [(f"symbol{index}", int(index != 0), GLOBAL_FUNCTION) for index in range(32)]
expected = ({"symbol0": False}, {name for name, section, _ in symbols if section}, {"libSceA.prx"})
for buckets, terminator in (((1,) * 32, None), (tuple(range(32, 0, -1)), None), ((17, 1, 0, 17), 16)):
with self.subTest(buckets=buckets, terminator=terminator):
image = bytearray(build_sectionless_elf(symbols, ["libSceA.prx"], gnu_hash=True, gnu_buckets=buckets))
chain_offset = gnu_hash_chain_offset(image)
if terminator is not None:
struct.pack_into("<I", image, chain_offset + (terminator - 1) * 4, 1)
with patch.object(import_audit, "unpack", wraps=import_audit.unpack) as unpack:
self.assertEqual(import_audit.elf_program_dynamic_symbols(Path("module.prx"), image), expected)
chain_reads = [call.args[2] for call in unpack.call_args_list
if call.args[0] == "<I" and chain_offset <= call.args[2] < chain_offset + len(symbols) * 4]
self.assertEqual(len(chain_reads), len(symbols))
self.assertEqual(set(chain_reads), set(range(chain_offset, chain_offset + len(symbols) * 4, 4)))
def test_sectionless_elf_gnu_hash_empty_buckets(self):
for buckets in ((), (0,), (0, 0, 0)):
with self.subTest(buckets=buckets):
image = build_sectionless_elf([], gnu_hash=True, gnu_buckets=buckets)
chain_offset = gnu_hash_chain_offset(image)
with patch.object(import_audit, "unpack", wraps=import_audit.unpack) as unpack:
self.assertEqual(import_audit.elf_program_dynamic_symbols(Path("module.prx"), image), ({}, set(), set()))
self.assertFalse(any(call.args[0] == "<I" and chain_offset <= call.args[2] < len(image)
for call in unpack.call_args_list))
def test_sectionless_elf_gnu_hash_requires_file_backed_chains(self):
symbols = [(f"symbol{index}", 1, GLOBAL_FUNCTION) for index in range(4)]
for buckets in ((1, 1), (3, 1, 2)):
for terminated in (False, True):
image = bytearray(build_sectionless_elf(symbols, gnu_hash=True, gnu_buckets=buckets))
chain_offset = gnu_hash_chain_offset(image)
chain_end = chain_offset + len(symbols) * 4
if not terminated:
struct.pack_into("<I", image, chain_end - 4, 0)
file_size = chain_end - int(terminated)
struct.pack_into("<QQ", image, 64 + 32, file_size, file_size)
for reader in (import_audit.elf_exports, import_audit.elf_imports):
with self.subTest(buckets=buckets, terminated=terminated, reader=reader.__name__):
with self.assertRaisesRegex(import_audit.AuditError, "GNU hash chain.*outside file-backed"):
reader(Path("unmapped-chain.prx"), image)
def test_sectionless_hash_metadata_must_be_file_backed(self):
for gnu_hash in (False, True):
image = bytearray(build_sectionless_elf([("AAAAAAAAAAA", 1, GLOBAL_FUNCTION)], gnu_hash=gnu_hash))