Skip to content

ICorDebugILCode::GetEHClauses returns no clauses for a ReJIT IL body with a small-format EH section #135919

Closed

Description

@pcshrosbree

TL;DR: ICorDebugILCode::GetEHClauses on ReJIT IL reports zero clauses when the EH section uses the small format, and can report clauses for a tiny-header method that has none. The cause is four defects in CordbReJitILCode::Init, which I reproduced on the shipped 10.0.12 runtime and on main, on linux-x64, windows-x64 and osx-arm64. Any debugger that reads the EH clauses of ReJIT IL is affected.

Description

CordbReJitILCode::Init (src/coreclr/debug/di/module.cpp) mis-reads ReJIT IL bodies in two ways.

  1. If the exception-handling section uses the small format (ECMA-335 II.25.4.6), ICorDebugILCode::GetEHClauses reports zero clauses. The same clauses in the fat format are reported correctly.
  2. If the method has a tiny header, the debugger can report EH clauses for a method that has none.

This affects the ICorDebugILCode returned by both ICorDebugFunction3::GetActiveReJitRequestILCode and ICorDebugILFrame4::GetCodeEx(ILCODE_REJIT_IL). I reproduced both problems on the shipped 10.0.12 runtime (linux-x64, windows-x64 and osx-arm64) and on current main. I exercised both entry points for the small-format problem, and GetActiveReJitRequestILCode for the tiny-header problem.

There are four defects. Line numbers are at b452b42.

The first three are in the small-format branch (L3356-L3390):

ULONG32 m_cClauses = (sectionHeader.DataSize - 4) / sizeof(IMAGE_COR_ILMETHOD_SECT_SMALL);
...
TargetBuffer buffer(ehClauseHeader + sizeof(IMAGE_COR_ILMETHOD_SECT_SMALL), m_cClauses*sizeof(IMAGE_COR_ILMETHOD_SECT_EH_CLAUSE_SMALL));
  1. The count is stored in a local, not the member. ULONG32 m_cClauses declares a local that shadows the member (L3361). The member stays at the 0 set in the constructor (L3229), and that member is what GetEHClauses returns (L3427).
  2. The divisor is the 2-byte section header, not the 12-byte clause. sizeof(IMAGE_COR_ILMETHOD_SECT_SMALL) is 2 (corhdr.h), while IMAGE_COR_ILMETHOD_SECT_EH_CLAUSE_SMALL is 12 (corhdr.h). The count comes out six times too large, so the debugger reads about six times too much target memory. If that memory is not readable, Init fails.
  3. The clauses are read from the wrong offset. The clauses follow a 2-byte Reserved field, so they start 4 bytes into the section (corhdr.h). The read starts at +2.

The fourth defect is in the section check before the small-format branch:

  1. A tiny header is treated as if it had section flags. The check for an EH section (L3308) tests CorILMethod_MoreSects (0x08) against the first header byte even when the header is tiny. A tiny header has no section flags: in a tiny header, the 0x08 bit is bit 1 of the code size. A tiny body whose code size is 2, 3, 6, 7, … is therefore treated as having an extra section, and the debugger reads, and may parse, whatever follows the code. COR_ILMETHOD_TINY::GetSect in corhlpr.h returns no section for a tiny header (L466-L469).

The branch for fat EH sections uses the right clause size and starting offset.

Because the small-format branch leaves the member's clause count at 0, GetEHClauses never exposes the clauses that branch decodes wrongly, although the over-read still happens and can make Init fail. The same zero count hides the bogus clauses parsed from bytes after a tiny body when those bytes look like a small EH section. Fixing only the small-format branch would therefore expose those bogus clauses, so the tiny-header check needs fixing too. The code with these four defects has not changed since the repository was first imported in 2015.

Who is affected

Any debugger that reads the EH clauses of ReJIT IL. A ReJIT version has a small EH section in either of two cases:

  • The profiler does not call SetILFunctionBody. The version then uses the method's default IL, which is its original IL unless something has replaced it (rejit.cpp, codeversion.cpp). Roslyn writes small EH headers whenever the regions fit. This covers profilers that ReJIT only to change codegen flags, and inliners ReJITted by the runtime.
  • The profiler supplies a small section itself. For example, SectEH_Emit in corhlpr.cpp picks the small format when the clauses fit.

Edit and Continue versions are not affected: the DAC only hands this code IL versions whose source is ReJIT (dacdbiimpl.cpp).

Reproduction

I wrote three small programs:

  • a C# app whose method Target has one try/catch; Roslyn emits it with a small EH section (DataSize 16, one clause: try 0+54, handler 54+5);
  • a profiler that calls RequestReJIT on Target. Its GetReJITParameters only calls SetCodegenFlags, so the ReJIT version uses the method's original IL;
  • an ICorDebug client that attaches and calls GetActiveReJitRequestILCode and then GetEHClauses. It also walks to a thread parked inside Target and calls GetCodeEx(ILCODE_REJIT_IL) and then GetEHClauses.

Two variants act as a control and as the tiny-header case:

  • The control has the profiler pass the same body to SetILFunctionBody with the EH section re-encoded in the fat format.
  • The tiny-header case ReJITs a two-byte method (ldc.i4.7; ret, tiny header byte 0x0A). The profiler passes its body followed by bytes shaped like a fat EH section. The runtime ignores anything after a tiny body.

These are the results with the shipped 10.0.12 runtime and its own mscordbi. All three platforms give identical results. Both entry points agree for the first two rows; the tiny-body row was measured through GetActiveReJitRequestILCode only, because no frame of the tiny method exists.

ReJIT body Expected linux-x64 windows-x64 osx-arm64
original IL, small EH section 1 clause 0 0 0
same clause, fat EH section (control) 1 clause 1 1 1
tiny body + bytes shaped like a fat EH section 0 clauses 1 (bogus: try 0+1, handler 1+1) 1 1

Fix

I have a fix and will open a PR. It does four things:

  • assigns the member;
  • divides by sizeof(IMAGE_COR_ILMETHOD_SECT_EH_CLAUSE_SMALL);
  • reads the clauses from offsetof(IMAGE_COR_ILMETHOD_SECT_EH_SMALL, Clauses);
  • checks CorILMethod_MoreSects only for fat headers.

With an mscordbi built from the fix, all three rows above come out as expected on all three platforms.

Note

This issue was prepared with AI assistance (Anthropic Claude and OpenAI Codex), under my direction. I reviewed it before posting.

Activity

  1. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Oct 16, 2026
  2. dotnet-policy-service commented on Oct 16, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
    See info in area-owners.md if you want to be subscribed.

  3. removed
    untriagedNew issue has not been triaged by the area owner
    on Oct 29, 2026
  4. added a commit that references this issue on Oct 29, 2026
    e111d4c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions