Skip to content

Checked build JIT assertion (pfnHelper != NULL) when accessing __declspec(thread) TLS from IJW/C++CLI managed entry points #133538

Description

@jkoritzinsky

Description

When a mixed-mode (IJW/C++/CLI) assembly declares a native, file-scope __declspec(thread) global (wrapped in #pragma unmanaged/#pragma managed) and that global is accessed from a public ref class static method, calling into that method from managed code under a Checked CoreCLR build crashes with a JIT assertion:

Assert failure(PID ####): pfnHelper != (PCODE)NULL

CORECLR! CEECodeGenInfo::getHelperFtn + 0x4E7
CLRJIT! Compiler::compGetHelperFtn + 0x35
CLRJIT! Lowering::LowerDirectCall + 0xA5
CLRJIT! Lowering::LowerCall + 0x429
CLRJIT! Lowering::LowerNode + 0x32C
CLRJIT! Lowering::DoPhase + 0x13B
CLRJIT! Phase::Run + 0x76
CLRJIT! Compiler::compCompile + 0x1AD2
CLRJIT! Compiler::compCompileHelper + 0xC51
CLRJIT! Compiler::compCompileAfterInit + 0x7B1
    File: src\coreclr\vm\jitinterface.cpp:11353

The assertion fires in CEECodeGenInfo::getHelperFtn (src/coreclr/vm/jitinterface.cpp:11404, in a FEATURE_PORTABLE_ENTRYPOINTS code path), where the JIT's Lowering::LowerDirectCall requests a JIT helper function pointer and the helper table entry is unpopulated.

Repro

Minimal repro (Windows, x64, Checked build):

test.cpp (compiled as an IJW/C++CLI assembly, e.g. via /clr):

#pragma unmanaged
__declspec(thread) int s_tlsFieldData = 51966;
#pragma managed

public ref class TlsTest
{
public:
    static int Test()
    {
        int value = s_tlsFieldData;
        s_tlsFieldData = value;
        s_tlsFieldData = 100;
        return s_tlsFieldData;
    }
};

Managed caller (any invocation mechanism — both plain reflection MethodInfo.Invoke and UnsafeAccessor reproduce identically):

Assembly a = Assembly.Load("TestTLSNative");
int result = (int)a.GetType("TlsTest").GetMethod("Test").Invoke(null, null);

Running this against a Checked CoreCLR build crashes with the assertion above. The same code runs successfully against Debug and Release builds. Switching the native storage from __declspec(thread) to a managed [System::ThreadStatic] field on the ref class avoids the crash entirely.

Analysis so far

  • The crash is independent of the managed→native call mechanism: it reproduces identically whether the native method is invoked via reflection (MethodInfo.Invoke) or via [UnsafeAccessor]/[UnsafeAccessorType].
  • The crash is tied specifically to __declspec(thread) TLS storage accessed through the IJW-hosted managed entry point; using [System::ThreadStatic] instead does not reproduce the crash.
  • This was discovered while converting src/tests regression tests that originally used IL .data tls declarations to C++/CLI (see Convert .data tls IL tests to C++/CLI #133533), where a reviewer requested using true native __declspec(thread) TLS (rather than [System::ThreadStatic]) "to actually be testing the right feature." Reintroducing __declspec(thread) reproduced this crash in all four converted tests.

Configuration

  • OS: Windows x64
  • Build: Checked
  • Repro requires /clr (IJW/C++CLI) mixed-mode assembly

Note

This issue was filed with assistance from GitHub Copilot.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    • Status
      No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions