Skip to content

feat(ghidra): admit RISC-V ilp32f ELF with a derived calling convention - #8401

Open
Entrpi wants to merge 20 commits into
morluto:mainfrom
Entrpi:feat-ghidra-riscv-ilp32f
Open

Entrpi wants to merge 20 commits into
morluto:mainfrom
Entrpi:feat-ghidra-riscv-ilp32f

Conversation

@Entrpi

@Entrpi Entrpi commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #8229 (merged) under decision D3, decided on #7477: option 1, a written calling-convention policy per ABI, ilp32 first, then ilp32f. This PR adds the ilp32f row. Review from 244c42306, the first commit: it records the row in the policy table and the decision record, and ilp32f sessions stay refused until the next commit implements it in the bridge.

Part of the embedded firmware roadmap #7955.

Summary

ESP32-P4, H4 and S31 builds use the single-float ABI ilp32f, which REA refused at the RISC-V lane's float-ABI check. Ghidra's RV32 compiler specification models ilp32d, which passes a double in a float register. Limiting its float entries to 4 bytes is not enough: measured on Ghidra 12.1.4, that still joins struct { float; int; } into a1:a0, where ilp32f passes the float in fa0 and the integer in a0. With this PR, ELF32 images that declare ilp32f and the F extension open in a new lane with a derived __rea_ilp32f prototype, and every argument of the fixture's calls sits where LLVM put it.

rea function h4.elf rea_riscv_abi_single_caller --provider ghidra --json

opens images whose Tag_RISCV_arch lists only extensions that Ghidra 12.1.4 decodes, such as builds with ESP-IDF 5.5's -march for the preview ESP32-H4, rv32imafc_zicsr_zifencei. Images that also declare B, Zcb/Zcmp/Zcmt or an Espressif vendor extension stay refused, and the refusal names those extensions. As of ESP-IDF v6.1, that includes every ESP32-P4 build (xesploop and xespv, plus xesppie on 5.5 before rev 3) and the 6.x builds for H4 (B and xespdsp) and S31 (xesploop and xespv). One prototype cannot express every ilp32f shape on Ghidra 12.1.4, so when an image's typed signatures use a shape __rea_ilp32f would misplace, REA refuses its analysis and names up to eight signatures.

Closes #7477. Refs #8229, #2063. Coordinates with #4811 (RISC-V PIE), which edits the same admission and profile functions.

Decision record: the ilp32f row of D3

D3 is decided on #7477; this row is the follow-up it names. docs/ghidra-riscv.md#ilp32f keeps the full policy.

  • Choice.
    • Lane elf32-exec-le-single-float-generic-v1, policy ilp32f-prototype-v1, prototype __rea_ilp32f, on the pinned RISCV:LE:32:default language with gcc. Admission also requires f in Tag_RISCV_arch.
    • __rea_ilp32f is one more row of feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention #8229's derivation in ReaGhidraRiscvCallingConvention.java. It limits the default float entries to 4 bytes, keeps fs0–fs11 preserved and keeps the shared rule that passes values of 9 bytes or more by reference. One new rule gives structures of up to 8 bytes Ghidra 12.1.4's join_dual_class with fillalternate: each 4-byte part that holds one float takes a float register, and each other part an integer register.
    • That rule decides by 4-byte part, not by the psABI's flattened fields. So it also splits struct { float; short; short; }, a float beside a pointer or a union (LLVM passes these in integer registers), and it cannot place _Float16 or a packed or over-aligned float structure. After assigning the prototype, the script compares the storage class it gives each signature type, as a first argument and as a return, with the psABI's, for every typed function and function-pointer type. If any differ, the bridge reports {policy, refused} with up to eight signatures, and REA refuses the session with that reason.
  • Alternatives (and what switching to each would take):
    • join_dual_class without the check: those signatures would get wrong storage without notice.
    • Correct typed functions with custom storage instead of refusing: more images are admitted, but the script becomes a second psABI allocator in Java, and a function and a pointer to it get different answers, because a function-pointer type cannot carry storage. Switching needs a new policy id.
    • Admit such images with the misplaced signatures marked unverified: the later widening. It admits images refused now and adds a per-signature limitation to the contract.
    • Keep refusing ilp32f: the lane row becomes a refusal again.
  • Costs of changing it. Code: S; one table row and its derivation row, plus the comparison. Once released, the single-float lane id and ilp32f-prototype-v1 persist in sessions, snapshots and Evidence; a changed derivation ships as a new policy id that invalidates that lane's snapshots only. Verification: npm run verify:ghidra:riscv.

Problem and expected behavior

  • Before: ELF32 e_flags 0x3 was refused: "…the single-float ABI (ilp32f) declared by e_flags has no verified calling-convention policy."
  • After: ELF32 single-float images that declare F and only decoded extensions are admitted with __rea_ilp32f; without F they are refused before any provider starts. An image whose signatures __rea_ilp32f would misplace opens, and its analysis is then refused with the signatures named. ilp32 and double-float behaviour and identity are unchanged.

Change and scope

  1. 244c42306: the ilp32f row of the decision record and policy table (with the required F), the session-report schema for {policy, refused}, docs, and fixture source with single-float-only shapes and a refused variant. ilp32f images pass admission, but until the next commit the bridge rejects the new policy id, so REA still refuses their sessions.
  2. 88c82b88a: the bridge row, its self-check (a float, double, two-float and float-and-int signature, and fs0–fs11 preserved), and the comparison with the psABI.
  3. 4f97b9202: the real lane: typed and stripped builds with ESP-IDF 5.5's ESP32-H4 -march, a refused unmodelled build, per-ABI storage tables, a call-site oracle that follows fa registers and per-byte stack memory, and header mutations.
  4. 6940199b4: the ilp32f limitation tests check facts, not wording.
  5. Merges of feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention #8229's heads (51419dca5, 5fd177413, 688c1d567, 2db1fa8a9); 688c1d567 takes feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention #8229's stripped return-storage check and records ilp32f's two stripped returns that differ (tagged at a0:4, flagged at a0:1). Merges of main up to 6e9ee97a6 (8acebd1f4, a5c020aae, ba174afe6); a5c020aae records feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention #8229's squash e669365c3 with -s ours, after checking that its git patch-id --stable equals that of d8ae325f1's own patch.

