Merge pull request #10352 from Bram910/fix/audit-dynamic-tables

fix(tools): validate ELF import table file ranges
This commit is contained in:
Dean authored and GitHub committed 2026-10-26 13:31:43 +13:00
commit e41113fd39
2 files changed
+92

No files matched your search

+3
View File
@@ -90,6 +90,7 @@ def elf_program_dynamic_symbols(path, data):
_, _, dynamic_offset, _, _, dynamic_size, _, _ = dynamic_segments[0]
if dynamic_size % 16:
raise AuditError(f"{path}: dynamic segment size {dynamic_size} is not a multiple of 16")
take(data, dynamic_offset, dynamic_size, path, "dynamic segment")
dynamic = {}
needed_offsets = []
terminated = False
@@ -207,6 +208,7 @@ def elf_imports(path, data):
if kind == 11:
if entry != 24 or size % 24 or link >= len(sections) or sections[link][1] != 3:
raise AuditError(f"{path}: invalid dynamic symbol table (entry size {entry}, size {size}, string table section {link})")
take(data, offset, size, path, "dynamic symbol table")
strings_offset, strings_size = sections[link][4], sections[link][5]
take(data, strings_offset, strings_size, path, "dynamic string table")
for position in range(0, size, 24):
@@ -218,6 +220,7 @@ def elf_imports(path, data):
elif kind == 6:
if entry != 16 or size % 16 or link >= len(sections) or sections[link][1] != 3:
raise AuditError(f"{path}: invalid dynamic section (entry size {entry}, size {size}, string table section {link})")
take(data, offset, size, path, "dynamic section")
strings_offset, strings_size = sections[link][4], sections[link][5]
take(data, strings_offset, strings_size, path, "dynamic string table")
terminated = False
+89
View File
@@ -104,6 +104,13 @@ def exporting(*names):
return build_elf([(name, 1, GLOBAL_FUNCTION) for name in names])
def elf_dynamic_range_fields(image):
sections = struct.unpack_from("<Q", image, 40)[0]
if sections:
return sections + 192 + 24, sections + 192 + 32
return 120 + 8, 120 + 32
def with_elf_symbol_table_range(image, offset, size):
image = bytearray(image)
section_offset = struct.unpack_from("<Q", image, 40)[0] + 64
@@ -1033,6 +1040,88 @@ class ImportAuditTests(unittest.TestCase):
self.assertIn(str(path), err)
self.assertIn("lies outside the file", err)
def test_elf_import_symbol_tables_reject_ranges_outside_the_file(self):
original = build_elf([("alpha", 0, GLOBAL_FUNCTION)])
for offset, size in ((len(original) + 1, 0), (2**64 - 1, 0),
(len(original) - 23, 24), (len(original), 24)):
with self.subTest(offset=offset, size=size):
with self.assertRaisesRegex(import_audit.AuditError, "dynamic symbol table.*outside the file"):
import_audit.elf_imports(Path("invalid-imports.prx"), with_elf_symbol_table_range(original, offset, size))
def test_elf_import_symbol_tables_can_end_at_eof(self):
original = build_elf([("alpha", 0, GLOBAL_FUNCTION)])
empty = with_elf_symbol_table_range(original, len(original), 0)
symbols, needed = import_audit.elf_imports(Path("empty.prx"), empty)
self.assertFalse(symbols)
self.assertFalse(needed)
sections = struct.unpack_from("<Q", original, 40)[0]
offset, size = struct.unpack_from("<QQ", original, sections + 64 + 24)
relocated = with_elf_symbol_table_range(original + original[offset:offset + size], len(original), size)
symbols, needed = import_audit.elf_imports(Path("relocated.prx"), relocated)
self.assertEqual(set(symbols), {"alpha"})
self.assertFalse(needed)
def test_elf_dynamic_table_ranges_are_checked_before_the_terminator(self):
for sectioned in (False, True):
original = build_elf([], ["libc.prx"]) if sectioned else build_sectionless_elf([], ["libc.prx"])
offset_field, size_field = elf_dynamic_range_fields(original)
offset = struct.unpack_from("<Q", original, offset_field)[0]
excessive_size = ((len(original) - offset) // 16 + 1) * 16
for start, size in ((offset, excessive_size), (offset, 2**64 - 16),
(len(original) + 1, 0), (2**64 - 1, 16)):
with self.subTest(sectioned=sectioned, offset=start, size=size):
image = bytearray(original)
struct.pack_into("<Q", image, offset_field, start)
struct.pack_into("<Q", image, size_field, size)
with self.assertRaisesRegex(import_audit.AuditError, "dynamic (segment|section).*outside the file"):
import_audit.elf_imports(Path("truncated.prx"), image)
def test_elf_dynamic_tables_ending_at_eof_and_terminated_padding(self):
for sectioned in (False, True):
original = build_elf([], ["libc.prx"]) if sectioned else build_sectionless_elf([], ["libc.prx"])
offset_field, size_field = elf_dynamic_range_fields(original)
offset = struct.unpack_from("<Q", original, offset_field)[0]
size = struct.unpack_from("<Q", original, size_field)[0]
for padding in (b"", struct.pack("<qQ", 1, 2**64 - 1)):
with self.subTest(sectioned=sectioned, padding=bool(padding)):
image = bytearray(original + original[offset:offset + size] + padding)
struct.pack_into("<Q", image, offset_field, len(original))
struct.pack_into("<Q", image, size_field, size + len(padding))
if not sectioned:
struct.pack_into("<QQ", image, 64 + 32, len(image), len(image))
struct.pack_into("<QQ", image, offset_field + 8, 0x400000 + len(original), 0x400000 + len(original))
struct.pack_into("<Q", image, size_field + 8, size + len(padding))
symbols, needed = import_audit.elf_imports(Path("valid.prx"), image)
self.assertFalse(symbols)
self.assertEqual(needed, {"libc.prx"})
def test_invalid_elf_dynamic_ranges_preserve_the_previous_report(self):
registry = self.work.root / "game.json"
report = self.work.root / "audit.json"
module = self.work.modules / "broken.prx"
write_registry(registry, [])
(self.work.libs / "libc.prx").write_bytes(exporting())
cases = []
for sectioned in (False, True):
original = build_elf([], ["libc.prx"]) if sectioned else build_sectionless_elf([], ["libc.prx"])
offset_field, size_field = elf_dynamic_range_fields(original)
offset = struct.unpack_from("<Q", original, offset_field)[0]
image = bytearray(original)
struct.pack_into("<Q", image, size_field, ((len(image) - offset) // 16 + 1) * 16)
cases.append(("dynamic section" if sectioned else "dynamic segment", image))
original = build_elf([], ["libc.prx"])
cases.append(("dynamic symbol table", with_elf_symbol_table_range(original, len(original) + 1, 0)))
for table, image in cases:
with self.subTest(table=table):
module.write_bytes(image)
report.write_bytes(b"previous report")
code, output, error = self.work.run(registry, "--json", str(report))
self.assertEqual((code, output), (2, ""))
self.assertIn(str(module), error)
self.assertIn(table, error)
self.assertNotIn("Traceback", error)
self.assertEqual(report.read_bytes(), b"previous report")
def test_sectionless_elf_gnu_hash(self):
image = build_sectionless_elf([
("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)],