[GH-ISSUE #8383] [Bug] Ghidra backward call paths can overstate caller completeness #6577

Open
opened 2026-11-01 21:12:23 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

  • I searched open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

A backward trace_call_path can report not_reached, complete caller coverage, and no residual unknown even when a computed call elsewhere could reach the queried function but has a different static Ghidra reference. This remains on the exact head of #8340, which fixes the forward callee-coverage case and explicitly excludes reverse coverage. The original broader report is #8277; this isolates the remaining reverse scope so it is not lost when that PR closes the original issue.

Reproduction

On macOS arm64 with REA main 6e9ee97a and, separately, PR #8340 head b7e0b280, use Ghidra 12.1.4 and a source-built, unexecuted arm64 Mach-O containing:

int initial(int x) { return x + 1; }
int alternate(int x) { return x - 1; }
int (*volatile slot)(int) = initial;
void select_alternate(void) { slot = alternate; }
int dispatch(int x) { return slot(x); }
int sequence(int x) { select_alternate(); return dispatch(x); }

Through real stdio MCP, resolve_native_call_targets at dispatch's blr reports the initial function as a computed, static provider-reference candidate. trace_call_path({start: "_audit_alternate", goal: "_audit_dispatch", direction: "backward"}) returns not_reached, complete coverage for alternate, and no residual unknown on both revisions. The fixture uses audit_ names and noinline attributes; its SHA-256 was f771508a5d2a8afee3419ad1a075c6f4ad499fbb91b2584f49c9c006fb908e05 and remained unchanged.

A source/disassembly reading indicates the stated straight-line sequence can write alternate into the slot before dispatch calls through it. This is a static inference, not a runtime observation or a claim that the entry point executes. The current Ghidra reference graph itself contains no dispatch-to-alternate edge.

Expected behavior

Make the scope of the negative and caller completeness honest when other computed sites may have unobserved targets, while keeping observed incoming edges and distinguishing a completed traversal of the recorded graph from a proven exhaustive caller set. The right production approach is open: a program-wide scan or address-taken analysis has real coverage and cost tradeoffs, and the observation does not mandate either.

Impact and scope

An agent can read a definite negative as evidence of absence even though the recorded graph does not settle all computed-call targets. Confirmed only on the fixture and configurations above. No runtime reachability, performance, IDA, or broader architecture claim is made.

REA version

Main 6e9ee97a69cb16c7150d8b550e705cba055ee4f0; PR #8340 b7e0b28083d3885ee80d3e23fee66c6ba03ef2dd.

Runtime and platform

macOS arm64, Node 24.22.1, JDK 21, Ghidra 12.1.4, TypeScript MCP SDK 2.3.1.

Target and artifact context

Source-owned executable; analyzed statically, not executed or modified. Independent disassembly and fixture details are in #8277 and its reverse controls.

Interface

MCP (stdio; advertised compact schemas fetched and used by the SDK).

Earliest failure stage

Provider relationship coverage, then composed path interpretation.

Relevant output or evidence

On the same candidate, #8340 changes the forward alternate path from not_reached/complete to unknown/partial; the known initial candidate and direct-call control remain reached, and a return-only leaf negative stays not_reached. The backward alternate path stays not_reached/complete with an empty residual list. Exporting each path Evidence bundle and importing it in a fresh target-free server preserved the respective result exactly. Baseline and PR snapshots are distinct runs; these checks do not establish runtime behavior.

Typed outcome or recovery expected

Preserve observed caller records while communicating uncertainty about potential missing computed callers; the exact contract and breadth are open.

Analysis or suspected cause

An outgoing inspection of the visited function cannot establish whether other functions have computed calls that may target it. A complete incoming-reference list is therefore not necessarily an exhaustive set of possible callers. This is source-derived reasoning, not a prescribed implementation.

Additional context

Related: #8277, #8340, #8200. Maintainers: @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0

Follow-up: direct-only control and a conservative prototype

On main 6e9ee97a, I exercised both real Ghidra 12.1.4 and Kuna 1.986 on the original computed-call fixture and a separate source-owned arm64 Mach-O containing only direct bl calls in its disassembled __text. The target binaries were not executed and their hashes remained unchanged; SDK 2.3.1 validated successful compact-schema MCP responses. Both providers reported the direct-only reverse negative as not_reached and found the observed direct reverse caller. On the computed fixture, Kuna reported the alternate reverse path unknown with its image-wide indirect-call caveat; Ghidra reported not_reached with complete caller coverage. Both providers' path records survived export/import into fresh target-free servers unchanged. Kuna's computed-fixture initial-target reverse query was also unknown; its recorded graph did not provide that particular positive, so this is not a claim that the two providers recover the same edges.

I then changed only an isolated experimental Ghidra bridge to mark every examined caller set partial with the limitation that incoming references do not prove all possible computed callers. On both fixtures the observed direct reverse caller stayed reached; both negative paths became unknown. This avoids the reproduced overstatement without a program-wide scan, but also loses the direct-only negative. Comparing the same direct-only function dossier with itself through compare_functions changed the calls dimension and overall result from unchanged on main to unknown with the prototype, because the dossier's combined call coverage now includes partial callers. This is an experimental tradeoff, not a proposed production patch, and does not establish a general rate of precision loss.

A design may need to distinguish exhaustion of the recorded provider graph from completeness over possible callers, and preserve direction-specific coverage for composed consumers. Any graph-scope result must still qualify provider discovery and analysis coverage; a completed traversal alone does not prove a whole-program negative. A more selective scan or address-taken rule remains untested and could have meaningful cost and coverage limits.

Originally created by @morluto on GitHub (Oct 31, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8383 ### Before submitting - [x] I searched open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior A backward `trace_call_path` can report `not_reached`, complete caller coverage, and no residual unknown even when a computed call elsewhere could reach the queried function but has a different static Ghidra reference. This remains on the exact head of #8340, which fixes the forward callee-coverage case and explicitly excludes reverse coverage. The original broader report is #8277; this isolates the remaining reverse scope so it is not lost when that PR closes the original issue. ### Reproduction On macOS arm64 with REA main `6e9ee97a` and, separately, PR #8340 head `b7e0b280`, use Ghidra 12.1.4 and a source-built, unexecuted arm64 Mach-O containing: ```c int initial(int x) { return x + 1; } int alternate(int x) { return x - 1; } int (*volatile slot)(int) = initial; void select_alternate(void) { slot = alternate; } int dispatch(int x) { return slot(x); } int sequence(int x) { select_alternate(); return dispatch(x); } ``` Through real stdio MCP, `resolve_native_call_targets` at dispatch's `blr` reports the initial function as a computed, static provider-reference candidate. `trace_call_path({start: "_audit_alternate", goal: "_audit_dispatch", direction: "backward"})` returns `not_reached`, complete coverage for alternate, and no residual unknown on both revisions. The fixture uses `audit_` names and noinline attributes; its SHA-256 was `f771508a5d2a8afee3419ad1a075c6f4ad499fbb91b2584f49c9c006fb908e05` and remained unchanged. A source/disassembly reading indicates the stated straight-line sequence can write alternate into the slot before dispatch calls through it. This is a static inference, not a runtime observation or a claim that the entry point executes. The current Ghidra reference graph itself contains no dispatch-to-alternate edge. ### Expected behavior Make the scope of the negative and caller completeness honest when other computed sites may have unobserved targets, while keeping observed incoming edges and distinguishing a completed traversal of the recorded graph from a proven exhaustive caller set. The right production approach is open: a program-wide scan or address-taken analysis has real coverage and cost tradeoffs, and the observation does not mandate either. ### Impact and scope An agent can read a definite negative as evidence of absence even though the recorded graph does not settle all computed-call targets. Confirmed only on the fixture and configurations above. No runtime reachability, performance, IDA, or broader architecture claim is made. ### REA version Main `6e9ee97a69cb16c7150d8b550e705cba055ee4f0`; PR #8340 `b7e0b28083d3885ee80d3e23fee66c6ba03ef2dd`. ### Runtime and platform macOS arm64, Node 24.22.1, JDK 21, Ghidra 12.1.4, TypeScript MCP SDK 2.3.1. ### Target and artifact context Source-owned executable; analyzed statically, not executed or modified. Independent disassembly and fixture details are in #8277 and its reverse controls. ### Interface MCP (stdio; advertised compact schemas fetched and used by the SDK). ### Earliest failure stage Provider relationship coverage, then composed path interpretation. ### Relevant output or evidence On the same candidate, #8340 changes the **forward** alternate path from `not_reached`/complete to `unknown`/partial; the known initial candidate and direct-call control remain `reached`, and a return-only leaf negative stays `not_reached`. The backward alternate path stays `not_reached`/complete with an empty residual list. Exporting each path Evidence bundle and importing it in a fresh target-free server preserved the respective result exactly. Baseline and PR snapshots are distinct runs; these checks do not establish runtime behavior. ### Typed outcome or recovery expected Preserve observed caller records while communicating uncertainty about potential missing computed callers; the exact contract and breadth are open. ### Analysis or suspected cause An outgoing inspection of the visited function cannot establish whether other functions have computed calls that may target it. A complete incoming-reference list is therefore not necessarily an exhaustive set of possible callers. This is source-derived reasoning, not a prescribed implementation. ### Additional context Related: #8277, #8340, #8200. Maintainers: @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0 ### Follow-up: direct-only control and a conservative prototype On main `6e9ee97a`, I exercised both real Ghidra 12.1.4 and Kuna 1.986 on the original computed-call fixture and a separate source-owned arm64 Mach-O containing only direct `bl` calls in its disassembled `__text`. The target binaries were not executed and their hashes remained unchanged; SDK 2.3.1 validated successful compact-schema MCP responses. Both providers reported the direct-only reverse negative as `not_reached` and found the observed direct reverse caller. On the computed fixture, Kuna reported the alternate reverse path `unknown` with its image-wide indirect-call caveat; Ghidra reported `not_reached` with complete caller coverage. Both providers' path records survived export/import into fresh target-free servers unchanged. Kuna's computed-fixture initial-target reverse query was also `unknown`; its recorded graph did not provide that particular positive, so this is not a claim that the two providers recover the same edges. I then changed only an isolated experimental Ghidra bridge to mark every examined caller set partial with the limitation that incoming references do not prove all possible computed callers. On both fixtures the observed direct reverse caller stayed `reached`; both negative paths became `unknown`. This avoids the reproduced overstatement without a program-wide scan, but also loses the direct-only negative. Comparing the same direct-only function dossier with itself through `compare_functions` changed the calls dimension and overall result from `unchanged` on main to `unknown` with the prototype, because the dossier's combined call coverage now includes partial callers. This is an experimental tradeoff, not a proposed production patch, and does not establish a general rate of precision loss. A design may need to distinguish exhaustion of the recorded provider graph from completeness over possible callers, and preserve direction-specific coverage for composed consumers. Any graph-scope result must still qualify provider discovery and analysis coverage; a completed traversal alone does not prove a whole-program negative. A more selective scan or address-taken rule remains untested and could have meaningful cost and coverage limits.
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#6577
No description provided.