[GH-ISSUE #6630] [Bug] Managed comparison reports a caller unchanged after its observed call target changes #4012

Closed
opened 2026-10-31 00:46: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/6630

Before submitting

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

Area

Native analysis or decompilation

Observed behavior

A production-module probe inspects two inert managed PE fixtures whose Main method contains identical call 0x0a000001; ret instructions. On the left, MemberRef row 1 names System.Console.Clear(); on the right, it names System.Console.WriteLine(). Inspection retains both different call targets with complete coverage. compareManagedMembers nevertheless returns status: "unchanged", dimensions: [], and no per-method limitation for Main. A control with the same call target on both sides also returns unchanged.

This was execution of inspectManagedMembersBytes and the production domain comparison function, not a CLI/MCP request or an authenticated Evidence workflow. The fixture assemblies were never loaded or executed.

Reproduction

Use a source checkout with its locked dependencies installed, placed at /tmp/rea-repro/rea for the normalized paths below. Save this executed probe as /tmp/rea-repro/probe.mts; only paths have been normalized. The two cases each ran once. The control changes only MVID; the failing case additionally changes the MemberRef method name.

import { release } from 'node:os';
import { buildManagedPeFixture, managedPeFixtureTarget } from '/tmp/rea-repro/rea/src/dotnet/ManagedPe.fixture.ts';
import { readManagedPeLayout } from '/tmp/rea-repro/rea/src/dotnet/ManagedPeReader.ts';
import { readManagedMetadataLayout, metadataRowOffset } from '/tmp/rea-repro/rea/src/dotnet/ManagedMetadataLayout.ts';
import { inspectManagedMembersBytes } from '/tmp/rea-repro/rea/src/dotnet/ManagedMemberInspector.ts';
import { compareManagedMembers } from '/tmp/rea-repro/rea/src/domain/managed/managedMemberComparison.ts';
function inspect(name: string, mvid: number) {
  const bytes = buildManagedPeFixture({references:['System.Runtime','System.Console'],targetFramework:null,
    typeRefScopeRaw:6,typeReferences:[{namespace:'System',name:'Console'},{namespace:'System',name:'Object'}],
    extendsRaw:13,memberRefParentRaw:17,memberRefName:name,memberRefSignature:Buffer.from([0,0,1]),
    mvid:Buffer.alloc(16,mvid),ilBody:Buffer.from([0x1a,0x28,1,0,0,0x0a,0x2a])});
  const pe=readManagedPeLayout(bytes); const cli=pe.cli!;
  const md=readManagedMetadataLayout(bytes,pe.rvaToOffset(cli.metadata.rva,cli.metadata.size,'probe'),cli.metadata.size);
  // Bind Console to AssemblyRef 2 and Object to AssemblyRef 1.
  bytes.writeUInt16LE(10,metadataRowOffset(md,1,2));
  bytes.writeUInt16LE(6,metadataRowOffset(md,1,3));
  return inspectManagedMembersBytes(bytes,managedPeFixtureTarget(bytes));
}
const left=inspect('Clear',1);
console.log(JSON.stringify({runtime:{node:process.version,platform:process.platform,arch:process.arch,release:release()}}));
for (const name of ['Clear','WriteLine']) {
  const right=inspect(name,2);
  const comparison=compareManagedMembers({evidenceId:'ev_'+'1'.repeat(64),result:left},{evidenceId:'ev_'+'2'.repeat(64),result:right});
  const method=comparison.methods[0]!;
  console.log(JSON.stringify({case:name==='Clear'?'same-target control':'changed-target',
    left_target:left.call_edges[0]!.target_declaring_type+'.'+left.call_edges[0]!.target_name,
    right_target:right.call_edges[0]!.target_declaring_type+'.'+right.call_edges[0]!.target_name,
    left_coverage:left.coverage,right_coverage:right.coverage,
    status:method.status,dimensions:method.dimensions,limitations:method.limitations,
    match:method.match.basis,coverage:comparison.coverage.status}));
}
mkdir -p /tmp/rea-repro/home /tmp/rea-repro/config
HOME=/tmp/rea-repro/home XDG_CONFIG_HOME=/tmp/rea-repro/config node --import /tmp/rea-repro/rea/node_modules/tsx/dist/loader.mjs /tmp/rea-repro/probe.mts

The synthetic Evidence IDs are direct-function inputs; they do not establish Evidence authentication or transport coverage.

Expected behavior

The documented comparison contract says it preserves observed differences and treats metadata tokens as build-local coordinates. A known change in the called member should prevent an unqualified unchanged result for the caller: retain the observed target difference, or return an explicit unknown/limitation if the comparison cannot establish target identity. Matching the same caller remains appropriate; this report concerns its resulting change status, not its pairing.

Contract: Member comparison documentation and build-local token boundary. The proposed correction does not require loading referenced assemblies or establishing CLR runtime behavior.

Impact and scope

The observed static comparison hides a concrete call-target name change already present in both input inventories. Analysts using the method status to locate changes can miss that difference. The failure occurred in the single changed-target pair tested; the unchanged control passed. Broader assembly coverage and runtime effects were not tested. Raw inspection still exposes the differing call_edges, so manually comparing those rows is an available workaround for this case. No data mutation or process lifecycle failure was observed.

REA version

Published package baseline supplied for this task: 6.7.0. Executed source revision: 00a9e95de91a4bd4d2a41f8aee704a53fa575750. The published package runtime was not separately executed.

Runtime and platform

Node.js v26.12.1; Linux 7.2.8-9-omarchy; x64. Source imports used the repository-locked tsx loader. No Hopper, Ghidra, CLR, or real-provider session was used.

Target and artifact context

Synthetic inert PE/CLI fixtures with distinct MVIDs, a static void Main method, an external System.Console TypeRef, and a no-argument void MemberRef. The six CIL bytes are 28 01 00 00 0a 2a. Both inspections report complete coverage. Independent CLR loading/validation of these fixtures was not performed.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Small output excerpt from the executed production-module probe:

{"case":"same-target control","left_target":"System.Console.Clear","right_target":"System.Console.Clear","left_coverage":{"state":"complete","issues":[]},"right_coverage":{"state":"complete","issues":[]},"status":"unchanged","dimensions":[],"limitations":[],"match":"exact-il-signature","coverage":"complete-within-inputs"}
{"case":"changed-target","left_target":"System.Console.Clear","right_target":"System.Console.WriteLine","left_coverage":{"state":"complete","issues":[]},"right_coverage":{"state":"complete","issues":[]},"status":"unchanged","dimensions":[],"limitations":[],"match":"exact-il-signature","coverage":"complete-within-inputs"}

The inspector preserves target names and declaring types. The production provider dispatches member inspection to this boundary. Source inspection also confirms that the shared comparison application calls this same domain function; the application/service and CLI/MCP layers were not executed by this probe.

Typed outcome or recovery expected

Keep the caller pair, but do not report it unchanged without qualifying the observed call-target difference. Restoration check, proposed and not run after a fix: the changed-target pair should expose a change or explicit unresolved-target limitation; the same-target control should remain unchanged with complete input coverage.

Analysis or suspected cause

Source-derived explanation: exact pairing hashes the caller signature and CIL digests; those are equal because the MemberRef token bytes are equal. Call-shape comparison retains only opcode and operand kind, dropping both retained target name and declaring type. An empty dimensions array with complete bodies produces unchanged. This explains the observed result without a claim that equal build-local tokens identify equal runtime callees.

Additional context

The supplied open-issue snapshot was searched for managed comparison, call-target, MemberRef, and call-shape reports. Live open and closed issue searches also found no matching call-target comparison report. Existing locals-signature comparison report #6513 concerns a separate retained facet and was skipped. No fix, build, full suite, or real-provider verification was performed.

Triage assessment

  • Impact level: P2
  • Assessment status: provisional
  • Impact basis: The executed comparison hides a retained call-target difference for the tested pair; the same-target control passes. This is a bounded static-analysis correctness failure. Whether maintainers intend call-target names to participate in the current comparison scope remains unconfirmed.
  • Workaround status: available
  • Workaround basis: Both executed inspections retain the different target names and declaring types in call_edges, allowing manual comparison of these rows for the demonstrated case.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6630 ### 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 Native analysis or decompilation ### Observed behavior A production-module probe inspects two inert managed PE fixtures whose Main method contains identical `call 0x0a000001; ret` instructions. On the left, MemberRef row 1 names `System.Console.Clear()`; on the right, it names `System.Console.WriteLine()`. Inspection retains both different call targets with complete coverage. `compareManagedMembers` nevertheless returns `status: "unchanged"`, `dimensions: []`, and no per-method limitation for Main. A control with the same call target on both sides also returns unchanged. This was execution of `inspectManagedMembersBytes` and the production domain comparison function, not a CLI/MCP request or an authenticated Evidence workflow. The fixture assemblies were never loaded or executed. ### Reproduction Use a source checkout with its locked dependencies installed, placed at `/tmp/rea-repro/rea` for the normalized paths below. Save this executed probe as `/tmp/rea-repro/probe.mts`; only paths have been normalized. The two cases each ran once. The control changes only MVID; the failing case additionally changes the MemberRef method name. ```ts import { release } from 'node:os'; import { buildManagedPeFixture, managedPeFixtureTarget } from '/tmp/rea-repro/rea/src/dotnet/ManagedPe.fixture.ts'; import { readManagedPeLayout } from '/tmp/rea-repro/rea/src/dotnet/ManagedPeReader.ts'; import { readManagedMetadataLayout, metadataRowOffset } from '/tmp/rea-repro/rea/src/dotnet/ManagedMetadataLayout.ts'; import { inspectManagedMembersBytes } from '/tmp/rea-repro/rea/src/dotnet/ManagedMemberInspector.ts'; import { compareManagedMembers } from '/tmp/rea-repro/rea/src/domain/managed/managedMemberComparison.ts'; function inspect(name: string, mvid: number) { const bytes = buildManagedPeFixture({references:['System.Runtime','System.Console'],targetFramework:null, typeRefScopeRaw:6,typeReferences:[{namespace:'System',name:'Console'},{namespace:'System',name:'Object'}], extendsRaw:13,memberRefParentRaw:17,memberRefName:name,memberRefSignature:Buffer.from([0,0,1]), mvid:Buffer.alloc(16,mvid),ilBody:Buffer.from([0x1a,0x28,1,0,0,0x0a,0x2a])}); const pe=readManagedPeLayout(bytes); const cli=pe.cli!; const md=readManagedMetadataLayout(bytes,pe.rvaToOffset(cli.metadata.rva,cli.metadata.size,'probe'),cli.metadata.size); // Bind Console to AssemblyRef 2 and Object to AssemblyRef 1. bytes.writeUInt16LE(10,metadataRowOffset(md,1,2)); bytes.writeUInt16LE(6,metadataRowOffset(md,1,3)); return inspectManagedMembersBytes(bytes,managedPeFixtureTarget(bytes)); } const left=inspect('Clear',1); console.log(JSON.stringify({runtime:{node:process.version,platform:process.platform,arch:process.arch,release:release()}})); for (const name of ['Clear','WriteLine']) { const right=inspect(name,2); const comparison=compareManagedMembers({evidenceId:'ev_'+'1'.repeat(64),result:left},{evidenceId:'ev_'+'2'.repeat(64),result:right}); const method=comparison.methods[0]!; console.log(JSON.stringify({case:name==='Clear'?'same-target control':'changed-target', left_target:left.call_edges[0]!.target_declaring_type+'.'+left.call_edges[0]!.target_name, right_target:right.call_edges[0]!.target_declaring_type+'.'+right.call_edges[0]!.target_name, left_coverage:left.coverage,right_coverage:right.coverage, status:method.status,dimensions:method.dimensions,limitations:method.limitations, match:method.match.basis,coverage:comparison.coverage.status})); } ``` ```sh mkdir -p /tmp/rea-repro/home /tmp/rea-repro/config HOME=/tmp/rea-repro/home XDG_CONFIG_HOME=/tmp/rea-repro/config node --import /tmp/rea-repro/rea/node_modules/tsx/dist/loader.mjs /tmp/rea-repro/probe.mts ``` The synthetic Evidence IDs are direct-function inputs; they do not establish Evidence authentication or transport coverage. ### Expected behavior The documented comparison contract says it preserves observed differences and treats metadata tokens as build-local coordinates. A known change in the called member should prevent an unqualified unchanged result for the caller: retain the observed target difference, or return an explicit unknown/limitation if the comparison cannot establish target identity. Matching the same caller remains appropriate; this report concerns its resulting change status, not its pairing. Contract: [Member comparison documentation](https://github.com/morluto/rea/blob/00a9e95de91a4bd4d2a41f8aee704a53fa575750/docs/managed-code-analysis.md#L314-L320) and [build-local token boundary](https://github.com/morluto/rea/blob/00a9e95de91a4bd4d2a41f8aee704a53fa575750/docs/managed-code-analysis.md#L389-L393). The proposed correction does not require loading referenced assemblies or establishing CLR runtime behavior. ### Impact and scope The observed static comparison hides a concrete call-target name change already present in both input inventories. Analysts using the method status to locate changes can miss that difference. The failure occurred in the single changed-target pair tested; the unchanged control passed. Broader assembly coverage and runtime effects were not tested. Raw inspection still exposes the differing `call_edges`, so manually comparing those rows is an available workaround for this case. No data mutation or process lifecycle failure was observed. ### REA version Published package baseline supplied for this task: 6.7.0. Executed source revision: `00a9e95de91a4bd4d2a41f8aee704a53fa575750`. The published package runtime was not separately executed. ### Runtime and platform Node.js v26.12.1; Linux 7.2.8-9-omarchy; x64. Source imports used the repository-locked tsx loader. No Hopper, Ghidra, CLR, or real-provider session was used. ### Target and artifact context Synthetic inert PE/CLI fixtures with distinct MVIDs, a static void Main method, an external `System.Console` TypeRef, and a no-argument void MemberRef. The six CIL bytes are `28 01 00 00 0a 2a`. Both inspections report complete coverage. Independent CLR loading/validation of these fixtures was not performed. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Small output excerpt from the executed production-module probe: ```json {"case":"same-target control","left_target":"System.Console.Clear","right_target":"System.Console.Clear","left_coverage":{"state":"complete","issues":[]},"right_coverage":{"state":"complete","issues":[]},"status":"unchanged","dimensions":[],"limitations":[],"match":"exact-il-signature","coverage":"complete-within-inputs"} {"case":"changed-target","left_target":"System.Console.Clear","right_target":"System.Console.WriteLine","left_coverage":{"state":"complete","issues":[]},"right_coverage":{"state":"complete","issues":[]},"status":"unchanged","dimensions":[],"limitations":[],"match":"exact-il-signature","coverage":"complete-within-inputs"} ``` [The inspector preserves target names and declaring types](https://github.com/morluto/rea/blob/00a9e95de91a4bd4d2a41f8aee704a53fa575750/src/dotnet/ManagedMemberRows.ts#L439-L449). [The production provider dispatches member inspection to this boundary](https://github.com/morluto/rea/blob/00a9e95de91a4bd4d2a41f8aee704a53fa575750/src/dotnet/ManagedStaticProvider.ts#L211-L218). Source inspection also confirms that the shared comparison application calls this same domain function; the application/service and CLI/MCP layers were not executed by this probe. ### Typed outcome or recovery expected Keep the caller pair, but do not report it unchanged without qualifying the observed call-target difference. Restoration check, proposed and not run after a fix: the changed-target pair should expose a change or explicit unresolved-target limitation; the same-target control should remain unchanged with complete input coverage. ### Analysis or suspected cause Source-derived explanation: [exact pairing hashes the caller signature and CIL digests](https://github.com/morluto/rea/blob/00a9e95de91a4bd4d2a41f8aee704a53fa575750/src/domain/managed/managedMemberComparisonMatch.ts#L51-L68); those are equal because the MemberRef token bytes are equal. [Call-shape comparison retains only opcode and operand kind](https://github.com/morluto/rea/blob/00a9e95de91a4bd4d2a41f8aee704a53fa575750/src/domain/managed/managedMemberComparisonItems.ts#L126-L131), dropping both retained target name and declaring type. [An empty dimensions array with complete bodies produces unchanged](https://github.com/morluto/rea/blob/00a9e95de91a4bd4d2a41f8aee704a53fa575750/src/domain/managed/managedMemberComparisonItems.ts#L438-L470). This explains the observed result without a claim that equal build-local tokens identify equal runtime callees. ### Additional context The supplied open-issue snapshot was searched for managed comparison, call-target, MemberRef, and call-shape reports. Live open and closed issue searches also found no matching call-target comparison report. Existing locals-signature comparison report #6513 concerns a separate retained facet and was skipped. No fix, build, full suite, or real-provider verification was performed. ## Triage assessment - Impact level: P2 - Assessment status: provisional - Impact basis: The executed comparison hides a retained call-target difference for the tested pair; the same-target control passes. This is a bounded static-analysis correctness failure. Whether maintainers intend call-target names to participate in the current comparison scope remains unconfirmed. - Workaround status: available - Workaround basis: Both executed inspections retain the different target names and declaring types in call_edges, allowing manual comparison of these rows for the demonstrated case.
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#4012
No description provided.