mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(managed): resolve member ownership through pointer tables (#1886)
Co-authored-by: LouisChen <[email protected]>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user