You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: re_search_code byte/immediate scan no longer runs a whole-binary analysis (2547s timeout) (#269)
* fix: re_search_code byte/immediate scan no longer runs a whole-binary analysis (2547s timeout)
The byte/immediate scan shelled to xrefs_probe.py, which runs a whole-binary radare2 `aaa` before
EVERY mode — but a raw memory scan doesn't need analysis (the `aaa` existed only to map a hit to its
containing function), so on a ~480MB target it hit the ~2547s size-scaled timeout. Fix, tiered like
the re_xrefs routing: a warm Ghidra Memory.findBytes scan (pyghidra_lib.search_bytes_core; served
headless via `ghidra_probe --search` WARM-ONLY + a resident-bridge `search` op) routed through
sandbox.decompiler.ghidra_op_backend — reuses the warm project / a live bridge, each hit mapped to its
function; the r2 xrefs_probe fallback now SKIPS `aaa` for search mode so it's fast too. Also fixes the
read-only warm open (_read_only_program, shared with re_script), latently broken under jpype (a boxed
JInt DEFAULT_VERSION + a Python object() that can't bind java.lang.Object).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address PR #269 review nits — guard found.add() overflow, reject negative immediates
Wrap search_bytes_core's found.add(1) so an AddressOverflowException at the very top of the address
space stops the scan cleanly instead of raising out and dropping already-found hits. And _search_patterns
now rejects negative immediates to match the r2 fallback's _IMM regex (which has no leading '-'), so the
same `immediate` resolves identically on both backends — a two's-complement value is searchable via
bytes_pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: src/hexgraph/agent/mcp_catalog.py
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -182,7 +182,7 @@
182
182
("read", "re_resolve", _t.resolve_address, "Triage a hex ADDRESS (a crash address, a pointer, a DAT_ label) WITHOUT a full decompile: returns {nearest_symbol + offset, section, containing_function (name+bounds when the symbol table knows it)}. Assembled server-side from the symbol table + section table (re_binutils_facts / pyelftools over the on-disk ELF) — cheap orientation before you spend a re_decompile_at. On a stripped binary it still resolves the section + nearest dynamic symbol (a FUN_ name needs a decompile). QUERY: records an Observation; adds no graph nodes.", {"type": "object", "properties": {"target_id": {"type": "string"}, "address": {"type": "string", "description": "hex address, e.g. 0x401200"}}, "required": ["target_id", "address"]}),
183
183
("read", "re_hexdump", _t.hexdump, "Dump raw BYTES at a virtual ADDRESS as hex + ascii (bounded — default 256, max 4096) — inspect a DAT_ table, an embedded key, a struct, or a string constant's exact bytes. Maps the vaddr to a file offset via the ELF program headers/sections and reads the on-disk artifact server-side (no decompile, no Docker). Bytes in a .bss/zero-fill region read as 00 with a note; an unmapped address is reported, not faked. QUERY: records an Observation; adds no graph nodes. (For the INSTRUCTIONS at an address use re_disassemble_range; this is the raw-bytes view.)", {"type": "object", "properties": {"target_id": {"type": "string"}, "address": {"type": "string", "description": "hex virtual address, e.g. 0x4c1000"}, "length": {"type": "integer", "description": "bytes to dump (default 256, clamped 1-4096)"}}, "required": ["target_id", "address"]}),
184
184
("read", "re_function_info", _t.function_info, "Lightweight metadata for a function (by NAME or ADDRESS) WITHOUT a full decompile: address, size, prototype/signature + calling convention (when known — from the symbol table, a prior decompilation, or recon), and #callers / #callees (from the call graph). The cheap 'what is this function' triage before deciding to re_decompile_function it. Fields not yet recovered are marked unknown (decompile to recover). QUERY: records an Observation; adds no graph nodes.", {"type": "object", "properties": {"target_id": {"type": "string"}, "function": {"type": "string", "description": "function name (or pass address)"}, "address": {"type": "string", "description": "hex address in the function (or pass function)"}}, "required": ["target_id"]}),
185
-
("read", "re_search_code", _t.search_code, "Search the WHOLE binary's code (not just already-decompiled bodies, which re_search_decompiled covers): a decompile-on-demand grep (`query`) over a BOUNDED candidate set you name in `functions` (so you control the cost — an unbounded whole-binary decompile is intentionally NOT offered), OR a BYTE/opcode pattern (`bytes_pattern`, hex pairs) / an IMMEDIATE constant (`immediate`) scanned across the mapped image (radare2 `/xj`//`/vj`, mapped to the containing function). To find CALLERS of a symbol/sink use re_xrefs (whole-program, indexed) — this does NOT duplicate it. Paginated. QUERY: records an Observation; adds no graph nodes.", {"type": "object", "properties": {"target_id": {"type": "string"}, "bytes_pattern": {"type": "string", "description": "hex byte pattern to scan for, e.g. 'deadbeef' or '48 8b'"}, "immediate": {"type": "string", "description": "an immediate/constant value to find (hex or decimal)"}, "functions": {"type": "array", "description": "bound the decompile-on-demand grep to these functions (with `query`)"}, "query": {"type": "string", "description": "substring to grep in the decompiled bodies of `functions`"}, "offset": {"type": "integer"}, "limit": {"type": "integer"}}, "required": ["target_id"]}),
185
+
("read", "re_search_code", _t.search_code, "Search the WHOLE binary's code (not just already-decompiled bodies, which re_search_decompiled covers): a decompile-on-demand grep (`query`) over a BOUNDED candidate set you name in `functions` (so you control the cost — an unbounded whole-binary decompile is intentionally NOT offered), OR a BYTE/opcode pattern (`bytes_pattern`, hex pairs) / an IMMEDIATE constant (`immediate`) scanned across the mapped image — a FAST memory scan over the WARM Ghidra project (reuses the analysis, no whole-binary re-scan; radare2 raw-scan fallback when Ghidra's off), each hit mapped to its containing function. To find CALLERS of a symbol/sink use re_xrefs (whole-program, indexed) — this does NOT duplicate it. Paginated. QUERY: records an Observation; adds no graph nodes.", {"type": "object", "properties": {"target_id": {"type": "string"}, "bytes_pattern": {"type": "string", "description": "hex byte pattern to scan for, e.g. 'deadbeef' or '48 8b'"}, "immediate": {"type": "string", "description": "an immediate/constant value to find (hex or decimal)"}, "functions": {"type": "array", "description": "bound the decompile-on-demand grep to these functions (with `query`)"}, "query": {"type": "string", "description": "substring to grep in the decompiled bodies of `functions`"}, "offset": {"type": "integer"}, "limit": {"type": "integer"}}, "required": ["target_id"]}),
186
186
("run", "re_script", _t.run_script, "ESCAPE-HATCH: run an AGENT-SUPPLIED PyGhidra Python-3 script against a target's already-built WARM Ghidra project and get its JSON output — full Ghidra-API power (data-flow slicing, BSim/FID, stack-frame analysis, custom P-Code walks) WITHOUT a pre-built tool per capability, for a query the other re_* verbs don't cover. GHIDRA-ONLY (radare2 unsupported — errors if headless Ghidra isn't the active backend). Runs inside the SAME hardened sandbox every probe uses (--network none, read-only rootfs, --cap-drop ALL, non-root) and opens the warm project READ-ONLY, so your script can inspect but NEVER mutate/corrupt it; the target binary is NEVER executed (Ghidra statically analyzes). WARM-ONLY: if the target has no warm project yet it errors → run re_analyze(target) first (this never runs a cold analysis). CONTRACT (same as HexGraph's built-in postScripts): your script receives `out_path = getScriptArgs()[0]` and MUST write its JSON result to that path — HexGraph reads it back and returns it. Tiny example: `import json; fm = currentProgram.getFunctionManager(); n = len(list(fm.getFunctions(True))); f = open(getScriptArgs()[0], 'w'); f.write(json.dumps({'function_count': n})); f.close()`. program / currentProgram / flat / monitor / out_path / getScriptArgs() and the ghidra.* API are in scope — it's Python 3 (write JSON to out_path, or assign the `result` variable). GATED: opt-in via features.ghidra.scripting (hidden until enabled — it's an arbitrary-code-in-sandbox surface). Records a `script` Observation; adds NO graph nodes. Long output is truncated to max_chars (default 6000, clamped) with a marker; obs_get reads the full payload.",
187
187
{"type": "object", "properties": {"target_id": {"type": "string"}, "script": {"type": "string", "description": "PyGhidra/Jython source; writes its JSON result to getScriptArgs()[0]"}, "max_chars": {"type": "integer", "description": _MAX_CHARS_DESC}}, "required": ["target_id", "script"]}),
188
188
("read", "re_search_symbols_project", _t.search_symbols_project, "Search a symbol/function NAME pattern across ALL targets in a project -> which target(s) DEFINE or IMPORT it (the name analogue of re_yara_sweep). For each non-archived target it tests the pattern against its imports/exports/defined symbols (and function names when known), sourced from stored recon metadata + prior re_binutils_facts Observations — no per-target decompile, no probe. Returns {pattern, matches:[{target_id, name, kind}], scanned, targets_without_symbols}. Use to locate which binary in a firmware defines a given sink or a shared helper. `regex=true` for a regex; `kind` scopes to imports|exports|defined. A target with no stored symbols is listed in targets_without_symbols (re_binutils_facts it first) so a miss isn't read as authoritative. QUERY: reads the substrate; adds no graph nodes.", {"type": "object", "properties": {"project_id": {"type": "string"}, "pattern": {"type": "string", "description": "substring (or regex) to match against symbol/function names"}, "kind": {"type": "string", "enum": ["imports", "exports", "defined", "all"], "description": "scope (default all)"}, "regex": {"type": "boolean"}, "limit": {"type": "integer", "description": "max matches to return"}}, "required": ["project_id", "pattern"]}),
0 commit comments