[GH-ISSUE #4561] [Bug] Ghidra synchronous Java prerequisite probes can outlive their timeout and block cancellation #2371

Closed
opened 2026-10-17 21:03:51 +00:00 by bsakaguchi · 4 comments
Owner

Originally created by @morluto on GitHub (Oct 16, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4561

Before submitting

  • Searched open and closed reports for Java probe/spawnSync findings.
  • Removed credentials and sensitive target data.

Area

Provider or capability discovery

Observed behavior

Source-only finding: Ghidra prerequisite discovery calls spawnSync(java, ..., {timeout:5000,maxBuffer:1MiB}) with default SIGTERM and no process-family owner. The synchronous call blocks the event loop. Its timeout does not prove bounded completion or cleanup of a noncooperative command and descendants.

Reproduction

NOT_ENOUGH_INFO: no hanging Java or descendant runtime reproduction performed. Inspected systemGhidraInstallationHost().probeJava at 8116dc0651; current audit extraction moves the same execution into src/ghidra/GhidraJavaProbe.ts without changing lifecycle authority.

Expected behavior

Prerequisite discovery should join an owned, cancellable probe and report unresolved cleanup when closure cannot be confirmed. Reuse existing process supervision and identity primitives; do not treat a timeout, numeric PID, or root-only termination as family cleanup proof.

Impact and scope

Source-derived possibility of indefinitely stalled CLI doctor/provider discovery or MCP event loop and surviving descendants. Actual affected users/frequency/platform-specific outcomes: NOT_ENOUGH_INFO. No claim of a real JVM failure or leaked process.

REA version

8116dc0651 (6.5.0 source)

Runtime and platform

Pinned Node 24.18.0 semantics; platform reproduction unavailable.

Target and artifact context

No reverse-engineering target needed; configured Java prerequisite command only.

Interface

CLI and MCP discovery (source-derived)

Earliest failure stage

Setup or provider admission

Relevant output or evidence

Repository call uses synchronous Node execution, a 5s timeout and no external process owner. Pinned Node 24.18.0 documentation states that synchronous spawn waits for complete exit even after the timeout signal; a child that handles SIGTERM without exiting can keep the parent waiting. Ordinary parent termination is not proof of descendant closure.

Typed outcome or recovery expected

Preserve cancellation/deadline and exact owned cleanup facts, including retry if cleanup fails.

Analysis or suspected cause

Synchronous prerequisite admission cannot use the current asynchronous process supervision lifecycle. A correct change needs coordinated provider discovery/admission ownership rather than merely changing the kill signal or moving spawnSync to a wrapper.

Additional context

Related #4560 preserves probe failure classification/output; that work does not establish bounded lifecycle completion. Related #4227 tracks broader ownership invariants. Proposed checks: a controlled selected Java command that handles SIGTERM, descendants with inherited output, cancellation while probing, final cleanup and retry; run through public CLI/MCP with external fixture ownership.

Originally created by @morluto on GitHub (Oct 16, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4561 ### Before submitting - [x] Searched open and closed reports for Java probe/spawnSync findings. - [x] Removed credentials and sensitive target data. ### Area Provider or capability discovery ### Observed behavior Source-only finding: Ghidra prerequisite discovery calls `spawnSync(java, ..., {timeout:5000,maxBuffer:1MiB})` with default SIGTERM and no process-family owner. The synchronous call blocks the event loop. Its timeout does not prove bounded completion or cleanup of a noncooperative command and descendants. ### Reproduction NOT_ENOUGH_INFO: no hanging Java or descendant runtime reproduction performed. Inspected `systemGhidraInstallationHost().probeJava` at 8116dc06512b4609d8db63cebc9f2ff8cb964151; current audit extraction moves the same execution into `src/ghidra/GhidraJavaProbe.ts` without changing lifecycle authority. ### Expected behavior Prerequisite discovery should join an owned, cancellable probe and report unresolved cleanup when closure cannot be confirmed. Reuse existing process supervision and identity primitives; do not treat a timeout, numeric PID, or root-only termination as family cleanup proof. ### Impact and scope Source-derived possibility of indefinitely stalled CLI doctor/provider discovery or MCP event loop and surviving descendants. Actual affected users/frequency/platform-specific outcomes: NOT_ENOUGH_INFO. No claim of a real JVM failure or leaked process. ### REA version 8116dc06512b4609d8db63cebc9f2ff8cb964151 (6.5.0 source) ### Runtime and platform Pinned Node 24.18.0 semantics; platform reproduction unavailable. ### Target and artifact context No reverse-engineering target needed; configured Java prerequisite command only. ### Interface CLI and MCP discovery (source-derived) ### Earliest failure stage Setup or provider admission ### Relevant output or evidence Repository call uses synchronous Node execution, a 5s timeout and no external process owner. [Pinned Node 24.18.0 documentation](https://github.com/nodejs/node/blob/v24.18.0/doc/api/child_process.md#child_processspawnsynccommand-args-options) states that synchronous spawn waits for complete exit even after the timeout signal; a child that handles SIGTERM without exiting can keep the parent waiting. Ordinary parent termination is not proof of descendant closure. ### Typed outcome or recovery expected Preserve cancellation/deadline and exact owned cleanup facts, including retry if cleanup fails. ### Analysis or suspected cause Synchronous prerequisite admission cannot use the current asynchronous process supervision lifecycle. A correct change needs coordinated provider discovery/admission ownership rather than merely changing the kill signal or moving spawnSync to a wrapper. ### Additional context Related #4560 preserves probe failure classification/output; that work does not establish bounded lifecycle completion. Related #4227 tracks broader ownership invariants. Proposed checks: a controlled selected Java command that handles SIGTERM, descendants with inherited output, cancellation while probing, final cleanup and retry; run through public CLI/MCP with external fixture ownership.
Author
Owner

@Nitin13-git commented on GitHub (Oct 17, 2026):

Reproduced on current main (origin/main at the time, macOS arm64, Node 24.18.0), with a stub java that ignores SIGTERM (trap "" TERM; sleep 20) selected through javaHome:

inspectGhidraInstallation({ environment: { PATH }, javaHome: "<dir with bin/java stub>" })
  • The call returned after 20.5 s, not the 5 s probe timeout, and a 100 ms setInterval ticked 0 times during the call, so the whole event loop was blocked (spawnSync at GhidraInstallation.ts probeJava).
  • A standalone spawnSync with the same options and killSignal: "SIGKILL" returned after 5.0 s; the same with an extra background child holding the output pipe also returned after 5.0 s. I have not applied this anywhere; it would bound the wait but not the 5 s event-loop block, cancellation, or descendant cleanup.

The proper fix needs an owned, cancellable (async) probe, but the probe sits behind synchronous provider methods (inspectAvailability(), inspectReadinessSnapshot(), createClient(), all part of the shared AnalysisProvider interface). Which design do you want?

  1. Make those provider methods async across the interface, or
  2. Probe once asynchronously during provider preparation and give the synchronous callers the cached observation (with a defined freshness rule).

The issue mentions an audit extraction into GhidraJavaProbe.ts; if that is already in progress, I will not duplicate it. Happy to implement whichever direction you choose.

<!-- gh-comment-id:6227278217 --> @Nitin13-git commented on GitHub (Oct 17, 2026): Reproduced on current `main` (`origin/main` at the time, macOS arm64, Node 24.18.0), with a stub `java` that ignores SIGTERM (`trap "" TERM; sleep 20`) selected through `javaHome`: ```ts inspectGhidraInstallation({ environment: { PATH }, javaHome: "<dir with bin/java stub>" }) ``` - The call returned after **20.5 s**, not the 5 s probe timeout, and a 100 ms `setInterval` ticked **0 times** during the call, so the whole event loop was blocked (`spawnSync` at `GhidraInstallation.ts` `probeJava`). - A standalone `spawnSync` with the same options and `killSignal: "SIGKILL"` returned after 5.0 s; the same with an extra background child holding the output pipe also returned after 5.0 s. I have not applied this anywhere; it would bound the wait but not the 5 s event-loop block, cancellation, or descendant cleanup. The proper fix needs an owned, cancellable (async) probe, but the probe sits behind synchronous provider methods (`inspectAvailability()`, `inspectReadinessSnapshot()`, `createClient()`, all part of the shared `AnalysisProvider` interface). Which design do you want? 1. Make those provider methods async across the interface, or 2. Probe once asynchronously during provider preparation and give the synchronous callers the cached observation (with a defined freshness rule). The issue mentions an audit extraction into `GhidraJavaProbe.ts`; if that is already in progress, I will not duplicate it. Happy to implement whichever direction you choose.
Author
Owner

@morluto commented on GitHub (Oct 17, 2026):

The original source-only finding now has a bounded runtime reproduction on main 71929ed37 (Node24.18.0, Darwin arm64), including public node scripts/rea.mjs doctor --json.

A controlled caller-selected Java executable records startup and SIGTERM. The fixture runs under REA's existing external runOwnedCommand/spawnOwnedProviderProcess owner with a seven-second deadline and exact process-group verification. It is an idle Node script posing as the selected prerequisite command, not a real JVM or Ghidra target.

Observed sequential controls:

Case Native probe result External completed task SIGTERM after selected child start
Fast properties reply Returned parsed Java observation Completed, 2.201s including first ownership preparation None
Child exits on SIGTERM Returned failed probe Completed, 5.396s 4.988s
Child ignores SIGTERM, direct probe Did not return External timeout and cleanup, 7.214s 4.987s
Child ignores SIGTERM, public doctor Doctor did not return External timeout and cleanup, 7.248s 4.986s

The direct probe's 100ms timer never fired in the ignored-SIGTERM case; it did fire after both completed direct controls. The doctor case does not instrument its internal timer. Every externally owned fixture group was verified empty after cleanup, and the allocated fixture directory was removed after evidence capture. These observations show outliving the five-second probe timeout and event-loop blocking; they do not claim an indefinitely observed hang, spontaneous JVM failure, or escaped descendants.

Context7 and DeepWiki corroborated the API distinction; the version-specific source was checked against Node24.18.0 child_process documentation: asynchronous spawning enables event-loop progress, while a sent signal alone does not prove exit or descendant cleanup.

The fix remains in progress. The complete migration must cover GhidraInstallationHost/inspection, GhidraProvider's cached readiness and synchronous client creation, registry candidate snapshots/selection, target-free session status, and doctor's currently file-only resource scope. Source review also identified the need to preserve exact interruption provenance and completed output interpretation through final process cleanup. These are integration requirements, not yet separately reproduced defect claims. A synchronous wrapper or kill-signal-only change would leave the ownership and cancellation contract incomplete.

No production code changed in this investigation; no native Ghidra/JVM, Windows/Linux lifecycle, MCP responsiveness, or descendant escape behavior was verified. Heavy Qodo/profiler jobs were not restarted.

<!-- gh-comment-id:6227991410 --> @morluto commented on GitHub (Oct 17, 2026): The original source-only finding now has a bounded runtime reproduction on main 71929ed37 (Node24.18.0, Darwin arm64), including public `node scripts/rea.mjs doctor --json`. A controlled caller-selected Java executable records startup and SIGTERM. The fixture runs under REA's existing external `runOwnedCommand`/`spawnOwnedProviderProcess` owner with a seven-second deadline and exact process-group verification. It is an idle Node script posing as the selected prerequisite command, not a real JVM or Ghidra target. Observed sequential controls: | Case | Native probe result | External completed task | SIGTERM after selected child start | | --- | --- | --- | --- | | Fast properties reply | Returned parsed Java observation | Completed, 2.201s including first ownership preparation | None | | Child exits on SIGTERM | Returned failed probe | Completed, 5.396s | 4.988s | | Child ignores SIGTERM, direct probe | Did not return | External timeout and cleanup, 7.214s | 4.987s | | Child ignores SIGTERM, public doctor | Doctor did not return | External timeout and cleanup, 7.248s | 4.986s | The direct probe's 100ms timer never fired in the ignored-SIGTERM case; it did fire after both completed direct controls. The doctor case does not instrument its internal timer. Every externally owned fixture group was verified empty after cleanup, and the allocated fixture directory was removed after evidence capture. These observations show outliving the five-second probe timeout and event-loop blocking; they do not claim an indefinitely observed hang, spontaneous JVM failure, or escaped descendants. Context7 and DeepWiki corroborated the API distinction; the version-specific source was checked against [Node24.18.0 child_process documentation](https://nodejs.org/download/release/v24.18.0/docs/api/child_process.html#child_processspawnsynccommand-args-options): asynchronous spawning enables event-loop progress, while a sent signal alone does not prove exit or descendant cleanup. The fix remains in progress. The complete migration must cover GhidraInstallationHost/inspection, GhidraProvider's cached readiness and synchronous client creation, registry candidate snapshots/selection, target-free session status, and doctor's currently file-only resource scope. Source review also identified the need to preserve exact interruption provenance and completed output interpretation through final process cleanup. These are integration requirements, not yet separately reproduced defect claims. A synchronous wrapper or kill-signal-only change would leave the ownership and cancellation contract incomplete. No production code changed in this investigation; no native Ghidra/JVM, Windows/Linux lifecycle, MCP responsiveness, or descendant escape behavior was verified. Heavy Qodo/profiler jobs were not restarted.
Author
Owner

@morluto commented on GitHub (Oct 17, 2026):

Prerequisite published in 150e996a86f2bec4982ba87f37bf502a8d5e8214: the shared readiness/status path now awaits owned observations.

  • Registry discovery is registered before production, carries the owner signal, joins every candidate through finalization, and joins shutdown. Status remains readable during shutdown without admitting another probe: candidate readiness is explicitly unknown while finalization is in progress.
  • CLI session status, registered MCP status, provider prompt completion, and affected callers await the observation. MCP preserves producer-authored typed failures instead of inferring cancellation from the latest signal.
  • Readiness and profile failures share one composition owner. Attributed child failures, completed readiness observations, exact cleanup dependencies and typed primary restoration survive mixed outcomes and cleanup retry.

Validation executed: 255 focused tests across 18 files, including registered MCP delayed readiness, status during shutdown, typed cancellation, physical failure during shutdown, denied cleanup/retry, provider switching and SDK-converted schema validity. Another 21 registry tests passed after strengthening the primary-restoration assertion. check:fast, docs:check and 5 README conformance controls passed. Controlled providers/process fixtures establish this scope; no native Ghidra/JVM claim is made.

Bounded completed production session-status tasks against 408704827, two alternating warm repeats plus separate first processes: ordinary status elapsed 0.18080/0.18258 s, CPU 0.28390/0.28324 s, sampled peak family RSS 199.87/199.84 MB, raw cl100k_base reply tokens 115/115. Status during held provider finalization: elapsed 0.18432/0.17863 s, CPU 0.27746/0.28922 s, RSS 199.76/199.25 MB, tokens 115/134 (the candidate honestly reports unknown readiness). No subprocesses launched in these measurements. True cold caches, whole-agent and CLI/MCP transport costs remain unmeasured; no performance improvement claimed.

Compatibility: the commit is marked breaking because candidate availability can now be unknown; consumers that assume only available/unavailable must handle it.

#4561 remains unfinished. The actual synchronous Java producer, installation inspection, Ghidra readiness sharing/cache, synchronous client creation and doctor process cleanup ownership still require migration. Heavy Qodo/profiler workloads stayed stopped.

<!-- gh-comment-id:6228225841 --> @morluto commented on GitHub (Oct 17, 2026): Prerequisite published in `150e996a86f2bec4982ba87f37bf502a8d5e8214`: the shared readiness/status path now awaits owned observations. - Registry discovery is registered before production, carries the owner signal, joins every candidate through finalization, and joins shutdown. Status remains readable during shutdown without admitting another probe: candidate readiness is explicitly `unknown` while finalization is in progress. - CLI session status, registered MCP status, provider prompt completion, and affected callers await the observation. MCP preserves producer-authored typed failures instead of inferring cancellation from the latest signal. - Readiness and profile failures share one composition owner. Attributed child failures, completed readiness observations, exact cleanup dependencies and typed primary restoration survive mixed outcomes and cleanup retry. Validation executed: 255 focused tests across 18 files, including registered MCP delayed readiness, status during shutdown, typed cancellation, physical failure during shutdown, denied cleanup/retry, provider switching and SDK-converted schema validity. Another 21 registry tests passed after strengthening the primary-restoration assertion. `check:fast`, `docs:check` and 5 README conformance controls passed. Controlled providers/process fixtures establish this scope; no native Ghidra/JVM claim is made. Bounded completed production session-status tasks against `408704827`, two alternating warm repeats plus separate first processes: ordinary status elapsed 0.18080/0.18258 s, CPU 0.28390/0.28324 s, sampled peak family RSS 199.87/199.84 MB, raw cl100k_base reply tokens 115/115. Status during held provider finalization: elapsed 0.18432/0.17863 s, CPU 0.27746/0.28922 s, RSS 199.76/199.25 MB, tokens 115/134 (the candidate honestly reports unknown readiness). No subprocesses launched in these measurements. True cold caches, whole-agent and CLI/MCP transport costs remain unmeasured; no performance improvement claimed. Compatibility: the commit is marked breaking because candidate availability can now be `unknown`; consumers that assume only available/unavailable must handle it. **#4561 remains unfinished.** The actual synchronous Java producer, installation inspection, Ghidra readiness sharing/cache, synchronous client creation and doctor process cleanup ownership still require migration. Heavy Qodo/profiler workloads stayed stopped.
Author
Owner

@morluto commented on GitHub (Oct 17, 2026):

Java subprocess boundary fixed on main in cc2e18a265, following shared ownership commits e567033a7 and fb146bf40.

The Java probe now uses bounded asynchronous owned commands, observes complete output before final cleanup, retains physical failures across late cancellation, and preserves exact preparation/process cleanup capabilities. Registry readiness/profile work shares observations only within one active task; later tasks and client admission are fresh. Client factories expose a closeable deferred owner immediately, and doctor joins cancellation and cleanup.

Executed 405 focused tests across 24 files (2 explicit skips), 109 provider/doctor controls rerun after final admission refinement, check:fast, docs:check (5 conformance controls), normal commit/pre-push hooks. Real idle Node fixtures acting as selected Java commands cover active cancellation, SIGTERM resistance with a responsive timer, completed valid/invalid reports followed by cancellation, refused cleanup and exact retry, preparation cleanup without adopting caller-owned error resources, and zero root exit with a detached descendant holding output open. Public compiled doctor --provider ghidra --json returned its own 5-second deadline error in about 5.6s, before the independent 15-second watchdog; its observed process group was empty. Earlier reproduction here needed external cleanup after 7s and direct synchronous probing blocked its timer.

Completed-task comparison against e567033a7, same controlled successful Java-shaped executable and equivalent provider facts; two alternating warm filesystem repeats plus separate first processes:

Task Elapsed baseline/candidate CPU baseline/candidate Sampled peak family RSS Raw reply tokens
One probe 0.059/0.823s 0.041/0.693s 78/235MB 91/91
Three sequential probes in one host 0.088/1.223s 0.079/1.026s 77/235MB 267/267
Public doctor 0.447/1.223s 0.624/1.237s 379/529MB 3133/3133

Ownership adds measurable preparation/verification cost; no speed or memory improvement claimed. CPU is wait4 root plus reaped children; RSS is a sampled PPID-family sum and may miss transient peaks. Tokens use cl100k_base on complete raw replies. True cold caches, full agent, MCP/catalog/retry and real-engine costs remain unmeasured.

Scope: macOS/Node 24.18.0 controlled process and file fixtures, plus public CLI/MCP lifecycle regressions. These are not real JVM/Ghidra behavior or Windows/Linux lifecycle verification. Separate source finding #4672 tracks the remaining synchronous unbounded installation properties read before Java starts; that file-port migration remains audit work. Full codebase audit remains active. Heavy Qodo/profiler workloads stayed stopped.

<!-- gh-comment-id:6229063075 --> @morluto commented on GitHub (Oct 17, 2026): Java subprocess boundary fixed on main in cc2e18a265198492dff1587a343bfb802d8c591b, following shared ownership commits e567033a7 and fb146bf40. The Java probe now uses bounded asynchronous owned commands, observes complete output before final cleanup, retains physical failures across late cancellation, and preserves exact preparation/process cleanup capabilities. Registry readiness/profile work shares observations only within one active task; later tasks and client admission are fresh. Client factories expose a closeable deferred owner immediately, and doctor joins cancellation and cleanup. Executed 405 focused tests across 24 files (2 explicit skips), 109 provider/doctor controls rerun after final admission refinement, check:fast, docs:check (5 conformance controls), normal commit/pre-push hooks. Real idle Node fixtures acting as selected Java commands cover active cancellation, SIGTERM resistance with a responsive timer, completed valid/invalid reports followed by cancellation, refused cleanup and exact retry, preparation cleanup without adopting caller-owned error resources, and zero root exit with a detached descendant holding output open. Public compiled doctor --provider ghidra --json returned its own 5-second deadline error in about 5.6s, before the independent 15-second watchdog; its observed process group was empty. Earlier reproduction here needed external cleanup after 7s and direct synchronous probing blocked its timer. Completed-task comparison against e567033a7, same controlled successful Java-shaped executable and equivalent provider facts; two alternating warm filesystem repeats plus separate first processes: | Task | Elapsed baseline/candidate | CPU baseline/candidate | Sampled peak family RSS | Raw reply tokens | | --- | --- | --- | --- | --- | | One probe | 0.059/0.823s | 0.041/0.693s | 78/235MB | 91/91 | | Three sequential probes in one host | 0.088/1.223s | 0.079/1.026s | 77/235MB | 267/267 | | Public doctor | 0.447/1.223s | 0.624/1.237s | 379/529MB | 3133/3133 | Ownership adds measurable preparation/verification cost; no speed or memory improvement claimed. CPU is wait4 root plus reaped children; RSS is a sampled PPID-family sum and may miss transient peaks. Tokens use cl100k_base on complete raw replies. True cold caches, full agent, MCP/catalog/retry and real-engine costs remain unmeasured. Scope: macOS/Node 24.18.0 controlled process and file fixtures, plus public CLI/MCP lifecycle regressions. These are not real JVM/Ghidra behavior or Windows/Linux lifecycle verification. Separate source finding #4672 tracks the remaining synchronous unbounded installation properties read before Java starts; that file-port migration remains audit work. Full codebase audit remains active. Heavy Qodo/profiler workloads stayed stopped.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#2371
No description provided.