[GH-ISSUE #6712] [Bug] Managed exception comparison treats build-local catch tokens as type identity #4080

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

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

Path-based managed comparison reports Main unchanged when its catch type changes from System.Exception to System.InvalidOperationException at the same TypeRef row. Moving the unchanged System.Exception catch type from row 2 to row 3 instead reports changed with exception-shape. Both cases have complete inspection coverage, equal CIL, and no per-method limitation. A same-type/same-row control reports unchanged.

This executed the production compareManagedMemberPaths application workflow, including real file resolution, snapshots, parsing and Evidence creation. CLI and MCP transports and CLR execution were not run.

Reproduction

In a source checkout with locked dependencies installed, save the following as /tmp/rea-managed-repro/probe.mts; paths below normalize the executed scratch location. Place the checkout at /tmp/rea-managed-repro/rea, or replace that prefix in the imports. The probe writes only inert fixtures in its disposable work directory. Main has a fat header, one ordinary catch clause, and CIL nop; leave.s IL_0006; pop; leave.s IL_0006; ret. The left catch type is always System.Exception at TypeRef row 2. Each of the three cases ran twice before this report was written.

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 } 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 { managedPeFixtureTarget } from '/tmp/rea-managed-repro/rea/src/dotnet/ManagedPe.fixture.ts';
import { compareManagedMemberPaths } from '/tmp/rea-managed-repro/rea/src/application/managed/ManagedMemberComparisonService.ts';
const root='/tmp/rea-managed-repro/work';
function assembly(name: string, mvid: number, row=2, moved=false) {
 const il=Buffer.from([0x00,0xde,0x03,0x26,0xde,0x00,0x2a]);
 const header=Buffer.alloc(12); header.writeUInt16LE(0x300b); header.writeUInt16LE(8,2); header.writeUInt32LE(il.length,4);
 const eh=Buffer.alloc(16);eh[0]=1;eh[1]=16;eh[8]=3;eh.writeUInt16LE(3,9);eh[11]=3;eh.writeUInt32LE(0x01000000+row,12);
 const types=moved?[{namespace:'System',name:'Object'},{namespace:'System',name}]:[{namespace:'System',name},{namespace:'System',name:'Object'}];
 const bytes=buildManagedPeFixture({targetFramework:null,references:['System.Runtime'],extendsRaw:((moved?2:3)<<2)|1,typeReferences:types,mvid:Buffer.alloc(16,mvid),ilBody:Buffer.concat([header,il,Buffer.alloc(1),eh])});
 const pe=readManagedPeLayout(bytes);const cli=pe.cli!;const md=readManagedMetadataLayout(bytes,pe.rvaToOffset(cli.metadata.rva,cli.metadata.size,'probe'),cli.metadata.size);
 for(const i of [2,3]) bytes.writeUInt16LE(6,metadataRowOffset(md,1,i));
 return bytes;
}
await writeFile(root+'/eh-left.dll',assembly('Exception',1));
for(const [label,name,row,moved] of [['same','Exception',2,false],['changed-type','InvalidOperationException',2,false],['moved-row','Exception',3,true]] as const){
 const bytes=assembly(name,2,row,moved);
 await writeFile(root+'/eh-right.dll',bytes);
 const inspection=inspectManagedMembersBytes(bytes,managedPeFixtureTarget(bytes));
 const r=await compareManagedMemberPaths({leftPath:root+'/eh-left.dll',rightPath:root+'/eh-right.dll'});
 if(!r.ok) throw r.error;
 const p:any=r.value.normalized_result;console.log(JSON.stringify({label,right_catch_type:'System.'+name,right_class_token:inspection.methods[0]?.body.exception_regions[0]?.class_token,right_body_status:inspection.methods[0]?.body.status,right_coverage:inspection.coverage,status:p.methods[0].status,dimensions:p.methods[0].dimensions,method_limitations:p.methods[0].limitations,comparison_coverage:p.coverage.status}));
}

