From 97dc6581f45adfe5fcaa9c98e77e7dc6bc423a6e Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:14:31 -0700 Subject: [PATCH] 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 --- src/dotnet/ManagedCilDecoding.test.ts | 38 +++++++++++++++++++ src/dotnet/ManagedMemberInstructionDecoder.ts | 28 +++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) 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; }