[AUTOMATED] feat(ghidra): ghidra-sim differential test harness — pin the GUI-path quality gap - #317
Merged
Merged
Conversation
… quality gap Extract the decompile_at_e2e MockJava loopback into a shared module (tests/ghidra_sim/mod.rs — AnswerSource-pluggable pump, wire builders, session tracer, dual-<function> doc parser, a markup->C flattener that replicates Java's getC() token cleaning (IllegalCharCppTransformer), badness scanners, line-diff metric) and build ghidra-sim v1 on top (tests/ghidra_sim/oracle.rs): a mock-Java answer source backed by kuna's own analysis of real vendored ELFs — bootstrap_from_object for bytes/labels, the real Sleigh re-encoded as wire <inst> docs for getPcode, and a tspec GENERATED from the loaded Sleigh's AddrSpaceManager so packed space indices agree by construction. getMappedSymbols/getExternalRef answer EMPTY at a marked PHASE-3 SEAM. tests/ghidra_sim_e2e.rs drives the full wire lifecycle (registerProgram -> setAction -> decompileAt x3 -> flushNative -> repeat -> deregister) over tests/bug-repro/faillog (sort/grep as an ignored breadth test), asserts the response-document schema (name/entry echo, markup opref/varref subset-of ast, 19-query legality + query-legal placement), and PINS today's Phase-2 reality: per-function raw-register leaks (106/64/60), Unique tokens (32/2/8), placeholders (49/25/17) of which the loader already knows names for 24/18/14 (Phase 3 drives to 0), getC()-mangled tokens (21/13/7, PR-C drives to 0), ghidra-vs-CLI line diff ratios (0.64/0.90/0.81 floors), getPcode traffic 1477/1003, and getMappedSymbols == 0 (Phase 3 flips to >=1). Pins move only with the provider/emitter change that earns them. [AUTOMATED] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
…hidra Add a `gates`-job step running `cargo test -p kuna-ghidra --release -- --include-ignored` (with the standard specs-skip canary grep) after the catalog checks: the workspace suite is skipped on internal PRs, and a GUI-path regression is exactly what the ghidra-sim pins exist to catch pre-merge. Cost: ~1-2 min of compile over the release deps `make binaries` already built, ~2 s of test runtime. `make test-ghidra` is the same run locally. [AUTOMATED] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
integrations/ghidra/live-smoke/: a manual/dev pyghidra rig that swaps DecompileProcessFactory.exepath to kuna_ghidra inside a real Ghidra, decompiles the same functions with both cores, and writes a side-by-side report with the same badness-scanner counts the in-tree harness pins (README covers the offline-pyghidra setup and the getC()-vs-GUI-panel rendering distinction). docs/ghidra-integration.md §11 rewritten around the shipped harness: what it covers, how to run it, where the pins live. [AUTOMATED] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
- Diff-ratio pins are now a BAND: ceilings [0.70,0.95,0.90] join the floors (a markup regression that makes the GUI text worse now fails instead of saturating), the vacuous `<= 1.0` assert is gone, and the flattened-C normalized line count is pinned per target (241/174/89) -- the assert that actually catches a <break>-token collapse. The pin comment now decomposes the ratio (Phase-3 symbol gap + option-preset skew until setOptions is wired) so nobody chases 0 with symbol work. - decompile_cli now routes through the SHARED per-function step (kuna_console::decompile_step::decompile_one, DIV-66) with the error-noreturn CALL_RETURN flow overrides built exactly as kuna_console::project does -- the previous direct drive call silently dropped them (latent: faillog has no error() sites, and every faillog pin re-measured IDENTICAL; sort/grep-class fixtures would have diverged from the real CLI). - Assert strength: warnings frames pinned trim-empty on registerProgram and decompileAt (no substring blocklists); markup oprefs AND varrefs pinned non-empty per class; ast varnode refs collected from the <varnodes> child only (all Java's buildVarnodeRefs keys -- an op operand ref undeclared there must not launder the subset assert); the name echo compares against the sim's code_label (which consults label_overrides) rather than the raw program lookup. - CI: the gates-job harness step tees its output to a log before the canary grep, so a FAILING pin still prints its diagnostics (the old command substitution aborted under bash -e before any echo). - live-smoke docs/script: kuna_ghidra is not built by `make binaries`; point at `cargo build --release -p kuna-ghidra`. Tests/CI/docs-only diff: the parity gates are untouched by construction. `cargo test -p kuna-ghidra --release -- --include-ignored` fully green; `make check-spec` green. No pinned value moved. [AUTOMATED] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
The branch was cut from #302 while main advanced to #316; the engine PRs in between (spillargtrial #315, stackguard #306, snip-reads #307, loadguardrange #308, ...) legitimately shift the ghidra-path output, so CI's merge-ref run measured register leaks 108 vs the pinned 106. Rebase onto origin/main and re-pin every value to the rebased tree: registers 106/64/60 -> 108/58/60, unique 32/2/8 -> 34/4/8, mangled 21/13/7 -> 21/6/7, c_lines 241/174/89 -> 243/128/89, diff ratios 0.643/0.898/0.811 -> 0.646/0.867/0.811 (band unchanged); placeholders/resolvable/traffic unchanged (49/25/17, 24/18/14, 1477/1003). The sub_3320 shrink (c_lines -26%, FS_OFFSET gone from its register set, mangled 13 -> 6) is the stackguard/snip-reads work stripping the canary sequence -- explainable, not suspicious. Determinism verified: three consecutive runs of the pins test are measurement-identical. Gates on the rebased tree: harness release + --include-ignored green, make test 675/675 PARITY OK, make test-stages PARITY OK, check-spec OK, kuna-ghidra dev-profile spot check green. The full workspace suite ran green on this branch pre-rebase and every added commit is already-CI- green main history, so it is covered by the branch + main CI runs. [AUTOMATED] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
mahaloz
force-pushed
the
feat/ghidra-sim-harness
branch
from
August 17, 2026 22:48
aa67ad5 to
27bbe6a
Compare
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.
Test harness for the Ghidra integration: make GUI-path regressions and the Phase-2 quality gap measurable numbers in CI, without a live Ghidra. This is PR-A of the ghidra-mode series; the Phase-3 (lazy symbol/type providers) and Phase-4 PRs land against this harness by flipping its pins.
What the harness is
decompiler/crates/kuna-ghidra/tests/ghidra_sim/mod.rs): the interactive single-threaded pipe double proven bydecompile_at_e2e.rs, extracted behind a pluggableAnswerSource— wire builders, query pump, session tracer, a dual-<function>doc parser, a markup→C flattener that replicates Java'sgetC()token cleaning (IllegalCharCppTransformer), badness scanners, and a normalized line-diff metric.decompile_at_e2e.rsis refactored onto it (still green).tests/ghidra_sim/oracle.rs): a mock-Java answer source backed by kuna's own analysis of real vendored ELFs —bootstrap_from_objectfor bytes/labels/facts, the real Sleigh re-encoded as wire<inst>docs forgetPcode, register/userop tables from the same Sleigh, and a tspec generated from the loaded Sleigh'sAddrSpaceManagerso packed space indices agree by construction.getMappedSymbols/getExternalRefanswer empty — the Phase-2 reality — at a clearly markedPHASE-3 SEAMwhere the<mapsym>encoder plugs in.tests/ghidra_sim_e2e.rs): drives registerProgram → setAction → decompileAt×3 → flushNative → repeat decompileAt → deregister overtests/bug-repro/faillog(plus asort/grepbreadth test), asserts the response-document schema (name/entry echo, markup opref/varref ⊆ ast, 19-query legality + query-legal-command placement, clean warnings), and decompiles the SAME functions through the in-process CLI drive to pin the gap the GUI user experiences.Today's pinned numbers (faillog, x86-64, stripped)
sub_2620(main)sub_3320sub_3ad0(Re-measured on main @
813ee131after rebasing — the engine PRs #303–#316 shift the ghidra-path output slightly; determinism verified over three identical runs.)Session query traffic (unchanged by the rebase):
getPcodetotal 1477 vs 1003 distinct decoded instructions (no p-code cache + no-return flow overrun into neighbours);getMappedSymbols= 0 (pinned; Phase 3 flips this to ≥ 1). flushNative + repeat decompile is pinned byte-identical (deterministic engine; Phase 3 turns this into the cache-clearing semantics test via the oracle'slabel_overridesseam).How the next PRs flip them:
ScopeGhidra/TypeFactoryGhidra):resolvable → 0(the 24/18/14 are PLT imports the loader already names —localtime,getopt_long,dcgettext, … rendering assub_ADDRonly becausegetMappedSymbolsis unanswered),getMappedSymbols ≥ 1, diff-ratio floors flip to ceilings, register/Unique leak counts collapse (r3 §8 defects a–e, g).mangled tokens → 0(see root cause below).Root cause found (reported, NOT fixed here — PR-C's job)
The live-repro type mangling (
unsigned_long__a1,(char__)) is two-sided:<type>token —tag_type(ty/decl_type/ret_type, …)atdecompiler/crates/kuna-decomp/src/p9_emit/printc.rs:1428,1451,2190,2679receives strings like"unsigned long *"(the code at :2683 even testsdecl_type.ends_with('*')). UpstreamPrintC::pushTypeStartpushes only the base type NAME as the type token; stars/spaces travel as separate syntax/op tokens.DecompileResults.getDecompiledFunction()(DecompileResults.java:211) runs every funcname/variable/type/field/label token throughIllegalCharCppTransformer.simplify, rewriting every non-identifier char to_— so"unsigned long *"+"a1"reads back asunsigned_long__a1. The GUI panel renders token text verbatim; scripts/exports/getC()consumers see the mangling.The harness flattener replicates (2) exactly, so (1) is pinned as the
mangled tokenscolumn and PR-C's fix is measurable. Fixing (1) is a multi-site token-granularity change (> 10 lines), out of scope here per the brief.CI + tooling
.github/workflows/tests.ymlgatesjob (runs on every PR): one new step —cargo test -p kuna-ghidra --release -- --include-ignoredwith the standard skip-canary grep. Cost: ~1–2 min compile over the already-built release deps + ~2 s of test runtime.make test-ghidra: the same run locally.make rust-test(dev profile) runs the faillog tests too (~12 s; the sort/grep breadth test is#[ignore]d there and picked up by the release contexts).integrations/ghidra/live-smoke/, manual/dev, not CI): a parameterized pyghidra script that swapsDecompileProcessFactory.exepathtokuna_ghidrainside a real Ghidra, decompiles the same functions with both cores, and writes a side-by-side report with the same scanner counts (README covers the offline-pyghidra setup).docs/ghidra-integration.md§11 rewritten around the harness.Gates
make test675/675 PARITY OK ·make test-stagesPARITY OK ·make rust-testgreen (the new tests run inside it) ·make check-specOK.🤖 Generated with Claude Code
https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
Review hardening (
aa67ad57): diff-ratio pins are now a band (floors + ceilings[0.70, 0.95, 0.90]) and the flattened-C normalized line count is pinned per target (241 / 174 / 89) — the assert that catches a<break>-token collapse the ratio floors alone could not; the vacuous<= 1.0assert is gone.decompile_clinow routes through the shareddecompile_step::decompile_onewith the error-noreturn CALL_RETURN flow overrides exactly askuna_console::projectbuilds them (latent fix — every faillog pin re-measured identical). Assert strength: warnings frames pinned trim-empty, markup oprefs AND varrefs non-empty per class, ast varnode refs collected from<varnodes>only (all Java'sbuildVarnodeRefskeys), name echo vs the sim's servedcode_label. The CI step nowtees its log so a failing pin prints diagnostics underbash -e. Live-smoke docs point atcargo build --release -p kuna-ghidra(notmake binaries). No pinned value moved.