[GH-ISSUE #6788] [Bug] build_call_path silently drops recorded unresolved call sites #4139

Closed
opened 2026-10-31 00:47:06 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

Evidence, artifacts, or reconstruction

Observed behavior

The registered build_call_path MCP tool accepts authenticated synthetic analyze_function Evidence containing an explicit unresolved call at 0x1004, then returns status: "not_found", search_scope.exhaustive: true, and an empty limitations array for a different goal. The recorded unresolved site and reason disappear from the derived result. The production shared call-coverage assessor identifies that same input as a blocker.

An otherwise identical empty-call control returns the same negative result. An explicit callee-read gap correctly returns unknown; caller-only incomplete coverage still loses the unresolved site. A zero-hop terminal-goal control returns found, as expected.

Reproduction

From the checkout below with locked dependencies installed, save this executed probe outside the repository as /tmp/call-path-probe.mts and run:

node --import ./node_modules/tsx/dist/loader.mjs /tmp/call-path-probe.mts "$PWD"
import {join,resolve} from 'node:path';
import {pathToFileURL} from 'node:url';
const repo=resolve(process.argv[2]!);
const load=(p:string)=>import(pathToFileURL(join(repo,p)).href);
const {Client,InMemoryTransport}=await load('node_modules/@modelcontextprotocol/client/dist/index.mjs');
const {BinarySession}=await load('src/application/binary/BinarySession.ts');
const {SessionProviderRouter}=await load('src/application/binary/SessionProviderRouter.ts');
const {AnalysisProviderRegistry}=await load('src/application/binary/AnalysisProviderRegistry.ts');
const {createServer}=await load('src/server/createServer.ts');
const {createEvidence}=await load('src/domain/evidence.ts');
const {FUNCTION_COMPARISON_EXAMPLE}=await load('src/contracts/functionComparisonExample.ts');
const {assessFunctionCallCoverage}=await load('src/domain/functionCallCoverage.ts');
const session=new BinarySession(SessionProviderRouter.selectable(new AnalysisProviderRegistry([]),[]));
const server=createServer({kind:'session',session},{environment:{}});
const client=new Client({name:'call-path-uncertainty-probe',version:'1'});
const [ct,st]=InMemoryTransport.createLinkedPair();
await server.connect(st);await client.connect(ct);
try {
 for (const [label,unresolved,coverage,goal] of [
  ['resolved-empty-control',[],{status:'complete'},'0x4000'],
  ['unresolved-complete',[{address:'0x1004',reason:'The observed call target is unresolved.'}],{status:'complete'},'0x4000'],
  ['unresolved-caller-only',[{address:'0x1004',reason:'The observed call target is unresolved.'}],{status:'partial',limitations:['Caller reads are partial.'],limited_directions:['callers']},'0x4000'],
  ['explicit-callee-gap-control',[],{status:'partial',limitations:['A callee read is partial.'],limited_directions:['callees']},'0x4000'],
  ['terminal-goal-control',[{address:'0x1004',reason:'The observed call target is unresolved.'}],{status:'complete'},'0x1000'],
 ] as const) {
  const template=FUNCTION_COMPARISON_EXAMPLE.left;
  const dossier={...structuredClone(template.normalized_result),callees:[],callers:[],unresolved_calls:unresolved,limitations:[]};
  const evidence=createEvidence({path:'/tmp/call-path-fixture',sha256:'a'.repeat(64),format:'mach-o'},template.provider,{operation:'analyze_function',parameters:{procedure:'0x1000'},result:dossier,confidence:'derived',authority:'shipped-artifact',relationshipCoverage:coverage});
  const recorded=session.recordEvidence(evidence);if(!recorded.ok)throw recorded.error;
  const reply=await client.callTool({name:'build_call_path',arguments:{functions:[evidence],start:{address:'0x1000'},goal:{address:goal}}});
  console.log(JSON.stringify({label,input_blockers:assessFunctionCallCoverage(evidence.relationship_coverage,dossier).blockers,isError:reply.isError??false,result:reply.structuredContent?.normalized_result??reply}));
 }
}finally{await client.close();await server.close();await session.close();}

All five cases ran once in one process, exited 0, and produced empty stderr. The script creates and records synthetic fixture Evidence through production constructors, then calls the real registered MCP tool over an in-memory transport. It does not launch a native provider or execute a target. The synthetic call site is an input observation for this boundary test, not a claim about an analyzed binary.

Expected behavior

Retain the explicitly recorded unresolved call site and reason in the derived limitations. Account for unresolved outgoing targets when deciding whether the expanded call closure establishes absence; otherwise make the result's restriction to resolved edges explicit. Do not silently project an unresolved outgoing observation into an unqualified exhaustive negative answer.

The tool-design contract distinguishes complete provider-observed sets from all possible runtime edges and requires provider caveats to remain in limitations. The shared assessor separately treats retained unresolved call sites as blockers even when relationship coverage is complete. This report does not claim that complete provider coverage proves every runtime call edge.

Impact and scope

Observed once for each of two unresolved-input variants at the public MCP boundary. An analyst consuming the derived result loses an explicit source uncertainty and receives the same absence answer as the empty-call control. Real-provider prevalence, CLI behavior, and any actual missed path were not measured. No security impact is claimed.

The demonstrated manual workaround is to use the source Evidence's retained unresolved_calls or the shared assessor's blocker output to preserve this uncertainty independently of the derived result. No missing call target is recovered by that workaround.

REA version

rea-agents 6.7.0; source main at a7180370332350bc0f998fbd643888221759570e. Source runtime with locked dependencies; the published compiled package was not replayed separately.

Runtime and platform

Node.js v24.22.1; Linux x64; repository-locked tsx and MCP client. No Hopper, Ghidra or IDA process ran.

Target and artifact context

Synthetic function dossiers based on the repository's public FUNCTION_COMPARISON_EXAMPLE, bound to one inert fixture subject. Their resolved callee set is empty; the failing cases additionally record one unresolved call site. The explicit subject digest is fixture scaffolding, not an observed file identity.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Reduced fields from the executed output:

case                         shared assessor unresolved blocker   tool status   exhaustive   unresolved caveat in tool result
resolved-empty-control       absent                               not_found     true         absent
unresolved-complete          present                              not_found     true         absent
unresolved-caller-only       present                              not_found     true         absent
explicit-callee-gap-control  absent                               unknown       false        n/a; callee-read caveat retained
terminal-goal-control        present                              found         true         absent

For unresolved-complete, the shared assessor returned Unresolved call at 0x1004: The observed call target is unresolved.; the successful MCP result returned limitations: [], paths: [], and shortest_hops: null. For unresolved-caller-only, the result retained only Caller reads are partial.

Evidence provenance: executed production MCP/coverage-assessor boundary with synthetic observations. Local verification: reproduced. Reproduction completeness: complete for those inputs; NOT_ENOUGH_INFO for a real-provider end-to-end example.

Typed outcome or recovery expected

Preserve the source caveat in derived Evidence. If an unresolved target prevents the requested absence conclusion, return unknown and non-exhaustive search. Preserve the explicit callee-gap and zero-hop controls. These are proposed restoration checks; no repair was attempted.

Analysis or suspected cause

Source-derived: parseFunctionEvidence computes and retains callCoverageBlockers. The forward search consumes only generic limitations, relationship coverage and facet availability, without consuming the unresolved-call blockers or raw unresolved sites. This explains the observed omission.

Additional context

Searches covered open and closed reports for build_call_path with unresolved and call-path with unresolved. Related #3988 concerns a missing IDA producer pagination marker; this fixture explicitly declares its relationship coverage. Related #2326 concerns provider-body omissions before Evidence is created; this report supplies the unresolved site in Evidence and loses it only during derived reconstruction. NOT_ENOUGH_INFO: first affected version and real-provider frequency. No reproduction was rerun after this draft was written. Prepared with AI assistance.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The production MCP reconstruction silently discards an explicitly retained unresolved outgoing call and emits the same exhaustive absence result as the empty-call control; an actual missed native path was not demonstrated.
  • Workaround status: available
  • Workaround basis: The executed shared assessor retained the exact unresolved-site blocker from the same source Evidence, allowing manual uncertainty preservation without modifying those observations.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6788 ## 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 Evidence, artifacts, or reconstruction ## Observed behavior The registered `build_call_path` MCP tool accepts authenticated synthetic `analyze_function` Evidence containing an explicit unresolved call at `0x1004`, then returns `status: "not_found"`, `search_scope.exhaustive: true`, and an empty limitations array for a different goal. The recorded unresolved site and reason disappear from the derived result. The production shared call-coverage assessor identifies that same input as a blocker. An otherwise identical empty-call control returns the same negative result. An explicit callee-read gap correctly returns `unknown`; caller-only incomplete coverage still loses the unresolved site. A zero-hop terminal-goal control returns `found`, as expected. ## Reproduction From the checkout below with locked dependencies installed, save this executed probe outside the repository as `/tmp/call-path-probe.mts` and run: ```sh node --import ./node_modules/tsx/dist/loader.mjs /tmp/call-path-probe.mts "$PWD" ``` ```ts import {join,resolve} from 'node:path'; import {pathToFileURL} from 'node:url'; const repo=resolve(process.argv[2]!); const load=(p:string)=>import(pathToFileURL(join(repo,p)).href); const {Client,InMemoryTransport}=await load('node_modules/@modelcontextprotocol/client/dist/index.mjs'); const {BinarySession}=await load('src/application/binary/BinarySession.ts'); const {SessionProviderRouter}=await load('src/application/binary/SessionProviderRouter.ts'); const {AnalysisProviderRegistry}=await load('src/application/binary/AnalysisProviderRegistry.ts'); const {createServer}=await load('src/server/createServer.ts'); const {createEvidence}=await load('src/domain/evidence.ts'); const {FUNCTION_COMPARISON_EXAMPLE}=await load('src/contracts/functionComparisonExample.ts'); const {assessFunctionCallCoverage}=await load('src/domain/functionCallCoverage.ts'); const session=new BinarySession(SessionProviderRouter.selectable(new AnalysisProviderRegistry([]),[])); const server=createServer({kind:'session',session},{environment:{}}); const client=new Client({name:'call-path-uncertainty-probe',version:'1'}); const [ct,st]=InMemoryTransport.createLinkedPair(); await server.connect(st);await client.connect(ct); try { for (const [label,unresolved,coverage,goal] of [ ['resolved-empty-control',[],{status:'complete'},'0x4000'], ['unresolved-complete',[{address:'0x1004',reason:'The observed call target is unresolved.'}],{status:'complete'},'0x4000'], ['unresolved-caller-only',[{address:'0x1004',reason:'The observed call target is unresolved.'}],{status:'partial',limitations:['Caller reads are partial.'],limited_directions:['callers']},'0x4000'], ['explicit-callee-gap-control',[],{status:'partial',limitations:['A callee read is partial.'],limited_directions:['callees']},'0x4000'], ['terminal-goal-control',[{address:'0x1004',reason:'The observed call target is unresolved.'}],{status:'complete'},'0x1000'], ] as const) { const template=FUNCTION_COMPARISON_EXAMPLE.left; const dossier={...structuredClone(template.normalized_result),callees:[],callers:[],unresolved_calls:unresolved,limitations:[]}; const evidence=createEvidence({path:'/tmp/call-path-fixture',sha256:'a'.repeat(64),format:'mach-o'},template.provider,{operation:'analyze_function',parameters:{procedure:'0x1000'},result:dossier,confidence:'derived',authority:'shipped-artifact',relationshipCoverage:coverage}); const recorded=session.recordEvidence(evidence);if(!recorded.ok)throw recorded.error; const reply=await client.callTool({name:'build_call_path',arguments:{functions:[evidence],start:{address:'0x1000'},goal:{address:goal}}}); console.log(JSON.stringify({label,input_blockers:assessFunctionCallCoverage(evidence.relationship_coverage,dossier).blockers,isError:reply.isError??false,result:reply.structuredContent?.normalized_result??reply})); } }finally{await client.close();await server.close();await session.close();} ``` All five cases ran once in one process, exited 0, and produced empty stderr. The script creates and records synthetic fixture Evidence through production constructors, then calls the real registered MCP tool over an in-memory transport. It does not launch a native provider or execute a target. The synthetic call site is an input observation for this boundary test, not a claim about an analyzed binary. ## Expected behavior Retain the explicitly recorded unresolved call site and reason in the derived limitations. Account for unresolved outgoing targets when deciding whether the expanded call closure establishes absence; otherwise make the result's restriction to resolved edges explicit. Do not silently project an unresolved outgoing observation into an unqualified exhaustive negative answer. The [tool-design contract](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/docs/tool-design.md#L60-L72) distinguishes complete provider-observed sets from all possible runtime edges and requires provider caveats to remain in limitations. The [shared assessor](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/src/domain/functionCallCoverage.ts#L88-L99) separately treats retained unresolved call sites as blockers even when relationship coverage is complete. This report does not claim that complete provider coverage proves every runtime call edge. ## Impact and scope Observed once for each of two unresolved-input variants at the public MCP boundary. An analyst consuming the derived result loses an explicit source uncertainty and receives the same absence answer as the empty-call control. Real-provider prevalence, CLI behavior, and any actual missed path were not measured. No security impact is claimed. The demonstrated manual workaround is to use the source Evidence's retained `unresolved_calls` or the shared assessor's blocker output to preserve this uncertainty independently of the derived result. No missing call target is recovered by that workaround. ## REA version `rea-agents` 6.7.0; source `main` at `a7180370332350bc0f998fbd643888221759570e`. Source runtime with locked dependencies; the published compiled package was not replayed separately. ## Runtime and platform Node.js v24.22.1; Linux x64; repository-locked tsx and MCP client. No Hopper, Ghidra or IDA process ran. ## Target and artifact context Synthetic function dossiers based on the repository's public `FUNCTION_COMPARISON_EXAMPLE`, bound to one inert fixture subject. Their resolved callee set is empty; the failing cases additionally record one unresolved call site. The explicit subject digest is fixture scaffolding, not an observed file identity. ## Interface MCP ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Reduced fields from the executed output: ```text case shared assessor unresolved blocker tool status exhaustive unresolved caveat in tool result resolved-empty-control absent not_found true absent unresolved-complete present not_found true absent unresolved-caller-only present not_found true absent explicit-callee-gap-control absent unknown false n/a; callee-read caveat retained terminal-goal-control present found true absent ``` For `unresolved-complete`, the shared assessor returned `Unresolved call at 0x1004: The observed call target is unresolved.`; the successful MCP result returned `limitations: []`, `paths: []`, and `shortest_hops: null`. For `unresolved-caller-only`, the result retained only `Caller reads are partial.` Evidence provenance: executed production MCP/coverage-assessor boundary with synthetic observations. Local verification: reproduced. Reproduction completeness: complete for those inputs; NOT_ENOUGH_INFO for a real-provider end-to-end example. ## Typed outcome or recovery expected Preserve the source caveat in derived Evidence. If an unresolved target prevents the requested absence conclusion, return `unknown` and non-exhaustive search. Preserve the explicit callee-gap and zero-hop controls. These are proposed restoration checks; no repair was attempted. ## Analysis or suspected cause Source-derived: [parseFunctionEvidence](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/src/domain/functionDossierEvidence.ts#L53-L65) computes and retains `callCoverageBlockers`. The [forward search](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/src/domain/callPath.ts#L257-L289) consumes only generic limitations, relationship coverage and facet availability, without consuming the unresolved-call blockers or raw unresolved sites. This explains the observed omission. ## Additional context Searches covered open and closed reports for `build_call_path` with `unresolved` and `call-path` with `unresolved`. Related #3988 concerns a missing IDA producer pagination marker; this fixture explicitly declares its relationship coverage. Related #2326 concerns provider-body omissions before Evidence is created; this report supplies the unresolved site in Evidence and loses it only during derived reconstruction. NOT_ENOUGH_INFO: first affected version and real-provider frequency. No reproduction was rerun after this draft was written. Prepared with AI assistance. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The production MCP reconstruction silently discards an explicitly retained unresolved outgoing call and emits the same exhaustive absence result as the empty-call control; an actual missed native path was not demonstrated. - Workaround status: available - Workaround basis: The executed shared assessor retained the exact unresolved-site blocker from the same source Evidence, allowing manual uncertainty preservation without modifying those observations.
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#4139
No description provided.