import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; import { buildManagedPeFixture, managedPeFixtureTarget, } from "./ManagedPe.fixture.js"; const managedBodyWithSections = (...sections: Buffer[]) => { const header = Buffer.alloc(12); header.writeUInt16LE(0x300b, 0); header.writeUInt16LE(8, 2); header.writeUInt32LE(1, 4); const bytes = buildManagedPeFixture({ ilBody: Buffer.concat([ header, Buffer.from([0x2a]), Buffer.alloc(3), ...sections, ]), }); return inspectManagedMembersBytes(bytes, managedPeFixtureTarget(bytes)) .methods[0]?.body; }; const managedBodyWithRegion = ({ il, flags = 0, tryOffset = 0, tryLength = 1, handlerOffset, handlerLength = 1, extra = 0x0200_0001, }: { readonly il: Buffer; readonly flags?: number; readonly tryOffset?: number; readonly tryLength?: number; readonly handlerOffset: number; readonly handlerLength?: number; readonly extra?: number; }) => { const header = Buffer.alloc(12); header.writeUInt16LE(0x300b, 0); header.writeUInt16LE(8, 2); header.writeUInt32LE(il.length, 4); const section = Buffer.alloc(16); section[0] = 0x01; section[1] = section.length; section.writeUInt16LE(flags, 4); section.writeUInt16LE(tryOffset, 6); section[8] = tryLength; section.writeUInt16LE(handlerOffset, 9); section[11] = handlerLength; section.writeUInt32LE(extra, 12); const bytes = buildManagedPeFixture({ ilBody: Buffer.concat([ header, il, Buffer.alloc((4 - (il.length % 4)) % 4), section, ]), }); return inspectManagedMembersBytes(bytes, managedPeFixtureTarget(bytes)) .methods[0]?.body; }; describe("managed CIL decoding", () => { it("reads fat method header size from the full flags-and-size word", () => { const il = Buffer.from([ 0x21, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x22, 0x00, 0x00, 0x80, 0x3f, 0x26, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0x26, 0xfe, 0x06, 0x01, 0x00, 0x00, 0x06, 0x26, 0x2a, ]); const header = Buffer.alloc(12); header.writeUInt16LE(0x3013, 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.methods[0]?.body).toMatchObject({ status: "present", header_format: "fat", il_size: il.length, il_sha256: createHash("sha256").update(il).digest("hex"), opcode_counts: { "ldc.i8": 1, pop: 4, "ldc.r4": 1, "ldc.r8": 1, ldftn: 1, ret: 1, }, issue: null, }); }); it("does not normalize reserved CIL opcodes as operand-free instructions", () => { const bytes = buildManagedPeFixture({ ilBody: Buffer.from([0x0a, 0x24, 0x2a]), }); const result = inspectManagedMembersBytes( bytes, managedPeFixtureTarget(bytes), ); expect(result.methods[0]?.body).toMatchObject({ status: "malformed", il_size: 2, normalized_il_sha256: null, decoded_instruction_count: 0, issue: "Unsupported CIL opcode 0x24 at IL offset 0", }); }); it("does not decode overlay bytes after the method's file-backed PE section", () => { const original = buildManagedPeFixture(); const bytes = Buffer.concat([original, Buffer.from([0x2a])]); const parsed = inspectManagedMembersBytes( original, managedPeFixtureTarget(original), ); const methodRow = parsed.methods[0]?.row_offset; expect(methodRow).toBeDefined(); if (methodRow === undefined) return; bytes.writeUInt32LE(0x2dff, methodRow); bytes[0xfff] = 0x06; const result = inspectManagedMembersBytes( bytes, managedPeFixtureTarget(bytes), ); expect(result.methods[0]?.body).toMatchObject({ status: "malformed", header_format: "unknown", issue: "Method body leaves file-backed PE section data", anchors: [], }); }); it("does not decode overlay exception sections after the file-backed PE section", () => { const original = buildManagedPeFixture(); const overlay = Buffer.from([0x01, 0x04, 0x00, 0x00]); const bytes = Buffer.concat([original, overlay]); const parsed = inspectManagedMembersBytes( original, managedPeFixtureTarget(original), ); const methodRow = parsed.methods[0]?.row_offset; expect(methodRow).toBeDefined(); if (methodRow === undefined) return; const body = Buffer.alloc(16); body.writeUInt16LE(0x301b, 0); body.writeUInt16LE(8, 2); body.writeUInt32LE(1, 4); body[12] = 0x2a; body.copy(bytes, 0xff0); bytes.writeUInt32LE(0x2df0, methodRow); overlay.copy(bytes, 0x1000); const result = inspectManagedMembersBytes( bytes, managedPeFixtureTarget(bytes), ); expect(result.methods[0]?.body).toMatchObject({ status: "malformed", issue: "Exception section header leaves artifact", exception_regions: [], }); }); it("keeps the documented decoded-CIL v1 golden vector stable", () => { const il = Buffer.from([0x00, 0x2a]); const tinyBytes = buildManagedPeFixture({ ilBody: Buffer.from([0x0a, ...il]), }); const fatHeader = Buffer.alloc(12); fatHeader.writeUInt16LE(0x3013, 0); fatHeader.writeUInt16LE(32, 2); fatHeader.writeUInt32LE(il.length, 4); fatHeader.writeUInt32LE(0x1100_0001, 8); const fatBytes = buildManagedPeFixture({ ilBody: Buffer.concat([fatHeader, il]), }); const tiny = inspectManagedMembersBytes( tinyBytes, managedPeFixtureTarget(tinyBytes), ); const fat = inspectManagedMembersBytes( fatBytes, managedPeFixtureTarget(fatBytes), ); expect(tiny.methods[0]?.body).toMatchObject({ status: "present", header_format: "tiny", max_stack: 8, init_locals: false, local_var_sig_token: null, il_size: 2, il_sha256: createHash("sha256").update(il).digest("hex"), normalized_il_sha256: "5e5fad7741cb44bca3a4f045546b7449990da343f612f5f34c0ca30e9eee0636", opcode_counts: { nop: 1, ret: 1 }, exception_regions: [], }); expect(fat.methods[0]?.body).toMatchObject({ header_format: "fat", max_stack: 32, init_locals: true, local_var_sig_token: "0x11000001", il_sha256: tiny.methods[0]?.body.il_sha256, normalized_il_sha256: tiny.methods[0]?.body.normalized_il_sha256, }); }); }); describe("managed exception region bounds", () => { it("marks out-of-method ranges malformed and preserves the raw EH clause", () => { expect( managedBodyWithRegion({ il: Buffer.from([0x2a]), tryOffset: 0, tryLength: 2, handlerOffset: 0, }), ).toMatchObject({ status: "malformed", il_size: 1, exception_regions: [ { flags: 0, try_offset: 0, try_length: 2, handler_offset: 0, handler_length: 1, class_token: "0x02000001", }, ], issue: "Exception clause try range leaves the method IL body", }); }); it("uses half-open method bounds and permits zero-length ranges", () => { expect( managedBodyWithRegion({ il: Buffer.from([0x00, 0x00, 0x2a]), tryOffset: 0, tryLength: 1, handlerOffset: 1, handlerLength: 2, }), ).toMatchObject({ status: "present", il_size: 3, issue: null }); expect( managedBodyWithRegion({ il: Buffer.from([0x2a]), tryOffset: 0, tryLength: 0, handlerOffset: 0, handlerLength: 0, }), ).toMatchObject({ status: "present", il_size: 1, issue: null }); }); it.each([ ["try start", { tryOffset: 1, tryLength: 4, handlerOffset: 5 }], ["try end", { tryOffset: 0, tryLength: 1, handlerOffset: 5 }], ["handler start", { tryLength: 5, handlerOffset: 1, handlerLength: 5 }], ["handler end", { tryLength: 5, handlerOffset: 0, handlerLength: 1 }], ] as const)( "preserves clauses with a mid-instruction %s", (_label, region) => { const body = managedBodyWithRegion({ il: Buffer.from([0x20, 0, 0, 0, 0, 0x2a]), ...region, }); expect(body).toMatchObject({ status: "malformed", exception_regions: [expect.any(Object)], issue: "Exception clause " + (_label.startsWith("try") ? "try" : "handler") + " range does not align with CIL instruction boundaries", }); }, ); it("rejects a filter start inside an instruction and retains the clause", () => { expect( managedBodyWithRegion({ il: Buffer.from([0x20, 0, 0, 0, 0, 0x2a]), flags: 1, tryLength: 5, handlerOffset: 5, extra: 1, }), ).toMatchObject({ status: "malformed", exception_regions: [expect.objectContaining({ filter_offset: 1 })], issue: "Exception clause filter range does not align with CIL instruction boundaries", }); }); }); describe("managed exception region prefix boundaries", () => { it("treats a CIL prefix chain and its opcode as one boundary", () => { expect( managedBodyWithRegion({ il: Buffer.from([0xfe, 0x12, 1, 0xfe, 0x13, 0x4a, 0x2a]), tryOffset: 0, tryLength: 6, handlerOffset: 6, }), ).toMatchObject({ status: "present", issue: null, il_size: 7 }); }); it("keeps constrained callvirt as a single region boundary", () => { const il = Buffer.from([0xfe, 0x16, 1, 0, 0, 1, 0x6f, 1, 0, 0, 6, 0x2a]); expect( managedBodyWithRegion({ il, tryOffset: 0, tryLength: 11, handlerOffset: 11, }), ).toMatchObject({ status: "present", issue: null, il_size: 12 }); expect( managedBodyWithRegion({ il, tryOffset: 6, tryLength: 1, handlerOffset: 11, }), ).toMatchObject({ status: "malformed", issue: "Exception clause try range does not align with CIL instruction boundaries", }); }); it.each([ ["start", { tryOffset: 5, tryLength: 1, handlerOffset: 6 }], [ "end after first prefix", { tryOffset: 0, tryLength: 3, handlerOffset: 6 }, ], [ "end after second prefix", { tryOffset: 0, tryLength: 5, handlerOffset: 6 }, ], ] as const)( "rejects a region %s between a prefix and its opcode", (_edge, region) => { expect( managedBodyWithRegion({ il: Buffer.from([0xfe, 0x12, 1, 0xfe, 0x13, 0x4a, 0x2a]), ...region, }), ).toMatchObject({ status: "malformed", exception_regions: [expect.any(Object)], issue: "Exception clause try range does not align with CIL instruction boundaries", }); }, ); }); describe("managed exception region flags and filters", () => { it("marks reserved clause flags malformed without dropping its bytes", () => { expect( managedBodyWithRegion({ il: Buffer.from([0x00, 0x2a]), flags: 8, tryLength: 1, handlerOffset: 1, }), ).toMatchObject({ status: "malformed", exception_regions: [expect.objectContaining({ flags: 8 })], issue: "Exception clause has unsupported flags 0x8", }); }); it.each([2, 4])( "accepts the defined finally/fault clause flag %s", (flags) => { expect( managedBodyWithRegion({ il: Buffer.from([0x00, 0x00, 0x2a]), flags, tryOffset: 0, tryLength: 1, handlerOffset: 1, handlerLength: 2, }), ).toMatchObject({ status: "present", issue: null }); }, ); it("validates filter ordering", () => { expect( managedBodyWithRegion({ il: Buffer.from([0x00, 0x00, 0xfe, 0x11, 0x2a]), flags: 1, tryOffset: 0, tryLength: 1, handlerOffset: 4, extra: 1, }), ).toMatchObject({ status: "present", il_size: 5, issue: null }); expect( managedBodyWithRegion({ il: Buffer.from([0x00, 0x00, 0x2a]), flags: 1, tryOffset: 0, tryLength: 1, handlerOffset: 1, extra: 1, }), ).toMatchObject({ status: "malformed", issue: "Exception clause filter must start before its handler within the method IL body", }); }); }); describe("managed exception section decoding", () => { it("marks an exception section with a partial clause as malformed", () => { const section = Buffer.alloc(17); section[0] = 0x01; section[1] = section.length; expect(managedBodyWithSections(section)).toMatchObject({ status: "malformed", exception_regions: [], issue: "Exception section size does not contain whole clauses", }); }); it("reads every chained exception section", () => { const first = Buffer.alloc(16); first[0] = 0x81; first[1] = first.length; const second = Buffer.alloc(16); second[0] = 0x01; second[1] = second.length; expect(managedBodyWithSections(first, second)).toMatchObject({ status: "present", issue: null, exception_regions: [ { flags: 0, try_offset: 0, try_length: 0, handler_offset: 0 }, { flags: 0, try_offset: 0, try_length: 0, handler_offset: 0 }, ], }); }); it("preserves decoded clauses when a chained section has an unsupported kind", () => { const first = Buffer.alloc(16); first[0] = 0x81; first[1] = first.length; const second = Buffer.from([0x02, 0x04, 0x00, 0x00]); expect(managedBodyWithSections(first, second)).toMatchObject({ status: "malformed", exception_regions: [expect.objectContaining({ flags: 0 })], issue: "Unsupported method data section kind 2", }); }); it("preserves decoded clauses when a later section has a partial clause", () => { const first = Buffer.alloc(16); first[0] = 0x81; first[1] = first.length; first.writeUInt16LE(2, 4); const later = Buffer.alloc(17); later[0] = 0x01; later[1] = later.length; expect(managedBodyWithSections(first, later)).toMatchObject({ status: "malformed", exception_regions: [expect.objectContaining({ flags: 2 })], issue: "Exception section size does not contain whole clauses", }); }); it.each([ { name: "unsupported section kind", section: Buffer.from([0x02, 0x04, 0x00, 0x00]), issue: "Unsupported method data section kind 2", }, { name: "section size outside artifact", section: Buffer.from([0x41, 0xff, 0xff, 0xff]), issue: "Exception section size leaves artifact", }, ])("rejects $name", ({ section, issue }) => { expect(managedBodyWithSections(section)).toMatchObject({ status: "malformed", exception_regions: [], issue, }); }); }); 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"); }); });