Skip to content

Commit 76c8df0

Browse files
branoverclaude
andauthored
fix: resident Ghidra bridge empties decompiles over time (DecompInterface leak + reused TaskMonitor) (#271)
* fix: resident Ghidra bridge empties decompiles over time (DecompInterface leak + reused TaskMonitor) On a long-lived resident Ghidra bridge (re_bridge_start), re_decompile_* started returning empty pseudo-C (signature + callees, no body) for many functions after hours of use. Two independent container-side defects in the bridge's pyghidra server caused it — both masked on the headless path (one process per call, so OS teardown reclaims everything) and surfacing only on the long-lived resident bridge. 1. Undisposed DecompInterface (dominant). _focus_facts and taint_core built a DecompInterface per call and never dispose()d it. Each spawns a native `decompile` subprocess + I/O threads, so a resident server accumulates them until pthread_create fails (EAGAIN) and decompileFunction can no longer start its subprocess -> decompileCompleted()==false -> empty body. Dispose in a finally (per request) / before return (taint reuses one across its loop). The emulator already disposed, so this was an oversight. 2. Reused ConsoleTaskMonitor. The probe minted one monitor and serve_bridge reused it for every RPC. decompileFunction(f, 60, monitor) cancels the monitor it is handed on the 60s timeout, and a ConsoleTaskMonitor stays cancelled, so a single slow function poisoned every later decompile into an empty body. Mint a fresh monitor per request (pass the ConsoleTaskMonitor class as a factory). The empty body is a FRESH failed decompile, not a stale cache read: the decompile path never reads back from the Observation store. (The identical content_hash across a target's observations is by design — it is the target's sha256, scoping obs to the exact bytes; dedup also keys on tool+args+result_kind, so it is not defeated.) Tests: added offline regression tests — a fresh-monitor-per-request check and two DecompInterface-dispose checks (success + error path), all of which fail on the old code. Full offline fast tier green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(taint): dispose the taint DecompInterface in a finally + cover it offline Addresses the two non-blocking merge-gate review findings on the resident-bridge decompiler-leak fix. taint_core reused ONE DecompInterface across its candidate loop but disposed it only in a bare pre-return statement, so an unsuppressed Java error in the loop (or in openProgram) leaked it on the error path — the same defect class this PR fixes for _focus_facts. Wrap the loop + return in try/finally (openProgram now inside the try), matching _focus_facts. Add two offline regression tests: taint_core disposes on the success path, and — the one that locks in the finding — disposes via the finally when the try raises (fails on the pre-finally pre-return dispose). The re-indent is logic-preserving (git diff -w shows only the try/finally wrapper). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1a9c52f commit 76c8df0

3 files changed

Lines changed: 451 additions & 220 deletions

File tree

src/hexgraph/sandbox/probes/ghidra_bridge_probe.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ def main() -> int:
4040
sys.stderr.write(f"ghidra_bridge_probe: project resident (cached={cached}); "
4141
f"serving JSON RPC on 0.0.0.0:{PORT}\n")
4242
sys.stderr.flush()
43-
L.serve_bridge("0.0.0.0", PORT, program, flat, ConsoleTaskMonitor()) # blocks forever
43+
# Pass the CLASS (a per-request monitor factory), not an instance: serve_bridge mints a
44+
# fresh ConsoleTaskMonitor per request so a decompile timeout — which cancels the monitor
45+
# it's handed, permanently for a ConsoleTaskMonitor — can't poison later decompiles into
46+
# empty bodies. See pyghidra_lib._serve_one.
47+
L.serve_bridge("0.0.0.0", PORT, program, flat, ConsoleTaskMonitor) # blocks forever
4448
except Exception as exc: # noqa: BLE001
4549
sys.stderr.write(f"ghidra_bridge_probe: {exc}\n")
4650
return 4

0 commit comments

Comments
 (0)