Skip to content

Load ARM64 and ARMNT COFF objects - #724

Open
zardus wants to merge 1 commit into
masterfrom
feature/fix-cle-coff-machines
Open

Load ARM64 and ARMNT COFF objects#724
zardus wants to merge 1 commit into
masterfrom
feature/fix-cle-coff-machines

Conversation

@zardus

@zardus zardus commented Aug 9, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

The COFF backend accepted only I386 and AMD64, so an ARM64 or ARMNT object failed
autodetection with "Unable to find a loader backend", and naming the backend
raised NotImplementedError, which is not a CLEError.

Both load now, with the relocation types those objects carry. ARMNT code is
Thumb-2 only, so its function symbols and extern stubs carry the Thumb bit.
ADDR32NB and ADDR64 also apply the addend held in the patched field, which
x86_64 .pdata needed already: without it a RUNTIME_FUNCTION got the same
begin and end address.

The regression tests load the ARM64, ARMNT and R4000 COFF objects that
angr/binaries master already carries, rather than assembling a container of
their own, so nothing here waits on a fixture change. angr.Project on these
objects additionally needs a Win32 syscall convention angr lacks for AArch64 and
ARM; that gap is not addressed here and cle.Loader is unaffected.

Validation: #724 (comment)

@zardus

zardus commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 42ac276321bc6a228ec2f8c4a04552c3b481c4e3 against baseline 45c6509c753d07f740099035cd41f7f473dc6f31.

  • Focused: python -m pytest tests/test_coff.py — 9 passed on head.
  • Regression: reverting cle/backends/coff.py to the baseline and keeping the tests fails 4 of those 9. test_arm64 and test_armnt with CLECompatibilityError: Unable to find a loader backend for .../tests/aarch64/coff_reloc_arm64.obj and .../tests/armel/coff_reloc_armnt.obj, test_unsupported_machine with NotImplementedError: Unsupported machine type raised from CoffParser._parse where a CLEError is expected, and test_x86_64_pdata_addends with assert 5352 < 5352 on the first RUNTIME_FUNCTION of tests/x86_64/fauxware.obj. Restoring the file makes all 9 pass again.
  • Inputs: the tests load tests/aarch64/coff_reloc_arm64.obj, tests/armel/coff_reloc_armnt.obj, tests/mips/coff_r4000.obj and the three i386 COFF objects, all of them already on angr/binaries master, which is what cle CI checks out. Nothing in this PR assembles a container at run time.
  • Lint/type: pre-commit run --all-files — all hooks pass, no file rewritten. Merge-base comparison on the two changed files: pylint 10.00 -> 10.00, pyright badness 0.0 -> 0.0.
  • Workspace gate: cle only; no other repository is touched, and the test-input check passes for cle.

Reproducer, using clang 21.1.8 rather than a fixture so it can be rerun from source:

cat > t.c <<'EOF'
void ext_fn(void);
static void loc_fn(void) { }
void (*const table[])(void) = { ext_fn, loc_fn };
void caller(void) { ext_fn(); loc_fn(); }
void *get_ext(void) { return (void *)ext_fn; }
void *get_loc(void) { return (void *)loc_fn; }
EOF
clang --target=aarch64-unknown-windows-msvc -c -O1 -ffreestanding t.c -o arm64.obj
clang --target=thumbv7-unknown-windows-msvc  -c -O1 -ffreestanding t.c -o armnt.obj
python -c "import cle; cle.Loader('arm64.obj')"

On the baseline that last line raises CLECompatibilityError: Unable to find a loader backend, and so does the ARMNT object. On head both load, and every relocated field decodes to the right target:

Object Site Relocation Result
arm64.obj .text+0x4 BRANCH26 b to 0x500000, the extern ext_fn
arm64.obj .text+0x8, +0xc PAGEBASE_REL21, PAGEOFFSET_12A adrp/add pair forms 0x500000
arm64.obj .text+0x14, +0x18 PAGEBASE_REL21, PAGEOFFSET_12A pair forms 0x400104, the local loc_fn
arm64.obj .rdata+0x0, +0x8 ADDR64 0x500000 and 0x400104
armnt.obj .text+0x14 BRANCH24T displacement resolves to 0x500000
armnt.obj .text+0x1e, +0x30 MOV32T 0x500001 and 0x400105, Thumb bit set
armnt.obj .rdata+0x0, +0x4 ADDR32 0x500001 and 0x400105, Thumb bit set

The bit patterns follow lld-link's applyRelARM and applyRelARM64, including reading the in-place addend and scaling PAGEOFFSET_12L by the access size. The two objects the tests load reach further than the clang output above: between them they cover both PAGEOFFSET_12A and 12L, including the 128-bit vector encoding's scaling, non-zero in-place addends on ADD, ADDR64 and ADDR32NB, and a function whose address is only taken, which must still carry the Thumb bit.

