[PR #2151] [MERGED] feat(ghidra): admit RISC-V ELF analysis through a pinned generic lane #5061

Closed
opened 2026-10-31 01:01:53 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/2151
Author: @NIU-123370
Created: 10/12/2026
Status: ✅ Merged
Merged: 10/12/2026
Merged by: @morluto

Base: main ← Head: feat/ghidra-riscv-lane


📝 Commits (5)

  • 961cb75 feat(ghidra): admit RISC-V ELF analysis through a pinned generic lane
  • 323c416 fix(ghidra): bound the RISC-V lane by declared flags and document it
  • e03ead3 fix(merge): reconcile RISC-V metadata with main
  • 783cddb fix(ghidra): align RISC-V pinning with admission
  • 2a05881 test(ghidra): verify RISC-V CLI and MCP workflows

📊 Changes

25 files changed (+1293 additions, -72 deletions)

View changed files

📝 docs/firmware-analysis.md (+4 -2)
➕ docs/ghidra-riscv.md (+105 -0)
📝 docs/testing.md (+1 -0)
📝 package.json (+1 -0)
➕ scripts/verify-real-ghidra-riscv.mjs (+391 -0)
➕ src/domain/binaryTarget.riscv.test.ts (+98 -0)
📝 src/domain/binaryTarget.ts (+45 -34)
📝 src/domain/binaryTargetTypes.ts (+48 -7)
📝 src/domain/peMachine.ts (+2 -2)
📝 src/domain/rawBinary.ts (+5 -2)
📝 src/ghidra/GhidraAnalysisProfile.ts (+27 -7)
📝 src/ghidra/GhidraProvider.ts (+1 -0)
📝 src/ghidra/GhidraProviderClient.ts (+4 -2)
📝 src/ghidra/GhidraPspProfile.ts (+3 -0)
📝 src/ghidra/GhidraRawBinaryLanguage.ts (+2 -2)
➕ src/ghidra/GhidraRiscvProfile.test.ts (+256 -0)
➕ src/ghidra/GhidraRiscvProfile.ts (+129 -0)
📝 src/hopper/HopperAnalysisProfile.ts (+15 -6)
📝 src/hopper/HopperMachOImage.ts (+5 -1)
📝 src/hopper/HopperProvider.ts (+8 -3)

...and 5 more files

📄 Description

Summary

Adds RISC-V ELF admission for the Ghidra provider, closing the reported target_unavailable: unsupported ELF architecture failure (#2063) for RV32 and RV64.

The lane is deliberately bounded and pinned:

  • EM_RISCV (243) becomes a provider-neutral family that carries its own ELF facts (elfClass, byteOrder, e_type, raw e_flags) alongside MIPS.
  • Admitted shape: little-endian ELF32/ELF64 ET_EXEC declaring EF_RISCV_FLOAT_ABI_DOUBLE, pinned to Ghidra's RISCV:LE:32:default / RISCV:LE:64:default with the gcc compiler specification.
  • Everything else is refused with a named reason rather than a guessed profile.

On the two questions in the issue: I found no one else working on this — no open PR referenced #2063, and main had no RISC-V branch in elfArchitecture() at 60259a9. I also know from the issue text that @desayVincent intended to implement this; if this shape is not the one you want, say so and I will drop it or rebase onto yours — I would rather this land as your design than mine. The measured language-selection finding below is offered either way.

Why the language is pinned, not auto-selected

Ghidra 12.1.4's Ghidra/Processors/RISCV/data/languages/riscv.opinion pins variant="default" for the 64-bit constraint, and leaves the 32-bit constraint with the in-file comment:

<constraint primary="243" processor="RISCV" endian="little" size="32"/> <!-- must choose between 'default' and 'AndeStar' variants -->

Measured on the installed release tree with analyzeHeadless against source-owned fixtures:

fixture Ghidra auto-selected
RV64 ELF, e_flags 0x5 (RVC, double-float ABI) RISCV:LE:64:default:gcc
RV32 ELF, e_flags 0x5 (RVC, double-float ABI), no Andes declaration RISCV:LE:32:AndeStar_v5:gcc

So the AndeStar result in the report is not specific to an Andes binary — it is what Ghidra chooses for an ordinary RV32 ELF. Accepting the header-based selection would silently analyze generic RV32 images under an Andes instruction set, so the lane states the language per width and refuses a caller-selected processor outside it.

Lane boundary

Declaration Outcome
LE, ELF64, ET_EXEC, float ABI double admitted, RISCV:LE:64:default + gcc
LE, ELF32, ET_EXEC, float ABI double admitted, RISCV:LE:32:default + gcc
big-endian refused — Ghidra 12.1.4 ships no big-endian RISC-V language
ET_REL, ET_DYN, other e_type refused with the declared value
EF_RISCV_RVE refused — 15-register ABI needs its own pspec/compiler verification
EF_RISCV_TSO refused — total-store-order memory model not verified here
soft / single / quad float ABI refused — the pinned generic cspec models double-float

Raw e_flags stay in Evidence as declarations (rvc_declared, rve_declared, tso_declared) and are not presented as instruction semantics.

One owner for the pinned import commitment

GhidraProviderClient compared the committed profile's language against a re-derived MIPS-only pin. That is fine while MIPS is the only pinned lane, but any second family committing a pin fails with The committed analysis profile and REA_GHIDRA_LANGUAGE_ID/REA_GHIDRA_COMPILER_SPEC_ID differ before the bridge starts — which is exactly what happened to this lane when it first ran. ghidraPinnedLanguage(target) is now the single owner used by both the profile commitment and the session readback, with a regression test asserting they agree for RV32 and RV64.

Relatedly, ten Exclude<BinaryArchitecture, "mips"> sites (peMachine, rawBinary, GhidraRawBinaryLanguage, Hopper profiles, binaryTarget/binaryTargetTypes) would each have needed a hand-added second member. They now read HeaderArchitecture, derived from ELF_METADATA_ARCHITECTURES / isElfMetadataArchitecture.

Hopper and IDA refuse RISC-V by name, matching the existing MIPS wording.

Verification

Host: macOS 15.7.7 arm64, Node 24.18.0 (.nvmrc), npm 11.16.0 (packageManager).

  • npm run check — passes (typecheck, lint + module boundaries, format:check, knip, metadata:check, architecture guards).
  • npm run test:fast — passes, 396 test files.
  • npm run verify:test-discovery — all 917 test files discovered exactly once.
  • Pre-existing MIPS/Hopper/IDA/language-override suites re-run unchanged: GhidraMipsProfile (41), GhidraLanguageOverride (18), HopperAnalysisProfile, binaryTarget (35), binaryTarget.mips (6) all pass.

Real provider: Ghidra 12.1.4 PUBLIC + JDK 21.0.12.1. Ghidra 12.1.4 ships no macOS decompiler (Decompiler/os/ contains only linux_x86_64 and win_x86_64), and its Makefile hard-codes -arch x86_64 on Darwin, so decompile and sleigh were built from the release sources for os/mac_arm_64; rea doctor then reports native_decompiler: ok matching darwin/arm64.

Fixtures were built locally for these runs from a small freestanding assembly probe with riscv64-elf-gcc 16.2.0 (-nostdlib -static -fno-pic -Wl,-e,rea_riscv_entry) — no third-party binaries, and the probe is not committed in this change because no verify:ghidra:riscv script consumes it yet.

command result
rea analyze rv64.elf --provider ghidra exit 0, provider Ghidra 12.1.4
rea analyze rv32.elf --provider ghidra exit 0, provider Ghidra 12.1.4
rea decompile rv64.elf rea_riscv_entry --provider ghidra exit 0, pseudocode returned
rea decompile rv32.elf rea_riscv_entry --provider ghidra exit 0, pseudocode returned

Committed profile readings from that evidence:

  • RV64: language_id: RISCV:LE:64:default, compiler_spec_id: gcc, language_selection: provider-pinned-v1, riscv_support_lane: elf64-exec-le-double-float-generic-v1, riscv_elf: {elf_class: 64, byte_order: "little", type: 2, flags: 5, rvc_declared: true}
  • RV32: same shape with RISCV:LE:32:default and elf32-exec-le-double-float-generic-v1

RV64 pseudocode:

void entry(void)
{
  gp = &__global_pointer_;
  rea_sum_table();
  return;
}

Refusal evidence on the same engine — a soft-float RV64 fixture (e_flags 0x1) is rejected before import with:

architecture_unsupported: This Ghidra RISC-V lane requires the EF_RISCV_FLOAT_ABI_DOUBLE declaration
(unspecified (soft-float convention) observed); the pinned generic language uses the double-float
compiler specification.

Not in this change

  • No verify:ghidra:riscv lane script. The runs above were performed manually against the installed engine. If you want the lane script in the shape of verify:ghidra:mips, I will add it here — I stopped rather than guess at the conformance-manifest contract.
  • Caller-supplied raw-binary entry keeps its existing four-family list; this is ELF-header admission only.
  • MCP-surface evidence for this lane is the shared CLI/MCP workflow plus unit tests; I did not run a live MCP session against the fixtures.
  • Andes, big-endian, RVE and TSO are refused, not supported.
  • Decompile output above is one function per fixture; I did not assert a call-graph or string-inventory completeness claim for RISC-V.

Follow-ups worth an issue if you want them: an ELF64 RV32-class mismatch shape (refused today by width-derived pinning, not by a dedicated reason), and any RISC-V relocation/PIE lane.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/2151 **Author:** [@NIU-123370](https://github.com/NIU-123370) **Created:** 10/12/2026 **Status:** ✅ Merged **Merged:** 10/12/2026 **Merged by:** [@morluto](https://github.com/morluto) **Base:** `main` ← **Head:** `feat/ghidra-riscv-lane` --- ### 📝 Commits (5) - [`961cb75`](https://github.com/morluto/rea/commit/961cb75e02b2352374195123375c7833a1954235) feat(ghidra): admit RISC-V ELF analysis through a pinned generic lane - [`323c416`](https://github.com/morluto/rea/commit/323c41656d854ccbf2dc973cf576a84bf1f717c6) fix(ghidra): bound the RISC-V lane by declared flags and document it - [`e03ead3`](https://github.com/morluto/rea/commit/e03ead34df739cdd17e8e21b30c3052a7499a59e) fix(merge): reconcile RISC-V metadata with main - [`783cddb`](https://github.com/morluto/rea/commit/783cddb229ff840bb9e91d366ba81a0d182a48f4) fix(ghidra): align RISC-V pinning with admission - [`2a05881`](https://github.com/morluto/rea/commit/2a05881bdb950aeceedf26455d46cd66d17721fb) test(ghidra): verify RISC-V CLI and MCP workflows ### 📊 Changes **25 files changed** (+1293 additions, -72 deletions) <details> <summary>View changed files</summary> 📝 `docs/firmware-analysis.md` (+4 -2) ➕ `docs/ghidra-riscv.md` (+105 -0) 📝 `docs/testing.md` (+1 -0) 📝 `package.json` (+1 -0) ➕ `scripts/verify-real-ghidra-riscv.mjs` (+391 -0) ➕ `src/domain/binaryTarget.riscv.test.ts` (+98 -0) 📝 `src/domain/binaryTarget.ts` (+45 -34) 📝 `src/domain/binaryTargetTypes.ts` (+48 -7) 📝 `src/domain/peMachine.ts` (+2 -2) 📝 `src/domain/rawBinary.ts` (+5 -2) 📝 `src/ghidra/GhidraAnalysisProfile.ts` (+27 -7) 📝 `src/ghidra/GhidraProvider.ts` (+1 -0) 📝 `src/ghidra/GhidraProviderClient.ts` (+4 -2) 📝 `src/ghidra/GhidraPspProfile.ts` (+3 -0) 📝 `src/ghidra/GhidraRawBinaryLanguage.ts` (+2 -2) ➕ `src/ghidra/GhidraRiscvProfile.test.ts` (+256 -0) ➕ `src/ghidra/GhidraRiscvProfile.ts` (+129 -0) 📝 `src/hopper/HopperAnalysisProfile.ts` (+15 -6) 📝 `src/hopper/HopperMachOImage.ts` (+5 -1) 📝 `src/hopper/HopperProvider.ts` (+8 -3) _...and 5 more files_ </details> ### 📄 Description ## Summary Adds RISC-V ELF admission for the Ghidra provider, closing the reported `target_unavailable: unsupported ELF architecture` failure (#2063) for RV32 and RV64. The lane is deliberately bounded and pinned: - `EM_RISCV` (243) becomes a provider-neutral family that carries its own ELF facts (`elfClass`, `byteOrder`, `e_type`, raw `e_flags`) alongside MIPS. - Admitted shape: little-endian ELF32/ELF64 `ET_EXEC` declaring `EF_RISCV_FLOAT_ABI_DOUBLE`, pinned to Ghidra's `RISCV:LE:32:default` / `RISCV:LE:64:default` with the `gcc` compiler specification. - Everything else is refused with a named reason rather than a guessed profile. **On the two questions in the issue:** I found no one else working on this — no open PR referenced #2063, and `main` had no RISC-V branch in `elfArchitecture()` at `60259a9`. I also know from the issue text that @desayVincent intended to implement this; if this shape is not the one you want, say so and I will drop it or rebase onto yours — I would rather this land as your design than mine. The measured language-selection finding below is offered either way. ## Why the language is pinned, not auto-selected Ghidra 12.1.4's `Ghidra/Processors/RISCV/data/languages/riscv.opinion` pins `variant="default"` for the 64-bit constraint, and leaves the 32-bit constraint with the in-file comment: ```xml <constraint primary="243" processor="RISCV" endian="little" size="32"/> <!-- must choose between 'default' and 'AndeStar' variants --> ``` Measured on the installed release tree with `analyzeHeadless` against source-owned fixtures: | fixture | Ghidra auto-selected | | --- | --- | | RV64 ELF, `e_flags 0x5` (RVC, double-float ABI) | `RISCV:LE:64:default:gcc` | | RV32 ELF, `e_flags 0x5` (RVC, double-float ABI), **no Andes declaration** | `RISCV:LE:32:AndeStar_v5:gcc` | So the AndeStar result in the report is not specific to an Andes binary — it is what Ghidra chooses for an ordinary RV32 ELF. Accepting the header-based selection would silently analyze generic RV32 images under an Andes instruction set, so the lane states the language per width and refuses a caller-selected processor outside it. ## Lane boundary | Declaration | Outcome | | --- | --- | | LE, ELF64, `ET_EXEC`, float ABI double | admitted, `RISCV:LE:64:default` + `gcc` | | LE, ELF32, `ET_EXEC`, float ABI double | admitted, `RISCV:LE:32:default` + `gcc` | | big-endian | refused — Ghidra 12.1.4 ships no big-endian RISC-V language | | `ET_REL`, `ET_DYN`, other `e_type` | refused with the declared value | | `EF_RISCV_RVE` | refused — 15-register ABI needs its own pspec/compiler verification | | `EF_RISCV_TSO` | refused — total-store-order memory model not verified here | | soft / single / quad float ABI | refused — the pinned generic cspec models double-float | Raw `e_flags` stay in Evidence as declarations (`rvc_declared`, `rve_declared`, `tso_declared`) and are not presented as instruction semantics. ## One owner for the pinned import commitment `GhidraProviderClient` compared the committed profile's language against a re-derived **MIPS-only** pin. That is fine while MIPS is the only pinned lane, but any second family committing a pin fails with `The committed analysis profile and REA_GHIDRA_LANGUAGE_ID/REA_GHIDRA_COMPILER_SPEC_ID differ` before the bridge starts — which is exactly what happened to this lane when it first ran. `ghidraPinnedLanguage(target)` is now the single owner used by both the profile commitment and the session readback, with a regression test asserting they agree for RV32 and RV64. Relatedly, ten `Exclude<BinaryArchitecture, "mips">` sites (`peMachine`, `rawBinary`, `GhidraRawBinaryLanguage`, Hopper profiles, `binaryTarget`/`binaryTargetTypes`) would each have needed a hand-added second member. They now read `HeaderArchitecture`, derived from `ELF_METADATA_ARCHITECTURES` / `isElfMetadataArchitecture`. Hopper and IDA refuse RISC-V by name, matching the existing MIPS wording. ## Verification Host: macOS 15.7.7 arm64, Node 24.18.0 (`.nvmrc`), npm 11.16.0 (`packageManager`). - `npm run check` — passes (typecheck, lint + module boundaries, `format:check`, knip, `metadata:check`, architecture guards). - `npm run test:fast` — passes, 396 test files. - `npm run verify:test-discovery` — all 917 test files discovered exactly once. - Pre-existing MIPS/Hopper/IDA/language-override suites re-run unchanged: `GhidraMipsProfile` (41), `GhidraLanguageOverride` (18), `HopperAnalysisProfile`, `binaryTarget` (35), `binaryTarget.mips` (6) all pass. Real provider: **Ghidra 12.1.4 PUBLIC + JDK 21.0.12.1**. Ghidra 12.1.4 ships no macOS decompiler (`Decompiler/os/` contains only `linux_x86_64` and `win_x86_64`), and its `Makefile` hard-codes `-arch x86_64` on Darwin, so `decompile` and `sleigh` were built from the release sources for `os/mac_arm_64`; `rea doctor` then reports `native_decompiler: ok` matching `darwin/arm64`. Fixtures were built locally for these runs from a small freestanding assembly probe with `riscv64-elf-gcc 16.2.0` (`-nostdlib -static -fno-pic -Wl,-e,rea_riscv_entry`) — no third-party binaries, and the probe is not committed in this change because no `verify:ghidra:riscv` script consumes it yet. | command | result | | --- | --- | | `rea analyze rv64.elf --provider ghidra` | exit 0, provider Ghidra 12.1.4 | | `rea analyze rv32.elf --provider ghidra` | exit 0, provider Ghidra 12.1.4 | | `rea decompile rv64.elf rea_riscv_entry --provider ghidra` | exit 0, pseudocode returned | | `rea decompile rv32.elf rea_riscv_entry --provider ghidra` | exit 0, pseudocode returned | Committed profile readings from that evidence: - RV64: `language_id: RISCV:LE:64:default`, `compiler_spec_id: gcc`, `language_selection: provider-pinned-v1`, `riscv_support_lane: elf64-exec-le-double-float-generic-v1`, `riscv_elf: {elf_class: 64, byte_order: "little", type: 2, flags: 5, rvc_declared: true}` - RV32: same shape with `RISCV:LE:32:default` and `elf32-exec-le-double-float-generic-v1` RV64 pseudocode: ```c void entry(void) { gp = &__global_pointer_; rea_sum_table(); return; } ``` Refusal evidence on the same engine — a soft-float RV64 fixture (`e_flags 0x1`) is rejected before import with: ``` architecture_unsupported: This Ghidra RISC-V lane requires the EF_RISCV_FLOAT_ABI_DOUBLE declaration (unspecified (soft-float convention) observed); the pinned generic language uses the double-float compiler specification. ``` ## Not in this change - **No `verify:ghidra:riscv` lane script.** The runs above were performed manually against the installed engine. If you want the lane script in the shape of `verify:ghidra:mips`, I will add it here — I stopped rather than guess at the conformance-manifest contract. - Caller-supplied raw-binary entry keeps its existing four-family list; this is ELF-header admission only. - MCP-surface evidence for this lane is the shared CLI/MCP workflow plus unit tests; I did not run a live MCP session against the fixtures. - Andes, big-endian, RVE and TSO are refused, not supported. - Decompile output above is one function per fixture; I did not assert a call-graph or string-inventory completeness claim for RISC-V. Follow-ups worth an issue if you want them: an ELF64 RV32-class mismatch shape (refused today by width-derived pinning, not by a dedicated reason), and any RISC-V relocation/PIE lane. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 01:01:53 +00:00
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#5061
No description provided.