Skip to content

UnsafeAccessorType loads DiagnosticSource to Default ALC breaking StartupHook isolation on .NET 10+ #4924

@alexeypukhov

Description

@alexeypukhov

Update 04/21/2026

-> which eventually leaves us with two workarounds:

  1. Customers must adjust their dependency and explicitly reference our version of DS (during build time or maybe updating deps json)
  2. Bring back AdditionalDeps/Store env vraibles setup in a form of a separate tool - script or .net app - shipped within installation packages or separately. NOTE this - as before - will only support portable application only

UnsafeAccessorType loads DiagnosticSource to Default ALC breaking StartupHook isolation on .NET 10+ (post PR #4783)

Summary

The UnsafeAccessorTypeAttribute (introduced in .NET 10) loads System.Diagnostics.DiagnosticSource into Default ALC during .NET runtime initialization, bypassing StartupHook isolation and causing type/state drift across ALCs.

This issue is already reproducible on .NET 10 and affects StartupHook isolation deployments on .NET 10+.

Background: StartupHook Isolation Mode

Our assembly conflict resolution approach (introduced in PR #4783), when the native profiler is not installed, uses StartupHook isolation to avoid assembly conflicts and works as follows:

  • The entire customer application is loaded into a custom (isolated) AssemblyLoadContext
  • The isolated context is set to be the CurrentContextualReflectionContext (via customALC.EnterContextualReflection()) to support runtime reflection redirection
  • All dependencies the customer app brings go through isolated ALC overriden Load() method picking up the highest version (ours or app)
  • Only assemblies that must remain in Default ALC (like System.Private.CoreLib, which loads before our StartupHook) are excluded from isolation

Goal: Maximize isolation to prevent version conflicts between customer dependencies and our agent dependencies.

The Problem: UnsafeAccessorType Bypasses Isolation

.NET 10 introduced a new JIT-level reflection mechanism via UnsafeAccessorTypeAttribute. Unlike traditional managed reflection, it performs JIT-level type resolution that sticks to the ALC context of the requesting assembly, bypassing CurrentContextualReflectionContext

Why UnsafeAccessorType breaks StartupHook isolation: When the requesting assembly is in Default ALC, the runtime try to load it to Default ALC. As a result the requesting assembly and all its dependencies may leaked to Default ALC.

General impact: Any assembly in Default ALC using UnsafeAccessorType will cause a leak to Default ALC.

Why This Is Especially Critical for OpenTelemetry

The .NET runtime uses this during initialization to access types from DiagnosticSource:

[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "GetInstance")]
[return: UnsafeAccessorType("System.Diagnostics.Metrics.MetricsEventSource, System.Diagnostics.DiagnosticSource")]
static extern object GetInstance(
    [UnsafeAccessorType("System.Diagnostics.Metrics.MetricsEventSource, System.Diagnostics.DiagnosticSource")] object? _);

Source: EventSourceInitHelper in System.Private.CoreLib v10.0.0

What happens:

  1. Runtime code in Default ALC (System.Private.CoreLib) uses UnsafeAccessorType to load MetricsEventSource from DS during initialization
  2. CurrentContextualReflectionContext is ignored, the requesting assembly is in Default ALC and DS is found in TPA, therefore, the runtime loads DS immediately to Default ALC—our custom ALC's Load() method is not invoked
  3. Later, customer code in the isolated ALC requests DS, triggering our Load() method which loads DS (same or different version) into the isolated ALC
  4. Result: Two DS instances across ALCs → type drift and state drift

Why this is critical:

  • DiagnosticSource is foundational to OpenTelemetry and must exist in only one ALC
  • Already happening on .NET 10

Proposed Solutions

A clean technical fix is likely not possible here

Primary Solution: Exclude DiagnosticSource from Isolation

On .NET 10+, treat DiagnosticSource like System.Private.CoreLiballow it to "leak" to Default ALC and never attempt to load it into the isolated ALC.

Open questions:

  • What other assemblies must be excluded? We need to identify DS's dependency tree to ensure everything DS depends on is also available in Default ALC.
  • However, when OpenTelemetry switches to DS v11, this workaround will stop working on net10.0: the applications running on .net 10 will get DS v10 loaded to default ALC automatically while OpenTelemetry will require DS v11. So we have two options after that: 1) if v11 doesn't introduce new API that the OpenTelemetry depends on, we may want to stick to DS v10 on net10.0 or 2) force customers to synchronize the DS version to satisfy Otel requirement

Alternative Workarounds

While these don't prevent the leak, they can control which version leaks to Default ALC:

1. Customer dependency alignment:
Ask customers to reference our version of DS dependency directly in their application project. This ensures the leaked version is compatible.

2. Additional Dependencies (through .NET env variables DOTNET_ADDITIONAL_DEPS and DOTNET_SHARED_STORE, framework-dependent applications only):
Use the additionalDeps mechanism to ensure our DS version is what leaks to Default ALC (rather than the customer's potentially incompatible version).

  • NOTE: we may want to provide an installation script that builds the additionalDeps configuration based on our dependencies that we already ship in tracer-home folder

Assessment: Both alternatives address the same underlying reality—DS will leak to Default ALC due to UnsafeAccessor. The goal is ensuring the right version (compatible with our agent) is what leaks.

Related Issue

This issue is related to: Issue #4923 - UnsafeAccessorType breaks Native Profiler redirection. Both stem from UnsafeAccessorType bypassing our assembly conflict resolution mechanisms, but affect differently different deployment modes.

Runtime environment

  • OpenTelemetry Automatic Instrumentation version: Discovered during work on PR #4783
  • OS: All platforms
  • .NET version: .NET 10+
  • Deployment mode: StartupHook-only isolation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Backlog

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions