You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
With the managed StartupHook active and the CLR profiler disabled, reflective materialization of a
custom attribute defined in a shared-framework assembly silently returns null in application
code, while the underlying IL metadata stays intact.
Concretely, FieldInfo.GetCustomAttribute<TAttribute>() returns null for an attribute that is present, while FieldInfo.GetCustomAttributesData() still reports the same attribute with its named arguments intact.
EnumMemberAttribute lives in System.Runtime.Serialization.Primitives, which ships with the runtime.
The auto-instrumentation distribution contains no copy of that assembly, so this is not a version
conflict — it is the same bits resolved into a different load context. Queried from inside the
application:
That is a shared-framework assembly owned by the isolated ALC rather than delegated to AssemblyLoadContext.Default.
The failure is silent and affects application correctness, not just telemetry:
GetCustomAttribute<T>() returns null instead of throwing, so attribute-driven code takes its
"no attribute present" branch.
In our case an ASP.NET Core TypeConverter fell through to intrinsic Enum.Parse, which rejects the
wire format, producing HTTP 400 on valid input.
The reverse direction is worse: our converter had a ?? memberName fallback, so responses silently emitted the CLR member name instead of the wire value — wrong data, no error anywhere.
Any attribute-driven library (serializers, validators, model binders, DI conventions) that materializes
attributes from shared-framework assemblies is exposed.
Expected behavior
Enabling the StartupHook should not change the observable behaviour of reflection in the instrumented application: GetCustomAttribute<TAttribute>() should return an attribute that is present, and type identity should stay consistent for both framework and first-party types.
For the reproduction below that means 200 rather than a validation failure — as on 1.14.1, and as on 1.15.0+ whenever CORECLR_ENABLE_PROFILING=1.
Screenshots
Not applicable — HTTP status codes and log excerpts are included inline below.
Runtime environment (please complete the following information):
OpenTelemetry Automatic Instrumentation version: 1.15.0 and 1.16.0 affected; 1.14.1 not affected
OS: Linux (Debian, glibc), arm64 — container image mcr.microsoft.com/dotnet/aspnet:10.0
(originally encountered on a musl/arm64 image as well)
The 1.15.0+ stack trace references IsolatedSetup.Initialize. This lines up with the StartupHook
isolation strategy introduced in #4783.
CORECLR_ENABLE_PROFILING is the switch
The hook only enters IsolatedSetup when profiling is off. Five controlled runs — same published
application, same instrumentation payload, same image, and the first HTTP request is the test request:
#
Config
CORECLR_ENABLE_PROFILING
Init path
Result
A
StartupHook only
0
Isolation Initialization.
400
B
StartupHook + third-party native profiler loaded
1 (other CLSID)
Normal Initialization.
200
C
Third-party native profiler only, no hook
1 (other CLSID)
—
200
D
StartupHook + this project's native profiler
1 (OTel CLSID)
Normal Initialization.
200
E
StartupHook, =1, no profiler binary loaded at all
1
Normal Initialization.
200
Row E is the important one: absence of any loaded profiler was verified via /proc/1/maps, yet the
application works. Nothing needs to actually instrument anything — the hook merely reads the variable
and takes the non-isolated path. With a foreign CLSID it logs The CLR profiler is enabled, but a different profiler ID was provided '{...}' and proceeds normally.
Practical consequence: deployments that inject a different APM agent (which sets CORECLR_ENABLE_PROFILING=1) are unaffected, while StartupHook-only deployments break. That made this
look environment-specific for some time.
#4924 covers System.Diagnostics.DiagnosticSource being pulled into the Default ALC via UnsafeAccessorType, breaking isolation from the other direction and causing version drift for the
instrumentation itself.
This report looks like the mirror image, and we believe it is distinct:
A different assembly (System.Runtime.Serialization.Primitives) moving in the opposite
direction — claimed by the isolated ALC instead of leaking to Default.
The victim is application code, not the instrumentation. The impact is silent wrong behaviour in
the host application rather than degraded telemetry.
No UnsafeAccessorType is involved anywhere in this repro.
If the fix for #4924 is the general one hinted at there — excluding shared-framework assemblies from
isolation rather than special-casing DiagnosticSource — this is plausibly the same root cause and
would be fixed alongside it. We are filing separately because the failure mode, the assembly, and the
blast radius all differ, and because a DiagnosticSource-only exclusion would not fix this.
Suggested direction
Delegating shared-framework assemblies to AssemblyLoadContext.Default would fix this symptom, but is likely not sufficient — see this comment: the application's entry assembly itself executes inside the isolated ALC under 1.15.0 while a shadow copy remains in Default, which breaks identity for first-party types too, outside the scope of any framework exclusion. That also explains why our own diagnostic saw a single consistent copy — it was running isolated.
The more useful question: is running the application's entry assembly in the isolated ALC intended in 1.15.0, and are the resulting type-identity mismatches in scope upstream or expected to be handled application-side?
Workaround, for others hitting this
Read the attribute from metadata instead of materializing it, matched by name, which makes it
identity-agnostic:
Verified to return 200 under 1.15.0 on a cold first request — metadata is unaffected by the ALC split.
Alternatives: pin to 1.14.1, or set CORECLR_ENABLE_PROFILING=1 (works, but relies on the branch above
rather than on anything supported, so not recommended for production).
Reproduce
Steps to reproduce the behavior:
Create a net10.0 ASP.NET Core application with the following Program.cs:
usingSystem.ComponentModel;usingSystem.Globalization;usingSystem.Reflection;usingSystem.Runtime.Serialization;usingMicrosoft.AspNetCore.Mvc;varbuilder=WebApplication.CreateBuilder(args);builder.Services.AddControllers();varapp=builder.Build();app.MapControllers();app.Run();[TypeConverter(typeof(EnumMemberTypeConverter))]publicenumLinkedStatus{[EnumMember(Value="LINKED")]Linked=0,[EnumMember(Value="NOT_LINKED")]NotLinked=1,}publicclassQueryParameters{publicLinkedStatus?LinkedStatus{get;set;}}[ApiController][Route("test")]publicclassTestController:ControllerBase{[HttpGet]publicIActionResultGet([FromQuery]QueryParametersq)// complex model, resolved lazily=>Ok(new{status=q.LinkedStatus?.ToString()});}publicclassEnumMemberTypeConverter(Typetype):EnumConverter(type){publicoverrideobjectConvertFrom(ITypeDescriptorContextcontext,CultureInfoculture,objectvalue){if(valueis not strings)returnbase.ConvertFrom(context,culture,value);foreach(varnameinEnum.GetNames(EnumType)){varattr=EnumType.GetField(name)!.GetCustomAttribute<EnumMemberAttribute>(inherit:true);if(string.Equals(attr?.Value,s,StringComparison.OrdinalIgnoreCase))returnEnum.Parse(EnumType,name);}returnbase.ConvertFrom(context,culture,value);}}
Publish it: dotnet publish -c Release -o out
Download and extract opentelemetry-dotnet-instrumentation-linux-glibc-arm64.zip for v1.16.0
(or v1.15.0) into ./otel, so that ./otel/net/OpenTelemetry.AutoInstrumentation.StartupHook.dll
exists.
Run it on a stock runtime image with only the StartupHook configured and the profiler off:
Confirm the hook initialized and took the isolated path (see the log excerpt below). This step
matters — a hook that fails to initialize produces a false negative.
Issue the test request. This must be the first request the process serves (see note 1):
Actual on 1.15.0 / 1.16.0: 400 The value 'NOT_LINKED' is not valid for LinkedStatus.
Repeat step 4 with -e CORECLR_ENABLE_PROFILING=1 (no profiler binary needed) and observe 200.
Two reproduction gotchas
The attribute must first be materialized on a request thread. Any startup-time or main-thread
read primes it and masks the bug — including merely declaring an unrelated action with a top-level LinkedStatus? parameter elsewhere in the application. A console-application repro does not
reproduce. The complex [FromQuery] model matters because MVC resolves that converter lazily, per
request.
Verify the hook actually initialized. Setting DOTNET_STARTUP_HOOKS without OTEL_DOTNET_AUTO_HOME makes the hook throw and the application returns 200 — a false negative:
[Error] Error in StartupHook initialization
Exception: Could not load file or assembly 'OpenTelemetry.AutoInstrumentation.Loader, Culture=neutral, PublicKeyToken=null'.
at OpenTelemetry.AutoInstrumentation.IsolatedSetup.Initialize(String instrumentationHomePath) in /_/src/OpenTelemetry.AutoInstrumentation.StartupHook/IsolatedSetup.cs:line 37
[Information] Isolation Initialization.
[Information] Rule Engine: MinSupportedFrameworkRule evaluation success.
[Information] Rule Engine: OpenTelemetrySdkMinimumVersionRule evaluation success.
[Warning] CORECLR_ENABLE_PROFILING environment variable is not set to '1'. The CLR Profiler is disabled and no bytecode instrumentations are going to be injected.
[Information] StartupHook initialized successfully!
Working run, 1.14.1, same configuration — note Initialization. rather than Isolation Initialization.:
[Information] Rule Engine: MinSupportedFrameworkRule evaluation success.
[Information] Rule Engine: OpenTelemetrySdkMinimumVersionRule evaluation success.
[Warning] CORECLR_ENABLE_PROFILING environment variable is not set to '1'. The CLR Profiler is disabled and no bytecode instrumentations are going to be injected.
[Information] Initialization.
[Information] StartupHook initialized successfully!
Tip: React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it. Learn more here.
Symptom
Describe the bug
With the managed StartupHook active and the CLR profiler disabled, reflective materialization of a
custom attribute defined in a shared-framework assembly silently returns
nullin applicationcode, while the underlying IL metadata stays intact.
Concretely,
FieldInfo.GetCustomAttribute<TAttribute>()returnsnullfor an attribute that is present, whileFieldInfo.GetCustomAttributesData()still reports the same attribute with its named arguments intact.EnumMemberAttributelives inSystem.Runtime.Serialization.Primitives, which ships with the runtime.The auto-instrumentation distribution contains no copy of that assembly, so this is not a version
conflict — it is the same bits resolved into a different load context. Queried from inside the
application:
That is a shared-framework assembly owned by the isolated ALC rather than delegated to
AssemblyLoadContext.Default.The failure is silent and affects application correctness, not just telemetry:
GetCustomAttribute<T>()returnsnullinstead of throwing, so attribute-driven code takes its"no attribute present" branch.
TypeConverterfell through to intrinsicEnum.Parse, which rejects thewire format, producing HTTP 400 on valid input.
?? memberNamefallback, so responses silently emitted the CLR member name instead of the wire value — wrong data, no error anywhere.Any attribute-driven library (serializers, validators, model binders, DI conventions) that materializes
attributes from shared-framework assemblies is exposed.
Expected behavior
Enabling the StartupHook should not change the observable behaviour of reflection in the instrumented application:
GetCustomAttribute<TAttribute>()should return an attribute that is present, and type identity should stay consistent for both framework and first-party types.For the reproduction below that means
200rather than a validation failure — as on 1.14.1, and as on 1.15.0+ wheneverCORECLR_ENABLE_PROFILING=1.Screenshots
Not applicable — HTTP status codes and log excerpts are included inline below.
Runtime environment (please complete the following information):
mcr.microsoft.com/dotnet/aspnet:10.0(originally encountered on a musl/arm64 image as well)
net10.0, framework-dependent)Additional context
Version bisect
Initialization.Isolation Initialization.Isolation Initialization.The 1.15.0+ stack trace references
IsolatedSetup.Initialize. This lines up with the StartupHookisolation strategy introduced in #4783.
CORECLR_ENABLE_PROFILINGis the switchThe hook only enters
IsolatedSetupwhen profiling is off. Five controlled runs — same publishedapplication, same instrumentation payload, same image, and the first HTTP request is the test request:
CORECLR_ENABLE_PROFILING0Isolation Initialization.1(other CLSID)Normal Initialization.1(other CLSID)1(OTel CLSID)Normal Initialization.=1, no profiler binary loaded at all1Normal Initialization.Row E is the important one: absence of any loaded profiler was verified via
/proc/1/maps, yet theapplication works. Nothing needs to actually instrument anything — the hook merely reads the variable
and takes the non-isolated path. With a foreign CLSID it logs
The CLR profiler is enabled, but a different profiler ID was provided '{...}'and proceeds normally.Practical consequence: deployments that inject a different APM agent (which sets
CORECLR_ENABLE_PROFILING=1) are unaffected, while StartupHook-only deployments break. That made thislook environment-specific for some time.
Relationship to #4924
#4924 covers
System.Diagnostics.DiagnosticSourcebeing pulled into the Default ALC viaUnsafeAccessorType, breaking isolation from the other direction and causing version drift for theinstrumentation itself.
This report looks like the mirror image, and we believe it is distinct:
System.Runtime.Serialization.Primitives) moving in the oppositedirection — claimed by the isolated ALC instead of leaking to Default.
the host application rather than degraded telemetry.
UnsafeAccessorTypeis involved anywhere in this repro.CORECLR_ENABLE_PROFILINGswitch, which UnsafeAccessorType loads DiagnosticSource to Default ALC breaking StartupHook isolation on .NET 10+ #4924 does not mention.If the fix for #4924 is the general one hinted at there — excluding shared-framework assemblies from
isolation rather than special-casing
DiagnosticSource— this is plausibly the same root cause andwould be fixed alongside it. We are filing separately because the failure mode, the assembly, and the
blast radius all differ, and because a
DiagnosticSource-only exclusion would not fix this.Suggested direction
Delegating shared-framework assemblies to
AssemblyLoadContext.Defaultwould fix this symptom, but is likely not sufficient — see this comment: the application's entry assembly itself executes inside the isolated ALC under 1.15.0 while a shadow copy remains inDefault, which breaks identity for first-party types too, outside the scope of any framework exclusion. That also explains why our own diagnostic saw a single consistent copy — it was running isolated.The more useful question: is running the application's entry assembly in the isolated ALC intended in 1.15.0, and are the resulting type-identity mismatches in scope upstream or expected to be handled application-side?
Workaround, for others hitting this
Read the attribute from metadata instead of materializing it, matched by name, which makes it
identity-agnostic:
Verified to return 200 under 1.15.0 on a cold first request — metadata is unaffected by the ALC split.
Alternatives: pin to 1.14.1, or set
CORECLR_ENABLE_PROFILING=1(works, but relies on the branch aboverather than on anything supported, so not recommended for production).
Reproduce
Steps to reproduce the behavior:
Create a
net10.0ASP.NET Core application with the followingProgram.cs:Publish it:
dotnet publish -c Release -o outDownload and extract
opentelemetry-dotnet-instrumentation-linux-glibc-arm64.zipfor v1.16.0(or v1.15.0) into
./otel, so that./otel/net/OpenTelemetry.AutoInstrumentation.StartupHook.dllexists.
Run it on a stock runtime image with only the StartupHook configured and the profiler off:
Confirm the hook initialized and took the isolated path (see the log excerpt below). This step
matters — a hook that fails to initialize produces a false negative.
Issue the test request. This must be the first request the process serves (see note 1):
curl "http://localhost:5000/test?linkedStatus=NOT_LINKED"200 {"status":"NotLinked"}400 The value 'NOT_LINKED' is not valid for LinkedStatus.Repeat step 4 with
-e CORECLR_ENABLE_PROFILING=1(no profiler binary needed) and observe200.Two reproduction gotchas
The attribute must first be materialized on a request thread. Any startup-time or main-thread
read primes it and masks the bug — including merely declaring an unrelated action with a top-level
LinkedStatus?parameter elsewhere in the application. A console-application repro does notreproduce. The complex
[FromQuery]model matters because MVC resolves that converter lazily, perrequest.
Verify the hook actually initialized. Setting
DOTNET_STARTUP_HOOKSwithoutOTEL_DOTNET_AUTO_HOMEmakes the hook throw and the application returns200— a false negative:Log file
/var/log/opentelemetry/dotnet/otel-dotnet-auto-<pid>-<app>-StartupHook-<date>.logFailing run, 1.15.0 (
CORECLR_ENABLE_PROFILING=0):Working run, 1.14.1, same configuration — note
Initialization.rather thanIsolation Initialization.:Tip: React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding
+1orme too, to help us triage it. Learn more here.