fix(managed): bound transient CIL by decoded instruction count (#1822)

fix(managed): charge transient CIL by decoded instructions
This commit is contained in:
jhuang-tw authored and GitHub committed 2026-10-11 22:51:41 +08:00
1 parent ca11162d02
commit 554cb45f7f
3 files changed
+72 -5

No files matched your search

+57
View File
@@ -248,6 +248,63 @@ describe("managed exported types", () => {
});
describe("managed member decoding budget", () => {
it("admits a wide-instruction method whose actual records fit the budget", () => {
// Five-byte branches all target the next instruction. A final ret leaves
// the method stack-neutral, despite over 256 KiB of valid IL bytes.
const il = Buffer.alloc(270_001);
for (let offset = 0; offset < il.length - 1; offset += 5) {
il[offset] = 0x38;
}
il[il.length - 1] = 0x2a;
const header = Buffer.alloc(12);
header.writeUInt16LE(0x3003, 0);
header.writeUInt16LE(8, 2);
header.writeUInt32LE(il.length, 4);
const bytes = buildManagedPeFixture({
ilBody: Buffer.concat([header, il]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
expect(result.types).toHaveLength(1);
expect(result.methods[0]?.body).toMatchObject({
status: "present",
il_size: il.length,
decoded_instruction_count: 54_001,
opcode_counts: { br: 54_000, ret: 1 },
});
});
it("still rejects dense one-byte instructions before exceeding capacity", () => {
// All NOPs: unlike wide instructions, each IL byte adds a decoded record.
// A true oversized transient must remain an actionable resource limit.
const il = Buffer.alloc(270_001);
il[il.length - 1] = 0x2a;
const header = Buffer.alloc(12);
header.writeUInt16LE(0x3003, 0);
header.writeUInt16LE(8, 2);
header.writeUInt32LE(il.length, 4);
const bytes = buildManagedPeFixture({
ilBody: Buffer.concat([header, il]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage).toMatchObject({
state: "unavailable",
issues: [{ code: "resource-limit", scope: "metadata.representation" }],
});
// Report the originating IL coordinate, not a generic decoding error.
expect(result.coverage.issues[0]?.offset).toBeGreaterThan(0);
expect(result.types).toEqual([]);
expect(result.methods).toEqual([]);
});
it("charges decoded output rather than accumulated decoding bounds", () => {
// Each bound fits alone, but 256 bytes per IL and signature byte together
// exceed the 64 MiB budget although the retained members are small.
@@ -1,4 +1,5 @@
import type { ManagedDecodeBudget } from "./ManagedDecodeBudget.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
import type {
ManagedExceptionRegion,
ManagedInstructionAnchor,
@@ -381,6 +382,7 @@ const readOperand = (
export const decodeInstructions = (
il: Buffer,
admitInstruction?: (offset: number) => void,
): {
readonly parsed: readonly ParsedInstruction[];
readonly instructionStarts: readonly number[];
@@ -409,6 +411,9 @@ export const decodeInstructions = (
issue = `Instruction ${descriptor.name} at IL offset ${String(start)} leaves method body`;
break;
}
// 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;
@@ -424,6 +429,7 @@ export const decodeInstructions = (
});
}
} catch (cause: unknown) {
if (cause instanceof ManagedReaderFailure) throw cause;
issue =
cause instanceof Error ? cause.message : "Instruction decode failed";
}
+9 -5
View File
@@ -100,11 +100,15 @@ export const methodBody = (
if (ilOffset > bytes.length - header.ilSize)
throw new RangeError("Method IL bytes leave artifact");
const il = bytes.subarray(ilOffset, ilOffset + header.ilSize);
// One byte can produce an instruction record, start index, normalized tuple,
// and anchor. Bound that bookkeeping before decoding; the retained body is
// reserved once it is known.
budget?.admitTransient(il.length * 256, ilOffset);
const decoded = decodeInstructions(il);
// 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,
budget === undefined
? undefined
: (start) => budget.admitTransient(256, ilOffset + start),
);
const opcodeCounts: Record<string, number> = {};
for (const instruction of decoded.parsed)
opcodeCounts[instruction.opcode] =