Expected behavior

Build-local catch tokens must not establish cross-build catch-type equality or inequality. Preserve the known type change, or make catch-type equivalence explicitly unknown when the retained facts cannot establish it. A row move alone should not become an observed catch-type change. The existing member comparison contract distinguishes observed differences and unknowns and says metadata tokens are build-local coordinates. This request concerns static catch declarations, not CLR binding or runtime exception behavior.

Impact and scope

The observed method status hides one changed catch declaration and reports another unchanged catch declaration as changed. This can misdirect cross-build review of exception handling. Both input inventories are complete in all three cases. Other assemblies, platforms, TypeDef/TypeSpec catches and real CLR loading were not tested. The fixture source retains the catch names; manual metadata inspection is a possible external workaround, but no REA option that produces the correct comparison for these inputs was established.

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. That tag has the same raw exception_regions comparison; its runtime was not separately executed.

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 PE/CLI assemblies with distinct MVIDs, static void Fixture.Program.Main, valid external exception TypeRefs bound to AssemblyRef 1 (System.Runtime), and an ordinary small EH section. The catch-token field and the referenced type names/row positions vary as shown. No target was loaded, executed or mutated by REA.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed compact output; the process exited 0:

{"label":"same","right_catch_type":"System.Exception","right_class_token":"0x01000002","right_body_status":"present","right_coverage":{"state":"complete","issues":[]},"status":"unchanged","dimensions":[],"method_limitations":[],"comparison_coverage":"complete-within-inputs"}
{"label":"changed-type","right_catch_type":"System.InvalidOperationException","right_class_token":"0x01000002","right_body_status":"present","right_coverage":{"state":"complete","issues":[]},"status":"unchanged","dimensions":[],"method_limitations":[],"comparison_coverage":"complete-within-inputs"}
{"label":"moved-row","right_catch_type":"System.Exception","right_class_token":"0x01000003","right_body_status":"present","right_coverage":{"state":"complete","issues":[]},"status":"changed","dimensions":["exception-shape"],"method_limitations":[],"comparison_coverage":"complete-within-inputs"}

The right catch names in this output label the fixture inputs; they are not claimed to be resolved catch-name output from REA. right_class_token, body status and coverage come from production inspection; status, dimensions, limitations and comparison coverage come from the path comparison workflow.

Source-derived explanation: the executed comparison serializes the raw EH regions, including build-local class_token, directly across artifacts. Equal row tokens therefore hide a changed referenced type, and unequal row tokens create a difference for the same type. Source comparison with the release tag confirms the same comparison expression there; this is not a release runtime reproduction.

Typed outcome or recovery expected

Keep the matched method pair. For the changed-type input, report the exception-type change or an explicit unresolved catch-type facet instead of an unqualified unchanged method. For the moved-row input, do not infer a type change solely from the token. Restoration check, proposed and not run after a fix: distinguish these cases from the same-type/same-row control while preserving complete CIL and the original catch coordinates.

Analysis or suspected cause

The contrary results follow directly from comparing class_token strings without resolving or qualifying the cross-build type identity. This report does not prescribe a representation or require external assembly loading.

Additional context

