From ee5cfbe05cd309a88003c711721e67a3bee36554 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:53:13 +0800 Subject: [PATCH] fix(dotnet): accept legacy small exception section sizes Apply CoreCLR-compatible clause counting while validating and advancing by the physical section extent. Preserve malformed, reserved, chained and truncation checks. Validation: managed decoder suite passes; no native compiler or runtime verification claimed. Fixes #3279 --- src/dotnet/ManagedCilDecoding.test.ts | 67 +++++++++++++++++++ src/dotnet/ManagedMemberInstructionDecoder.ts | 30 ++++++++- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/dotnet/ManagedCilDecoding.test.ts b/src/dotnet/ManagedCilDecoding.test.ts index b24497db9a..8214d0e85d 100644 --- a/src/dotnet/ManagedCilDecoding.test.ts +++ b/src/dotnet/ManagedCilDecoding.test.ts @@ -7,6 +7,7 @@ import { buildManagedPeFixture, managedPeFixtureTarget, } from "./ManagedPe.fixture.js"; +import { parseExceptionRegions } from "./ManagedMemberInstructionDecoder.js"; const managedBodyWithSections = (...sections: Buffer[]) => { const header = Buffer.alloc(12); @@ -666,6 +667,59 @@ describe("managed exception region flags and filters", () => { }); describe("managed exception section decoding", () => { + it("reads legacy small EH DataSize without counting its header", () => { + const section = Buffer.alloc(16); + section[0] = 0x01; + section[1] = 12; + + expect(managedBodyWithSections(section)).toMatchObject({ + status: "present", + issue: null, + exception_regions: [expect.any(Object)], + normalized_il_sha256: expect.any(String), + }); + }); + + it("advances a chained legacy section by its physical size", () => { + const legacy = Buffer.alloc(16); + legacy[0] = 0x81; + legacy[1] = 12; + const conformant = Buffer.alloc(16); + conformant[0] = 0x01; + conformant[1] = 16; + + expect(managedBodyWithSections(legacy, conformant)).toMatchObject({ + status: "present", + issue: null, + exception_regions: [expect.any(Object), expect.any(Object)], + normalized_il_sha256: expect.any(String), + }); + }); + + it("rejects a legacy-sized clause whose physical bytes leave the artifact", () => { + const section = Buffer.alloc(12); + section[0] = 0x01; + section[1] = 12; + + expect(parseExceptionRegions(section, 0, section.length)).toMatchObject({ + status: "malformed", + regions: [], + issue: "Exception section size leaves artifact", + }); + }); + + it("still rejects a small EH DataSize that matches neither layout", () => { + const section = Buffer.alloc(17); + section[0] = 0x01; + section[1] = 13; + + expect(managedBodyWithSections(section)).toMatchObject({ + status: "malformed", + exception_regions: [], + issue: "Exception section size does not contain whole clauses", + }); + }); + it("marks an exception section with a partial clause as malformed", () => { const section = Buffer.alloc(17); section[0] = 0x01; @@ -695,6 +749,19 @@ describe("managed exception section decoding", () => { }); }); + it("keeps small section reserved bytes invalid", () => { + const section = Buffer.alloc(16); + section[0] = 0x01; + section[1] = 16; + section[2] = 1; + + expect(managedBodyWithSections(section)).toMatchObject({ + status: "malformed", + exception_regions: [], + issue: "Small exception section reserved bytes are nonzero", + }); + }); + it("preserves decoded clauses when a chained section has an unsupported kind", () => { const first = Buffer.alloc(16); first[0] = 0x81; diff --git a/src/dotnet/ManagedMemberInstructionDecoder.ts b/src/dotnet/ManagedMemberInstructionDecoder.ts index 0c04b7a368..7fbe8810b0 100644 --- a/src/dotnet/ManagedMemberInstructionDecoder.ts +++ b/src/dotnet/ManagedMemberInstructionDecoder.ts @@ -543,11 +543,36 @@ export const parseExceptionRegions = ( "Exception section size leaves artifact", regions, ); - if ((size - 4) % clauseSize !== 0) + let count: number; + let sectionSize = size; + if (fat) { + if ((size - 4) % clauseSize !== 0) + return malformedExceptionRegions( + "Exception section size does not contain whole clauses", + regions, + ); + count = (size - 4) / clauseSize; + } else if ((size - 4) % clauseSize === 0) { + count = (size - 4) / clauseSize; + } else if (size >= clauseSize && size % clauseSize === 0) { + // CoreCLR accommodates legacy VB/MC++ sections whose DataSize omits the + // four-byte section header. DataSize is still not the physical extent. + count = size / clauseSize; + sectionSize = 4 + count * clauseSize; + } else { return malformedExceptionRegions( "Exception section size does not contain whole clauses", regions, ); + } + if ( + sectionOffset > methodEnd - sectionSize || + sectionOffset > bytes.length - sectionSize + ) + return malformedExceptionRegions( + "Exception section size leaves artifact", + regions, + ); if ( !fat && (bytes[sectionOffset + 2] !== 0 || bytes[sectionOffset + 3] !== 0) @@ -557,7 +582,6 @@ export const parseExceptionRegions = ( regions, ); - const count = (size - 4) / clauseSize; // Accumulates with the method IL bound; the body reserves what it retains. budget?.admitTransient(count * 256, sectionOffset); for (const region of readExceptionClauses( @@ -569,7 +593,7 @@ export const parseExceptionRegions = ( regions.push(region); hasMoreSections = (kind & 0x80) !== 0; - sectionOffset += size; + sectionOffset += sectionSize; } return { status: "complete", regions }; };