Caveats:

  • Whether an undefined symbol is Thumb is decided from its COFF symbol type. MSVC types a referenced external as a function — strcmp in tests/x86_64/fauxware.obj is 0x20 — but clang leaves it untyped, so a clang-built object that only takes the address of an extern function still gets a pointer without the Thumb bit. Nothing in such an object distinguishes that symbol from data.
  • IMAGE_REL_ARM64_SECREL_LOW12A/HIGH12A and the other unlisted types are still logged and skipped, as unhandled x86 types already are.
  • Coff.is_compatible now also claims files beginning c4 01 or 64 aa; the only files in angr/binaries that do are the two COFF objects these tests load.
  • angr.Project on these objects raises KeyError: 'Win32' from SimWindows, which has no syscall convention for AArch64 or ARM. That is an angr-side gap and is not addressed here; cle.Loader is unaffected.
  • Found by a corpus sweep of Windows COFF objects, in which every ARM64 and ARMNT unit stopped at this check.

Corpus measurement of the open queue, 2026-08-15 — a prerequisite, not an independent recovery

Correcting the record. The open pull-request queue was scored against 733 objects drawn from a sweep's own failing units (35 error classes, 49 architectures, 16 containers), with each repository's current master as the baseline rather than the revisions the sweep pinned. Each object is loaded with auto_load_libs=False, use_sim_procedures=False and then run through CFGFast(normalize=True, data_references=False, resolve_indirect_jumps=True, force_complete_scan=False) with a 120-second timeout.

The class here is NotImplementedError from CoffParser._parse, 758 corpus units; 24 were measured.

Branches applied Objects that complete CFGFast
#724 alone 0 of 24
#724 + angr#6794 24 of 24

Alone, all 24 move from NotImplementedError to KeyError: 'Win32' out of SimWindows — precisely the angr-side gap the last caveat names, now with a count against it. With angr#6794 applied the whole sample completes, recovering between 1 and 22 blocks per object.

So the corpus effect of this change is real but conditional, and the record should say so: this PR is a prerequisite for the class rather than a change that clears any of it by itself. Loading is a separate claim from analysis, and the loading claim above — that cle.Loader opens these objects and every relocated field decodes to the right target — is unaffected by any of this.

@angr-bot

angr-bot commented Aug 9, 2026

Copy link
Copy Markdown
Member

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

@ltfish
ltfish force-pushed the feature/fix-cle-coff-machines branch 2 times, most recently from b1b16c3 to 6551c7a Compare August 10, 2026 19:09
The COFF backend rejected every machine type but I386 and AMD64. CoffParser
raised NotImplementedError, which is not a CLEError, and Coff.is_compatible
applied the same two-machine filter, so autodetection never reached the backend
either and loading an ARM64 or ARMNT object ended in "Unable to find a loader
backend". Both are ordinary output of Windows-on-ARM toolchains, and archinfo
resolves both to the architectures the PE backend already uses for them.

Both are accepted now. The machine check and is_compatible read the same
COFF_MACHINE_TO_ARCH_NAME table, and an unsupported machine raises
CLECompatibilityError naming the type it rejected instead of NotImplementedError
naming nothing.

Accepting the header alone does not make the object useful, so this also
implements the relocation types those objects carry. ADDR32, ADDR32NB, ADDR64,
REL32, SECREL and SECTION are the patch shapes the existing classes already
handle. BRANCH26, PAGEBASE_REL21 and PAGEOFFSET_12A/12L patch an immediate field
inside a single ARM64 instruction, and MOV32T and BRANCH24T patch the two
halfwords of a Thumb-2 instruction.

ARMNT code is Thumb-2 only, so function symbols carry the Thumb bit the way CLE
already reports Thumb code elsewhere, and so do the extern stubs that stand in
for undefined functions. Nothing in the object states this, so without it every
function address the backend reports is an ARM address.

ADDR32NB and ADDR64 ignored the addend held in the field they patch, unlike
DIR32, DIR32NB and REL32 beside them. The .pdata of the x86_64 object in the
binaries repository shows the cost: the second ADDR32NB of a RUNTIME_FUNCTION
record holds the function's end offset as its addend, so BeginAddress and
EndAddress were relocated to the same address. Both apply the addend now, which
the ARM64 objects need as well.

The tests load the ARM64, ARMNT and R4000 objects the binaries repository now
carries, relocate each one through cle.Loader and decode the patched fields back
out, rather than assembling a COFF container of their own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zardus
zardus force-pushed the feature/fix-cle-coff-machines branch from 6551c7a to 42ac276 Compare August 10, 2026 21:56
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