Files
rea/src/dotnet/ManagedMethodBodyReader.ts
T
morluto 667402846c fix(managed): validate metadata operands and locals declarations
Classify MemberRef operands from parsed signatures, validate signature table references and LocalVarSig blobs, and compare observed locals declaration presence. Keep partial CIL facts and transient-budget ownership at the method reader.

Fixes #6578
Fixes #6572
Fixes #6558
Fixes #6513
2026-10-23 18:30:45 +08:00

328 lines
11 KiB
TypeScript

import {
admitManagedProjection,
type ManagedDecodeBudget,
} from "./ManagedDecodeBudget.js";
import type { ManagedPeLayout } from "./ManagedPeReader.js";
import type { ManagedMethodBody } from "./ManagedMemberInspectorCore.js";
import {
decodeInstructions,
parseExceptionRegions,
} from "./ManagedMemberInstructionDecoder.js";
import { validateExceptionRegionRanges } from "./ManagedExceptionRegionValidation.js";
import {
metadataRowCursor,
readMetadataBlob,
sha256Bytes,
} from "./ManagedMetadataHeaps.js";
import { validateLocalSignature } from "./ManagedMemberInspectorCore.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
const bodyStatus = (
decodedIssue: string | null,
truncatedInstructions: number,
exceptionRegions: ReturnType<typeof parseExceptionRegions> | null,
): "malformed" | "partial" | "present" => {
if (decodedIssue !== null || exceptionRegions?.status === "malformed")
return "malformed";
if (truncatedInstructions > 0) 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: {
readonly implFlags: number;
readonly flags: number;
};
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 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),
scanned: true,
issues,
};
const native =
(context.implementation.implFlags & 7) !== 0 ||
(context.implementation.flags & 0x2000) !== 0;
if (native) {
const issue =
"CIL decoding is unavailable for native, runtime, unmanaged, or P/Invoke implementations";
return {
body: emptyMethodBody(rva, "partial", issue),
scanned: true,
issues,
};
}
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");
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) => 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 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 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`;
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:0x${header.localSig.toString(16).padStart(8, "0")}.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,
);
const signatureIssue = validateLocalSignature(
context.metadataLayout,
blob,
{
scope: signatureScope,
blobOffset: blob.byteOffset - bytes.byteOffset,
},
context.budget,
);
if (signatureIssue !== null) {
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 =
localSigFailure ??
bodyStatus(decoded.issue, decoded.truncated, exceptionRegions);
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:
header.localSig === 0
? null
: `0x${header.localSig.toString(16).padStart(8, "0")}`,
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:
localSigIssue ??
decoded.issue ??
(exceptionRegions?.status === "malformed"
? exceptionRegions.issue
: null) ??
(decoded.truncated > 0
? "Instruction decoding stopped before the end of the method body"
: null),
};
return {
body: admitManagedProjection(body, context.budget),
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",
),
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,
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,
});