Not included: the shapes the check refuses (above); quad-float, RVE, TSO, ELF64 soft- or single-float, ET_DYN (#4811); and images that declare undecoded extensions (above).

Contract and boundary impact

  • Semantic owner and earliest changed stage: feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention #8229's owners (src/domain/riscvIsaDeclaration.ts, the tables in src/ghidra/GhidraRiscvProfile.ts, bridge/ghidra/ReaGhidraRiscvCallingConvention.java). The earliest changed stage is Ghidra admission: the lane row and its required F.
  • Contribution scope fit and linked maintainer decision: static analysis in the existing Ghidra RISC-V lane; no target execution, injection, mutation or protection recovery. No maintainer scope decision is required for static analysis in an existing lane (CONTRIBUTING); design decision D3, which names ilp32f as its follow-up, is recorded on [Design] Admit RISC-V ilp32 and ilp32f ELF in the Ghidra lane with a per-ABI calling-convention policy #7477.
  • CLI and MCP/tool-catalog contract: no new inputs or tools. ELF32 single-float images are admitted. New refusals: "Tag_RISCV_arch declares … without f, which the single-float ABI (ilp32f) declared by e_flags requires." before any provider starts, and, from the first analysis call, "RISC-V calling-convention policy ilp32f-prototype-v1 refused this image, so REA does not serve its analysis: … See docs/ghidra-riscv.md." The ELF32 float-ABI refusal now lists three admitted ABIs. A failed calling-convention handshake, MIPS soft-float included, now also carries its reason as the error's userMessage, not only as diagnostics.reason.
  • Provider, bridge, target-format, or platform compatibility: Ghidra on Linux and macOS (RISC-V is outside Windows Ghidra P0). The bridge script gains the ilp32f row; its session report may carry a refusal of at most 2,048 characters.
  • Evidence, artifact, provenance, or reconstruction contract: single-float profiles carry riscv_elf.float_abi: "single", riscv_elf.arch and riscv_calling_convention: ilp32f-prototype-v1; session Evidence states the prototype and its counts. ilp32 and double-float profiles are unchanged (tested).
  • Target selection, authority, upstream behavior, and cleanup details: as in feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention #8229. A refused policy ends the session before any analysis is served, with the normal cleanup.
  • Generated metadata (docs/public/product-catalog.json), package, or installation impact: none; the bridge source already ships.

Expected effects:

  • Read existing files or captured data.
  • Launch or attach to a process. (Ghidra, as before.)
  • Inject instrumentation or run scripts/code. (REA's fixed Ghidra scripts, as before; no target execution.)
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated: four test files; per-file coverage below.
  • Base reproduction or other evidence: source-derived, main refuses ELF32 e_flags 0x3 ("no verified calling-convention policy"). Measured on Ghidra 12.1.4 (recorded in feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention #8229's docs): 4-byte float entries alone put struct { float; int; } in a1:a0. The conformance checks refuse the stock model's placements: a whole double in fa0, and that structure joined into an integer pair.
  • User-visible CLI/MCP output: below, from the CLI on this PR's head.
  • Remaining proof gaps: no real-Ghidra run on macOS or Windows; no image built by GCC or ESP-IDF itself (the fixtures are LLVM builds with ESP-IDF 5.5's ESP32-H4 -march). float _Complex gets integer storage without notice, because Ghidra 12.1.4 imports it from DWARF as an untyped qword (the stock double-float lanes share this). With D hardware and the ilp32f ABI, Ghidra preserves all 64 bits of fs0–fs11, where the psABI preserves the low 32.
Per-file test coverage (counts on the head)
  • src/ghidra/GhidraRiscvProfile.test.ts (66 tests): the single-float lane row and where F is required; ilp32f admission and its refusals; the single-float profile's exact parameters; the stated ABI, prototype and ISA; and the launch with the single-float policy and its outcome.
  • src/ghidra/GhidraSessionValues.test.ts (40 tests): the ilp32 and ilp32f outcomes and an ilp32f refusal accepted. Refused: an unknown policy, a custom-storage count, an ilp32 refusal, a refusal with counts, and an empty or unbounded refusal.
  • tests/boundary/providers/ghidra/ghidraLauncher.test.ts (59 tests, 3 Windows-only skipped): the post-script for both policy ids.
  • tests/conformance/ghidra/riscv-checks.test.ts (12 tests): per-ABI tables and the call-site oracle across both callers, with fa registers, halfword stores and sub-word stack words. Also ilp32f storage, refusing a float-and-integer structure the wrong way round, the stock pair and a whole double in fa0. Stripped storage with ilp32f's recorded exceptions, refusing a stripped pair return that loses its high word. Both callers' pseudocode, refusing an unaccounted register value.
CLI output on this PR's head

For the single-float build of the fixture (-march=rv32imafc_zicsr_zifencei -mabi=ilp32f), the committed profile:

{"language_id":"RISCV:LE:32:default","compiler_spec_id":"gcc",
 "riscv_support_lane":"elf32-exec-le-single-float-generic-v1",
 "riscv_calling_convention":"ilp32f-prototype-v1",
 "riscv_elf":{"elf_class":32,"byte_order":"little","type":2,"flags":3,
   "float_abi_double":false,"rvc_declared":true,"rve_declared":false,
   "tso_declared":false,"float_abi":"single",
   "arch":"rv32i2p1_m2p0_a2p1_f2p2_c2p0_zicsr2p0_zifencei2p0_zmmul1p0"}}

Excerpts of both callers' pseudocode:

local_10 = rea_riscv_abi_pick(1.5,2.5,sel);
local_20 = rea_riscv_abi_mixed(local_c,4.75,5.5,6.125);
rVar2 = rea_riscv_abi_pair((rea_riscv_abi_pair)0x4118000041080000,10.5);
rVar3 = rea_riscv_abi_tagged((rea_riscv_abi_tagged)0xc41380000,local_c);
rVar2 = rea_riscv_abi_flagged((rea_riscv_abi_flagged)0x41b400000000002a,23.5);
fVar1 = rea_riscv_abi_fill(1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,(rea_riscv_abi_tagged)0x1c41dc0000,29.5
                          );
rVar3 = (*rea_riscv_abi_tagged_callback)((rea_riscv_abi_tagged)0x2041fc0000,sel);

The limitations include "RISC-V calling-convention outcome: 16 functions and 16 function definitions use __rea_ilp32f." The same source built with REA_RISCV_ABI_UNMODELLED opens, and its first analysis call fails:

{"code":"execution_failure",
 "message":"RISC-V calling-convention policy ilp32f-prototype-v1 refused this image, so REA does not serve its analysis: 2 functions and 1 function-pointer type pass by-value types whose psABI storage __rea_ilp32f cannot express: rea_riscv_abi_spread (returns rea_riscv_abi_spread, rea_riscv_abi_spread); rea_riscv_abi_half (returns _Float16, _Float16); function-pointer type _func_void_rea_riscv_abi_spread_int (rea_riscv_abi_spread). See docs/ghidra-riscv.md."}

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

Checks ran on the head ba174afe6, which merges main 6e9ee97a6.

  • macOS arm64 (Darwin 25.5.0), Node 24.22.1, npm 11.19.1:
    • npm run check:fast, format:check, docs:check (built steps executed) and verify:module-boundaries — passed.
    • npx knip — one finding, the unused exported type DarwinHelperCache (src/process/DarwinHelperCache.ts:465). knip on a built main 6e9ee97a6 prints the same output, so this PR adds none.
    • npm run test:focused -- 22 files: the 4 test files this PR changes, the other RISC-V test files, and tests of the code it shares with main's latest changes — 948 passed, 4 skipped (Windows-only).
  • Real Ghidra 12.1.4 in a Debian 13 container (Linux x86_64; OpenJDK 21.0.12, Node 24.22.1, Debian's LLVM 19.1.7; npm ci --ignore-scripts), on ba174afe6:
    • npm run verify:ghidra:bridge-sources — passed: javac 21.0.12 compiles all 11 bridge sources against Ghidra's 195 jars.
    • npm run verify:ghidra:riscv — passed for both ABIs: "mocked": false, cleanup complete, process lineage verified with no descendants. The typed ilp32f build has the compiler's storage at all 13 calls of both callers, the stripped builds return where the tables say, and the unmodelled build is refused with its three signatures named. Per fixture below.
    • npm run verify:ghidra:mips — passed, run because this PR changes the shared handshake's failure message: all six fixtures, including the soft-float pair with __rea_soft_float; process lineage verified with no descendants.
  • Both hosts' installs differ from the lockfile only in the transitive fast-uri 3.1.8 and qs 6.16.0 (lockfile: 3.1.9 and 6.16.1).
  • Not run: real Ghidra on macOS or Windows (the macOS host has no Ghidra 12.1.4, only JDK 25).
Lane results per fixture on ba174af
  • RV32 and RV64 double-float: the unchanged lanes. RV32 relabelled 0x1 and 0x3 is admitted over MCP and the CLI through the soft- and single-float lanes; RV64 0x1 (lp64), 0x3 (lp64f) and 0x80000005 are refused.
  • RV32 ilp32, typed and stripped (ESP32-C3's -march): unchanged from feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention #8229. 13 functions, with 12 and 0 function definitions; the soft-float image relabelled 0x3 lacks F and is refused.
  • RV32 ilp32f, typed (ESP-IDF 5.5's ESP32-H4 -march=rv32imafc_zicsr_zifencei): the single-float lane and ilp32f-prototype-v1. fence.i, csrr, fmv.w.x and fmv.x.w decode. Every inspect_native_api prototype uses __rea_ilp32f with the table's storage, for example fa0:4,a0:4 for the flagged structure, a0:4,fa0:4 for the tagged one, and the integer pair a1:4,a0:4 for a double. 16 functions and 16 function definitions use the prototype; 0x80000003 is refused.
  • RV32 ilp32f, stripped: the same allocated bytes. Ghidra's inferred storage matches the table: one word per location, except the recorded tagged, flagged and fill. Returns are in the typed registers, except tagged at a0:4 and flagged at a0:1, which leave their float in fa0. 16 functions and 0 function definitions.
  • Unmodelled build: admitted by its headers, then refused over MCP and the CLI with the bridge's reason, which names its three signatures.
  • Call-site oracles: LLVM 19.1.7 llvm-objdump places every expected word for the 10 ilp32 calls and the 13 ilp32f calls, in integer and float registers and on the stack.
  • Snapshots save and replay, CLI and MCP agree, and source bytes are unchanged for every fixture.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. Admission widens; the changed refusal texts are listed above.
  • Real Hopper/Ghidra, browser, or OS coverage: real Ghidra 12.1.4 on Linux x86_64 only; no Hopper.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: no new authority.

Author readiness

Human owner:

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.
Entrpi added 20 commits October 28, 2026 10:36
Decision D3 (morluto#7477), provisional: admit ELF32 soft-float (ilp32) RISC-V
images through a lane that keeps the pinned RISCV:LE:32:default language and
corrects the calling convention in the bridge.

This commit is the contract only; admission is unchanged.

- The domain owns the e_flags float-ABI vocabulary and parses
  .riscv.attributes and Tag_RISCV_arch into a declared, absent or
  unavailable ISA declaration.
- The Ghidra profile owns the lane table (class and float ABI), lane ids
  built from fields, the ilp32-prototype-v1 policy and its __rea_ilp32
  prototype, and the extensions Ghidra 12.1.4 decodes with p-code.
- The session handshake accepts the bridge's calling-convention report.
- docs/ghidra-riscv.md keeps the decision record and per-ABI policies for
  ilp32 and ilp32f; a test keeps it in step with the tables.
- tests/conformance/c/riscv-float-abi.c is the source the policies were
  measured on.
ESP32-C2, C3, C5, C6, C61, H2 and H21 builds are ELF32 soft-float (ilp32)
images, which the RISC-V lane refused at its double-float check. Ghidra
12.1.4's generic RV32 compiler specification models ilp32d, so its stock
prototype would read float and double arguments from fa0-fa7.

- Target resolution reads the one SHT_RISCV_ATTRIBUTES section through the
  same bounded ELF declaration reader as range validation, and records
  Tag_RISCV_arch as declared, absent or unavailable.
- Admission selects a lane from the class and float-ABI table. The soft-float
  lane also requires an RV32I Tag_RISCV_arch that declares only extensions
  Ghidra decodes with p-code; the refusal names any others (Zcb, Zcmp, Zcmt,
  Zb*, xesp*).
- Soft-float profiles commit float_abi, arch, riscv_calling_convention
  ilp32-prototype-v1 and the lane elf32-exec-le-soft-float-generic-v1.
  Double-float profiles are unchanged.
- ReaGhidraRiscvCallingConvention.java runs after analysis. It derives
  __rea_ilp32 from the default prototype without floating-point entries and
  rules, with fs0-fs11 killed by calls, self-checks its storage and assigns
  it to every non-thunk function, external function and function
  definition. Its report is written last.
- The provider client requires that report exactly when the profile commits
  the policy, before any operation, and keeps it as session limitations and
  caller-project history.

ELF32 single-float (ilp32f), quad-float and ELF64 soft- or single-float
images stay refused, now with a reason built from the lane table.
verify:ghidra:riscv now also builds tests/conformance/c/riscv-float-abi.c
with ESP32-C3's -march=rv32imc_zicsr_zifencei -mabi=ilp32, typed with DWARF
and linked again with --strip-all. Every allocated section of the stripped
image must keep its address and bytes.

- riscv-checks.mjs holds the ilp32 storage of each fixture function together
  with the words the compiler's call site writes there. An LLVM objdump
  reading of the caller must find every word at its stated location: an
  unaligned named double, the a7/stack split, two floats in an integer pair,
  a large structure by reference, aligned variadic pairs and an indirect
  call.
- Through CLI and stdio MCP, the soft-float builds must report the soft-float
  lane and ilp32-prototype-v1, __rea_ilp32 with exactly that storage (one
  integer word per location when stripped), caller pseudocode that passes the
  source constants and names no floating-point register, the prototype
  outcome in retained Evidence, and decoded fence.i and csrr.
- The RV32 double-float image relabelled as soft-float, refused before, is
  admitted through the new lane. RV64 soft-float, single-float and unsupported
  bits are refused with their reasons.
- riscv-checks.test.ts runs the checks offline against recorded LLVM and
  Ghidra output, including Ghidra's stock ilp32d prototypes as a negative
  control.
The psABI passes any value wider than 2 x XLEN by reference, scalars
included. __rea_ilp32 converted only structures and unions over 8 bytes to
pointers, so a long double (16 bytes) was joined into a1-a4. Measured on
Ghidra 12.1.4, rea_riscv_abi_quad(long double value, int sel) read value
from a4:4,a3:4,a2:4,a1:4 and sel from a5, where clang passes a pointer in
a1 and sel in a2.

- ReaGhidraRiscvCallingConvention.java now has one derivation driven by a
  per-policy row (floating-point entry size, preserved floating-point
  registers). Every policy inserts a rule before the input list's final
  join that converts any value of 9 bytes or more to a pointer; a return
  that size already becomes a caller buffer. The self-check requires a
  16-byte value to pass by reference.
- The fixture gains rea_riscv_abi_quad. The call-site oracle now checks the
  words the by-reference pointers point at, for the large structure and the
  long double, and the recorded data comes from an LLVM 19 build.
- docs/ghidra-riscv.md: the decision record and the ilp32 policy row.

ilp32-prototype-v1 keeps its id: no release carries it yet.
Decision D3 (morluto#7477), provisional, gains its ilp32f row: ELF32 images that
declare the single-float ABI are admitted through the lane
elf32-exec-le-single-float-generic-v1 with policy ilp32f-prototype-v1 and
the __rea_ilp32f prototype, keeping the pinned RISCV:LE:32:default language.

- The lane table gains the row, and admission requires the F extension in
  Tag_RISCV_arch beside the decoded-extension check.
- docs/ghidra-riscv.md records how the one-float-one-integer rule is
  realised: the shared derivation's row limits float entries to 4 bytes,
  keeps fs0-fs11 preserved and adds Ghidra 12.1.4's join_dual_class with
  fillalternate for structures of up to 8 bytes. The prototype is the only
  thing that assigns storage: a check of every typed function and
  function-pointer type against the psABI refuses the image when one uses a
  shape the prototype misplaces (a float with two shorts, a pointer or a
  union, _Float16, packed or over-aligned float structures). float _Complex,
  which Ghidra imports as a qword, stays unmodelled. Custom storage for typed
  functions and an unverified-signature admission are recorded alternatives.
- The session report for ilp32f can carry the bridge's refusal instead of
  the outcome; the handshake turns it into the user-facing reason.
- tests/conformance/c/riscv-float-abi.c gains single-float-only shapes and a
  refused variant; the ilp32 build's code is unchanged.

Until the bridge implements the policy (next commit), its post-analysis
script rejects the new policy id and REA refuses ilp32f sessions.
ReaGhidraRiscvCallingConvention.java gains ilp32f-prototype-v1 as a row of
its one derivation: float entries limited to 4 bytes, fs0-fs11 preserved,
and structures that Ghidra's dual-class join splits between register files.
The ilp32 row and its checks are unchanged.

- For that row the derivation inserts, before the input and output lists'
  final join, a rule giving structures of up to 8 bytes join_dual_class
  with fillalternate. The shared rule still passes values of 9 bytes or
  more by reference.
- The self-check also pins the storage of a float, double, two-float and
  float-and-int signature, and requires fs0-fs11 preserved.
- After assigning the prototype to every function and function definition,
  the script compares the storage class it gives each signature type, as a
  first argument or return, with the psABI's from the flattened fields. It
  checks every typed function and every definition a pointer type refers
  to. When any signature uses a shape the prototype misplaces, the report
  carries a refusal naming up to eight signatures instead of the outcome,
  and REA refuses the session with that reason.

The launcher test covers both policy ids.
verify:ghidra:riscv now also builds tests/conformance/c/riscv-float-abi.c
with ESP32-H4's -march=rv32imafc_zicsr_zifencei -mabi=ilp32f, typed with
DWARF and linked again with --strip-all, and a typed build with
REA_RISCV_ABI_UNMODELLED that the bridge must refuse.

- riscv-checks.mjs holds per-ABI tables: the ilp32 and ilp32f storage of
  each fixture function with the words its caller's call site writes there
  (and, for by-reference values, the words the pointer points at), the
  callers, and the indirect calls. Stripped builds record where Ghidra's
  inference differs from one word per typed location.
- The objdump call-site oracle now follows fa registers (flw, fsw, fmv.w.x)
  and tracks stack memory per byte, so word loads see byte and halfword
  stores, across both callers.
- Through CLI and stdio MCP the single-float builds must report the lane,
  ilp32f-prototype-v1 and __rea_ilp32f with the table's storage, including
  float-and-integer structures both ways round, float registers running
  out, long double by reference and an indirect float-and-integer call.
- riscv-isa-probe.S adds fmv.w.x and fmv.x.w when the build has F.
- The unmodelled build opens, then its first analysis call over MCP and the
  CLI fails with the bridge's reason naming its three signatures: a
  function passing a three-field structure, a _Float16 function and a
  function-pointer type passing the three-field structure.
- Header mutations: RV32 double-float relabelled single-float is admitted;
  the soft-float image relabelled single-float lacks F and is refused; RV64
  lp64f and high bits are refused.
The limitation tests froze a full explanatory sentence, the note count and
array positions, so a harmless rewording failed them: the pattern Greptile
flagged on morluto#7486. Assert the facts the notes must carry instead: the pinned
language, the static-linkage boundary, one note naming the declared ABI,
the ABI the pinned language models and the prototype REA applies, and the
declared ISA.
Like the ilp32 tests, the ilp32f test froze its explanatory sentence, the
note count and array positions. Assert one note naming the declared ilp32f
ABI, the modelled ilp32d and the applied `__rea_ilp32f` prototype, and the
declared ISA.
Takes main d48e472, which includes the squash merges of morluto#7481-morluto#7485.

Conflict, resolved: GhidraRiscvProfile.test.ts imports this branch's lane
and policy tables plus ghidraRiscvUnsupportedReason, which morluto#7484's
refusal-table assertions use; embedded-integration holds the same list.
morluto#7484's readback test merges cleanly and appears once.
The stripped branch of assertRiscvIlp32Storage compared parameter storage
only, so an inferred return that lost the high word of a register pair,
such as rea_riscv_abi_wide at a0:4 instead of a1:4,a0:4, still passed.

Stripped returns must now use the typed build's storage, or none when the
result goes through caller memory. Recovered type names are still not
compared: a test accepts other type names and refuses the dropped high word.
Takes ilp32 d02fc6b: main d48e472 and the check of stripped return
storage.

Conflicts, resolved by keeping both rules:
- riscv-checks.mjs: stripped functions get one inferred word per parameter
  location and the typed return storage (none for caller-memory results),
  unless the table records what Ghidra infers instead. The table's
  `stripped` is now { return, parameters }; the recorded ilp32f output
  gives tagged a0:4 and flagged a0:1, which leave their float in fa0.
- docs/ghidra-riscv.md: ilp32f's text for both builds, with the stripped
  return storage.

Semantic fixes: the new return-storage message names the function, not the
ABI; the ilp32 test of it uses assertRiscvStorage; an ilp32f test refuses a
stripped pair return that loses its high word, through a helper that keeps
the ilp32f block under oxlint's 150-line limit.
Takes ilp32 d8ae325, which records decision D3 as decided in morluto#7477. The merge is clean.
e6d0618 (morluto#8228) is the parent of morluto#8229's squash e669365, which the next merge records. The merge is clean.
Recorded with -s ours: this branch already holds morluto#8229's final head d8ae325, and git patch-id --stable of the squash equals that of d8ae325's own patch (7bc824d8360616b7).
The merge is clean. Main's changes to GhidraRiscvProfile.test.ts (morluto#8295), GhidraProviderClient.ts and docs/testing.md touch other regions than this branch.
@greptile-apps

greptile-apps Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5 Tier: apex

[Medium impact] The changes affect Ghidra's RISC-V analysis workflow and its calling-convention handshake.

The PR appears safe to merge, with two non-blocking improvements to the new startup scan.

Findings

  1. P2 Empty arrays slow startup ▶
  2. P2 Discarded descriptions consume memory ▶
Fix with agent prompt
### Issue 1
bridge/ghidra/ReaGhidraRiscvCallingConvention.java:438-440
If an imported array contains empty `Structure` elements, `flatten` revisits every element without adding any fields. The two-field cutoff never stops that case, so a large array count can cause millions of identical visits during startup. Classify the element type once and combine its field count with the array length instead of expanding every element.

### Issue 2
bridge/ghidra/ReaGhidraRiscvCallingConvention.java:346
The eight-example limit applies only after `refused` stores a full description of every rejected signature. Large typed images with repeated unsupported types or long C++ names therefore retain descriptions that will never be shown. Keep the totals separately and collect at most eight already-shortened examples while scanning.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds an ELF32 single-float lane using __rea_ilp32f, requires F in the declared ISA, and refuses analysis when typed signatures do not match the chosen calling convention.

  • Eligible ELF32 ilp32f images enter the Ghidra lane.
  • Ghidra applies an ilp32f prototype and refuses mismatched signatures.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["ELF32 declares single-float ABI"] --> B{"F and supported extensions declared?"}
  B -->|No| C["Refuse before provider startup"]
  B -->|Yes| D["Install __rea_ilp32f"]
  D --> E["Compare typed signatures"]
  E -->|Mismatch| F["Report refused signatures"]
  F --> G["Stop before serving analysis"]
  E -->|Match| H["Report applied policy and counts"]
  H --> I["Serve analysis"]
Loading

Reviews (1) · Last reviewed commit: "Merge upstream main 6e9ee97a6 into feat-..." · Reviewed by Greptile

Comment on lines +438 to +440
if (type instanceof Array array) {
for (int index = 0; index < array.getNumElements(); index += 1)
if (!flatten(array.getDataType(), fields) || fields.size() > 2) return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Empty arrays slow startup

If an imported array contains empty Structure elements, flatten revisits every element without adding any fields. The two-field cutoff never stops that case, so a large array count can cause millions of identical visits during startup. Classify the element type once and combine its field count with the array length instead of expanding every element.

Prompt To Fix With AI
This is a comment left during a code review.
Path: bridge/ghidra/ReaGhidraRiscvCallingConvention.java
Line: 438-440

Comment:
**Empty arrays slow startup**

If an imported array contains empty `Structure` elements, `flatten` revisits every element without adding any fields. The two-field cutoff never stops that case, so a large array count can cause millions of identical visits during startup. Classify the element type once and combine its field count with the array length instead of expanding every element.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
misplaced.add(parameter.getFormalDataType().getName());
if (misplaced.isEmpty()) continue;
functions += 1;
refused.add(function.getName(true) + " (" + String.join(", ", misplaced) + ")");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Discarded descriptions consume memory

The eight-example limit applies only after refused stores a full description of every rejected signature. Large typed images with repeated unsupported types or long C++ names therefore retain descriptions that will never be shown. Keep the totals separately and collect at most eight already-shortened examples while scanning.

Prompt To Fix With AI
This is a comment left during a code review.
Path: bridge/ghidra/ReaGhidraRiscvCallingConvention.java
Line: 346

Comment:
**Discarded descriptions consume memory**

The eight-example limit applies only after `refused` stores a full description of every rejected signature. Large typed images with repeated unsupported types or long C++ names therefore retain descriptions that will never be shown. Keep the totals separately and collect at most eight already-shortened examples while scanning.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
@morluto
morluto marked this pull request as ready for review November 1, 2026 04:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T04:34:34.198595Z ba174af Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba174afe69

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (component.getLength() == 0) continue;
if (component.getDataType() instanceof BitFieldDataType bitField) {
if (bitField.getBitSize() == 0) continue;
if (bitField.getBaseDataType().getLength() > 4) return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use bit-field width when checking the XLEN limit

For a typed ilp32f parameter or return such as struct { float f; unsigned long long b : 1; }, the 1-bit field satisfies the XLEN limit even though its declared base type occupies 8 bytes. Local Clang 17 lowers this parameter to float, i32, using fa0 and a0. This check instead makes psabiKind report INTEGER, so misplacedSignatures rejects a correct FLOAT_INTEGER placement and refuses the entire image. Bound the field using getBitSize() rather than its base datatype’s length, and cover this shape in the ABI regression fixture.

AGENTS.md reference: AGENTS.md:L84-L84

Useful? React with 👍 / 👎.

@Entrpi

Entrpi commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

Disclosure: this PR's H4 evidence was measured while Ghidra 12.1.4 loaded ESP-IDF's .flash.text into an overlay address space, which its DWARF import does not reach (#8461). The signature refusal check therefore saw DWARF types for IRAM functions only: 272 of H4's 278 DWARF-described flash functions had no imported signature. The fix, #8464, moves flash code into the default address space; once it lands, this branch will merge it and measure H4 again.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant