[GH-ISSUE #5744] [Feature] Analyze Thumb-only Cortex-M firmware in the raw-binary lane instead of refusing its entry point and decoding it as ARM state #3491

Open
opened 2026-10-31 00:41:06 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @Keylessboi on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5744

Before submitting

  • I searched the open and closed issues and pull requests for a related proposal.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This request does not include credentials, private samples, or sensitive target data.

Area

Native analysis or decompilation

Current limitation

Thumb-only (Cortex-M) firmware cannot be analyzed through format=raw-binary: the architecture is not expressible, the required entry point is refused, and the only reachable ARM language decodes the bytes as ARM state and reports a confident-looking result anyway. Unlike #4246, which asks for a general caller-selected language and an independent base address, this is a specific denied target class where no combination of current inputs produces correct output, including the ones REA accepts.

Observed on this machine against REA 6.7.0 (Ghidra 12.1.2, bridge verified against 12.1.4), on a self-owned Cortex-M image (a headerless container whose segments are Thumb code, sha256 84615566c8cdd6ac0eb7fb1f83ac0f7407131c74c6055fa30369ccf708cf51e9):

  1. The architecture cannot be named. BINARY_ARCHITECTURES admits x86, x86_64, arm, arm64, 6502, m68k (src/domain/rawBinary.ts:12-19). There is no Thumb or Cortex-M value, so the caller cannot state what the image is.

  2. The lane rejects the entry point a Cortex-M image requires. src/domain/rawBinary.ts:113-120 refuses any arm entry not divisible by 4:

    $ rea analyze <image> --target-format raw-binary --architecture arm --entry-point 0x3757c5
    {"error":"Analysis failed","code":"invalid_request",...,"message":"Cannot open artifact: raw-binary arm entry_point 0x3757c5 must be 4-byte aligned.",...,
     "details":{"reason":"raw-binary arm entry_point 0x3757c5 must be 4-byte aligned","constraint":"load_selection_rejected"}}
    

    That address is not misaligned in any meaningful sense: it is a real reset-handler address from the image, and it is odd because bit 0 of a Cortex-M function pointer is the Thumb state bit, not part of the address. Masking entry & ~1 is what the hardware does. REA therefore refuses the one address the caller must supply, and the failure names the input as invalid rather than the lane as unable to represent the target.

  3. The accepted form silently produces wrong results with ordinary coverage claims. Forcing a 4-byte-aligned entry so the lane accepts the image succeeds, and fixes the language where the caller cannot change it:

    "language_id":"ARM:LE:32:v7","language_selection":"caller-supplied-v1","entry_point":"0x3757c4"
    "procedure_count":1816,"string_count":382
    

    ARM:LE:32:v7 is ARM state. The bytes at that entry are Thumb: b0 b5 0f 4c 0f 49 10 4d 00 22 20 46 a8 47 is, as Thumb, a clean 7-instruction prologue (push {r4,r5,r7,lr}, three ldr literal loads, movs, mov, blx). As ARM state, those same bytes do not decode at all. REA nevertheless returned an overview reporting 1816 procedures and 382 strings — a plausible-shaped result that is not a decoding of this binary. The only signal is prose buried in a long limitations list: "Thumb-only, Cortex-M, 65C02, 65816 … and other processor variants are unsupported in this increment." A caller reading procedure_count: 1816 has no reason to look for it.

REA_GHIDRA_LANGUAGE_ID does not help: it is refused for raw-binary (src/ghidra/GhidraAnalysisProfile.ts:110-118, cross-referenced in #4246), so arm cannot be redirected to the needed language. Ghidra already ships it — ARM:LE:32:Cortex is present in this installation's ARM.ldefs, alongside ARM:LE:32:v7 — so the gap is REA's admission, not the provider's capability.

This is the same shape as the denial in #1745 for 32-bit ARM/ARMv7: a firmware class that is not supported and cannot be worked around from the CLI or MCP.

Use cases

Firmware and bootloader images from Cortex-M parts are common and are reachable from a public entry point: appliance, router, and peripheral dumps, and consumer firmware bundles such as this one. The practical impact is not only that analysis is unavailable — it is that a wrong analysis is returned with the same envelope as a correct one. On a repair, recovery, or attribution task, a caller has no local way to distinguish "1816 procedures were recovered from this image" from "1816 procedures were invented by decoding Thumb bytes as ARM".

Measured here: the reset handler at the image's real entry is 7 Thumb instructions; the ARM-state pass decodes none of them.

Desired outcome and success criteria

For a Cortex-M raw image, a caller can state the family and entry so REA imports and decodes it correctly, with the Thumb state bit handled as hardware handles it.

  • architecture (or an equivalent) admits a Thumb / Cortex-M family — a distinct value, e.g. thumb or cortex-m, not a change to what arm means.
  • The entry address is masked to entry & ~1 before use as a code address, and bit 0 is not reported as a misalignment, because for this family it is a state bit.
  • The selected language for this family is one Ghidra declares Thumb: ARM:LE:32:Cortex is present in the installed catalog here and is the obvious candidate.
  • Success: on a self-compiled arm-none-eabi Cortex-M object linked and objcopy -O binary, entry at an odd address, functions, strings, and control flow are recovered and match what a human disassembler shows for the same bytes; the reset handler reads as a prologue, not as ARM.
  • Fail-closed: if the family cannot be honored, the import is refused with a diagnostic naming the unsupported family, rather than returning an overview that looks like decoded output.
  • Open: whether arm should gain a Thumb variant or stay strictly ARM state; and whether the review should key on the language rather than the family so a future family is a catalog entry rather than a code change.

Expected interaction with targets

Read existing files or captured data

Target and scope details (optional)

Caller-selected local headerless image; read-only import by a caller-configured Ghidra. No execution of the target, no network, no new provider. The specific concern is that the fix must not widen what arm means today, so images that are genuinely ARM state keep the current behavior and the current evidence claims. This request is only for the Thumb/Cortex-M class; the general language and base-address questions remain #4246.

Verification available so far

I have a prototype or observed results

Verification details (optional)

All of the following is observed, not predicted, on REA 6.7.0 / Ghidra 12.1.2, against a self-owned image:

  • The load_selection_rejected failure for entry 0x3757c5 and for 0x3757c6, quoted above.
  • A successful import at 0x3757c4 reporting language_id: "ARM:LE:32:v7" and procedure_count: 1816, with the unsupported-family sentence present only in the limitations array.
  • The byte-level check: b0 b5 0f 4c 0f 49 10 4d 00 22 20 46 a8 47 decodes to a 7-instruction Thumb prologue and to zero instructions as ARM state.

I can provide the byte range and the expected disassembly without the surrounding image, so a checker needs no sample from me. A verify-real-ghidra-thumb.mjs-style check could compile a tiny Cortex-M object with arm-none-eabi-gcc, objcopy -O binary it, and assert that an odd entry decodes to the expected prologue with a Thumb language.

Alternatives considered

  • architecture=arm with a rounded entry: accepted, wrong language, wrong result, no coverage flag. This is the failure being reported.
  • REA_GHIDRA_LANGUAGE_ID=ARM:LE:32:Cortex: refused for raw-binary.
  • execute_rizin_command with e asm.arch=arm; e asm.cpu=cortexm; e asm.bits=16: works and is what I used to decode the firmware, but it gives raw disassembly only — no functions, no decompiler, no Evidence — so it is not a substitute for the native lane, and it means the only correct path available to a caller is the one outside the lane being requested.
  • Treating the container as a header-based format: the container is not one REA recognizes, so this is not available.
  • #4245 and #4246: related and necessary, but neither addresses this class. #4245 asks how a headerless blob's architecture might be proposed, and #4246 asks for a general language passthrough plus an independent base address; under both, a Cortex-M image still has no admitted family and its odd entry is still refused as invalid input.

Possible direction

  • Add a thumb/cortex-m family to BINARY_ARCHITECTURES with its own entry rule (mask bit 0 rather than require 4-byte alignment) and its own language (ARM:LE:32:Cortex checked against the installed catalog at selection time).
  • Report the family and language in the overview so a caller can see which decode they got, and mark a defaulted or unsupported language as such rather than only in the limitations prose.
  • Prefer a catalog-validated language id as the profile key, which would also let #4246 and this request share one mechanism instead of adding a second per-family lane.

References

  • REA 6.7.0 source at the baseline read here: src/domain/rawBinary.ts:12-19 (admitted families), :113-120 (4-byte ARM alignment refusal), src/ghidra/GhidraRawBinaryLanguage.ts:14-15,47-52 ("Big-endian ARM, Thumb-only, Cortex-M … are out of scope"; arm → ARM:LE:32:v7), src/ghidra/GhidraAnalysisProfile.ts:110-118 (raw-binary language override refusal).
  • docs/raw-binary.md — lists Thumb-only and Cortex-M as unsupported.
  • Installed catalog: $GHIDRA_INSTALL_DIR/Ghidra/Processors/ARM/data/languages/ARM.ldefs declares ARM:LE:32:Cortex and ARM:BE:32:Cortex alongside ARM:LE:32:v7.
  • Related: #4246 (caller-selected language and base address), #4245 (architecture proposal for headerless blobs), #1745 (32-bit ARM/ARMv7 lane confirmation), #3532 (SH-4 lane), PR #4018 (big-endian m68k lane), #1747 (PowerPC REA_GHIDRA_LANGUAGE_ID precedent), #3483 (Xtensa ELF).
  • Search: open and closed issues were listed and searched locally for Thumb, Cortex-M, Thumb-only, raw-binary, language ID, and entry point alignment; no proposal or maintainer decision covering the Thumb-only class was found.
Originally created by @Keylessboi on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5744 ### Before submitting - [X] I searched the open and closed issues and pull requests for a related proposal. - [X] I understand the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope) must be reviewed before integration; open questions are welcome here. - [X] This request does not include credentials, private samples, or sensitive target data. ### Area Native analysis or decompilation ### Current limitation Thumb-only (Cortex-M) firmware cannot be analyzed through `format=raw-binary`: the architecture is not expressible, the required entry point is refused, and the only reachable ARM language decodes the bytes as ARM state and reports a confident-looking result anyway. Unlike #4246, which asks for a general caller-selected language and an independent base address, this is a specific denied target class where no combination of current inputs produces correct output, including the ones REA accepts. Observed on this machine against REA 6.7.0 (Ghidra 12.1.2, bridge verified against 12.1.4), on a self-owned Cortex-M image (a headerless container whose segments are Thumb code, `sha256 84615566c8cdd6ac0eb7fb1f83ac0f7407131c74c6055fa30369ccf708cf51e9`): 1. **The architecture cannot be named.** `BINARY_ARCHITECTURES` admits `x86`, `x86_64`, `arm`, `arm64`, `6502`, `m68k` (`src/domain/rawBinary.ts:12-19`). There is no Thumb or Cortex-M value, so the caller cannot state what the image is. 2. **The lane rejects the entry point a Cortex-M image requires.** `src/domain/rawBinary.ts:113-120` refuses any `arm` entry not divisible by 4: ``` $ rea analyze <image> --target-format raw-binary --architecture arm --entry-point 0x3757c5 {"error":"Analysis failed","code":"invalid_request",...,"message":"Cannot open artifact: raw-binary arm entry_point 0x3757c5 must be 4-byte aligned.",..., "details":{"reason":"raw-binary arm entry_point 0x3757c5 must be 4-byte aligned","constraint":"load_selection_rejected"}} ``` That address is not misaligned in any meaningful sense: it is a real reset-handler address from the image, and it is odd **because bit 0 of a Cortex-M function pointer is the Thumb state bit**, not part of the address. Masking `entry & ~1` is what the hardware does. REA therefore refuses the one address the caller must supply, and the failure names the input as invalid rather than the lane as unable to represent the target. 3. **The accepted form silently produces wrong results with ordinary coverage claims.** Forcing a 4-byte-aligned entry so the lane accepts the image succeeds, and fixes the language where the caller cannot change it: ```json "language_id":"ARM:LE:32:v7","language_selection":"caller-supplied-v1","entry_point":"0x3757c4" "procedure_count":1816,"string_count":382 ``` `ARM:LE:32:v7` is ARM state. The bytes at that entry are Thumb: `b0 b5 0f 4c 0f 49 10 4d 00 22 20 46 a8 47` is, as Thumb, a clean 7-instruction prologue (`push {r4,r5,r7,lr}`, three `ldr` literal loads, `movs`, `mov`, `blx`). As ARM state, those same bytes do not decode at all. REA nevertheless returned an overview reporting 1816 procedures and 382 strings — a plausible-shaped result that is not a decoding of this binary. The only signal is prose buried in a long limitations list: "Thumb-only, Cortex-M, 65C02, 65816 … and other processor variants are unsupported in this increment." A caller reading `procedure_count: 1816` has no reason to look for it. `REA_GHIDRA_LANGUAGE_ID` does not help: it is refused for raw-binary (`src/ghidra/GhidraAnalysisProfile.ts:110-118`, cross-referenced in #4246), so `arm` cannot be redirected to the needed language. Ghidra already ships it — `ARM:LE:32:Cortex` is present in this installation's `ARM.ldefs`, alongside `ARM:LE:32:v7` — so the gap is REA's admission, not the provider's capability. This is the same shape as the denial in #1745 for 32-bit ARM/ARMv7: a firmware class that is not supported and cannot be worked around from the CLI or MCP. ### Use cases Firmware and bootloader images from Cortex-M parts are common and are reachable from a public entry point: appliance, router, and peripheral dumps, and consumer firmware bundles such as this one. The practical impact is not only that analysis is unavailable — it is that a wrong analysis is returned with the same envelope as a correct one. On a repair, recovery, or attribution task, a caller has no local way to distinguish "1816 procedures were recovered from this image" from "1816 procedures were invented by decoding Thumb bytes as ARM". Measured here: the reset handler at the image's real entry is 7 Thumb instructions; the ARM-state pass decodes none of them. ### Desired outcome and success criteria For a Cortex-M raw image, a caller can state the family and entry so REA imports and decodes it correctly, with the Thumb state bit handled as hardware handles it. - `architecture` (or an equivalent) admits a Thumb / Cortex-M family — a distinct value, e.g. `thumb` or `cortex-m`, not a change to what `arm` means. - The entry address is masked to `entry & ~1` before use as a code address, and bit 0 is not reported as a misalignment, because for this family it is a state bit. - The selected language for this family is one Ghidra declares Thumb: `ARM:LE:32:Cortex` is present in the installed catalog here and is the obvious candidate. - Success: on a self-compiled `arm-none-eabi` Cortex-M object linked and `objcopy -O binary`, entry at an odd address, functions, strings, and control flow are recovered and match what a human disassembler shows for the same bytes; the reset handler reads as a prologue, not as ARM. - Fail-closed: if the family cannot be honored, the import is refused with a diagnostic naming the unsupported family, rather than returning an overview that looks like decoded output. - Open: whether `arm` should gain a Thumb variant or stay strictly ARM state; and whether the review should key on the language rather than the family so a future family is a catalog entry rather than a code change. ### Expected interaction with targets Read existing files or captured data ### Target and scope details (optional) Caller-selected local headerless image; read-only import by a caller-configured Ghidra. No execution of the target, no network, no new provider. The specific concern is that the fix must not widen what `arm` means today, so images that are genuinely ARM state keep the current behavior and the current evidence claims. This request is only for the Thumb/Cortex-M class; the general language and base-address questions remain #4246. ### Verification available so far I have a prototype or observed results ### Verification details (optional) All of the following is observed, not predicted, on REA 6.7.0 / Ghidra 12.1.2, against a self-owned image: - The `load_selection_rejected` failure for entry `0x3757c5` and for `0x3757c6`, quoted above. - A successful import at `0x3757c4` reporting `language_id: "ARM:LE:32:v7"` and `procedure_count: 1816`, with the unsupported-family sentence present only in the limitations array. - The byte-level check: `b0 b5 0f 4c 0f 49 10 4d 00 22 20 46 a8 47` decodes to a 7-instruction Thumb prologue and to zero instructions as ARM state. I can provide the byte range and the expected disassembly without the surrounding image, so a checker needs no sample from me. A `verify-real-ghidra-thumb.mjs`-style check could compile a tiny Cortex-M object with `arm-none-eabi-gcc`, `objcopy -O binary` it, and assert that an odd entry decodes to the expected prologue with a Thumb language. ### Alternatives considered - `architecture=arm` with a rounded entry: accepted, wrong language, wrong result, no coverage flag. This is the failure being reported. - `REA_GHIDRA_LANGUAGE_ID=ARM:LE:32:Cortex`: refused for raw-binary. - `execute_rizin_command` with `e asm.arch=arm; e asm.cpu=cortexm; e asm.bits=16`: works and is what I used to decode the firmware, but it gives raw disassembly only — no functions, no decompiler, no Evidence — so it is not a substitute for the native lane, and it means the only correct path available to a caller is the one outside the lane being requested. - Treating the container as a header-based format: the container is not one REA recognizes, so this is not available. - #4245 and #4246: related and necessary, but neither addresses this class. #4245 asks how a headerless blob's architecture might be proposed, and #4246 asks for a general language passthrough plus an independent base address; under both, a Cortex-M image still has no admitted family and its odd entry is still refused as invalid input. ### Possible direction - Add a `thumb`/`cortex-m` family to `BINARY_ARCHITECTURES` with its own entry rule (mask bit 0 rather than require 4-byte alignment) and its own language (`ARM:LE:32:Cortex` checked against the installed catalog at selection time). - Report the family and language in the overview so a caller can see which decode they got, and mark a defaulted or unsupported language as such rather than only in the limitations prose. - Prefer a catalog-validated language id as the profile key, which would also let #4246 and this request share one mechanism instead of adding a second per-family lane. ### References - REA 6.7.0 source at the baseline read here: `src/domain/rawBinary.ts:12-19` (admitted families), `:113-120` (4-byte ARM alignment refusal), `src/ghidra/GhidraRawBinaryLanguage.ts:14-15,47-52` ("Big-endian ARM, Thumb-only, Cortex-M … are out of scope"; `arm` → `ARM:LE:32:v7`), `src/ghidra/GhidraAnalysisProfile.ts:110-118` (raw-binary language override refusal). - `docs/raw-binary.md` — lists Thumb-only and Cortex-M as unsupported. - Installed catalog: `$GHIDRA_INSTALL_DIR/Ghidra/Processors/ARM/data/languages/ARM.ldefs` declares `ARM:LE:32:Cortex` and `ARM:BE:32:Cortex` alongside `ARM:LE:32:v7`. - Related: #4246 (caller-selected language and base address), #4245 (architecture proposal for headerless blobs), #1745 (32-bit ARM/ARMv7 lane confirmation), #3532 (SH-4 lane), PR #4018 (big-endian m68k lane), #1747 (PowerPC `REA_GHIDRA_LANGUAGE_ID` precedent), #3483 (Xtensa ELF). - Search: open and closed issues were listed and searched locally for Thumb, Cortex-M, Thumb-only, raw-binary, language ID, and entry point alignment; no proposal or maintainer decision covering the Thumb-only class was found.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3491
No description provided.