Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion tools/import_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,18 @@ def file_offset(address, size, what):
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
Expand Down
52 changes: 49 additions & 3 deletions tools/tests/test_import_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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))
Expand Down Expand Up @@ -1040,6 +1041,51 @@ def test_sectionless_elf_gnu_hash(self):
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))
dynamic = struct.unpack_from("<IIQQQQQQ", image, 120)
tags = dict(struct.unpack_from("<qQ", image, position)
for position in range(dynamic[2], dynamic[2] + dynamic[5], 16))
chain_offset = tags[0x6ffffef5] - 0x400000 + 24 + len(buckets) * 4
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[4] == "GNU hash chain entry"]
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)
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[4] == "GNU hash chain entry" for call in unpack.call_args_list))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this assertion is tied to the label string in import_audit.py:129. If that label is renamed, the count test above fails loudly, but this assertFalse(any(...)) quietly checks nothing. A shared constant, or matching on the chain offset range, would remove the coupling.


def test_sectionless_elf_gnu_hash_overlapping_buckets_require_file_backed_chains(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: in all four subtests the first bucket's own walk reaches the unbacked chain entry and raises, so no later bucket runs and the visited set is never consulted; the test passes unchanged on main. It still guards against tolerating unbacked chain entries, but the overlap in its name plays no role. A rename, or a layout such as (1, 3) with entry 2 terminated so the error comes from the second bucket after the set is populated, would make the name match.

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))
dynamic = struct.unpack_from("<IIQQQQQQ", image, 120)
tags = dict(struct.unpack_from("<qQ", image, position)
for position in range(dynamic[2], dynamic[2] + dynamic[5], 16))
chain_offset = tags[0x6ffffef5] - 0x400000 + 24 + len(buckets) * 4
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))
Expand Down
Loading