[GH-ISSUE #6502] [Bug] compare_functions equates reference addresses across different architecture slices #3898

Closed
opened 2026-10-31 00:45:03 +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/6502

Before submitting

  • I searched open issues for related reports. Closed issues were not searched.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

Public MCP compare_functions reports references: unchanged with no reference limitations when two supplied observations share a universal-container digest but declare different subject architectures (arm64 and x86_64). Each has one data-reference edge from main at 0x1000 to 0x2000; the observed target procedures have distinct stable names, helper and other. The complete fixture is reported unchanged as well.

The comparison treats the identical virtual addresses as identical endpoints because the outer artifact digests match. Virtual addresses in separate architecture slices do not establish the same endpoint. This was executed with authenticated synthetic provider observations through the production MCP endpoint; no native provider or physical universal binary was analyzed.

Reproduction

From the built checkout with locked dependencies installed, run the following with repository-pinned Node.js. These are the two executed reference cases, excerpted from the original four-case probe; the removed cases concern address-space normalization. No replay after drafting is claimed.

node --input-type=module <<'JS'
const repo=process.cwd();
const load=(path)=>import(`${repo}/dist/${path}.js`);
const {Client,InMemoryTransport}=await import(`${repo}/node_modules/@modelcontextprotocol/client/dist/index.mjs`);
const {BinarySession}=await load('application/binary/BinarySession');
const {SessionProviderRouter}=await load('application/binary/SessionProviderRouter');
const {AnalysisProviderRegistry}=await load('application/binary/AnalysisProviderRegistry');
const {createServer}=await load('server/createServer');
const {createEvidence}=await load('domain/evidence');
const {FUNCTION_COMPARISON_EXAMPLE}=await load('contracts/functionComparisonExample');
const session=new BinarySession(SessionProviderRouter.selectable(new AnalysisProviderRegistry([]),[]));
const server=createServer({kind:'session',session},{environment:{}});
const client=new Client({name:'native-coordinate-probe',version:'1'});
const [ct,st]=InMemoryTransport.createLinkedPair();
await server.connect(st);await client.connect(ct);
const base=structuredClone(FUNCTION_COMPARISON_EXAMPLE.left.normalized_result);
function observe({space='',architecture='arm64',callee='helper',digest='1',withReferences=false}) {
 const d=structuredClone(base);const at=n=>`${space}0x${n.toString(16)}`;
 d.procedure.address=at(0x1000);
 d.comments=[{address:at(0x1000),kind:'comment',text:'entry'}];
 d.referenced_strings=[{address:at(0x2000),source_address:at(0x1000),value:'hello'}];
 d.basic_blocks=[{start:at(0x1000),end:at(0x1004),successors:[]}];
 if(withReferences) {
  const p=(address,name)=>({address,name,classification:null,body:{available:false,reason:'fixture'}});
  d.outgoing_references=[{source_address:at(0x1000),target_address:at(0x2000),source_procedure:p(at(0x1000),'main'),target_procedure:p(at(0x2000),callee),kind:{available:true,provenance:'ghidra-reference-manager',type:'READ',flow:false,call:false,jump:false,data:true,read:true,write:false,indirect:false,computed:false,conditional:false,terminal:false,primary:true,operand_index:0,external:false}}];
 }
 return createEvidence({path:'/tmp/public-fixture',sha256:digest.repeat(64),format:'mach-o-universal',architecture},{id:'fixture',name:'Synthetic provider observations',version:'1'},{operation:'analyze_function',parameters:{procedure:'main'},result:d,confidence:'derived',authority:'shipped-artifact',relationshipCoverage:{status:'complete'}});
}
const cases=[
 ['same-architecture-renamed',observe({withReferences:true}),observe({withReferences:true,callee:'other'})],
 ['different-slices',observe({withReferences:true}),observe({withReferences:true,architecture:'x86_64',callee:'other'})],
];
try {
 for(const [name,left,right]of cases) {
  const result=await client.callTool({name:'compare_functions',arguments:{left,right}});

  const r=result.structuredContent?.normalized_result;
  console.log(JSON.stringify({name,isError:result.isError??false,subject_architectures:[left.subject.architecture,right.subject.architecture],status:r?.status,dimensions:r?.dimensions?.map(({dimension,status,limitations})=>({dimension,status,limitations})),error:r?undefined:result}));
 }
}finally{await client.close();await server.close();await session.close();}
JS

Each case ran once. The same-architecture control changes only the target's display name while keeping its physical coordinate; references: unchanged is appropriate there. The failing case additionally changes the explicit subject architecture to x86_64, but still receives the same unqualified equality.

Expected behavior

Preserve subject architecture when deciding whether raw endpoint addresses belong to one coordinate context. Equal container bytes alone do not prove equality between locations inside different architecture slices. Compare available stable endpoint facts across those contexts or report uncertainty; do not declare the endpoints equal solely because their virtual offsets coincide.

Architecture is already part of semantic subject identity in src/domain/evidence.ts:252. The reference-comparison contract in docs/mcp-contracts.md:1150 preserves endpoint identity and requires unresolved identities to remain unknown. This fixture supplies explicit unequal architectures, so the missing distinction is known rather than an inferred host setting.

Impact and scope

P2: a bounded cross-slice comparison can falsely certify equal native reference endpoints and miss a changed referenced procedure. The observed run used one synthetic arm64/x86_64 pair; real provider acquisition and natural frequency were not tested. Analysts can compare the per-slice retained reference names and coordinates manually. The same-architecture rename control remains valid and must not be broken by a correction.

REA version

Source revision 4c547008c53ee9f0ff2368e7d874a2c3347aa235, package version 6.7.0. This is a built main checkout; the published 6.7.0 release was not separately replayed.

Runtime and platform

Node.js v24.18.0; Linux x86_64; repository-locked MCP client 2.3.1. No real native analysis provider was launched.

Target and artifact context

Authenticated analyze_function Evidence with the same mach-o-universal container digest, equal provider identity, and explicit different subject architectures. One known READ reference per dossier targets helper versus other at numerically equal virtual addresses. A universal binary may map distinct architecture slices at the same virtual address; these synthetic observations isolate the comparison behavior without compiling one.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed result projection:

same-architecture-renamed:
  subject_architectures=[arm64,arm64]
  isError=false; status=unchanged; references=unchanged; reference limitations=[]
different-slices:
  subject_architectures=[arm64,x86_64]
  isError=false; status=unchanged; references=unchanged; reference limitations=[]

Source-derived cause: src/domain/functionReferenceComparison.ts:186 defines sameArtifact using only left.subject.digest.sha256 === right.subject.digest.sha256. That selects src/domain/functionReferenceComparison.ts:87, which reduces endpoints to raw addresses and ignores the supplied procedure identity. Subject architecture and analysis context are not considered before making this choice. Both inline records are authenticated through src/server/registerFunctionComparisonTool.ts:48.

Typed outcome or recovery expected

The cross-slice reference result must not be unqualified unchanged on raw-coordinate equality alone. Known stable target names can support a changed result; unavailable or ambiguous correspondence should remain unknown. A pure display rename at the same coordinate in the same architecture must remain unchanged.

Analysis or suspected cause

Container identity is being used as executable-coordinate identity. The failing branch suppresses name comparison whenever outer digests match, even when the authenticated subjects explicitly identify different architectures. A production universal-binary/provider replay would extend coverage but is not needed to observe this accepted-input comparison result.

Additional context

Open-issue searches: repo:morluto/rea is:issue is:open "compare_functions", "compare_functions" "architecture", "compare_functions" "slice", and "references" "architecture". #5535 concerns false changes from Hopper generated names across different artifacts; #5191 concerns tool vocabulary and collapsed slice names, not reference equivalence between explicit different architectures. No matching open report was found. Closed issues were not searched. No fix or restoration run was performed.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The public comparison accepts explicit different architecture subjects but reports coincident reference addresses as identical without a limitation, losing a known distinction between their coordinate contexts.
  • Workaround status: available
  • Workaround basis: Per-slice source Evidence retains architecture and reference procedure identity, allowing manual review of this bounded comparison without relying on the incorrect equality.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6502 ### Before submitting - [x] I searched open issues for related reports. Closed issues were not searched. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior Public MCP `compare_functions` reports `references: unchanged` with no reference limitations when two supplied observations share a universal-container digest but declare different subject architectures (`arm64` and `x86_64`). Each has one data-reference edge from `main` at `0x1000` to `0x2000`; the observed target procedures have distinct stable names, `helper` and `other`. The complete fixture is reported `unchanged` as well. The comparison treats the identical virtual addresses as identical endpoints because the outer artifact digests match. Virtual addresses in separate architecture slices do not establish the same endpoint. This was executed with authenticated synthetic provider observations through the production MCP endpoint; no native provider or physical universal binary was analyzed. ### Reproduction From the built checkout with locked dependencies installed, run the following with repository-pinned Node.js. These are the two executed reference cases, excerpted from the original four-case probe; the removed cases concern address-space normalization. No replay after drafting is claimed. ```sh node --input-type=module <<'JS' const repo=process.cwd(); const load=(path)=>import(`${repo}/dist/${path}.js`); const {Client,InMemoryTransport}=await import(`${repo}/node_modules/@modelcontextprotocol/client/dist/index.mjs`); const {BinarySession}=await load('application/binary/BinarySession'); const {SessionProviderRouter}=await load('application/binary/SessionProviderRouter'); const {AnalysisProviderRegistry}=await load('application/binary/AnalysisProviderRegistry'); const {createServer}=await load('server/createServer'); const {createEvidence}=await load('domain/evidence'); const {FUNCTION_COMPARISON_EXAMPLE}=await load('contracts/functionComparisonExample'); const session=new BinarySession(SessionProviderRouter.selectable(new AnalysisProviderRegistry([]),[])); const server=createServer({kind:'session',session},{environment:{}}); const client=new Client({name:'native-coordinate-probe',version:'1'}); const [ct,st]=InMemoryTransport.createLinkedPair(); await server.connect(st);await client.connect(ct); const base=structuredClone(FUNCTION_COMPARISON_EXAMPLE.left.normalized_result); function observe({space='',architecture='arm64',callee='helper',digest='1',withReferences=false}) { const d=structuredClone(base);const at=n=>`${space}0x${n.toString(16)}`; d.procedure.address=at(0x1000); d.comments=[{address:at(0x1000),kind:'comment',text:'entry'}]; d.referenced_strings=[{address:at(0x2000),source_address:at(0x1000),value:'hello'}]; d.basic_blocks=[{start:at(0x1000),end:at(0x1004),successors:[]}]; if(withReferences) { const p=(address,name)=>({address,name,classification:null,body:{available:false,reason:'fixture'}}); d.outgoing_references=[{source_address:at(0x1000),target_address:at(0x2000),source_procedure:p(at(0x1000),'main'),target_procedure:p(at(0x2000),callee),kind:{available:true,provenance:'ghidra-reference-manager',type:'READ',flow:false,call:false,jump:false,data:true,read:true,write:false,indirect:false,computed:false,conditional:false,terminal:false,primary:true,operand_index:0,external:false}}]; } return createEvidence({path:'/tmp/public-fixture',sha256:digest.repeat(64),format:'mach-o-universal',architecture},{id:'fixture',name:'Synthetic provider observations',version:'1'},{operation:'analyze_function',parameters:{procedure:'main'},result:d,confidence:'derived',authority:'shipped-artifact',relationshipCoverage:{status:'complete'}}); } const cases=[ ['same-architecture-renamed',observe({withReferences:true}),observe({withReferences:true,callee:'other'})], ['different-slices',observe({withReferences:true}),observe({withReferences:true,architecture:'x86_64',callee:'other'})], ]; try { for(const [name,left,right]of cases) { const result=await client.callTool({name:'compare_functions',arguments:{left,right}}); const r=result.structuredContent?.normalized_result; console.log(JSON.stringify({name,isError:result.isError??false,subject_architectures:[left.subject.architecture,right.subject.architecture],status:r?.status,dimensions:r?.dimensions?.map(({dimension,status,limitations})=>({dimension,status,limitations})),error:r?undefined:result})); } }finally{await client.close();await server.close();await session.close();} JS ``` Each case ran once. The same-architecture control changes only the target's display name while keeping its physical coordinate; `references: unchanged` is appropriate there. The failing case additionally changes the explicit subject architecture to `x86_64`, but still receives the same unqualified equality. ### Expected behavior Preserve subject architecture when deciding whether raw endpoint addresses belong to one coordinate context. Equal container bytes alone do not prove equality between locations inside different architecture slices. Compare available stable endpoint facts across those contexts or report uncertainty; do not declare the endpoints equal solely because their virtual offsets coincide. Architecture is already part of semantic subject identity in [src/domain/evidence.ts:252](https://github.com/morluto/rea/blob/4c547008c53ee9f0ff2368e7d874a2c3347aa235/src/domain/evidence.ts#L252). The reference-comparison contract in [docs/mcp-contracts.md:1150](https://github.com/morluto/rea/blob/4c547008c53ee9f0ff2368e7d874a2c3347aa235/docs/mcp-contracts.md#L1150) preserves endpoint identity and requires unresolved identities to remain unknown. This fixture supplies explicit unequal architectures, so the missing distinction is known rather than an inferred host setting. ### Impact and scope P2: a bounded cross-slice comparison can falsely certify equal native reference endpoints and miss a changed referenced procedure. The observed run used one synthetic arm64/x86_64 pair; real provider acquisition and natural frequency were not tested. Analysts can compare the per-slice retained reference names and coordinates manually. The same-architecture rename control remains valid and must not be broken by a correction. ### REA version Source revision `4c547008c53ee9f0ff2368e7d874a2c3347aa235`, package version `6.7.0`. This is a built main checkout; the published 6.7.0 release was not separately replayed. ### Runtime and platform Node.js `v24.18.0`; Linux x86_64; repository-locked MCP client `2.3.1`. No real native analysis provider was launched. ### Target and artifact context Authenticated `analyze_function` Evidence with the same `mach-o-universal` container digest, equal provider identity, and explicit different subject architectures. One known `READ` reference per dossier targets `helper` versus `other` at numerically equal virtual addresses. A universal binary may map distinct architecture slices at the same virtual address; these synthetic observations isolate the comparison behavior without compiling one. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed result projection: ```text same-architecture-renamed: subject_architectures=[arm64,arm64] isError=false; status=unchanged; references=unchanged; reference limitations=[] different-slices: subject_architectures=[arm64,x86_64] isError=false; status=unchanged; references=unchanged; reference limitations=[] ``` Source-derived cause: [src/domain/functionReferenceComparison.ts:186](https://github.com/morluto/rea/blob/4c547008c53ee9f0ff2368e7d874a2c3347aa235/src/domain/functionReferenceComparison.ts#L186) defines `sameArtifact` using only `left.subject.digest.sha256 === right.subject.digest.sha256`. That selects [src/domain/functionReferenceComparison.ts:87](https://github.com/morluto/rea/blob/4c547008c53ee9f0ff2368e7d874a2c3347aa235/src/domain/functionReferenceComparison.ts#L87), which reduces endpoints to raw addresses and ignores the supplied procedure identity. Subject architecture and analysis context are not considered before making this choice. Both inline records are authenticated through [src/server/registerFunctionComparisonTool.ts:48](https://github.com/morluto/rea/blob/4c547008c53ee9f0ff2368e7d874a2c3347aa235/src/server/registerFunctionComparisonTool.ts#L48). ### Typed outcome or recovery expected The cross-slice reference result must not be unqualified `unchanged` on raw-coordinate equality alone. Known stable target names can support a changed result; unavailable or ambiguous correspondence should remain unknown. A pure display rename at the same coordinate in the same architecture must remain unchanged. ### Analysis or suspected cause Container identity is being used as executable-coordinate identity. The failing branch suppresses name comparison whenever outer digests match, even when the authenticated subjects explicitly identify different architectures. A production universal-binary/provider replay would extend coverage but is not needed to observe this accepted-input comparison result. ### Additional context Open-issue searches: `repo:morluto/rea is:issue is:open "compare_functions"`, `"compare_functions" "architecture"`, `"compare_functions" "slice"`, and `"references" "architecture"`. #5535 concerns false changes from Hopper generated names across different artifacts; #5191 concerns tool vocabulary and collapsed slice names, not reference equivalence between explicit different architectures. No matching open report was found. Closed issues were not searched. No fix or restoration run was performed. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The public comparison accepts explicit different architecture subjects but reports coincident reference addresses as identical without a limitation, losing a known distinction between their coordinate contexts. - Workaround status: available - Workaround basis: Per-slice source Evidence retains architecture and reference procedure identity, allowing manual review of this bounded comparison without relying on the incorrect equality.
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#3898
No description provided.