Skip to content

idasql-inspired analysis layer: SQLite index, SQL query, Ghidra daemon#19

Open
Night1099 wants to merge 21 commits into
masterfrom
feat/idasql-index-ghidra-daemon
Open

idasql-inspired analysis layer: SQLite index, SQL query, Ghidra daemon#19
Night1099 wants to merge 21 commits into
masterfrom
feat/idasql-index-ghidra-daemon

Conversation

@Night1099

Copy link
Copy Markdown
Collaborator

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.h parser, and a warm
per-project Ghidra daemon with transparent routing. Static findings that
used to live only in a flat kb.h are now also queryable as structured data,
and repeat Ghidra work (decompile / export / kb-apply) skips the ~3s JVM cold
start.

The whole branch was reviewed end-to-end and then validated on a real binary
(Mount & Blade: Warband mb_warband.exe, 32-bit DX9) — which surfaced and fixed
a real kb-apply transaction bug that only appears against actual Ghidra.

What's new

retools/index.py — per-game SQLite index (GameIndex)

  • Schema for funcs/names/xrefs/strings/imports/entries/segments/blocks, plus
    callers / callees / grep views.
  • WAL-with-DELETE fallback, schema-version guard, bit-63 address guard.
  • Source-priority upsert: authoritative Ghidra rows overwrite provisional
    bootstrap rows, and re-running bootstrap can never downgrade them back.
  • index status <Game> CLI.

retools/query.py — read-only SQL front-end

  • query <Game> "SELECT …" over index.db via a file:…?mode=ro connection
    (can never mutate the index). Text + --json envelope, --list-tables,
    --schema TABLE.

retools/kb.py — one kb.h parser

  • Single source of truth for the kb.h grammar; the three former duplicate
    parsers (decompiler, context, bootstrap) now delegate to it.

retools/pyghidra_backend.pyexport + kb-apply

  • export seeds index.db with authoritative source='ghidra'
    funcs/names/xrefs/blocks.
  • kb-apply pushes kb.h names/labels/prototypes into the Ghidra project.
  • All Ghidra imports are lazy so the module loads with no JDK.

retools/ghidra_server.py + ghidra_client.py — per-project daemon

  • Warm Ghidra program on port 27043 (livetools owns 27042). decompile,
    export, and kb-apply route through a live daemon automatically;
    RETOOLS_GHIDRA_COLD=1 / --cold forces cold. Tracks itself in
    patches/<Game>/ghidra/.state.json.

bootstrap.py / context.py

  • Bootstrap seeds index.db (source='bootstrap') from pefile data.
  • context assemble resolves callees from index.db when present, falling
    back to the disassembly scan otherwise.

Real-world validation (Warband)

Ran the full pipeline on mb_warband.exe:

Step Result
bootstrap seeds index.db (9,648 funcs, 35,201 strings, 376 imports) + kb.h
Ghidra analyze ~96s, reusable project
export (via daemon) funcs=10,243, names=10,243, xrefs=220,237, blocks=137,368
source-priority funcs = 12,502 = 10,243 Ghidra + 2,258 surviving bootstrap (no downgrade)
warm decompile 0.46s → 0.15s (cached DecompInterface) vs 3.3s cold
kb-apply 2,890 functions renamed from kb.h, persisted + re-exported
context assemble real named callee edges from the index

master vs this branch, same task: master reaches the same answer but with
16 modules (no index/query/ghidra_server), a bootstrap that produces no
index.db, ~4s cold decompiles with no warm path, and context callees
limited to edx: indirect call placeholders. The improvements don't change the
answer — they make the toolkit queryable and fast to iterate (SQL over
220k xrefs, 0.15s warm decompiles, named cross-references).

Review findings fixed

Correctness

  • Ghidra daemon cross-project routing hardened: pid-first liveness check +
    server-side game-identity handshake + dropped SO_REUSEADDR.
  • export default --db derives from --project (was the binary stem →
    wrong dir); _iter_blocks keys blocks by containing-function entry.
  • _route_daemon propagates socket.timeout instead of silently cold-retrying
    into a mid-operation daemon; sends absolute paths (cwd-independent).
  • context.assemble: verifies the start is a known function before trusting an
    empty callee result, falls back on a schemaless index.db (was crashing),
    recovers indirect-call markers, and prefers fresh kb.h names.
  • index.db path anchoring unified; schema-version repaired on interrupted
    first open; decompiler.py script-mode import fixed.
  • kb-apply transaction bug (found on real Ghidra): removed the manual
    program.save() that fails with "Unable to lock due to active transaction"
    inside pyghidra's open_program context — persist via project.save on exit
    (cold) / program close (daemon); @ entries at data addresses become labels,
    not bogus functions.

Cleanup / conventions

  • ix_xrefs_from_func index; narrowed catch-all excepts; single-threaded daemon
    loop (removes the lock, close-variant split, thread list, idle self-connect);
    reused framing; error response on malformed frames; cached DecompInterface;
    parse kb.h once per assemble; shared resolve_db; one bootstrap string
    sweep; extracted shared _kb_apply_txn.

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.

Still needs manual verification on a Ghidra-equipped host

The real Ghidra export / kb-apply class paths and the daemon warm-program
lifecycle were exercised on Warband in this branch; a second independent host
run is worthwhile before merge.

🤖 Generated with Claude Code

Night1099 and others added 21 commits July 16, 2026 13:15
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).
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>
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>
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