Skip to content

Register the STM32 backend so CLE can load Cortex-M flash images - #716

Merged
twizmwazin merged 2 commits into
masterfrom
feature/stm32-backend
Aug 17, 2026
Merged

Register the STM32 backend so CLE can load Cortex-M flash images#716
twizmwazin merged 2 commits into
masterfrom
feature/stm32-backend

Conversation

@zardus

@zardus zardus commented Aug 9, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

cle/backends/stm32.py was never imported by cle.backends, so its register_backend call never ran and the backend stayed out of ALL_BACKENDS. Loading a raw Cortex-M flash image raised CLECompatibilityError instead.

Importing it exposed two more defects. is_compatible read from the stream's current position, but Loader._static_backend probes every backend with one shared stream and never rewinds it. And the entry point had the reset vector's Thumb bit masked off, which makes angr lift the reset handler as ARM.

The tests use the Nucleo-L152RE flash image already in binaries and check the entry point against the ELF build of the same firmware.

Validation: #716 (comment)

cle/backends/stm32.py was never imported by cle.backends, so its
register_backend("stm32", ...) call never ran and the backend stayed out of
ALL_BACKENDS. Loading a raw Cortex-M flash image raised CLECompatibilityError
instead of selecting the backend.

Import and export it alongside the other backends, and repair the two things
that were wrong once it became reachable. is_compatible read from the stream's
current position, but Loader._static_backend probes every backend with one
shared stream and never rewinds it, so the vector table check ran against the
tail of the previous backend's read and never matched. The entry point was the
reset vector with its Thumb bit masked off, which makes angr lift the reset
handler as ARM; the ELF build of the same firmware reports the odd address.

Also drop VectorTable.get_irq_handler, which read a self._data attribute the
structure never had, and raise CLECompatibilityError rather than a bare
ValueError when an image requested as "stm32" is too short for a vector table.
@zardus

zardus commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 9cf81983c83846bdd15eb90a6e3faa82086a4ddc against baseline b58ea02a446106647cdaae32bdf91b7062404cc1.

  • Regression: python -m pytest tests/test_stm32.py -q — 7 passed on head. On the baseline the fixture does not load at all: cle.Loader("binaries/tests/armel/i2c_master_read-nucleol152re.bin") raises CLECompatibilityError: Unable to find a loader backend, and "stm32" in cle.ALL_BACKENDS is False; on head it loads as STM32Backend with entry 0x8001695
  • Regression, per fix: reverting the is_compatible rewind to a tell()/seek(pos) pair fails 4 of the 7, starting with autodetection (CLECompatibilityError); restoring the Thumb-bit mask on the entry fails the ELF comparison (assert 134223508 == 134223509); dropping the CLECompatibilityError mapping fails the truncated-image test with a bare ValueError
  • Full suite: python -m pytest -q in cle — 209 passed, 9 skipped on head; 202 passed, 9 skipped on the baseline, so the delta is the 7 new tests
  • Backend selection: walked all 1698 files of angr/binaries at 58841bf0d9e71ca7b215f404ba61e1924c712906 through the Loader._static_backend probe order. tests/armel/i2c_master_read-nucleol152re.bin is the only file that resolves to STM32Backend; every other file keeps the backend it had, so is_default = True shadows nothing
  • Lint/type: merge-base pylint and pyright comparison over the changed files — cle/__init__.py, cle/backends/__init__.py, cle/backends/stm32.py all 10.00 -> 10.00, tests/test_stm32.py new file 10.00; pyright badness 0.0 -> 0.0 on all four
  • Hooks: pre-commit run over the changed files — passed, tree unchanged
  • Workspace gate: cle only; the other ecosystem suites are not selected because this branch touches one repository

End-to-end effect on the angr path, with angr 9.3.3.dev0:

import angr

TESTS = "binaries/tests/armel"
raw = angr.Project(f"{TESTS}/i2c_master_read-nucleol152re.bin", auto_load_libs=False)
elf = angr.Project(f"{TESTS}/i2c_master_read-nucleol152re.elf", auto_load_libs=False)
for p in (raw, elf):
    print(hex(p.entry), p.factory.block(p.entry).thumb)

On the baseline the first Project raises CLECompatibilityError. With the backend registered but the Thumb bit masked, it prints 0x8001694 False against the ELF's 0x8001695 True, and the reset handler lifts to unrelated ARM instructions. On head both lines read 0x8001695 True.

Caveats: the 9 skips are the pre-existing TODO markers in tests/test_macho_bindinghelper.py. The heuristic itself is unchanged — a 64-byte read, an initial stack pointer in 0x20000000..0x20100000, and a reset vector with the Thumb bit set — so images outside the common STM32 SRAM window still need main_opts={"backend": "stm32"}. VectorTable.get_irq_handler is broken on master and stays that way here; see the review thread.

@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_716

Comment thread cle/backends/stm32.py
"""Reset handler address with Thumb bit cleared"""
return self.reset_handler & (~1)

def get_irq_handler(self, irq_num: int) -> int:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't make this unrelated change in this PR. This is perhaps a real bug, but address it in a separate discreet PR and keep this one focused

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

You are right, it is unrelated. Restored in 9cf81983c83846bdd15eb90a6e3faa82086a4ddc, so the diff is now only the registration, the is_compatible rewind and the Thumb bit.

For whoever picks it up: get_irq_handler reads self._data, which VectorTable never sets — from_bytes builds the structure with from_buffer_copy, so calling it raises AttributeError. It also could not work as written even with the attribute, because from_bytes copies only the first 64 bytes and every IRQ vector starts at offset 64. Nothing in the tree calls it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you fix it and add test cases?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Done, in #763 against master.

from_bytes now keeps the image it was given rather than only the 64 bytes the ctypes fields cover, which is what get_irq_handler needs to reach a peripheral vector at all, and a negative IRQ number raises instead of silently returning a system exception vector. Three tests cover it, reading the Nucleo-L152RE image's IRQ vectors and checking them against Default_Handler in the ELF build of the same firmware; all three fail on master with the original AttributeError.

It is broken -- it reads a self._data attribute the structure never sets --
but that is not this change's subject.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@twizmwazin
twizmwazin merged commit a2ccdda into master Aug 17, 2026
19 checks passed
@twizmwazin
twizmwazin deleted the feature/stm32-backend branch August 17, 2026 17:16
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.

3 participants