fix(managed): resolve member ownership through pointer tables (#1886)

Co-authored-by: LouisChen <[email protected]>
This commit is contained in:
LouisChen
2026-10-12 01:40:48 +08:00
committed by GitHub
co-authored by LouisChen
parent 369cd3dfdc
commit a95795c6ed
11 changed files with 766 additions and 26 deletions
+11
View File
@@ -126,6 +126,17 @@ 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.
For unoptimized `#-` metadata, nonempty `FieldPtr` and `MethodPtr` tables
translate TypeDef list positions to physical member definitions. REA resolves
these indirections for member ownership and local attribute-constructor types;
field and method tokens still identify the original definition rows.
`field_list` and `method_list` retain the metadata's list coordinates, which
refer to the pointer table when present rather than necessarily contiguous
definition rows. Malformed or ambiguous pointers leave the owner unknown with
partial coverage while preserving readable member and attribute facts.
The retained inverse indexes are admitted against the existing representation
budget before allocation.
`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,
+14 -10
View File
@@ -19,6 +19,10 @@ import {
readMetadataString,
sha256Bytes,
} from "./ManagedMetadataHeaps.js";
import {
memberListIndexSize,
memberListTable,
} from "./ManagedMemberPointers.js";
export type ManagedType = ManagedMemberInspection["types"][number];
export type ManagedExportedType = NonNullable<
@@ -93,8 +97,8 @@ const readTypeRange = (
row: number,
issues: ManagedParseIssue[],
): TypeRange => {
const methods = layout.table(6);
const fields = layout.table(4);
const methods = memberListTable(layout, 6);
const fields = memberListTable(layout, 4);
const cursor = metadataRowCursor(bytes, layout, 2, row);
cursor.readUInt32();
const nameIndex = cursor.readIndex(layout.stringIndexSize);
@@ -114,8 +118,8 @@ const readTypeRange = (
issues,
) ?? null;
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
const fieldStart = cursor.readIndex(layout.tableIndexSize(4));
const methodStart = cursor.readIndex(layout.tableIndexSize(6));
const fieldStart = cursor.readIndex(memberListIndexSize(layout, 4));
const methodStart = cursor.readIndex(memberListIndexSize(layout, 6));
let nextField = (fields?.rowCount ?? 0) + 1;
let nextMethod = (methods?.rowCount ?? 0) + 1;
const typeTable = layout.table(2);
@@ -125,8 +129,8 @@ const readTypeRange = (
next.readIndex(layout.stringIndexSize);
next.readIndex(layout.stringIndexSize);
next.readIndex(layout.codedIndexSize("TypeDefOrRef"));
nextField = next.readIndex(layout.tableIndexSize(4));
nextMethod = next.readIndex(layout.tableIndexSize(6));
nextField = next.readIndex(memberListIndexSize(layout, 4));
nextMethod = next.readIndex(memberListIndexSize(layout, 6));
}
return {
token: metadataToken(2, row),
@@ -209,8 +213,8 @@ export const parseTypes = (
readonly issues: readonly ManagedParseIssue[];
} => {
const typeTable = layout.table(2);
const fields = layout.table(4);
const methods = layout.table(6);
const fields = memberListTable(layout, 4);
const methods = memberListTable(layout, 6);
const items: ManagedType[] = [];
const issues: ManagedParseIssue[] = [];
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1) {
@@ -233,8 +237,8 @@ export const parseTypes = (
const extendsRaw = cursor.readIndex(
layout.codedIndexSize("TypeDefOrRef"),
);
const fieldStart = cursor.readIndex(layout.tableIndexSize(4));
const methodStart = cursor.readIndex(layout.tableIndexSize(6));
const fieldStart = cursor.readIndex(memberListIndexSize(layout, 4));
const methodStart = cursor.readIndex(memberListIndexSize(layout, 6));
const nextRange = ranges[row] ?? null;
const fieldRange = rowRange(
fields,
@@ -0,0 +1,96 @@
import { describe, expect, it, vi } from "vitest";
import {
attachManagedDecodeBudget,
ManagedDecodeBudget,
} from "./ManagedDecodeBudget.js";
import { memberListRowLookup } from "./ManagedMemberPointers.js";
import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import { buildManagedMemberPointerFixture } from "./ManagedPe.fixture.js";
import { readManagedPeLayout } from "./ManagedPeReader.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
const representationCapacity = 64 * 1024 * 1024;
const pointerFixture = () => {
const bytes = buildManagedMemberPointerFixture();
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("Fixture has no CLI metadata");
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(pe.cli.metadata.rva, pe.cli.metadata.size, "cli.metadata"),
pe.cli.metadata.size,
);
// A fresh owner lets the test set exact remaining capacity independently of
// the metadata version/string admission already exercised by the parser.
const budget = new ManagedDecodeBudget();
attachManagedDecodeBudget(layout, budget);
const indexBytes =
((layout.table(6)?.rowCount ?? 0) + 1) * Uint32Array.BYTES_PER_ELEMENT +
256;
return { bytes, layout, budget, indexBytes };
};
describe("managed member pointer index capacity", () => {
it("rejects insufficient capacity before allocating the inverse index", () => {
const { bytes, layout, budget, indexBytes } = pointerFixture();
budget.reserve(representationCapacity - indexBytes + 1, null);
let allocations = 0;
let failure: unknown;
// Proxy forwards static BYTES_PER_ELEMENT too: losing it would produce a
// NaN admission failure and falsely pass a before-allocation assertion.
vi.stubGlobal(
"Uint32Array",
new Proxy(Uint32Array, {
construct(target, arguments_, newTarget) {
allocations += 1;
return Reflect.construct(target, arguments_, newTarget);
},
}),
);
try {
memberListRowLookup(bytes, layout, 6);
} catch (cause: unknown) {
failure = cause;
} finally {
vi.unstubAllGlobals();
}
expect(failure).toBeInstanceOf(ManagedReaderFailure);
expect(failure).toMatchObject({
issue: {
code: "resource-limit",
scope: "metadata.representation",
offset: layout.table(5)?.offset,
},
});
expect(allocations).toBe(0);
});
it("reuses one layout's index without allocating or charging it again", () => {
const { bytes, layout, budget, indexBytes } = pointerFixture();
budget.reserve(representationCapacity - indexBytes, null);
let allocations = 0;
let observed: number[];
vi.stubGlobal(
"Uint32Array",
new Proxy(Uint32Array, {
construct(target, arguments_, newTarget) {
allocations += 1;
return Reflect.construct(target, arguments_, newTarget);
},
}),
);
try {
const first = memberListRowLookup(bytes, layout, 6);
const repeated = memberListRowLookup(bytes, layout, 6);
observed = [first(1), first(2), repeated(1), repeated(2)];
} finally {
vi.unstubAllGlobals();
}
expect(observed).toEqual([2, 1, 2, 1]);
expect(allocations).toBe(1);
expect(() => budget.reserve(1, null)).toThrow(ManagedReaderFailure);
});
});
+275
View File
@@ -0,0 +1,275 @@
import { describe, expect, it } from "vitest";
import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import { readManagedPeLayout } from "./ManagedPeReader.js";
import {
buildManagedMemberPointerFixture,
managedPeFixtureTarget,
} from "./ManagedPe.fixture.js";
const metadataLayout = (bytes: Buffer) => {
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("Fixture omitted CLI metadata");
return readManagedMetadataLayout(
bytes,
pe.rvaToOffset(pe.cli.metadata.rva, pe.cli.metadata.size, "cli.metadata"),
pe.cli.metadata.size,
);
};
describe("managed member pointer ownership", () => {
it.each([
{ table: "fields" as const, prefix: "0x040000" },
{ table: "methods" as const, prefix: "0x060000" },
])("maps reordered pointer slots to physical $table", ({ table, prefix }) => {
const bytes = buildManagedMemberPointerFixture();
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage).toEqual({ state: "complete", issues: [] });
expect(result.types.map((type) => type.full_name)).toEqual([
"<Module>",
"Fixture.A",
"Fixture.B",
]);
expect(
result[table].map(({ token, declaring_type, declaring_type_token }) => ({
token,
declaring_type,
declaring_type_token,
})),
).toEqual([
{
token: `${prefix}01`,
declaring_type: "Fixture.B",
declaring_type_token: "0x02000003",
},
{
token: `${prefix}02`,
declaring_type: "Fixture.A",
declaring_type_token: "0x02000002",
},
]);
});
it("resolves a local custom-attribute constructor through MethodPtr", () => {
const bytes = buildManagedMemberPointerFixture();
const target = managedPeFixtureTarget(bytes);
const artifact = inspectManagedArtifactBytes(bytes, target);
const members = inspectManagedMembersBytes(bytes, target);
expect(artifact.metadata.stream_names).toContain("#-");
expect(artifact.coverage).toEqual({ state: "complete", issues: [] });
expect(artifact.attributes).toEqual([
expect.objectContaining({
parent_token: "0x20000001",
constructor_token: "0x06000001",
type_name: "Fixture.B",
}),
]);
expect(artifact.attributes[0]?.type_name).toBe(
members.methods[0]?.declaring_type,
);
});
it.each([
{
label: "identity pointer tables",
fieldPointers: [1, 2],
methodPointers: [1, 2],
streamName: "#-" as const,
},
{
label: "ordinary direct lists",
fieldPointers: null,
methodPointers: null,
streamName: "#~" as const,
},
{
label: "empty pointer tables with direct lists",
fieldPointers: [],
methodPointers: [],
streamName: "#-" as const,
},
])("preserves $label", ({ fieldPointers, methodPointers, streamName }) => {
const bytes = buildManagedMemberPointerFixture({
fieldPointers,
methodPointers,
streamName,
});
const target = managedPeFixtureTarget(bytes);
const members = inspectManagedMembersBytes(bytes, target);
const artifact = inspectManagedArtifactBytes(bytes, target);
expect(members.coverage).toEqual({ state: "complete", issues: [] });
expect(members.fields.map((field) => field.declaring_type)).toEqual([
"Fixture.A",
"Fixture.B",
]);
expect(members.methods.map((method) => method.declaring_type)).toEqual([
"Fixture.A",
"Fixture.B",
]);
expect(artifact.attributes[0]?.type_name).toBe("Fixture.A");
});
});
describe("invalid managed member pointers", () => {
describe.each([
{
table: "fields" as const,
pointers: "fieldPointers" as const,
scope: "metadata.FieldPtr",
prefix: "0x040000",
names: ["firstField", "secondField"],
independent: "methods" as const,
},
{
table: "methods" as const,
pointers: "methodPointers" as const,
scope: "metadata.MethodPtr",
prefix: "0x060000",
names: [".ctor", ".ctor"],
independent: "fields" as const,
},
])(
"invalid $table pointers",
({ table, pointers, scope, prefix, names, independent }) => {
it.each([
{ label: "zero row", rows: [0, 1], owners: [null, null] },
{ label: "out-of-range row", rows: [3, 1], owners: [null, null] },
{ label: "duplicate row", rows: [1, 1], owners: [null, null] },
{ label: "omitted row", rows: [1], owners: ["Fixture.A", null] },
])(
"preserves member facts and independent ownership for $label",
({ rows, owners }) => {
const bytes = buildManagedMemberPointerFixture({ [pointers]: rows });
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage.state).toBe("partial");
expect(result.coverage.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "invalid-row",
scope,
offset: expect.any(Number),
}),
]),
);
expect(
result[table].map(
({ token, name, declaring_type, declaring_type_token }) => ({
token,
name,
declaring_type,
declaring_type_token,
}),
),
).toEqual(
owners.map((owner, index) => ({
token: `${prefix}0${index + 1}`,
name: names[index],
declaring_type: owner,
declaring_type_token: owner === null ? null : "0x02000002",
})),
);
expect(
result[independent].map((member) => member.declaring_type),
).toEqual(["Fixture.B", "Fixture.A"]);
expect(
result.coverage.issues.some(
(issue) =>
issue.scope ===
(table === "fields"
? "metadata.MethodPtr"
: "metadata.FieldPtr"),
),
).toBe(false);
},
);
},
);
it.each([
{ label: "zero row", methodPointers: [2, 0] },
{ label: "duplicate row", methodPointers: [2, 2] },
{ label: "omitted constructor row", methodPointers: [2] },
])(
"preserves attribute facts with an unknown constructor owner for $label",
({ methodPointers }) => {
const controlBytes = buildManagedMemberPointerFixture();
const control = inspectManagedArtifactBytes(
controlBytes,
managedPeFixtureTarget(controlBytes),
).attributes[0];
if (control === undefined)
throw new Error("Fixture omitted custom attribute");
expect(control.type_name).toBe("Fixture.B");
const bytes = buildManagedMemberPointerFixture({ methodPointers });
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage.state).toBe("partial");
expect(result.coverage.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "invalid-row",
scope: "metadata.MethodPtr",
}),
]),
);
expect(result.attributes).toEqual([
{ ...control, row_offset: expect.any(Number), type_name: null },
]);
},
);
it.each([
{ name: "FieldPtr", table: 3 },
{ name: "MethodPtr", table: 5 },
])("rejects a table stream ending inside a $name row", ({ table }) => {
const bytes = buildManagedMemberPointerFixture();
const layout = metadataLayout(bytes);
const pointers = layout.table(table);
if (pointers === undefined)
throw new Error("Fixture omitted pointer table");
const versionLength = bytes.readUInt32LE(layout.rootOffset + 12);
const firstStreamHeader =
((layout.rootOffset + 16 + versionLength + 3) & ~3) + 4;
const streamNameEnd = bytes.indexOf(0, firstStreamHeader + 8);
expect(bytes.toString("ascii", firstStreamHeader + 8, streamNameEnd)).toBe(
"#-",
);
const tablesOffset =
layout.rootOffset + bytes.readUInt32LE(firstStreamHeader);
bytes.writeUInt32LE(
pointers.offset + pointers.rowSize - 1 - tablesOffset,
firstStreamHeader + 4,
);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage).toMatchObject({
state: "unavailable",
issues: [
expect.objectContaining({
code: "invalid-metadata-root",
scope: "metadata.table-data",
}),
],
});
expect(result.fields).toEqual([]);
expect(result.methods).toEqual([]);
});
});
+101
View File
@@ -0,0 +1,101 @@
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
managedFailure,
ManagedReaderFailure,
} from "./ManagedReaderFailure.js";
type MemberTable = 4 | 6;
type ListRowLookup = (definitionRow: number) => number;
const lookups = new WeakMap<
ManagedMetadataLayout,
Map<MemberTable, ListRowLookup | ManagedReaderFailure>
>();
/** TypeDef list coordinates refer to the nonempty pointer table when present. */
export const memberListTable = (
layout: ManagedMetadataLayout,
table: MemberTable,
) => {
const pointers = layout.table(table - 1);
return pointers !== undefined && pointers.rowCount > 0
? pointers
: layout.table(table);
};
/** List columns must accommodate either physical definition or pointer rows. */
export const memberListIndexSize = (
layout: ManagedMetadataLayout,
table: MemberTable,
): 2 | 4 =>
layout.tableIndexSize(table - 1) === 4 || layout.tableIndexSize(table) === 4
? 4
: 2;
/** Resolve physical definition rows to list positions without changing token identity. */
export const memberListRowLookup = (
bytes: Buffer,
layout: ManagedMetadataLayout,
table: MemberTable,
): ListRowLookup => {
const pointers = layout.table(table - 1);
if (pointers === undefined || pointers.rowCount === 0) return (row) => row;
let byTable = lookups.get(layout);
const cached = byTable?.get(table);
if (cached instanceof ManagedReaderFailure) throw cached;
if (cached !== undefined) return cached;
const definitionCount = layout.table(table)?.rowCount ?? 0;
// The layout already admitted the source table extent. Reserve retained index
// storage and its fixed bookkeeping before allocating either container.
managedDecodeBudget(layout).reserve(
(definitionCount + 1) * Uint32Array.BYTES_PER_ELEMENT + 256,
pointers.offset,
);
if (byTable === undefined) {
byTable = new Map();
lookups.set(layout, byTable);
}
const positions = new Uint32Array(definitionCount + 1);
try {
const width = layout.tableIndexSize(table);
for (let row = 1; row <= pointers.rowCount; row += 1) {
const offset: number = pointers.offset + (row - 1) * pointers.rowSize;
const definition =
width === 2 ? bytes.readUInt16LE(offset) : bytes.readUInt32LE(offset);
if (definition === 0 || definition > definitionCount)
throw managedFailure(
"invalid-row",
`metadata.${pointers.name}`,
`Pointer row ${String(row)} names an out-of-range definition row ${String(definition)}`,
offset,
);
if (positions[definition] !== 0)
throw managedFailure(
"invalid-row",
`metadata.${pointers.name}`,
`Definition row ${String(definition)} has more than one pointer row; its declaring type is ambiguous`,
offset,
);
positions[definition] = row;
}
const lookup: ListRowLookup = (definition) => {
const position = positions[definition] ?? 0;
if (position === 0)
throw managedFailure(
"invalid-row",
`metadata.${pointers.name}`,
`Definition row ${String(definition)} has no pointer row identifying its declaring type`,
pointers.offset,
);
return position;
};
byTable.set(table, lookup);
return lookup;
} catch (cause: unknown) {
// Invalid metadata must not rebuild and charge the same retained index for
// every attribute/member that asks for its owner.
if (cause instanceof ManagedReaderFailure) byTable.set(table, cause);
throw cause;
}
};
+19 -2
View File
@@ -31,6 +31,7 @@ import {
} from "./ManagedMetadataHeaps.js";
import { readManagedValue } from "./ManagedReaderFailure.js";
import { methodBody } from "./ManagedMethodBodyReader.js";
import { memberListRowLookup } from "./ManagedMemberPointers.js";
export const parseFields = (
bytes: Buffer,
@@ -46,6 +47,10 @@ export const parseFields = (
const issues: ManagedParseIssue[] = [];
const table = layout.table(4);
const declaringType = createDeclaringTypeLookup(ranges, "field");
const listRow = readManagedValue(
() => memberListRowLookup(bytes, layout, 4),
issues,
);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
readManagedValue(() => {
const cursor = metadataRowCursor(bytes, layout, 4, row);
@@ -62,7 +67,11 @@ export const parseFields = (
cursor.readIndex(layout.blobIndexSize),
layout.blob.size,
);
const declared = declaringType(row);
const position =
listRow === undefined
? undefined
: readManagedValue(() => listRow(row), issues);
const declared = position === undefined ? null : declaringType(position);
const token = metadataToken(4, row);
fields.push({
token,
@@ -237,6 +246,10 @@ export const parseMethods = ({
const issues: ManagedParseIssue[] = [];
const table = layout.table(6);
const declaringType = createDeclaringTypeLookup(ranges, "method");
const listRow = readManagedValue(
() => memberListRowLookup(bytes, layout, 6),
issues,
);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
readManagedValue(() => {
const cursor = metadataRowCursor(bytes, layout, 6, row);
@@ -256,7 +269,11 @@ export const parseMethods = ({
layout.blob.size,
);
cursor.readIndex(layout.tableIndexSize(8));
const declared = declaringType(row);
const position =
listRow === undefined
? undefined
: readManagedValue(() => listRow(row), issues);
const declared = position === undefined ? null : declaringType(position);
const token = metadataToken(6, row);
methods.push({
token,
+1 -1
View File
@@ -155,7 +155,7 @@ const readAttributes = (
} => {
const attributes = readRows(layout.table(12), (row) =>
readManagedValue(
() => readCustomAttribute(bytes, layout, row, heapExtent(layout)),
() => readCustomAttribute(bytes, layout, row, heapExtent(layout), issues),
issues,
),
);
+21 -8
View File
@@ -1,4 +1,9 @@
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import {
memberListIndexSize,
memberListRowLookup,
memberListTable,
} from "./ManagedMemberPointers.js";
import type {
ManagedArtifactInspection,
@@ -17,7 +22,7 @@ import {
readMetadataGuid,
readMetadataString,
} from "./ManagedMetadataHeaps.js";
import { managedFailure } from "./ManagedReaderFailure.js";
import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
import type { ManagedResourceDirectory } from "./ManagedMetadataInventory.js";
type ModuleIdentity = NonNullable<ManagedArtifactInspection["module"]>;
@@ -242,21 +247,24 @@ const declaringTypeForMethod = (
heapExtent: number,
): string | null => {
const types = layout.table(2);
const methods = layout.table(6);
const methods = memberListTable(layout, 6);
if (types === undefined || methods === undefined) return null;
if (methodRow < 1 || methodRow > (layout.table(6)?.rowCount ?? 0))
return null;
const position = memberListRowLookup(bytes, layout, 6)(methodRow);
for (let row = 1; row <= types.rowCount; row += 1) {
const cursor = metadataRowCursor(bytes, layout, 2, row);
cursor.readUInt32();
const nameIndex = cursor.readIndex(layout.stringIndexSize);
const namespaceIndex = cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
cursor.readIndex(layout.tableIndexSize(4));
const methodStart = cursor.readIndex(layout.tableIndexSize(6));
cursor.readIndex(memberListIndexSize(layout, 4));
const methodStart = cursor.readIndex(memberListIndexSize(layout, 6));
const methodEnd =
row === types.rowCount
? methods.rowCount + 1
: methodListForType(bytes, layout, row + 1);
if (methodRow < methodStart || methodRow >= methodEnd) continue;
if (position < methodStart || position >= methodEnd) continue;
const name = readMetadataString(bytes, layout, nameIndex, heapExtent);
const namespace = readMetadataString(
bytes,
@@ -279,8 +287,8 @@ const methodListForType = (
cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
cursor.readIndex(layout.tableIndexSize(4));
return cursor.readIndex(layout.tableIndexSize(6));
cursor.readIndex(memberListIndexSize(layout, 4));
return cursor.readIndex(memberListIndexSize(layout, 6));
};
const attributeTypeName = (
@@ -377,6 +385,7 @@ export const readCustomAttribute = (
layout: ManagedMetadataLayout,
row: number,
heapExtent: number,
issues: ManagedParseIssue[],
): CustomAttribute => {
const cursor = metadataRowCursor(bytes, layout, 12, row);
const parentRaw = cursor.readIndex(
@@ -432,7 +441,11 @@ export const readCustomAttribute = (
: `CustomAttribute constructor coded index 0x${typeRaw.toString(16)} is invalid: ${typeReason}`,
typeOffset,
);
const typeName = attributeTypeName(bytes, layout, typeRaw, heapExtent);
const typeName =
readManagedValue(
() => attributeTypeName(bytes, layout, typeRaw, heapExtent),
issues,
) ?? null;
return {
parent_token: parent,
constructor_token: metadataCodedToken(
+1 -1
View File
@@ -15,7 +15,7 @@ const ROW_SIZE_FACTORIES: readonly RowSizeFactory[] = [
({ stringSize: s, guidSize: g }) => 2 + s + g * 3,
({ codedSize: c, stringSize: s }) => c("ResolutionScope") + s * 2,
({ tableSize: t, codedSize: c, stringSize: s }) =>
4 + s * 2 + c("TypeDefOrRef") + t(4) + t(6),
4 + s * 2 + c("TypeDefOrRef") + Math.max(t(3), t(4)) + Math.max(t(5), t(6)),
({ tableSize: t }) => t(4),
({ stringSize: s, blobSize: b }) => 2 + s + b,
({ tableSize: t }) => t(6),
+133 -4
View File
@@ -190,6 +190,7 @@ const typeDefRow = ({
fieldListSize,
methodListSize,
extendsRaw = (1 << 2) | 1,
flags = 0x0010_0001,
}: {
readonly name: number;
readonly namespace: number;
@@ -200,9 +201,10 @@ const typeDefRow = ({
readonly fieldListSize: MetadataIndexSize;
readonly methodListSize: MetadataIndexSize;
readonly extendsRaw?: number;
readonly flags?: number;
}): Buffer =>
Buffer.concat([
u32(0x0010_0001),
u32(flags),
metadataIndex(name, stringSize),
metadataIndex(namespace, stringSize),
metadataIndex(extendsRaw, typeDefOrRefSize),
@@ -231,6 +233,7 @@ interface MethodDefRowInput {
readonly rva?: number;
readonly flags?: number;
readonly implFlags?: number;
readonly parameterList?: number;
}
const methodDefRow = ({
@@ -242,6 +245,7 @@ const methodDefRow = ({
rva = 0,
flags = 0x0016,
implFlags = 0,
parameterList = 0,
}: MethodDefRowInput): Buffer =>
Buffer.concat([
u32(rva),
@@ -249,7 +253,7 @@ const methodDefRow = ({
u16(flags),
metadataIndex(name, stringSize),
metadataIndex(signature, blobSize),
metadataIndex(0, parameterSize),
metadataIndex(parameterList, parameterSize),
]);
const memberRefRow = ({
@@ -416,6 +420,7 @@ const metadataRoot = (
strings: Buffer,
guid: Buffer,
blob: Buffer,
tableStreamName: "#~" | "#-" = "#~",
): Buffer => {
const version = Buffer.from("v4.0.30319\0\0", "utf8");
const header = Buffer.concat([
@@ -429,7 +434,7 @@ const metadataRoot = (
u16(4),
]);
const streamHeaderSize =
metadataStreamHeader(0, 0, "#~").length +
metadataStreamHeader(0, 0, tableStreamName).length +
metadataStreamHeader(0, 0, "#Strings").length +
metadataStreamHeader(0, 0, "#GUID").length +
metadataStreamHeader(0, 0, "#Blob").length;
@@ -442,7 +447,7 @@ const metadataRoot = (
offset = align4(offset + guid.length);
const blobOffset = offset;
const headers = Buffer.concat([
metadataStreamHeader(tablesOffset, tables.length, "#~"),
metadataStreamHeader(tablesOffset, tables.length, tableStreamName),
metadataStreamHeader(stringsOffset, strings.length, "#Strings"),
metadataStreamHeader(guidOffset, guid.length, "#GUID"),
metadataStreamHeader(blobOffset, blob.length, "#Blob"),
@@ -990,6 +995,130 @@ export const buildManagedPeFixture = (
buildManagedFixtureMetadata(options, methodRva),
);
/** Two attribute classes whose pointer-table slots can reorder physical members. */
export const buildManagedMemberPointerFixture = (
options: {
readonly fieldPointers?: readonly number[] | null;
readonly methodPointers?: readonly number[] | null;
readonly streamName?: "#~" | "#-";
} = {},
): Buffer => {
const fieldPointers =
options.fieldPointers === undefined ? [2, 1] : options.fieldPointers;
const methodPointers =
options.methodPointers === undefined ? [2, 1] : options.methodPointers;
// Both instance constructors call System.Attribute::.ctor(), then return.
const body = Buffer.from([0x1e, 0x02, 0x28, 0x01, 0, 0, 0x0a, 0x2a]);
const image = buildManagedFixtureImage({ ilBody: body }, (methodRva) => {
const strings = new StringHeap();
const blobs = new BlobHeap();
const moduleName = strings.add("MemberPointers.dll");
const assemblyName = strings.add("Fixture.MemberPointers");
const namespace = strings.add("Fixture");
const typeNames = ["<Module>", "A", "B"].map((name) => strings.add(name));
const fieldNames = ["firstField", "secondField"].map((name) =>
strings.add(name),
);
const constructorName = strings.add(".ctor");
const attributeName = strings.add("Attribute");
const systemNamespace = strings.add("System");
const runtimeName = strings.add("System.Runtime");
const fieldSignature = blobs.add(Buffer.from([0x06, 0x08]));
const constructorSignature = blobs.add(
Buffer.from([0x20, 0x01, 0x01, 0x0e]),
);
const baseConstructorSignature = blobs.add(Buffer.from([0x20, 0, 0x01]));
const attributeValue = blobs.add(fixedStringAttributeBlob("fixture"));
const rows = new Map<number, readonly Buffer[]>([
[0, [moduleRow(moduleName, 2, 2)]],
// ResolutionScope selects AssemblyRef 1, which contains System.Attribute.
[
1,
[
Buffer.concat([
u16((1 << 2) | 2),
u16(attributeName),
u16(systemNamespace),
]),
],
],
[
2,
typeNames.map((name, index) =>
typeDefRow({
name,
namespace: index === 0 ? 0 : namespace,
fieldList: index === 2 ? 2 : 1,
methodList: index === 2 ? 2 : 1,
stringSize: 2,
typeDefOrRefSize: 2,
fieldListSize: 2,
methodListSize: 2,
extendsRaw: index === 0 ? 0 : (1 << 2) | 1,
flags: index === 0 ? 0 : 0x0010_0001,
}),
),
],
[4, fieldNames.map((name) => fieldRow(name, fieldSignature, 2, 2))],
[
6,
[1, 2].map(() =>
methodDefRow({
name: constructorName,
signature: constructorSignature,
stringSize: 2,
blobSize: 2,
parameterSize: 2,
parameterList: 1,
rva: methodRva,
flags: 0x1886,
}),
),
],
[
10,
[
memberRefRow({
name: constructorName,
signature: baseConstructorSignature,
stringSize: 2,
blobSize: 2,
parentSize: 2,
}),
],
],
// The assembly attribute constructor is physical MethodDef row 1.
[
12,
[
customAttributeRow(
attributeValue,
{ customAttributeTypeRaw: (1 << 3) | 2 },
2,
2,
2,
),
],
],
[32, [assemblyRow(assemblyName, 2, 2)]],
[35, [assemblyRefRow(runtimeName, 0, false, 2, 2)]],
]);
if (fieldPointers !== null) rows.set(3, fieldPointers.map(u16));
if (methodPointers !== null) rows.set(5, methodPointers.map(u16));
return metadataRoot(
tablesStream(rows, 0n, 0),
strings.toBuffer(),
guidHeap(),
blobs.toBuffer(),
options.streamName ?? "#-",
);
});
// The shared PE shell defaults to an executable; this fixture is a DLL.
image.writeUInt16LE(image.readUInt16LE(0x84 + 18) | 0x2000, 0x84 + 18);
image.writeUInt32LE(0, 0x0200 + 20);
return image;
};
/** Build a syntactically valid PE32 fixture with no CLI directory. */
export const buildNativePeFixture = (): Buffer => {
const image = buildManagedPeFixture();
@@ -0,0 +1,94 @@
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { expect } from "vitest";
import { parseEvidence } from "../../src/domain/evidence.js";
import { buildManagedMemberPointerFixture } from "../../src/dotnet/ManagedPe.fixture.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest(
"CLI and stdio MCP retain physical member identities through reordered pointers",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-managed-member-pointers-");
const path = join(root, "reordered.dll");
const bytes = buildManagedMemberPointerFixture();
const sha256 = createHash("sha256").update(bytes).digest("hex");
await writeFile(path, bytes);
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent" },
stderr: "pipe",
});
transport.stderr?.on("data", () => undefined);
const client = new Client({ name: "managed-pointer-parity", version: "1" });
try {
await client.connect(transport);
for (const name of [
"inspect_managed_artifact",
"inspect_managed_members",
] as const) {
const cliResult = await cli.run({
arguments: [name.replaceAll("_", "-"), path, "--json"],
});
expect(cliResult.exitCode, cliResult.stderr).toBe(0);
const cliEvidence = parseEvidence(cliResult.json);
const response = await client.callTool({ name, arguments: { path } });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const mcpEvidence = parseEvidence(response.structuredContent);
expect(mcpEvidence.normalized_result).toEqual(
cliEvidence.normalized_result,
);
expect(mcpEvidence.subject?.digest.sha256).toBe(sha256);
expect(cliEvidence.subject?.digest.sha256).toBe(sha256);
expect(mcpEvidence.normalized_result).toMatchObject({
coverage: { state: "complete", issues: [] },
});
if (name === "inspect_managed_artifact") {
expect(mcpEvidence.normalized_result).toMatchObject({
attributes: [
expect.objectContaining({
constructor_token: "0x06000001",
type_name: "Fixture.B",
}),
],
});
} else {
expect(mcpEvidence.normalized_result).toMatchObject({
fields: [
expect.objectContaining({
token: "0x04000001",
declaring_type: "Fixture.B",
}),
expect.objectContaining({
token: "0x04000002",
declaring_type: "Fixture.A",
}),
],
methods: [
expect.objectContaining({
token: "0x06000001",
declaring_type: "Fixture.B",
}),
expect.objectContaining({
token: "0x06000002",
declaring_type: "Fixture.A",
}),
],
});
}
}
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
},
);