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:
parent
35a4e18d30
commit
bda4ada7fb
|
|
@ -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" }),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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]),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue