Skip to content

Reject malformed ELF symbol metadata - #853

Open
zardus wants to merge 1 commit into
masterfrom
feature/fix-c-23
Open

zardus wants to merge 1 commit into
masterfrom
feature/fix-c-23

Conversation

@zardus

@zardus zardus commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

Three unavailable ARM Cortex-M ELF binaries carry relocations whose symbol index is 2,097,161 even though their section-backed symbol tables contain only 553 or 662 entries. Loading each binary logs an ELFParseError: expected 4, found 0 traceback while pyelftools tries to read beyond the table:

  • 32648bd04baf266713d8f5b26a7d0279bd0d34ffa339101e08929b1454ba6c97
  • 8724d18dfbb82d33eada4f66875b846ff68026dc8c15b0505b8bb1a524ba1915
  • 95cd4fbe94fc27af342b9de952615d6061c704f6b43a39fb131b5180ff7d1a3d

The failure is observable while ELF.__register_relocs resolves relocation symbol index 0x200009 in each binary.

Malformed GNU version metadata has the same trust-boundary problem: a version-table ordinal absent from the parsed version definitions raises KeyError while constructing a symbol.

Root cause

ELF.get_symbol passes integer symbol identifiers directly to pyelftools. A section-backed SymbolTableSection.get_symbol computes an entry offset without checking num_symbols(), so an untrusted relocation index can seek beyond the section. The synthetic table inferred from PT_DYNAMIC is different: it has no section type, and its inferred count is not a reliable upper bound.

The version lookup similarly indexed _versions under the assumption that every ordinal referenced by .gnu.version has a matching definition.

Fix

Reject negative indices and indices at or beyond num_symbols() for real SHT_SYMTAB and SHT_DYNSYM tables, returning None so the malformed relocation is skipped. Preserve the existing synthetic PT_DYNAMIC behavior, and use a non-raising lookup for unknown version ordinals.

Focused tests cover negative and oversized indices, an unknown version ordinal, the synthetic-table exception, and the PLT/extern paths that depend on that exception.

Testing

The affected objects were measured before and after with one store-built environment per side. OBJECT denotes each unavailable binary identified by the corresponding SHA-256 above.

ENV=/nix/store/6n7araxwls4qn22g82qvjzwr4iwd1sly-python3-3.12.13-env; OBJECT='<unavailable-binary>'; RTDB_BASE=$(mktemp -d); export OBJECT RTDB_BASE; "$ENV/bin/python" -P -c 'import angr,logging,os; rows=[]; h=logging.Handler(logging.ERROR); h.emit=rows.append; log=logging.getLogger("cle.backends.elf.elf"); log.propagate=False; log.addHandler(h); p=angr.Project(os.environ["OBJECT"],auto_load_libs=False); log.removeHandler(h); o=p.loader.main_object; print(type(o).__name__,o.arch.name,len(o.symbols),len(o.relocs),len(rows),sum(r.exc_info is not None and type(r.exc_info[1]).__name__=="ELFParseError" for r in rows))'
ENV=/nix/store/2mijvwvc1dl632dxn4s09cfpykzqavkx-python3-3.12.13-env; OBJECT='<unavailable-binary>'; RTDB_BASE=$(mktemp -d); export OBJECT RTDB_BASE; "$ENV/bin/python" -P -c 'import angr,logging,os; rows=[]; h=logging.Handler(logging.ERROR); h.emit=rows.append; log=logging.getLogger("cle.backends.elf.elf"); log.propagate=False; log.addHandler(h); p=angr.Project(os.environ["OBJECT"],auto_load_libs=False); log.removeHandler(h); o=p.loader.main_object; print(type(o).__name__,o.arch.name,len(o.symbols),len(o.relocs),len(rows),sum(r.exc_info is not None and type(r.exc_info[1]).__name__=="ELFParseError" for r in rows))'

All 3 affected objects changed from 3 cle ELF error records, including 1 ELFParseError, to 2 error records and 0 ELFParseErrors. Backend, architecture, entry point, address range, section and segment inventories, symbol count, and relocation count stayed identical.

Regression set: 3 other ARM Cortex-M ELF objects from the same decbench sweep family. Their complete probe summaries were byte-for-byte identical before and after; all loaded with 0 cle ELF error records, and 0 of 3 got worse.

Local validation at head 5218e54ebbb290693881fcf047a81a56e2fc65e3:

  • focused ELF and PLT/extern regressions: 7 passed
  • cle: 286 passed, 9 skipped; one unrelated missing-fixture failure from the already-merged Mach-O: Load a file that carries no LC_SYMTAB #843, whose prerequisite Add a Mach-O relocatable object that carries no symbol table binaries#235 is still open
  • merge-base pylint: 10.00/10.00 for both changed files; pyright: 0 added errors
  • full gate: test-inputs, test-packages, pre-commit, feature-build, archinfo, pypcode, pyvex, pysoot, angr Rust, and worktree cleanliness passed; unrelated existing failures remained in workspace campaign tests, the mono component-list invariant, the missing cle fixture above, and one unchanged angr reaching-definitions test

The complete cle suite requires the fixture from angr/binaries#235, which must merge before this pull request.

sync: angr/binaries#235

Validation: #853 (comment)

session: sharpen

@zardus

zardus commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Complete stdout from the loader-error probe for one affected unavailable binary; the fields are backend, architecture, symbol count, relocation count, cle ELF error count, and ELFParseError count.

Before — pyelftools reads beyond the section-backed symbol table and cle records an ELFParseError:

cle master 997d432
ELF ARMCortexM 662 0 3 1

After — the invalid index is rejected before parsing, so no ELFParseError is recorded:

with 5218e54
ELF ARMCortexM 662 0 2 0

@zardus

zardus commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 5218e54ebbb290693881fcf047a81a56e2fc65e3 against baseline 997d4322678fe5876d9f83e0dd34e1052813b9db.

Dataset A/B, using one store-built environment per side and each of the three unavailable binaries identified by SHA-256 in the pull request body:

ENV=/nix/store/6n7araxwls4qn22g82qvjzwr4iwd1sly-python3-3.12.13-env; OBJECT='<unavailable-binary>'; RTDB_BASE=$(mktemp -d); export OBJECT RTDB_BASE; "$ENV/bin/python" -P -c 'import angr,logging,os; rows=[]; h=logging.Handler(logging.ERROR); h.emit=rows.append; log=logging.getLogger("cle.backends.elf.elf"); log.propagate=False; log.addHandler(h); p=angr.Project(os.environ["OBJECT"],auto_load_libs=False); log.removeHandler(h); o=p.loader.main_object; print(type(o).__name__,o.arch.name,len(o.symbols),len(o.relocs),len(rows),sum(r.exc_info is not None and type(r.exc_info[1]).__name__=="ELFParseError" for r in rows))'
ENV=/nix/store/2mijvwvc1dl632dxn4s09cfpykzqavkx-python3-3.12.13-env; OBJECT='<unavailable-binary>'; RTDB_BASE=$(mktemp -d); export OBJECT RTDB_BASE; "$ENV/bin/python" -P -c 'import angr,logging,os; rows=[]; h=logging.Handler(logging.ERROR); h.emit=rows.append; log=logging.getLogger("cle.backends.elf.elf"); log.propagate=False; log.addHandler(h); p=angr.Project(os.environ["OBJECT"],auto_load_libs=False); log.removeHandler(h); o=p.loader.main_object; print(type(o).__name__,o.arch.name,len(o.symbols),len(o.relocs),len(rows),sum(r.exc_info is not None and type(r.exc_info[1]).__name__=="ELFParseError" for r in rows))'

Result: all 3 affected objects changed from 3 cle ELF error records with 1 ELFParseError to 2 error records with 0 ELFParseErrors. The 3-object same-family regression set remained byte-for-byte identical with 0 errors before and after; worse: 0.

Focused regression command:

cd features/fix-c-23/repos/cle && ../../../../nix/run.sh --feature fix-c-23 -- pytest --import-mode=append -q tests/test_elf.py tests/test_extern.py::test_ppc64_abiv1_untyped_function_import tests/test_plt.py::TestCheckPltEntries::test_x86_64_fauxware tests/test_plt.py::TestCheckPltEntries::test_x86_64_simple_overflow_nopie

Result: 7 passed in 0.58s.

Merge-base CI diff checks:

nix/run.sh --feature fix-c-23 -- python3 -P .agents/skills/angr-validate-workspace/scripts/run-ci-diff-checks.py --repository features/fix-c-23/repos/cle

Result: pylint 10.00 to 10.00 for cle/backends/elf/elf.py; new tests/test_elf.py 10.00. Pyright errors 46 to 46 for production and 0 to 0 for the test.

Complete collection gate:

./feature.sh test fix-c-23

Passed suites: test-inputs, test-packages, pre-commit, feature-build, archinfo (42 tests and 34 subtests), pypcode (46 tests and 187 subtests), pyvex (87), pysoot, angr Rust, and worktree cleanliness. Cle passed 286 tests and skipped 9; its only failure is a missing Mach-O fixture introduced by already-merged #843 and still waiting on angr/binaries#235. Angr passed 3,747 tests, skipped 47, xfailed 2, and failed one unchanged test that passes a now-invalid third argument to Register. Workspace campaign tests and mono each have unrelated existing failures. The feature did not adopt angr-management, so that suite did not run.

@angr-bot

angr-bot commented Oct 1, 2026

Copy link
Copy Markdown
Member

Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_853

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants