[GH-ISSUE #8390] [Bug] Incomplete selected Ghidra project can produce complete call-path negatives #6581

Open
opened 2026-11-01 21:12:25 +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/8390

Before submitting

  • I searched open and closed issues and pull requests for related reports.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

A selected Ghidra project with function entries but no analyzed instructions or call references can produce trace_call_path results of not_reached with relationship_coverage.status: "complete" in both directions, even for a direct call visible in the original binary. The Evidence limitations correctly say the selected project's original analysis completeness and settings are unknown; that caveat does not affect the structured coverage or goal status.

Reproduction

  1. Compile the fixture below for arm64 Mach-O with clang -arch arm64 -O1 -g -fno-inline -fno-optimize-sibling-calls fixture.c -o fixture. Do not execute it. otool -tvV fixture shows a direct bl _audit_initial in _audit_dispatch.
  2. Import it into a disposable Ghidra 12.1.4 project with analyzeHeadless <project-dir> partial -import fixture -noanalysis -scriptPath <scripts-dir> -postScript CreateUnanalyzedFunctions.java. The post-script below creates only three named function bodies from this build's independently disassembled addresses; it intentionally does not disassemble instructions or analyze the program. It logged zero instructions and zero callers for each function. Adjust the addresses to match a different build.
  3. Configure REA with REA_GHIDRA_PROJECT_PATH=<project-dir>/partial.gpr and REA_GHIDRA_PROGRAM_NAME=fixture. Open the same target via MCP/Ghidra and call trace_call_path with { "start": "_audit_dispatch", "goal": "_audit_initial", "direction": "forward" }, then with { "start": "_audit_initial", "goal": "_audit_dispatch", "direction": "backward" }. Both return not_reached, complete coverage and no residual unknowns. procedure_assembly for dispatch is empty; procedure_callees and procedure_callers return empty arrays. Both selectors resolve to their authored entries.
  4. Control: open the same target in a fresh REA/Ghidra import without project selection. The assembly includes the direct call; both paths return reached. Export/import into fresh target-free servers retains both modes' path results and limitations exactly.
volatile int audit_global = 7;
__attribute__((noinline)) int audit_initial(int x) { return x + audit_global; }
__attribute__((noinline)) int audit_alternate(int x) { return x - audit_global; }
__attribute__((noinline)) int audit_dispatch(int x) { return audit_initial(x); }
__attribute__((noinline)) int audit_sequence(int x) { return audit_dispatch(x); }
int main(void) { return audit_sequence(audit_global); }

The headless script is below. Creating the functions is an intentional test-project edit, performed before REA opened its read-only private copy.

import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.listing.Function;
import ghidra.program.model.symbol.SourceType;

public class CreateUnanalyzedFunctions extends GhidraScript {
    @Override public void run() throws Exception {
        String[][] functions = {
            {"_audit_initial", "1000003c0", "1000003cf"},
            {"_audit_alternate", "1000003d0", "1000003df"},
            {"_audit_dispatch", "1000003e0", "1000003f3"}
        };
        for (String[] spec : functions) {
            Address start = toAddr(spec[1]);
            Address end = toAddr(spec[2]);
            Function f = getFunctionAt(start);
            if (f == null) {
                f = currentProgram.getFunctionManager().createFunction(spec[0], start,
                    new AddressSet(start, end), SourceType.USER_DEFINED);
            }
            println("AUDIT function=" + f.getName() + " start=" + start +
                " instructions=" + currentProgram.getListing().getInstructions(f.getBody(), true).hasNext() +
                " callers=" + f.getCallingFunctions(monitor).size());
        }
    }
}

Expected behavior

Distinguish a complete traversal of observed project references from completeness of caller/callee discovery when the selected project's analysis is not established. Preserve the observed empty relationships and the selected project; do not silently reimport, auto-analyze, or modify it. The exact coverage contract and conditions that can establish completeness need design work.

Impact and scope

An agent following structured goal status and relationship coverage can interpret an undiscovered direct call as absent. One deliberately incomplete selected project and a fresh-import control were exercised; this does not claim every selected project is incomplete, nor does it establish runtime behavior. The target and all original selected-project regular-file digests were unchanged during REA sessions. The imported binary hash identifies the admitted target and does not independently establish arbitrary edited project memory.

REA version

Main 6e9ee97a69cb16c7150d8b550e705cba055ee4f0 and separately PR #8340 head b7e0b28083d3885ee80d3e23fee66c6ba03ef2dd (source builds); both reproduced the selected-project results.

Runtime and platform

macOS arm64; Node 24.22.1; Ghidra 12.1.4; JDK 21; TypeScript MCP SDK 2.3.1 using advertised compact output schemas.

Target and artifact context

Source-owned arm64 Mach-O; SHA-256 4eed434fcc763b9b5eb17820922d11bdba5229e94cb20210ef8cac84f4a407c8. Neither target nor caller-selected project was modified by REA.

Interface

MCP (stdio).

Earliest failure stage

Native analysis or reconstruction: provider relationship coverage, then composed path interpretation.

Relevant output or evidence

Selected forward and backward: goal_status: "not_reached", per-node status: "complete", residual_unknowns: []. Fresh forward and backward: goal_status: "reached"; fresh _audit_dispatch assembly shows bl 0x1000003c0. Selected Evidence includes: “Original analysis completeness and settings are unknown.” Exported path records and limitations were identical after offline import. Successful calls passed the SDK's advertised output schemas.

Typed outcome or recovery expected

Expose the limitation in the direction-specific coverage / path result where it matters to a negative, while retaining the separately useful observed-graph facts. Exact shape is open.

Analysis or suspected cause

Source-derived reasoning: Ghidra bridge callRelationshipCoverage starts from complete and checks unrepresented references/decoded call instructions already present in the selected program. In this controlled project the relevant instruction listing is empty, so that check does not establish that the original call inventory was examined. This reasoning has not been generalized to other ways a project may be incomplete.

Additional context

Related to #8383 (potential missing computed callers even in a fresh analyzed project), #8277 / #8340 (computed callee targets), #8208 (a selected project may lack a function), and #8287 (inventory scope). This case uses a known direct call and both function entries exist. Maintainers: @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0

Originally created by @morluto on GitHub (Oct 31, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8390 ### Before submitting - [x] I searched open and closed issues and pull requests for related reports. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior A selected Ghidra project with function entries but no analyzed instructions or call references can produce `trace_call_path` results of `not_reached` with `relationship_coverage.status: "complete"` in both directions, even for a direct call visible in the original binary. The Evidence limitations correctly say the selected project's original analysis completeness and settings are unknown; that caveat does not affect the structured coverage or goal status. ### Reproduction 1. Compile the fixture below for arm64 Mach-O with `clang -arch arm64 -O1 -g -fno-inline -fno-optimize-sibling-calls fixture.c -o fixture`. Do not execute it. `otool -tvV fixture` shows a direct `bl _audit_initial` in `_audit_dispatch`. 2. Import it into a disposable Ghidra 12.1.4 project with `analyzeHeadless <project-dir> partial -import fixture -noanalysis -scriptPath <scripts-dir> -postScript CreateUnanalyzedFunctions.java`. The post-script below creates only three named function bodies from this build's independently disassembled addresses; it intentionally does not disassemble instructions or analyze the program. It logged zero instructions and zero callers for each function. Adjust the addresses to match a different build. 3. Configure REA with `REA_GHIDRA_PROJECT_PATH=<project-dir>/partial.gpr` and `REA_GHIDRA_PROGRAM_NAME=fixture`. Open the same target via MCP/Ghidra and call `trace_call_path` with `{ "start": "_audit_dispatch", "goal": "_audit_initial", "direction": "forward" }`, then with `{ "start": "_audit_initial", "goal": "_audit_dispatch", "direction": "backward" }`. Both return `not_reached`, complete coverage and no residual unknowns. `procedure_assembly` for dispatch is empty; `procedure_callees` and `procedure_callers` return empty arrays. Both selectors resolve to their authored entries. 4. Control: open the same target in a fresh REA/Ghidra import without project selection. The assembly includes the direct call; both paths return `reached`. Export/import into fresh target-free servers retains both modes' path results and limitations exactly. ```c volatile int audit_global = 7; __attribute__((noinline)) int audit_initial(int x) { return x + audit_global; } __attribute__((noinline)) int audit_alternate(int x) { return x - audit_global; } __attribute__((noinline)) int audit_dispatch(int x) { return audit_initial(x); } __attribute__((noinline)) int audit_sequence(int x) { return audit_dispatch(x); } int main(void) { return audit_sequence(audit_global); } ``` The headless script is below. Creating the functions is an intentional test-project edit, performed before REA opened its read-only private copy. ```java import ghidra.app.script.GhidraScript; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSet; import ghidra.program.model.listing.Function; import ghidra.program.model.symbol.SourceType; public class CreateUnanalyzedFunctions extends GhidraScript { @Override public void run() throws Exception { String[][] functions = { {"_audit_initial", "1000003c0", "1000003cf"}, {"_audit_alternate", "1000003d0", "1000003df"}, {"_audit_dispatch", "1000003e0", "1000003f3"} }; for (String[] spec : functions) { Address start = toAddr(spec[1]); Address end = toAddr(spec[2]); Function f = getFunctionAt(start); if (f == null) { f = currentProgram.getFunctionManager().createFunction(spec[0], start, new AddressSet(start, end), SourceType.USER_DEFINED); } println("AUDIT function=" + f.getName() + " start=" + start + " instructions=" + currentProgram.getListing().getInstructions(f.getBody(), true).hasNext() + " callers=" + f.getCallingFunctions(monitor).size()); } } } ``` ### Expected behavior Distinguish a complete traversal of observed project references from completeness of caller/callee discovery when the selected project's analysis is not established. Preserve the observed empty relationships and the selected project; do not silently reimport, auto-analyze, or modify it. The exact coverage contract and conditions that can establish completeness need design work. ### Impact and scope An agent following structured goal status and relationship coverage can interpret an undiscovered direct call as absent. One deliberately incomplete selected project and a fresh-import control were exercised; this does not claim every selected project is incomplete, nor does it establish runtime behavior. The target and all original selected-project regular-file digests were unchanged during REA sessions. The imported binary hash identifies the admitted target and does not independently establish arbitrary edited project memory. ### REA version Main `6e9ee97a69cb16c7150d8b550e705cba055ee4f0` and separately PR #8340 head `b7e0b28083d3885ee80d3e23fee66c6ba03ef2dd` (source builds); both reproduced the selected-project results. ### Runtime and platform macOS arm64; Node 24.22.1; Ghidra 12.1.4; JDK 21; TypeScript MCP SDK 2.3.1 using advertised compact output schemas. ### Target and artifact context Source-owned arm64 Mach-O; SHA-256 `4eed434fcc763b9b5eb17820922d11bdba5229e94cb20210ef8cac84f4a407c8`. Neither target nor caller-selected project was modified by REA. ### Interface MCP (stdio). ### Earliest failure stage Native analysis or reconstruction: provider relationship coverage, then composed path interpretation. ### Relevant output or evidence Selected forward and backward: `goal_status: "not_reached"`, per-node `status: "complete"`, `residual_unknowns: []`. Fresh forward and backward: `goal_status: "reached"`; fresh `_audit_dispatch` assembly shows `bl 0x1000003c0`. Selected Evidence includes: “Original analysis completeness and settings are unknown.” Exported path records and limitations were identical after offline import. Successful calls passed the SDK's advertised output schemas. ### Typed outcome or recovery expected Expose the limitation in the direction-specific coverage / path result where it matters to a negative, while retaining the separately useful observed-graph facts. Exact shape is open. ### Analysis or suspected cause Source-derived reasoning: Ghidra bridge `callRelationshipCoverage` starts from complete and checks unrepresented references/decoded call instructions already present in the selected program. In this controlled project the relevant instruction listing is empty, so that check does not establish that the original call inventory was examined. This reasoning has not been generalized to other ways a project may be incomplete. ### Additional context Related to #8383 (potential missing computed callers even in a fresh analyzed project), #8277 / #8340 (computed callee targets), #8208 (a selected project may lack a function), and #8287 (inventory scope). This case uses a known direct call and both function entries exist. Maintainers: @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0
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#6581
No description provided.