Skip to content

Load static archives with a /SYM64/ symbol table - #736

Open
zardus wants to merge 2 commits into
masterfrom
feature/fix-cle-ar-sym64
Open

Load static archives with a /SYM64/ symbol table#736
zardus wants to merge 2 commits into
masterfrom
feature/fix-cle-ar-sym64

Conversation

@zardus

@zardus zardus commented Aug 10, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

cle.Loader cannot open a static archive whose index is the 64-bit variant. GNU ar names that member /SYM64/; arpy classifies it as a member whose real name lives in the long filename table and then raises a bare ValueError parsing SYM64 as the offset into that table. Every mips64 archive is written this way, as is any archive too large to index with 32-bit offsets.

arpy discards the symbol table for either spelling, so the AR backend now skips a leading /SYM64/ member itself, and only when the member header fits inside the file. It also wraps arpy's parse failures in CLEInvalidBinaryError. The classification really belongs in arpy, but both the pinned 1.1.1 and the current release get it wrong.

The regression loads a real mips64 /SYM64/ library added by angr/binaries#176, so the checks here stay red until that merges.

Validation: #736 (comment)

GNU ar names the archive symbol table "/" for the 32-bit index and
"/SYM64/" for the 64-bit one, which it writes for mips64 targets and for
archives too large to index with 32-bit offsets. arpy only recognizes
the first spelling: it classifies "/SYM64/" as a member whose real name
lives in the long filename table and then raises ValueError parsing
"SYM64" as the offset into that table, so no such archive loads at all.

arpy discards the symbol table for either spelling, so skip a leading
64-bit one instead of handing it to arpy, and only when its header fits
inside the file. Wrap arpy's parse failures in CLEInvalidBinaryError as
well, so they no longer escape cle.Loader as a bare ValueError or an
arpy exception.
@zardus

zardus commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 1ffb9f17daeb4049fc4dc5f5ba3f1255963eeebd against baseline b58ea02a446106647cdaae32bdf91b7062404cc1.

  • Focused: pytest tests/test_static_archive.py — 4 passed
  • Regression: with cle/backends/static_archive.py reverted to the baseline and the tests left alone, three of the four fail. test_sym64_symbol_table and test_sym64_size_past_end both raise ValueError: invalid literal for int() with base 10: b'SYM64' from arpy.py:277, escaping cle.Loader uncaught, and test_bad_header_magic raises arpy.ArchiveFormatError: file header magic doesn't match. The 32-bit control test_symbol_table passes either way, which is what it is there for.
  • Full suite: pytest tests — 206 passed, 9 skipped, on Python 3.12.13 with arpy 1.1.1
  • Lint/type against the merge base: pylint cle/backends/static_archive.py 9.67 -> 9.81 and tests/test_static_archive.py 10.00 as a new file; pyright badness 0.20 -> 0.11 and 0.00
  • Pre-commit: the 24 configured hooks over --all-files, all passed or skipped, no rewrites

The regression loads tests/mips64/sym64_archive.a, added by angr/binaries#176: a 3,104-byte MIPS64 big-endian library indexed by a /SYM64/ table, holding one member x11_xcb.o. The 32-bit control loads tests_src/i2c_master_read-nucleol152re/mbed/TARGET_NUCLEO_L152RE/TOOLCHAIN_GCC_ARM/libmbed.a, already in angr/binaries, which has an ordinary / symbol table and a // long filename table. The two malformed cases copy one of those fixtures and overwrite a single member header field — the terminating magic and the symbol table size — so each input differs from a real archive only in the field under test. cle's CI checks out binaries master unconditionally, so the checks here stay red until angr/binaries#176 merges; the results above come from a local checkout of that branch. GNU binutils 2.46 reads both fixtures: ar t lists every member, including the ones named through the long filename table, and nm --print-armap prints the index for the /SYM64/ and / spellings alike.

The failure came out of a sweep of real-world static archives run on cle 638301bbf02865e53394e6643e1eb6e66083ee94. All 118 mips64 archives in it use /SYM64/ and none of them loaded, every one raising ValueError: invalid literal for int() with base 10: b'SYM64'. The new fixture is one of those archives, sha256 992dec1ee6f8400f30759f7998d7f3699261a39f7d7c03d175f1d5f7269e5782, taken from a public OS distribution.

Inputs probed by hand beyond the regression: even- and odd-length symbol tables, an empty index, a size field padded with spaces, a negative size, a non-numeric size, a size running past the end of the file, a /nnn long filename member that must not be taken for an index, a BSD member, and a /SYM64/ archive nested inside another. Each now either loads or raises CLEInvalidBinaryError; on the baseline every /SYM64/ case raised a bare ValueError. Removing the file-length bound on its own makes test_sym64_size_past_end fail with ValueError: No arch is assigned yet.

Caveats: the first Windows CI attempt failed before collecting anything, when two pytest-xdist workers raced to install the pyvex FFI parser cache and os.replace hit PermissionError: [WinError 5]; the rerun is green and an unrelated cle PR started at the same minute failed identically. An archive holding no members at all still fails with ValueError: No arch is assigned yet out of Backend.arch, which is unchanged from the baseline and independent of the symbol table spelling. Only cle and binaries are checked out for this branch, so the other workspace suites did not run.

@angr-bot

Copy link
Copy Markdown
Member

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

The first version of this test assembled its own ar archives with struct.pack.
That is a test input committed to the wrong repository in a form review does
not notice, and a hand-assembled container has a shape no toolchain emits, so
it can pass while the real format still fails.

Load fixtures instead:

- tests/mips64/sym64_archive.a, new in angr/binaries: a 3,104-byte MIPS64
  big-endian static library, from a public OS distribution, whose index is a
  "/SYM64/" symbol table.
- tests_src/.../libmbed.a, already in angr/binaries: an ARM library with an
  ordinary "/" symbol table and a "//" long filename table, covering the case
  that has to keep working. It needs rebase_granularity to dodge an unrelated
  R_ARM_THM_CALL range failure, which is what cle's error message for that
  failure asks for.

The two malformed inputs are copies of those fixtures with one member header
field overwritten -- the terminating magic, and the symbol table size -- so
each differs from a real archive only in the field under test.

Drop test_bad_long_name_offset. It invented a "/nope" member to reach the same
int() failure a "/SYM64/" header reaches for real, and test_sym64_size_past_end
now covers that path with the real spelling.
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