Repository navigation
Conversation
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.
[Medium impact] The PR changes analysis behavior in Ghidra's RISC-V lane and exposes calling-convention refusal reasons to Ghidra callers. Cache the new padding walks before merging to meet the repository's performance requirement. Findings
Fix with agent prompt### Issue 1
bridge/ghidra/ReaGhidraRiscvCallingConvention.java:554-560
For a chain of structures wrapping one `float`, `unexplainedBytes` calls `naturalAlignment` on every remaining chain. Doubling the nesting depth roughly quadruples this work during startup. `heldFields` does not cache either new result.
Cache both results per stripped `DataType`, or compute them with the existing per-type facts. This must be addressed before merging to satisfy the repository directive: “Avoid unnecessary work and copies.”
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryAdds an ELF32 single-float lane using
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[ELF32 single-float image] --> B{ISA declarations fit?}
B -->|No| C[Refuse before launch]
B -->|Yes| D[Run Ghidra analysis]
D --> E[Install __rea_ilp32f]
E --> F[Compare typed signatures]
F --> G{Any misplaced or unknown type?}
G -->|Yes| H[Report refusal and close session]
G -->|No| I[Serve analysis]
Reviews (13) · Last reviewed commit: "fix(ghidra): refuse ilp32f values whose ..." · Reviewed by Greptile |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
|
Disclosure: this PR's H4 evidence was measured while Ghidra 12.1.4 loaded ESP-IDF's |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Ghidra's DWARF import gives an encoding it does not map an untyped byte, word, dword or qword, so float _Complex, which takes two float registers, showed as an integer in a0 and a1. The policy cannot know such a value's class, so it now refuses any value of up to 8 bytes that holds one, alone or inside a structure. The session limitation no longer lists complex values.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Hey @Entrpi, thanks for this! The addition looks good. What's left: there is 1 unresolved review thread from Greptile, about "Padded complex arguments slip through". Greptile is at 4/5 on the current head, mainly over "Padded complex arguments slip through", so addressing that thread should bump it. Happy to get this in once that's sorted out. |
An over-aligned structure of one float _Complex is 16 bytes, so the 8-byte guard let it fall through to a reference, although GCC and Clang pass it in fa0 and fa1. Decide on the bytes the fields hold, which is what the float convention limits, not on the padded length.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
The held-bytes and untyped checks revisited a nested type for every member, so a chain of structures that each hold two of the last one cost about 2^29 calls before any size check. One cached walk now answers both, once per type.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Thanks! The padded-complex thread is fixed in |
Main moved docs/ghidra-riscv.md to docs/providers/. The branch's refusal and limitation texts and its decision-record test use the new path, and the testing-lanes table keeps main's project-coverage row with the branch's ilp32f RISC-V row.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 82a28e59ad
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Static checks failed on main 5553acf's own src/cli/coreNativeCommands.ts formatting, which main has since fixed.
DWARF omits unnamed bit-fields of nonzero width, which the psABI counts:
Clang 19 passes struct { float f; unsigned : 1; } in fa0 and a0, and a
following int in a1, while Ghidra sees one float in 8 bytes. A value
with a float whose structure has bytes that no member and no natural
alignment padding explain now has an unknown class, so the policy
refuses the image. Over-aligned structures of up to 8 bytes are refused
too. verify:ghidra:riscv's unmodelled build passes such a structure.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 823083f8c6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
TL;DR: I ran the 4 changed test files on head 823083f under three Node versions. All 4 files passed each time, with 175 tests passed and 3 skipped. In CI, one test failed in a file this PR does not change. I did not run the real Ghidra check.
The 4 files are The PR changes 18 files against the merge base. That is 4 test files and 14 others. I ran only the 4 test files. The head is 2 commits behind main b46f4fc. GitHub shows mergeable CI on the head, read live with
Not run
Test setupCommandsnpm ci --offline
npm run test:prepare
xvfb-run -a npm run test:focused -- src/ghidra/GhidraRiscvProfile.test.ts src/ghidra/GhidraSessionValues.test.ts tests/boundary/providers/ghidra/ghidraLauncher.test.ts tests/conformance/ghidra/riscv-checks.test.ts🤖 Written and tested with Claude Code (Sonnet 5.5, verified by Opus 5.5), posted with my OK 🦊 |
unexplainedBytes and naturalAlignment recomputed nested types on every
chain, so a deeper nesting cost quadratically more. Both now keep one
result per stripped DataType, as held() does.
The guide now states that an unnamed bit-field in natural padding, as in
struct { char c; unsigned : 1; float f; }, leaves no DWARF trace: Clang 19
passes it in a0 and a1 but the lane admits it with the float in fa0.
Refusing every padded float value would also refuse common layouts.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4748c92459
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| List<Boolean> fields = new ArrayList<>(); | ||
| // C passes no array by value, so an array here is a GNU vector, which Ghidra imports as one. | ||
| DataType value = strip(type); | ||
| if (!(value instanceof Union) && !(value instanceof Array) && flatten(type, fields)) { |
There was a problem hiding this comment.
Refuse transparent-union parameters after marker erasure
For an ilp32f signature such as typedef struct { float f; } F; typedef union { F f; int i; } U __attribute__((transparent_union));, Clang 17 lowers a U parameter as a float in fa0, while its DWARF formal type is only an ordinary union. This guard therefore assigns psabiKind as INTEGER, and __rea_ilp32f also places the imported union in a0, so misplacedSignatures sees agreement and admits the image with wrong storage; preserve the attribute at import or refuse unions whose erased first-member convention could use float registers.
AGENTS.md reference: AGENTS.md:L19-L19
Useful? React with 👍 / 👎.
A transparent union passes as its first member, but DWARF does not record
the attribute: Clang 19 passes union { struct { float f; } s; unsigned u; }
with transparent_union in fa0, while Ghidra and the psABI class see an
ordinary union in a0, so the lane admitted wrong storage. A union parameter
whose first member would take a float register now refuses the policy,
unless Clang would ignore the attribute: a floating-point first member, or
another member of a different size or alignment. Returns are unaffected.
verify:ghidra:riscv's unmodelled build passes one such union and three
ordinary ones.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 719f3d27dc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| DataType returned = function.getReturn().getFormalDataType(); | ||
| if (misplaced(model, returned, true)) misplaced.add("returns " + returned.getName()); | ||
| for (Parameter parameter : function.getParameters()) | ||
| if (misplacedParameter(model, parameter.getFormalDataType())) |
There was a problem hiding this comment.
Reject K&R float parameters before applying ilp32f
For an ilp32f binary built from an old-style definition such as float f(x) float x;, Clang 20 passes the default-promoted double in a0/a1 while emitting a float formal DIE and omitting DW_AT_prototyped. This comparison therefore sees FLOAT on both sides, applies __rea_ilp32f's fa0 storage, and admits the session even though every call and entry is decoded from the wrong register file. Preserve the prototype marker at import or refuse unprototyped signatures before this check.
AGENTS.md reference: AGENTS.md:L19-L19
Useful? React with 👍 / 👎.
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
doublein a float register. Limiting its float entries to 4 bytes is not enough: measured on Ghidra 12.1.4, that still joinsstruct { float; int; }intoa1:a0, where ilp32f passes the float infa0and the integer ina0. With this PR, ELF32 images that declare ilp32f and the F extension open in a new lane with a derived__rea_ilp32fprototype, and every argument of the fixture's calls sits where LLVM put it.opens images whose
Tag_RISCV_archlists only extensions that Ghidra 12.1.4 decodes, such as builds with ESP-IDF 5.5's-marchfor 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 (xesploopandxespv, plusxesppieon 5.5 before rev 3) and the 6.x builds for H4 (B andxespdsp) and S31 (xesploopandxespv). One prototype cannot express every ilp32f shape on Ghidra 12.1.4, so when an image's typed signatures use a shape__rea_ilp32fwould 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#ilp32fkeeps the full policy.elf32-exec-le-single-float-generic-v1, policyilp32f-prototype-v1, prototype__rea_ilp32f, on the pinnedRISCV:LE:32:defaultlanguage withgcc. Admission also requiresfinTag_RISCV_arch.__rea_ilp32fis one more row of feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention #8229's derivation inReaGhidraRiscvCallingConvention.java. It limits the default float entries to 4 bytes, keepsfs0–fs11preserved 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'sjoin_dual_classwithfillalternate: each 4-byte part that holds onefloattakes a float register, and each other part an integer register.struct { float; short; short; }, afloatbeside a pointer or a union (LLVM passes these in integer registers), and it cannot place_Float16or 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 every count and up to eight shortened signatures, and REA refuses the session with that reason. Flattening follows the compilers: a nonzero array of empty C++ classes and a flexible array member, wherever it starts, use the integer convention, as does a GNU vector passed directly, a bit-field counts by its width rather than its declared type, and each array element type is classified once. Ghidra imports a trailing[0]array like a flexible one, so that shape is refused too. A value whose fields hold up to 8 bytes, whatever padding alignment adds, and include a type Ghidra left untyped, such asfloat _Complex, is refused, because Ghidra's import gives an encoding it does not map an untypedbyte,word,dwordorqword. A C++ class with a non-trivial copy constructor or destructor is not detected or refused: Ghidra keeps only its fields and GCC's DWARF does not record the rule, so sessions state it as a limitation. ilp32f sessions also state two types whose distinction Ghidra's import erases: a GNU vector inside a structure, and_Atomic(float), which GCC 14.2 places as the policy does and Clang 19 does not.Tag_RISCV_archthat declaresdorzcd: ilp32f then preserves only the low 32 bits offs0–fs11, and Ghidra's model preserves all 64.join_dual_classwithout the check: those signatures would get wrong storage without notice.ilp32f-prototype-v1persist 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
e_flags0x3was refused: "…the single-float ABI (ilp32f) declared by e_flags has no verified calling-convention policy."__rea_ilp32f; without F they are refused before any provider starts. An image whose signatures__rea_ilp32fwould misplace opens, and its analysis is then refused with the signatures named. ilp32 and double-float behaviour and identity are unchanged.Change and scope
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.88c82b88a: the bridge row, its self-check (a float, double, two-float and float-and-int signature, andfs0–fs11preserved), and the comparison with the psABI.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 followsfaregisters and per-byte stack memory, and header mutations.6940199b4: the ilp32f limitation tests check facts, not wording.51419dca5,5fd177413,688c1d567,2db1fa8a9);688c1d567takes 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 (taggedata0:4,flaggedata0:1). Merges ofmainup tod3ab0faf5(8acebd1f4,a5c020aae,ba174afe6,bfef6976b);a5c020aaerecords feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention #8229's squashe669365c3with-s ours, after checking that itsgit patch-id --stableequals that ofd8ae325f1'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
src/domain/riscvIsaDeclaration.ts, the tables insrc/ghidra/GhidraRiscvProfile.ts,bridge/ghidra/ReaGhidraRiscvCallingConvention.java). The earliest changed stage is Ghidra admission: the lane row and its required F.userMessage, not only asdiagnostics.reason.riscv_elf.float_abi: "single",riscv_elf.archandriscv_calling_convention: ilp32f-prototype-v1; session Evidence states the prototype and its counts. ilp32 and double-float profiles are unchanged (tested).docs/public/product-catalog.json), package, or installation impact: none; the bridge source already ships.Expected effects:
Evidence and regression coverage
mainrefuses ELF32e_flags0x3("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 putstruct { float; int; }ina1:a0. The conformance checks refuse the stock model's placements: a wholedoubleinfa0, and that structure joined into an integer pair.-march). A GNU vector inside a structure and Clang's_Atomic(float)get wrong storage, which every ilp32f session states as a limitation. The stock double-float lanes still showfloat _Complexas an integer. The compiler placements of the new shapes come from one-off GCC 14.2 and Clang 19 builds; the lane builds with LLVM only.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, withfaregisters, 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 wholedoubleinfa0. 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:
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_UNMODELLEDopens, and its first analysis call fails. Onef3ea15bbthe lane checks that the reason counts "7 functions and 1 function-pointer type", namesrea_riscv_abi_spread,rea_riscv_abi_half,rea_riscv_abi_flexible,rea_riscv_abi_padded,rea_riscv_abi_vector,rea_riscv_abi_complex, the C++rea_riscv_abi_emptiesand the function-pointer type, and does not namerea_riscv_abi_elementsor the bit-field functionrea_riscv_abi_bits. Onbfef6976b, before those shapes were added, it read:{"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:
Validation performed
On the head
cca443532, which walks each field type once:npm run verify:ghidra:riscvandverify:ghidra:bridge-sourcespassed in a Linux container (Ghidra 12.1.4),"mocked": false, cleanup complete.npm run check:fast,format:check,docs:check,knipand the 4 test files this PR changes (175 passed, 3 skipped) passed. With a temporary, uncommitted chain of 29 nested structures,verify:ghidra:riscvtook 442 s on the earlier walk and 385 s on this one, one run each. CI passed on this head.On
c6345b0d7, which refuses an untyped value that alignment pads past 8 bytes:npm run verify:ghidra:riscvandverify:ghidra:bridge-sourcespassed in a Linux container (Ghidra 12.1.4),"mocked": false, cleanup complete. The lane checks that the reason names the over-alignedrea_riscv_abi_aligned, still counts 7 functions, and does not namerea_riscv_abi_complexes, whose twofloat _Complexfields pass by reference. With the old guard and this fixture the lane fails, because the reason leaves outrea_riscv_abi_aligned.npm run check:fast,format:check,docs:check,knipand the 4 test files this PR changes (175 passed, 3 skipped) passed. CI passed there.On
7f54acacb, which mergesmain71f680d7cwithout conflicts:npm run verify:ghidra:riscvandverify:ghidra:bridge-sourcespassed in the same container,"mocked": false, cleanup complete.npm run check:fast,format:check,docs:check,knipand the same 4 test files passed. CI passed there; it did not run on808271a78, for a reason not established.On
ef3ea15bb, which closes the review findings after042d25f94inc6659e329and refuses untyped values (808271a78after it changesdocs/ghidra-riscv.mdonly, anddocs:checkpassed there):npm run verify:ghidra:riscvandverify:ghidra:bridge-sourcespassed in a Linux container (Ghidra 12.1.4),"mocked": false, cleanup complete. The lane's header-mutated double-float image, now declared ilp32f, is refused for itsd.npm run check:fast,docs:checkand three test files (Ghidra RISC-V profile, session values, conformance checks; 119 tests) passed. The same lane failed without each fix: with the fixture ahead of the bridge change, the reason did not namerea_riscv_abi_paddedorrea_riscv_abi_vectoron042d25f94's bridge, orrea_riscv_abi_complexonc6659e329's.On
042d25f94, which adds the hidden-reference limitation to0933597de:npm run check:fastanddocs:checkpassed, and 15 files of the Ghidra RISC-V profile, session-value, provider-client and conformance-check tests passed (684 passed, 3 skipped).On
0933597de, which fixes the review findings in the flattener and the refusal after87fdfc89dmergedmainc3647ade1without conflicts:npm run verify:ghidra:riscvandverify:ghidra:bridge-sourcespassed in a Linux container (Ghidra 12.1.4),"mocked": false, cleanup complete, process lineage verified with no descendants. Source-derived: onbfef6976bthe empty-class and flexible shapes are admitted and the bit-field shape is refused, so those checks would fail there; that was not run.On
bfef6976b, which mergesmaind3ab0faf5.mainmoved the real-toolchain lanes section todocs/testing-lanes.md, so theverify:ghidra:riscvrow that names the ilp32f fixtures moves there unchanged.npm run check:fastanddocs:check— passed.format:checkfails only on 16 files that this PR does not change; they are byte-identical tomain, whose own CI fails the same way.npm run test:focused --17 files: the test files this PR changes and the Ghidra tests thatmainchanged — 590 passed, 3 skipped.npm run verify:ghidra:riscv— passed ("mocked": false, cleanup complete).The checks below ran on the previous head
ba174afe6, which mergesmain6e9ee97a6.npm run check:fast,format:check,docs:check(built steps executed) andverify:module-boundaries— passed.npx knip— one finding, the unused exported typeDarwinHelperCache(src/process/DarwinHelperCache.ts:465). knip on a builtmain6e9ee97a6prints 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 withmain's latest changes — 948 passed, 4 skipped (Windows-only).npm ci --ignore-scripts), onba174afe6: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.fast-uri3.1.8 andqs6.16.0 (lockfile: 3.1.9 and 6.16.1).Lane results per fixture on ba174af
0x1and0x3is admitted over MCP and the CLI through the soft- and single-float lanes; RV640x1(lp64),0x3(lp64f) and0x80000005are refused.-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 relabelled0x3lacks F and is refused.-march=rv32imafc_zicsr_zifencei): the single-float lane andilp32f-prototype-v1.fence.i,csrr,fmv.w.xandfmv.x.wdecode. Everyinspect_native_apiprototype uses__rea_ilp32fwith the table's storage, for examplefa0:4,a0:4for the flagged structure,a0:4,fa0:4for the tagged one, and the integer paira1:4,a0:4for adouble. 16 functions and 16 function definitions use the prototype;0x80000003is refused.tagged,flaggedandfill. Returns are in the typed registers, excepttaggedata0:4andflaggedata0:1, which leave their float infa0. 16 functions and 0 function definitions.llvm-objdumpplaces every expected word for the 10 ilp32 calls and the 13 ilp32f calls, in integer and float registers and on the stack.Compatibility, safety, and release
Author readiness
Human owner: @Entrpi
Before merge
type(scope): outcome.