Skip to content

Lowercase the AR, COFF and Universal2 backend names - #799

Open
zardus wants to merge 1 commit into
masterfrom
feature/backend-name-case
Open

Lowercase the AR, COFF and Universal2 backend names#799
zardus wants to merge 1 commit into
masterfrom
feature/backend-name-case

Conversation

@zardus

@zardus zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

Selecting the static-archive backend by name fails, on the lowercase spelling
that every other backend in the registry teaches:

>>> ld = cle.Loader("binaries/tests_src/i2c_master_read-nucleol152re/mbed/TARGET_NUCLEO_L152RE/TOOLCHAIN_GCC_ARM/libmbed.a",
...                 main_opts={"backend": "ar"})
cle.errors.CLEError: Invalid backend: ar
>>> "ar" in cle.ALL_BACKENDS
False

{"backend": "coff"} on binaries/tests/x86_64/fauxware.obj and
{"backend": "universal2"} on binaries/tests/multi_arch/fauxware_macho_multiarch
fail the same way, while {"backend": "elf"} works. The spellings that do work
for these three are AR, COFF and Universal2, which appear nowhere outside
their own registration lines: not in cle's README, not in cle's docs, and not in
angr's table of backend names. So the only way to find them is to read the source
of each backend, and a caller who generalises from elf, pe, mach-o or
blob gets a CLEError instead of a loader.

Root cause

Loader._backend_resolver looks a name up exactly:

        elif backend in ALL_BACKENDS:
            return ALL_BACKENDS[backend]

and the names it looks up are inconsistent. Nineteen of the twenty-two entries in
ALL_BACKENDS are lowercase; register_backend("AR", StaticArchive),
register_backend("COFF", Coff) and register_backend("Universal2", Universal2)
are not. Each was spelled that way in the commit that added the backend, by three
different authors, and no commit message discusses the choice.

Fix

Rename those three registrations to match their siblings, so the convention the
registry already follows holds for every entry, and resolve a backend name
without regard to case.

The second half is what keeps the rename safe rather than being a convenience.
angr's .adb serializer writes cle's registered name into the database
(LoaderSerializer.backend2name) and hands it straight back to cle.Loader as
main_opts["backend"] when the database is reopened, so every database saved
from an AR, COFF or Universal2 object has the capitalized spelling on disk. With
the rename alone, reopening one of those raises AngrDBError from
CLEError: Invalid backend: COFF; with case-insensitive resolution it loads.

Registering the lowercase names as extra aliases would do the same job and cost
more: ALL_BACKENDS is iterated for format autodetection and is the list
angr-management fills its backend dropdown from, so each alias would add a
duplicate probe and a duplicate menu entry, and it would make
backend2name, a dict inversion, pick its winner by insertion order.

Testing

tests/test_backend_names.py loads a real static archive, a real COFF object and
a real universal binary by their lowercase names, checks that the capitalized
spellings an existing .adb holds still resolve, checks that an unregistered name
is still rejected, and asserts that every registered name is lowercase, which is
the property the resolver now depends on. Its three lowercase cases and the
lowercase assertion fail at the merge base with CLEError: Invalid backend: ar,
: coff and : universal2. The archive is
binaries/tests_src/i2c_master_read-nucleol152re/mbed/TARGET_NUCLEO_L152RE/TOOLCHAIN_GCC_ARM/libmbed.a,
the only committed !<arch> fixture that loads today.

Validation: #799 (comment)

session: sharpen

Nineteen of the twenty-two names registered into ALL_BACKENDS are lowercase.
Three are not, and _backend_resolver looks a name up exactly, so following the
convention every other backend teaches raises CLEError: Invalid backend: ar.

Rename the three to match their siblings, and resolve a backend name without
regard to case so the old spellings keep working. That second half is not
decoration: angr's .adb serializer writes the registered name into the database
and feeds it straight back to Loader on reopen, so a database saved from an AR,
COFF or Universal2 object holds the capitalized spelling on disk.
@zardus

zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Selecting each of the three backends by its lowercase name, before and after this
change. The script prints the resolved cle.__file__ first, so each side names the
tree it ran in.

the reproducer
"""Select a backend by the lowercase name every other cle backend teaches."""

import os
import sys

import cle
from cle.backends import ALL_BACKENDS

binaries = sys.argv[1]

print("cle:", cle.__file__)
print("registered names:", sorted(ALL_BACKENDS))
print()

targets = [
    (
        "ar",
        os.path.join(
            binaries,
            "tests_src/i2c_master_read-nucleol152re/mbed/TARGET_NUCLEO_L152RE/TOOLCHAIN_GCC_ARM/libmbed.a",
        ),
        {"rebase_granularity": 0x1000},
    ),
    ("coff", os.path.join(binaries, "tests/x86_64/fauxware.obj"), {}),
    ("universal2", os.path.join(binaries, "tests/multi_arch/fauxware_macho_multiarch"), {}),
]

for name, path, opts in targets:
    print(f'{name!r} in ALL_BACKENDS: {name in ALL_BACKENDS}')
    try:
        loader = cle.Loader(path, main_opts={"backend": name}, auto_load_libs=False, **opts)
    except Exception as e:  # noqa: BLE001
        print(f'  cle.Loader(..., main_opts={{"backend": "{name}"}}) -> {type(e).__name__}: {e}')
    else:
        print(f'  cle.Loader(..., main_opts={{"backend": "{name}"}}) -> {type(loader.main_object).__name__}')
    print()

Before — the lowercase name every other backend teaches is not registered:

cle at the merge base
cle: <cle at the merge base>/cle/__init__.py
registered names: ['AR', 'COFF', 'Universal2', 'apk', 'backedcgc', 'blob', 'cart', 'cgc', 'dex', 'elf', 'elfcore', 'hex', 'jar', 'mach-o', 'minidump', 'named_region', 'pe', 'srec', 'stm32', 'te', 'uefi', 'xbe']

'ar' in ALL_BACKENDS: False
  cle.Loader(..., main_opts={"backend": "ar"}) -> CLEError: Invalid backend: ar

'coff' in ALL_BACKENDS: False
  cle.Loader(..., main_opts={"backend": "coff"}) -> CLEError: Invalid backend: coff

'universal2' in ALL_BACKENDS: False
  cle.Loader(..., main_opts={"backend": "universal2"}) -> CLEError: Invalid backend: universal2

After — all three resolve, and the registry is uniformly lowercase:

cle with this change
cle: <cle with this change>/cle/__init__.py
registered names: ['apk', 'ar', 'backedcgc', 'blob', 'cart', 'cgc', 'coff', 'dex', 'elf', 'elfcore', 'hex', 'jar', 'mach-o', 'minidump', 'named_region', 'pe', 'srec', 'stm32', 'te', 'uefi', 'universal2', 'xbe']

'ar' in ALL_BACKENDS: True
  cle.Loader(..., main_opts={"backend": "ar"}) -> StaticArchive

'coff' in ALL_BACKENDS: True
  cle.Loader(..., main_opts={"backend": "coff"}) -> Coff

'universal2' in ALL_BACKENDS: True
  cle.Loader(..., main_opts={"backend": "universal2"}) -> Universal2

The capitalized spellings that an existing .adb holds still resolve. Writing a
database from a COFF object with cle at the merge base, then reopening it three ways:

.adb round trip
=== .adb round trip ===
--- write with cle at the merge base ---
provenance cle:  <cle at the merge base>/cle/__init__.py
provenance angr: <angr>/angr/__init__.py
loaded as: Coff
stored backend: [('COFF',)]
--- reopen with cle with this change ---
provenance cle:  <cle with this change>/cle/__init__.py
provenance angr: <angr>/angr/__init__.py
stored backend: [('COFF',)]
reopened as: Coff
--- reopen with the rename applied but the resolver left case-sensitive ---
provenance cle:  <cle, rename only>/cle/__init__.py
provenance angr: <angr>/angr/__init__.py
stored backend: [('COFF',)]
REOPEN FAILED: AngrDBError: 
  caused by CLEError: Invalid backend: COFF

@zardus

zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 1ed92cc97c03e81b5281b87bb2154dcdc81fa2fb against baseline c7e0d4db664a45b59c84967ac42d04f82a97b087.

