Files
rea/src/dotnet/ManagedMethodBodyReader.ts
T

382 lines
13 KiB
TypeScript

import type { ManagedMethodImplementationFlags } from "./ManagedMethodDefinitionRows.js";
import {
admitManagedProjection,
type ManagedDecodeBudget,
} from "./ManagedDecodeBudget.js";
import type { ManagedPeLayout } from "./ManagedPeReader.js";
import type {
ManagedMethodBody,
ParsedTokenInstruction,
} from "./ManagedMemberInspectorCore.js";
import {
decodeInstructions,
parseExceptionRegions,
} from "./ManagedMemberInstructionDecoder.js";
import { validateExceptionRegionRanges } from "./ManagedExceptionRegionValidation.js";
import {
metadataRowCursor,
metadataTokenFact,
type MetadataTokenFact,
readMetadataBlob,
sha256Bytes,
} from "./ManagedMetadataHeaps.js";
import {
readLocalSignatureFacts,
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,
exceptionRegions: ReturnType<typeof parseExceptionRegions> | null,
localSigFailure: "malformed" | "partial" | null,
): "malformed" | "partial" | "present" => {
if (
decodedIssue !== null ||
exceptionRegions?.status === "malformed" ||
localSigFailure === "malformed"
)
return "malformed";
if (truncatedInstructions > 0 || localSigFailure === "partial")
return "partial";
return "present";
};
interface MethodBodyHeader {
readonly format: "tiny" | "fat";
readonly size: number;
readonly flags: number;
readonly maxStack: number;
readonly ilSize: number;
readonly localSig: number;
}
const readMethodBodyHeader = (
bytes: Buffer,
offset: number,
): MethodBodyHeader => {
const first = bytes.readUInt8(offset);
if ((first & 3) === 2)
return {
format: "tiny",
size: 1,
flags: 0,
maxStack: 8,
ilSize: first >> 2,
localSig: 0,
};
if ((first & 3) !== 3) throw new RangeError("Unsupported method body header");
if (offset > bytes.length - 12)
throw new RangeError("Fat method body header leaves artifact");
const flagsAndSize = bytes.readUInt16LE(offset);
const headerDwords = flagsAndSize >>> 12;
if (headerDwords < 3)
throw new RangeError("Fat method body header is smaller than 12 bytes");
const size = headerDwords * 4;
if (offset > bytes.length - size)
throw new RangeError("Method body header leaves artifact");
return {
format: "fat",
size,
flags: flagsAndSize & 0x0fff,
maxStack: bytes.readUInt16LE(offset + 2),
ilSize: bytes.readUInt32LE(offset + 4),
localSig: bytes.readUInt32LE(offset + 8),
};
};
export interface MethodBodyContext {
readonly implementation: ManagedMethodImplementationFlags;
readonly budget: ManagedDecodeBudget;
readonly metadataLayout: ManagedMetadataLayout;
readonly methodToken: string;
readonly memberRefKinds: ReadonlyMap<string, "field" | "method">;
}
/** Decode admitted managed CIL, retaining unavailable implementation metadata as partial. */
export const methodBody = (
bytes: Buffer,
pe: ManagedPeLayout,
rva: number,
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
* validity. A body with no CIL, absent or native, has nothing left unscanned.
*/
readonly scanned: boolean;
} => {
const issues: ManagedParseIssue[] = [];
// Without CIL there is nothing left unscanned.
if (rva === 0)
return {
body: emptyMethodBody(rva, "absent", null),
tokenInstructions: [],
catchTokens: EMPTY_CATCH_TOKENS,
scanned: true,
issues,
};
const native =
context.implementation.codeType !== "il" ||
context.implementation.managedKind !== "managed" ||
context.implementation.pinvokeFlag;
if (native) {
const issue =
"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,
};
}
try {
const { offset, available: methodExtent } = pe.rvaFileRange(
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");
const il = bytes.subarray(ilOffset, ilOffset + header.ilSize);
// Bound the actual instruction records before allocating them. One raw
// byte is only an upper bound on count; wide operands do not create a
// separate instruction for every byte. Retained output is charged below.
const decoded = decodeInstructions(
il,
(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> = {};
for (const instruction of decoded.parsed)
opcodeCounts[instruction.opcode] =
(opcodeCounts[instruction.opcode] ?? 0) + 1;
// Keep every decoded instruction: implicit constants such as ldc.i4.N
// are encoded in opcodes whose operand kind is "none".
const anchors = decoded.parsed.map((instruction) => ({
il_offset: instruction.offset,
opcode: instruction.opcode,
operand_kind: instruction.operandKind,
operand: instruction.operand,
}));
const normalized = Buffer.from(
JSON.stringify(
decoded.parsed.map(({ opcode, operandKind, operand }) => [
opcode,
operandKind,
operand,
]),
),
"utf8",
);
const methodEnd = ilOffset + header.ilSize;
const sectionOffset = Math.ceil(methodEnd / 4) * 4;
const exceptionRegions =
header.format === "fat" && (header.flags & 8) !== 0
? validateExceptionRegionRanges(
parseExceptionRegions(
bytes,
sectionOffset,
offset + methodExtent,
context.budget,
),
header.ilSize,
decoded.instructionStarts,
)
: null;
let localVarSignature: ManagedMethodBody["local_var_signature"] = null;
let localSigIssue: string | null = null;
let localSigFailure: "malformed" | "partial" | null = null;
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 ${token} does not name an existing StandAloneSig row`;
localSigFailure = "malformed";
issues.push({
code: "invalid-row",
scope: `MethodDef:${context.methodToken}.body.local_var_sig_token`,
offset: offset + 8,
detail: localSigIssue,
});
} else {
const signatureScope = `metadata.StandAloneSig:${token}.signature`;
try {
const cursor = metadataRowCursor(
bytes,
context.metadataLayout,
17,
row,
);
const blob = readMetadataBlob(
bytes,
context.metadataLayout,
cursor.readIndex(context.metadataLayout.blobIndexSize),
context.metadataLayout.blob?.size ?? 0,
);
const signatureFacts = readLocalSignatureFacts(
context.metadataLayout,
blob,
{
scope: signatureScope,
blobOffset: blob.byteOffset - bytes.byteOffset,
},
context.budget,
);
if (signatureFacts.ok) localVarSignature = signatureFacts.value;
else {
const signatureIssue = signatureFacts.error;
localSigIssue = signatureIssue.detail;
localSigFailure =
signatureIssue.code === "unsupported-signature"
? "partial"
: "malformed";
issues.push(signatureIssue);
}
} catch (cause: unknown) {
if (
cause instanceof ManagedReaderFailure &&
cause.issue.code === "resource-limit"
)
throw cause;
if (!(cause instanceof ManagedReaderFailure)) throw cause;
localSigIssue = cause.issue.detail;
localSigFailure = "malformed";
issues.push({ ...cause.issue, scope: signatureScope });
}
}
}
const status = bodyStatus(
decoded.issue,
decoded.truncated,
exceptionRegions,
localSigFailure,
);
// Independent decoding failures must survive together. Present malformed
// instruction/section facts before local-signature limitations.
const bodyIssues = [
decoded.issue,
exceptionRegions?.status === "malformed" ? exceptionRegions.issue : null,
localSigIssue,
decoded.truncated > 0
? "Instruction decoding stopped before the end of the method body"
: null,
].filter((issue): issue is string => issue !== null);
const body: ManagedMethodBody = {
status,
header_format: header.format,
rva,
file_offset: offset,
max_stack: header.maxStack,
init_locals: (header.flags & 0x10) !== 0,
local_var_sig_token: localSignature?.token ?? null,
local_var_signature: localVarSignature,
il_size: header.ilSize,
il_sha256: sha256Bytes(il),
normalized_il_sha256:
status === "present" ? sha256Bytes(normalized) : null,
instruction_count: decoded.count,
decoded_instruction_count: decoded.parsed.length,
truncated_instructions: decoded.truncated,
opcode_counts: opcodeCounts,
anchors,
exception_regions: exceptionRegions?.regions ?? [],
issue: bodyIssues.length > 0 ? bodyIssues.join("; ") : 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: admittedBody,
tokenInstructions,
catchTokens,
scanned: decoded.issue === null && decoded.truncated === 0,
issues,
};
} catch (cause: unknown) {
if (
cause instanceof ManagedReaderFailure &&
cause.issue.code === "resource-limit"
)
throw cause;
if (
!(cause instanceof ManagedReaderFailure) &&
!(cause instanceof RangeError)
)
throw cause;
return {
body: emptyMethodBody(
rva,
"malformed",
cause instanceof Error ? cause.message : "Method body parse failed",
),
tokenInstructions: [],
catchTokens: EMPTY_CATCH_TOKENS,
scanned: false,
issues,
};
} finally {
// IL and exception-section bookkeeping coexist until the body is built.
context.budget.releaseTransient();
}
};
const emptyMethodBody = (
rva: number,
status: "absent" | "malformed" | "partial",
issue: string | null,
): ManagedMethodBody => ({
status,
header_format: status === "absent" ? "none" : "unknown",
rva,
file_offset: null,
max_stack: null,
init_locals: null,
local_var_sig_token: null,
local_var_signature: null,
il_size: 0,
il_sha256: null,
normalized_il_sha256: null,
instruction_count: 0,
decoded_instruction_count: 0,
truncated_instructions: 0,
opcode_counts: {},
anchors: [],
exception_regions: [],
issue,
});