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: detach Ghidra enrichment on reveal — target_reveal_dir hung 2+ hours
Revealing a firmware child materializes its recon nodes AND (if Ghidra
headless enrichment is enabled) runs Ghidra's full-inventory analysis on it —
previously synchronous, inline, inside the reveal call. reveal_dir loops over
every hidden child under a directory prefix and calls this per child with no
aggregate bound. A real incident: revealing an Erlang erts/bin directory (a
dozen-plus binaries — erl, beam.smp, dialyzer, run_erl, ...) ran a cold
headless Ghidra full-analysis on each one sequentially, hanging the MCP call
for 2+ hours. Same architectural bug class as promote_file (fixed in #274
earlier tonight) — a bulk operation fanning out N synchronous heavy per-item
calls inside one blocking request, no detachment, no bound.
Split _materialize_on_reveal: node materialization from already-stored recon
facts stays synchronous (fast, no re-run). Ghidra enrichment now runs
DETACHED via a new `ghidra_enrich` Task type, reusing the
create_task/spawn_detached_task infrastructure built for the promote_file
fix. Idempotent + self-healing, same pattern: a target already marked
enriched or with a queued/running enrichment task is skipped; a task that
ended without marking success (died, or a soft failure) gets retried on the
next reveal call rather than silently staying unenriched forever.
set_visible/reveal_dir now report enrichment_queued so callers (MCP, REST,
the FilesystemBrowser UI) know background work is still in flight rather
than assuming a fast return means nothing happened.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: batch reveal_dir's Ghidra enrichment into one detached process
Review of #275 flagged a real gap: reveal_dir spawned one INDEPENDENT
detached process per revealed binary needing enrichment. A directory with a
dozen-plus binaries (the incident's own erts/bin case) would launch that
many CONCURRENT cold headless Ghidra containers (--memory 2g --cpus 2.0
each) — genuine new resource contention the original blocking code never
had (it ran them one at a time, just synchronously).
reveal_dir now collects every target that needs enrichment across the whole
call and queues ONE `ghidra_enrich_batch` task that processes them
sequentially in a single detached process — still off the request path, but
no concurrency blowup. set_visible's single-target path is unaffected (spawning
one process for one target was never the problem). This also incidentally
restores reveal_dir's prior single-commit-point atomicity (the per-child
commit the review flagged as a second, smaller issue) as a side effect of
consolidating the spawn to one place.
Also: mark the Task failed (not stuck "queued" forever) if spawn_detached_task
itself raises, in both the single and batch paths — a permanently-queued task
would wrongly satisfy the already-running self-heal check and block every
future retry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
("write", "target_set_visible", _t.set_visible, "REVEAL (visible=true) or re-HIDE (visible=false) ONE target in the curated graph. Firmware ELF children are hidden by default — unpack registers each (searchable via target_list(include_hidden=true), addressable, recon-enriched) but they'd flood the graph, so they contribute NOTHING to it until revealed. Revealing materializes the target's recon symbol/string nodes from the already-stored facts (no re-run), so it joins the graph + Targets pane. Use after target_list(include_hidden=true)/fs_list surfaces a child worth analyzing. Returns {target_id, name, visible, materialized}.",
128
+
("write", "target_set_visible", _t.set_visible, "REVEAL (visible=true) or re-HIDE (visible=false) ONE target in the curated graph. Firmware ELF children are hidden by default — unpack registers each (searchable via target_list(include_hidden=true), addressable, recon-enriched) but they'd flood the graph, so they contribute NOTHING to it until revealed. Revealing materializes the target's recon symbol/string nodes from the already-stored facts (no re-run) so it joins the graph + Targets pane immediately; optional Ghidra enrichment (if enabled) runs DETACHED in the background — a cold headless pass can take minutes, this call does not wait for it. Use after target_list(include_hidden=true)/fs_list surfaces a child worth analyzing. Returns {target_id, name, visible, materialized, enrichment_queued}.",
("write", "target_reveal_dir", _t.reveal_dir, "REVEAL every HIDDEN child of a firmware whose rootfs path is under `prefix` (e.g. prefix='usr/sbin' reveals all ELFs in /usr/sbin) — the bulk counterpart to target_set_visible for bringing a whole directory of binaries into the curated graph at once. An empty prefix reveals ALL hidden children. Materializes each revealed child's recon nodes from stored facts (no re-run). `target_id` is the firmware. Returns {firmware_target_id, prefix, revealed, target_ids}.",
130
+
("write", "target_reveal_dir", _t.reveal_dir, "REVEAL every HIDDEN child of a firmware whose rootfs path is under `prefix` (e.g. prefix='usr/sbin' reveals all ELFs in /usr/sbin) — the bulk counterpart to target_set_visible for bringing a whole directory of binaries into the curated graph at once. An empty prefix reveals ALL hidden children. Materializes each revealed child's recon nodes from stored facts (no re-run) — fast, even for a directory with many binaries; optional per-binary Ghidra enrichment (if enabled) runs DETACHED in the background instead of blocking this call (a dozen+ binaries analyzed sequentially can take hours). `target_id` is the firmware. Returns {firmware_target_id, prefix, revealed, target_ids, enrichment_queued}.",
131
131
{"type": "object", "properties": {"project_id": {"type": "string"}, "target_id": {"type": "string", "description": "the firmware target whose children to reveal"}, "prefix": {"type": "string", "description": "rootfs directory prefix (e.g. 'usr/sbin'); empty reveals all hidden children"}}, "required": ["project_id", "target_id"]}),
0 commit comments