diff --git a/src/dotnet/ManagedCilDecoding.test.ts b/src/dotnet/ManagedCilDecoding.test.ts index c1ed21130..55a008b72 100644 --- a/src/dotnet/ManagedCilDecoding.test.ts +++ b/src/dotnet/ManagedCilDecoding.test.ts @@ -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"); + }); +}); diff --git a/src/dotnet/ManagedMemberInstructionDecoder.ts b/src/dotnet/ManagedMemberInstructionDecoder.ts index 7ef518733..b05dd083f 100644 --- a/src/dotnet/ManagedMemberInstructionDecoder.ts +++ b/src/dotnet/ManagedMemberInstructionDecoder.ts @@ -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; }