Skip to content

Add IJW profiler regression test for transition FunctionID (#120151) - #131578

Open
lateralusX wants to merge 5 commits into
dotnet:mainfrom
lateralusX:lateralusX/add-regression-test-120151
Open

Add IJW profiler regression test for transition FunctionID (#120151)#131578
lateralusX wants to merge 5 commits into
dotnet:mainfrom
lateralusX:lateralusX/add-regression-test-120151

Conversation

@lateralusX

Copy link
Copy Markdown
Member

When a C++/CLI (IJW) method invokes a managed method through a raw native function pointer, the resulting reverse (unmanaged->managed) marshaling stub emits a profiler code-transition callback. Prior to #117901 that stub loaded its secret argument (a UMEntryThunkData*) as the "MethodDesc", so the profiler received a bogus, non-null FunctionID; calling GetFunctionInfo on it crashed with an access violation. The fix routes the stub to EmitLoadNullPtr so the transition reports a NULL FunctionID, but nothing guarded the behavior.

Add a self-contained Windows-only profiler test under src/tests/profiler/ijw that reproduces the scenario (managed -> native 'call' -> managed target by pointer) and validates the transition contract:

  • every non-null transition FunctionID must resolve via GetFunctionInfo;
  • the by-pointer target is reported CALL on entry and RETURN on exit;
  • the nested reverse-stub transitions surrounding it must report a NULL FunctionID rather than a bogus pointer.

On unfixed runtimes the test fails via the GetFunctionInfo access violation.

…0151)

When a C++/CLI (IJW) method invokes a managed method through a raw native
function pointer, the resulting reverse (unmanaged->managed) marshaling stub
emits a profiler code-transition callback. Prior to dotnet#117901 that stub loaded
its secret argument (a UMEntryThunkData*) as the "MethodDesc", so the profiler
received a bogus, non-null FunctionID; calling GetFunctionInfo on it crashed
with an access violation. The fix routes the stub to EmitLoadNullPtr so the
transition reports a NULL FunctionID, but nothing guarded the behavior.

Add a self-contained Windows-only profiler test under src/tests/profiler/ijw
that reproduces the scenario (managed -> native 'call' -> managed target by
pointer) and validates the transition contract:

- every non-null transition FunctionID must resolve via GetFunctionInfo;
- the by-pointer target is reported CALL on entry and RETURN on exit;
- the nested reverse-stub transitions surrounding it must report a NULL
  FunctionID rather than a bogus pointer.

On unfixed runtimes the test fails via the GetFunctionInfo access violation.
Copilot AI review requested due to automatic review settings July 30, 2026 09:01
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a Windows-only IJW (C++/CLI) profiler regression test to validate code-transition callback behavior when a managed method is invoked via a raw native function pointer, along with a new native profiler implementation used by the test.

Changes:

  • Added a new IjwProfiler implementation under the shared test profiler DLL to validate transition FunctionID validity and expected transition shape.
  • Added a new IJW profilee mixed-mode DLL (IjwProfileeDll) plus managed harness (ijw.csproj / ijw.cs) to drive the repro scenario.
  • Wired the new profiler into the native profiler build (CMakeLists.txt) and COM class factory.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/tests/profiler/native/ijw/ijwprofiler.h Defines the new IJW-focused profiler used by the regression test.
src/tests/profiler/native/ijw/ijwprofiler.cpp Implements transition validation logic and pass/fail reporting for the profiler test.
src/tests/profiler/native/CMakeLists.txt Adds the new IJW profiler implementation to the shared profiler DLL build.
src/tests/profiler/native/classfactory.cpp Registers the new profiler CLSID so it can be instantiated by the test runner.
src/tests/profiler/ijw/IjwProfileeDll.cpp Mixed-mode IJW profilee implementing the managed→native→managed-by-pointer call shape.
src/tests/profiler/ijw/ijw.csproj New Windows-only profiler test project that builds the native pieces and runs under ProfilerTestRunner.
src/tests/profiler/ijw/ijw.cs Managed harness that launches the profiled process and triggers the IJW-by-pointer scenario.
src/tests/profiler/ijw/CMakeLists.txt CMake project for building/installing the IJW mixed-mode profilee DLL.
Comments suppressed due to low confidence (2)

src/tests/profiler/native/ijw/ijwprofiler.h:44

  • Track whether any nested transitions were observed (and whether they were NULL FunctionID) so Shutdown() can assert the contract was exercised, not just that no non-null value was seen incidentally.
private:
    std::atomic<int> _failures;
    std::atomic<int> _transitions;

src/tests/profiler/native/ijw/ijwprofiler.cpp:124

  • The nested-transition check only fails on non-null FunctionID, but it doesn’t record that a nested transition was actually seen (and that it was NULL). Incrementing counters here lets Shutdown() assert the contract was exercised and reduces the chance of a false pass if the runtime stops reporting these transitions.
    // Any non-target transition seen while executing the managed target is a
    // nested reverse (unmanaged->managed) marshaling stub. Post-fix these report
    // a NULL FunctionID; a non-null value here is exactly the 120151 bug (a
    // resolvable-but-wrong pointer would slip past the check above).
    if (_insideTarget.load() && functionID != 0)
    {
        _failures++;
        printf("FAIL: nested %s inside target reported non-null FunctionID=0x%p (reason=%d)\n",
               which, (void*)functionID, (int)reason);
        fflush(stdout);
    }

Comment thread src/tests/profiler/native/ijw/ijwprofiler.h
Comment thread src/tests/profiler/native/ijw/ijwprofiler.cpp
Copilot AI review requested due to automatic review settings July 30, 2026 10:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

src/tests/profiler/ijw/ijw.csproj:8

  • is already set for all profiler tests via src/tests/profiler/Directory.Build.targets, so the project-local property (and the comment claiming it’s needed for CMakeProjectReference) is redundant and potentially misleading.
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <!-- Needed for CMakeProjectReference -->
    <RequiresProcessIsolation>true</RequiresProcessIsolation>
    <CrossGenTest>false</CrossGenTest>

src/tests/profiler/ijw/ijw.cs:23

  • The reflection-based load path can throw or null-ref with little context (e.g., GetType/GetMethod returning null). Adding explicit error messages here makes failures actionable if the IJW profilee DLL isn’t found or is missing the expected type/method.
            Assembly ijwDll = Assembly.Load("IjwProfileeDll");
            Type testType = ijwDll.GetType("TestClass");
            object testInstance = Activator.CreateInstance(testType);
            MethodInfo method = testType.GetMethod("CallManagedFunctionByPointer");
            return (int)method.Invoke(testInstance, null);

src/tests/profiler/native/ijw/ijwprofiler.cpp:23

  • IjwProfiler::Initialize assumes pCorProfilerInfo is non-null after Profiler::Initialize, but Profiler::Initialize returns S_OK even if QI fails and can leave pCorProfilerInfo == nullptr. This can lead to a null dereference when calling SetEventMask2, obscuring the actual failure mode.
    Profiler::Initialize(pICorProfilerInfoUnk);

    HRESULT hr = S_OK;
    if (FAILED(hr = pCorProfilerInfo->SetEventMask2(COR_PRF_MONITOR_CODE_TRANSITIONS | COR_PRF_DISABLE_INLINING, 0)))
    {

Copilot AI review requested due to automatic review settings July 30, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 30, 2026 17:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/tests/profiler/ijw/IjwProfileeDll.cpp:17

  • The profilee depends on the compiler's per-function /clr defaults to get the required repro shape (native helper calling a managed target via raw function pointer). Most existing IJW tests explicitly use #pragma managed / #pragma unmanaged to avoid this being a compiler-version-dependent detail; without explicit pragmas this test could silently stop exercising the reverse-stub path (e.g., if call or ManagedByPointerTarget ends up compiled in the wrong mode). Consider making the managed/unmanaged boundaries explicit here and updating the comment accordingly.
// Compiled with default /clr per-function codegen (no #pragma managed/unmanaged);
// the reporter's sample used a file-static function, but a named function
// reproduces identically and lets the profiler match it by name.
__declspec(noinline) void ManagedByPointerTarget() {}
__declspec(noinline) static void call(void (*f)()) { f(); }

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants