[GH-ISSUE #5535] [Bug] compare_functions reports false call and reference changes for Hopper's generated EntryPoint label #3370

Closed
opened 2026-10-31 00:39:39 +00:00 by bsakaguchi · 2 comments
Owner

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

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 and references as changed when comparing an unchanged function in a Mach-O executable and its symbol-stripped copy analyzed by Hopper. The caller changes from _main to Hopper's generated EntryPoint label. Procedure bytes, incoming callsite, pseudocode, assembly and CFG match. find_changed_behavior repeats the two dimensions as changed static candidates. The overall comparison and aggregate behavior status remain unknown.

These results were independently reproduced through the built production stdio MCP server. This submission uses that verifier's recorded results.

Reproduction

The independent verifier compiled this fixture and stripped a copy:

work=$(mktemp -d)
cat > "$work/prog.c" <<'C'
#include <stdio.h>
int unchanged(int x) { return x + 1; }
int changed(int x) { return x * 2; }
int removed(int x) { return x - 3; }
int main(void) {
  printf("%d %d %d\n", unchanged(1), changed(2), removed(3));
  return 0;
}
C
clang -O0 -g -o "$work/prog-v1" "$work/prog.c"
strip -o "$work/prog-v1-stripped" "$work/prog-v1"
nm -n "$work/prog-v1"
nm -n "$work/prog-v1-stripped"
  1. Start node scripts/rea.mjs mcp from the built source checkout in a stdio MCP client. The verifier used isolated home/config/data/cache/state/temp directories and sequential Hopper sessions.
  2. Call open_binary with {"path":"<absolute work directory>/prog-v1","provider_id":"hopper"}, then analyze_function with {"procedure":"_unchanged"}. Retain the complete returned structuredContent Evidence as left. Call close_binary with {}.
  3. Open prog-v1-stripped with the same provider, then call analyze_function with {"procedure":"sub_100000460"}. Retain its complete Evidence as right, then call close_binary. This address is the observed compiler result; another toolchain may require selecting the procedure at the original function's address with search_procedures.
  4. Call compare_functions with {"left":left,"right":right}, substituting the complete unedited Evidence objects, and retain its complete structuredContent as comparison. Call find_changed_behavior with {"comparisons":[comparison]}.
  5. Repeat both comparison calls with left against itself and right against itself as controls.

The recorded cross-artifact run produced two changed dimensions and two static candidates. Both self-comparisons produced unchanged calls, unknown references and zero changed static candidates. Both opened Hopper documents closed successfully. Repetition frequency beyond this recorded run was not measured.

Expected behavior

A provider-generated display label alone should not establish a different caller or reference endpoint across artifacts. Preserve uncertainty when stable endpoint identity or reference kinds are unavailable; this fixture should produce no changed calls/references static candidate solely because _main became EntryPoint.

The source contract in docs/mcp-contracts.md requires stable cross-artifact procedure identities and preserves unknown identity/kind facts. Independent known facts that contradict every possible pairing must still establish changed. This expectation does not require self-comparison references to become unchanged, because Hopper did not expose complete reference kinds.

Impact and scope

P2. The demonstrated stripped-artifact comparison produces two false structural change conclusions and misleading static candidates. Overall statuses remain unknown, other dimensions remain available, and these are not observations of runtime behavior. Manually checking the matching procedure bytes, incoming callsite and generated caller labels lets an analyst reject these particular candidates. Other providers, generated-label forms and runtime frequency were not tested.

REA version

6.7.0; source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node v26.12.0; macOS arm64; Apple clang 21.0.0, clang-2100.3.34.2; Hopper bundle 6.1.0-demo, build 42.6295552. Exact macOS release: NOT_ENOUGH_INFO; the verifier recorded OS family and architecture only.

Target and artifact context

Locally compiled arm64 Mach-O executable and its symbol-stripped copy. No target execution, live external API or saved Hopper database was used.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Selected fields from the observed comparison, projecting the calls/references elements of the dimension array:

{
  "status": "unknown",
  "summary": {"unchanged": 5, "changed": 2, "unknown": 1},
  "calls": {"status": "changed", "left_count": 1, "right_count": 1, "limitations": []},
  "references": {"status": "changed", "left_count": 1, "right_count": 1},
  "aggregate": {
    "behavior_status": "unknown",
    "summary": {"observed_changes": 0, "static_candidates": 2, "contradictions": 0, "unresolved": 1}
  }
}

The aggregate entry above projects the separate find_changed_behavior result. References also retained generated-name and unavailable-kind limitations despite reporting changed.

The original function is _unchanged at 0x100000460, with caller _main at 0x1000004a8. The stripped function is sub_100000460, with caller EntryPoint at the same address. Both incoming references are 0x1000004c4 to 0x100000460. nm confirmed _main and _unchanged disappear after stripping. The verifier checked matching 24 procedure bytes and the four-byte incoming call instruction, with otool -l confirming the mapping. Pseudocode, assembly and CFG compare unchanged.

The verifier also ran npm run test:focused -- tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts tests/boundary/mcp/functionComparisonMcp.test.ts: 2 files and 13 tests passed.

Typed outcome or recovery expected

Retain unknown endpoint identity/reference-kind facts and emit no changed calls/references static candidate for this generated-label mismatch. Keep the self-comparison controls and genuine known-conflict detection intact.

Analysis or suspected cause

Source-derived reasoning: isAutoName in src/domain/functionComparisonNormalization.ts:205-206 recognizes hexadecimal sub_, fcn. and FUN_ forms but omits EntryPoint. Call matching consumes it at src/domain/functionComparisonDimensions.ts:326-342; reference identity consumes it at src/domain/functionReferenceComparison.ts:97-114. The generated label consequently becomes a supposedly stable name. All three implementation files remain identical on fetched origin/main at cf5194f68e286a79ee4b4ffbe626f9f5b5bbbaf1; no later implementation fix was found.

Additional context

Prepared with AI assistance and independently reproduced with the real Hopper provider through public MCP calls. Related #5131 addressed address-generated reference labels using controlled Evidence. Its fix recognizes those forms but does not cover the real Hopper EntryPoint trigger reported here. No known-good version was established.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5535 ### 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` and `references` as `changed` when comparing an unchanged function in a Mach-O executable and its symbol-stripped copy analyzed by Hopper. The caller changes from `_main` to Hopper's generated `EntryPoint` label. Procedure bytes, incoming callsite, pseudocode, assembly and CFG match. `find_changed_behavior` repeats the two dimensions as changed static candidates. The overall comparison and aggregate behavior status remain `unknown`. These results were independently reproduced through the built production stdio MCP server. This submission uses that verifier's recorded results. ### Reproduction The independent verifier compiled this fixture and stripped a copy: ```sh work=$(mktemp -d) cat > "$work/prog.c" <<'C' #include <stdio.h> int unchanged(int x) { return x + 1; } int changed(int x) { return x * 2; } int removed(int x) { return x - 3; } int main(void) { printf("%d %d %d\n", unchanged(1), changed(2), removed(3)); return 0; } C clang -O0 -g -o "$work/prog-v1" "$work/prog.c" strip -o "$work/prog-v1-stripped" "$work/prog-v1" nm -n "$work/prog-v1" nm -n "$work/prog-v1-stripped" ``` 1. Start `node scripts/rea.mjs mcp` from the built source checkout in a stdio MCP client. The verifier used isolated home/config/data/cache/state/temp directories and sequential Hopper sessions. 2. Call `open_binary` with `{"path":"<absolute work directory>/prog-v1","provider_id":"hopper"}`, then `analyze_function` with `{"procedure":"_unchanged"}`. Retain the complete returned `structuredContent` Evidence as `left`. Call `close_binary` with `{}`. 3. Open `prog-v1-stripped` with the same provider, then call `analyze_function` with `{"procedure":"sub_100000460"}`. Retain its complete Evidence as `right`, then call `close_binary`. This address is the observed compiler result; another toolchain may require selecting the procedure at the original function's address with `search_procedures`. 4. Call `compare_functions` with `{"left":left,"right":right}`, substituting the complete unedited Evidence objects, and retain its complete `structuredContent` as `comparison`. Call `find_changed_behavior` with `{"comparisons":[comparison]}`. 5. Repeat both comparison calls with `left` against itself and `right` against itself as controls. The recorded cross-artifact run produced two changed dimensions and two static candidates. Both self-comparisons produced unchanged calls, unknown references and zero changed static candidates. Both opened Hopper documents closed successfully. Repetition frequency beyond this recorded run was not measured. ### Expected behavior A provider-generated display label alone should not establish a different caller or reference endpoint across artifacts. Preserve uncertainty when stable endpoint identity or reference kinds are unavailable; this fixture should produce no changed calls/references static candidate solely because `_main` became `EntryPoint`. The source contract in [docs/mcp-contracts.md](https://github.com/morluto/rea/blob/074768901f3cdc8205b61773fb1faf6f6c7b3ca8/docs/mcp-contracts.md#function-comparison-limitations) requires stable cross-artifact procedure identities and preserves unknown identity/kind facts. Independent known facts that contradict every possible pairing must still establish `changed`. This expectation does not require self-comparison references to become `unchanged`, because Hopper did not expose complete reference kinds. ### Impact and scope P2. The demonstrated stripped-artifact comparison produces two false structural change conclusions and misleading static candidates. Overall statuses remain unknown, other dimensions remain available, and these are not observations of runtime behavior. Manually checking the matching procedure bytes, incoming callsite and generated caller labels lets an analyst reject these particular candidates. Other providers, generated-label forms and runtime frequency were not tested. ### REA version 6.7.0; source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node v26.12.0; macOS arm64; Apple clang 21.0.0, clang-2100.3.34.2; Hopper bundle 6.1.0-demo, build 42.6295552. Exact macOS release: NOT_ENOUGH_INFO; the verifier recorded OS family and architecture only. ### Target and artifact context Locally compiled arm64 Mach-O executable and its symbol-stripped copy. No target execution, live external API or saved Hopper database was used. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Selected fields from the observed comparison, projecting the calls/references elements of the dimension array: ```json { "status": "unknown", "summary": {"unchanged": 5, "changed": 2, "unknown": 1}, "calls": {"status": "changed", "left_count": 1, "right_count": 1, "limitations": []}, "references": {"status": "changed", "left_count": 1, "right_count": 1}, "aggregate": { "behavior_status": "unknown", "summary": {"observed_changes": 0, "static_candidates": 2, "contradictions": 0, "unresolved": 1} } } ``` The `aggregate` entry above projects the separate `find_changed_behavior` result. References also retained generated-name and unavailable-kind limitations despite reporting changed. The original function is `_unchanged` at `0x100000460`, with caller `_main` at `0x1000004a8`. The stripped function is `sub_100000460`, with caller `EntryPoint` at the same address. Both incoming references are `0x1000004c4` to `0x100000460`. `nm` confirmed `_main` and `_unchanged` disappear after stripping. The verifier checked matching 24 procedure bytes and the four-byte incoming call instruction, with `otool -l` confirming the mapping. Pseudocode, assembly and CFG compare unchanged. The verifier also ran `npm run test:focused -- tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts tests/boundary/mcp/functionComparisonMcp.test.ts`: 2 files and 13 tests passed. ### Typed outcome or recovery expected Retain unknown endpoint identity/reference-kind facts and emit no changed calls/references static candidate for this generated-label mismatch. Keep the self-comparison controls and genuine known-conflict detection intact. ### Analysis or suspected cause Source-derived reasoning: `isAutoName` in `src/domain/functionComparisonNormalization.ts:205-206` recognizes hexadecimal `sub_`, `fcn.` and `FUN_` forms but omits `EntryPoint`. Call matching consumes it at `src/domain/functionComparisonDimensions.ts:326-342`; reference identity consumes it at `src/domain/functionReferenceComparison.ts:97-114`. The generated label consequently becomes a supposedly stable name. All three implementation files remain identical on fetched `origin/main` at `cf5194f68e286a79ee4b4ffbe626f9f5b5bbbaf1`; no later implementation fix was found. ### Additional context Prepared with AI assistance and independently reproduced with the real Hopper provider through public MCP calls. Related #5131 addressed address-generated reference labels using controlled Evidence. Its fix recognizes those forms but does not cover the real Hopper `EntryPoint` trigger reported here. No known-good version was established.
Author
Owner

@matbrgz commented on GitHub (Oct 20, 2026):

Cross-referencing with canonical issue #4681 and sibling #5534 regarding function comparison anomalies and false call/reference mutations on synthetic provider entrypoints. Cataloged under Cluster AO in consolidation tracker #4330.

<!-- gh-comment-id:6293050223 --> @matbrgz commented on GitHub (Oct 20, 2026): Cross-referencing with canonical issue #4681 and sibling #5534 regarding function comparison anomalies and false call/reference mutations on synthetic provider entrypoints. Cataloged under Cluster AO in consolidation tracker #4330.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Relationship comparison now bases endpoint identity on observed symbol provenance, not display-name spelling. A default/generated or unknown-origin EntryPoint cannot establish cross-artifact identity, so calls/references become unknown instead of false changed. Imported stable names remain comparable.

Implementation: functionRelationshipComparison.ts, functionComparisonReferenceFactsMcp.test.ts.

The relevant regression checks in functionComparisonReferenceFactsMcp.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380760206 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Relationship comparison now bases endpoint identity on observed symbol provenance, not display-name spelling. A default/generated or unknown-origin EntryPoint cannot establish cross-artifact identity, so calls/references become unknown instead of false changed. Imported stable names remain comparable. Implementation: [functionRelationshipComparison.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/functionRelationshipComparison.ts#L119), [functionComparisonReferenceFactsMcp.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts#L914). The relevant regression checks in [functionComparisonReferenceFactsMcp.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#3370
No description provided.