[AUTOMATED] feat(ghidra): Phase 4 — full response encode (rename/retype, switch recovery, real signatures in the GUI) - #319
Merged
Conversation
…pe, switch recovery, real signatures)
[AUTOMATED] The decompileAt response now carries the complete first-<function>
document in the upstream child order (funcdata.cc:734): <addr> + <localdb> +
<ast> + <highlist> + <jumptablelist> + <prototype>, plus <parammeasures> under
the paramid action — everything the native Ghidra GUI features consume.
Encoders (kuna-decomp, all upstream-faithful ports over PackedEncode):
- Datatype::encodeRef/encode/encodeBasic/encodeTypedef (type.cc:462-560) +
TypeField/TypeBitField::encode; get_unsized_id/has_same_variable_base
completed through the existing hash_size; decode_integer_format added.
Differential-tested against the shipped decode side (encode_ref output
re-interns to the identical Rc through decode_type).
- FuncProto::encode (fspec.cc:4625) + EffectRecord::encode + the model-diff
effect/likelytrash lists; params travel as <localdb> cat-0 symbols (the
upstream symbol-backed-store wire shape).
- ScopeLocal::encode (varmap.cc:462) -> Database::encode_scope +
Symbol::encode{_header,_body} + SymbolEntry::encode (database.cc:187-2660):
nonzero ids always, >=1 entry per mapsym, positional <parent>+<rangelist>,
cat-0 params with slot index + exact storage.
- Funcdata::encode_high (funcdata.cc:658) + HighVariable class rules
(variable.cc:839) + the encode-time symbol-link pass kuna_link_high_symbols
(the ActionNameVars::linkSymbols stand-in for kuna's string-named highs;
runs only from Funcdata::encode, standalone path untouched).
- Funcdata::encode_jump_table over the already-ported JumpTable::encode,
emitted independently of savetree; session jumpload toggle reaches
FlowInfo::record_jumploads per decompile.
- ParamIDAnalysis unstubbed (the justproto arm reads the real recovered
FuncProto); <rank> always emitted; the paramid action answers a
parammeasures-ONLY doc (ghidra_process.cc:318).
Symbol-id echo discipline (r5 §5): invented symbols carry internal
SYMBOL_ID_BASE-range ids; globals echo the REAL host DB id
(GlobalEntry/GlobalContainer::symbol_id from the getMappedSymbols record) and
symref is OMITTED when unknown — never fabricated.
Rename/retype persistence loop: the function <localdb> answer's non-param
locals decode (RemoteLocalVar) and seed the fresh Funcdata — typelocked
locals as mapped/usepoint symbol seeds, plain renames (typelock=false, the
GUI-rename shape) as ScopeLocal::nameRecommend records (the C++ mechanism:
such symbols never survive clearUnlockedCategory(-1)), applied by the
ActionNameVars port (recommended_name_for, varmap.cc:1050 arms).
Markup type-token fidelity: EmitMarkup::tag_type splits rendered declarators
into word <type> tokens + <syntax> separators, so Java's getC()
(IllegalCharCppTransformer) no longer mangles "unsigned long *" into
unsigned_long__; markup-emitter-only, standalone bytes identical.
Harness: parse_decompile_doc decode-validates the full response the way Java
would; assert_phase4_traps asserts every r5 §3 hard-throw per function; new
switch-analyzer-shape, paramid-shape, and rename-persistence echo-back tests.
Pins: mangled [57,10,24]->[0,0,0]; diff bands [0.10..0.34]->[0.02..0.15]
(measured 0.050/0.079/0.099); c_lines unchanged.
Live-verified on stock Ghidra 12.1.2 (fmt::main, the committed
integrations/ghidra/live-smoke/phase4_features.py rig): real signature,
rename/retype round-trips that persist re-decompiles and hit the RIGHT DB
variable, DecompilerSwitchAnalysisCmd rebuilding all 10 case references from
kuna's jumptablelist, HighParamID decoding.
Gates: make test 675/675 PARITY OK, make test-stages PARITY OK, make
rust-test green, make check-spec OK, make test-ghidra green. No baseline
re-pins, no new options, no ghidra-mode default changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
[AUTOMATED] Spec-live prose for the Phase-4 encoders in their owning chapters: 00-overview (the full-response encode, the encode-time symbol-link pass, symbol-id echo discipline, the paramid doc shape, the rename/retype persistence loop), 04 (FuncProto::encode), 05 (the Datatype::encodeRef wire marshal-out), 06 (ScopeLocal::encode + the NameRecommend port). docs/ghidra-integration.md: the Phase-4 checklist (shipped vs deliberately deferred) and the §9 seam-table rows (Funcdata::encode, TypeFactory marshal-out, ParamIDAnalysis, DB-symbol-id echo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
…age renames, conflict-safe symbol ids [AUTOMATED] Six confirmed findings from the adversarial review of PR #319. C1/C5 (id-contract hole): the markup's `<vardecl symref>` still emitted the Phase-2 varnode-create-index placeholder, which can never resolve now that every real id is SYMBOL_ID_BASE-range — Java logged "Invalid symbol reference" per declaration per decompile and rename/retype was DEAD on declaration-line tokens. The symbol-link pass now runs BEFORE the markup is printed and the declaration carries the real LocalSymbolMap id plus the representative's varref; the create-index survives only as the fallback for a high the analysis left symbol-less. Making the reorder safe required the link pass to stop mutating the analysis scope: it now emits WIRE-ONLY symbols (`database::WireSymbol`) that are encoded into `<localdb>` and referenced by `<high symref>`/`<vardecl symref>` but never enter the scope, so the pass cannot perturb the printed C at any point in the sequence. (The first attempt DID move it — ghidra-sim's `c_lines` pin caught +1 line on faillog sub_2620 — which is why the design changed.) C2 (HIGH, renames reverted in front of the user): host locals whose only SymbolEntry is a `<hash>` DynamicEntry were dropped on decode. That is the storage class Java writes for every `requiresDynamicStorage` variable (unique-space representatives, `splitOutMergeGroup` products), so renaming such a variable silently reverted on the next decompile. Hash entries are now kept (`RemoteEntry::hash`, `RemoteLocalVar::hash`) and applied through the full upstream mechanism: `ScopeLocal::dynRecommend` + `recoverNameRecommendationsForSymbols`'s hash loop (`Funcdata::kuna_apply_dynamic_recommendations` over `DynamicHash::findVarnode`, run at the top of the naming pass), with typelocked ones seeded as dynamic Symbols. Hashes use the upstream budget of 8 because Java hardcodes the same (`DynamicHash.java:440`) and a disagreeing hash cannot round-trip. C3: name recommendations matched only the high's NAME REPRESENTATIVE's def address; upstream's `findVarnodeWritten` matches ANY varnode of (size,addr) written at the usepoint, so a rename recorded at a non-representative instance reverted. Now matched across every instance, plus upstream's `!sym->isNameUndefined()` guard (never paint over a resolved symbol) and a cat-0 guard (a parameter's name comes from the prototype). C4 (wrong-target rename): the encode-time link pass bound any unattached named high to the smallest containing SymbolEntry with none of the naming pass's conflict logic — so a high the naming pass deliberately routed to `vN` because binding it was a storage CONFLICT inherited the PARAMETER's symbol id, and renaming that variable in the GUI renamed the parameter. The naming pass now RECORDS its bind decisions (`HighVariable::kuna_link_symbol`) and the encode materializes symbols only for highs left unbound — a covered-but-unbound high gets a data-flow-HASHED wire symbol (upstream `buildDynamicSymbol`), never the conflicting entry's id. C6: the decoded cat-0 parameter storage and the `<prototype model=…>` were parsed and discarded, so kuna echoed model-rederived storage and Java's `checkFullCommit` force-committed a kuna-rederived signature over the user's on any parameter rename. Both now ride through (`RemoteParam::storage`, `RemoteFunctionFacts::{model,param_storage}` → `apply_locked_prototype_with_model` + `apply_mapped_params`), and `add_param_symbol` categorizes an existing overlapping entry as the parameter slot instead of skipping it. LOWs: `encode_scope`'s defensive symbol skips now also withhold the id from `<high symref>` (`Database::encodable_symbol_ids` — an orphan reference is the Java hard-throw the skip exists to avoid); the harness asserts the r5 §3 0-sized-data-type trap; the live rig's two vacuous PASS criteria are real (collateral detection commits the decoded locals first, and the param rename asserts decoded-vs-DB storage/ordinal equality BEFORE renaming, so a forced full commit can no longer masquerade as an in-place edit). Tests: a unit test drives the dynamic-recommendation mechanism end to end (hash a varnode, seed the recommendation, apply, assert the name and Symbol land on that variable); a sim test asserts no `<high>` borrows a cat-0 parameter's symref; the harness pins unresolvable `<vardecl symref>`s (0/1/0 — the systemic case is fixed, one group-member residue documented). All other pins unchanged, C text byte-identical (c_lines 283/39/92). Live-verified on stock Ghidra 12.1.2 (fmt::main): 13/13 checks PASS, including the strengthened ones — 21 committed DB variables for collateral detection and decoded param storage/ordinals equal to the database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
…sistence channels, dynRecommend [AUTOMATED] 00-overview: the naming pass's recorded bind decision vs the encode-time WIRE-ONLY symbols (and why the encode never re-derives a container binding — a conflict-separated high must not inherit the parameter's id), the four rename/retype persistence channels (mapped/dynamic × typelocked/rename-only), and the host model + exact parameter storage echo. 06: the dynamic (hash-keyed) recommendation list beside the address-keyed one. ghidra-integration.md: the review-round checklist entry and the two remaining deferred items (including the pinned single unresolved <vardecl symref>). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
…delta-review findings [AUTOMATED] The branch was RED on its own gate: `ghidra_sim_sort_grep_breadth` (the #[ignore]d breadth test `make test-ghidra` and the CI gates job run with `--include-ignored`) failed on tests/bug-repro/sort::sub_6370 with "NO <vardecl symref> resolves against <localdb>". ROOT CAUSE (a port gap, not a heuristic). sub_6370 declares `char v1 [16]` and `char v2 [24]`, two stack aggregates used ONLY as `memcmp(v1,v2,0x10)` — i.e. reached exclusively through a `&sym` reference. The whole HighVariable of such a variable is the CONSTANT PTRSUB offset operand (one instance, const space, is_constant=true, is_addr_tied=false), so `kuna_link_high_symbols` skips it at the constant guard and `kuna_high_symbol_wire_id`'s re-derivation loop — which only scans addr-tied instances — cannot see it either. Both declarations fell back to the varnode create index; 2 of 2, hence "wholesale". The Symbols were in <localdb> the whole time (v1/v2, cat -1, real ids). `Funcdata::link_symbol_reference` copies the referenced Symbol's name, offset and type onto that high but DROPPED its identity, where upstream `Varnode::setSymbolReference` (varnode.cc:465) → `HighVariable::setSymbolReference(entry->getSymbol(), off)` (variable.cc:283) carries the Symbol itself. The identity is now recorded (`HighVariable::kuna_ref_symbol`, local scope only) and read by the declaration's symref. Deliberately a SEPARATE field from `kuna_link_symbol`: that one feeds <high symref>, and such a high encodes class="constant", where Java's HighConstant.decode does nothing with a mapped local symref — so nothing is gained there, and keeping it out preserves the one-owner invariant `ghidra_sim_faillog_high_symrefs_are_not_shared_with_params` asserts. This also closes the pinned residual: PIN_FAILLOG_VARDECL_UNRESOLVED moves [0,1,0] -> [0,0,0] (sub_3320's survivor was the same class). Every declaration in the corpus now resolves; the pin doc states ZERO is the contract. TEST HYGIENE (a harness bug, fixed here). The previous round's recorded verification ran `cargo test -p kuna-ghidra --release` WITHOUT `--include-ignored` — its log reads "ghidra_sim_sort_grep_breadth ... ignored" — so the breadth test was never executed locally and nothing said so. `make test-ghidra` now tees its output and fails on TWO canaries: the CI skip canary (specs missing/unusable => every sim test skips and passes vacuously; worktrees hit this because KUNA_SPECS/SLEIGHHOME do not reach the cargo suites), and a breadth canary demanding the literal "test ghidra_sim_sort_grep_breadth ... ok" line. The breadth test itself now asserts 0 unresolved PER TARGET (the generic "resolved > 0" gate only catches wholesale breakage; a 20/20 -> 1/20 regression passed silently). DELTA-REVIEW FINDINGS (batched): #2 HIGH `add_param_symbol` promoted ANY storage-OVERLAPPING symbol into parameter slot i. Upstream `ProtoStoreSymbol::setInput` (fspec.cc:3150) does the opposite — it looks up BY SLOT and removes+recreates on an addr/size mismatch; it never promotes an unrelated local. And this runs in ActionRestructureVarnode (standalone included) where `clear_unlocked_category_negative` only clears cat<0, so a wrong promotion is permanent. Now categorizes only on an EXACT (addr,size) match; comment rewritten to the real mechanism and both failure modes (checkFullCommit's storage compare; getDatabaseParameter keying the DB slot off getCategoryIndex). #3 MED WireSymbol::encode bypassed the 0-size-type filter added for scope symbols (MappedEntry.decode throws and Java discards the whole result). `kuna_link_high_symbols` now routes a 0-sized dtype to the HASHED shape (DynamicEntry.decode has no size check) and `WireSymbol::is_encodable` is the backstop in both the encode and the id set. #4 MED <vardecl symref> was not filtered through the encodable predicate. Every branch of `kuna_high_symbol_wire_id` now passes `ScopeLocal::symbol_is_encodable` (new O(1) single-symbol form — the set build would run per declaration on every decompile). #5 MED `kuna_apply_dynamic_recommendations` runs at the TOP of the naming pass, where the ported isNameUndefined guard is vacuous, and it CREATES a Symbol where upstream RENAMES one. kuna fuses linkSymbols with the vN assignment into one walk, so there is no "after linking, before defaults" point to move it to; the equivalent guard is applied against the SCOPE instead — a hash landing on storage the walk will bind to a function_parameter, or to any Symbol with a defined name, is skipped. Also: find_varnode's `vnlist2[pos]` is a checked `.get()` (the hash is host-supplied), and the hardcoded budget of 8 carries the "deliberately not `dynamichashmax`" note. #6 MED the WIRE arm of encode_high skipped the encodable_symbols guard; `encodable` is empty for a has_no_code() function, so class="local" + symref against a <localdb> never written is the HighLocal.decode hard-throw. Wire arm now takes the same gate (falls to class="other"). #7 LOW Funcdata::clear() left kuna_wire_symbols / kuna_wire_symbol_for_high behind — HighVariableId-keyed maps outliving the arena that issued the ids. #8 LOW declaration tokens carried `varref`; upstream emitVarDecl pushes an explicitly NULL Varnode, and the omission is load-bearing (ClangVariableToken.getHighVariable returns inst.getHigh() from inside its `inst != null` block and never reaches the parent-decl fallback). Dropped; the create index remains the symref fallback. #9 LOW slot skew: `to_pieces` compacts out params with no dtype while param_storage carried absolute rp.index. Both now use the compacted basis. #10 LOW live rig: `len(before) >= 2` hard-failed on a one-local function (now >= 1, with a NOTE at 1), and the param rename target + its recorded old name now both come from the post-commit result. Doc correction (reviewer-verified): checkFullCommit inspects the parameter count, each categoryIndex and each storage — never the model name. The C6 prose in docs/spec/00-overview.md claimed otherwise; corrected to "the storage echo is the load-bearing half". Spec prose also gains the &symbol identity record, the vardecl encodable filter, and the dynRecommend placement divergence; ghidra-integration.md's deferred list is re-cut. Gates: make test 675/675 PARITY OK; make test-stages 484/484 PARITY OK; make rust-test green; make check-spec OK; make test-ghidra green WITH the breadth test proven to have run (11 sim tests, 6/6 breadth targets at vardecl_unresolved=0). C text byte-identical — c_lines 283/39/92 and every other faillog pin unchanged. No baseline re-pins, no options, no DIV rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 4 of the Ghidra integration (
docs/ghidra-integration.md§12; Phases 1–3 = #135/#317/#318): thedecompileAtresponse now carries the FULL first-<function>document —<localdb>,<highlist>,<jumptablelist>,<prototype>,<parammeasures>— plus the markup type-token fix, which is what lights up the native GUI features on a kuna core: real signature display, variable rename/retype that persists, switch recovery through the stock analyzer, and the param-ID analyzer.What lights up (live-verified on stock Ghidra 12.1.2,
fmt::main, pyghidra rig)Every line below is a
[PASS]fromintegrations/ghidra/live-smoke/phase4_features.py(new, committed) — the full report is reproduced at the end.int main(int argc,char **argv)— model__stdcall, extrapop 8, retint, 2 params, from<prototype>+ localdb cat-0 symbols<localdb>absent — no rename/retype targets at allHighFunctionDBUtil.updateDBVariablerenamesv11 @ RSI:8→ the DB variable at the same storage changes, zero collateral renames, and the new name survives the event-driven re-decompile (kuna_ren_probein the next getC())ulongpersists across re-decompile (typelocked localdb seed)argc → kuna_param_probelands on the DB Parameter (cat-0 + slot index + exact storage — no forced full-signature commit)<jumptablelist>absent — analyzer no-opHighFunction.getJumpTables()= 10 cases + 10 labels; after deleting all references from the BRANCHIND,DecompilerSwitchAnalysisCmdrebuilds all 10 case references from kuna's tables<parammeasures>stubbedparamid+ parammeasures →getHighParamID()decodes 2 inputs + 1 output (ranked)unsigned_long__a1(the IllegalCharCppTransformer mangle)unsigned long *a1— zero mangled tokensThe encode (kuna-decomp)
Upstream child order
addr → localdb → ast → highlist → jumptablelist → prototype(Funcdata::encode, funcdata.cc:734):Datatype::encodeRef/encode(substrate/dtype.rs) — the cross-cutting type marshal-out:<typeref name id>for id-carrying types (variable-length emits the size-independenthash_sizeid + instance size;get_unsized_id/has_same_variable_basecompleted), full<type>per C++ subclass override otherwise (pointer/array one-level descent, struct field/bitfield interleave, enum<val>s,<def>typedefs, PTRREL's full-ptrto +<off>). Differential-tested against kuna's own shipped decode: whatencode_refwrites,decode_typeresolves to the identical internedRc.FuncProto::encode(p4_calls/fspec.rs) +EffectRecord::encode+ model-diff effect/likelytrash lists. Params deliberately travel as localdb cat-0 symbols (upstream's symbol-backed store shape).<localdb>—ScopeLocal::encode(p6_variables/varmap.rs) →Database::encode_scope/Symbol::encode{_header,_body}/SymbolEntry::encode(p0_knowledge/database.rs), nametree order, equate symbols included.<highlist>—Funcdata::encode_high+ the encode-time symbol-link passkuna_link_high_symbols(the C++ActionNameVars::linkSymbolsstand-in: kuna names highs with strings, so the Symbols the wire needs are attached/materialized just before encoding; runs only fromFuncdata::encode, so the standalone path is untouched). Five-way class rule verbatim (variable.cc:839).<jumptablelist>— the already-portedJumpTable::encodewired in, emitted independently of savetree; the sessionjumploadtoggle now reachesFlowInfo::record_jumploadsper decompile (loadtables collected).<parammeasures>—ParamIDAnalysisunstubbed (the justproto arm now reads the real recoveredFuncProto),<rank>always on; under actionparamidit is the ONLY doc child (ghidra_process.cc:318).EmitMarkup::tag_typesplits a rendered declarator into word<type>tokens +<syntax>separators (template payloads stay intact). Markup-emitter-only, so standalone output is byte-identical by construction.Symbol-id discipline (r5 §5)
SYMBOL_ID_BASE-range ids (legal on the wire native→Java; Java's decode already zeroes received internal-range ids —remote_provider.rs:522).GlobalEntry/GlobalContainergainedsymbol_id, filled from the getMappedSymbols record;<high class="global" symref>is emitted only when a real id is known — never fabricated (a wrong non-internal id silently renames the wrong DB symbol).The rename/retype persistence loop
A GUI edit is a DB write + event-driven re-decompile; Java's next getMappedSymbols answer carries the edited local in the function
<localdb>(LocalSymbolMap.grabFromFunction). kuna now decodes those non-param locals (RemoteLocalVar) and seeds the fresh Funcdata:typelock=falseonly for Undefined types) → real mapped/usepoint symbol seeds (survive restructure's typelock-keep rule);ScopeLocal::nameRecommendmechanism (such symbols never surviveclearUnlockedCategory(-1)upstream either), applied by theActionNameVarsport (recommended_name_for, three-arm use-address match per varmap.cc:1050).Root-caused live: the first implementation seeded renames as symbols and they vanished at restructure — exactly why upstream keeps them as recommendations. Verified against a live DecompileDebug capture (
<symbol typelock="false" namelock="true" cat="-1"><typeref name="undefined8">).Harness (ghidra-sim)
parse_decompile_docnow decode-validates the whole response the way Java would, andassert_phase4_trapsasserts every r5 §3 hard-throw on every decompiled function:nonzero symbol ids; ≥1 SymbolEntry (+ uselimit rangelist) per mapsym; positional
<parent>+<rangelist>before<symbollist>; localdb-before-highlist and ast-before-highlist order; legal high class vocabulary; local/paramsymrefresolves in the just-decoded localdb;reprefresolves in the just-decoded ast; prototype model+extrapop+returnsym(addr+type);<rank>per measure; cat-0 symbols carry the slot index; unknown<function>/<doc>children are a test failure (Java discards the whole result).New configuration tests: the switch-analyzer shape (
noc+notree+jumpload→ function-only doc, no ast/highlist/markup, jumptablelist with ≥2 dests and ≥1 loadtable), the paramid shape (parammeasures-ONLY doc), and the rename-persistence echo-back (serve a kuna-encoded local back renamed withtypelock=false→ the new name renders and re-encodes).Pin table (
ghidra_sim_e2e.rs, faillog)PIN_FAILLOG_MANGLED_TOKENSPIN_FAILLOG_DIFF_FLOOR(measured)PIN_FAILLOG_DIFF_CEILINGPIN_FAILLOG_C_LINESThe ghidra-vs-CLI gap collapsed to pure per-function analysis skew — the getC() mangling half of the residue is gone.
Gates
make test675/675 PARITY OK ·make test-stagesPARITY OK ·make rust-testgreen (full workspace) ·make check-specOK ·make test-ghidragreen (incl. the release breadth test). No baselines re-pinned; no options added; no ghidra-mode default changed (no new DIV rows). Spec prose updated in the same PR (00-overview, 04, 05, 06) +docs/ghidra-integration.mdPhase-4 checkboxes and §9 seam table.Deliberately deferred (documented in ghidra-integration.md)
structureGraph(FunctionGraph nested layout only), the four signature commands (BSim), overlay spaces,getStringDatacharset fidelity — all with clean §10 degradation.<hash>dynamic-storage symbols for unique-space locals in the link pass (renames of unique-storage temps fall back to Java's address-keyedDynamicEntry.buildpath meanwhile);<override>/child-<scope>statics (Java skips both); the standalonecollectNameRecsharvest + dynamic-hash recommendation list.Live rig
integrations/ghidra/live-smoke/phase4_features.py(committed; same env askuna_vs_stock.py). Full run on fmt::main:Review revisions (adversarial review round — 6 confirmed findings, all fixed)
Commits
49e1b84f(code+tests) ande6b85c1b(docs).<vardecl symref>still emitted the Phase-2 varnode-create-index placeholder — unresolvable now that every real id isSYMBOL_ID_BASE-range, so Java logged "Invalid symbol reference" per declaration per decompile and rename/retype was dead on declaration-line tokensvarref; create-index survives only as the fallback for a high the analysis left symbol-less. Making that reorder safe required the pass to stop mutating the analysis scope — see the design note below.<hash>DynamicEntry were dropped on decode — the storage class Java writes for everyrequiresDynamicStoragevariable (unique-space representatives,splitOutMergeGroupproducts), so renaming one silently revertedRemoteEntry::hash,RemoteLocalVar::hash) and applied through the full upstream mechanism:ScopeLocal::dynRecommend+recoverNameRecommendationsForSymbols's hash loop (kuna_apply_dynamic_recommendationsoverDynamicHash::find_varnode, at the top of the naming pass); typelocked ones seed as dynamic Symbols. Hash budget pinned to upstream's 8 — Java hardcodes the same (DynamicHash.java:440) and a disagreeing hash cannot round-trip.findVarnodeWrittenmatches any varnode of (size,addr) written at the usepoint, so renames recorded at another instance reverted!sym->isNameUndefined()guard and a cat-0 guard (a parameter's name comes from the prototype).vNinherited the parameter's id, so renaming it renamed the parameterHighVariable::kuna_link_symbol); the encode materializes symbols only for highs left unbound, and a covered-but-unbound (conflict) high gets a data-flow-hashed wire symbol — upstream'sbuildDynamicSymbol— never the conflicting entry's id.<prototype model>were parsed then discarded; kuna echoed model-rederived storage, so Java'scheckFullCommitforce-committed a kuna-rederived signature over the user's on any param renameRemoteParam::storage,RemoteFunctionFacts::{model,param_storage}→apply_locked_prototype_with_model+apply_mapped_params);add_param_symbolcategorizes an existing overlapping entry as the parameter slot instead of skipping it.encode_scopeskips could orphan a<high symref>— the Java hard-throw the skip guards against; (b) two vacuous live-rig PASS criteria; (c) missing r5 §3 0-sized-datatype trapsymreftoo (Database::encodable_symbol_ids); (b) collateral detection now commits the decoded locals first (21 DB variables on fmt/main) and the param check asserts decoded-vs-DB storage/ordinal equality before renaming, so a forced full commit can no longer masquerade as an in-place edit; (c) trap added.Design note: wire-only symbols (why the C text is provably unchanged)
The reviewers' C1 fix (link pass before the markup) was re-derived as text-neutral. It was not — the harness caught it: mutating the local scope before printing moved faillog
sub_2620from 283 → 284 lines. The pass therefore no longer touches the analysis scope at all: it emitsdatabase::WireSymbolrecords — encoded into<localdb>and referenced by<high symref>/<vardecl symref>, never entering the scope — so it cannot perturb decompilation wherever it runs.c_linesis back to 283/39/92, i.e. byte-identical C, and that pin is the standing proof.Pins (this round)
PIN_FAILLOG_C_LINESPIN_FAILLOG_MANGLED_TOKENSPIN_FAILLOG_VARDECL_UNRESOLVEDNew tests
dynamic_name_recommendation_renames_the_hashed_variable— hashes a real varnode, seeds the recommendation, applies it, asserts the name and a Symbol land on that variable (the C2 mechanism, non-vacuously).ghidra_sim_faillog_high_symrefs_are_not_shared_with_params— no non-param<high>may reference a cat-0 parameter symbol (the C4 regression).ghidra_sim_faillog_dynamic_rename_persistence— hash-storage rename round-trip through the oracle's new dynamic-entry serving.<vardecl symref>resolution + the 0-sized-datatype trap; the oracle serves<hash>locals and typelock=false/true shapes.Live re-verification (stock Ghidra 12.1.2, fmt::main)
13/13 PASS, including the strengthened criteria:
Gates (post-fix)
make test675/675 PARITY OK ·make test-stagesPARITY OK ·make rust-testexit 0 (316 suites) ·make check-specOK ·make test-ghidragreen. No baseline re-pins, no options, no DIV rows.Review round 2 (delta review of
e6b85c1b+ the red CI gate) — 10 findingsThe branch was red on its own gate:
ghidra_sim_sort_grep_breadth(the#[ignore]d sort/grep breadth test thatmake test-ghidraand the CI gates job run with--include-ignored) failed ontests/bug-repro/sort/sub_6370with "NO<vardecl symref>resolves against<localdb>". Fixed, plus the nine other findings from the delta review.The CI failure — root cause
sub_6370declares two stack aggregates,char v1 [16]andchar v2 [24], that are only ever used asmemcmp(v1,v2,0x10)— i.e. reached exclusively through a&symreference. The whole HighVariable of such a variable is the constantPTRSUBoffset operand (dumped: one instance,constspace,is_constant=true,is_addr_tied=false), andkuna_link_high_symbolsskips constants by design (upstream'sif (!vn->isPersist())create guard never fires for one), whilekuna_high_symbol_wire_id's re-derivation loop only scans addr-tied instances. Both declarations therefore fell back to the varnode create index — 2 of 2, hence "wholesale". The Symbols themselves were in<localdb>all along (v1/v2, cat −1, real ids).The missing link was an actual port gap, not a heuristic:
Funcdata::link_symbol_referencecopies the referenced Symbol's name, offset and type onto that high but dropped its identity, where upstreamVarnode::setSymbolReference(varnode.cc:465) →HighVariable::setSymbolReference(entry->getSymbol(), off)(variable.cc:283) carries the Symbol itself. The identity is now recorded (HighVariable::kuna_ref_symbol) and read by the declaration'ssymref.It is deliberately a separate field from
kuna_link_symbol: the latter feeds<high symref>, and such a high encodesclass="constant", where Java'sHighConstant.decodedoes nothing at all with a mapped local symref — so nothing is gained there, and keeping it out preserves the one-owner invariantghidra_sim_faillog_high_symrefs_are_not_shared_with_paramsasserts.This also closed the pinned residual:
PIN_FAILLOG_VARDECL_UNRESOLVEDmoves [0,1,0] → [0,0,0] — the sub_3320 survivor was the same class. Every declaration in the corpus now resolves, and the pin doc says ZERO is the contract, not a high-water mark.Test hygiene — why a green local run shipped a red CI
The previous round's recorded verification run (
scratchpad/ghidra_sim_run.log) reads:— i.e.
cargo test -p kuna-ghidra --releasewithout--include-ignored. The breadth test was never executed locally, and nothing said so.That is a harness bug, and
make test-ghidrais now two canaries instead of a barecargo test:skipping (… .sla …)) — with specs missing or unusable every ghidra-sim test prints a skip notice and returns early by design, so a green run proves nothing. CI greps for it; the Makefile did not. Worktrees hit this constantly (KUNA_SPECS/SLEIGHHOMEdo not reach the cargo suites).test ghidra_sim_sort_grep_breadth ... okline, so any invocation that loses--include-ignoredfails loudly instead of exiting 0.The breadth test itself was also strengthened per the reviewer's note that the generic
resolved > 0gate only catches wholesale breakage: it now asserts 0 unresolved per target (and printsvardecl_unresolved/vardecls), so a 20/20 → 1/20 regression can no longer pass.The other nine
add_param_symbolpromoted any storage-overlapping symbol into parameter sloti; the citedProtoStoreSymbol::setInputdoes the opposite (looks up by slot, and removes+recreates on an addr/size mismatch). Not ghidra-gated — it runs inActionRestructureVarnode, andclear_unlocked_category_negativeonly clearscat < 0, so a wrong promotion is permanent(addr, size)match; comment rewritten to the real upstream mechanism and to both failure modes (checkFullCommitstorage compare;getDatabaseParameterkeying the DB slot offgetCategoryIndex)WireSymbol::encodebypassed the 0-size-type filter added for scope symbols →MappedEntry.decodehard-throw discards the whole resultkuna_link_high_symbolsroutes a 0-sized dtype to the hashed shape (DynamicEntry.decodehas no size check), andWireSymbol::is_encodableis the backstop in both the encode and the id set<vardecl symref>was not filtered through the encodable predicate → a defensively-skipped symbol yields an unresolvable declaration refkuna_high_symbol_wire_idnow passesScopeLocal::symbol_is_encodable(new O(1) single-symbol form — the set build would be per-declaration on every decompile) and falls back to the create indexkuna_apply_dynamic_recommendationsruns at the top of the naming pass, where the portedisNameUndefinedguard is vacuous, and it creates a Symbol where upstream renames onevNassignment into one walk, so there is no "after linking, before defaults" point): the equivalent guard is applied against the scope — a hash landing on storage the walk will bind to afunction_parameter, or to any Symbol with a defined name, is skipped. Divergence documented indocs/spec/06and the ghidra-integration deferred list. Also:find_varnode'svnlist2[pos]is now a checked.get()(the hash is host-supplied), and the hardcoded 8 carries the "notdynamichashmax" noteencode_highskipped theencodable_symbolsguard;encodableis empty for ahas_no_code()function →class="local"+ symref against a<localdb>never written →HighLocal.decodehard-throwclass="other")Funcdata::clear()leftkuna_wire_symbols/kuna_wire_symbol_for_highbehind —HighVariableId-keyed maps outliving the arena that issued the idshigh_bankvarref; upstreamemitVarDeclpushes an explicitly null Varnode, and the omission is load-bearing (ClangVariableToken.getHighVariablereturnsinst.getHigh()from inside itsinst != nullblock, never reaching the parent-decl fallback)varref— upstream shape restored; the create index remains thesymreffallbackto_piecescompacts out params with no dtype whileparam_storagecarried absoluterp.index→ count/index disagreement →checkFullCommitfiresparam_storageslots are counted in the same compacted basislen(before) >= 2hard-fails on a function with one DB local;p0 = decoded[0] if decoded else p0swapped the rename target mid-section>= 1(0 committed = the rig's premise failed) with a NOTE at 1; the rename target and its recorded old name now both come from the post-commit resultDoc correction (reviewer-verified):
checkFullCommitinspects the parameter count, eachcategoryIndexand each storage — never the model name. The C6 prose indocs/spec/00-overview.mdsaid otherwise; corrected to "the storage echo is the load-bearing half; the model rides along because the storage kuna would otherwise derive comes from it".Pins (this round)
PIN_FAILLOG_VARDECL_UNRESOLVED&sym-reference class; ZERO is now the contractvardecl_unresolvedPIN_FAILLOG_C_LINESPIN_FAILLOG_MANGLED_TOKENS/ placeholders / registers / uniquesGates (round 2)
make test675/675 PARITY OK ·make test-stages484/484 PARITY OK ·make rust-testgreen ·make check-specOK ·make test-ghidragreen with the breadth test proven to have run (ghidra_sim_sort_grep_breadth ... ok, 11 sim tests, both canaries armed). No baseline re-pins, no options added, no DIV rows.🤖 Generated with Claude Code
https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC