[GH-ISSUE #6853] [Bug] Function call comparison equates distinct same-named procedures #4194

Closed
opened 2026-10-31 00:47:35 +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/6853

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

Public MCP compare_functions reports calls: unchanged with no call-identity limitation when distinct caller/callee procedures share a name. Public verify_reconstruction then passes a structural calls claim. The same comparison's references dimension correctly distinguishes exact within-artifact coordinates or preserves cross-artifact name ambiguity.

Executed with production source modules and authenticated synthetic analyze_function Evidence; no native provider or target was launched.

Reproduction

With locked dependencies installed, save the following as /tmp/call-identity-probe.mts. The executed source tree was /tmp/agent-work.0TqT4J/rea; change repo and the final evidence-output path for another location. Run with the pinned Node.js: node --import /tmp/agent-work.0TqT4J/rea/node_modules/tsx/dist/loader.mjs /tmp/call-identity-probe.mts.

import { writeFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
const repo='/tmp/agent-work.0TqT4J/rea';
const load=(p:string)=>import(pathToFileURL(`${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:example}=await load('src/contracts/functionComparisonExample.ts');
const base=structuredClone(example.left.normalized_result);
const proc=(address:string,name:string)=>({address,name,classification:null,body:{available:false,reason:'Synthetic observation omits byte ranges.'}});
const reference=(source:any,target:any)=>({source_address:source.address,target_address:target.address,source_procedure:source,target_procedure:target,kind:{available:true,provenance:'ghidra-reference-manager',type:'UNCONDITIONAL_CALL',flow:true,call:true,jump:false,data:false,read:false,write:false,indirect:false,computed:false,conditional:false,terminal:false,primary:true,operand_index:0,external:false}});
function evidence(id:string,digest:string,entry:string,targets:string[]){
 const d=structuredClone(base); d.procedure={...d.procedure,...proc(entry,'worker')};
 d.basic_blocks=[{start:entry,end:`0x${(BigInt(entry)+4n).toString(16)}`,successors:[]}];
 d.callees=targets.map(address=>proc(address,'helper'));
 d.outgoing_references=d.callees.map(target=>reference(proc(entry,'worker'),target));
 return createEvidence({path:'/tmp/inert-comparison-fixture',sha256:digest.repeat(64),format:'elf',architecture:'x86_64'},example.left.provider,{operation:'analyze_function',parameters:{procedure:entry,observation:id},result:d,confidence:'derived',authority:'shipped-artifact',relationshipCoverage:{status:'complete'}});
}
const session=new BinarySession(SessionProviderRouter.selectable(new AnalysisProviderRegistry([]),[]));
const server=createServer({kind:'session',session},{environment:{}});
const client=new Client({name:'call-identity-probe',version:'1'});
const[ct,st]=InMemoryTransport.createLinkedPair();
const results=[];
try{
 await server.connect(st); await client.connect(ct);
 for(const [name,left,right] of [
  ['self-control',evidence('self','1','0x1000',['0x2000']),evidence('self','1','0x1000',['0x2000'])],
  ['same-artifact-distinct-helpers',evidence('left','1','0x1000',['0x2000']),evidence('right','1','0x1100',['0x3000'])],
  ['cross-artifact-ambiguous-helpers',evidence('left','1','0x1000',['0x2000','0x3000']),evidence('right','2','0x1100',['0x4000','0x5000'])]
 ] as const){
  const compared=await client.callTool({name:'compare_functions',arguments:{left,right}});
  const e=compared.structuredContent as any;
  const verified=compared.isError?null:await client.callTool({name:'verify_reconstruction',arguments:{specification:{name:'Observed calls agree',claims:[{kind:'structural-function',claim_id:'calls-agree',title:'Calls agree',comparison_evidence_id:e.evidence_id,dimension:'calls'}]}}});
  const result={name,left,right,compared,verified};results.push(result);
  console.log(JSON.stringify({name,isError:compared.isError??false,status:e?.normalized_result?.status,function_match:e?.normalized_result?.function_match,dimensions:e?.normalized_result?.dimensions?.filter((x:any)=>['calls','references'].includes(x.dimension)),verification:verified?.structuredContent}));
 }
}finally{await client.close();await server.close();await session.close();}
await writeFile('/tmp/agent-work.0TqT4J/comparison/call-identity-results.json',JSON.stringify(results,null,2));

Each of the three admitted cases ran once. An earlier probe revision was rejected because it included dossier-only signature/locals fields in a strict reference procedure; the script above corrects that fixture error. No successful case was replayed after drafting.

Expected behavior

Retain call endpoint identity. Different known addresses within the same artifact must not be equated solely by a shared label. Across artifacts, observed same-named procedures at multiple entries cannot prove equal call targets; report unknown with an ambiguity limitation. A selected structural calls claim must not pass on that unsupported equality.

The public comparison contract compares calls as a distinct dimension. REA's endpoint-identity semantics explain why a repeated name cannot establish equal procedure endpoints; the reference dimension already implements this distinction.

Impact and scope

P2: incorrect equality propagates into a passing, explicitly selected reconstruction obligation. The cross-build fixture has two different helper entries per dossier, complete relationship coverage and all required facets available. The same-artifact fixture compares two distinct worker entries, each calling a different helper entry. Overall comparison statuses remain unknown and changed, respectively; no global behavior-equivalence claim is made.

Workaround observed in the same calls: inspect the retained raw callee addresses and the references dimension, which returns unknown or changed instead of false equality. Real-provider prevalence, public CLI delivery and the published npm archive were not exercised.

REA version

Package 6.7.0; exact source revision 96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9 (main source snapshot). npm latest had already been confirmed as 6.7.0 by the parent investigation.

Runtime and platform

Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64; locked tsx 4.23.15 and MCP client 2.3.1. No Hopper/Ghidra invocation.

Target and artifact context

Inert synthetic x86_64 ELF-subject observations created by production createEvidence; no ELF file is executed or parsed. Calls and reference facts describe the same synthetic relationships. Function-byte ranges, native API facts and value flow are explicitly unavailable in the fixture.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed projection from call-identity-results.json:

self-control: overall=unchanged; calls=unchanged; references=unchanged
  calls-only verification=unknown (same observation on both sides)
same-artifact-distinct-helpers:
  left worker@0x1000 -> helper@0x2000
  right worker@0x1100 -> helper@0x3000
  overall=changed; calls=unchanged; calls.limitations=[]
  references=changed; calls-only verification=pass
cross-artifact-ambiguous-helpers:
  left callees: helper@0x2000, helper@0x3000
  right callees: helper@0x4000, helper@0x5000
  overall=unknown; calls=unchanged; calls.limitations=[]
  references=unknown: A reference name does not uniquely identify an observed
    procedure entry, so matching names do not establish equal endpoints.
  calls-only verification=pass

Evidence provenance: observed public in-memory MCP results; local verification: reproduced once with the complete script; reproduction completeness: complete for these synthetic observations. The raw record and summary retain the source Evidence, complete comparison and verification responses. Provider/runtime fidelity is untested.

Typed outcome or recovery expected

Calls must retain changed for established same-artifact endpoint differences and unknown for cross-artifact repeated-name ambiguity. The structural calls claim should fail or remain unknown accordingly. The positive self-control should retain its existing source-independence limitation.

Analysis or suspected cause

Source-derived: callProjection retains only direction and name, discarding every procedure address. compareCalls checks generated labels and coverage but never checks repeated-name ambiguity before comparing that reduced multiset. By contrast, the reference comparator explicitly preserves this ambiguity. No dependency failure is implicated.

Additional context

Prepared with AI assistance. Duplicate searches were made in both open and closed GitHub issues for "compare_functions" "duplicate", "call" "same-named", and "callee" "address" "comparison"; raw queries/results are retained with the draft. Reviewed related #3506 (proposed callee-backed normalized text views), #5131/#4210 (reference endpoint semantics), #2222 (incomplete call coverage), and #5535 (generated Hopper labels). This fixture uses nonempty ordinary labels, complete call coverage and the existing calls dimension, and does not request text normalization. No matching report was found. Earlier good/bad version: NOT_ENOUGH_INFO; no history bisect or fix was run.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Authenticated public MCP observations with ambiguous or distinct same-named callees yield false call equality and a passing calls-only reconstruction claim; the failure is bounded to this comparison facet and affected inputs.
  • Workaround status: available
  • Workaround basis: The same executed comparisons retain raw endpoint addresses and return changed or unknown reference facts, allowing callers to inspect identity and withhold the false calls-only conclusion without changing source Evidence.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6853 ### 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 Public MCP `compare_functions` reports `calls: unchanged` with no call-identity limitation when distinct caller/callee procedures share a name. Public `verify_reconstruction` then passes a structural `calls` claim. The same comparison's `references` dimension correctly distinguishes exact within-artifact coordinates or preserves cross-artifact name ambiguity. Executed with production source modules and authenticated synthetic `analyze_function` Evidence; no native provider or target was launched. ### Reproduction With locked dependencies installed, save the following as `/tmp/call-identity-probe.mts`. The executed source tree was `/tmp/agent-work.0TqT4J/rea`; change `repo` and the final evidence-output path for another location. Run with the pinned Node.js: `node --import /tmp/agent-work.0TqT4J/rea/node_modules/tsx/dist/loader.mjs /tmp/call-identity-probe.mts`. ```typescript import { writeFile } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; const repo='/tmp/agent-work.0TqT4J/rea'; const load=(p:string)=>import(pathToFileURL(`${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:example}=await load('src/contracts/functionComparisonExample.ts'); const base=structuredClone(example.left.normalized_result); const proc=(address:string,name:string)=>({address,name,classification:null,body:{available:false,reason:'Synthetic observation omits byte ranges.'}}); const reference=(source:any,target:any)=>({source_address:source.address,target_address:target.address,source_procedure:source,target_procedure:target,kind:{available:true,provenance:'ghidra-reference-manager',type:'UNCONDITIONAL_CALL',flow:true,call:true,jump:false,data:false,read:false,write:false,indirect:false,computed:false,conditional:false,terminal:false,primary:true,operand_index:0,external:false}}); function evidence(id:string,digest:string,entry:string,targets:string[]){ const d=structuredClone(base); d.procedure={...d.procedure,...proc(entry,'worker')}; d.basic_blocks=[{start:entry,end:`0x${(BigInt(entry)+4n).toString(16)}`,successors:[]}]; d.callees=targets.map(address=>proc(address,'helper')); d.outgoing_references=d.callees.map(target=>reference(proc(entry,'worker'),target)); return createEvidence({path:'/tmp/inert-comparison-fixture',sha256:digest.repeat(64),format:'elf',architecture:'x86_64'},example.left.provider,{operation:'analyze_function',parameters:{procedure:entry,observation:id},result:d,confidence:'derived',authority:'shipped-artifact',relationshipCoverage:{status:'complete'}}); } const session=new BinarySession(SessionProviderRouter.selectable(new AnalysisProviderRegistry([]),[])); const server=createServer({kind:'session',session},{environment:{}}); const client=new Client({name:'call-identity-probe',version:'1'}); const[ct,st]=InMemoryTransport.createLinkedPair(); const results=[]; try{ await server.connect(st); await client.connect(ct); for(const [name,left,right] of [ ['self-control',evidence('self','1','0x1000',['0x2000']),evidence('self','1','0x1000',['0x2000'])], ['same-artifact-distinct-helpers',evidence('left','1','0x1000',['0x2000']),evidence('right','1','0x1100',['0x3000'])], ['cross-artifact-ambiguous-helpers',evidence('left','1','0x1000',['0x2000','0x3000']),evidence('right','2','0x1100',['0x4000','0x5000'])] ] as const){ const compared=await client.callTool({name:'compare_functions',arguments:{left,right}}); const e=compared.structuredContent as any; const verified=compared.isError?null:await client.callTool({name:'verify_reconstruction',arguments:{specification:{name:'Observed calls agree',claims:[{kind:'structural-function',claim_id:'calls-agree',title:'Calls agree',comparison_evidence_id:e.evidence_id,dimension:'calls'}]}}}); const result={name,left,right,compared,verified};results.push(result); console.log(JSON.stringify({name,isError:compared.isError??false,status:e?.normalized_result?.status,function_match:e?.normalized_result?.function_match,dimensions:e?.normalized_result?.dimensions?.filter((x:any)=>['calls','references'].includes(x.dimension)),verification:verified?.structuredContent})); } }finally{await client.close();await server.close();await session.close();} await writeFile('/tmp/agent-work.0TqT4J/comparison/call-identity-results.json',JSON.stringify(results,null,2)); ``` Each of the three admitted cases ran once. An earlier probe revision was rejected because it included dossier-only `signature`/`locals` fields in a strict reference procedure; the script above corrects that fixture error. No successful case was replayed after drafting. ### Expected behavior Retain call endpoint identity. Different known addresses within the same artifact must not be equated solely by a shared label. Across artifacts, observed same-named procedures at multiple entries cannot prove equal call targets; report `unknown` with an ambiguity limitation. A selected structural calls claim must not pass on that unsupported equality. The [public comparison contract](https://github.com/morluto/rea/blob/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9/src/contracts/sessionToolContracts.ts#L117) compares calls as a distinct dimension. REA's [endpoint-identity semantics](https://github.com/morluto/rea/blob/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9/docs/mcp-contracts.md#L1167) explain why a repeated name cannot establish equal procedure endpoints; the reference dimension already implements this distinction. ### Impact and scope P2: incorrect equality propagates into a passing, explicitly selected reconstruction obligation. The cross-build fixture has two different `helper` entries per dossier, complete relationship coverage and all required facets available. The same-artifact fixture compares two distinct `worker` entries, each calling a different `helper` entry. Overall comparison statuses remain `unknown` and `changed`, respectively; no global behavior-equivalence claim is made. Workaround observed in the same calls: inspect the retained raw callee addresses and the `references` dimension, which returns `unknown` or `changed` instead of false equality. Real-provider prevalence, public CLI delivery and the published npm archive were not exercised. ### REA version Package `6.7.0`; exact source revision `96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9` (main source snapshot). npm latest had already been confirmed as 6.7.0 by the parent investigation. ### Runtime and platform Node.js `v24.22.1`; Linux `7.2.8-9-omarchy` x86_64; locked `tsx` 4.23.15 and MCP client 2.3.1. No Hopper/Ghidra invocation. ### Target and artifact context Inert synthetic x86_64 ELF-subject observations created by production `createEvidence`; no ELF file is executed or parsed. Calls and reference facts describe the same synthetic relationships. Function-byte ranges, native API facts and value flow are explicitly unavailable in the fixture. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed projection from `call-identity-results.json`: ```text self-control: overall=unchanged; calls=unchanged; references=unchanged calls-only verification=unknown (same observation on both sides) same-artifact-distinct-helpers: left worker@0x1000 -> helper@0x2000 right worker@0x1100 -> helper@0x3000 overall=changed; calls=unchanged; calls.limitations=[] references=changed; calls-only verification=pass cross-artifact-ambiguous-helpers: left callees: helper@0x2000, helper@0x3000 right callees: helper@0x4000, helper@0x5000 overall=unknown; calls=unchanged; calls.limitations=[] references=unknown: A reference name does not uniquely identify an observed procedure entry, so matching names do not establish equal endpoints. calls-only verification=pass ``` Evidence provenance: observed public in-memory MCP results; local verification: reproduced once with the complete script; reproduction completeness: complete for these synthetic observations. The raw record and summary retain the source Evidence, complete comparison and verification responses. Provider/runtime fidelity is untested. ### Typed outcome or recovery expected Calls must retain `changed` for established same-artifact endpoint differences and `unknown` for cross-artifact repeated-name ambiguity. The structural calls claim should fail or remain unknown accordingly. The positive self-control should retain its existing source-independence limitation. ### Analysis or suspected cause Source-derived: [`callProjection`](https://github.com/morluto/rea/blob/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9/src/domain/functionComparisonDimensions.ts#L614) retains only direction and name, discarding every procedure address. [`compareCalls`](https://github.com/morluto/rea/blob/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9/src/domain/functionComparisonDimensions.ts#L594) checks generated labels and coverage but never checks repeated-name ambiguity before comparing that reduced multiset. By contrast, the [reference comparator](https://github.com/morluto/rea/blob/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9/src/domain/functionReferenceComparison.ts#L212) explicitly preserves this ambiguity. No dependency failure is implicated. ### Additional context Prepared with AI assistance. Duplicate searches were made in both open and closed GitHub issues for `"compare_functions" "duplicate"`, `"call" "same-named"`, and `"callee" "address" "comparison"`; raw queries/results are retained with the draft. Reviewed related #3506 (proposed callee-backed normalized *text* views), #5131/#4210 (reference endpoint semantics), #2222 (incomplete call coverage), and #5535 (generated Hopper labels). This fixture uses nonempty ordinary labels, complete call coverage and the existing `calls` dimension, and does not request text normalization. No matching report was found. Earlier good/bad version: NOT_ENOUGH_INFO; no history bisect or fix was run. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Authenticated public MCP observations with ambiguous or distinct same-named callees yield false call equality and a passing calls-only reconstruction claim; the failure is bounded to this comparison facet and affected inputs. - Workaround status: available - Workaround basis: The same executed comparisons retain raw endpoint addresses and return changed or unknown reference facts, allowing callers to inspect identity and withhold the false calls-only conclusion without changing source Evidence.
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#4194
No description provided.