Files
reverse-engineering-anything/src/dotnet/ManagedMetadataExtraData.test.ts
T
Kaoru0822andClaude Opus 5.5 2a9252e4e6 fix(managed): report metadata range failures at artifact file offsets (#3508)
ManagedMetadataLayout's range check reported offsets relative to the buffer
it was given: the metadata root for version and stream headers, and the
table stream for the tables header, row counts and table data. Other
diagnostics in the same reader already use artifact file offsets, so a
truncated table stream at file offset 888 was reported at offset 24, inside
the DOS header.

Pass each buffer's artifact file offset to the range check so every
invalid-metadata-root issue carries a file offset.

Fixes #3364

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:06:06 -07:00

230 lines
8.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { readManagedPeLayout } from "./ManagedPeReader.js";
import {
buildManagedPeFixture,
managedPeFixtureTarget,
} from "./ManagedPe.fixture.js";
const fixtureTablesStream = (bytes: Buffer) => {
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("Fixture omitted CLI metadata");
const root = pe.rvaToOffset(
pe.cli.metadata.rva,
pe.cli.metadata.size,
"cli.metadata",
);
const versionLength = bytes.readUInt32LE(root + 12);
const streamHeader = ((root + 16 + versionLength + 3) & ~3) + 4;
expect(
bytes.toString(
"ascii",
streamHeader + 8,
bytes.indexOf(0, streamHeader + 8),
),
).toBe("#~");
const tablesOffset = root + bytes.readUInt32LE(streamHeader);
const valid = bytes.readBigUInt64LE(tablesOffset + 8);
let count = 0;
for (let table = 0; table < 64; table += 1)
if ((valid & (1n << BigInt(table))) !== 0n) count += 1;
return { streamHeader, tablesOffset, extraDataOffset: 24 + count * 4 };
};
describe("CLR metadata tables ExtraData extension", () => {
it.each([
{ label: "absent extension", metadataExtraData: undefined },
{ label: "zero word", metadataExtraData: 0 },
{ label: "nonzero word", metadataExtraData: 0x1234_5678 },
{ label: "maximum unsigned word", metadataExtraData: 0xffff_ffff },
])(
"preserves metadata and member identities with $label",
({ metadataExtraData }) => {
const plain = buildManagedPeFixture();
const plainTarget = managedPeFixtureTarget(plain);
const baseline = inspectManagedArtifactBytes(plain, plainTarget);
const baselineMembers = inspectManagedMembersBytes(plain, plainTarget);
const bytes = buildManagedPeFixture(
metadataExtraData === undefined ? {} : { metadataExtraData },
);
const target = managedPeFixtureTarget(bytes);
const artifact = inspectManagedArtifactBytes(bytes, target);
const members = inspectManagedMembersBytes(bytes, target);
const shift = metadataExtraData === undefined ? 0 : 4;
expect(artifact.coverage).toEqual({ state: "complete", issues: [] });
expect(members.coverage).toEqual({ state: "complete", issues: [] });
if (baseline.module === null || baseline.assembly === null)
throw new Error("Fixture omitted module or assembly");
expect(artifact.module).toEqual({
...baseline.module,
row_offset: baseline.module.row_offset + shift,
});
expect(artifact.assembly).toEqual({
...baseline.assembly,
row_offset: baseline.assembly.row_offset + shift,
});
expect(artifact.metadata.table_row_counts).toEqual(
baseline.metadata.table_row_counts,
);
expect(artifact.target_frameworks).toEqual(baseline.target_frameworks);
expect(members.types).toEqual(
baselineMembers.types.map((type) => ({
...type,
row_offset: type.row_offset + shift,
})),
);
expect(members.fields).toEqual(
baselineMembers.fields.map((field) => ({
...field,
row_offset: field.row_offset + shift,
})),
);
expect(members.methods).toEqual(
baselineMembers.methods.map((method) => ({
...method,
row_offset: method.row_offset + shift,
})),
);
expect(members.call_edges).toEqual(baselineMembers.call_edges);
expect(members.field_accesses).toEqual(baselineMembers.field_accesses);
},
);
});
describe("CLR ExtraData index widths and boundaries", () => {
it("combines ExtraData with actual four-byte string heap indexes", () => {
const fieldName = "f".repeat(65_536);
const plain = buildManagedPeFixture({ fieldName });
const baseline = inspectManagedMembersBytes(
plain,
managedPeFixtureTarget(plain),
);
const bytes = buildManagedPeFixture({
fieldName,
metadataExtraData: 0x1234_5678,
});
const { tablesOffset } = fixtureTablesStream(bytes);
expect(bytes[tablesOffset + 6]).toBe(0x41);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(baseline.coverage).toEqual({ state: "complete", issues: [] });
expect(result.coverage).toEqual({ state: "complete", issues: [] });
expect(result.module?.name).toBe("Fixture.dll");
expect(result.module?.mvid).toBe(baseline.module?.mvid);
expect(result.types.map((type) => type.full_name)).toEqual([
"Fixture.Program",
]);
expect(result.fields.map(({ token, name }) => ({ token, name }))).toEqual([
{ token: "0x04000001", name: fieldName },
]);
// Main is placed after the large field name, so its index exceeds 0xffff.
expect(result.methods.map(({ token, name }) => ({ token, name }))).toEqual([
{ token: "0x06000001", name: "Main" },
]);
});
it("honors ExtraData in an unoptimized table stream without pointer tables", () => {
const bytes = buildManagedPeFixture({ metadataExtraData: 0x1234_5678 });
const baselineTarget = managedPeFixtureTarget(bytes);
const baseline = inspectManagedArtifactBytes(bytes, baselineTarget);
const baselineMembers = inspectManagedMembersBytes(bytes, baselineTarget);
const { streamHeader } = fixtureTablesStream(bytes);
bytes.write("#-", streamHeader + 8, "ascii");
const target = managedPeFixtureTarget(bytes);
const result = inspectManagedArtifactBytes(bytes, target);
const members = inspectManagedMembersBytes(bytes, target);
expect(result.coverage).toEqual({ state: "complete", issues: [] });
expect(members.coverage).toEqual({ state: "complete", issues: [] });
expect(result.metadata.stream_names).toContain("#-");
expect(result.metadata.stream_names).not.toContain("#~");
expect(result.module).toEqual(baseline.module);
expect(result.assembly).toEqual(baseline.assembly);
expect(members.types).toEqual(baselineMembers.types);
expect(members.fields).toEqual(baselineMembers.fields);
expect(members.methods).toEqual(baselineMembers.methods);
});
it("reports a truncated row count before interpreting ExtraData", () => {
const bytes = buildManagedPeFixture({ metadataExtraData: 0x1234_5678 });
const { streamHeader, tablesOffset, extraDataOffset } =
fixtureTablesStream(bytes);
bytes.writeUInt32LE(extraDataOffset - 1, streamHeader + 4);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage).toMatchObject({
state: "unavailable",
issues: [
expect.objectContaining({
code: "invalid-metadata-root",
scope: "metadata.table-row-count",
// The last row count's artifact file offset, not its stream offset.
offset: tablesOffset + extraDataOffset - 4,
}),
],
});
expect(result.types).toEqual([]);
expect(result.methods).toEqual([]);
});
it("reports a stream extent beyond the metadata root at its file offset", () => {
const bytes = buildManagedPeFixture();
const { streamHeader, tablesOffset } = fixtureTablesStream(bytes);
bytes.writeUInt32LE(0xffff_ffff, streamHeader + 4);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage.issues).toEqual([
expect.objectContaining({
code: "invalid-metadata-root",
scope: "metadata.#~",
offset: tablesOffset,
}),
]);
});
it.each([0, 1, 3])(
"rejects an ExtraData word with only %i declared bytes",
(availableBytes) => {
const bytes = buildManagedPeFixture({ metadataExtraData: 0x1234_5678 });
const { streamHeader, tablesOffset, extraDataOffset } =
fixtureTablesStream(bytes);
expect(bytes[tablesOffset + 6]).toBe(0x40);
expect(bytes.readUInt32LE(tablesOffset + extraDataOffset)).toBe(
0x1234_5678,
);
// Only the declared tables-stream extent is shortened; the PE and heaps stay intact.
bytes.writeUInt32LE(extraDataOffset + availableBytes, streamHeader + 4);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage).toMatchObject({
state: "unavailable",
issues: [
expect.objectContaining({
code: "invalid-metadata-root",
scope: "metadata.tables-extra-data",
offset: tablesOffset + extraDataOffset,
}),
],
});
expect(result.module).toBeNull();
expect(result.types).toEqual([]);
expect(result.methods).toEqual([]);
},
);
});