mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 21:36:13 +00:00
merge: consolidate upstream managed token facts
This commit is contained in:
commit
8d3ed58cb7
15 files changed
+595
-215
No files matched your search
@@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
fixtureUserStringHeap,
|
||||
managedPeFixtureTarget,
|
||||
} from "./ManagedPe.fixture.js";
|
||||
import { parseExceptionRegions } from "./ManagedMemberInstructionDecoder.js";
|
||||
@@ -490,6 +491,65 @@ describe("managed complete operand decoding", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("retains named references and loaded literals before a truncated operand", () => {
|
||||
const { heap, tokens } = fixtureUserStringHeap(["observed"]);
|
||||
const literalToken = tokens[0]!;
|
||||
const il = Buffer.from([
|
||||
0x28, 1, 0, 0, 0x0a, 0x7e, 1, 0, 0, 0x04, 0x26, 0x72, 0, 0, 0, 0, 0x26,
|
||||
0x20,
|
||||
]);
|
||||
il.writeUInt32LE(Number.parseInt(literalToken, 16), 12);
|
||||
const bytes = buildManagedPeFixture({
|
||||
memberRefParentRaw: 8,
|
||||
memberRefName: "ObservedCall",
|
||||
fieldName: "ObservedField",
|
||||
ilBody: Buffer.concat([Buffer.from([(il.length << 2) | 2]), il]),
|
||||
userStringHeap: heap,
|
||||
});
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
|
||||
expect(result.methods[0]?.body).toMatchObject({
|
||||
status: "malformed",
|
||||
il_size: il.length,
|
||||
il_sha256: createHash("sha256").update(il).digest("hex"),
|
||||
normalized_il_sha256: null,
|
||||
decoded_instruction_count: 5,
|
||||
anchors: [
|
||||
{ opcode: "call", operand: "0x0a000001" },
|
||||
{ opcode: "ldsfld", operand: "0x04000001" },
|
||||
{ opcode: "pop", operand: null },
|
||||
{ opcode: "ldstr", operand: literalToken },
|
||||
{ opcode: "pop", operand: null },
|
||||
],
|
||||
issue: "Instruction ldc.i4 at IL offset 17 leaves method body",
|
||||
});
|
||||
expect(result.call_edges).toMatchObject([
|
||||
{
|
||||
opcode: "call",
|
||||
target_token: "0x0a000001",
|
||||
target_name: "ObservedCall",
|
||||
target_declaring_type: "Fixture.Program",
|
||||
},
|
||||
]);
|
||||
expect(result.field_accesses).toMatchObject([
|
||||
{
|
||||
opcode: "ldsfld",
|
||||
field_token: "0x04000001",
|
||||
field_name: "ObservedField",
|
||||
field_declaring_type: "Fixture.Program",
|
||||
},
|
||||
]);
|
||||
expect(result.user_strings).toMatchObject({
|
||||
state: "partial",
|
||||
literals: [{ token: literalToken, value: "observed" }],
|
||||
issues: [],
|
||||
});
|
||||
expect(result.coverage.state).toBe("partial");
|
||||
});
|
||||
|
||||
it("keeps a complete operand-bearing body decodable", () => {
|
||||
expect(
|
||||
fatBody(Buffer.from([0x00, 0x20, 0x01, 0x00, 0x00, 0x00, 0x2a])),
|
||||
@@ -894,3 +954,82 @@ describe("managed exception section decoding", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed catch fact retention", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "local TypeDef",
|
||||
classToken: 0x0200_0001,
|
||||
typeRefScopeRaw: 0,
|
||||
classType: "Fixture.Program",
|
||||
identityResolved: true,
|
||||
},
|
||||
{
|
||||
name: "TypeRef with an unavailable AssemblyRef scope",
|
||||
classToken: 0x0100_0001,
|
||||
typeRefScopeRaw: (9 << 2) | 2,
|
||||
classType: "System.Runtime.Versioning.TargetFrameworkAttribute",
|
||||
identityResolved: false,
|
||||
},
|
||||
{
|
||||
name: "TypeRef with a cyclic scope",
|
||||
classToken: 0x0100_0001,
|
||||
typeRefScopeRaw: (1 << 2) | 3,
|
||||
classType: "System.Runtime.Versioning.TargetFrameworkAttribute",
|
||||
identityResolved: false,
|
||||
},
|
||||
])(
|
||||
"retains $name catch facts before a malformed later EH section",
|
||||
({ classToken, typeRefScopeRaw, classType, identityResolved }) => {
|
||||
const header = Buffer.alloc(12);
|
||||
header.writeUInt16LE(0x300b, 0);
|
||||
header.writeUInt16LE(8, 2);
|
||||
header.writeUInt32LE(1, 4);
|
||||
const first = Buffer.alloc(16);
|
||||
first[0] = 0x81;
|
||||
first[1] = first.length;
|
||||
first.writeUInt32LE(classToken, 12);
|
||||
const later = Buffer.alloc(17);
|
||||
later[0] = 0x01;
|
||||
later[1] = later.length;
|
||||
const il = Buffer.from([0x2a]);
|
||||
const bytes = buildManagedPeFixture({
|
||||
typeRefScopeRaw,
|
||||
ilBody: Buffer.concat([header, il, Buffer.alloc(3), first, later]),
|
||||
});
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
|
||||
expect(result.methods[0]?.body).toMatchObject({
|
||||
status: "malformed",
|
||||
il_sha256: createHash("sha256").update(il).digest("hex"),
|
||||
normalized_il_sha256: null,
|
||||
decoded_instruction_count: 1,
|
||||
anchors: [{ opcode: "ret", operand: null }],
|
||||
exception_regions: [
|
||||
{
|
||||
flags: 0,
|
||||
class_token: `0x${classToken.toString(16).padStart(8, "0")}`,
|
||||
class_type: classType,
|
||||
class_type_name_sha256: identityResolved
|
||||
? expect.any(String)
|
||||
: null,
|
||||
class_type_identity_resolved: identityResolved,
|
||||
},
|
||||
],
|
||||
issue: "Exception section size does not contain whole clauses",
|
||||
});
|
||||
expect(result.coverage.state).toBe("partial");
|
||||
if (!identityResolved)
|
||||
expect(result.coverage.issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
code: "invalid-row",
|
||||
scope: "metadata.TypeRef:0x01000001",
|
||||
}),
|
||||
);
|
||||
expect(result.user_strings?.state).toBe("complete");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -79,8 +79,7 @@ export const validateExceptionRegionRanges = (
|
||||
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 };
|
||||
if (issue !== null) return { ...parsed, status: "malformed", issue };
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
@@ -18,8 +18,11 @@ import { parseUserStrings } from "./ManagedUserStrings.js";
|
||||
import {
|
||||
readCompressed,
|
||||
signature,
|
||||
isParsedTokenInstruction,
|
||||
type DecodedMethod,
|
||||
type ManagedMethod,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
import { decodeInstructions } from "./ManagedMemberInstructionDecoder.js";
|
||||
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
|
||||
import { managedTypeReferences } from "./ManagedTypeReferences.js";
|
||||
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
|
||||
@@ -35,6 +38,19 @@ import {
|
||||
managedPeFixtureTarget,
|
||||
} from "./ManagedPe.fixture.js";
|
||||
|
||||
/** Decode real token operands for capacity tests that do not need a PE method body. */
|
||||
const decodedTokenInstructions = (
|
||||
opcode: number,
|
||||
tokens: readonly number[],
|
||||
) => {
|
||||
const il = Buffer.alloc(tokens.length * 5);
|
||||
for (const [index, token] of tokens.entries()) {
|
||||
il.writeUInt8(opcode, index * 5);
|
||||
il.writeUInt32LE(token, index * 5 + 1);
|
||||
}
|
||||
return decodeInstructions(il).parsed.filter(isParsedTokenInstruction);
|
||||
};
|
||||
|
||||
/** A tiny-header body that loads each `ldstr` token, discards it, and returns. */
|
||||
const ldstrBody = (tokens: readonly string[]): Buffer => {
|
||||
const il = Buffer.concat([
|
||||
@@ -1329,16 +1345,14 @@ describe("managed user strings", () => {
|
||||
const layout = { userStrings: null } as unknown as ManagedMetadataLayout;
|
||||
// Leave 1 KiB of the 64 MiB budget: too little for 100 distinct tokens.
|
||||
managedDecodeBudget(layout).reserve(64 * 1024 * 1024 - 1024, null);
|
||||
const method = {
|
||||
body: {
|
||||
anchors: Array.from({ length: 100 }, (_, index) => ({
|
||||
il_offset: index * 5,
|
||||
opcode: "ldstr",
|
||||
operand_kind: "string",
|
||||
operand: `0x70${(index + 1).toString(16).padStart(6, "0")}`,
|
||||
})),
|
||||
},
|
||||
} as unknown as ManagedMethod;
|
||||
const method: DecodedMethod = {
|
||||
method: {} as ManagedMethod,
|
||||
tokenInstructions: decodedTokenInstructions(
|
||||
0x72,
|
||||
Array.from({ length: 100 }, (_, index) => 0x70000001 + index),
|
||||
),
|
||||
catchTokens: new Map(),
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
parseUserStrings(Buffer.alloc(0), layout, {
|
||||
@@ -1512,16 +1526,14 @@ describe("managed user string issues", () => {
|
||||
} as unknown as ManagedMetadataLayout;
|
||||
// Leave 24 KiB: enough for the failures, not for 512 bytes leaked each.
|
||||
managedDecodeBudget(layout).reserve(64 * 1024 * 1024 - 24 * 1024, null);
|
||||
const method = {
|
||||
body: {
|
||||
anchors: Array.from({ length: 64 }, (_, index) => ({
|
||||
il_offset: index * 5,
|
||||
opcode: "ldstr",
|
||||
operand_kind: "string",
|
||||
operand: `0x70${(1 + index * 2).toString(16).padStart(6, "0")}`,
|
||||
})),
|
||||
},
|
||||
} as unknown as ManagedMethod;
|
||||
const method: DecodedMethod = {
|
||||
method: {} as ManagedMethod,
|
||||
tokenInstructions: decodedTokenInstructions(
|
||||
0x72,
|
||||
Array.from({ length: 64 }, (_, index) => 0x70000001 + index * 2),
|
||||
),
|
||||
catchTokens: new Map(),
|
||||
};
|
||||
|
||||
expect(
|
||||
parseUserStrings(heap, layout, { methods: [method], unscannedMethods: 0 })
|
||||
@@ -1801,23 +1813,25 @@ describe("managed member decoding budget", () => {
|
||||
|
||||
describe("managed derived edges", () => {
|
||||
it("admits call edges before the derived graph exceeds the budget", () => {
|
||||
const anchor = {
|
||||
il_offset: 0,
|
||||
opcode: "call",
|
||||
operand_kind: "method",
|
||||
operand: "0x0a000001",
|
||||
};
|
||||
const method = {
|
||||
token: "0x06000001",
|
||||
declaring_type: "T".repeat(2000),
|
||||
name: "m",
|
||||
body: { anchors: Array.from({ length: 16 }, () => anchor) },
|
||||
} as unknown as ManagedMethod;
|
||||
|
||||
const decoded: DecodedMethod = {
|
||||
method,
|
||||
tokenInstructions: decodedTokenInstructions(
|
||||
0x28,
|
||||
Array(16).fill(0x0a000001),
|
||||
),
|
||||
catchTokens: new Map(),
|
||||
};
|
||||
|
||||
const budget = new ManagedDecodeBudget();
|
||||
// Exercise the same capacity boundary without constructing 64 MiB of edges.
|
||||
budget.reserve(64 * 1024 * 1024 - 16 * 1024, null);
|
||||
expect(() => edges([method], new Map(), budget)).toThrow(
|
||||
expect(() => edges([decoded], new Map(), budget)).toThrow(
|
||||
ManagedReaderFailure,
|
||||
);
|
||||
});
|
||||
@@ -1839,22 +1853,18 @@ describe("managed derived caller names", () => {
|
||||
token: "0x06000001",
|
||||
declaring_type: declaringType,
|
||||
name: "m",
|
||||
body: {
|
||||
anchors: [
|
||||
{
|
||||
il_offset: 0,
|
||||
opcode: "call",
|
||||
operand_kind: "method",
|
||||
operand: "0x0a000001",
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as ManagedMethod;
|
||||
|
||||
const decoded: DecodedMethod = {
|
||||
method,
|
||||
tokenInstructions: decodedTokenInstructions(0x28, [0x0a000001]),
|
||||
catchTokens: new Map(),
|
||||
};
|
||||
|
||||
const budget = new ManagedDecodeBudget();
|
||||
// Only the remaining capacity matters to refusal before interpolation.
|
||||
budget.reserve(64 * 1024 * 1024 - 1024, null);
|
||||
expect(() => edges([method], new Map(), budget)).toThrow(
|
||||
expect(() => edges([decoded], new Map(), budget)).toThrow(
|
||||
ManagedReaderFailure,
|
||||
);
|
||||
expect(built).toBe(false);
|
||||
|
||||
@@ -319,38 +319,35 @@ export const inspectManagedMembersBytes = (
|
||||
methodLimit,
|
||||
});
|
||||
appendManagedParseIssues(issues, fields.issues, methods.issues);
|
||||
const resolvedMethods = methods.methods.map((method) => ({
|
||||
const resolvedMethods = methods.methods.map(({ method, catchTokens }) => ({
|
||||
...method,
|
||||
body: {
|
||||
...method.body,
|
||||
exception_regions: method.body.exception_regions.map((region) => {
|
||||
const classType = resolveTypeName(region.class_token);
|
||||
const tokenTable =
|
||||
region.class_token === null
|
||||
? null
|
||||
: Number.parseInt(region.class_token.slice(2, 4), 16);
|
||||
const tokenRow =
|
||||
region.class_token === null
|
||||
? 0
|
||||
: Number.parseInt(region.class_token.slice(4), 16);
|
||||
const identityResolved =
|
||||
region.class_token === null
|
||||
? null
|
||||
: classType !== null && tokenTable === 2
|
||||
? declarationTypeName(2, tokenRow) !== null
|
||||
: false;
|
||||
return {
|
||||
...region,
|
||||
class_type: classType,
|
||||
class_type_name_sha256:
|
||||
region.class_token !== null &&
|
||||
(tokenTable === 1 || tokenTable === 2)
|
||||
? (declarationTypeName(tokenTable, tokenRow)?.toString("hex") ??
|
||||
null)
|
||||
: null,
|
||||
class_type_identity_resolved: identityResolved,
|
||||
};
|
||||
}),
|
||||
exception_regions: method.body.exception_regions.map(
|
||||
(region, ordinal) => {
|
||||
const reference = catchTokens.get(ordinal) ?? null;
|
||||
const classType = resolveTypeName(reference);
|
||||
const identityResolved =
|
||||
reference === null
|
||||
? null
|
||||
: classType !== null && reference.table === 2
|
||||
? declarationTypeName(2, reference.row) !== null
|
||||
: false;
|
||||
return {
|
||||
...region,
|
||||
class_type: classType,
|
||||
class_type_name_sha256:
|
||||
reference !== null &&
|
||||
(reference.table === 1 || reference.table === 2)
|
||||
? (declarationTypeName(
|
||||
reference.table,
|
||||
reference.row,
|
||||
)?.toString("hex") ?? null)
|
||||
: null,
|
||||
class_type_identity_resolved: identityResolved,
|
||||
};
|
||||
},
|
||||
),
|
||||
},
|
||||
}));
|
||||
// #US literals sit beside the member inventory and carry their own
|
||||
@@ -370,16 +367,19 @@ export const inspectManagedMembersBytes = (
|
||||
);
|
||||
appendManagedParseIssues(issues, targets.issues);
|
||||
const incompleteMembers =
|
||||
[...fields.fields, ...memberRefs.refs, ...methods.methods].some(
|
||||
[...fields.fields, ...memberRefs.refs].some(
|
||||
(member) => member.signature.parse_status !== "decoded",
|
||||
) ||
|
||||
resolvedMethods.some(
|
||||
(method) => method.signature.parse_status !== "decoded",
|
||||
) ||
|
||||
resolvedMethods.some(
|
||||
(method) =>
|
||||
method.body.status === "partial" ||
|
||||
method.body.status === "malformed",
|
||||
);
|
||||
const related = edges(
|
||||
resolvedMethods,
|
||||
methods.methods,
|
||||
targets.members,
|
||||
managedDecodeBudget(layout),
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
decodeMetadataCodedIndex,
|
||||
metadataCodedIndexCoordinate,
|
||||
type MetadataCodedIndexCoordinate,
|
||||
type MetadataTokenFact,
|
||||
metadataToken,
|
||||
readMetadataString,
|
||||
sha256Bytes,
|
||||
@@ -92,19 +93,19 @@ export interface MemberCore {
|
||||
|
||||
export const memberTargetIdentity = (
|
||||
name: string,
|
||||
declaringToken: string | null,
|
||||
declaringType: Pick<MetadataTokenFact, "table" | "row"> | null,
|
||||
signature: ManagedSignature,
|
||||
typeName: DeclarationTypeName,
|
||||
): string | null => {
|
||||
if (
|
||||
declaringToken === null ||
|
||||
declaringType === null ||
|
||||
signature.resolved_sha256 === null ||
|
||||
signature.parse_status !== "decoded"
|
||||
)
|
||||
return null;
|
||||
const table = Number.parseInt(declaringToken.slice(2, 4), 16);
|
||||
const { table, row } = declaringType;
|
||||
if (table !== 1 && table !== 2) return null;
|
||||
const owner = typeName(table, Number.parseInt(declaringToken.slice(4), 16));
|
||||
const owner = typeName(table, row);
|
||||
return owner === null
|
||||
? null
|
||||
: createHash("sha256")
|
||||
@@ -120,6 +121,21 @@ export interface ParsedInstruction {
|
||||
readonly opcode: string;
|
||||
readonly operandKind: ManagedInstructionAnchor["operand_kind"];
|
||||
readonly operand: string | null;
|
||||
readonly reference: MetadataTokenFact | null;
|
||||
}
|
||||
|
||||
export type ParsedTokenInstruction = ParsedInstruction & {
|
||||
readonly reference: MetadataTokenFact;
|
||||
};
|
||||
|
||||
export const isParsedTokenInstruction = (
|
||||
instruction: ParsedInstruction,
|
||||
): instruction is ParsedTokenInstruction => instruction.reference !== null;
|
||||
|
||||
export interface DecodedMethod {
|
||||
readonly method: ManagedMethod;
|
||||
readonly tokenInstructions: readonly ParsedTokenInstruction[];
|
||||
readonly catchTokens: ReadonlyMap<number, MetadataTokenFact>;
|
||||
}
|
||||
|
||||
const rowRange = (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
metadataTokenFact,
|
||||
type MetadataTokenFact,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
import type { ManagedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
|
||||
import type {
|
||||
@@ -240,20 +244,14 @@ const OPCODES = new Map<
|
||||
[0xfe1e, { name: "readonly.", operand: "none" }],
|
||||
]);
|
||||
|
||||
const tokenKind = (token: string): ManagedInstructionAnchor["operand_kind"] => {
|
||||
if (token.startsWith("0x06") || token.startsWith("0x2b")) return "method";
|
||||
if (token.startsWith("0x04")) return "field";
|
||||
if (token.startsWith("0x70")) return "string";
|
||||
if (token.startsWith("0x11")) return "signature";
|
||||
// MemberRef rows can name either fields or methods; the table alone cannot
|
||||
// establish the kind. Callers with parsed metadata may supply that fact.
|
||||
if (token.startsWith("0x0a")) return "token";
|
||||
if (
|
||||
token.startsWith("0x01") ||
|
||||
token.startsWith("0x02") ||
|
||||
token.startsWith("0x1b")
|
||||
)
|
||||
return "type";
|
||||
const tokenKind = (table: number): ManagedInstructionAnchor["operand_kind"] => {
|
||||
if (table === 6 || table === 0x2b) return "method";
|
||||
if (table === 4) return "field";
|
||||
if (table === 0x70) return "string";
|
||||
if (table === 0x11) return "signature";
|
||||
// MemberRefs need their signature to distinguish methods from fields.
|
||||
if (table === 0x0a) return "token";
|
||||
if (table === 1 || table === 2 || table === 0x1b) return "type";
|
||||
return "token";
|
||||
};
|
||||
|
||||
@@ -261,6 +259,7 @@ type OperandResult = {
|
||||
readonly next: number;
|
||||
readonly anchorKind: ManagedInstructionAnchor["operand_kind"];
|
||||
readonly value: string | null;
|
||||
readonly reference?: MetadataTokenFact;
|
||||
};
|
||||
|
||||
type OperandReader = (il: Buffer, offset: number) => OperandResult;
|
||||
@@ -425,21 +424,22 @@ const readOperand = (
|
||||
}
|
||||
if (!TOKEN_OPERAND_KINDS.has(kind) && kind !== "token")
|
||||
return { next: offset, anchorKind: "unknown", value: null };
|
||||
const raw = il.readUInt32LE(offset);
|
||||
const token = `0x${raw.toString(16).padStart(8, "0")}`;
|
||||
const reference = metadataTokenFact(il.readUInt32LE(offset));
|
||||
const { token } = reference;
|
||||
return {
|
||||
next: offset + TOKEN_OPERAND_WIDTH,
|
||||
anchorKind:
|
||||
TOKEN_OPERAND_KINDS.get(kind) ??
|
||||
(kind === "token" ? memberRefKind?.(token) : undefined) ??
|
||||
tokenKind(token),
|
||||
tokenKind(reference.table),
|
||||
value: token,
|
||||
reference,
|
||||
};
|
||||
};
|
||||
|
||||
export const decodeInstructions = (
|
||||
il: Buffer,
|
||||
admitInstruction?: (offset: number) => void,
|
||||
admitInstruction?: (offset: number, hasReference: boolean) => void,
|
||||
memberRefKind?: (token: string) => "field" | "method" | undefined,
|
||||
): {
|
||||
readonly parsed: readonly ParsedInstruction[];
|
||||
@@ -476,15 +476,18 @@ export const decodeInstructions = (
|
||||
issue = `Instruction ${descriptor.name} at IL offset ${String(start)} leaves method body`;
|
||||
break;
|
||||
}
|
||||
// The extent is admitted; bound transient records before allocating token facts.
|
||||
admitInstruction?.(
|
||||
start,
|
||||
TOKEN_OPERAND_KINDS.has(descriptor.operand) ||
|
||||
descriptor.operand === "token",
|
||||
);
|
||||
const operand = readOperand(
|
||||
il,
|
||||
offset,
|
||||
descriptor.operand,
|
||||
memberRefKind,
|
||||
);
|
||||
// Charge each actual instruction before growing the decoded arrays.
|
||||
// A multi-byte operand must not consume a full record per raw IL byte.
|
||||
admitInstruction?.(start);
|
||||
offset = operand.next;
|
||||
if (PREFIX_OPCODES.has(descriptor.name)) {
|
||||
prefixStart ??= start;
|
||||
@@ -497,6 +500,7 @@ export const decodeInstructions = (
|
||||
opcode: descriptor.name,
|
||||
operandKind: operand.anchorKind,
|
||||
operand: operand.value,
|
||||
reference: operand.reference ?? null,
|
||||
});
|
||||
}
|
||||
} catch (cause: unknown) {
|
||||
@@ -517,6 +521,7 @@ export const parseExceptionRegions = (
|
||||
budget?: ManagedDecodeBudget,
|
||||
): ExceptionRegionParseResult => {
|
||||
const regions: ManagedExceptionRegion[] = [];
|
||||
const catchTokens = new Map<number, MetadataTokenFact>();
|
||||
let sectionOffset = offset;
|
||||
let hasMoreSections = true;
|
||||
while (hasMoreSections) {
|
||||
@@ -529,6 +534,7 @@ export const parseExceptionRegions = (
|
||||
return malformedExceptionRegions(
|
||||
"Exception section header leaves artifact",
|
||||
regions,
|
||||
catchTokens,
|
||||
);
|
||||
|
||||
const kind = bytes.readUInt8(sectionOffset);
|
||||
@@ -536,6 +542,7 @@ export const parseExceptionRegions = (
|
||||
return malformedExceptionRegions(
|
||||
`Unsupported method data section kind ${String(kind & 0x3f)}`,
|
||||
regions,
|
||||
catchTokens,
|
||||
);
|
||||
const fat = (kind & 0x40) !== 0;
|
||||
const size = fat
|
||||
@@ -552,6 +559,7 @@ export const parseExceptionRegions = (
|
||||
return malformedExceptionRegions(
|
||||
"Exception section size leaves artifact",
|
||||
regions,
|
||||
catchTokens,
|
||||
);
|
||||
let count: number;
|
||||
let sectionSize = size;
|
||||
@@ -560,6 +568,7 @@ export const parseExceptionRegions = (
|
||||
return malformedExceptionRegions(
|
||||
"Exception section size does not contain whole clauses",
|
||||
regions,
|
||||
catchTokens,
|
||||
);
|
||||
count = (size - 4) / clauseSize;
|
||||
} else if ((size - 4) % clauseSize === 0) {
|
||||
@@ -573,6 +582,7 @@ export const parseExceptionRegions = (
|
||||
return malformedExceptionRegions(
|
||||
"Exception section size does not contain whole clauses",
|
||||
regions,
|
||||
catchTokens,
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -582,6 +592,7 @@ export const parseExceptionRegions = (
|
||||
return malformedExceptionRegions(
|
||||
"Exception section size leaves artifact",
|
||||
regions,
|
||||
catchTokens,
|
||||
);
|
||||
if (
|
||||
!fat &&
|
||||
@@ -590,45 +601,64 @@ export const parseExceptionRegions = (
|
||||
return malformedExceptionRegions(
|
||||
"Small exception section reserved bytes are nonzero",
|
||||
regions,
|
||||
catchTokens,
|
||||
);
|
||||
|
||||
// Accumulates with the method IL bound; the body reserves what it retains.
|
||||
budget?.admitTransient(count * 256, sectionOffset);
|
||||
for (const region of readExceptionClauses(
|
||||
bytes,
|
||||
sectionOffset + 4,
|
||||
count,
|
||||
fat,
|
||||
{ start: sectionOffset + 4, count, fat, budget },
|
||||
catchTokens,
|
||||
regions.length,
|
||||
))
|
||||
regions.push(region);
|
||||
|
||||
hasMoreSections = (kind & 0x80) !== 0;
|
||||
sectionOffset += sectionSize;
|
||||
}
|
||||
return { status: "complete", regions };
|
||||
return { status: "complete", regions, catchTokens };
|
||||
};
|
||||
|
||||
export type ExceptionRegionParseResult =
|
||||
| {
|
||||
readonly status: "complete";
|
||||
readonly regions: ManagedExceptionRegion[];
|
||||
readonly catchTokens: ReadonlyMap<number, MetadataTokenFact>;
|
||||
}
|
||||
| {
|
||||
readonly status: "malformed";
|
||||
readonly regions: ManagedExceptionRegion[];
|
||||
readonly catchTokens: ReadonlyMap<number, MetadataTokenFact>;
|
||||
readonly issue: string;
|
||||
};
|
||||
|
||||
const malformedExceptionRegions = (
|
||||
issue: string,
|
||||
regions: ManagedExceptionRegion[] = [],
|
||||
): ExceptionRegionParseResult => ({ status: "malformed", regions, issue });
|
||||
regions: ManagedExceptionRegion[],
|
||||
catchTokens: ReadonlyMap<number, MetadataTokenFact>,
|
||||
): ExceptionRegionParseResult => ({
|
||||
status: "malformed",
|
||||
regions,
|
||||
catchTokens,
|
||||
issue,
|
||||
});
|
||||
|
||||
const readExceptionClauses = (
|
||||
bytes: Buffer,
|
||||
start: number,
|
||||
count: number,
|
||||
fat: boolean,
|
||||
{
|
||||
start,
|
||||
count,
|
||||
fat,
|
||||
budget,
|
||||
}: {
|
||||
readonly start: number;
|
||||
readonly count: number;
|
||||
readonly fat: boolean;
|
||||
readonly budget: ManagedDecodeBudget | undefined;
|
||||
},
|
||||
catchTokens: Map<number, MetadataTokenFact>,
|
||||
firstOrdinal: number,
|
||||
): ManagedExceptionRegion[] => {
|
||||
const regions: ManagedExceptionRegion[] = [];
|
||||
const clauseSize = fat ? 24 : 12;
|
||||
@@ -636,6 +666,9 @@ const readExceptionClauses = (
|
||||
const clause = start + index * clauseSize;
|
||||
const flags = fat ? bytes.readUInt32LE(clause) : bytes.readUInt16LE(clause);
|
||||
const extra = bytes.readUInt32LE(clause + (fat ? 20 : 8));
|
||||
if (flags === 0) budget?.admitTransient(256, clause);
|
||||
const reference = flags === 0 ? metadataTokenFact(extra) : null;
|
||||
if (reference !== null) catchTokens.set(firstOrdinal + index, reference);
|
||||
regions.push({
|
||||
flags,
|
||||
try_offset: fat
|
||||
@@ -650,8 +683,7 @@ const readExceptionClauses = (
|
||||
handler_length: fat
|
||||
? bytes.readUInt32LE(clause + 16)
|
||||
: bytes.readUInt8(clause + 7),
|
||||
class_token:
|
||||
flags === 0 ? `0x${extra.toString(16).padStart(8, "0")}` : null,
|
||||
class_token: reference?.token ?? null,
|
||||
class_type: null,
|
||||
class_type_name_sha256: null,
|
||||
class_type_identity_resolved: null,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type ManagedFieldAccess,
|
||||
type ManagedMemberRef,
|
||||
type ManagedMethod,
|
||||
type DecodedMethod,
|
||||
type MemberCore,
|
||||
type SignatureTypeName,
|
||||
type DeclarationTypeName,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
import {
|
||||
metadataToken,
|
||||
type MetadataTokenFact,
|
||||
metadataRowCursor,
|
||||
decodeMetadataCodedIndex,
|
||||
readMetadataBlob,
|
||||
@@ -121,7 +123,7 @@ export const parseFields = (
|
||||
kind: "field",
|
||||
identitySha256: memberTargetIdentity(
|
||||
name,
|
||||
declared?.token ?? null,
|
||||
declared === undefined ? null : { table: 2, row: owner },
|
||||
decoded,
|
||||
declarationTypeName,
|
||||
),
|
||||
@@ -215,7 +217,9 @@ export const parseMemberRefs = (
|
||||
signatureTypeName,
|
||||
declarationTypeName,
|
||||
}: {
|
||||
readonly typeName: (token: string | null) => string | null;
|
||||
readonly typeName: (
|
||||
reference: Pick<MetadataTokenFact, "table" | "row" | "token"> | null,
|
||||
) => string | null;
|
||||
readonly signatureTypeName: SignatureTypeName;
|
||||
readonly declarationTypeName: DeclarationTypeName;
|
||||
},
|
||||
@@ -264,7 +268,7 @@ export const parseMemberRefs = (
|
||||
: `MemberRef parent coded index 0x${parentRaw.toString(16)} is invalid: ${parent.reason}`,
|
||||
});
|
||||
const parentToken = parent.kind === "valid" ? parent.token : null;
|
||||
const parentName = typeName(parentToken);
|
||||
const parentName = typeName(parent.kind === "valid" ? parent : null);
|
||||
const decoded = budgetedSignature(
|
||||
sig,
|
||||
layout,
|
||||
@@ -288,7 +292,7 @@ export const parseMemberRefs = (
|
||||
genericArity: signatureGenericArity(decoded),
|
||||
identitySha256: memberTargetIdentity(
|
||||
name,
|
||||
parentToken,
|
||||
parent.kind === "valid" ? parent : null,
|
||||
decoded,
|
||||
declarationTypeName,
|
||||
),
|
||||
@@ -325,7 +329,7 @@ export const parseMethods = ({
|
||||
methodOffset = 0,
|
||||
methodLimit = Number.MAX_SAFE_INTEGER,
|
||||
}: ParseMethodsInput): {
|
||||
readonly methods: readonly ManagedMethod[];
|
||||
readonly methods: readonly DecodedMethod[];
|
||||
readonly core: ReadonlyMap<string, MemberCore>;
|
||||
readonly issues: readonly ManagedParseIssue[];
|
||||
/**
|
||||
@@ -334,7 +338,7 @@ export const parseMethods = ({
|
||||
*/
|
||||
readonly unscannedMethods: number;
|
||||
} => {
|
||||
const methods: ManagedMethod[] = [];
|
||||
const methods: DecodedMethod[] = [];
|
||||
const core = new Map<string, MemberCore>();
|
||||
const issues: ManagedParseIssue[] = [];
|
||||
let unscannedMethods = 0;
|
||||
@@ -404,7 +408,9 @@ export const parseMethods = ({
|
||||
genericArity: signatureGenericArity(decoded),
|
||||
identitySha256: memberTargetIdentity(
|
||||
name,
|
||||
declared?.token ?? null,
|
||||
declared === undefined || owner === undefined
|
||||
? null
|
||||
: { table: 2, row: owner },
|
||||
decoded,
|
||||
declarationTypeName,
|
||||
),
|
||||
@@ -418,17 +424,22 @@ export const parseMethods = ({
|
||||
});
|
||||
appendManagedParseIssues(issues, read.issues);
|
||||
if (!read.scanned) unscannedMethods += 1;
|
||||
managedDecodeBudget(layout).reserve(128, rowOffset);
|
||||
methods.push({
|
||||
token,
|
||||
row_offset: rowOffset,
|
||||
declaring_type_token: declared?.token ?? null,
|
||||
declaring_type: declared?.fullName ?? null,
|
||||
name,
|
||||
rva,
|
||||
impl_flags: implFlags,
|
||||
flags,
|
||||
signature: decoded,
|
||||
body: read.body,
|
||||
tokenInstructions: read.tokenInstructions,
|
||||
catchTokens: read.catchTokens,
|
||||
method: {
|
||||
token,
|
||||
row_offset: rowOffset,
|
||||
declaring_type_token: declared?.token ?? null,
|
||||
declaring_type: declared?.fullName ?? null,
|
||||
name,
|
||||
rva,
|
||||
impl_flags: implFlags,
|
||||
flags,
|
||||
signature: decoded,
|
||||
body: read.body,
|
||||
},
|
||||
});
|
||||
}, issues);
|
||||
if (mayBeSelected && methods.length === produced) unscannedMethods += 1;
|
||||
@@ -445,12 +456,12 @@ const NAMEABLE_KINDS = new Map<string, ReadonlySet<MemberCore["kind"]>>([
|
||||
["field", new Set(["field"])],
|
||||
]);
|
||||
|
||||
const targetKind = (token: string): ManagedCallEdge["target_kind"] =>
|
||||
token.startsWith("0x06")
|
||||
const targetKind = (table: number): ManagedCallEdge["target_kind"] =>
|
||||
table === 6
|
||||
? "method-def"
|
||||
: token.startsWith("0x0a")
|
||||
: table === 0x0a
|
||||
? "member-ref"
|
||||
: token.startsWith("0x2b")
|
||||
: table === 0x2b
|
||||
? "method-spec"
|
||||
: "unknown";
|
||||
|
||||
@@ -468,7 +479,7 @@ const namedTarget = (
|
||||
};
|
||||
|
||||
export const edges = (
|
||||
methods: readonly ManagedMethod[],
|
||||
methods: readonly DecodedMethod[],
|
||||
members: ReadonlyMap<string, MemberCore>,
|
||||
budget: ManagedDecodeBudget,
|
||||
): {
|
||||
@@ -477,14 +488,23 @@ export const edges = (
|
||||
} => {
|
||||
const callEdges: ManagedCallEdge[] = [];
|
||||
const fieldAccesses: ManagedFieldAccess[] = [];
|
||||
for (const method of methods) {
|
||||
const derives = method.body.anchors.some(
|
||||
isManagedMemberReferenceInstruction,
|
||||
for (const { method, tokenInstructions } of methods) {
|
||||
const derives = tokenInstructions.some((instruction) =>
|
||||
isManagedMemberReferenceInstruction({
|
||||
opcode: instruction.opcode,
|
||||
operand_kind: instruction.operandKind,
|
||||
operand: instruction.operand,
|
||||
}),
|
||||
);
|
||||
if (derives) budget.admitTransient(callerNameBytes(method), null);
|
||||
// Built once per method, only after its exact size was admitted.
|
||||
const caller = derives ? callerName(method) : method.name;
|
||||
for (const anchor of method.body.anchors) {
|
||||
for (const instruction of tokenInstructions) {
|
||||
const anchor = {
|
||||
opcode: instruction.opcode,
|
||||
operand_kind: instruction.operandKind,
|
||||
operand: instruction.operand,
|
||||
};
|
||||
if (!isManagedMemberReferenceInstruction(anchor)) continue;
|
||||
const { operand } = anchor;
|
||||
const target = namedTarget(members, anchor.operand_kind, operand);
|
||||
@@ -494,12 +514,12 @@ export const edges = (
|
||||
admitManagedProjection(
|
||||
{
|
||||
caller_token: method.token,
|
||||
il_offset: anchor.il_offset,
|
||||
il_offset: instruction.offset,
|
||||
target_identity_sha256: target?.identitySha256 ?? null,
|
||||
caller,
|
||||
opcode: anchor.opcode,
|
||||
target_token: operand,
|
||||
target_kind: targetKind(operand),
|
||||
target_kind: targetKind(instruction.reference.table),
|
||||
target_name: target?.name ?? null,
|
||||
target_declaring_type: target?.declaringType ?? null,
|
||||
},
|
||||
@@ -512,7 +532,7 @@ export const edges = (
|
||||
admitManagedProjection(
|
||||
{
|
||||
method_token: method.token,
|
||||
il_offset: anchor.il_offset,
|
||||
il_offset: instruction.offset,
|
||||
target_identity_sha256: target?.identitySha256 ?? null,
|
||||
method: caller,
|
||||
opcode: anchor.opcode,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
readMetadataBlob,
|
||||
metadataTokenFact,
|
||||
readMetadataString,
|
||||
readMetadataUserString,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
@@ -97,7 +98,7 @@ describe("managed metadata heaps", () => {
|
||||
} as unknown as ManagedMetadataLayout;
|
||||
|
||||
expect(() =>
|
||||
readMetadataUserString(bytes, layout, "0x70000001"),
|
||||
readMetadataUserString(bytes, layout, metadataTokenFact(0x70000001)),
|
||||
).toThrowError(
|
||||
expect.objectContaining({
|
||||
issue: expect.objectContaining({
|
||||
|
||||
@@ -235,13 +235,14 @@ export const readMetadataBlob = (
|
||||
export const readMetadataUserString = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
token: string,
|
||||
reference: Pick<MetadataTokenFact, "token" | "row">,
|
||||
): {
|
||||
readonly offset: number;
|
||||
readonly value: string;
|
||||
/** Conformance differences observed beside the readable literal. */
|
||||
readonly issues: readonly ManagedParseIssue[];
|
||||
} => {
|
||||
const { token, row: index } = reference;
|
||||
const heap = layout.userStrings;
|
||||
const scope = `metadata.#US:${token}`;
|
||||
if (heap === null)
|
||||
@@ -250,7 +251,6 @@ export const readMetadataUserString = (
|
||||
scope,
|
||||
"ldstr references #US, but the metadata has no #US stream",
|
||||
);
|
||||
const index = Number.parseInt(token.slice(4), 16);
|
||||
if (index >= heap.size)
|
||||
throw managedFailure(
|
||||
"invalid-heap-index",
|
||||
@@ -378,6 +378,20 @@ export const readMetadataGuid = (
|
||||
export const metadataToken = (table: number, row: number): string =>
|
||||
`0x${(table * 0x01_00_00_00 + row).toString(16).padStart(8, "0")}`;
|
||||
|
||||
/** Full-token coordinates do not establish that a table row or heap item exists. */
|
||||
export interface MetadataTokenFact {
|
||||
readonly raw: number;
|
||||
readonly table: number;
|
||||
readonly row: number;
|
||||
readonly token: string;
|
||||
}
|
||||
|
||||
export const metadataTokenFact = (raw: number): MetadataTokenFact => {
|
||||
const table = raw >>> 24;
|
||||
const row = raw & 0x00ff_ffff;
|
||||
return { raw, table, row, token: metadataToken(table, row) };
|
||||
};
|
||||
|
||||
/** Hash bytes without exposing their contents. */
|
||||
export const sha256Bytes = (bytes: Buffer): string =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
@@ -3,7 +3,10 @@ import {
|
||||
type ManagedDecodeBudget,
|
||||
} from "./ManagedDecodeBudget.js";
|
||||
import type { ManagedPeLayout } from "./ManagedPeReader.js";
|
||||
import type { ManagedMethodBody } from "./ManagedMemberInspectorCore.js";
|
||||
import type {
|
||||
ManagedMethodBody,
|
||||
ParsedTokenInstruction,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
import {
|
||||
decodeInstructions,
|
||||
parseExceptionRegions,
|
||||
@@ -11,14 +14,21 @@ import {
|
||||
import { validateExceptionRegionRanges } from "./ManagedExceptionRegionValidation.js";
|
||||
import {
|
||||
metadataRowCursor,
|
||||
metadataTokenFact,
|
||||
type MetadataTokenFact,
|
||||
readMetadataBlob,
|
||||
sha256Bytes,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
import { validateLocalSignature } from "./ManagedMemberInspectorCore.js";
|
||||
import {
|
||||
validateLocalSignature,
|
||||
isParsedTokenInstruction,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
|
||||
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
|
||||
|
||||
const EMPTY_CATCH_TOKENS: ReadonlyMap<number, MetadataTokenFact> = new Map();
|
||||
|
||||
const bodyStatus = (
|
||||
decodedIssue: string | null,
|
||||
truncatedInstructions: number,
|
||||
@@ -92,6 +102,8 @@ export const methodBody = (
|
||||
context: MethodBodyContext,
|
||||
): {
|
||||
readonly body: ManagedMethodBody;
|
||||
readonly tokenInstructions: readonly ParsedTokenInstruction[];
|
||||
readonly catchTokens: ReadonlyMap<number, MetadataTokenFact>;
|
||||
readonly issues: readonly ManagedParseIssue[];
|
||||
/**
|
||||
* Whether every CIL instruction was decoded, independent of exception-region
|
||||
@@ -104,6 +116,8 @@ export const methodBody = (
|
||||
if (rva === 0)
|
||||
return {
|
||||
body: emptyMethodBody(rva, "absent", null),
|
||||
tokenInstructions: [],
|
||||
catchTokens: EMPTY_CATCH_TOKENS,
|
||||
scanned: true,
|
||||
issues,
|
||||
};
|
||||
@@ -115,6 +129,8 @@ export const methodBody = (
|
||||
"CIL decoding is unavailable for native, runtime, unmanaged, or P/Invoke implementations";
|
||||
return {
|
||||
body: emptyMethodBody(rva, "partial", issue),
|
||||
tokenInstructions: [],
|
||||
catchTokens: EMPTY_CATCH_TOKENS,
|
||||
scanned: true,
|
||||
issues,
|
||||
};
|
||||
@@ -137,7 +153,10 @@ export const methodBody = (
|
||||
// separate instruction for every byte. Retained output is charged below.
|
||||
const decoded = decodeInstructions(
|
||||
il,
|
||||
(start) => context.budget.admitTransient(256, ilOffset + start),
|
||||
(start, hasReference) => {
|
||||
context.budget.admitTransient(256, ilOffset + start);
|
||||
if (hasReference) context.budget.admitTransient(256, ilOffset + start);
|
||||
},
|
||||
(token) => context.memberRefKinds.get(token),
|
||||
);
|
||||
const opcodeCounts: Record<string, number> = {};
|
||||
@@ -179,12 +198,13 @@ export const methodBody = (
|
||||
: null;
|
||||
let localSigIssue: string | null = null;
|
||||
let localSigFailure: "malformed" | "partial" | null = null;
|
||||
if (header.localSig !== 0) {
|
||||
const table = header.localSig >>> 24;
|
||||
const row = header.localSig & 0x00ff_ffff;
|
||||
const localSignature =
|
||||
header.localSig === 0 ? null : metadataTokenFact(header.localSig);
|
||||
if (localSignature !== null) {
|
||||
const { table, row, token } = localSignature;
|
||||
const rows = context.metadataLayout.table(17)?.rowCount ?? 0;
|
||||
if (table !== 17 || row === 0 || row > rows) {
|
||||
localSigIssue = `LocalVarSigTok 0x${header.localSig.toString(16).padStart(8, "0")} does not name an existing StandAloneSig row`;
|
||||
localSigIssue = `LocalVarSigTok ${token} does not name an existing StandAloneSig row`;
|
||||
localSigFailure = "malformed";
|
||||
issues.push({
|
||||
code: "invalid-row",
|
||||
@@ -193,7 +213,7 @@ export const methodBody = (
|
||||
detail: localSigIssue,
|
||||
});
|
||||
} else {
|
||||
const signatureScope = `metadata.StandAloneSig:0x${header.localSig.toString(16).padStart(8, "0")}.signature`;
|
||||
const signatureScope = `metadata.StandAloneSig:${token}.signature`;
|
||||
try {
|
||||
const cursor = metadataRowCursor(
|
||||
bytes,
|
||||
@@ -247,10 +267,7 @@ export const methodBody = (
|
||||
file_offset: offset,
|
||||
max_stack: header.maxStack,
|
||||
init_locals: (header.flags & 0x10) !== 0,
|
||||
local_var_sig_token:
|
||||
header.localSig === 0
|
||||
? null
|
||||
: `0x${header.localSig.toString(16).padStart(8, "0")}`,
|
||||
local_var_sig_token: localSignature?.token ?? null,
|
||||
il_size: header.ilSize,
|
||||
il_sha256: sha256Bytes(il),
|
||||
normalized_il_sha256:
|
||||
@@ -271,8 +288,26 @@ export const methodBody = (
|
||||
? "Instruction decoding stopped before the end of the method body"
|
||||
: null),
|
||||
};
|
||||
const admittedBody = admitManagedProjection(body, context.budget);
|
||||
const tokenInstructions: ParsedTokenInstruction[] = [];
|
||||
// These records outlive transient decoding; the public projection contains only strings.
|
||||
for (const instruction of decoded.parsed) {
|
||||
if (!isParsedTokenInstruction(instruction)) continue;
|
||||
context.budget.reserve(
|
||||
256 + (tokenInstructions.length === 0 ? 64 : 0),
|
||||
ilOffset + instruction.offset,
|
||||
);
|
||||
tokenInstructions.push(instruction);
|
||||
}
|
||||
const catchTokens = exceptionRegions?.catchTokens.size
|
||||
? exceptionRegions.catchTokens
|
||||
: EMPTY_CATCH_TOKENS;
|
||||
if (catchTokens.size > 0)
|
||||
context.budget.reserve(64 + catchTokens.size * 256, sectionOffset);
|
||||
return {
|
||||
body: admitManagedProjection(body, context.budget),
|
||||
body: admittedBody,
|
||||
tokenInstructions,
|
||||
catchTokens,
|
||||
scanned: decoded.issue === null && decoded.truncated === 0,
|
||||
issues,
|
||||
};
|
||||
@@ -293,6 +328,8 @@ export const methodBody = (
|
||||
"malformed",
|
||||
cause instanceof Error ? cause.message : "Method body parse failed",
|
||||
),
|
||||
tokenInstructions: [],
|
||||
catchTokens: EMPTY_CATCH_TOKENS,
|
||||
scanned: false,
|
||||
issues,
|
||||
};
|
||||
|
||||
@@ -5,12 +5,13 @@ import {
|
||||
methodGenericArity,
|
||||
specSignatureFacts,
|
||||
type SignatureTypeName,
|
||||
type ManagedMethod,
|
||||
type DecodedMethod,
|
||||
type MemberCore,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
decodeMetadataCodedIndex,
|
||||
type MetadataTokenFact,
|
||||
metadataRowCursor,
|
||||
readMetadataBlob,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
@@ -24,7 +25,7 @@ import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
|
||||
export const edgeTargets = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
methods: readonly ManagedMethod[],
|
||||
methods: readonly DecodedMethod[],
|
||||
cores: readonly ReadonlyMap<string, MemberCore>[],
|
||||
signatureTypeName: SignatureTypeName,
|
||||
): {
|
||||
@@ -36,15 +37,15 @@ export const edgeTargets = (
|
||||
const seen = new Set<string>();
|
||||
// Generic arity by method token, so instantiations of one method decode it once.
|
||||
const arities = new Map<string, number | null>();
|
||||
for (const method of methods)
|
||||
for (const { operand } of method.body.anchors) {
|
||||
if (operand?.startsWith("0x2b") !== true || seen.has(operand)) continue;
|
||||
seen.add(operand);
|
||||
for (const { tokenInstructions } of methods)
|
||||
for (const { reference } of tokenInstructions) {
|
||||
if (reference.table !== 0x2b || seen.has(reference.token)) continue;
|
||||
seen.add(reference.token);
|
||||
readManagedValue(
|
||||
() =>
|
||||
readMethodSpec(
|
||||
{ bytes, layout, members, issues, arities, signatureTypeName },
|
||||
operand,
|
||||
reference,
|
||||
),
|
||||
issues,
|
||||
);
|
||||
@@ -73,15 +74,11 @@ const readMethodSpec = (
|
||||
readonly arities: Map<string, number | null>;
|
||||
readonly signatureTypeName: SignatureTypeName;
|
||||
},
|
||||
token: string,
|
||||
reference: MetadataTokenFact,
|
||||
): void => {
|
||||
const { token, row } = reference;
|
||||
const scope = `metadata.MethodSpec:${token}`;
|
||||
const cursor = metadataRowCursor(
|
||||
bytes,
|
||||
layout,
|
||||
0x2b,
|
||||
Number.parseInt(token.slice(4), 16),
|
||||
);
|
||||
const cursor = metadataRowCursor(bytes, layout, 0x2b, row);
|
||||
const methodRaw = cursor.readIndex(layout.codedIndexSize("MethodDefOrRef"));
|
||||
const instantiation = cursor.readIndex(layout.blobIndexSize);
|
||||
const method = decodeMetadataCodedIndex(
|
||||
@@ -100,8 +97,9 @@ const readMethodSpec = (
|
||||
? "MethodSpec method coded index 0x0 is null, but the Method column must reference a row"
|
||||
: `MethodSpec method coded index 0x${methodRaw.toString(16)} is invalid: ${method.reason}`,
|
||||
});
|
||||
const generic = method.kind === "valid" ? method.token : null;
|
||||
const target = generic === null ? undefined : members.get(generic);
|
||||
const methodReference = method.kind === "valid" ? method : null;
|
||||
const target =
|
||||
methodReference === null ? undefined : members.get(methodReference.token);
|
||||
// A MethodSpec instantiates a method, so a field target never lends it a
|
||||
// name, even to a field operand.
|
||||
if (target !== undefined && target.kind !== "field")
|
||||
@@ -113,13 +111,17 @@ const readMethodSpec = (
|
||||
signatureTypeName,
|
||||
);
|
||||
const { types } = instantiationFacts;
|
||||
if (generic === null) return;
|
||||
if (methodReference === null) return;
|
||||
const generic = methodReference.token;
|
||||
// Prefer the arity the target's owner decoded; read the raw signature only
|
||||
// for a target nothing decoded, such as a MethodDef outside the selection.
|
||||
if (target?.genericArity === undefined && !arities.has(generic))
|
||||
arities.set(
|
||||
generic,
|
||||
methodGenericArity(layout, methodSignatureBlob(bytes, layout, generic)),
|
||||
methodGenericArity(
|
||||
layout,
|
||||
methodSignatureBlob(bytes, layout, methodReference),
|
||||
),
|
||||
);
|
||||
const arity =
|
||||
target?.genericArity === undefined
|
||||
@@ -155,10 +157,8 @@ const readMethodSpec = (
|
||||
const methodSignatureBlob = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
token: string,
|
||||
{ table, row }: Pick<MetadataTokenFact, "table" | "row">,
|
||||
): Buffer => {
|
||||
const table = Number.parseInt(token.slice(2, 4), 16);
|
||||
const row = Number.parseInt(token.slice(4), 16);
|
||||
let signatureIndex: number;
|
||||
if (table === 6) {
|
||||
signatureIndex = readMethodDefinitionRow(bytes, layout, row).signatureIndex;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
metadataRowCursor,
|
||||
type MetadataTokenFact,
|
||||
metadataToken,
|
||||
readMetadataBlob,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
@@ -29,7 +30,9 @@ export const typeNameLookup = (
|
||||
ranges: readonly TypeRange[],
|
||||
issues: ManagedParseIssue[],
|
||||
references: ManagedTypeReferenceLookup,
|
||||
): ((token: string | null) => string | null) => {
|
||||
): ((
|
||||
reference: Pick<MetadataTokenFact, "table" | "row" | "token"> | null,
|
||||
) => string | null) => {
|
||||
const names = new Map<string, string | null>();
|
||||
const definitionName = (table: 1 | 2, row: number): string | null => {
|
||||
if (table === 2) {
|
||||
@@ -59,11 +62,10 @@ export const typeNameLookup = (
|
||||
? null
|
||||
: definitionName(nominalGenericType.table, nominalGenericType.row);
|
||||
};
|
||||
return (token) => {
|
||||
if (token === null) return null;
|
||||
return (reference) => {
|
||||
if (reference === null) return null;
|
||||
const { token, table, row } = reference;
|
||||
if (names.has(token)) return names.get(token) ?? null;
|
||||
const table = Number.parseInt(token.slice(2, 4), 16);
|
||||
const row = Number.parseInt(token.slice(4), 16);
|
||||
const name =
|
||||
readManagedValue(
|
||||
() =>
|
||||
|
||||
@@ -7,11 +7,14 @@ import type {
|
||||
ManagedParseIssue,
|
||||
} from "../domain/managed/managedArtifact.js";
|
||||
import type {
|
||||
ManagedMethod,
|
||||
DecodedMethod,
|
||||
ManagedUserString,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
import { readMetadataUserString } from "./ManagedMetadataHeaps.js";
|
||||
import {
|
||||
readMetadataUserString,
|
||||
type MetadataTokenFact,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
import { readManagedValue } from "./ManagedReaderFailure.js";
|
||||
|
||||
type UserStringInventory = NonNullable<ManagedMemberInspection["user_strings"]>;
|
||||
@@ -40,7 +43,7 @@ export const parseUserStrings = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
parsed: {
|
||||
readonly methods: readonly ManagedMethod[];
|
||||
readonly methods: readonly DecodedMethod[];
|
||||
/** Methods that may be in scope but whose CIL was not fully scanned. */
|
||||
readonly unscannedMethods: number;
|
||||
/** Unreadable selected type names may hide a requested type. */
|
||||
@@ -54,27 +57,27 @@ export const parseUserStrings = (
|
||||
(parsed.unreadableSelectedTypes ?? 0) === 0;
|
||||
const userStrings: ManagedUserString[] = [];
|
||||
const issues: ManagedParseIssue[] = [];
|
||||
const tokens = new Set<string>();
|
||||
const tokens = new Map<number, MetadataTokenFact>();
|
||||
try {
|
||||
// Only ldstr loads a literal; a malformed ldtoken can carry a 0x70 token.
|
||||
for (const method of methods)
|
||||
for (const { opcode, operand } of method.body.anchors)
|
||||
if (opcode === "ldstr" && operand !== null && !tokens.has(operand)) {
|
||||
for (const { tokenInstructions } of methods)
|
||||
for (const { opcode, reference } of tokenInstructions)
|
||||
if (opcode === "ldstr" && !tokens.has(reference.raw)) {
|
||||
budget.admitTransient(USER_STRING_TOKEN_BYTES, null);
|
||||
tokens.add(operand);
|
||||
tokens.set(reference.raw, reference);
|
||||
}
|
||||
// Tokens are fixed-width lowercase hex, so string order is token order.
|
||||
const references: string[] = [];
|
||||
for (const token of [...tokens].sort()) {
|
||||
if (token.startsWith("0x70")) references.push(token);
|
||||
// An operand from another table names no #US item, with or without
|
||||
// the stream.
|
||||
// Unsigned token order is the same as their fixed-width lowercase hex order.
|
||||
const references: MetadataTokenFact[] = [];
|
||||
for (const reference of [...tokens.values()].sort(
|
||||
(left, right) => left.raw - right.raw,
|
||||
)) {
|
||||
if (reference.table === 0x70) references.push(reference);
|
||||
else
|
||||
issues.push(
|
||||
admitManagedProjection(
|
||||
{
|
||||
code: "invalid-heap-index",
|
||||
scope: `metadata.#US:${token}`,
|
||||
scope: `metadata.#US:${reference.token}`,
|
||||
offset: null,
|
||||
detail: "ldstr operand does not name an item inside #US",
|
||||
},
|
||||
@@ -97,17 +100,20 @@ export const parseUserStrings = (
|
||||
);
|
||||
return inventory(userStrings, issues, scanned);
|
||||
}
|
||||
for (const token of references) {
|
||||
for (const reference of references) {
|
||||
const failures: ManagedParseIssue[] = [];
|
||||
readManagedValue(() => {
|
||||
const {
|
||||
offset,
|
||||
value,
|
||||
issues: observed,
|
||||
} = readMetadataUserString(bytes, layout, token);
|
||||
} = readMetadataUserString(bytes, layout, reference);
|
||||
for (const issue of observed) failures.push(issue);
|
||||
userStrings.push(
|
||||
admitManagedProjection({ token, offset, value }, budget),
|
||||
admitManagedProjection(
|
||||
{ token: reference.token, offset, value },
|
||||
budget,
|
||||
),
|
||||
);
|
||||
}, failures);
|
||||
for (const failure of failures)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -53,6 +54,85 @@ cliTest(
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"managed CLI inspects malformed MethodSpecs used by isolated non-call instructions",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-managed-methodspec-operands-",
|
||||
);
|
||||
for (const [opcode, name, operandKind] of [
|
||||
[0x71, "ldobj", "type"],
|
||||
[0x7e, "ldsfld", "field"],
|
||||
[0xd0, "ldtoken", "method"],
|
||||
] as const) {
|
||||
const path = join(root, `${name}.dll`);
|
||||
// Each body has one token operand; no call can trigger MethodSpec
|
||||
// inspection on behalf of the non-call instruction under observation.
|
||||
const il = Buffer.from([opcode, 1, 0, 0, 0x2b, 0x2a]);
|
||||
await writeFile(
|
||||
path,
|
||||
buildManagedPeFixture({
|
||||
memberRefName: "NotAMethod",
|
||||
memberRefSignature: Buffer.from([0x06, 0x0e]),
|
||||
methodSpecMethods: [(1 << 1) | 1],
|
||||
ilBody: Buffer.concat([Buffer.from([(il.length << 2) | 2]), il]),
|
||||
}),
|
||||
);
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-managed-members", path, "--json"],
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(result.json).toMatchObject({
|
||||
operation: "inspect_managed_members",
|
||||
subject: { local_path: path },
|
||||
normalized_result: {
|
||||
methods: [
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
status: "present",
|
||||
il_sha256: createHash("sha256").update(il).digest("hex"),
|
||||
normalized_il_sha256: expect.any(String),
|
||||
anchors: [
|
||||
expect.objectContaining({
|
||||
opcode: name,
|
||||
operand_kind: operandKind,
|
||||
operand: "0x2b000001",
|
||||
}),
|
||||
expect.objectContaining({ opcode: "ret", operand: null }),
|
||||
],
|
||||
issue: null,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
call_edges: [],
|
||||
field_accesses:
|
||||
name === "ldsfld"
|
||||
? [
|
||||
expect.objectContaining({
|
||||
opcode: "ldsfld",
|
||||
field_token: "0x2b000001",
|
||||
field_name: null,
|
||||
field_declaring_type: null,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
coverage: {
|
||||
state: "partial",
|
||||
issues: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "invalid-row",
|
||||
scope: "metadata.MethodSpec:0x2b000001",
|
||||
detail:
|
||||
"MethodSpec method does not have a decodable method signature",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"managed CLI retains metadata and CIL after repeated local constructor owner lookups",
|
||||
async ({ cli }) => {
|
||||
|
||||
@@ -775,6 +775,25 @@ describe("managed comparison post-admission read failures", () => {
|
||||
});
|
||||
|
||||
describe("managed comparison snapshot admission and cleanup", () => {
|
||||
const snapshotReaderWithClose =
|
||||
(
|
||||
closeHandle: (owner: OwnedFileHandle) => Promise<void>,
|
||||
): typeof readManagedSnapshot =>
|
||||
async (path, operation, options) => {
|
||||
// Target resolution owns separate handles. Inject only while the real
|
||||
// snapshot reader owns the descriptor whose retry this test observes.
|
||||
const close = vi
|
||||
.spyOn(OwnedFileHandle.prototype, "close")
|
||||
.mockImplementation(function (this: OwnedFileHandle) {
|
||||
return closeHandle(this);
|
||||
});
|
||||
try {
|
||||
return await readManagedSnapshot(path, operation, options);
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
}
|
||||
};
|
||||
|
||||
it("rejects a sparse oversized snapshot through the default reader before reading it", async () => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-managed-oversized-snapshot-",
|
||||
@@ -825,26 +844,28 @@ describe("managed comparison snapshot admission and cleanup", () => {
|
||||
const originalClose = OwnedFileHandle.prototype.close;
|
||||
const owners: OwnedFileHandle[] = [];
|
||||
let attempts = 0;
|
||||
const close = vi
|
||||
.spyOn(OwnedFileHandle.prototype, "close")
|
||||
.mockImplementation(async function (this: OwnedFileHandle) {
|
||||
owners.push(this);
|
||||
attempts += 1;
|
||||
if (attempts === 1) throw new Error("injected transient close failure");
|
||||
await originalClose.call(this);
|
||||
});
|
||||
try {
|
||||
const result = await compareManagedMemberPaths(
|
||||
{ leftPath, rightPath },
|
||||
{},
|
||||
{
|
||||
readSnapshot: snapshotReaderWithClose(async (owner) => {
|
||||
owners.push(owner);
|
||||
attempts += 1;
|
||||
if (attempts === 1)
|
||||
throw new Error("injected transient close failure");
|
||||
await originalClose.call(owner);
|
||||
}),
|
||||
},
|
||||
resources,
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(owners[0]).toBe(owners[1]);
|
||||
expect(attempts).toBe(3);
|
||||
expect(owners[2]).not.toBe(owners[0]);
|
||||
expect(owners.map((owner) => owner.handle.fd)).toEqual([-1, -1, -1]);
|
||||
expect(resources.hasPending).toBe(false);
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
await resources.close();
|
||||
}
|
||||
});
|
||||
@@ -860,17 +881,16 @@ describe("managed comparison snapshot admission and cleanup", () => {
|
||||
const resources = new ArtifactResourceScope();
|
||||
const cleanupCause = new Error("injected persistent close failure");
|
||||
const owners: OwnedFileHandle[] = [];
|
||||
const close = vi
|
||||
.spyOn(OwnedFileHandle.prototype, "close")
|
||||
.mockImplementation(async function (this: OwnedFileHandle) {
|
||||
owners.push(this);
|
||||
throw cleanupCause;
|
||||
});
|
||||
let result: Awaited<ReturnType<typeof compareManagedMemberPaths>>;
|
||||
try {
|
||||
result = await compareManagedMemberPaths(
|
||||
{ leftPath, rightPath },
|
||||
{},
|
||||
{
|
||||
readSnapshot: snapshotReaderWithClose(async (owner) => {
|
||||
owners.push(owner);
|
||||
throw cleanupCause;
|
||||
}),
|
||||
},
|
||||
resources,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
@@ -879,12 +899,16 @@ describe("managed comparison snapshot admission and cleanup", () => {
|
||||
code: "cleanup_incomplete",
|
||||
details: { cleanup: "incomplete", resources: [leftPath] },
|
||||
});
|
||||
expect(owners).toHaveLength(2);
|
||||
expect(owners[0]).toBe(owners[1]);
|
||||
expect(owners[0]?.handle.fd).toBeGreaterThanOrEqual(0);
|
||||
expect(resources.hasPending).toBe(true);
|
||||
expect(resources.cleanupResources).toEqual([leftPath]);
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
await resources.close();
|
||||
}
|
||||
expect(owners[0]?.handle.fd).toBe(-1);
|
||||
expect(resources.hasPending).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user