[GH-ISSUE #6824] [Bug] Empty function names are treated as stable symbol matches #4168

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

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

compareFunctions reports function_match: {status: "matched", method: "symbol"} when both schema-valid function dossiers have an empty procedure name. With otherwise equivalent observed facets from different fixture artifacts, the overall result is unchanged. Empty strings do not supply a symbol identity. The generated-name control correctly reports an ambiguous match and overall unknown.

This was reproduced through production source functions with generated, authenticated Evidence records, not through a native provider or public MCP session. The records' artifact paths/digests are fixture identities; no physical target binary was inspected.

Reproduction

With locked dependencies installed, save the script as /tmp/empty-names.mjs. From a 6.7.0 source checkout run:

node --import tsx /tmp/empty-names.mjs "$PWD" --raw-evidence

For the main revision below, omit --raw-evidence: its comparison function takes already-parsed snapshots. Both variants were executed and returned the same results.

import assert from 'node:assert/strict';
import {resolve} from 'node:path';
import {pathToFileURL} from 'node:url';
const root=resolve(process.argv[2]??'.');
const load=p=>import(pathToFileURL(resolve(root,p)).href);
const {functionDossierSchema}=await load('src/domain/hopperValues.ts');
const {createEvidence}=await load('src/domain/evidence.ts');
const {parseFunctionEvidence}=await load('src/domain/functionDossierEvidence.ts');
const {compareFunctions}=await load('src/domain/functionComparison.ts');
function snapshot(digit,base,name){
 const dossier=functionDossierSchema.parse({
  procedure:{address:base,name,classification:null,body:{available:false,reason:'fixture body not requested'},signature:'void(void)',locals:[]},
  pseudocode:'return;',assembly:['ret'],comments:[],callers:[],callees:[],incoming_references:[],outgoing_references:[],referenced_strings:[],referenced_names:[],native_api:null,native_value_flow:null,
  basic_blocks:[{start:base,end:'0x'+(BigInt(base)+1n).toString(16),successors:[]}],
  facet_availability:Object.fromEntries(['pseudocode','assembly','comments','callers','callees','references','strings_names','cfg'].map(k=>[k,{available:true}])),limitations:[]
 });
 const e=createEvidence({path:`/fixture/${digit}`,sha256:digit.repeat(64),format:'mach-o'},{id:'fixture',name:'fixture',version:'1'},{operation:'analyze_function',parameters:{procedure:base},result:dossier,confidence:'observed',relationshipCoverage:{status:'complete'}});
 const parsed=parseFunctionEvidence(e,{operation:'compare_functions',path:[]});
 if(process.argv.includes('--raw-evidence')) return e;
 if('ok' in parsed){assert.ok(parsed.ok);return parsed.value;}
 return parsed;
}
for(const [kind,leftName,rightName] of [['empty','',''],['generated','sub_1000','sub_2000'],['named','example','example']]){
 const result=compareFunctions(snapshot('1','0x1000',leftName),snapshot('2','0x2000',rightName));
 console.log(JSON.stringify({case:kind,status:result.status,function_match:result.function_match,summary:result.summary}));
}

Expected behavior

Keep missing symbol identity ambiguous, or reject an empty name at the contract boundary. A blank name must not become a stable symbol match merely because it is not an address-generated label.

This is consistent with the same codebase's function inventory projection, which excludes row.value.length === 0 from exact-symbol matching, and reference endpoint comparison, which treats an empty procedure name as missing identity.

Impact and scope

The executed comparison falsely claims a symbol correspondence and returns an overall unchanged result for valid-shape records lacking symbol identity. The generated-name control demonstrates the existing uncertainty behavior that is bypassed by empty strings.

Reach is limited to source/imported Evidence with empty procedure names. Native provider reach and real-world frequency were not established. The current Hopper bridge falls back to a hexadecimal address when no name exists, so this report does not claim that bridge emits empty names. No native binary, user Evidence store, or running process was modified.

REA version

Latest published rea-agents 6.7.0 at 27dc42d10249f0410904a75c0997842eb03b931c; also reproduced against main source 567b55947486516b9d35d30b7202ac69c6f40f6d.

Runtime and platform

Linux x86_64; Node.js v24.18.0 for the released source and v24.22.1 for main. Each source tree used its locked dependencies and tsx. No Hopper/Ghidra instance ran.

Target and artifact context

Two generated analyze_function Evidence records with distinct fixture artifact SHA-256 identities, equivalent complete facets and relative CFG shape, and empty names. Separate generated-name and stable-name controls use the same fixture construction.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Output from each revision:

{"case":"empty","status":"unchanged","function_match":{"status":"matched","method":"symbol","left_name":"","right_name":""},"summary":{"unchanged":8,"changed":0,"unknown":0}}
{"case":"generated","status":"unknown","function_match":{"status":"ambiguous","method":"explicit","left_name":"sub_1000","right_name":"sub_2000"},"summary":{"unchanged":7,"changed":0,"unknown":1}}
{"case":"named","status":"unchanged","function_match":{"status":"matched","method":"symbol","left_name":"example","right_name":"example"},"summary":{"unchanged":8,"changed":0,"unknown":0}}

The schema validation and Evidence parsing completed successfully before comparison. These observations establish the production domain boundary only; they do not establish native-provider frequency or MCP transport behavior.

Typed outcome or recovery expected

An ambiguous function match and an unknown overall comparison for missing symbol identity, while keeping the named control matched. A typed rejection of the empty name would also avoid the false correspondence.

Analysis or suspected cause

Source-derived: functionMatch defines stable names solely by negating isAutoName. The empty string fails that generated-name regex, so it is classified as stable. Matching empty strings then take the exact-symbol branch. The identity dimension has the same omission, allowing all eight dimensions to report unchanged.

Alternative considered: explicit caller selection can authorize a comparison without asserting symbol correspondence. The result specifically labels its correspondence as method: "symbol", while the generated-name control uses method: "explicit" and remains ambiguous. The issue is the asserted identity, not permission to compare the records.

Additional context

No earlier good revision or native-provider reproduction was established. Existing generated-name comparison tests were inspected before drafting; this case uses empty strings admitted by procedureIdentitySchema instead of an address-generated name.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Production comparison accepts the generated valid-shape Evidence and falsely reports a stable symbol match and unchanged result for two missing names; real-provider reach remains unestablished.
  • Workaround status: unverified
  • Workaround basis: A consumer could reject empty names before using the comparison, but no built-in REA recovery workflow was demonstrated.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6824 ### 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 `compareFunctions` reports `function_match: {status: "matched", method: "symbol"}` when both schema-valid function dossiers have an empty procedure name. With otherwise equivalent observed facets from different fixture artifacts, the overall result is `unchanged`. Empty strings do not supply a symbol identity. The generated-name control correctly reports an ambiguous match and overall `unknown`. This was reproduced through production source functions with generated, authenticated Evidence records, not through a native provider or public MCP session. The records' artifact paths/digests are fixture identities; no physical target binary was inspected. ### Reproduction With locked dependencies installed, save the script as `/tmp/empty-names.mjs`. From a 6.7.0 source checkout run: ```sh node --import tsx /tmp/empty-names.mjs "$PWD" --raw-evidence ``` For the main revision below, omit `--raw-evidence`: its comparison function takes already-parsed snapshots. Both variants were executed and returned the same results. ```js import assert from 'node:assert/strict'; import {resolve} from 'node:path'; import {pathToFileURL} from 'node:url'; const root=resolve(process.argv[2]??'.'); const load=p=>import(pathToFileURL(resolve(root,p)).href); const {functionDossierSchema}=await load('src/domain/hopperValues.ts'); const {createEvidence}=await load('src/domain/evidence.ts'); const {parseFunctionEvidence}=await load('src/domain/functionDossierEvidence.ts'); const {compareFunctions}=await load('src/domain/functionComparison.ts'); function snapshot(digit,base,name){ const dossier=functionDossierSchema.parse({ procedure:{address:base,name,classification:null,body:{available:false,reason:'fixture body not requested'},signature:'void(void)',locals:[]}, pseudocode:'return;',assembly:['ret'],comments:[],callers:[],callees:[],incoming_references:[],outgoing_references:[],referenced_strings:[],referenced_names:[],native_api:null,native_value_flow:null, basic_blocks:[{start:base,end:'0x'+(BigInt(base)+1n).toString(16),successors:[]}], facet_availability:Object.fromEntries(['pseudocode','assembly','comments','callers','callees','references','strings_names','cfg'].map(k=>[k,{available:true}])),limitations:[] }); const e=createEvidence({path:`/fixture/${digit}`,sha256:digit.repeat(64),format:'mach-o'},{id:'fixture',name:'fixture',version:'1'},{operation:'analyze_function',parameters:{procedure:base},result:dossier,confidence:'observed',relationshipCoverage:{status:'complete'}}); const parsed=parseFunctionEvidence(e,{operation:'compare_functions',path:[]}); if(process.argv.includes('--raw-evidence')) return e; if('ok' in parsed){assert.ok(parsed.ok);return parsed.value;} return parsed; } for(const [kind,leftName,rightName] of [['empty','',''],['generated','sub_1000','sub_2000'],['named','example','example']]){ const result=compareFunctions(snapshot('1','0x1000',leftName),snapshot('2','0x2000',rightName)); console.log(JSON.stringify({case:kind,status:result.status,function_match:result.function_match,summary:result.summary})); } ``` ### Expected behavior Keep missing symbol identity ambiguous, or reject an empty name at the contract boundary. A blank name must not become a stable symbol match merely because it is not an address-generated label. This is consistent with the same codebase's [function inventory projection](https://github.com/morluto/rea/blob/567b55947486516b9d35d30b7202ac69c6f40f6d/src/domain/native/functionInventory.ts), which excludes `row.value.length === 0` from exact-symbol matching, and [reference endpoint comparison](https://github.com/morluto/rea/blob/567b55947486516b9d35d30b7202ac69c6f40f6d/src/domain/functionReferenceComparison.ts), which treats an empty procedure name as missing identity. ### Impact and scope The executed comparison falsely claims a symbol correspondence and returns an overall `unchanged` result for valid-shape records lacking symbol identity. The generated-name control demonstrates the existing uncertainty behavior that is bypassed by empty strings. Reach is limited to source/imported Evidence with empty procedure names. Native provider reach and real-world frequency were not established. The current Hopper bridge falls back to a hexadecimal address when no name exists, so this report does not claim that bridge emits empty names. No native binary, user Evidence store, or running process was modified. ### REA version Latest published `rea-agents` 6.7.0 at `27dc42d10249f0410904a75c0997842eb03b931c`; also reproduced against main source `567b55947486516b9d35d30b7202ac69c6f40f6d`. ### Runtime and platform Linux x86_64; Node.js v24.18.0 for the released source and v24.22.1 for main. Each source tree used its locked dependencies and tsx. No Hopper/Ghidra instance ran. ### Target and artifact context Two generated `analyze_function` Evidence records with distinct fixture artifact SHA-256 identities, equivalent complete facets and relative CFG shape, and empty names. Separate generated-name and stable-name controls use the same fixture construction. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Output from each revision: ```json {"case":"empty","status":"unchanged","function_match":{"status":"matched","method":"symbol","left_name":"","right_name":""},"summary":{"unchanged":8,"changed":0,"unknown":0}} {"case":"generated","status":"unknown","function_match":{"status":"ambiguous","method":"explicit","left_name":"sub_1000","right_name":"sub_2000"},"summary":{"unchanged":7,"changed":0,"unknown":1}} {"case":"named","status":"unchanged","function_match":{"status":"matched","method":"symbol","left_name":"example","right_name":"example"},"summary":{"unchanged":8,"changed":0,"unknown":0}} ``` The schema validation and Evidence parsing completed successfully before comparison. These observations establish the production domain boundary only; they do not establish native-provider frequency or MCP transport behavior. ### Typed outcome or recovery expected An ambiguous function match and an unknown overall comparison for missing symbol identity, while keeping the named control matched. A typed rejection of the empty name would also avoid the false correspondence. ### Analysis or suspected cause Source-derived: [`functionMatch`](https://github.com/morluto/rea/blob/567b55947486516b9d35d30b7202ac69c6f40f6d/src/domain/functionComparisonNormalization.ts#L16-L37) defines stable names solely by negating `isAutoName`. The empty string fails that generated-name regex, so it is classified as stable. Matching empty strings then take the exact-symbol branch. The identity dimension has the same omission, allowing all eight dimensions to report unchanged. Alternative considered: explicit caller selection can authorize a comparison without asserting symbol correspondence. The result specifically labels its correspondence as `method: "symbol"`, while the generated-name control uses `method: "explicit"` and remains ambiguous. The issue is the asserted identity, not permission to compare the records. ### Additional context No earlier good revision or native-provider reproduction was established. Existing generated-name comparison tests were inspected before drafting; this case uses empty strings admitted by `procedureIdentitySchema` instead of an address-generated name. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Production comparison accepts the generated valid-shape Evidence and falsely reports a stable symbol match and unchanged result for two missing names; real-provider reach remains unestablished. - Workaround status: unverified - Workaround basis: A consumer could reject empty names before using the comparison, but no built-in REA recovery workflow was demonstrated.
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#4168
No description provided.