idasql-inspired analysis layer: SQLite index, SQL query, Ghidra daemon - #19
Conversation
Create new kb.py module with single source of truth for parsing kb.h knowledge-base files. Provides: - parse_kb(text_or_path) -> Kb with functions, globals, typedefs - KbFunction, KbGlobal, Kb dataclasses - extract_function_name() for signature parsing - read_existing_addresses() for bootstrap dedup Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement GameIndex writer with schema for per-game analysis results. Provides transactional replace-by-source, WAL with DELETE fallback, schema-version guard, and read-only connections. Skips addresses with bit 63 set (out of signed-64 range) with stderr warning. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…ce overwrite 1. Extend _is_addr_col to include from_func column (stores function-start addresses in xrefs that must be range-checked like other address columns). 2. Change INSERT to INSERT OR REPLACE to allow authoritative Ghidra rows (source='ghidra') to overwrite provisional bootstrap rows (source='bootstrap') at overlapping addresses. 3. Update replace() docstring to document last-writer-wins cross-source overwrite. Add two new tests: - test_cross_source_overwrite: verify bootstrap row replaced by ghidra at same address - test_from_func_high_bit_skipped: verify from_func column bit-63 guard works Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Applies kb.h function names, prototypes, global labels, and typedefs into an analyzed Ghidra program in one transaction via _kb_apply_program.
Adds ghidra_client.py/ghidra_server.py cloning the livetools daemon pattern on port 27043 for a warm per-project Ghidra program, so repeat decompiles skip the ~3s JVM cold start. pyghidra_backend's decompile/ export/kb_apply now route through a live daemon transparently when one is running for the project, falling back to the existing cold in-process path otherwise (RETOOLS_GHIDRA_COLD=1 / --cold forces cold).
…ong-binary reuse, cover routing branch
Add retools.kb, retools.index, retools.query, retools.ghidra_server, and retools.ghidra_client to the import check module list. All five modules import cleanly without Ghidra installed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ends Documents the Task 1-10 additions (retools.index, retools.query, pyghidra_backend export/kb-apply, ghidra_server/ghidra_client daemon) across all four IDE trees, and reframes decompiler backend guidance from "two peer backends" to "Ghidra primary (indexed, daemon-backed, kb-applied); r2ghidra zero-setup fallback and second opinion." Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dra daemon state file
…aden query errors
Addresses the findings from the branch-wide review and a real bug found running the full Ghidra path on a real binary (Warband mb_warband.exe). Correctness - ghidra daemon: reject cross-project routing. is_daemon_alive checks the recorded pid before the shared port, and the server validates the game per command (wrong_project -> client cold-falls-back); dropped SO_REUSEADDR so a second daemon can't silently bind an occupied port. - pyghidra export: default --db derives from --project, not the binary stem (was writing patches/<stem>/index.db); _iter_blocks keys blocks by the containing function entry, not the block's own start. - _route_daemon: a socket.timeout now propagates instead of silently cold- retrying into a daemon mid-operation (no double-execution); sends absolute binary/db/kb paths so the daemon can't resolve them against a different cwd. - context.assemble: verify the queried start is a known function before trusting an empty callee result, fall back to the scan on a schemaless index.db (was crashing), recover indirect-call markers, and let a fresh kb.h name outrank the exported Ghidra name. - index.db path anchoring: bootstrap (writer) and context (reader) now resolve the same <project>/index.db from the same --project. - index schema-version: repair a missing version row on open instead of leaving the DB permanently versionless after an interrupted first open. - decompiler.py: put repo root on sys.path so `python retools/decompiler.py` resolves its package imports (script-mode invocation was broken). - kb-apply (found on real Ghidra): remove the manual program.save() that fails with "Unable to lock due to active transaction" inside pyghidra's open_program context; persist via pyghidra's project.save on context exit (cold) / by closing the program (daemon). @ entries at data addresses (RTTI vtables, string refs) now become labels, not bogus functions. Cleanup / conventions - index: add ix_xrefs_from_func; source-priority upsert so re-running bootstrap can't downgrade authoritative Ghidra funcs; narrow catch-alls in GameIndex.close and ghidra_client.read_state. - daemon: single-threaded accept loop (commands already serialize), removing the lock, the close-variant split, the unbounded thread list, and the idle self-connect; reuse ghidra_client's framing; answer malformed frames with an error instead of a silent pass; cache the DecompInterface for warm repeats. - context: parse kb.h once per assemble. - query/index: share resolve_db so both CLIs print the same guidance. - bootstrap: one string sweep feeds both the error-string KB seed and the index seed. - extract _kb_apply_txn shared by cold + daemon paths. Tests: 123 passed / 1 skipped across all changed modules, including new coverage for the daemon lifecycle, identity handshake, source-priority, empty-xrefs fallback, and the kb-apply label path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… their own address appended
|
Can test later today |
…e lock A forced-cold decompile/export/kb-apply (--cold or RETOOLS_GHIDRA_COLD=1) opened the project in-process even when this project's own daemon was live. The Ghidra project lock is exclusive, so the open died with a raw LockException traceback instead of the [error] ... string every other failure path returns. _route_daemon now checks daemon liveness before the cold branch and, when the daemon holds the lock, returns a clean error telling the user to stop it or drop --cold; with no live daemon it falls through to the cold path as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review + follow-up fix
What the PR doesAdds an idasql-inspired analysis layer: a per-game SQLite index ( Verification run
Solid overall — the core is functional and the performance win is real. Findings1. [Medium — fixed in 2. [Low, not fixed] Daemon Minor notes: the single-connection daemon uses a 300s socket timeout, so a stalled client blocks others for up to 5 min (fine for local single-user use); #2 and the minor notes are non-blocking. |
Summary
Adds an idasql-inspired analysis layer to the RE toolkit: a per-game SQLite
index, a read-only SQL front-end, a consolidated
kb.hparser, and a warmper-project Ghidra daemon with transparent routing. Static findings that
used to live only in a flat
kb.hare now also queryable as structured data,and repeat Ghidra work (decompile / export / kb-apply) skips the ~3s JVM cold
start.
What's new
retools/index.py— per-game SQLite index (GameIndex)callers/callees/grepviews.bootstrap rows, and re-running bootstrap can never downgrade them back.
index status <Game>CLI.retools/query.py— read-only SQL front-endquery <Game> "SELECT …"overindex.dbvia afile:…?mode=roconnection(can never mutate the index). Text +
--jsonenvelope,--list-tables,--schema TABLE.retools/kb.py— onekb.hparserkb.hgrammar; the three former duplicateparsers (decompiler, context, bootstrap) now delegate to it.
retools/pyghidra_backend.py—export+kb-applyexportseedsindex.dbwith authoritativesource='ghidra'funcs/names/xrefs/blocks.
kb-applypusheskb.hnames/labels/prototypes into the Ghidra project.retools/ghidra_server.py+ghidra_client.py— per-project daemondecompile,export, andkb-applyroute through a live daemon automatically;RETOOLS_GHIDRA_COLD=1/--coldforces cold. Tracks itself inpatches/<Game>/ghidra/.state.json.bootstrap.py/context.pyindex.db(source='bootstrap') from pefile data.context assembleresolves callees fromindex.dbwhen present, fallingback to the disassembly scan otherwise.
index.db(9,648 funcs, 35,201 strings, 376 imports) +kb.hanalyzeexport(via daemon)kb-applykb.h, persisted + re-exportedcontext assemblemaster vs this branch, same task: master reaches the same answer but with
16 modules (no
index/query/ghidra_server), a bootstrap that produces noindex.db, ~4s cold decompiles with no warm path, andcontextcalleeslimited to
edx: indirect callplaceholders. The improvements don't change theanswer — they make the toolkit queryable and fast to iterate (SQL over
220k xrefs, 0.15s warm decompiles, named cross-references).
Review findings fixed
Correctness
server-side game-identity handshake + dropped
SO_REUSEADDR.exportdefault--dbderives from--project(was the binary stem →wrong dir);
_iter_blockskeys blocks by containing-function entry._route_daemonpropagatessocket.timeoutinstead of silently cold-retryinginto a mid-operation daemon; sends absolute paths (cwd-independent).
context.assemble: verifies the start is a known function before trusting anempty callee result, falls back on a schemaless
index.db(was crashing),recovers indirect-call markers, and prefers fresh
kb.hnames.index.dbpath anchoring unified; schema-version repaired on interruptedfirst open;
decompiler.pyscript-mode import fixed.kb-applytransaction bug (found on real Ghidra): removed the manualprogram.save()that fails with "Unable to lock due to active transaction"inside pyghidra's
open_programcontext — persist viaproject.saveon exit(cold) / program close (daemon);
@entries at data addresses become labels,not bogus functions.
Cleanup / conventions
ix_xrefs_from_funcindex; narrowed catch-all excepts; single-threaded daemonloop (removes the lock, close-variant split, thread list, idle self-connect);
reused framing; error response on malformed frames; cached DecompInterface;
parse
kb.honce per assemble; sharedresolve_db; one bootstrap stringsweep; extracted shared
_kb_apply_txn.Code Reveiw ran and done CODE_REVIEW.md