[GH-ISSUE #6713] [Bug] Managed comparison reports a method unchanged after its observed field target changes #4081

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

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

compareManagedMemberPaths reports Fixture.Program.Main unchanged when an aligned ldsfld site changes from Dependency.Counters.Counter to Dependency.Counters.Total, or from Dependency.Counters.Counter to Dependency.OtherCounters.Counter. Production inspection retains the differing field_name or field_declaring_type in field_accesses. The caller's CIL still contains ldsfld 0x0a000001; pop; ret, so its build-local token bytes and CIL digests are unchanged. Comparison reports complete input coverage, dimensions: [], and no method limitation. The same-field control also reports unchanged.

This executed the real path-comparison application workflow and source inspector. CLI/MCP transports and CLR loading were not run; the synthetic fixtures were never executed.

Reproduction

Use the stated source checkout with locked dependencies installed. Save this probe as /tmp/rea-managed-repro/probe.mts, with the checkout at /tmp/rea-managed-repro/rea, or substitute its import prefix. These paths normalize the executed scratch location. It writes inert PE/CLI fixtures only. The final three-case probe ran once; earlier probes with different fixture layouts were exploratory and are not the basis of this report.

mkdir -p /tmp/rea-managed-repro/work /tmp/rea-managed-repro/home /tmp/rea-managed-repro/config
HOME=/tmp/rea-managed-repro/home XDG_CONFIG_HOME=/tmp/rea-managed-repro/config node --import /tmp/rea-managed-repro/rea/node_modules/tsx/dist/loader.mjs /tmp/rea-managed-repro/probe.mts
import { writeFile } from 'node:fs/promises';
import { buildManagedPeFixture, managedPeFixtureTarget } from '/tmp/rea-managed-repro/rea/src/dotnet/ManagedPe.fixture.ts';
import { readManagedPeLayout } from '/tmp/rea-managed-repro/rea/src/dotnet/ManagedPeReader.ts';
import { readManagedMetadataLayout, metadataRowOffset } from '/tmp/rea-managed-repro/rea/src/dotnet/ManagedMetadataLayout.ts';
import { inspectManagedMembersBytes } from '/tmp/rea-managed-repro/rea/src/dotnet/ManagedMemberInspector.ts';
import { compareManagedMemberPaths } from '/tmp/rea-managed-repro/rea/src/application/managed/ManagedMemberComparisonService.ts';
const root='/tmp/rea-managed-repro/work';
function assembly(field:string,mvid:number,type='Counters') {
 const bytes=buildManagedPeFixture({targetFramework:null,references:['Fixture.Dependency','System.Runtime'],typeReferences:[{namespace:'Dependency',name:type},{namespace:'System',name:'Object'}],extendsRaw:13,memberRefName:field,memberRefParentRaw:17,memberRefSignature:Buffer.from([0x06,0x08]),mvid:Buffer.alloc(16,mvid),ilBody:Buffer.from([0x1e,0x7e,1,0,0,0x0a,0x26,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);
 bytes.writeUInt16LE(6,metadataRowOffset(md,1,2));bytes.writeUInt16LE(10,metadataRowOffset(md,1,3));
 return bytes;
}
const leftBytes=assembly('Counter',1);await writeFile(root+'/field-left.dll',leftBytes);
console.log(JSON.stringify({label:'left',access:inspectManagedMembersBytes(leftBytes,managedPeFixtureTarget(leftBytes)).field_accesses[0]}));
for(const [label,field,type] of [['same','Counter','Counters'],['changed-field','Total','Counters'],['changed-owner','Counter','OtherCounters']] as const){
 const bytes=assembly(field,2,type);await writeFile(root+'/field-right.dll',bytes);
 const inspection=inspectManagedMembersBytes(bytes,managedPeFixtureTarget(bytes));
 const compared=await compareManagedMemberPaths({leftPath:root+'/field-left.dll',rightPath:root+'/field-right.dll'});if(!compared.ok)throw compared.error;
 const r:any=compared.value.normalized_result;const m=r.methods[0];
 console.log(JSON.stringify({label,access:inspection.field_accesses[0],coverage:inspection.coverage,method_status:m.status,dimensions:m.dimensions,limitations:m.limitations,comparison_coverage:r.coverage.status}));
}

Expected behavior

A known difference in the field targeted by an aligned access should prevent an unqualified unchanged result for the accessing method: retain the observed target-name/type difference or explicitly mark that facet unknown. Keep pairing the same caller; this concerns its resulting status. The member comparison contract separates observed differences from unknowns and treats tokens as build-local coordinates. The edge-target documentation identifies field_accesses[].field_declaring_type as the declaring type. No external assembly loading or runtime field binding is needed to preserve the observed name difference.

Impact and scope

Method-change review can miss a concrete field-target difference already retained in both input observations. Both changed cases and the unchanged control have complete member coverage. The verified scope is one static field access through a MemberRef, with either its name or parent type changed at the same row. Other opcodes, overload identities, real applications, platforms and downstream incidents were not tested. Manually comparing the retained field_accesses rows is an observed available workaround for these cases.

REA version

Executed main revision: the pinned source link in Relevant output or evidence. Package manifest: 6.7.0. Latest published release supplied and source-compared: rea-agents-6.7.0. The release also computes field shape from opcode/kind without field targets. Its runtime was not separately executed; current main has additional call-target comparison that still does not examine field accesses.

Runtime and platform

Node.js v26.12.1; Linux x64; repository-locked tsx loader. No .NET runtime, Hopper or Ghidra was used. The exact repository-pinned Node version was not separately exercised.

Target and artifact context

Self-made inert managed PE/CLI fixtures with distinct MVIDs and a static void Main. MemberRef row 1 declares an i4 field in a fabricated Fixture.Dependency assembly; its TypeRef has a valid AssemblyRef scope. The caller has seven CIL bytes: 7e 01 00 00 0a 26 2a. The external dependency was not supplied, loaded or executed, so actual field availability and CLR validation are not claimed. This does not affect the observed difference between the retained static declarations.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Executed output; the process exited 0:

{"label":"left","access":{"method_token":"0x06000001","method":"Fixture.Program.Main","opcode":"ldsfld","field_token":"0x0a000001","field_name":"Counter","field_declaring_type":"Dependency.Counters"}}
{"label":"same","access":{"method_token":"0x06000001","method":"Fixture.Program.Main","opcode":"ldsfld","field_token":"0x0a000001","field_name":"Counter","field_declaring_type":"Dependency.Counters"},"coverage":{"state":"complete","issues":[]},"method_status":"unchanged","dimensions":[],"limitations":[],"comparison_coverage":"complete-within-inputs"}
{"label":"changed-field","access":{"method_token":"0x06000001","method":"Fixture.Program.Main","opcode":"ldsfld","field_token":"0x0a000001","field_name":"Total","field_declaring_type":"Dependency.Counters"},"coverage":{"state":"complete","issues":[]},"method_status":"unchanged","dimensions":[],"limitations":[],"comparison_coverage":"complete-within-inputs"}
{"label":"changed-owner","access":{"method_token":"0x06000001","method":"Fixture.Program.Main","opcode":"ldsfld","field_token":"0x0a000001","field_name":"Counter","field_declaring_type":"Dependency.OtherCounters"},"coverage":{"state":"complete","issues":[]},"method_status":"unchanged","dimensions":[],"limitations":[],"comparison_coverage":"complete-within-inputs"}

The access and coverage records above come from the production inspector, and status/dimensions/limitations/comparison coverage come from the path-comparison application result.

The executed field-shape comparison discards both field token and retained target names, keeping only opcode and operand kind. methodDimensions receives call edges but no field accesses. Equal raw CIL therefore leaves this known field-target difference unreported. The release source has the same target-free field-shape projection; no release execution is claimed.

Typed outcome or recovery expected

Keep the caller match and preserve the field-target difference in its change result, or attach an explicit unknown facet rather than reporting unchanged with no limitation. Restoration check, proposed and not run after a fix: both changed-target cases should stop reporting an unqualified unchanged method; the same-target control should remain unchanged. Keep the original build-local tokens as evidence rather than interpreting them as durable identities.

Analysis or suspected cause

Source-derived: caller signature and raw/decoded CIL are equal; fieldShape compares only ldsfld/field; the retained field_accesses names never participate. This concerns static declaration differences and does not assert runtime binding equivalence or prescribe an implementation.

Additional context

Open and closed searches for managed field comparison, field-target and field/unchanged reports found no matching report. Closed #6630 concerns changed call targets; main now compares call_edges but this field-access case still reproduces. #5151 asks to join anchors and relationship rows by IL offset; these fixtures have exactly one access, so no alignment ambiguity exists. No repository edits, builds, full suites, commits, PRs or issues were made.

Triage assessment

  • Impact level: P2
  • Assessment status: provisional
  • Impact basis: Production comparison hides two retained static field-target name differences with complete input coverage. The bounded correctness failure is observed; whether maintainers intend field-target names to participate in the current comparison scope remains unconfirmed.
  • Workaround status: available
  • Workaround basis: The executed inspections retain the different field_name and field_declaring_type values, so manually comparing field_accesses reveals both demonstrated changes.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6713 ### 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 `compareManagedMemberPaths` reports `Fixture.Program.Main` unchanged when an aligned `ldsfld` site changes from `Dependency.Counters.Counter` to `Dependency.Counters.Total`, or from `Dependency.Counters.Counter` to `Dependency.OtherCounters.Counter`. Production inspection retains the differing `field_name` or `field_declaring_type` in `field_accesses`. The caller's CIL still contains `ldsfld 0x0a000001; pop; ret`, so its build-local token bytes and CIL digests are unchanged. Comparison reports complete input coverage, `dimensions: []`, and no method limitation. The same-field control also reports unchanged. This executed the real path-comparison application workflow and source inspector. CLI/MCP transports and CLR loading were not run; the synthetic fixtures were never executed. ### Reproduction Use the stated source checkout with locked dependencies installed. Save this probe as `/tmp/rea-managed-repro/probe.mts`, with the checkout at `/tmp/rea-managed-repro/rea`, or substitute its import prefix. These paths normalize the executed scratch location. It writes inert PE/CLI fixtures only. The final three-case probe ran once; earlier probes with different fixture layouts were exploratory and are not the basis of this report. ```sh mkdir -p /tmp/rea-managed-repro/work /tmp/rea-managed-repro/home /tmp/rea-managed-repro/config HOME=/tmp/rea-managed-repro/home XDG_CONFIG_HOME=/tmp/rea-managed-repro/config node --import /tmp/rea-managed-repro/rea/node_modules/tsx/dist/loader.mjs /tmp/rea-managed-repro/probe.mts ``` ```ts import { writeFile } from 'node:fs/promises'; import { buildManagedPeFixture, managedPeFixtureTarget } from '/tmp/rea-managed-repro/rea/src/dotnet/ManagedPe.fixture.ts'; import { readManagedPeLayout } from '/tmp/rea-managed-repro/rea/src/dotnet/ManagedPeReader.ts'; import { readManagedMetadataLayout, metadataRowOffset } from '/tmp/rea-managed-repro/rea/src/dotnet/ManagedMetadataLayout.ts'; import { inspectManagedMembersBytes } from '/tmp/rea-managed-repro/rea/src/dotnet/ManagedMemberInspector.ts'; import { compareManagedMemberPaths } from '/tmp/rea-managed-repro/rea/src/application/managed/ManagedMemberComparisonService.ts'; const root='/tmp/rea-managed-repro/work'; function assembly(field:string,mvid:number,type='Counters') { const bytes=buildManagedPeFixture({targetFramework:null,references:['Fixture.Dependency','System.Runtime'],typeReferences:[{namespace:'Dependency',name:type},{namespace:'System',name:'Object'}],extendsRaw:13,memberRefName:field,memberRefParentRaw:17,memberRefSignature:Buffer.from([0x06,0x08]),mvid:Buffer.alloc(16,mvid),ilBody:Buffer.from([0x1e,0x7e,1,0,0,0x0a,0x26,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); bytes.writeUInt16LE(6,metadataRowOffset(md,1,2));bytes.writeUInt16LE(10,metadataRowOffset(md,1,3)); return bytes; } const leftBytes=assembly('Counter',1);await writeFile(root+'/field-left.dll',leftBytes); console.log(JSON.stringify({label:'left',access:inspectManagedMembersBytes(leftBytes,managedPeFixtureTarget(leftBytes)).field_accesses[0]})); for(const [label,field,type] of [['same','Counter','Counters'],['changed-field','Total','Counters'],['changed-owner','Counter','OtherCounters']] as const){ const bytes=assembly(field,2,type);await writeFile(root+'/field-right.dll',bytes); const inspection=inspectManagedMembersBytes(bytes,managedPeFixtureTarget(bytes)); const compared=await compareManagedMemberPaths({leftPath:root+'/field-left.dll',rightPath:root+'/field-right.dll'});if(!compared.ok)throw compared.error; const r:any=compared.value.normalized_result;const m=r.methods[0]; console.log(JSON.stringify({label,access:inspection.field_accesses[0],coverage:inspection.coverage,method_status:m.status,dimensions:m.dimensions,limitations:m.limitations,comparison_coverage:r.coverage.status})); } ``` ### Expected behavior A known difference in the field targeted by an aligned access should prevent an unqualified unchanged result for the accessing method: retain the observed target-name/type difference or explicitly mark that facet unknown. Keep pairing the same caller; this concerns its resulting status. The [member comparison contract](https://github.com/morluto/rea/blob/rea-agents-6.7.0/docs/managed-code-analysis.md#member-comparison) separates observed differences from unknowns and treats tokens as build-local coordinates. The [edge-target documentation](https://github.com/morluto/rea/blob/rea-agents-6.7.0/docs/managed-code-analysis.md#edge-target-declaring-types) identifies `field_accesses[].field_declaring_type` as the declaring type. No external assembly loading or runtime field binding is needed to preserve the observed name difference. ### Impact and scope Method-change review can miss a concrete field-target difference already retained in both input observations. Both changed cases and the unchanged control have complete member coverage. The verified scope is one static field access through a MemberRef, with either its name or parent type changed at the same row. Other opcodes, overload identities, real applications, platforms and downstream incidents were not tested. Manually comparing the retained `field_accesses` rows is an observed available workaround for these cases. ### REA version Executed main revision: the pinned source link in Relevant output or evidence. Package manifest: 6.7.0. Latest published release supplied and source-compared: `rea-agents-6.7.0`. The release also computes field shape from opcode/kind without field targets. Its runtime was not separately executed; current main has additional call-target comparison that still does not examine field accesses. ### Runtime and platform Node.js v26.12.1; Linux x64; repository-locked tsx loader. No .NET runtime, Hopper or Ghidra was used. The exact repository-pinned Node version was not separately exercised. ### Target and artifact context Self-made inert managed PE/CLI fixtures with distinct MVIDs and a static void `Main`. MemberRef row 1 declares an `i4` field in a fabricated `Fixture.Dependency` assembly; its TypeRef has a valid AssemblyRef scope. The caller has seven CIL bytes: `7e 01 00 00 0a 26 2a`. The external dependency was not supplied, loaded or executed, so actual field availability and CLR validation are not claimed. This does not affect the observed difference between the retained static declarations. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Executed output; the process exited 0: ```json {"label":"left","access":{"method_token":"0x06000001","method":"Fixture.Program.Main","opcode":"ldsfld","field_token":"0x0a000001","field_name":"Counter","field_declaring_type":"Dependency.Counters"}} {"label":"same","access":{"method_token":"0x06000001","method":"Fixture.Program.Main","opcode":"ldsfld","field_token":"0x0a000001","field_name":"Counter","field_declaring_type":"Dependency.Counters"},"coverage":{"state":"complete","issues":[]},"method_status":"unchanged","dimensions":[],"limitations":[],"comparison_coverage":"complete-within-inputs"} {"label":"changed-field","access":{"method_token":"0x06000001","method":"Fixture.Program.Main","opcode":"ldsfld","field_token":"0x0a000001","field_name":"Total","field_declaring_type":"Dependency.Counters"},"coverage":{"state":"complete","issues":[]},"method_status":"unchanged","dimensions":[],"limitations":[],"comparison_coverage":"complete-within-inputs"} {"label":"changed-owner","access":{"method_token":"0x06000001","method":"Fixture.Program.Main","opcode":"ldsfld","field_token":"0x0a000001","field_name":"Counter","field_declaring_type":"Dependency.OtherCounters"},"coverage":{"state":"complete","issues":[]},"method_status":"unchanged","dimensions":[],"limitations":[],"comparison_coverage":"complete-within-inputs"} ``` The `access` and `coverage` records above come from the production inspector, and status/dimensions/limitations/comparison coverage come from the path-comparison application result. [The executed field-shape comparison discards both field token and retained target names, keeping only opcode and operand kind](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/src/domain/managed/managedMemberComparisonItems.ts#L216-L225). `methodDimensions` receives call edges but no field accesses. Equal raw CIL therefore leaves this known field-target difference unreported. The release source has the same target-free field-shape projection; no release execution is claimed. ### Typed outcome or recovery expected Keep the caller match and preserve the field-target difference in its change result, or attach an explicit unknown facet rather than reporting unchanged with no limitation. Restoration check, proposed and not run after a fix: both changed-target cases should stop reporting an unqualified unchanged method; the same-target control should remain unchanged. Keep the original build-local tokens as evidence rather than interpreting them as durable identities. ### Analysis or suspected cause Source-derived: caller signature and raw/decoded CIL are equal; `fieldShape` compares only `ldsfld`/`field`; the retained `field_accesses` names never participate. This concerns static declaration differences and does not assert runtime binding equivalence or prescribe an implementation. ### Additional context Open and closed searches for managed field comparison, field-target and field/unchanged reports found no matching report. Closed #6630 concerns changed call targets; main now compares `call_edges` but this field-access case still reproduces. #5151 asks to join anchors and relationship rows by IL offset; these fixtures have exactly one access, so no alignment ambiguity exists. No repository edits, builds, full suites, commits, PRs or issues were made. ## Triage assessment - Impact level: P2 - Assessment status: provisional - Impact basis: Production comparison hides two retained static field-target name differences with complete input coverage. The bounded correctness failure is observed; whether maintainers intend field-target names to participate in the current comparison scope remains unconfirmed. - Workaround status: available - Workaround basis: The executed inspections retain the different field_name and field_declaring_type values, so manually comparing field_accesses reveals both demonstrated changes.
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#4081
No description provided.