Every run below used the workspace's Python with PYTHONPATH pointing at the tree under test; each arm printed cle.__file__ and it resolved inside that tree.

  • Regression: python -m pytest tests/test_backend_names.py -q — baseline 4 failed, 4 passed; head 8 passed. The four baseline failures are CLEError: Invalid backend: ar, : coff, : universal2, and the lowercase-registry assertion reporting ['COFF', 'AR', 'Universal2'].
  • Focused: python -m pytest tests -q -n 4 (cle's own suite, gcc provisioned with nix shell nixpkgs#gcc) — baseline 242 passed, 9 skipped; head 250 passed, 9 skipped. Without gcc on PATH both arms additionally fail tests/test_clemory.py::test_cclemory, which builds a cffi extension.
  • Downstream: python -m pytest angr/tests/serialization angr/tests/angrdb -q -n 4 against angr at a9ca247a5bcadf0ad8ecccb61be5f76dc4cd5050 — identical on both arms, 1 failed, 77 passed, 2 xfailed. The failure is test_pickle.py::TestPickle::test_pickling, which cannot resolve libz3.so outside the workspace shell and fails the same way at the baseline.
  • Serialization: an .adb written from tests/x86_64/fauxware.obj at the baseline stores backend='COFF'; it reopens as Coff at the head, and reopens as AngrDBError caused by CLEError: Invalid backend: COFF on a control tree carrying the rename without the resolver change. A pickled Project is unaffected in either direction, since it carries the backend class rather than its name.
  • Autodetection: [c.__name__ for c in ALL_BACKENDS.values()] is byte-identical on both arms, so the order Loader._static_backend probes in is unchanged.
  • Lint/type: run-ci-diff-checks.py --repository cle — no regression on any of the five changed files; tests/test_backend_names.py is new and scores pylint 10.00/10.00 with pyright badness 0.0.
  • Hooks: pre-commit run --all-files in the branch worktree — every hook passed and rewrote nothing.
  • Workspace gate: NOT RUN. Corpus-sweep workers hold the shared native libraries under the primary virtual environment for the duration of this change, and this branch's feature instance has never had its shell entered, so the first entry is the case that rebuilds unicornlib.so, librustylib.so and libpyvex.so in the primary checkouts underneath them. The suites that therefore did not run are archinfo, pypcode, pyvex, claripy, angr, angr-management, pysoot, the Rust tests, and the workspace's own checks; hosted CI covers them.

Caveats: tests/test_backend_names.py loads binaries/tests_src/i2c_master_read-nucleol152re/mbed/TARGET_NUCLEO_L152RE/TOOLCHAIN_GCC_ARM/libmbed.a, which is the only committed !<arch> fixture the AR backend can load today — tests/mips64/sym64_archive.a stops at arpy's SYM64 handling and tests/aarch64/bsd_symdef_archive.a hands its __.SYMDEF SORTED member to the loader. Renaming the keys changes what angr-management's load dialog shows in its backend dropdown, since it fills that from ALL_BACKENDS directly; the names it offers stay valid because they come from the same dict.

@angr-bot

Copy link
Copy Markdown
Member

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

@zardus

zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

How much of a corpus's failure surface this removes, measured rather than
argued.

Sample. 12,000 objects drawn uniformly at random, from a seeded permutation,
out of a 624,920-object internal corpus of compiler- and vendor-produced
binaries; 11,989 were retrievable and probed. Every object carries a catalogue
recipe naming its backend, and 537 of them name it ar, coff or universal2.
Rates carry 95% Wilson intervals.

Method. Each object is loaded with its declared recipe passed through
verbatim — main_opts={"backend": "ar"} and so on — and CFGFast is taken as
far as the first failure. The same 1,018 objects are probed against master
(eac0e5540516b9199dd6a91933e80dc774ea3eac) and against this branch's head
(1ed92cc97c03e81b5281b87bb2154dcdc81fa2fb) in one environment, so before and
after are the same objects. The comparison is keyed on exception type and
function rather than on file:line, because a patch that edits the failing file
moves every line below its hunk.

Before. 537 / 11,989 = 4.48% of the sample (CI 4.12–4.86) never reach a
parser: CLEError: Invalid backend: ar (372), : coff (103), : universal2
(62). cle registers 20 of its 23 backends in lower case and these three in mixed
case, so anything that writes backend names in lower case — a catalogue, a
config file, a database column — is refused by name before a byte is read.

After. All 537 resolve, and 261 of them go all the way to CFG — 2.18% of
the sample (CI 1.93–2.45): 183 static archives, 67 COFF objects and 11
Universal 2 binaries; 146 Linux, 67 Windows, 16 OpenBSD, 12 OpenIndiana, 10
macOS; 152 64-bit and 109 32-bit.

Residual. The other 276 get past the name and stop further in, on defects
this change exposes rather than causes: 158 AttributeError: 'ArchiveFileData' object has no attribute 'fileno' from the UEFI backend, 51
CLECompatibilityError: Unsupported Mach-O file type: 8, 36
NotImplementedError: Unsupported machine type in COFF, 23 CLEOperationError: Jump target out of range for reloc R_ARM_THM_CALL, 4 Ran out of room in address space, 4 ArchNotFound.

Case folding also does not reach a separate group of 276 objects whose declared
backend name matches no cle backend under any capitalisation: 246 ihex (cle
registers the Intel HEX backend as hex), 13 apj-container, 5
esp-flash-application, 5 esp-flash-image, 3 uf2, 3 libdragon-dso, 1
raw-bin. The 246 ihex ones do load once the name is translated to hex
222 of them reach CFG — so what is left for cle after this change is the 30
whose format has no backend at all.

Control. 481 objects that already reached CFG on master and do not declare
one of these three names are unchanged on this head — 0 of 481 differ.

The corpus is not redistributable, so its objects are described by architecture,
format and OS rather than named. One object in the sample is byte-identical to a
file tracked in angr/binaries and shows the ihex residual on a public path:
binaries/tests/armhf/decompiler/06aa650f61d71744c6709c7c092d9169.hex gives
CLEError: Invalid backend: ihex on this head as on master, while hex
resolves.

session: sharpen

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