[GH-ISSUE #5438] [Bug] Hopper reverse caller queries omit a resolved direct tail call #3296

Closed
opened 2026-10-31 00:38:53 +00:00 by bsakaguchi · 1 comment
Owner

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

Before submitting

  • I searched the 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

With provider_id: "hopper", a resolved direct tail call appears in forward queries but disappears from reverse queries. procedure_callers for _target_fn returns [] with complete relationship coverage, although _call_direct has that function as a resolved direct callee. Backward get_call_graph omits the edge, and trace_call_path reports the known caller as not_reached without residual unknowns.

Reproduction

Independently reproduced in one successful stdio MCP session using the built source checkout and node scripts/rea.mjs mcp.

Compile this fixture with clang -O1 -g -o fixture fixture.c:

#include <stdio.h>
__attribute__((noinline)) int target_fn(int x) { return x * 5; }
__attribute__((noinline)) int call_through(int (*fp)(int), int x) { return fp(x); }
__attribute__((noinline)) int call_direct(int x) { return target_fn(x); }
int main(void) {
  printf("%d %d\n", call_through(target_fn, 3), call_direct(4));
  return 0;
}
  1. Start that MCP server with REA_ANALYSIS_PROVIDER=hopper and explicit isolated HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME, and XDG_STATE_HOME directories.
  2. Call open_binary with provider_id: "hopper" and path set to the fixture's absolute path. Call list_procedures and procedure_address for _target_fn and _call_direct. The tested addresses were 0x100000460 and 0x100000474; substitute the returned addresses if your build shifts them.
  3. Call procedure_assembly with {"procedure":"_call_direct"}, procedure_callees with the same arguments, and procedure_references with {"procedure":"_call_direct","direction":"outgoing"}.
  4. Call procedure_callers with {"procedure":"_target_fn"}, get_call_graph with {"address":"0x100000460","direction":"backward"}, and trace_call_path with {"start":"0x100000460","goal":"0x100000474","direction":"backward"}.
  5. Corroborating queries were xrefs with {"address":"0x100000460"}, procedure_references with {"procedure":"_target_fn","direction":"incoming"}, and analyze_function with {"procedure":"_target_fn"}. Finish with close_binary and close the MCP client.

All calls succeeded, including document closure. No timeout or retry occurred.

Expected behavior

procedure_callers should include the already resolved direct caller, and backward graph/path traversal should follow that edge. src/contracts/officialToolContracts.ts:160-167 promises resolved direct caller/callee results; its unresolved-indirect exception does not cover an edge classified as direct by the same session.

docs/mcp-contracts.md:66-71 describes complete provider-observed relationship sets. Lines 90-100 reserve not_reached for exhausted traversal without unresolved relationship reads. These are source-grounded expectations, not claims about every possible runtime call target.

Impact and scope

This produces a false absence in static caller searches and backward path tracing for the tested direct tail call. Impact is bounded but consequential, assessed as P2. Querying the known candidate's forward callees or outgoing references recovered this exact edge in the same session. Inventory-wide inversion was not tested as a workaround.

Scope is one freshly compiled Mach-O arm64 fixture and one successful session. Other tail-call forms, architectures, and provider versions were not tested.

REA version

rea-agents 6.7.0; source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node v26.12.0; macOS 27.0.1 arm64; Hopper Disassembler 6.1.0-demo, build 42.6295552; Apple clang 21.0.0, clang-2100.3.34.2.

Target and artifact context

Newly compiled thin Mach-O arm64 executable from the fixture above. SHA-256: dcff7b50da4b46a5d5e3028a83be82af56e991148301ae7f9894f45a280fb12f.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

These are selected fields from the independently captured MCP responses, not complete response envelopes:

procedure_assembly(_call_direct)
  "0x100000474: b _target_fn"
procedure_callees(_call_direct)
  ["0x100000460"]
procedure_references(_call_direct, outgoing)
  source_address: "0x100000474", target_address: "0x100000460"
  call: {classification: "direct", provider_type: 2}
procedure_callers(_target_fn)
  [], relationship_coverage: {status: "complete"}
get_call_graph(0x100000460, backward)
  one target node, calls: [], relationship_coverage: {status: "complete"}
trace_call_path(0x100000460, 0x100000474, backward)
  goal_status: "not_reached", edges: [], truncated: false
  residual_unknowns: [], target relationship_coverage: {status: "complete"}

xrefs and incoming references contained only 0x10000048c, the address-taking reference from _main, and omitted the tail-call site. analyze_function(_target_fn) returned callers: [], facet_availability.callers.available: true, and complete relationship coverage. Facet availability alone means the facet was observed; it is not the exhaustiveness claim.

Typed outcome or recovery expected

Return the known resolved direct caller and a backward path that reaches it. If a relationship read is incomplete, preserve that uncertainty so traversal cannot report exhaustive absence. Changing coverage alone would not restore the promised resolved-direct-caller result.

Analysis or suspected cause

Source-derived boundary analysis: bridge/hopper_bridge.py:948-951 reads separate native caller/callee lists; lines 372-386 preserve native call-reference classifications. src/hopper/HopperProvider.ts:280-284 declares complete caller/callee and dossier relationship coverage. src/application/CallRelationshipTraversal.ts:7 selects caller lookup for backward traversal. This does not establish whether the native provider or REA adapter should implement the correction.

Additional context

Prepared with AI assistance and independently reproduced through the supported MCP entry point. The submitter inspected the retained raw responses and verifier notes; no new provider run is claimed here.

Fetched upstream main at 052956af3416fa01d93837e54fdec3ad2739f6d1. The only later change in the cited paths adds an unrelated documentation link, so no plausible upstream fix was found. Searched open and closed issues and PRs for procedure_callers, tail call, hopper_bridge, and Hopper callers. Related Ghidra reports #4891 and #2326 concern different providers and inputs.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5438 ### Before submitting - [x] I searched the 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 With `provider_id: "hopper"`, a resolved direct tail call appears in forward queries but disappears from reverse queries. `procedure_callers` for `_target_fn` returns `[]` with complete relationship coverage, although `_call_direct` has that function as a resolved direct callee. Backward `get_call_graph` omits the edge, and `trace_call_path` reports the known caller as `not_reached` without residual unknowns. ### Reproduction Independently reproduced in one successful stdio MCP session using the built source checkout and `node scripts/rea.mjs mcp`. Compile this fixture with `clang -O1 -g -o fixture fixture.c`: ```c #include <stdio.h> __attribute__((noinline)) int target_fn(int x) { return x * 5; } __attribute__((noinline)) int call_through(int (*fp)(int), int x) { return fp(x); } __attribute__((noinline)) int call_direct(int x) { return target_fn(x); } int main(void) { printf("%d %d\n", call_through(target_fn, 3), call_direct(4)); return 0; } ``` 1. Start that MCP server with `REA_ANALYSIS_PROVIDER=hopper` and explicit isolated `HOME`, `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_CACHE_HOME`, and `XDG_STATE_HOME` directories. 2. Call `open_binary` with `provider_id: "hopper"` and `path` set to the fixture's absolute path. Call `list_procedures` and `procedure_address` for `_target_fn` and `_call_direct`. The tested addresses were `0x100000460` and `0x100000474`; substitute the returned addresses if your build shifts them. 3. Call `procedure_assembly` with `{"procedure":"_call_direct"}`, `procedure_callees` with the same arguments, and `procedure_references` with `{"procedure":"_call_direct","direction":"outgoing"}`. 4. Call `procedure_callers` with `{"procedure":"_target_fn"}`, `get_call_graph` with `{"address":"0x100000460","direction":"backward"}`, and `trace_call_path` with `{"start":"0x100000460","goal":"0x100000474","direction":"backward"}`. 5. Corroborating queries were `xrefs` with `{"address":"0x100000460"}`, `procedure_references` with `{"procedure":"_target_fn","direction":"incoming"}`, and `analyze_function` with `{"procedure":"_target_fn"}`. Finish with `close_binary` and close the MCP client. All calls succeeded, including document closure. No timeout or retry occurred. ### Expected behavior `procedure_callers` should include the already resolved direct caller, and backward graph/path traversal should follow that edge. `src/contracts/officialToolContracts.ts:160-167` promises resolved direct caller/callee results; its unresolved-indirect exception does not cover an edge classified as direct by the same session. `docs/mcp-contracts.md:66-71` describes complete provider-observed relationship sets. Lines 90-100 reserve `not_reached` for exhausted traversal without unresolved relationship reads. These are source-grounded expectations, not claims about every possible runtime call target. ### Impact and scope This produces a false absence in static caller searches and backward path tracing for the tested direct tail call. Impact is bounded but consequential, assessed as P2. Querying the known candidate's forward callees or outgoing references recovered this exact edge in the same session. Inventory-wide inversion was not tested as a workaround. Scope is one freshly compiled Mach-O arm64 fixture and one successful session. Other tail-call forms, architectures, and provider versions were not tested. ### REA version rea-agents 6.7.0; source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node v26.12.0; macOS 27.0.1 arm64; Hopper Disassembler 6.1.0-demo, build 42.6295552; Apple clang 21.0.0, clang-2100.3.34.2. ### Target and artifact context Newly compiled thin Mach-O arm64 executable from the fixture above. SHA-256: `dcff7b50da4b46a5d5e3028a83be82af56e991148301ae7f9894f45a280fb12f`. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence These are selected fields from the independently captured MCP responses, not complete response envelopes: ```text procedure_assembly(_call_direct) "0x100000474: b _target_fn" procedure_callees(_call_direct) ["0x100000460"] procedure_references(_call_direct, outgoing) source_address: "0x100000474", target_address: "0x100000460" call: {classification: "direct", provider_type: 2} procedure_callers(_target_fn) [], relationship_coverage: {status: "complete"} get_call_graph(0x100000460, backward) one target node, calls: [], relationship_coverage: {status: "complete"} trace_call_path(0x100000460, 0x100000474, backward) goal_status: "not_reached", edges: [], truncated: false residual_unknowns: [], target relationship_coverage: {status: "complete"} ``` `xrefs` and incoming references contained only `0x10000048c`, the address-taking reference from `_main`, and omitted the tail-call site. `analyze_function(_target_fn)` returned `callers: []`, `facet_availability.callers.available: true`, and complete relationship coverage. Facet availability alone means the facet was observed; it is not the exhaustiveness claim. ### Typed outcome or recovery expected Return the known resolved direct caller and a backward path that reaches it. If a relationship read is incomplete, preserve that uncertainty so traversal cannot report exhaustive absence. Changing coverage alone would not restore the promised resolved-direct-caller result. ### Analysis or suspected cause Source-derived boundary analysis: `bridge/hopper_bridge.py:948-951` reads separate native caller/callee lists; lines 372-386 preserve native call-reference classifications. `src/hopper/HopperProvider.ts:280-284` declares complete caller/callee and dossier relationship coverage. `src/application/CallRelationshipTraversal.ts:7` selects caller lookup for backward traversal. This does not establish whether the native provider or REA adapter should implement the correction. ### Additional context Prepared with AI assistance and independently reproduced through the supported MCP entry point. The submitter inspected the retained raw responses and verifier notes; no new provider run is claimed here. Fetched upstream `main` at `052956af3416fa01d93837e54fdec3ad2739f6d1`. The only later change in the cited paths adds an unrelated documentation link, so no plausible upstream fix was found. Searched open and closed issues and PRs for `procedure_callers`, `tail call`, `hopper_bridge`, and `Hopper callers`. Related Ghidra reports #4891 and #2326 concern different providers and inputs.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Source review at d87e73f shows useful partial progress: Hopper now declares caller/callee coverage unknown, including the dossier. The traversal consumer records unknown coverage as a residual uncertainty, so the original exhaustive not_reached conclusion should no longer follow from an empty caller list. This is source-derived; I did not rerun Hopper.

The missing relationship remains unaddressed in the inspected producer: the bridge still reads independent native caller and callee lists, without reconciling an already resolved forward tail-call edge into the reverse set. The current regression mocks empty callers and a resolved callee, then asserts that those results remain unchanged with unknown coverage. It verifies honest uncertainty, not recovery of the caller.

Keeping this open is therefore appropriate for the resolved-direct-caller requirement. Backward graph/path queries still select caller lookup; a coverage change alone cannot supply the missing edge. Closure should repeat the reported optimized arm64 fixture through real Hopper/MCP and show _call_direct in _target_fn callers and the backward graph, with the backward path reaching it. Retain uncertainty for genuinely unresolved relationships. If independent, potentially asymmetric inventories are the intended supported contract instead, that limitation needs an explicit decision and consistent tool/documentation wording; it is narrower than restoring the reported direct relationship.

<!-- gh-comment-id:6380873692 --> @coygeek commented on GitHub (Oct 24, 2026): Source review at `d87e73f` shows useful partial progress: [Hopper now declares caller/callee coverage unknown](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/hopper/HopperProvider.ts#L347-L361), including the dossier. The traversal consumer records unknown coverage as a residual uncertainty, so the original exhaustive `not_reached` conclusion should no longer follow from an empty caller list. This is source-derived; I did not rerun Hopper. The missing relationship remains unaddressed in the inspected producer: [the bridge still reads independent native caller and callee lists](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/bridge/hopper_bridge.py#L1057-L1068), without reconciling an already resolved forward tail-call edge into the reverse set. The [current regression](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/hopper/HopperProvider.test.ts#L92-L135) mocks empty callers and a resolved callee, then asserts that those results remain unchanged with unknown coverage. It verifies honest uncertainty, not recovery of the caller. Keeping this open is therefore appropriate for the resolved-direct-caller requirement. Backward graph/path queries still [select caller lookup](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/application/CallRelationshipTraversal.ts#L4-L15); a coverage change alone cannot supply the missing edge. Closure should repeat the reported optimized arm64 fixture through real Hopper/MCP and show `_call_direct` in `_target_fn` callers and the backward graph, with the backward path reaching it. Retain uncertainty for genuinely unresolved relationships. If independent, potentially asymmetric inventories are the intended supported contract instead, that limitation needs an explicit decision and consistent tool/documentation wording; it is narrower than restoring the reported direct relationship.
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#3296
No description provided.