[GH-ISSUE #6815] [Bug] Nested custom-attribute names omit their enclosing types #4162

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

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

inspectManagedArtifactBytes returns attributes[0].type_name: "InnerAttribute" for both Fixture.Empty0+InnerAttribute and Fixture.Empty1+InnerAttribute, omitting the enclosing type and its namespace. Both results report complete metadata and coverage without issues. A top-level control correctly returns Fixture.InnerAttribute.

Observed through the production byte-reader API with benign synthetic PE fixtures; no CLR or target code was executed.

Reproduction

From this source checkout with locked dependencies installed, save the following as /tmp/rea-nested-attributes.mts and run REA_ROOT="$PWD" node_modules/.bin/tsx /tmp/rea-nested-attributes.mts /tmp/rea-nested-attribute-evidence:

import { mkdirSync, writeFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
const repo = process.env.REA_ROOT;
if (!repo) throw new Error('Set REA_ROOT to the REA source checkout');
const out = process.argv[2];
if (!out) throw new Error('Pass an evidence output directory');
mkdirSync(out, { recursive: true });
const load = (file: string) => import(pathToFileURL(`${repo}/src/dotnet/${file}.ts`).href);
const { buildManagedPeFixture, managedPeFixtureTarget } = await load('ManagedPe.fixture');
const { inspectManagedArtifactBytes } = await load('ManagedArtifactInspector');
const { inspectManagedMembersBytes } = await load('ManagedMemberInspector');
const { readManagedPeLayout } = await load('ManagedPeReader');
const { readManagedMetadataLayout, metadataRowOffset } = await load('ManagedMetadataLayout');
const results = [];
for (const enclosing of [null, 1, 2]) {
  const bytes = buildManagedPeFixture({ additionalEmptyTypes: 2,
    typeName: 'InnerAttribute', typeNamespace: 'Fixture', extendsRaw: 9,
    typeReferences: [{ namespace: 'System', name: 'Attribute' }],
    nestedTypes: enclosing === null ? [] : [[3, enclosing]],
    customAttributeTypeRaw: 10, methodName: '.ctor', methodFlags: 0x1886,
    methodSignature: Buffer.from([0x20, 1, 1, 0x0e]),
    memberRefParentRaw: 17, memberRefSignature: Buffer.from([0x20, 0, 1]),
    ilBody: Buffer.from([0x1e, 2, 0x28, 1, 0, 0, 0x0a, 0x2a]),
  });
  const pe = readManagedPeLayout(bytes), cli = pe.cli;
  const md = readManagedMetadataLayout(bytes, pe.rvaToOffset(cli.metadata.rva, cli.metadata.size, 'probe'), cli.metadata.size);
  // Bind System.Attribute to AssemblyRef 1; C# nested TypeDefs have an empty namespace.
  bytes.writeUInt16LE(6, metadataRowOffset(md, 1, 2));
  if (enclosing !== null) bytes.writeUInt16LE(0, metadataRowOffset(md, 2, 3) + 6);
  bytes.writeUInt16LE(bytes.readUInt16LE(0x84 + 18) | 0x2000, 0x84 + 18);
  bytes.writeUInt32LE(0, cli.headerOffset + 20);
  const path = `${out}/attribute-${enclosing ?? 'top-level'}.dll`;
  writeFileSync(path, bytes);
  const target = managedPeFixtureTarget(bytes, path);
  const artifact = inspectManagedArtifactBytes(bytes, target);
  const members = inspectManagedMembersBytes(bytes, target);
  const result = { enclosing, expectedTypeName: enclosing === null ? 'Fixture.InnerAttribute' : `Fixture.Empty${enclosing - 1}+InnerAttribute`,
    node: process.version, platform: process.platform, arch: process.arch,
    sha256: target.sha256, bytes: bytes.length,
    attribute: artifact.attributes[0], metadata: artifact.metadata, coverage: artifact.coverage,
    memberCoverage: members.coverage, type: members.types.find((t: any) => t.token === '0x02000003') };
  results.push(result);
  console.log(JSON.stringify(result));
}
writeFileSync(`${out}/nested-attributes.json`, JSON.stringify(results, null, 2) + '\n');

The one probe run inspected the top-level control and both nested variants. Each fixture points CustomAttribute.Type at the local attribute constructor and records the nested owner in NestedClass.

Expected behavior

An attribute type name should retain its enclosing chain, such as Fixture.Empty0+InnerAttribute, or explicitly remain unresolved. Two distinct nested attribute types should not receive the same apparently resolved name with no limitation.

ECMA-335 II.7.3 makes the enclosing type part of nested type resolution. .NET TypeName.FullName documents + between enclosing and nested names.

Impact and scope

The attribute inventory loses which nested attribute type was applied, so distinct attributes with the same leaf name become indistinguishable by type_name. This reproduced for both local nested-TypeDef variants; external nested TypeRef constructors were not runtime tested. The constructor token remains available for manual resolution through managed member/nesting metadata; that workaround was not exercised end to end.

REA version

6.7.0; source revision e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4.

Runtime and platform

Node.js v24.22.1; Linux x86_64; source imports through the locked tsx dependency. No analysis engine or CLR required.

Target and artifact context

Synthetic 4 KiB managed PE libraries with a local System.Attribute subclass constructor and one assembly-level CustomAttribute. Nested variants differ in their enclosing TypeDef row.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Selected fields from captured stdout (no second execution):

{"expectedTypeName":"Fixture.InnerAttribute","actualTypeName":"Fixture.InnerAttribute","enclosingTypeToken":null,"coverage":{"state":"complete","issues":[]}}
{"expectedTypeName":"Fixture.Empty0+InnerAttribute","actualTypeName":"InnerAttribute","enclosingTypeToken":"0x02000001","coverage":{"state":"complete","issues":[]}}
{"expectedTypeName":"Fixture.Empty1+InnerAttribute","actualTypeName":"InnerAttribute","enclosingTypeToken":"0x02000002","coverage":{"state":"complete","issues":[]}}

The same member inspection retains enclosing_type_token and distinct name_sha256 values, while the artifact attribute name loses the enclosing chain.

Typed outcome or recovery expected

Preserve a qualified nested attribute name, or return an explicit unresolved result and limitation rather than a flattened name with complete coverage.

Analysis or suspected cause

Source-derived cause: readTypeName builds only namespace + name. Constructor type lookup uses that helper for attribute constructors without consulting NestedClass; its TypeRef path also ignores the enclosing ResolutionScope. Only the local TypeDef path was runtime probed.

Additional context

All-state searches for nested attribute, "attribute" "type_name", and nested managed issues found no matching report. Related #5412 concerns member selection, #6760 concerns catch-target comparison, and #3154 requests custom-attribute argument decoding. They do not describe the attribute inventory name failure.

The reproduction uses parser fixtures. Compiler-generated nested-attribute files, CLR admission, external nested-TypeRef constructors, and public CLI/MCP delivery were not tested.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Two observed nested attribute types with different enclosing owners collapse to the same inventory type name, with complete coverage and no issue explaining the lost identity.
  • Workaround status: unverified
  • Workaround basis: The results retain constructor and enclosing-type tokens that could support manual resolution through managed member metadata, but that reconstruction workflow was not exercised end to end.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6815 ### 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 `inspectManagedArtifactBytes` returns `attributes[0].type_name: "InnerAttribute"` for both `Fixture.Empty0+InnerAttribute` and `Fixture.Empty1+InnerAttribute`, omitting the enclosing type and its namespace. Both results report complete metadata and coverage without issues. A top-level control correctly returns `Fixture.InnerAttribute`. Observed through the production byte-reader API with benign synthetic PE fixtures; no CLR or target code was executed. ### Reproduction From this source checkout with locked dependencies installed, save the following as `/tmp/rea-nested-attributes.mts` and run `REA_ROOT="$PWD" node_modules/.bin/tsx /tmp/rea-nested-attributes.mts /tmp/rea-nested-attribute-evidence`: ```ts import { mkdirSync, writeFileSync } from 'node:fs'; import { pathToFileURL } from 'node:url'; const repo = process.env.REA_ROOT; if (!repo) throw new Error('Set REA_ROOT to the REA source checkout'); const out = process.argv[2]; if (!out) throw new Error('Pass an evidence output directory'); mkdirSync(out, { recursive: true }); const load = (file: string) => import(pathToFileURL(`${repo}/src/dotnet/${file}.ts`).href); const { buildManagedPeFixture, managedPeFixtureTarget } = await load('ManagedPe.fixture'); const { inspectManagedArtifactBytes } = await load('ManagedArtifactInspector'); const { inspectManagedMembersBytes } = await load('ManagedMemberInspector'); const { readManagedPeLayout } = await load('ManagedPeReader'); const { readManagedMetadataLayout, metadataRowOffset } = await load('ManagedMetadataLayout'); const results = []; for (const enclosing of [null, 1, 2]) { const bytes = buildManagedPeFixture({ additionalEmptyTypes: 2, typeName: 'InnerAttribute', typeNamespace: 'Fixture', extendsRaw: 9, typeReferences: [{ namespace: 'System', name: 'Attribute' }], nestedTypes: enclosing === null ? [] : [[3, enclosing]], customAttributeTypeRaw: 10, methodName: '.ctor', methodFlags: 0x1886, methodSignature: Buffer.from([0x20, 1, 1, 0x0e]), memberRefParentRaw: 17, memberRefSignature: Buffer.from([0x20, 0, 1]), ilBody: Buffer.from([0x1e, 2, 0x28, 1, 0, 0, 0x0a, 0x2a]), }); const pe = readManagedPeLayout(bytes), cli = pe.cli; const md = readManagedMetadataLayout(bytes, pe.rvaToOffset(cli.metadata.rva, cli.metadata.size, 'probe'), cli.metadata.size); // Bind System.Attribute to AssemblyRef 1; C# nested TypeDefs have an empty namespace. bytes.writeUInt16LE(6, metadataRowOffset(md, 1, 2)); if (enclosing !== null) bytes.writeUInt16LE(0, metadataRowOffset(md, 2, 3) + 6); bytes.writeUInt16LE(bytes.readUInt16LE(0x84 + 18) | 0x2000, 0x84 + 18); bytes.writeUInt32LE(0, cli.headerOffset + 20); const path = `${out}/attribute-${enclosing ?? 'top-level'}.dll`; writeFileSync(path, bytes); const target = managedPeFixtureTarget(bytes, path); const artifact = inspectManagedArtifactBytes(bytes, target); const members = inspectManagedMembersBytes(bytes, target); const result = { enclosing, expectedTypeName: enclosing === null ? 'Fixture.InnerAttribute' : `Fixture.Empty${enclosing - 1}+InnerAttribute`, node: process.version, platform: process.platform, arch: process.arch, sha256: target.sha256, bytes: bytes.length, attribute: artifact.attributes[0], metadata: artifact.metadata, coverage: artifact.coverage, memberCoverage: members.coverage, type: members.types.find((t: any) => t.token === '0x02000003') }; results.push(result); console.log(JSON.stringify(result)); } writeFileSync(`${out}/nested-attributes.json`, JSON.stringify(results, null, 2) + '\n'); ``` The one probe run inspected the top-level control and both nested variants. Each fixture points CustomAttribute.Type at the local attribute constructor and records the nested owner in NestedClass. ### Expected behavior An attribute type name should retain its enclosing chain, such as `Fixture.Empty0+InnerAttribute`, or explicitly remain unresolved. Two distinct nested attribute types should not receive the same apparently resolved name with no limitation. [ECMA-335 II.7.3](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) makes the enclosing type part of nested type resolution. [.NET TypeName.FullName](https://source.dot.net/System.Private.CoreLib/src/runtime/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeName.cs.html) documents `+` between enclosing and nested names. ### Impact and scope The attribute inventory loses which nested attribute type was applied, so distinct attributes with the same leaf name become indistinguishable by `type_name`. This reproduced for both local nested-TypeDef variants; external nested TypeRef constructors were not runtime tested. The constructor token remains available for manual resolution through managed member/nesting metadata; that workaround was not exercised end to end. ### REA version 6.7.0; source revision `e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4`. ### Runtime and platform Node.js v24.22.1; Linux x86_64; source imports through the locked tsx dependency. No analysis engine or CLR required. ### Target and artifact context Synthetic 4 KiB managed PE libraries with a local `System.Attribute` subclass constructor and one assembly-level CustomAttribute. Nested variants differ in their enclosing TypeDef row. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Selected fields from captured stdout (no second execution): ```jsonl {"expectedTypeName":"Fixture.InnerAttribute","actualTypeName":"Fixture.InnerAttribute","enclosingTypeToken":null,"coverage":{"state":"complete","issues":[]}} {"expectedTypeName":"Fixture.Empty0+InnerAttribute","actualTypeName":"InnerAttribute","enclosingTypeToken":"0x02000001","coverage":{"state":"complete","issues":[]}} {"expectedTypeName":"Fixture.Empty1+InnerAttribute","actualTypeName":"InnerAttribute","enclosingTypeToken":"0x02000002","coverage":{"state":"complete","issues":[]}} ``` The same member inspection retains `enclosing_type_token` and distinct `name_sha256` values, while the artifact attribute name loses the enclosing chain. ### Typed outcome or recovery expected Preserve a qualified nested attribute name, or return an explicit unresolved result and limitation rather than a flattened name with complete coverage. ### Analysis or suspected cause Source-derived cause: [readTypeName](https://github.com/morluto/rea/blob/e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4/src/dotnet/ManagedMetadataInventoryRows.ts#L240) builds only `namespace + name`. [Constructor type lookup](https://github.com/morluto/rea/blob/e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4/src/dotnet/ManagedMetadataInventoryRows.ts#L285) uses that helper for attribute constructors without consulting NestedClass; its TypeRef path also ignores the enclosing ResolutionScope. Only the local TypeDef path was runtime probed. ### Additional context All-state searches for `nested attribute`, `"attribute" "type_name"`, and nested managed issues found no matching report. Related #5412 concerns member selection, #6760 concerns catch-target comparison, and #3154 requests custom-attribute argument decoding. They do not describe the attribute inventory name failure. The reproduction uses parser fixtures. Compiler-generated nested-attribute files, CLR admission, external nested-TypeRef constructors, and public CLI/MCP delivery were not tested. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Two observed nested attribute types with different enclosing owners collapse to the same inventory type name, with complete coverage and no issue explaining the lost identity. - Workaround status: unverified - Workaround basis: The results retain constructor and enclosing-type tokens that could support manual resolution through managed member metadata, but that reconstruction workflow was not exercised end to end.
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#4162
No description provided.