fix(dotnet): validate managed metadata and CIL boundaries (#938)

* perf(dotnet): reuse decoded metadata rows

* test(dotnet): preserve expanded fixture metadata

* style(dotnet): format metadata inventory

* fix(dotnet): bound method bodies to PE section data

* fix(dotnet): derive fixture metadata index widths

* fix(dotnet): validate managed metadata bounds

* test(dotnet): cover large metadata ReadyToRun layout

* fix(dotnet): validate exception instruction boundaries

* fix(dotnet): report invalid metadata coded indexes

* fix(dotnet): clarify MemberForwarded null diagnostics

* fix(dotnet): honor complete CIL and EH boundaries

* fix(dotnet): validate metadata identity rows and offsets
This commit is contained in:
morluto 2026-10-08 00:17:26 +08:00 committed by GitHub
parent 35a4e18d30
commit bda4ada7fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1825 additions and 223 deletions

View File

@ -1,6 +1,8 @@
import { describe, expect, it } from "vitest";
import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
import {
buildManagedPeFixture,
buildNativePeFixture,
@ -43,4 +45,37 @@ describe("managed artifact failure classification", () => {
expect.objectContaining({ code: "invalid-tables" }),
]);
});
it("keeps malformed reference and attribute issues across inventory callers", () => {
const bytes = buildManagedPeFixture({
references: [
"System.Runtime",
"System.Runtime",
"UnityEngine.CoreModule",
],
malformedAssemblyReferenceRows: [3],
malformedCustomAttributeRows: [1],
});
const target = managedPeFixtureTarget(bytes);
const artifact = inspectManagedArtifactBytes(bytes, target);
const members = inspectManagedMembersBytes(bytes, target);
const boundaries = inspectManagedNativeBoundariesBytes(bytes, target);
const issueCodes = ["invalid-heap-index", "invalid-row"];
expect(artifact.references.map(({ name }) => name)).toEqual([
"System.Runtime",
"System.Runtime",
]);
expect(artifact.coverage.issues.map(({ code }) => code)).toEqual(
issueCodes,
);
expect(members.coverage.issues.map(({ code }) => code)).toEqual(issueCodes);
expect(boundaries.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({ code: "invalid-heap-index" }),
expect.objectContaining({ code: "invalid-row" }),
],
});
});
});

View File

@ -9,8 +9,66 @@ import {
buildManagedPeFixture,
managedPeFixtureTarget,
} from "./ManagedPe.fixture.js";
import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import { readManagedPeLayout } from "./ManagedPeReader.js";
const metadataLayoutFor = (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 artifact inventory", () => {
it.each([
{ moduleRowCount: 0, assemblyRowCount: 1, scope: "metadata.Module" },
{ moduleRowCount: 2, assemblyRowCount: 1, scope: "metadata.Module" },
{ moduleRowCount: 1, assemblyRowCount: 2, scope: "metadata.Assembly" },
])("marks invalid identity table cardinality partial: %j", (options) => {
const bytes = buildManagedPeFixture(options);
const target = managedPeFixtureTarget(bytes);
const results = [
inspectManagedArtifactBytes(bytes, target),
inspectManagedMembersBytes(bytes, target),
inspectManagedNativeBoundariesBytes(bytes, target),
];
for (const result of results) {
expect(result.metadata.status).toBe("partial");
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-row",
scope: options.scope,
}),
],
});
}
if (options.moduleRowCount === 0) expect(results[0]?.module).toBeNull();
else expect(results[0]?.module?.name).toBe("Fixture.dll");
});
it.each([
{ moduleRowCount: 1, assemblyRowCount: 0, targetFramework: null },
{ moduleRowCount: 1, assemblyRowCount: 1 },
])("keeps valid identity table cardinalities complete: %j", (options) => {
const bytes = buildManagedPeFixture(options);
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.metadata.status).toBe("complete");
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
expect(result.assembly?.name ?? null).toBe(
options.assemblyRowCount === 0 ? null : "Fixture.Managed",
);
});
it("statically inspects PE files with more than 96 sections", () => {
const fixture = buildManagedPeFixture();
const peOffset = fixture.readUInt32LE(0x3c);
@ -77,7 +135,133 @@ describe("managed artifact inventory", () => {
]);
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
});
describe("managed artifact coded indexes", () => {
it("reports an invalid ManifestResource implementation while retaining the resource", () => {
const bytes = buildManagedPeFixture({ resourceImplementationRaw: 9 });
const target = managedPeFixtureTarget(bytes);
const result = inspectManagedArtifactBytes(bytes, target);
const boundaries = inspectManagedNativeBoundariesBytes(bytes, target);
expect(result.resources).toMatchObject([
{
name: "Fixture.resources",
implementation_token: null,
embedded: false,
},
]);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-row",
scope: "metadata.ManifestResource:0x28000001",
detail: expect.stringContaining(
"coded index 0x9 is invalid: coded index selects row 2 in table 35, which has 1 rows",
),
}),
],
});
const layout = metadataLayoutFor(bytes);
expect(result.coverage.issues[0]?.offset).toBe(
(result.resources[0]?.row_offset ?? 0) + 8 + layout.stringIndexSize,
);
expect(boundaries.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
scope: "metadata.ManifestResource:0x28000001",
}),
],
});
});
it("reports an invalid CustomAttribute constructor coded index", () => {
const bytes = buildManagedPeFixture({ customAttributeTypeRaw: 19 });
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.attributes).toEqual([]);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-row",
scope: "metadata.CustomAttribute:0x0c000001",
detail: expect.stringContaining(
"coded index 0x13 is invalid: coded index selects row 2 in table 10, which has 1 rows",
),
}),
],
});
});
it("reports null for the required CustomAttribute constructor coded index", () => {
const bytes = buildManagedPeFixture({ customAttributeTypeRaw: 0 });
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.attributes).toEqual([]);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-row",
detail:
"CustomAttribute constructor coded index 0x0 is null, but Type must reference a MethodDef or MemberRef row",
}),
],
});
});
it("reports coded-index column offsets after widened string and coded-index columns", () => {
const references = Array.from(
{ length: 2_500 },
(_, index) => `Reference.${"r".repeat(24)}.${String(index)}`,
);
const bytes = buildManagedPeFixture({
references,
extendsRaw: 9,
customAttributeTypeRaw: 19,
});
const layout = metadataLayoutFor(bytes);
const artifact = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
const members = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
const extendsIssue = members.coverage.issues.find((issue) =>
issue.scope.startsWith("metadata.TypeDef:"),
);
const attributeIssue = artifact.coverage.issues.find((issue) =>
issue.scope.startsWith("metadata.CustomAttribute:"),
);
const typeDef = members.types[0];
const attributeTable = layout.table(12);
expect(layout.stringIndexSize).toBe(4);
expect(layout.codedIndexSize("HasCustomAttribute")).toBe(4);
expect(typeDef).toBeDefined();
expect(extendsIssue?.offset).toBe(
(typeDef?.row_offset ?? 0) + 4 + 2 * layout.stringIndexSize,
);
expect(attributeTable).toBeDefined();
expect(attributeIssue?.offset).toBe(
(attributeTable?.offset ?? 0) +
layout.codedIndexSize("HasCustomAttribute"),
);
});
});
describe("managed artifact reference inventory", () => {
it("returns all references inline while classifying from the full inventory", () => {
const bytes = buildManagedPeFixture({
references: ["System.Runtime", "UnityEngine.CoreModule"],
@ -93,6 +277,60 @@ describe("managed artifact inventory", () => {
expect(result.coverage.issues).toEqual([]);
});
it("preserves duplicate AssemblyRef rows in metadata order", () => {
const bytes = buildManagedPeFixture({
references: [
"UnityEngine.CoreModule",
"UnityEngine.CoreModule",
"System.Runtime",
],
});
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.references.map(({ name }) => name)).toEqual([
"UnityEngine.CoreModule",
"UnityEngine.CoreModule",
"System.Runtime",
]);
expect(result.classification.runtime_family).toBe("unity-mono");
});
});
describe("managed fixture data layout", () => {
it("keeps ReadyToRun metadata and resources intact with large metadata", () => {
const resource = Buffer.alloc(2_048, 0x52);
const fieldSignature = Buffer.concat([
Buffer.from([0x06]),
Buffer.alloc(8_000, 0x0f),
Buffer.from([0x08]),
]);
const bytes = buildManagedPeFixture({
fieldSignature,
readyToRun: true,
resourceData: resource,
});
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.classification).toMatchObject({
status: "managed",
implementation: "cil-and-ready-to-run",
});
expect(result.resources[0]).toMatchObject({
embedded: true,
data_length: resource.length,
data_sha256: createHash("sha256").update(resource).digest("hex"),
});
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
});
describe("managed artifact metadata identity", () => {
it("accepts CLI metadata GUIDs without RFC UUID version or variant bits", () => {
const bytes = buildManagedPeFixture({
mvid: Buffer.from("3aebc60edc4a544b1f458b4ed40b33b1", "hex"),
@ -140,6 +378,55 @@ describe("managed artifact inventory", () => {
});
});
it("derives the string heap width after expanded AssemblyRef names", () => {
const references = Array.from(
{ length: 5_000 },
(_, index) => `Reference.${String(index).padStart(4, "0")}`,
);
const bytes = buildManagedPeFixture({ references });
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.references).toHaveLength(references.length);
expect(result.references.at(-1)?.name).toBe("Reference.4999");
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
const malformedBytes = buildManagedPeFixture({
references,
malformedAssemblyReferenceRows: [5_000],
});
const malformedResult = inspectManagedArtifactBytes(
malformedBytes,
managedPeFixtureTarget(malformedBytes),
);
expect(malformedResult.references).toHaveLength(4_999);
expect(malformedResult.coverage).toMatchObject({
state: "partial",
issues: [expect.objectContaining({ code: "invalid-heap-index" })],
});
});
it("derives ResolutionScope and Implementation widths at 16,384 AssemblyRefs", () => {
const references = Array.from(
{ length: 16_384 },
(_, index) => `Reference.${String(index).padStart(5, "0")}`,
);
const bytes = buildManagedPeFixture({ references });
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.references).toHaveLength(references.length);
expect(result.references.at(-1)?.name).toBe("Reference.16383");
expect(result.resources).toMatchObject([
{ name: "Fixture.resources", embedded: true, data_length: 13 },
]);
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
it("preserves U+FEFF in references and fixed attribute strings", () => {
const bytes = buildManagedPeFixture({
references: ["\uFEFFSystem.Runtime", "System.\uFEFFRuntime"],

View File

@ -25,6 +25,48 @@ const managedBodyWithSections = (...sections: Buffer[]) => {
.methods[0]?.body;
};
const managedBodyWithRegion = ({
il,
flags = 0,
tryOffset = 0,
tryLength = 1,
handlerOffset,
handlerLength = 1,
extra = 0x0200_0001,
}: {
readonly il: Buffer;
readonly flags?: number;
readonly tryOffset?: number;
readonly tryLength?: number;
readonly handlerOffset: number;
readonly handlerLength?: number;
readonly extra?: number;
}) => {
const header = Buffer.alloc(12);
header.writeUInt16LE(0x300b, 0);
header.writeUInt16LE(8, 2);
header.writeUInt32LE(il.length, 4);
const section = Buffer.alloc(16);
section[0] = 0x01;
section[1] = section.length;
section.writeUInt16LE(flags, 4);
section.writeUInt16LE(tryOffset, 6);
section[8] = tryLength;
section.writeUInt16LE(handlerOffset, 9);
section[11] = handlerLength;
section.writeUInt32LE(extra, 12);
const bytes = buildManagedPeFixture({
ilBody: Buffer.concat([
header,
il,
Buffer.alloc((4 - (il.length % 4)) % 4),
section,
]),
});
return inspectManagedMembersBytes(bytes, managedPeFixtureTarget(bytes))
.methods[0]?.body;
};
describe("managed CIL decoding", () => {
it("reads fat method header size from the full flags-and-size word", () => {
const il = Buffer.from([
@ -79,6 +121,60 @@ describe("managed CIL decoding", () => {
});
});
it("does not decode overlay bytes after the method's file-backed PE section", () => {
const original = buildManagedPeFixture();
const bytes = Buffer.concat([original, Buffer.from([0x2a])]);
const parsed = inspectManagedMembersBytes(
original,
managedPeFixtureTarget(original),
);
const methodRow = parsed.methods[0]?.row_offset;
expect(methodRow).toBeDefined();
if (methodRow === undefined) return;
bytes.writeUInt32LE(0x2dff, methodRow);
bytes[0xfff] = 0x06;
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.methods[0]?.body).toMatchObject({
status: "malformed",
header_format: "unknown",
issue: "Method body leaves file-backed PE section data",
anchors: [],
});
});
it("does not decode overlay exception sections after the file-backed PE section", () => {
const original = buildManagedPeFixture();
const overlay = Buffer.from([0x01, 0x04, 0x00, 0x00]);
const bytes = Buffer.concat([original, overlay]);
const parsed = inspectManagedMembersBytes(
original,
managedPeFixtureTarget(original),
);
const methodRow = parsed.methods[0]?.row_offset;
expect(methodRow).toBeDefined();
if (methodRow === undefined) return;
const body = Buffer.alloc(16);
body.writeUInt16LE(0x301b, 0);
body.writeUInt16LE(8, 2);
body.writeUInt32LE(1, 4);
body[12] = 0x2a;
body.copy(bytes, 0xff0);
bytes.writeUInt32LE(0x2df0, methodRow);
overlay.copy(bytes, 0x1000);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.methods[0]?.body).toMatchObject({
status: "malformed",
issue: "Exception section header leaves artifact",
exception_regions: [],
});
});
it("keeps the documented decoded-CIL v1 golden vector stable", () => {
const il = Buffer.from([0x00, 0x2a]);
const tinyBytes = buildManagedPeFixture({
@ -125,6 +221,218 @@ describe("managed CIL decoding", () => {
});
});
describe("managed exception region bounds", () => {
it("marks out-of-method ranges malformed and preserves the raw EH clause", () => {
expect(
managedBodyWithRegion({
il: Buffer.from([0x2a]),
tryOffset: 0,
tryLength: 2,
handlerOffset: 0,
}),
).toMatchObject({
status: "malformed",
il_size: 1,
exception_regions: [
{
flags: 0,
try_offset: 0,
try_length: 2,
handler_offset: 0,
handler_length: 1,
class_token: "0x02000001",
},
],
issue: "Exception clause try range leaves the method IL body",
});
});
it("uses half-open method bounds and permits zero-length ranges", () => {
expect(
managedBodyWithRegion({
il: Buffer.from([0x00, 0x00, 0x2a]),
tryOffset: 0,
tryLength: 1,
handlerOffset: 1,
handlerLength: 2,
}),
).toMatchObject({ status: "present", il_size: 3, issue: null });
expect(
managedBodyWithRegion({
il: Buffer.from([0x2a]),
tryOffset: 0,
tryLength: 0,
handlerOffset: 0,
handlerLength: 0,
}),
).toMatchObject({ status: "present", il_size: 1, issue: null });
});
it.each([
["try start", { tryOffset: 1, tryLength: 4, handlerOffset: 5 }],
["try end", { tryOffset: 0, tryLength: 1, handlerOffset: 5 }],
["handler start", { tryLength: 5, handlerOffset: 1, handlerLength: 5 }],
["handler end", { tryLength: 5, handlerOffset: 0, handlerLength: 1 }],
] as const)(
"preserves clauses with a mid-instruction %s",
(_label, region) => {
const body = managedBodyWithRegion({
il: Buffer.from([0x20, 0, 0, 0, 0, 0x2a]),
...region,
});
expect(body).toMatchObject({
status: "malformed",
exception_regions: [expect.any(Object)],
issue:
"Exception clause " +
(_label.startsWith("try") ? "try" : "handler") +
" range does not align with CIL instruction boundaries",
});
},
);
it("rejects a filter start inside an instruction and retains the clause", () => {
expect(
managedBodyWithRegion({
il: Buffer.from([0x20, 0, 0, 0, 0, 0x2a]),
flags: 1,
tryLength: 5,
handlerOffset: 5,
extra: 1,
}),
).toMatchObject({
status: "malformed",
exception_regions: [expect.objectContaining({ filter_offset: 1 })],
issue:
"Exception clause filter range does not align with CIL instruction boundaries",
});
});
});
describe("managed exception region prefix boundaries", () => {
it("treats a CIL prefix chain and its opcode as one boundary", () => {
expect(
managedBodyWithRegion({
il: Buffer.from([0xfe, 0x12, 1, 0xfe, 0x13, 0x4a, 0x2a]),
tryOffset: 0,
tryLength: 6,
handlerOffset: 6,
}),
).toMatchObject({ status: "present", issue: null, il_size: 7 });
});
it("keeps constrained callvirt as a single region boundary", () => {
const il = Buffer.from([0xfe, 0x16, 1, 0, 0, 1, 0x6f, 1, 0, 0, 6, 0x2a]);
expect(
managedBodyWithRegion({
il,
tryOffset: 0,
tryLength: 11,
handlerOffset: 11,
}),
).toMatchObject({ status: "present", issue: null, il_size: 12 });
expect(
managedBodyWithRegion({
il,
tryOffset: 6,
tryLength: 1,
handlerOffset: 11,
}),
).toMatchObject({
status: "malformed",
issue:
"Exception clause try range does not align with CIL instruction boundaries",
});
});
it.each([
["start", { tryOffset: 5, tryLength: 1, handlerOffset: 6 }],
[
"end after first prefix",
{ tryOffset: 0, tryLength: 3, handlerOffset: 6 },
],
[
"end after second prefix",
{ tryOffset: 0, tryLength: 5, handlerOffset: 6 },
],
] as const)(
"rejects a region %s between a prefix and its opcode",
(_edge, region) => {
expect(
managedBodyWithRegion({
il: Buffer.from([0xfe, 0x12, 1, 0xfe, 0x13, 0x4a, 0x2a]),
...region,
}),
).toMatchObject({
status: "malformed",
exception_regions: [expect.any(Object)],
issue:
"Exception clause try range does not align with CIL instruction boundaries",
});
},
);
});
describe("managed exception region flags and filters", () => {
it("marks reserved clause flags malformed without dropping its bytes", () => {
expect(
managedBodyWithRegion({
il: Buffer.from([0x00, 0x2a]),
flags: 8,
tryLength: 1,
handlerOffset: 1,
}),
).toMatchObject({
status: "malformed",
exception_regions: [expect.objectContaining({ flags: 8 })],
issue: "Exception clause has unsupported flags 0x8",
});
});
it.each([2, 4])(
"accepts the defined finally/fault clause flag %s",
(flags) => {
expect(
managedBodyWithRegion({
il: Buffer.from([0x00, 0x00, 0x2a]),
flags,
tryOffset: 0,
tryLength: 1,
handlerOffset: 1,
handlerLength: 2,
}),
).toMatchObject({ status: "present", issue: null });
},
);
it("validates filter ordering", () => {
expect(
managedBodyWithRegion({
il: Buffer.from([0x00, 0x00, 0xfe, 0x11, 0x2a]),
flags: 1,
tryOffset: 0,
tryLength: 1,
handlerOffset: 4,
extra: 1,
}),
).toMatchObject({ status: "present", il_size: 5, issue: null });
expect(
managedBodyWithRegion({
il: Buffer.from([0x00, 0x00, 0x2a]),
flags: 1,
tryOffset: 0,
tryLength: 1,
handlerOffset: 1,
extra: 1,
}),
).toMatchObject({
status: "malformed",
issue:
"Exception clause filter must start before its handler within the method IL body",
});
});
});
describe("managed exception section decoding", () => {
it("marks an exception section with a partial clause as malformed", () => {
const section = Buffer.alloc(17);
@ -155,18 +463,34 @@ describe("managed exception section decoding", () => {
});
});
it("reports malformed data in a chained section", () => {
it("preserves decoded clauses when a chained section has an unsupported kind", () => {
const first = Buffer.alloc(16);
first[0] = 0x81;
first[1] = first.length;
const second = Buffer.from([0x02, 0x04, 0x00, 0x00]);
expect(managedBodyWithSections(first, second)).toMatchObject({
status: "malformed",
exception_regions: [],
exception_regions: [expect.objectContaining({ flags: 0 })],
issue: "Unsupported method data section kind 2",
});
});
it("preserves decoded clauses when a later section has a partial clause", () => {
const first = Buffer.alloc(16);
first[0] = 0x81;
first[1] = first.length;
first.writeUInt16LE(2, 4);
const later = Buffer.alloc(17);
later[0] = 0x01;
later[1] = later.length;
expect(managedBodyWithSections(first, later)).toMatchObject({
status: "malformed",
exception_regions: [expect.objectContaining({ flags: 2 })],
issue: "Exception section size does not contain whole clauses",
});
});
it.each([
{
name: "unsupported section kind",

View File

@ -0,0 +1,86 @@
import type { ManagedExceptionRegion } from "./ManagedMemberInspectorCore.js";
import type { ExceptionRegionParseResult } from "./ManagedMemberInstructionDecoder.js";
const clauseFlagsIssue = (region: ManagedExceptionRegion): string | null =>
[0, 1, 2, 4].includes(region.flags)
? null
: `Exception clause has unsupported flags 0x${region.flags.toString(16)}`;
const methodExtentIssue = (
start: number,
length: number,
ilSize: number,
name: "try" | "handler",
): string | null =>
start <= ilSize && length <= ilSize - start
? null
: `Exception clause ${name} range leaves the method IL body`;
const instructionBoundaryIssue = (
start: number,
length: number,
boundaries: ReadonlySet<number>,
name: "try" | "handler" | "filter",
): string | null => {
const end = start + length;
return boundaries.has(start) && boundaries.has(end)
? null
: `Exception clause ${name} range does not align with CIL instruction boundaries`;
};
const filterOrderingIssue = (
region: ManagedExceptionRegion,
ilSize: number,
): string | null => {
if (region.flags !== 1) return null;
const offset = region.filter_offset;
return offset !== null && offset < ilSize && offset < region.handler_offset
? null
: "Exception clause filter must start before its handler within the method IL body";
};
const clauseIssue = (
region: ManagedExceptionRegion,
ilSize: number,
boundaries: ReadonlySet<number>,
): string | null =>
clauseFlagsIssue(region) ??
methodExtentIssue(region.try_offset, region.try_length, ilSize, "try") ??
instructionBoundaryIssue(
region.try_offset,
region.try_length,
boundaries,
"try",
) ??
methodExtentIssue(
region.handler_offset,
region.handler_length,
ilSize,
"handler",
) ??
instructionBoundaryIssue(
region.handler_offset,
region.handler_length,
boundaries,
"handler",
) ??
(region.flags === 1 && region.filter_offset !== null
? instructionBoundaryIssue(region.filter_offset, 0, boundaries, "filter")
: null) ??
filterOrderingIssue(region, ilSize);
/** Validate EH extents and instruction boundaries while retaining each clause. */
export const validateExceptionRegionRanges = (
parsed: ExceptionRegionParseResult,
ilSize: number,
instructionOffsets: readonly number[],
): ExceptionRegionParseResult => {
if (parsed.status === "malformed") return parsed;
const boundaries = new Set([...instructionOffsets, ilSize]);
for (const region of parsed.regions) {
const issue = clauseIssue(region, ilSize, boundaries);
if (issue !== null)
return { status: "malformed", regions: parsed.regions, issue };
}
return parsed;
};

View File

@ -111,6 +111,115 @@ describe("managed member inspection", () => {
});
});
describe("managed member reference bounds", () => {
it("retains members and reports an out-of-range TypeDef coded index as partial", () => {
const bytes = buildManagedPeFixture({ extendsRaw: 12 });
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.types[0]).toMatchObject({
token: "0x02000001",
extends_token: null,
});
expect(result.fields).toHaveLength(1);
expect(result.methods).toHaveLength(1);
expect(result.metadata.status).toBe("partial");
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
{
code: "invalid-row",
scope: "metadata.TypeDef:0x02000001",
detail: expect.stringContaining(
"TypeDef Extends coded index 0xc is invalid: coded index selects row 3 in table 2, which has 1 rows",
),
},
],
});
expect(result.coverage.issues[0]?.offset).toBe(
(result.types[0]?.row_offset ?? 0) + 8,
);
});
it("retains a MemberRef with an out-of-range parent as an unknown partial reference", () => {
const bytes = buildManagedPeFixture({ memberRefParentRaw: 40 });
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.member_refs[0]).toMatchObject({
token: "0x0a000001",
parent_token: null,
name: ".ctor",
});
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
{
code: "invalid-row",
scope: "metadata.MemberRef:0x0a000001",
detail: expect.stringContaining(
"MemberRef parent coded index 0x28 is invalid: coded index selects row 5 in table 2, which has 1 rows",
),
},
],
});
expect(result.coverage.issues[0]?.offset).toBe(
result.member_refs[0]?.row_offset,
);
});
it("reports a null required MemberRef parent without dropping the reference", () => {
const bytes = buildManagedPeFixture({ memberRefParentRaw: 0 });
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.member_refs[0]).toMatchObject({
token: "0x0a000001",
parent_token: null,
});
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-row",
detail:
"MemberRef parent coded index 0x0 is null, but the Class column must reference a row",
}),
],
});
});
it("decodes deeply nested supported pointer signatures without recursion", () => {
const depth = 8_000;
const bytes = buildManagedPeFixture({
fieldSignature: Buffer.concat([
Buffer.from([0x06]),
Buffer.alloc(depth, 0x0f),
Buffer.from([0x08]),
]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.fields[0]?.signature).toMatchObject({
kind: "field",
parse_status: "decoded",
field_type: `${"i4"}${"*".repeat(depth)}`,
issue: null,
});
expect(result.methods[0]?.body.status).toBe("present");
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
});
it.each([
Buffer.alloc(0),
Buffer.from([0]),

View File

@ -116,9 +116,11 @@ export const inspectManagedMembersBytes = (
const { layout, inventory } = readMemberInventory(bytes, pe);
issues.push(...inventory.issues);
const ranges = typeRanges(bytes, layout);
const types = parseTypes(bytes, layout, ranges);
const parsedTypes = parseTypes(bytes, layout, ranges);
issues.push(...parsedTypes.issues);
const fields = parseFields(bytes, layout, ranges);
const memberRefs = parseMemberRefs(bytes, layout);
issues.push(...memberRefs.issues);
const methods = parseMethods({
bytes,
layout,
@ -150,7 +152,7 @@ export const inspectManagedMembersBytes = (
requires_artifact_sha256: target.sha256,
requires_mvid: inventory.module?.mvid ?? null,
},
types,
types: parsedTypes.types,
fields: fields.fields,
methods: methods.methods,
member_refs: memberRefs.refs,

View File

@ -2,7 +2,10 @@ import {
managedFailure,
ManagedReaderFailure,
} from "./ManagedReaderFailure.js";
import type { ManagedMemberInspection } from "../domain/managed/managedArtifact.js";
import type {
ManagedMemberInspection,
ManagedParseIssue,
} from "../domain/managed/managedArtifact.js";
import {
type ManagedMetadataLayout,
type MetadataTableLayout,
@ -10,6 +13,7 @@ import {
import {
metadataRowCursor,
metadataCodedToken,
metadataCodedTokenInvalidReason,
metadataToken,
readMetadataString,
sha256Bytes,
@ -190,11 +194,15 @@ export const parseTypes = (
bytes: Buffer,
layout: ManagedMetadataLayout,
ranges: readonly TypeRange[],
): readonly ManagedType[] => {
): {
readonly types: readonly ManagedType[];
readonly issues: readonly ManagedParseIssue[];
} => {
const typeTable = layout.table(2);
const fields = layout.table(4);
const methods = layout.table(6);
const items: ManagedType[] = [];
const issues: ManagedParseIssue[] = [];
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1) {
const cursor = metadataRowCursor(bytes, layout, 2, row);
const flags = cursor.readUInt32();
@ -210,6 +218,7 @@ export const parseTypes = (
cursor.readIndex(layout.stringIndexSize),
layout.strings.size,
);
const extendsOffset = cursor.offset;
const extendsRaw = cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
const fieldStart = cursor.readIndex(layout.tableIndexSize(4));
const methodStart = cursor.readIndex(layout.tableIndexSize(6));
@ -220,6 +229,19 @@ export const parseTypes = (
methodStart,
nextRange?.methodStart ?? 0,
);
const extendsReason = metadataCodedTokenInvalidReason(
extendsRaw,
2,
[2, 1, 27],
layout.rowCounts,
);
if (extendsReason !== null)
issues.push({
code: "invalid-row",
scope: `metadata.TypeDef:${metadataToken(2, row)}`,
offset: extendsOffset,
detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsReason}`,
});
items.push({
token: metadataToken(2, row),
row_offset: cursor.start,
@ -227,7 +249,12 @@ export const parseTypes = (
name,
full_name: fullName(namespace, name),
flags,
extends_token: metadataCodedToken(extendsRaw, 2, [2, 1, 27]),
extends_token: metadataCodedToken(
extendsRaw,
2,
[2, 1, 27],
layout.rowCounts,
),
field_list: {
first_row: fieldRange.first,
last_row: fieldRange.last,
@ -240,7 +267,7 @@ export const parseTypes = (
},
});
}
return items;
return { types: items, issues };
};
const readCompressed = (
@ -301,55 +328,48 @@ const ELEMENT_TYPES = new Map<number, string>([
[0x1c, "object"],
]);
const ELEMENT_TYPE_SUFFIXES = new Map<number, string>([
[0x0f, "*"],
[0x10, "&"],
]);
const readTypeSignature = (
blob: Buffer,
offset: number,
): { readonly value: string; readonly next: number } => {
const kind = blob[offset];
const suffixes: string[] = [];
let cursor = offset;
let kind = blob[cursor];
while (kind === 0x0f || kind === 0x10 || kind === 0x1d) {
suffixes.push(kind === 0x0f ? "*" : kind === 0x10 ? "&" : "[]");
cursor += 1;
kind = blob[cursor];
}
if (kind === undefined)
throw managedFailure(
"invalid-blob",
"signature",
"truncated type signature",
offset,
cursor,
);
const named = ELEMENT_TYPES.get(kind);
if (named !== undefined) return { value: named, next: offset + 1 };
const suffix = ELEMENT_TYPE_SUFFIXES.get(kind);
if (suffix !== undefined) {
const inner = readTypeSignature(blob, offset + 1);
return { value: `${inner.value}${suffix}`, next: inner.next };
let value: string;
let next: number;
if (named !== undefined) {
value = named;
next = cursor + 1;
} else if (kind === 0x11 || kind === 0x12) {
const token = readCompressed(blob, cursor + 1);
value = `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`;
next = token.next;
} else if (kind === 0x1e || kind === 0x13) {
const variable = readCompressed(blob, cursor + 1);
value = `${kind === 0x1e ? "mvar" : "var"}:${String(variable.value)}`;
next = variable.next;
} else {
throw managedFailure(
"unsupported-signature",
"signature",
`unsupported element type 0x${kind.toString(16)}`,
cursor,
);
}
if (kind === 0x11 || kind === 0x12) {
const token = readCompressed(blob, offset + 1);
return {
value: `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`,
next: token.next,
};
}
if (kind === 0x1d) {
const inner = readTypeSignature(blob, offset + 1);
return { value: `${inner.value}[]`, next: inner.next };
}
if (kind === 0x1e) {
const variable = readCompressed(blob, offset + 1);
return { value: `mvar:${String(variable.value)}`, next: variable.next };
}
if (kind === 0x13) {
const variable = readCompressed(blob, offset + 1);
return { value: `var:${String(variable.value)}`, next: variable.next };
}
throw managedFailure(
"unsupported-signature",
"signature",
`unsupported element type 0x${kind.toString(16)}`,
offset,
);
return { value: value + suffixes.reverse().join(""), next };
};
const callingConvention = (value: number): string => {

View File

@ -9,6 +9,14 @@ const opcode = (
): { readonly name: string; readonly operand: string } | undefined =>
OPCODES.get(code);
const PREFIX_OPCODES = new Set([
"unaligned.",
"volatile.",
"tail.",
"constrained.",
"readonly.",
]);
const OPCODES = new Map<
number,
{ readonly name: string; readonly operand: string }
@ -379,12 +387,15 @@ export const decodeInstructions = (
il: Buffer,
): {
readonly parsed: readonly ParsedInstruction[];
readonly instructionStarts: readonly number[];
readonly count: number;
readonly truncated: number;
readonly issue: string | null;
} => {
const parsed: ParsedInstruction[] = [];
const instructionStarts: number[] = [];
let offset = 0;
let prefixStart: number | null = null;
let issue: string | null = null;
try {
while (offset < il.length) {
@ -403,6 +414,12 @@ export const decodeInstructions = (
break;
}
offset = operand.next;
if (PREFIX_OPCODES.has(descriptor.name)) {
prefixStart ??= start;
} else {
instructionStarts.push(prefixStart ?? start);
prefixStart = null;
}
parsed.push({
offset: start,
opcode: descriptor.name,
@ -415,7 +432,7 @@ export const decodeInstructions = (
cause instanceof Error ? cause.message : "Instruction decode failed";
}
const truncated = offset < il.length && issue === null ? 1 : 0;
return { parsed, count: parsed.length, truncated, issue };
return { parsed, instructionStarts, count: parsed.length, truncated, issue };
};
export const parseExceptionRegions = (
@ -435,12 +452,14 @@ export const parseExceptionRegions = (
)
return malformedExceptionRegions(
"Exception section header leaves artifact",
regions,
);
const kind = bytes.readUInt8(sectionOffset);
if ((kind & 0x3f) !== 1)
return malformedExceptionRegions(
`Unsupported method data section kind ${String(kind & 0x3f)}`,
regions,
);
const fat = (kind & 0x40) !== 0;
const size = fat
@ -456,10 +475,12 @@ export const parseExceptionRegions = (
)
return malformedExceptionRegions(
"Exception section size leaves artifact",
regions,
);
if ((size - 4) % clauseSize !== 0)
return malformedExceptionRegions(
"Exception section size does not contain whole clauses",
regions,
);
if (
!fat &&
@ -467,6 +488,7 @@ export const parseExceptionRegions = (
)
return malformedExceptionRegions(
"Small exception section reserved bytes are nonzero",
regions,
);
const count = (size - 4) / clauseSize;
@ -478,24 +500,21 @@ export const parseExceptionRegions = (
return { status: "complete", regions };
};
interface ExceptionRegionParseSuccess {
readonly status: "complete";
readonly regions: ManagedExceptionRegion[];
}
interface ExceptionRegionParseFailure {
readonly status: "malformed";
readonly regions: [];
readonly issue: string;
}
type ExceptionRegionParseResult =
| ExceptionRegionParseSuccess
| ExceptionRegionParseFailure;
export type ExceptionRegionParseResult =
| {
readonly status: "complete";
readonly regions: ManagedExceptionRegion[];
}
| {
readonly status: "malformed";
readonly regions: ManagedExceptionRegion[];
readonly issue: string;
};
const malformedExceptionRegions = (
issue: string,
): ExceptionRegionParseFailure => ({ status: "malformed", regions: [], issue });
regions: ManagedExceptionRegion[] = [],
): ExceptionRegionParseResult => ({ status: "malformed", regions, issue });
const readExceptionClauses = (
bytes: Buffer,

View File

@ -1,4 +1,5 @@
import type { ManagedPeLayout } from "./ManagedPeReader.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
createDeclaringTypeLookup,
@ -17,6 +18,7 @@ import {
metadataToken,
metadataRowCursor,
metadataCodedToken,
metadataCodedTokenInvalidReason,
readMetadataBlob,
readMetadataString,
} from "./ManagedMetadataHeaps.js";
@ -71,9 +73,11 @@ export const parseMemberRefs = (
): {
readonly refs: readonly ManagedMemberRef[];
readonly core: ReadonlyMap<string, MemberRefCore>;
readonly issues: readonly ManagedParseIssue[];
} => {
const refs: ManagedMemberRef[] = [];
const core = new Map<string, MemberRefCore>();
const issues: ManagedParseIssue[] = [];
const table = layout.table(10);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
const cursor = metadataRowCursor(bytes, layout, 10, row);
@ -93,16 +97,37 @@ export const parseMemberRefs = (
layout.blob.size,
);
const token = metadataToken(10, row);
const parentReason = metadataCodedTokenInvalidReason(
parentRaw,
3,
[2, 1, 26, 6, 27],
layout.rowCounts,
);
if (parentReason !== null || parentRaw === 0)
issues.push({
code: "invalid-row",
scope: `metadata.MemberRef:${token}`,
offset: cursor.start,
detail:
parentReason === null
? "MemberRef parent coded index 0x0 is null, but the Class column must reference a row"
: `MemberRef parent coded index 0x${parentRaw.toString(16)} is invalid: ${parentReason}`,
});
refs.push({
token,
row_offset: cursor.start,
parent_token: metadataCodedToken(parentRaw, 3, [2, 1, 26, 6, 27]),
parent_token: metadataCodedToken(
parentRaw,
3,
[2, 1, 26, 6, 27],
layout.rowCounts,
),
name,
signature: signature(sig),
});
core.set(token, { token, name });
}
return { refs, core };
return { refs, core, issues };
};
interface ParseMethodsInput {

View File

@ -253,10 +253,32 @@ export const metadataCodedToken = (
raw: number,
bits: number,
tables: readonly (number | undefined)[],
rowCounts: readonly number[],
): string | null => {
if (raw === 0) return null;
const tag = raw & (2 ** bits - 1);
const row = Math.floor(raw / 2 ** bits);
const table = tables[tag];
return table === undefined || row === 0 ? null : metadataToken(table, row);
return table === undefined || row === 0 || row > (rowCounts[table] ?? 0)
? null
: metadataToken(table, row);
};
/** Explain why a non-null coded index cannot name an admitted metadata row. */
export const metadataCodedTokenInvalidReason = (
raw: number,
bits: number,
tables: readonly (number | undefined)[],
rowCounts: readonly number[],
): string | null => {
if (raw === 0) return null;
const tag = raw & (2 ** bits - 1);
const row = Math.floor(raw / 2 ** bits);
const table = tables[tag];
if (table === undefined) return `coded index tag ${tag} is not defined`;
if (row === 0) return `coded index selects row zero in table ${table}`;
const count = rowCounts[table] ?? 0;
return row > count
? `coded index selects row ${row} in table ${table}, which has ${count} rows`
: null;
};

View File

@ -73,17 +73,48 @@ const uniqueIssues = (
const heapExtent = (layout: ManagedMetadataLayout): number =>
Math.max(layout.strings.size, layout.blob.size);
const validateIdentityTableCounts = (
layout: ManagedMetadataLayout,
issues: ManagedParseIssue[],
): void => {
const moduleRows = layout.table(0)?.rowCount ?? 0;
if (moduleRows !== 1)
issues.push({
code: "invalid-row",
scope: "metadata.Module",
offset: layout.table(0)?.offset ?? null,
detail: `Module table must contain exactly one row; found ${String(moduleRows)}`,
});
const assemblyRows = layout.table(32)?.rowCount ?? 0;
if (assemblyRows > 1)
issues.push({
code: "invalid-row",
scope: "metadata.Assembly",
offset: layout.table(32)?.offset ?? null,
detail: `Assembly table can contain at most one row; found ${String(assemblyRows)}`,
});
};
const readReferences = (
bytes: Buffer,
layout: ManagedMetadataLayout,
issues: ManagedParseIssue[],
): readonly AssemblyReference[] =>
readRows(layout.table(35), (row) =>
): {
readonly references: readonly AssemblyReference[];
readonly referenceNames: readonly string[];
} => {
const references = readRows(layout.table(35), (row) =>
safeRead(
() => readAssemblyReference(bytes, layout, row, heapExtent(layout)),
issues,
),
);
return {
references,
referenceNames: [...new Set(references.map(({ name }) => name))].sort(),
};
};
const readResources = ({
bytes,
@ -114,53 +145,27 @@ const readAttributes = (
bytes: Buffer,
layout: ManagedMetadataLayout,
issues: ManagedParseIssue[],
): readonly CustomAttribute[] =>
readRows(layout.table(12), (row) =>
): {
readonly attributes: readonly CustomAttribute[];
readonly targetFrameworks: readonly string[];
} => {
const attributes = readRows(layout.table(12), (row) =>
safeRead(
() => readCustomAttribute(bytes, layout, row, heapExtent(layout)),
issues,
),
);
const collectReferenceNames = (
bytes: Buffer,
layout: ManagedMetadataLayout,
issues: ManagedParseIssue[],
): readonly string[] => {
const names: string[] = [];
const referenceTable = layout.table(35);
for (let row = 1; row <= (referenceTable?.rowCount ?? 0); row += 1) {
const reference = safeRead(
() => readAssemblyReference(bytes, layout, row, heapExtent(layout)),
issues,
);
if (reference !== undefined) names.push(reference.name);
}
return names;
};
const collectTargetFrameworks = (
bytes: Buffer,
layout: ManagedMetadataLayout,
issues: ManagedParseIssue[],
): readonly string[] => {
const targetFrameworks: string[] = [];
const attributeTable = layout.table(12);
for (let row = 1; row <= (attributeTable?.rowCount ?? 0); row += 1) {
const attribute = safeRead(
() => readCustomAttribute(bytes, layout, row, heapExtent(layout)),
issues,
);
const targetFrameworks = new Set<string>();
for (const attribute of attributes) {
if (
attribute?.parent_token === metadataToken(32, 1) &&
attribute.parent_token === metadataToken(32, 1) &&
attribute.type_name ===
"System.Runtime.Versioning.TargetFrameworkAttribute" &&
attribute.decoded_fixed_string !== null &&
!targetFrameworks.includes(attribute.decoded_fixed_string)
attribute.decoded_fixed_string !== null
)
targetFrameworks.push(attribute.decoded_fixed_string);
targetFrameworks.add(attribute.decoded_fixed_string);
}
return targetFrameworks;
return { attributes, targetFrameworks: [...targetFrameworks].sort() };
};
/** Inventory identity tables without CLR reflection or execution. */
@ -170,27 +175,30 @@ export const readManagedMetadataInventory = (
resourceDirectory: ManagedResourceDirectory | null,
): ManagedMetadataInventory => {
const issues: ManagedParseIssue[] = [];
validateIdentityTableCounts(layout, issues);
const module =
safeRead(() => readModule(bytes, layout, heapExtent(layout)), issues) ??
null;
const assembly =
safeRead(() => readAssembly(bytes, layout, heapExtent(layout)), issues) ??
null;
const references = readReferences(bytes, layout, issues);
const { references, referenceNames } = readReferences(bytes, layout, issues);
const resources = readResources({
bytes,
layout,
resourceDirectory,
issues,
});
const attributes = readAttributes(bytes, layout, issues);
const referenceNames = collectReferenceNames(bytes, layout, issues);
const targetFrameworks = collectTargetFrameworks(bytes, layout, issues);
const { attributes, targetFrameworks } = readAttributes(
bytes,
layout,
issues,
);
return {
module,
assembly,
targetFrameworks: [...targetFrameworks].sort(),
referenceNames: [...new Set(referenceNames)].sort(),
targetFrameworks,
referenceNames,
references,
resources,
attributes,

View File

@ -9,6 +9,7 @@ import {
import {
metadataRowCursor,
metadataCodedToken,
metadataCodedTokenInvalidReason,
metadataToken,
readMetadataBlob,
readMetadataGuid,
@ -365,6 +366,7 @@ export const readCustomAttribute = (
const parentRaw = cursor.readIndex(
layout.codedIndexSize("HasCustomAttribute"),
);
const typeOffset = cursor.offset;
const typeRaw = cursor.readIndex(
layout.codedIndexSize("CustomAttributeType"),
);
@ -381,23 +383,48 @@ export const readCustomAttribute = (
6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42,
44, 43,
],
layout.rowCounts,
);
const parentReason = metadataCodedTokenInvalidReason(
parentRaw,
5,
[
6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42,
44, 43,
],
layout.rowCounts,
);
if (parent === null)
throw managedFailure(
"invalid-row",
"metadata.CustomAttribute",
"CustomAttribute parent coded index is invalid",
`metadata.CustomAttribute:${metadataToken(12, row)}`,
`CustomAttribute parent coded index 0x${parentRaw.toString(16)} is invalid${parentReason === null ? " because the required parent is null" : `: ${parentReason}`}`,
cursor.start,
);
const typeReason = metadataCodedTokenInvalidReason(
typeRaw,
3,
[undefined, undefined, 6, 10],
layout.rowCounts,
);
if (typeReason !== null || typeRaw === 0)
throw managedFailure(
"invalid-row",
`metadata.CustomAttribute:${metadataToken(12, row)}`,
typeReason === null
? "CustomAttribute constructor coded index 0x0 is null, but Type must reference a MethodDef or MemberRef row"
: `CustomAttribute constructor coded index 0x${typeRaw.toString(16)} is invalid: ${typeReason}`,
typeOffset,
);
const typeName = attributeTypeName(bytes, layout, typeRaw, heapExtent);
return {
parent_token: parent,
constructor_token: metadataCodedToken(typeRaw, 3, [
undefined,
undefined,
6,
10,
]),
constructor_token: metadataCodedToken(
typeRaw,
3,
[undefined, undefined, 6, 10],
layout.rowCounts,
),
type_name: typeName,
value_length: value.length,
value_sha256: sha256Bytes(value),
@ -434,6 +461,7 @@ export const readResource = ({
cursor.readIndex(layout.stringIndexSize),
Math.max(layout.strings.size, layout.blob.size),
);
const implementationOffset = cursor.offset;
const implementationRaw = cursor.readIndex(
layout.codedIndexSize("Implementation"),
);
@ -441,7 +469,21 @@ export const readResource = ({
implementationRaw,
2,
[38, 35, 39],
layout.rowCounts,
);
const implementationReason = metadataCodedTokenInvalidReason(
implementationRaw,
2,
[38, 35, 39],
layout.rowCounts,
);
if (implementationReason !== null)
issues.push({
code: "invalid-row",
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
offset: implementationOffset,
detail: `ManifestResource implementation coded index 0x${implementationRaw.toString(16)} is invalid: ${implementationReason}`,
});
let dataLength: number | null = null;
let dataSha256: string | null = null;
if (implementationRaw === 0) {

View File

@ -4,6 +4,7 @@ import {
decodeInstructions,
parseExceptionRegions,
} from "./ManagedMemberInstructionDecoder.js";
import { validateExceptionRegionRanges } from "./ManagedExceptionRegionValidation.js";
import { sha256Bytes } from "./ManagedMetadataHeaps.js";
const bodyStatus = (
@ -60,6 +61,19 @@ const readMethodBodyHeader = (
};
};
const readExceptionRegions = (
bytes: Buffer,
sectionOffset: number,
methodEnd: number,
ilSize: number,
instructionOffsets: readonly number[],
): ReturnType<typeof parseExceptionRegions> =>
validateExceptionRegionRanges(
parseExceptionRegions(bytes, sectionOffset, methodEnd),
ilSize,
instructionOffsets,
);
/** Decode admitted managed CIL, retaining unavailable implementation metadata as partial. */
export const methodBody = (
bytes: Buffer,
@ -82,7 +96,13 @@ export const methodBody = (
);
try {
const offset = pe.rvaToOffset(rva, 1, "method.body");
const methodExtent = pe.rvaAvailableBytes(rva, "method.body");
const header = readMethodBodyHeader(bytes, offset);
if (
header.size > methodExtent ||
header.ilSize > methodExtent - header.size
)
throw new RangeError("Method body leaves file-backed PE section data");
const ilOffset = offset + header.size;
if (ilOffset > bytes.length - header.ilSize)
throw new RangeError("Method IL bytes leave artifact");
@ -114,7 +134,13 @@ export const methodBody = (
const sectionOffset = (methodEnd + 3) & ~3;
const exceptionRegions =
header.format === "fat" && (header.flags & 8) !== 0
? parseExceptionRegions(bytes, sectionOffset, bytes.length)
? readExceptionRegions(
bytes,
sectionOffset,
offset + methodExtent,
header.ilSize,
decoded.instructionStarts,
)
: null;
const status = bodyStatus(
decoded.issue,

View File

@ -8,6 +8,7 @@ import { type ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
metadataRowCursor,
metadataCodedToken,
metadataCodedTokenInvalidReason,
metadataToken,
readMetadataString,
} from "./ManagedMetadataHeaps.js";
@ -127,6 +128,7 @@ interface ParseImplMapsContext {
readonly heapExtent: number;
readonly modules: readonly ModuleRef[];
readonly members: ReadonlyMap<string, MemberCore>;
readonly issues: ManagedParseIssue[];
}
export const parseImplMaps = ({
@ -135,6 +137,7 @@ export const parseImplMaps = ({
heapExtent,
modules,
members,
issues,
}: ParseImplMapsContext): readonly NativeImport[] => {
const moduleNames = new Map(modules.map((module) => [module.token, module]));
const imports: NativeImport[] = [];
@ -142,10 +145,31 @@ export const parseImplMaps = ({
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
const cursor = metadataRowCursor(bytes, layout, 28, row);
const mappingFlags = cursor.readUInt16();
const memberToken = metadataCodedToken(
cursor.readIndex(layout.codedIndexSize("MemberForwarded")),
const memberForwardedOffset = cursor.offset;
const memberForwardedRaw = cursor.readIndex(
layout.codedIndexSize("MemberForwarded"),
);
const memberForwardedReason = metadataCodedTokenInvalidReason(
memberForwardedRaw,
1,
[4, 6],
layout.rowCounts,
);
if (memberForwardedReason !== null || memberForwardedRaw === 0)
issues.push({
code: "invalid-row",
scope: `metadata.ImplMap:${metadataToken(28, row)}`,
offset: memberForwardedOffset,
detail:
memberForwardedReason === null
? "ImplMap MemberForwarded coded index 0x0 is null, but the column must reference a Field or MethodDef row"
: `ImplMap MemberForwarded coded index 0x${memberForwardedRaw.toString(16)} is invalid: ${memberForwardedReason}`,
});
const memberToken = metadataCodedToken(
memberForwardedRaw,
1,
[4, 6],
layout.rowCounts,
);
const importName = readMetadataString(
bytes,

View File

@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { createHash } from "node:crypto";
import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
import {
@ -68,11 +71,74 @@ describe("managed native boundaries", () => {
ready_to_run: true,
mixed_mode_or_native_header: true,
});
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
expect(result.limitations).toContain(
"P/Invoke rows prove managed import declarations only; this inspection does not verify that a native library, export, thunk, or provider-qualified function exists.",
);
});
});
describe("managed native boundary coded indexes", () => {
it("retains P/Invoke imports with an invalid MemberForwarded index as partial evidence", () => {
const bytes = buildManagedPeFixture({
pinvoke: { memberForwardedRaw: 5 },
});
const result = inspectManagedNativeBoundariesBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.pinvoke_imports).toMatchObject([
{
member_token: null,
member_kind: "unknown",
member_name: null,
import_name: "MessageBoxW",
},
]);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-row",
scope: "metadata.ImplMap:0x1c000001",
detail: expect.stringContaining(
"coded index 0x5 is invalid: coded index selects row 2 in table 6, which has 1 rows",
),
}),
],
});
expect(result.coverage.issues[0]?.offset).toBe(
(result.pinvoke_imports[0]?.row_offset ?? 0) + 2,
);
});
it("reports null for the required MemberForwarded index", () => {
const bytes = buildManagedPeFixture({
pinvoke: { memberForwardedRaw: 0 },
});
const result = inspectManagedNativeBoundariesBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.pinvoke_imports).toMatchObject([
{ member_token: null, member_kind: "unknown" },
]);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-row",
detail:
"ImplMap MemberForwarded coded index 0x0 is null, but the column must reference a Field or MethodDef row",
}),
],
});
});
});
describe("managed native boundary inspection", () => {
it("returns complete member inventories inline and reports unavailable metadata", () => {
const bytes = buildManagedPeFixture();
const paged = inspectManagedMembersBytes(
@ -158,3 +224,34 @@ describe("managed native boundaries", () => {
);
});
});
describe("managed PE fixture layout", () => {
it("keeps ReadyToRun metadata separate from expanded metadata and resources", () => {
const resourceData = Buffer.alloc(64 * 1024, 0x5a);
const bytes = buildManagedPeFixture({
readyToRun: true,
references: Array.from(
{ length: 2_200 },
(_, index) => `Reference.${String(index).padStart(4, "0")}`,
),
resourceData,
});
const target = managedPeFixtureTarget(bytes);
const artifact = inspectManagedArtifactBytes(bytes, target);
const boundaries = inspectManagedNativeBoundariesBytes(bytes, target);
expect(artifact.coverage).toMatchObject({ state: "complete", issues: [] });
expect(artifact.references).toHaveLength(2_200);
expect(artifact.resources[0]).toMatchObject({
data_length: resourceData.length,
data_sha256: createHash("sha256").update(resourceData).digest("hex"),
});
expect(boundaries.cli_native).toMatchObject({
ready_to_run_signature: true,
managed_native_header_size: 4,
});
expect(boundaries.cli_native.managed_native_header_rva).toBeGreaterThan(
0x2700,
);
});
});

View File

@ -156,6 +156,7 @@ export const inspectManagedNativeBoundariesBytes = (
});
}
const heapExtent = Math.max(layout.strings.size, layout.blob.size);
const issues = [...inventory.issues];
const moduleRefs = parseModuleRefs(bytes, layout, heapExtent);
const members = new Map([
...parseFields(bytes, layout, heapExtent),
@ -167,6 +168,7 @@ export const inspectManagedNativeBoundariesBytes = (
heapExtent,
modules: moduleRefs,
members,
issues,
});
const pinvokeTokens = new Set(
imports
@ -187,6 +189,6 @@ export const inspectManagedNativeBoundariesBytes = (
imports,
implementations,
native: cliNative(pe),
issues: [],
issues,
});
};

View File

@ -5,22 +5,31 @@ import type { BinaryTarget } from "../domain/binaryTarget.js";
interface ManagedPeFixtureOptions {
readonly cliFlags?: number;
readonly corruptMetadataSignature?: boolean;
readonly customAttributeTypeRaw?: number;
readonly extendsRaw?: number;
readonly fieldName?: string;
readonly fieldSignature?: Buffer;
readonly ilBody?: Buffer;
readonly malformedAssemblyReferenceRows?: readonly number[];
readonly malformedCustomAttributeRows?: readonly number[];
readonly metadataValidMaskExtra?: bigint;
readonly methodName?: string;
readonly methodSignature?: Buffer;
readonly moduleRowCount?: number;
readonly assemblyRowCount?: number;
readonly memberRefParentRaw?: number;
readonly mvid?: Buffer;
readonly pinvoke?: {
readonly importName?: string;
readonly memberForwardedRaw?: number;
readonly mappingFlags?: number;
readonly moduleName?: string;
};
readonly readyToRun?: boolean;
readonly references?: readonly string[];
readonly resourceImplementationRaw?: number;
readonly resourceData?: Buffer;
readonly targetFramework?: string;
readonly targetFramework?: string | null;
readonly typeName?: string;
readonly typeNamespace?: string;
}
@ -35,6 +44,10 @@ class StringHeap {
readonly #chunks: Buffer[] = [Buffer.from([0])];
#size = 1;
get size(): number {
return this.#size;
}
add(value: string): number {
if (value.length === 0) return 0;
const index = this.#size;
@ -53,13 +66,26 @@ class BlobHeap {
readonly #chunks: Buffer[] = [Buffer.from([0])];
#size = 1;
get size(): number {
return this.#size;
}
add(value: Buffer): number {
if (value.length === 0) return 0;
if (value.length > 0x7f)
throw new RangeError("Managed PE fixture blob is too large");
const index = this.#size;
this.#chunks.push(Buffer.from([value.length]), value);
this.#size += value.length + 1;
const prefix =
value.length <= 0x7f
? Buffer.from([value.length])
: value.length <= 0x3fff
? Buffer.from([0x80 | (value.length >> 8), value.length & 0xff])
: Buffer.from([
0xc0 | (value.length >> 24),
(value.length >> 16) & 0xff,
(value.length >> 8) & 0xff,
value.length & 0xff,
]);
this.#chunks.push(prefix, value);
this.#size += value.length + prefix.length;
return index;
}
@ -106,33 +132,92 @@ const u32 = (value: number): Buffer => {
return bytes;
};
const moduleRow = (name: number): Buffer =>
Buffer.concat([u16(0), u16(name), u16(1), u16(0), u16(0)]);
type MetadataIndexSize = 2 | 4;
const typeRefRow = (name: number, namespace: number): Buffer =>
Buffer.concat([u16(0), u16(name), u16(namespace)]);
const indexSize = (rows: number, codedTagBits = 0): MetadataIndexSize =>
rows < 2 ** (16 - codedTagBits) ? 2 : 4;
const typeDefRow = (
const metadataIndex = (value: number, size: MetadataIndexSize): Buffer =>
size === 2 ? u16(value) : u32(value);
const codedIndexSize = (
rows: readonly number[],
tagBits: number,
): MetadataIndexSize => indexSize(Math.max(...rows), tagBits);
const moduleRow = (
name: number,
namespace: number,
fieldList: number,
methodList: number,
stringSize: MetadataIndexSize,
guidSize: MetadataIndexSize,
): Buffer =>
Buffer.concat([
u32(0x0010_0001),
u16(name),
u16(namespace),
u16((1 << 2) | 1),
u16(fieldList),
u16(methodList),
u16(0),
metadataIndex(name, stringSize),
metadataIndex(1, guidSize),
metadataIndex(0, guidSize),
metadataIndex(0, guidSize),
]);
const fieldRow = (name: number, signature: number): Buffer =>
Buffer.concat([u16(0x0001), u16(name), u16(signature)]);
const typeRefRow = (
name: number,
namespace: number,
stringSize: MetadataIndexSize,
resolutionScopeSize: MetadataIndexSize,
): Buffer =>
Buffer.concat([
metadataIndex(0, resolutionScopeSize),
metadataIndex(name, stringSize),
metadataIndex(namespace, stringSize),
]);
const typeDefRow = ({
name,
namespace,
fieldList,
methodList,
stringSize,
typeDefOrRefSize,
fieldListSize,
methodListSize,
extendsRaw = (1 << 2) | 1,
}: {
readonly name: number;
readonly namespace: number;
readonly fieldList: number;
readonly methodList: number;
readonly stringSize: MetadataIndexSize;
readonly typeDefOrRefSize: MetadataIndexSize;
readonly fieldListSize: MetadataIndexSize;
readonly methodListSize: MetadataIndexSize;
readonly extendsRaw?: number;
}): Buffer =>
Buffer.concat([
u32(0x0010_0001),
metadataIndex(name, stringSize),
metadataIndex(namespace, stringSize),
metadataIndex(extendsRaw, typeDefOrRefSize),
metadataIndex(fieldList, fieldListSize),
metadataIndex(methodList, methodListSize),
]);
const fieldRow = (
name: number,
signature: number,
stringSize: MetadataIndexSize,
blobSize: MetadataIndexSize,
): Buffer =>
Buffer.concat([
u16(0x0001),
metadataIndex(name, stringSize),
metadataIndex(signature, blobSize),
]);
interface MethodDefRowInput {
readonly name: number;
readonly signature: number;
readonly stringSize: MetadataIndexSize;
readonly blobSize: MetadataIndexSize;
readonly parameterSize: MetadataIndexSize;
readonly rva?: number;
readonly flags?: number;
readonly implFlags?: number;
@ -141,6 +226,9 @@ interface MethodDefRowInput {
const methodDefRow = ({
name,
signature,
stringSize,
blobSize,
parameterSize,
rva = 0,
flags = 0x0016,
implFlags = 0,
@ -149,29 +237,56 @@ const methodDefRow = ({
u32(rva),
u16(implFlags),
u16(flags),
u16(name),
u16(signature),
u16(0),
metadataIndex(name, stringSize),
metadataIndex(signature, blobSize),
metadataIndex(0, parameterSize),
]);
const memberRefRow = (name: number, signature: number): Buffer =>
Buffer.concat([u16((1 << 3) | 1), u16(name), u16(signature)]);
const memberRefRow = ({
name,
signature,
stringSize,
blobSize,
parentSize,
parentRaw = (1 << 3) | 1,
}: {
readonly name: number;
readonly signature: number;
readonly stringSize: MetadataIndexSize;
readonly blobSize: MetadataIndexSize;
readonly parentSize: MetadataIndexSize;
readonly parentRaw?: number;
}): Buffer =>
Buffer.concat([
metadataIndex(parentRaw, parentSize),
metadataIndex(name, stringSize),
metadataIndex(signature, blobSize),
]);
const moduleRefRow = (name: number): Buffer => Buffer.concat([u16(name)]);
const moduleRefRow = (name: number, stringSize: MetadataIndexSize): Buffer =>
Buffer.concat([metadataIndex(name, stringSize)]);
const implMapRow = (
importName: number,
importScope: number,
mappingFlags: number,
stringSize: MetadataIndexSize,
memberForwardedSize: MetadataIndexSize,
moduleRefSize: MetadataIndexSize,
memberForwardedRaw: number,
): Buffer =>
Buffer.concat([
u16(mappingFlags),
u16((1 << 1) | 1),
u16(importName),
u16(importScope),
metadataIndex(memberForwardedRaw, memberForwardedSize),
metadataIndex(importName, stringSize),
metadataIndex(importScope, moduleRefSize),
]);
const assemblyRow = (name: number): Buffer =>
const assemblyRow = (
name: number,
stringSize: MetadataIndexSize,
blobSize: MetadataIndexSize,
): Buffer =>
Buffer.concat([
u32(0x0000_8004),
u16(1),
@ -179,29 +294,71 @@ const assemblyRow = (name: number): Buffer =>
u16(3),
u16(4),
u32(0),
u16(0),
u16(name),
u16(0),
metadataIndex(0, blobSize),
metadataIndex(name, stringSize),
metadataIndex(0, stringSize),
]);
const assemblyRefRow = (name: number, keyOrToken: number): Buffer =>
const assemblyRefRow = (
name: number,
keyOrToken: number,
malformedName: boolean,
stringSize: MetadataIndexSize,
blobSize: MetadataIndexSize,
): Buffer =>
Buffer.concat([
u16(8),
u16(0),
u16(0),
u16(0),
u32(0),
u16(keyOrToken),
u16(name),
u16(0),
u16(0),
metadataIndex(keyOrToken, blobSize),
metadataIndex(
malformedName ? (stringSize === 2 ? 0xffff : 0xffff_ffff) : name,
stringSize,
),
metadataIndex(0, stringSize),
metadataIndex(0, blobSize),
]);
const customAttributeRow = (value: number): Buffer =>
Buffer.concat([u16((1 << 5) | 14), u16((1 << 3) | 3), u16(value)]);
const customAttributeRow = (
value: number,
options: ManagedPeFixtureOptions,
parentIndexSize: 2 | 4,
typeIndexSize: MetadataIndexSize,
blobSize: MetadataIndexSize,
): Buffer =>
Buffer.concat([
parentIndexSize === 2
? u16(
options.malformedCustomAttributeRows?.includes(1)
? 0xffff
: (1 << 5) | 14,
)
: u32(
options.malformedCustomAttributeRows?.includes(1)
? 0xffff_ffff
: (1 << 5) | 14,
),
metadataIndex(
options.customAttributeTypeRaw ?? (1 << 3) | 3,
typeIndexSize,
),
metadataIndex(value, blobSize),
]);
const manifestResourceRow = (name: number): Buffer =>
Buffer.concat([u32(0), u32(2), u16(name), u16(0)]);
const manifestResourceRow = (
name: number,
stringSize: MetadataIndexSize,
implementationSize: MetadataIndexSize,
implementationRaw: number,
): Buffer =>
Buffer.concat([
u32(0),
u32(2),
metadataIndex(name, stringSize),
metadataIndex(implementationRaw, implementationSize),
]);
const fixedStringAttributeBlob = (value: string): Buffer => {
const bytes = Buffer.from(textEncoder.encode(value));
@ -270,11 +427,13 @@ const metadataRoot = (
const tablesStream = (
rows: ReadonlyMap<number, readonly Buffer[]>,
extraValidMask: bigint,
heapSizes: number,
): Buffer => {
let valid = extraValidMask;
for (const table of rows.keys()) valid |= 1n << BigInt(table);
const header = Buffer.alloc(24);
header.writeUInt8(2, 4);
header.writeUInt8(heapSizes, 6);
header.writeUInt8(1, 7);
header.writeBigUInt64LE(valid, 8);
const counts: Buffer[] = [];
@ -292,8 +451,269 @@ const tablesStream = (
return Buffer.concat([header, ...counts, ...data]);
};
interface FixtureMetadataRowContext {
readonly options: ManagedPeFixtureOptions;
readonly methodRva: number;
readonly referenceStringIndexes: readonly number[];
readonly indexes: {
readonly moduleName: number;
readonly assemblyName: number;
readonly attributeName: number;
readonly attributeNamespace: number;
readonly constructorName: number;
readonly typeName: number;
readonly typeNamespace: number;
readonly fieldName: number;
readonly methodName: number;
readonly pinvokeModuleName: number | null;
readonly pinvokeImportName: number | null;
readonly resourceName: number;
readonly tokenBlob: number;
readonly fieldSignature: number;
readonly methodSignature: number;
readonly constructorSignature: number;
readonly attributeBlob: number;
};
readonly heapSizes: {
readonly strings: MetadataIndexSize;
readonly blobs: MetadataIndexSize;
readonly guids: MetadataIndexSize;
};
}
interface FixtureIndexWidths {
readonly table: (index: number) => MetadataIndexSize;
readonly coded: (
tables: readonly number[],
tagBits: number,
) => MetadataIndexSize;
readonly resolutionScope: MetadataIndexSize;
readonly typeDefOrRef: MetadataIndexSize;
readonly memberRefParent: MetadataIndexSize;
readonly hasCustomAttribute: MetadataIndexSize;
readonly customAttributeType: MetadataIndexSize;
readonly implementation: MetadataIndexSize;
}
const repeatedRows = (
count: number,
createRow: () => Buffer,
): readonly Buffer[] => Array.from({ length: count }, createRow);
const customAttributeRows = (
{
options,
indexes,
heapSizes,
}: Pick<FixtureMetadataRowContext, "options" | "indexes" | "heapSizes">,
widths: FixtureIndexWidths,
): readonly Buffer[] =>
options.targetFramework === null
? []
: [
customAttributeRow(
indexes.attributeBlob,
options,
widths.hasCustomAttribute,
widths.customAttributeType,
heapSizes.blobs,
),
];
const fixtureIndexWidths = ({
options,
referenceStringIndexes,
indexes,
}: FixtureMetadataRowContext): FixtureIndexWidths => {
const rowCounts = new Map<number, number>([
[0, options.moduleRowCount ?? 1],
[1, 1],
[2, 1],
[4, 1],
[6, 1],
[8, 0],
[10, 1],
[12, options.targetFramework === null ? 0 : 1],
...(indexes.pinvokeModuleName === null ? [] : ([[26, 1]] as const)),
...(indexes.pinvokeImportName === null ? [] : ([[28, 1]] as const)),
[32, options.assemblyRowCount ?? 1],
[35, referenceStringIndexes.length],
[40, 1],
]);
const count = (table: number): number => rowCounts.get(table) ?? 0;
const table = (index: number): MetadataIndexSize => indexSize(count(index));
const coded = (
tables: readonly number[],
tagBits: number,
): MetadataIndexSize => codedIndexSize(tables.map(count), tagBits);
return {
table,
coded,
resolutionScope: coded([0, 26, 35, 1], 2),
typeDefOrRef: coded([2, 1, 27], 2),
memberRefParent: coded([2, 1, 26, 6, 27], 3),
hasCustomAttribute: coded(
[
6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42,
44, 43,
],
5,
),
customAttributeType: coded([6, 10], 3),
implementation: coded([38, 35, 39], 2),
};
};
const fixtureMetadataRows = ({
options,
methodRva,
referenceStringIndexes,
indexes,
heapSizes,
}: FixtureMetadataRowContext): ReadonlyMap<number, readonly Buffer[]> => {
const widths = fixtureIndexWidths({
options,
methodRva,
referenceStringIndexes,
indexes,
heapSizes,
});
const rows = new Map<number, readonly Buffer[]>([
[
0,
repeatedRows(options.moduleRowCount ?? 1, () =>
moduleRow(indexes.moduleName, heapSizes.strings, heapSizes.guids),
),
],
[
1,
[
typeRefRow(
indexes.attributeName,
indexes.attributeNamespace,
heapSizes.strings,
widths.resolutionScope,
),
],
],
[
2,
[
typeDefRow({
name: indexes.typeName,
namespace: indexes.typeNamespace,
fieldList: 1,
methodList: 1,
stringSize: heapSizes.strings,
typeDefOrRefSize: widths.typeDefOrRef,
fieldListSize: widths.table(4),
methodListSize: widths.table(6),
...(options.extendsRaw === undefined
? {}
: { extendsRaw: options.extendsRaw }),
}),
],
],
[
4,
[
fieldRow(
indexes.fieldName,
indexes.fieldSignature,
heapSizes.strings,
heapSizes.blobs,
),
],
],
[
6,
[
methodDefRow({
name: indexes.methodName,
signature: indexes.methodSignature,
stringSize: heapSizes.strings,
blobSize: heapSizes.blobs,
parameterSize: widths.table(8),
rva: methodRva,
flags: options.pinvoke === undefined ? 0x0016 : 0x2016,
}),
],
],
[
10,
[
memberRefRow({
name: indexes.constructorName,
signature: indexes.constructorSignature,
stringSize: heapSizes.strings,
blobSize: heapSizes.blobs,
parentSize: widths.memberRefParent,
...(options.memberRefParentRaw === undefined
? {}
: { parentRaw: options.memberRefParentRaw }),
}),
],
],
[12, customAttributeRows({ options, indexes, heapSizes }, widths)],
...(indexes.pinvokeModuleName === null
? []
: ([
[26, [moduleRefRow(indexes.pinvokeModuleName, heapSizes.strings)]],
] as const)),
...(indexes.pinvokeImportName === null
? []
: ([
[
28,
[
implMapRow(
indexes.pinvokeImportName,
1,
options.pinvoke?.mappingFlags ?? 0x0344,
heapSizes.strings,
widths.coded([4, 6], 1),
widths.table(26),
options.pinvoke?.memberForwardedRaw ?? (1 << 1) | 1,
),
],
],
] as const)),
[
32,
repeatedRows(options.assemblyRowCount ?? 1, () =>
assemblyRow(indexes.assemblyName, heapSizes.strings, heapSizes.blobs),
),
],
[
35,
referenceStringIndexes.map((name, index) =>
assemblyRefRow(
name,
indexes.tokenBlob,
options.malformedAssemblyReferenceRows?.includes(index + 1) ?? false,
heapSizes.strings,
heapSizes.blobs,
),
),
],
[
40,
[
manifestResourceRow(
indexes.resourceName,
heapSizes.strings,
widths.implementation,
options.resourceImplementationRaw ?? 0,
),
],
],
]);
return rows;
};
const buildManagedFixtureMetadata = (
options: ManagedPeFixtureOptions,
methodRva: number,
): Buffer => {
const strings = new StringHeap();
const blobs = new BlobHeap();
@ -329,50 +749,45 @@ const buildManagedFixtureMetadata = (
const constructorSignature = blobs.add(Buffer.from([0x20, 0x01, 0x01, 0x0e]));
const attributeBlob = blobs.add(
fixedStringAttributeBlob(
options.targetFramework ?? ".NETCoreApp,Version=v8.0",
options.targetFramework === null
? ".NETCoreApp,Version=v8.0"
: (options.targetFramework ?? ".NETCoreApp,Version=v8.0"),
),
);
const rows = new Map<number, readonly Buffer[]>([
[0, [moduleRow(moduleName)]],
[1, [typeRefRow(attributeName, attributeNamespace)]],
[2, [typeDefRow(typeName, typeNamespace, 1, 1)]],
[4, [fieldRow(fieldName, fieldSignature)]],
[
6,
[
methodDefRow({
name: methodName,
signature: methodSignature,
rva: 0x2800,
flags: options.pinvoke === undefined ? 0x0016 : 0x2016,
}),
],
],
[10, [memberRefRow(constructorName, constructorSignature)]],
[12, [customAttributeRow(attributeBlob)]],
...(pinvokeModuleName === null
? []
: ([[26, [moduleRefRow(pinvokeModuleName)]]] as const)),
...(pinvokeImportName === null
? []
: ([
[
28,
[
implMapRow(
pinvokeImportName,
1,
options.pinvoke?.mappingFlags ?? 0x0344,
),
],
],
] as const)),
[32, [assemblyRow(assemblyName)]],
[35, referenceStringIndexes.map((name) => assemblyRefRow(name, tokenBlob))],
[40, [manifestResourceRow(resourceName)]],
]);
const stringSize = indexSize(strings.size);
const blobSize = indexSize(blobs.size);
const guidSize = indexSize(guidHeap(options.mvid).length);
const rows = fixtureMetadataRows({
options,
methodRva,
referenceStringIndexes,
indexes: {
moduleName,
assemblyName,
attributeName,
attributeNamespace,
constructorName,
typeName,
typeNamespace,
fieldName,
methodName,
pinvokeModuleName,
pinvokeImportName,
resourceName,
tokenBlob,
fieldSignature,
methodSignature,
constructorSignature,
attributeBlob,
},
heapSizes: { strings: stringSize, blobs: blobSize, guids: guidSize },
});
const heapFlags =
(stringSize === 4 ? 0x01 : 0) |
(guidSize === 4 ? 0x02 : 0) |
(blobSize === 4 ? 0x04 : 0);
const metadata = metadataRoot(
tablesStream(rows, options.metadataValidMaskExtra ?? 0n),
tablesStream(rows, options.metadataValidMaskExtra ?? 0n, heapFlags),
strings.toBuffer(),
guidHeap(options.mvid),
blobs.toBuffer(),
@ -383,14 +798,56 @@ const buildManagedFixtureMetadata = (
const buildManagedFixtureImage = (
options: ManagedPeFixtureOptions,
metadata: Buffer,
buildMetadata: (methodRva: number) => Buffer,
): Buffer => {
const resourceData = options.resourceData ?? Buffer.from("resource-data");
const resourceDirectory = Buffer.concat([
u32(resourceData.length),
resourceData,
]);
const image = Buffer.alloc(0x1000);
const initialMetadata = buildMetadata(0);
const resourceOffset = Math.max(
0x0800,
Math.ceil((0x0300 + initialMetadata.length) / 0x0200) * 0x0200,
);
const resourceRva = 0x2000 + resourceOffset - 0x0200;
const bodyOffset = Math.max(
0x0a00,
Math.ceil((resourceOffset + resourceDirectory.length) / 0x0200) * 0x0200,
);
const methodRva = 0x2000 + bodyOffset - 0x0200;
const metadata = buildMetadata(methodRva);
const body =
options.ilBody ??
Buffer.from([
0x32, 0x02, 0x7b, 0x01, 0x00, 0x00, 0x04, 0x28, 0x01, 0x00, 0x00, 0x0a,
0x2a,
]);
const legacyReadyToRunOffset = 0x0900;
const metadataEnd = 0x0300 + metadata.length;
const resourceEnd = resourceOffset + resourceDirectory.length;
const bodyEnd = bodyOffset + body.length;
const overlapsReadyToRun = (start: number, end: number): boolean =>
legacyReadyToRunOffset < end && legacyReadyToRunOffset + 4 > start;
const readyToRunOffset =
options.readyToRun === true
? overlapsReadyToRun(0x0300, metadataEnd) ||
overlapsReadyToRun(resourceOffset, resourceEnd) ||
overlapsReadyToRun(bodyOffset, bodyEnd)
? Math.ceil(Math.max(metadataEnd, resourceEnd, bodyEnd) / 4) * 4
: legacyReadyToRunOffset
: null;
const rawSectionSize =
Math.ceil(
Math.max(
0x0e00,
0x0100 + metadata.length,
bodyOffset - 0x0200 + body.length,
readyToRunOffset === null ? 0 : readyToRunOffset - 0x0200 + 4,
) / 0x0200,
) * 0x0200;
const virtualSectionSize = Math.max(0x1000, rawSectionSize);
const image = Buffer.alloc(0x0200 + rawSectionSize);
image.write("MZ", 0, "ascii");
image.writeUInt32LE(0x80, 0x3c);
image.writeUInt32LE(0x0000_4550, 0x80);
@ -405,16 +862,19 @@ const buildManagedFixtureImage = (
image.writeUInt32LE(0x0040_0000, optional + 28);
image.writeUInt32LE(0x1000, optional + 32);
image.writeUInt32LE(0x200, optional + 36);
image.writeUInt32LE(0x3000, optional + 56);
image.writeUInt32LE(
Math.ceil((0x2000 + virtualSectionSize) / 0x1000) * 0x1000,
optional + 56,
);
image.writeUInt32LE(0x200, optional + 60);
image.writeUInt32LE(16, optional + 92);
image.writeUInt32LE(0x2000, optional + 96 + 14 * 8);
image.writeUInt32LE(72, optional + 96 + 14 * 8 + 4);
const section = optional + 0x00e0;
image.write(".text\0\0\0", section, "ascii");
image.writeUInt32LE(0x1000, section + 8);
image.writeUInt32LE(virtualSectionSize, section + 8);
image.writeUInt32LE(0x2000, section + 12);
image.writeUInt32LE(0x0e00, section + 16);
image.writeUInt32LE(rawSectionSize, section + 16);
image.writeUInt32LE(0x0200, section + 20);
image.writeUInt32LE(0x6000_0020, section + 36);
const cli = 0x0200;
@ -425,22 +885,16 @@ const buildManagedFixtureImage = (
image.writeUInt32LE(metadata.length, cli + 12);
image.writeUInt32LE(options.cliFlags ?? 1, cli + 16);
image.writeUInt32LE(0x0600_0001, cli + 20);
image.writeUInt32LE(0x2600, cli + 24);
image.writeUInt32LE(resourceRva, cli + 24);
image.writeUInt32LE(resourceDirectory.length, cli + 28);
if (options.readyToRun === true) {
image.writeUInt32LE(0x2700, cli + 64);
if (readyToRunOffset !== null) {
image.writeUInt32LE(0x2000 + readyToRunOffset - 0x0200, cli + 64);
image.writeUInt32LE(4, cli + 68);
image.write("RTR\0", 0x0900, "ascii");
image.write("RTR\0", readyToRunOffset, "ascii");
}
(
options.ilBody ??
Buffer.from([
0x32, 0x02, 0x7b, 0x01, 0x00, 0x00, 0x04, 0x28, 0x01, 0x00, 0x00, 0x0a,
0x2a,
])
).copy(image, 0x0a00);
body.copy(image, bodyOffset);
metadata.copy(image, 0x0300);
resourceDirectory.copy(image, 0x0800);
resourceDirectory.copy(image, resourceOffset);
return image;
};
@ -448,7 +902,9 @@ const buildManagedFixtureImage = (
export const buildManagedPeFixture = (
options: ManagedPeFixtureOptions = {},
): Buffer =>
buildManagedFixtureImage(options, buildManagedFixtureMetadata(options));
buildManagedFixtureImage(options, (methodRva) =>
buildManagedFixtureMetadata(options, methodRva),
);
/** Build a syntactically valid PE32 fixture with no CLI directory. */
export const buildNativePeFixture = (): Buffer => {

View File

@ -41,6 +41,7 @@ export interface ManagedPeLayout {
readonly cliDirectoryPresent: boolean;
readonly cliIssue: ManagedParseIssue | null;
rvaToOffset(rva: number, size: number, scope: string): number;
rvaAvailableBytes(rva: number, scope: string): number;
}
interface RvaMapping {
@ -355,5 +356,22 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
cliIssue,
rvaToOffset: (rva, size, scope) =>
mapRva({ bytes, sections, rva, size, scope }),
rvaAvailableBytes: (rva, scope) => {
mapRva({ bytes, sections, rva, size: 1, scope });
for (const section of sections) {
if (rva < section.virtualAddress) continue;
const within = rva - section.virtualAddress;
if (within < section.rawSize)
return Math.min(
section.rawSize - within,
bytes.length - section.rawOffset - within,
);
}
throw managedFailure(
"invalid-directory",
scope,
`${scope} RVA is not covered by file-backed PE section data`,
);
},
};
};