Live open and closed searches for exception-shape, catch type, and exception/token/comparison reports found no matching issue. Closed #3279 concerns legacy small-EH section sizes; closed #6630 concerns call-target names; closed #6513 concerns locals-signature presence. These fixtures use standard section sizes, no calls and no locals signatures. No source changes, build, full test suite, commits or publication were performed.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The executed production path comparison hides a concrete catch-type change and reports a row relocation as a change, with complete input coverage and no method limitation. The effect is bounded to static cross-build comparison results.
  • Workaround status: unverified
  • Workaround basis: Manual inspection of the source metadata can distinguish these declarations, but no REA-only corrected comparison path was demonstrated.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6712 ### 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 Path-based managed comparison reports `Main` unchanged when its catch type changes from `System.Exception` to `System.InvalidOperationException` at the same TypeRef row. Moving the unchanged `System.Exception` catch type from row 2 to row 3 instead reports `changed` with `exception-shape`. Both cases have complete inspection coverage, equal CIL, and no per-method limitation. A same-type/same-row control reports unchanged. This executed the production `compareManagedMemberPaths` application workflow, including real file resolution, snapshots, parsing and Evidence creation. CLI and MCP transports and CLR execution were not run. ### Reproduction In a source checkout with locked dependencies installed, save the following as `/tmp/rea-managed-repro/probe.mts`; paths below normalize the executed scratch location. Place the checkout at `/tmp/rea-managed-repro/rea`, or replace that prefix in the imports. The probe writes only inert fixtures in its disposable work directory. `Main` has a fat header, one ordinary catch clause, and CIL `nop; leave.s IL_0006; pop; leave.s IL_0006; ret`. The left catch type is always `System.Exception` at TypeRef row 2. Each of the three cases ran twice before this report was written. ```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 } 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 { managedPeFixtureTarget } from '/tmp/rea-managed-repro/rea/src/dotnet/ManagedPe.fixture.ts'; import { compareManagedMemberPaths } from '/tmp/rea-managed-repro/rea/src/application/managed/ManagedMemberComparisonService.ts'; const root='/tmp/rea-managed-repro/work'; function assembly(name: string, mvid: number, row=2, moved=false) { const il=Buffer.from([0x00,0xde,0x03,0x26,0xde,0x00,0x2a]); const header=Buffer.alloc(12); header.writeUInt16LE(0x300b); header.writeUInt16LE(8,2); header.writeUInt32LE(il.length,4); const eh=Buffer.alloc(16);eh[0]=1;eh[1]=16;eh[8]=3;eh.writeUInt16LE(3,9);eh[11]=3;eh.writeUInt32LE(0x01000000+row,12); const types=moved?[{namespace:'System',name:'Object'},{namespace:'System',name}]:[{namespace:'System',name},{namespace:'System',name:'Object'}]; const bytes=buildManagedPeFixture({targetFramework:null,references:['System.Runtime'],extendsRaw:((moved?2:3)<<2)|1,typeReferences:types,mvid:Buffer.alloc(16,mvid),ilBody:Buffer.concat([header,il,Buffer.alloc(1),eh])}); const pe=readManagedPeLayout(bytes);const cli=pe.cli!;const md=readManagedMetadataLayout(bytes,pe.rvaToOffset(cli.metadata.rva,cli.metadata.size,'probe'),cli.metadata.size); for(const i of [2,3]) bytes.writeUInt16LE(6,metadataRowOffset(md,1,i)); return bytes; } await writeFile(root+'/eh-left.dll',assembly('Exception',1)); for(const [label,name,row,moved] of [['same','Exception',2,false],['changed-type','InvalidOperationException',2,false],['moved-row','Exception',3,true]] as const){ const bytes=assembly(name,2,row,moved); await writeFile(root+'/eh-right.dll',bytes); const inspection=inspectManagedMembersBytes(bytes,managedPeFixtureTarget(bytes)); const r=await compareManagedMemberPaths({leftPath:root+'/eh-left.dll',rightPath:root+'/eh-right.dll'}); if(!r.ok) throw r.error; const p:any=r.value.normalized_result;console.log(JSON.stringify({label,right_catch_type:'System.'+name,right_class_token:inspection.methods[0]?.body.exception_regions[0]?.class_token,right_body_status:inspection.methods[0]?.body.status,right_coverage:inspection.coverage,status:p.methods[0].status,dimensions:p.methods[0].dimensions,method_limitations:p.methods[0].limitations,comparison_coverage:p.coverage.status})); } ``` ### Expected behavior Build-local catch tokens must not establish cross-build catch-type equality or inequality. Preserve the known type change, or make catch-type equivalence explicitly unknown when the retained facts cannot establish it. A row move alone should not become an observed catch-type change. The existing [member comparison contract](https://github.com/morluto/rea/blob/rea-agents-6.7.0/docs/managed-code-analysis.md#member-comparison) distinguishes observed differences and unknowns and says metadata tokens are build-local coordinates. This request concerns static catch declarations, not CLR binding or runtime exception behavior. ### Impact and scope The observed method status hides one changed catch declaration and reports another unchanged catch declaration as changed. This can misdirect cross-build review of exception handling. Both input inventories are complete in all three cases. Other assemblies, platforms, TypeDef/TypeSpec catches and real CLR loading were not tested. The fixture source retains the catch names; manual metadata inspection is a possible external workaround, but no REA option that produces the correct comparison for these inputs was established. ### 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`. That tag has the same raw `exception_regions` comparison; its runtime was not separately executed. ### 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 PE/CLI assemblies with distinct MVIDs, static void `Fixture.Program.Main`, valid external exception TypeRefs bound to AssemblyRef 1 (`System.Runtime`), and an ordinary small EH section. The catch-token field and the referenced type names/row positions vary as shown. No target was loaded, executed or mutated by REA. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed compact output; the process exited 0: ```json {"label":"same","right_catch_type":"System.Exception","right_class_token":"0x01000002","right_body_status":"present","right_coverage":{"state":"complete","issues":[]},"status":"unchanged","dimensions":[],"method_limitations":[],"comparison_coverage":"complete-within-inputs"} {"label":"changed-type","right_catch_type":"System.InvalidOperationException","right_class_token":"0x01000002","right_body_status":"present","right_coverage":{"state":"complete","issues":[]},"status":"unchanged","dimensions":[],"method_limitations":[],"comparison_coverage":"complete-within-inputs"} {"label":"moved-row","right_catch_type":"System.Exception","right_class_token":"0x01000003","right_body_status":"present","right_coverage":{"state":"complete","issues":[]},"status":"changed","dimensions":["exception-shape"],"method_limitations":[],"comparison_coverage":"complete-within-inputs"} ``` The right catch names in this output label the fixture inputs; they are not claimed to be resolved catch-name output from REA. `right_class_token`, body status and coverage come from production inspection; status, dimensions, limitations and comparison coverage come from the path comparison workflow. Source-derived explanation: [the executed comparison serializes the raw EH regions, including build-local `class_token`, directly across artifacts](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/src/domain/managed/managedMemberComparisonItems.ts#L144-L148). Equal row tokens therefore hide a changed referenced type, and unequal row tokens create a difference for the same type. Source comparison with the release tag confirms the same comparison expression there; this is not a release runtime reproduction. ### Typed outcome or recovery expected Keep the matched method pair. For the changed-type input, report the exception-type change or an explicit unresolved catch-type facet instead of an unqualified unchanged method. For the moved-row input, do not infer a type change solely from the token. Restoration check, proposed and not run after a fix: distinguish these cases from the same-type/same-row control while preserving complete CIL and the original catch coordinates. ### Analysis or suspected cause The contrary results follow directly from comparing `class_token` strings without resolving or qualifying the cross-build type identity. This report does not prescribe a representation or require external assembly loading. ### Additional context Live open and closed searches for `exception-shape`, `catch type`, and exception/token/comparison reports found no matching issue. Closed #3279 concerns legacy small-EH section sizes; closed #6630 concerns call-target names; closed #6513 concerns locals-signature presence. These fixtures use standard section sizes, no calls and no locals signatures. No source changes, build, full test suite, commits or publication were performed. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The executed production path comparison hides a concrete catch-type change and reports a row relocation as a change, with complete input coverage and no method limitation. The effect is bounded to static cross-build comparison results. - Workaround status: unverified - Workaround basis: Manual inspection of the source metadata can distinguish these declarations, but no REA-only corrected comparison path 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#4080
No description provided.