Skip to content

Put Binary Ninja ahead of Ghidra in the fallback order - #230

Open
subwire wants to merge 2 commits into
mainfrom
binja-precedence
Open

Put Binary Ninja ahead of Ghidra in the fallback order#230
subwire wants to merge 2 commits into
mainfrom
binja-precedence

Conversation

@subwire

@subwire subwire commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #227 — branched from export-discoverability because both edit SKILL.md. Review the top commit only; retarget to main once #227 lands.

The doc routes agents away from a licensed backend

SKILL.md said:

Only switch backends if IDA fails to load the binary; fall through in this order: ida → ghidra → angr. Use binja only when explicitly requested.

So the fallback chain omits the one other licensed backend, and explicitly tells agents not to reach for it.

Measured

Over 60 solve sessions on a 40-binary corpus:

backend solves that loaded it
ida 58 (first choice in 20/20 and 38/38 where a backend was named)
ghidra 18
angr 5
binja 1

The IDA guidance works — agents reach for it every time. But when IDA dies they go straight to Ghidra, because that is what the doc says. A paid Binary Ninja license sat essentially unused while the fallback ran on the unlicensed backend.

Binja is available in our image, so this was purely the doc:

$ decompiler backend status binja --json
{ "available": true, "backend": "binja", "module": "binaryninja" }

That fallback isn't free

In the same run, a Ghidra load died outright on a binary named timo#3 (IllegalArgumentException: name contains invalid character, fixed separately in #229). The agent burned a backend switch and several minutes on a binary Binary Ninja would have opened.

Change

Fallback order becomes ida → binja → ghidra → angr, with a pointer to decompiler backend status binja --json so an agent can check in one call whether it is licensed here rather than assuming either way.

This is deliberately a check-then-use, not an assumption: where binja is not installed, the status call reports unavailable and the chain falls through to ghidra exactly as before.

🤖 Generated with Claude Code

subwire and others added 2 commits July 28, 2026 16:50
`export` was added in #222 to replace the load -> list_functions ->
decompile -> decompile -> ... loop that everyone otherwise scripts by
hand. Across 60 solve sessions on a 40-binary benchmark it was invoked
**zero times**.

Not because agents rejected it. SKILL.md -- 589 lines, the doc agents
actually read -- never mentions `export`. It says "Always start with
`list_functions` and `list_strings`", which is precisely the loop export
exists to collapse, so agents did that instead. The four sessions that
found the verb at all found it by running `decompiler export --help`
inside a batch of `--help` calls at command 60-85 of ~130, long after
they had done the work by hand. One of them then hand-wrote eight
per-function .c files -- a worse copy of export's own output -- having
spent 112 CLI calls to get there. A 117-function binary exports in 31
seconds.

Two changes:

**SKILL.md** now leads the "first moves" section with export, shows the
export-then-grep pattern, and says when *not* to use it (very large
binaries, or when you already know the one function you want). The
interrogative path is still documented, just no longer the only thing
documented.

**`--min-size`** makes the output worth reading rather than only
grepping. Import stubs dominate the function count of any dynamically
linked binary: on the crackme above, 93 of 117 functions were PLT
thunks and they were 45% of the exported pseudocode. `--min-size 16`
cuts that binary to 24 functions and 108K (from 117 and 480K) while
keeping every function a human analyst annotated by hand. Opt-in;
default 0 exports everything exactly as before. The manifest reports
`skipped_below_min_size` so the trim is never silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SKILL.md told agents "use `binja` only when explicitly requested" and
documented the fallback chain as `ida → ghidra → angr`, leaving the one
other *licensed* backend out of it entirely.

The result, measured over 60 solve sessions: binja was loaded in **1**.
Agents reached for IDA first every time (20/20 and 38/38 where a backend
was named -- the IDA guidance works), and when IDA died they went
straight to Ghidra, because that is what the doc says to do. So a paid
Binary Ninja license sat unused while the fallback path ran on the
unlicensed backend.

That fallback is not free. In the same run a Ghidra load died outright
on a binary named `timo#3` (see the name-sanitizing fix), costing a
backend switch and several minutes on a binary binja would have opened.

Fallback order is now `ida → binja → ghidra → angr`, with a pointer to
`decompiler backend status binja --json` so an agent can tell in one
call whether it is licensed here rather than assuming. Where binja is
not installed the guidance is unchanged in effect: the check reports
unavailable and the chain falls through to ghidra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant