mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(managed): report ExportedType rows in managed member inspection (#1828)
* fix(managed): report ExportedType rows in managed member inspection Facade assemblies such as System.Runtime.dll provide their types only through ExportedType rows (type forwarders), which member inspection never read, so they reported complete coverage with no types. Report each row as declared with its flags, forwarder bit, TypeDefId hint, and implementation token, and record invalid or null implementation coded indexes as partial coverage. Comparisons now state that exported types are not compared when either side has them. Fixes #1812 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(managed): read earlier member Evidence without exported types Member Evidence recorded before ExportedType rows were read omits the field. Parse it as null (not examined) instead of rejecting it, and state in comparisons that such an observation's exported types are unknown. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(managed): leave exported types unknown when metadata is unavailable Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0b4ce83618
commit
ed1519ddee
@@ -126,6 +126,15 @@ Member inspection binds build-local tokens, signatures, row offsets, method
|
||||
bodies, and decoded relationships to that artifact and MVID. Missing or malformed
|
||||
facts remain explicit in coverage and per-member status.
|
||||
|
||||
`exported_types` reports ExportedType rows as declared. Facade assemblies such
|
||||
as .NET `System.Runtime.dll` provide their types this way: `forwarder: true`
|
||||
marks a type forwarder, and `implementation_token` names the AssemblyRef, File,
|
||||
or enclosing ExportedType row that implements it. REA does not load the
|
||||
implementing assembly to resolve the forwarded definition, and
|
||||
`compare_managed_members` does not compare exported types. Member Evidence
|
||||
recorded before this field existed reads back with `exported_types: null`,
|
||||
which means the table was not examined.
|
||||
|
||||
CLI `#GUID` heap values retain their exact 16-byte GUID text. REA does not impose
|
||||
RFC 4122 UUID version or variant bits on MVID, EncId, or EncBaseId because
|
||||
ECMA-335 metadata does not require those bit patterns. Tokens and RVAs are
|
||||
|
||||
@@ -47,6 +47,7 @@ const emptyManagedMembers = (
|
||||
requires_mvid: mvid,
|
||||
},
|
||||
types: [],
|
||||
exported_types: [],
|
||||
fields: [],
|
||||
methods: [],
|
||||
member_refs: [],
|
||||
|
||||
@@ -110,6 +110,18 @@ const managedTypeSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
/** One ExportedType row: a type this assembly exports from another module or forwards. */
|
||||
const managedExportedTypeSchema = z.object({
|
||||
token: tokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
namespace: z.string(),
|
||||
name: z.string(),
|
||||
flags: z.number().int().min(0).max(0xffff_ffff),
|
||||
forwarder: z.boolean(),
|
||||
type_def_id: z.number().int().min(0).max(0xffff_ffff),
|
||||
implementation_token: tokenSchema.nullable(),
|
||||
});
|
||||
|
||||
const managedFieldSchema = z.object({
|
||||
token: tokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
@@ -420,6 +432,9 @@ export const managedMemberInspectionSchema = z.object({
|
||||
requires_mvid: cliMetadataGuidSchema.nullable(),
|
||||
}),
|
||||
types: z.array(managedTypeSchema),
|
||||
// Member Evidence recorded before ExportedType rows were read omits this
|
||||
// field; null means the table was not examined, not that it was empty.
|
||||
exported_types: z.array(managedExportedTypeSchema).nullable().default(null),
|
||||
fields: z.array(managedFieldSchema),
|
||||
methods: z.array(managedMethodSchema),
|
||||
member_refs: z.array(managedMemberReferenceSchema),
|
||||
|
||||
@@ -96,5 +96,16 @@ export const comparisonLimitations = (
|
||||
];
|
||||
if (left.coverage.state !== "complete" || right.coverage.state !== "complete")
|
||||
limitations.push("At least one managed member observation is partial.");
|
||||
if (left.exported_types === null || right.exported_types === null)
|
||||
limitations.push(
|
||||
"At least one managed member observation predates ExportedType inspection; its exported and forwarded types are unknown.",
|
||||
);
|
||||
if (
|
||||
(left.exported_types?.length ?? 0) > 0 ||
|
||||
(right.exported_types?.length ?? 0) > 0
|
||||
)
|
||||
limitations.push(
|
||||
"ExportedType rows, including type forwarders, are observed but not compared; an unchanged comparison does not establish an unchanged exported or forwarded type set.",
|
||||
);
|
||||
return limitations;
|
||||
};
|
||||
|
||||
@@ -5,9 +5,12 @@ import { ManagedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import { edges } from "./ManagedMemberRows.js";
|
||||
import type { ManagedMethod } from "./ManagedMemberInspectorCore.js";
|
||||
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
|
||||
import { managedMemberInspectionSchema } from "../domain/managed/managedArtifact.js";
|
||||
import { comparisonLimitations } from "../domain/managed/managedMemberComparisonCoverage.js";
|
||||
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
buildNativePeFixture,
|
||||
managedPeFixtureTarget,
|
||||
} from "./ManagedPe.fixture.js";
|
||||
|
||||
@@ -138,6 +141,112 @@ describe("managed member inspection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed exported types", () => {
|
||||
it("reports type forwarders and nested exported types with their implementation scope", () => {
|
||||
const bytes = buildManagedPeFixture({
|
||||
exportedTypes: [
|
||||
{
|
||||
namespace: "System",
|
||||
name: "Object",
|
||||
flags: 0x0020_0001,
|
||||
implementationRaw: (1 << 2) | 1,
|
||||
},
|
||||
{
|
||||
namespace: "",
|
||||
name: "Enumerator",
|
||||
flags: 0x0000_0002,
|
||||
implementationRaw: (1 << 2) | 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
|
||||
expect(result.metadata.table_row_counts).toMatchObject({
|
||||
ExportedType: 2,
|
||||
});
|
||||
expect(result.exported_types).toMatchObject([
|
||||
{
|
||||
token: "0x27000001",
|
||||
namespace: "System",
|
||||
name: "Object",
|
||||
flags: 0x0020_0001,
|
||||
forwarder: true,
|
||||
type_def_id: 0,
|
||||
implementation_token: "0x23000001",
|
||||
},
|
||||
{
|
||||
token: "0x27000002",
|
||||
namespace: "",
|
||||
name: "Enumerator",
|
||||
forwarder: false,
|
||||
implementation_token: "0x27000001",
|
||||
},
|
||||
]);
|
||||
expect(result.coverage).toEqual({ state: "complete", issues: [] });
|
||||
});
|
||||
|
||||
it("leaves exported types unknown when CLI metadata is unavailable", () => {
|
||||
const bytes = buildNativePeFixture();
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
|
||||
expect(result.coverage.state).toBe("unavailable");
|
||||
expect(result.exported_types).toBeNull();
|
||||
});
|
||||
|
||||
it("reads member Evidence recorded before exported types as unexamined", () => {
|
||||
const bytes = buildManagedPeFixture();
|
||||
const { exported_types: _omitted, ...legacy } = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
const parsed = managedMemberInspectionSchema.parse(legacy);
|
||||
|
||||
expect(parsed.exported_types).toBeNull();
|
||||
expect(comparisonLimitations(parsed, parsed)).toContain(
|
||||
"At least one managed member observation predates ExportedType inspection; its exported and forwarded types are unknown.",
|
||||
);
|
||||
});
|
||||
|
||||
it("retains an exported type whose implementation is out of range as partial", () => {
|
||||
const bytes = buildManagedPeFixture({
|
||||
exportedTypes: [
|
||||
{
|
||||
namespace: "System",
|
||||
name: "Object",
|
||||
flags: 0x0020_0001,
|
||||
implementationRaw: (9 << 2) | 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
|
||||
expect(result.exported_types).toMatchObject([
|
||||
{ token: "0x27000001", name: "Object", implementation_token: null },
|
||||
]);
|
||||
expect(result.coverage).toMatchObject({
|
||||
state: "partial",
|
||||
issues: [
|
||||
{
|
||||
code: "invalid-row",
|
||||
scope: "metadata.ExportedType:0x27000001",
|
||||
detail: expect.stringContaining(
|
||||
"ExportedType implementation coded index 0x25 is invalid",
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed member decoding budget", () => {
|
||||
it("charges decoded output rather than accumulated decoding bounds", () => {
|
||||
// Each bound fits alone, but 256 bytes per IL and signature byte together
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { parseTypes, typeRanges } from "./ManagedMemberInspectorCore.js";
|
||||
import {
|
||||
edges,
|
||||
parseExportedTypes,
|
||||
parseFields,
|
||||
parseMemberRefs,
|
||||
parseMethods,
|
||||
@@ -60,6 +61,7 @@ const unavailable = (
|
||||
requires_mvid: null,
|
||||
},
|
||||
types: [],
|
||||
exported_types: null,
|
||||
fields: [],
|
||||
methods: [],
|
||||
member_refs: [],
|
||||
@@ -117,6 +119,8 @@ export const inspectManagedMembersBytes = (
|
||||
const ranges = typeRanges(bytes, layout, issues);
|
||||
const parsedTypes = parseTypes(bytes, layout, ranges);
|
||||
issues.push(...parsedTypes.issues);
|
||||
const exportedTypes = parseExportedTypes(bytes, layout);
|
||||
issues.push(...exportedTypes.issues);
|
||||
const fields = parseFields(bytes, layout, ranges);
|
||||
const memberRefs = parseMemberRefs(bytes, layout);
|
||||
issues.push(...memberRefs.issues);
|
||||
@@ -165,6 +169,7 @@ export const inspectManagedMembersBytes = (
|
||||
requires_mvid: inventory.module?.mvid ?? null,
|
||||
},
|
||||
types: parsedTypes.types,
|
||||
exported_types: exportedTypes.exportedTypes,
|
||||
fields: fields.fields,
|
||||
methods: methods.methods,
|
||||
member_refs: memberRefs.refs,
|
||||
@@ -180,6 +185,7 @@ export const inspectManagedMembersBytes = (
|
||||
limitations: [
|
||||
"Metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
|
||||
"CIL instruction anchors are decoded from file-backed method bodies only; no target assembly is loaded or executed.",
|
||||
"Exported types are reported from ExportedType rows as declared; forwarded types are not resolved in their implementing assemblies.",
|
||||
"Signatures are decoded for common ECMA-335 primitive, class, valuetype, pointer, byref, array, and generic variable forms; unsupported forms retain raw signature hashes.",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -21,6 +21,9 @@ import {
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
|
||||
export type ManagedType = ManagedMemberInspection["types"][number];
|
||||
export type ManagedExportedType = NonNullable<
|
||||
ManagedMemberInspection["exported_types"]
|
||||
>[number];
|
||||
export type ManagedField = ManagedMemberInspection["fields"][number];
|
||||
export type ManagedMethod = ManagedMemberInspection["methods"][number];
|
||||
export type ManagedMemberRef = ManagedMemberInspection["member_refs"][number];
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
signature,
|
||||
type FieldCore,
|
||||
type ManagedCallEdge,
|
||||
type ManagedExportedType,
|
||||
type ManagedField,
|
||||
type ManagedFieldAccess,
|
||||
type ManagedMemberRef,
|
||||
@@ -78,6 +79,77 @@ export const parseFields = (
|
||||
return { fields, core, issues };
|
||||
};
|
||||
|
||||
const TYPE_FORWARDER_FLAG = 0x0020_0000;
|
||||
|
||||
/** Read ExportedType rows, including type forwarders, as declared. */
|
||||
export const parseExportedTypes = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
): {
|
||||
readonly exportedTypes: readonly ManagedExportedType[];
|
||||
readonly issues: readonly ManagedParseIssue[];
|
||||
} => {
|
||||
const exportedTypes: ManagedExportedType[] = [];
|
||||
const issues: ManagedParseIssue[] = [];
|
||||
const table = layout.table(39);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
readManagedValue(() => {
|
||||
const cursor = metadataRowCursor(bytes, layout, 39, row);
|
||||
const flags = cursor.readUInt32();
|
||||
const typeDefId = cursor.readUInt32();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
layout.strings.size,
|
||||
);
|
||||
const namespace = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
layout.strings.size,
|
||||
);
|
||||
const implementationOffset = cursor.offset;
|
||||
const implementationRaw = cursor.readIndex(
|
||||
layout.codedIndexSize("Implementation"),
|
||||
);
|
||||
const token = metadataToken(39, row);
|
||||
const implementationReason = metadataCodedTokenInvalidReason(
|
||||
implementationRaw,
|
||||
2,
|
||||
[38, 35, 39],
|
||||
layout.rowCounts,
|
||||
);
|
||||
if (implementationReason !== null || implementationRaw === 0)
|
||||
issues.push({
|
||||
code: "invalid-row",
|
||||
scope: `metadata.ExportedType:${token}`,
|
||||
offset: implementationOffset,
|
||||
detail:
|
||||
implementationReason === null
|
||||
? "ExportedType implementation coded index 0x0 is null, but the Implementation column must reference a row"
|
||||
: `ExportedType implementation coded index 0x${implementationRaw.toString(16)} is invalid: ${implementationReason}`,
|
||||
});
|
||||
exportedTypes.push({
|
||||
token,
|
||||
row_offset: cursor.start,
|
||||
namespace,
|
||||
name,
|
||||
flags,
|
||||
forwarder: (flags & TYPE_FORWARDER_FLAG) !== 0,
|
||||
type_def_id: typeDefId,
|
||||
implementation_token: metadataCodedToken(
|
||||
implementationRaw,
|
||||
2,
|
||||
[38, 35, 39],
|
||||
layout.rowCounts,
|
||||
),
|
||||
});
|
||||
}, issues);
|
||||
}
|
||||
return { exportedTypes, issues };
|
||||
};
|
||||
|
||||
export const parseMemberRefs = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
|
||||
@@ -7,6 +7,12 @@ interface ManagedPeFixtureOptions {
|
||||
readonly corruptMetadataSignature?: boolean;
|
||||
readonly customAttributeTypeRaw?: number;
|
||||
readonly extendsRaw?: number;
|
||||
readonly exportedTypes?: readonly {
|
||||
readonly namespace: string;
|
||||
readonly name: string;
|
||||
readonly flags: number;
|
||||
readonly implementationRaw: number;
|
||||
}[];
|
||||
readonly fieldName?: string;
|
||||
readonly fieldSignature?: Buffer;
|
||||
readonly ilBody?: Buffer;
|
||||
@@ -361,6 +367,21 @@ const manifestResourceRow = (
|
||||
metadataIndex(implementationRaw, implementationSize),
|
||||
]);
|
||||
|
||||
const exportedTypeRow = (
|
||||
exported: { readonly name: number; readonly namespace: number },
|
||||
flags: number,
|
||||
stringSize: MetadataIndexSize,
|
||||
implementationSize: MetadataIndexSize,
|
||||
implementationRaw: number,
|
||||
): Buffer =>
|
||||
Buffer.concat([
|
||||
u32(flags),
|
||||
u32(0),
|
||||
metadataIndex(exported.name, stringSize),
|
||||
metadataIndex(exported.namespace, stringSize),
|
||||
metadataIndex(implementationRaw, implementationSize),
|
||||
]);
|
||||
|
||||
const fixedStringAttributeBlob = (value: string): Buffer => {
|
||||
const bytes = Buffer.from(textEncoder.encode(value));
|
||||
if (bytes.length > 0x7f)
|
||||
@@ -469,6 +490,10 @@ interface FixtureMetadataRowContext {
|
||||
readonly pinvokeModuleName: number | null;
|
||||
readonly pinvokeImportName: number | null;
|
||||
readonly resourceName: number;
|
||||
readonly exportedTypes: readonly {
|
||||
readonly name: number;
|
||||
readonly namespace: number;
|
||||
}[];
|
||||
readonly tokenBlob: number;
|
||||
readonly fieldSignature: number;
|
||||
readonly methodSignature: number;
|
||||
@@ -521,6 +546,29 @@ const customAttributeRows = (
|
||||
),
|
||||
];
|
||||
|
||||
const exportedTypeTable = (
|
||||
options: ManagedPeFixtureOptions,
|
||||
indexes: FixtureMetadataRowContext["indexes"],
|
||||
heapSizes: FixtureMetadataRowContext["heapSizes"],
|
||||
widths: FixtureIndexWidths,
|
||||
): readonly (readonly [number, readonly Buffer[]])[] =>
|
||||
indexes.exportedTypes.length === 0
|
||||
? []
|
||||
: [
|
||||
[
|
||||
39,
|
||||
indexes.exportedTypes.map((exported, index) =>
|
||||
exportedTypeRow(
|
||||
exported,
|
||||
options.exportedTypes?.[index]?.flags ?? 0,
|
||||
heapSizes.strings,
|
||||
widths.implementation,
|
||||
options.exportedTypes?.[index]?.implementationRaw ?? 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
const fixtureIndexWidths = ({
|
||||
options,
|
||||
referenceStringIndexes,
|
||||
@@ -539,6 +587,7 @@ const fixtureIndexWidths = ({
|
||||
...(indexes.pinvokeImportName === null ? [] : ([[28, 1]] as const)),
|
||||
[32, options.assemblyRowCount ?? 1],
|
||||
[35, referenceStringIndexes.length],
|
||||
[39, indexes.exportedTypes.length],
|
||||
[40, 1],
|
||||
]);
|
||||
const count = (table: number): number => rowCounts.get(table) ?? 0;
|
||||
@@ -697,6 +746,7 @@ const fixtureMetadataRows = ({
|
||||
),
|
||||
),
|
||||
],
|
||||
...exportedTypeTable(options, indexes, heapSizes, widths),
|
||||
[
|
||||
40,
|
||||
[
|
||||
@@ -736,6 +786,12 @@ const buildManagedFixtureMetadata = (
|
||||
? null
|
||||
: strings.add(options.pinvoke.importName ?? "MessageBoxW");
|
||||
const resourceName = strings.add("Fixture.resources");
|
||||
const exportedTypes = (options.exportedTypes ?? []).map(
|
||||
({ namespace, name }) => ({
|
||||
name: strings.add(name),
|
||||
namespace: strings.add(namespace),
|
||||
}),
|
||||
);
|
||||
const referenceNames = options.references ?? ["System.Runtime"];
|
||||
const referenceStringIndexes = referenceNames.map((name) =>
|
||||
strings.add(name),
|
||||
@@ -775,6 +831,7 @@ const buildManagedFixtureMetadata = (
|
||||
pinvokeModuleName,
|
||||
pinvokeImportName,
|
||||
resourceName,
|
||||
exportedTypes,
|
||||
tokenBlob,
|
||||
fieldSignature,
|
||||
methodSignature,
|
||||
|
||||
Reference in New Issue
Block a user