fix(managed): bound transient CIL by decoded instruction count (#1822)
fix(managed): charge transient CIL by decoded instructions
This commit is contained in:
1 parent
ca11162d02
commit
554cb45f7f
3 files changed
+72
-5
No files matched your search
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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] =
|
||||
|
||||
Reference in new issue
Block a user