fix(dotnet): name the instruction when a CIL operand leaves the method body (#2697)

The decoder already reports "Instruction X at IL offset N leaves method
body" when an operand's computed end passes the IL body, but only after
the operand was read. Operand readers use Buffer reads, so an operand
cut off by the body end threw a RangeError first. The catch-all stored
Node's message ("Attempt to access memory outside buffer bounds" or an
offset RangeError) as body.issue, dropping the instruction and offset.
A body ending in a lone 0xFE two-byte opcode prefix failed the same way.

Treat a RangeError from the operand read as the operand leaving the body
so the existing diagnostic applies, and check the second opcode byte
before reading it. Malformed status, partial coverage and earlier
decoded instructions are unchanged.

Fixes #2677

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-13 06:14:31 -07:00 committed by GitHub
parent cdc41fc86c
commit 97dc6581f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 64 additions and 2 deletions

View File

@ -510,3 +510,41 @@ describe("managed exception section decoding", () => {
});
});
});
describe("managed CIL truncated operands", () => {
const tinyBody = (il: readonly number[]) => {
const bytes = buildManagedPeFixture({
ilBody: Buffer.from([(il.length << 2) | 0x02, ...il]),
});
const inspection = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
return { body: inspection.methods[0]?.body, coverage: inspection.coverage };
};
it.each([
[[0x20], 0, "Instruction ldc.i4 at IL offset 0 leaves method body"],
[
[0x00, 0x20, 0x01],
1,
"Instruction ldc.i4 at IL offset 1 leaves method body",
],
[[0x2b], 0, "Instruction br.s at IL offset 0 leaves method body"],
[[0x45, 0x02], 0, "Instruction switch at IL offset 0 leaves method body"],
[
[0xfe, 0x09, 0x00],
0,
"Instruction ldarg at IL offset 0 leaves method body",
],
[[0x00, 0xfe], 1, "Two-byte CIL opcode at IL offset 1 leaves method body"],
])("names the instruction and IL offset for IL %j", (il, decoded, issue) => {
const { body, coverage } = tinyBody(il);
expect(body).toMatchObject({
status: "malformed",
decoded_instruction_count: decoded,
issue,
});
expect(coverage.state).toBe("partial");
});
});

View File

@ -384,6 +384,22 @@ const readOperand = (
return { next: offset, anchorKind: "unknown", value: null };
};
/** An operand cut off by the end of the IL body has no decodable value. */
const readOperandWithin = (
il: Buffer,
offset: number,
kind: string,
): OperandResult | undefined => {
try {
return readOperand(il, offset, kind);
} catch (cause: unknown) {
// Buffer reads past the IL body throw RangeError; report it as the
// instruction leaving the method body rather than as a Buffer message.
if (cause instanceof RangeError) return undefined;
throw cause;
}
};
export const decodeInstructions = (
il: Buffer,
admitInstruction?: (offset: number) => void,
@ -404,14 +420,22 @@ export const decodeInstructions = (
const start = offset;
const first = il.readUInt8(offset);
offset += 1;
if (first === 0xfe && offset >= il.length) {
issue = `Two-byte CIL opcode at IL offset ${String(start)} leaves method body`;
break;
}
const code = first === 0xfe ? 0xfe00 + il.readUInt8(offset++) : first;
const descriptor = OPCODES.get(code);
if (descriptor === undefined) {
issue = `Unsupported CIL opcode 0x${code.toString(16)} at IL offset ${String(start)}`;
break;
}
const operand = readOperand(il, offset, descriptor.operand);
if (operand.next < offset || operand.next > il.length) {
const operand = readOperandWithin(il, offset, descriptor.operand);
if (
operand === undefined ||
operand.next < offset ||
operand.next > il.length
) {
issue = `Instruction ${descriptor.name} at IL offset ${String(start)} leaves method body`;
break;
}