Skip to content

Fix debug assertion in FindTightlyBoundUnboxingStub_DEBUG / FindTightlyBoundWrappedMethodDesc_DEBUG#124377

Closed
pavelsavara wants to merge 1 commit intodotnet:mainfrom
pavelsavara:fix_genmeth_debug
Closed

Fix debug assertion in FindTightlyBoundUnboxingStub_DEBUG / FindTightlyBoundWrappedMethodDesc_DEBUG#124377
pavelsavara wants to merge 1 commit intodotnet:mainfrom
pavelsavara:fix_genmeth_debug

Conversation

@pavelsavara
Copy link
Member

@pavelsavara pavelsavara commented Feb 13, 2026

Problem

The debug-only verification functions FindTightlyBoundUnboxingStub_DEBUG and FindTightlyBoundWrappedMethodDesc_DEBUG in src/coreclr/vm/genmeth.cpp use a different iteration strategy than their production counterparts, causing them to miss certain MethodDescs and trigger false assertion failures.

The production functions (FindTightlyBoundUnboxingStub and FindTightlyBoundWrappedMethodDesc) use chunk-based iteration via MethodTable::IntroducedMethodIterator::GetNext(), which physically walks all MethodDescs in the MethodDescChunks owned by the MethodTable. This correctly enumerates every MethodDesc regardless of its slot assignment.

The debug functions used slot-based iteration via MethodTable::MethodIterator, which iterates slot indices 0..GetNumMethods()-1 and resolves each slot to a MethodDesc via GetMethodDescForSlot(). Additionally, each function applied a virtual/non-virtual filter:

  • FindTightlyBoundUnboxingStub_DEBUG filtered with IsVirtual() — only checked slots in the virtual range (index < GetNumVirtuals())
  • FindTightlyBoundWrappedMethodDesc_DEBUG filtered with !IsVirtual() — only checked slots in the non-virtual range (index >= GetNumVirtuals())

This causes failures because slot-based iteration can miss MethodDescs that are not reachable through the slot table, and the virtual/non-virtual filters further restrict the search to the wrong range.

Root cause

For generic value types, the MethodTableBuilder allocates tightly-bound unboxing stubs as follows (methodtablebuilder.cpp, around line 7113):

if (NeedsTightlyBoundUnboxingStub(*it))
{
    if (bmtGenerics->GetNumGenericArgs() == 0) {
        size += sizeof(MethodDesc::NonVtableSlot);  // non-generic: wrapped copy gets NonVtableSlot
    }
    else {
        bmtVT->cVtableSlots++;  // generic: wrapped copy gets a vtable slot
    }
}

For generic value types, the "wrapped" (non-unboxing) copy of each method is placed in a vtable slot (virtual range). But FindTightlyBoundWrappedMethodDesc_DEBUG only searched non-virtual slots via its !IsVirtual() filter, so it never found the wrapped method for generic value types and returned NULL.

The assertion at the call site then fails:

_ASSERTE(pResultMD == pMDescInCanonMT ||
         pResultMD == FindTightlyBoundWrappedMethodDesc_DEBUG(pMDescInCanonMT));

Because the production function correctly found the wrapped method (via chunk walk), but the debug function returned NULL (due to the wrong slot filter).

Why is this latent

The assertion only fires when FindOrCreateAssociatedMethodDesc is called with specific parameters that reach the FindTightlyBoundWrappedMethodDesc or FindTightlyBoundUnboxingStub code paths. These paths are exercised during:

  • JIT compilation: When getCallInfo resolves a virtual method on a generic value type with allowInstParam=TRUE, it reaches FindTightlyBoundWrappedMethodDesc.
  • Reflection: When FindOrCreateAssociatedMethodDescForReflection resolves a virtual method on a value type with forceBoxedEntryPoint=TRUE, it reaches FindTightlyBoundUnboxingStub.

Both paths are commonly exercised during normal execution, but the assertion failure only manifests in Debug builds. In Release builds, the _ASSERTE macros are no-ops, so the bug is invisible.

The assertion fires when the specific method being resolved has an unboxing stub whose slot assignment falls outside the range searched by the debug function — this is the common case for generic value types.

Fix

Changed both debug functions to use IntroducedMethodIterator (chunk-based iteration) instead of MethodIterator (slot-based iteration), matching the approach used by the production functions. Removed the IsVirtual() / !IsVirtual() filters since chunk-based iteration enumerates all MethodDescs directly without relying on slot indices.

Before (slot-based, with wrong filter)

MethodTable::MethodIterator it(pMD->GetCanonicalMethodTable());
it.MoveToEnd();
for (; it.IsValid(); it.Prev()) {
    if (it.IsVirtual()) {           // or !it.IsVirtual() for the other function
        MethodDesc* pCurMethod = it.GetMethodDesc();
        // ... matching logic ...
    }
}

After (chunk-based, no filter needed)

MethodTable::IntroducedMethodIterator it(pMD->GetCanonicalMethodTable());
for (; it.IsValid(); it.Next()) {
    MethodDesc* pCurMethod = it.GetMethodDesc();
    // ... matching logic ...
}

TODO Test - #124386

Added src/tests/Loader/classloader/generics/VSD/Struct_UnboxingStubLookup.cs which:

  1. Defines a generic struct Processor<T> implementing multiple interfaces (IProcessor<T>, IIdentity) with virtual methods and object overrides.
  2. Calls virtual methods on the generic struct through [MethodImpl(NoInlining)] helper methods with various type instantiations (reference types and value types). This triggers JIT's getCallInfoFindOrCreateAssociatedMethodDescFindTightlyBoundWrappedMethodDesc on the canonical method table.
  3. Exercises the reflection path (GetInterfaceMap on generic struct instantiations) to trigger FindOrCreateAssociatedMethodDescForReflectionFindTightlyBoundUnboxingStub.

In Debug builds without the fix, this test crashes with the assertion failure. With the fix, it passes.

@pavelsavara pavelsavara added this to the 11.0.0 milestone Feb 13, 2026
@pavelsavara pavelsavara self-assigned this Feb 13, 2026
@pavelsavara pavelsavara added arch-wasm WebAssembly architecture area-VM-coreclr labels Feb 13, 2026
Copilot AI review requested due to automatic review settings February 13, 2026 12:05
@dotnet-policy-service
Copy link
Contributor

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes debug-only functions in genmeth.cpp that verify the correctness of optimized method descriptor lookup logic for generic methods on value types.

Changes:

  • Replaced MethodTable::MethodIterator with MethodTable::IntroducedMethodIterator in two DEBUG helper functions
  • Removed incorrect virtual/non-virtual filtering logic that was using IsVirtual() checks
  • Simplified iteration from backward (MoveToEnd() + Prev()) to forward (Next())

@pavelsavara pavelsavara changed the title Fix genmeth debug Fix debug assertion in FindTightlyBoundUnboxingStub_DEBUG / FindTightlyBoundWrappedMethodDesc_DEBUG Feb 13, 2026
@pavelsavara pavelsavara added the NO-REVIEW Experimental/testing PR, do NOT review it label Feb 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-VM-coreclr NO-REVIEW Experimental/testing PR, do NOT review it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants