VYPR
Medium severity5.3GHSA Advisory· Published May 6, 2026· Updated May 11, 2026

CVE-2026-41310

CVE-2026-41310

Description

OpenTelemetry.Exporter.Zipkin is the .NET Zipkin exporter for OpenTelemetry. In versions 1.15.2 and earlier, the Zipkin exporter remote endpoint cache accepts unbounded key growth derived from span attributes. In high-cardinality scenarios, a process using Zipkin export for client or producer spans could experience avoidable memory growth under sustained unique remote endpoint values, increasing process memory usage over time and degrading availability. This issue is fixed in version 1.15.3, which introduces a bounded, thread-safe LRU cache for remote endpoints with a fixed maximum size.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
OpenTelemetry.Exporter.ZipkinNuGet
< 1.15.31.15.3

Affected products

2

Patches

1
c724f4bd6fd8

[Exporter.Zipkin] Harden memory usage for endpoint caching and array tag serialization (#7081)

https://github.com/open-telemetry/opentelemetry-dotnetPiotr KiełkowiczApr 17, 2026via ghsa
8 files changed · +280 6
  • src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md+3 0 modified
    @@ -6,6 +6,9 @@ Notes](../../RELEASENOTES.md).
     
     ## Unreleased
     
    +* Harden memory usage for endpoint caching and array tag serialization.
    +  ([#7081](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7081))
    +
     ## 1.15.2
     
     Released 2026-Apr-08
    
  • src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinActivityConversionExtensions.cs+11 6 modified
    @@ -1,7 +1,6 @@
     // Copyright The OpenTelemetry Authors
     // SPDX-License-Identifier: Apache-2.0
     
    -using System.Collections.Concurrent;
     using System.Diagnostics;
     using OpenTelemetry.Internal;
     using OpenTelemetry.Trace;
    @@ -11,11 +10,12 @@ namespace OpenTelemetry.Exporter.Zipkin.Implementation;
     internal static class ZipkinActivityConversionExtensions
     {
         internal const string ZipkinErrorFlagTagName = "error";
    +    internal const int MaxRemoteEndpointCacheSize = 1024;
         private const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
         private const long UnixEpochTicks = 621355968000000000L; // = DateTimeOffset.FromUnixTimeMilliseconds(0).Ticks
         private const long UnixEpochMicroseconds = UnixEpochTicks / TicksPerMicrosecond;
     
    -    private static readonly ConcurrentDictionary<(string, int), ZipkinEndpoint> RemoteEndpointCache = new();
    +    private static readonly ZipkinEndpointLruCache RemoteEndpointCache = new(MaxRemoteEndpointCacheSize);
     
         internal static ZipkinSpan ToZipkinSpan(this Activity activity, ZipkinEndpoint localEndpoint, bool useShortTraceIds = false)
         {
    @@ -60,6 +60,12 @@ internal static long ToEpochMicroseconds(this DateTimeOffset dateTimeOffset)
         internal static long ToEpochMicroseconds(this TimeSpan timeSpan)
             => timeSpan.Ticks / TicksPerMicrosecond;
     
    +    internal static int GetRemoteEndpointCacheCount()
    +        => RemoteEndpointCache.Count;
    +
    +    internal static void ClearRemoteEndpointCache()
    +        => RemoteEndpointCache.Clear();
    +
         internal static long ToEpochMicroseconds(this DateTime utcDateTime)
         {
             // Truncate sub-microsecond precision before offsetting by the Unix Epoch to avoid
    @@ -204,13 +210,12 @@ private static void ExtractActivitySource(Activity activity, ref PooledList<KeyV
     
             static ZipkinEndpoint? TryCreateEndpoint(string? remoteEndpoint)
             {
    -            if (remoteEndpoint != null)
    +            if (remoteEndpoint == null)
                 {
    -                var endpoint = RemoteEndpointCache.GetOrAdd((remoteEndpoint, default), ZipkinEndpoint.Create);
    -                return endpoint;
    +                return null;
                 }
     
    -            return null;
    +            return RemoteEndpointCache.GetOrAdd(remoteEndpoint);
             }
     
             var remoteEndpoint = activity.GetTagItem(SemanticConventions.AttributePeerService) as string;
    
  • src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpointLruCache.cs+82 0 added
    @@ -0,0 +1,82 @@
    +// Copyright The OpenTelemetry Authors
    +// SPDX-License-Identifier: Apache-2.0
    +
    +using OpenTelemetry.Internal;
    +
    +namespace OpenTelemetry.Exporter.Zipkin.Implementation;
    +
    +internal sealed class ZipkinEndpointLruCache
    +{
    +    private readonly int capacity;
    +    private readonly Dictionary<string, LinkedListNode<CacheEntry>> cache;
    +    private readonly LinkedList<CacheEntry> lruList = new();
    +    private readonly Lock sync = new();
    +
    +    public ZipkinEndpointLruCache(int capacity)
    +    {
    +        Guard.ThrowIfOutOfRange(capacity, min: 1);
    +
    +        this.capacity = capacity;
    +        this.cache = new Dictionary<string, LinkedListNode<CacheEntry>>(StringComparer.Ordinal);
    +    }
    +
    +    public int Count
    +    {
    +        get
    +        {
    +            lock (this.sync)
    +            {
    +                return this.cache.Count;
    +            }
    +        }
    +    }
    +
    +    public ZipkinEndpoint GetOrAdd(string serviceName)
    +    {
    +        Guard.ThrowIfNullOrWhitespace(serviceName);
    +
    +        lock (this.sync)
    +        {
    +            if (this.cache.TryGetValue(serviceName, out var existingNode))
    +            {
    +                this.lruList.Remove(existingNode);
    +                this.lruList.AddFirst(existingNode);
    +                return existingNode.Value.Endpoint;
    +            }
    +
    +            var endpoint = ZipkinEndpoint.Create(serviceName);
    +            var createdNode = new LinkedListNode<CacheEntry>(new CacheEntry(serviceName, endpoint));
    +
    +            this.lruList.AddFirst(createdNode);
    +            this.cache[serviceName] = createdNode;
    +
    +            if (this.cache.Count > this.capacity)
    +            {
    +                var nodeToEvict = this.lruList.Last;
    +                if (nodeToEvict != null)
    +                {
    +                    this.lruList.RemoveLast();
    +                    this.cache.Remove(nodeToEvict.Value.ServiceName);
    +                }
    +            }
    +
    +            return endpoint;
    +        }
    +    }
    +
    +    public void Clear()
    +    {
    +        lock (this.sync)
    +        {
    +            this.cache.Clear();
    +            this.lruList.Clear();
    +        }
    +    }
    +
    +    private readonly record struct CacheEntry(string ServiceName, ZipkinEndpoint Endpoint)
    +    {
    +        public string ServiceName { get; } = ServiceName;
    +
    +        public ZipkinEndpoint Endpoint { get; } = Endpoint;
    +    }
    +}
    
  • src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj+1 0 modified
    @@ -17,6 +17,7 @@
         <Compile Include="$(RepoRoot)\src\Shared\ActivityHelperExtensions.cs" Link="Includes\ActivityHelperExtensions.cs" />
         <Compile Include="$(RepoRoot)\src\Shared\EnvironmentVariables\*.cs" Link="Includes\EnvironmentVariables\%(Filename).cs" />
         <Compile Include="$(RepoRoot)\src\Shared\ExceptionExtensions.cs" Link="Includes\ExceptionExtensions.cs" />
    +    <Compile Include="$(RepoRoot)\src\Shared\Shims\Lock.cs" Link="Includes\Shims\Lock.cs" />
         <Compile Include="$(RepoRoot)\src\Shared\Options\*.cs" Link="Includes\Options\%(Filename).cs" />
         <Compile Include="$(RepoRoot)\src\Shared\SemanticConventions.cs" Link="Includes\SemanticConventions.cs" />
         <Compile Include="$(RepoRoot)\src\Shared\Shims\NullableAttributes.cs" Link="Includes\Shims\NullableAttributes.cs" />
    
  • src/Shared/TagWriter/JsonStringArrayTagWriter.cs+7 0 modified
    @@ -36,6 +36,8 @@ internal readonly struct JsonArrayTagWriterState(MemoryStream stream, Utf8JsonWr
     
         internal sealed class JsonArrayTagWriter : ArrayTagWriter<JsonArrayTagWriterState>
         {
    +        private const int MaxThreadStaticStreamCapacity = 64 * 1024;
    +
             [ThreadStatic]
             private static MemoryStream? threadStream;
     
    @@ -91,6 +93,11 @@ private static JsonArrayTagWriterState EnsureWriter()
                 else
                 {
                     threadStream.SetLength(0);
    +                if (threadStream.Capacity > MaxThreadStaticStreamCapacity)
    +                {
    +                    threadStream.Capacity = 0;
    +                }
    +
                     threadWriter!.Reset(threadStream);
                     return new(threadStream, threadWriter);
                 }
    
  • test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityExporterRemoteEndpointTests.cs+67 0 modified
    @@ -2,6 +2,7 @@
     // SPDX-License-Identifier: Apache-2.0
     
     using OpenTelemetry.Exporter.Zipkin.Tests;
    +using OpenTelemetry.Trace;
     using Xunit;
     
     namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests;
    @@ -33,4 +34,70 @@ public void GenerateActivity_RemoteEndpointResolutionPriority(RemoteEndpointPrio
             Assert.NotNull(zipkinSpan.RemoteEndpoint);
             Assert.Equal(testCase.ExpectedResult, zipkinSpan.RemoteEndpoint.ServiceName);
         }
    +
    +    [Fact]
    +    public void GenerateActivity_RemoteEndpointCacheIsBounded()
    +    {
    +        ZipkinActivityConversionExtensions.ClearRemoteEndpointCache();
    +
    +        for (var i = 0; i < ZipkinActivityConversionExtensions.MaxRemoteEndpointCacheSize + 200; i++)
    +        {
    +            using var activity = ZipkinActivitySource.CreateTestActivity(
    +                additionalAttributes: new Dictionary<string, object>
    +                {
    +                    [SemanticConventions.AttributePeerService] = $"service-{i}",
    +                });
    +
    +            var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint);
    +            Assert.Equal($"service-{i}", zipkinSpan.RemoteEndpoint?.ServiceName);
    +        }
    +
    +        Assert.Equal(
    +            ZipkinActivityConversionExtensions.MaxRemoteEndpointCacheSize,
    +            ZipkinActivityConversionExtensions.GetRemoteEndpointCacheCount());
    +    }
    +
    +    [Fact]
    +    public void GenerateActivity_RemoteEndpointCacheEvictsLeastRecentlyUsedEntry()
    +    {
    +        ZipkinActivityConversionExtensions.ClearRemoteEndpointCache();
    +
    +        ZipkinEndpoint? firstEndpoint = null;
    +
    +        for (var i = 0; i < ZipkinActivityConversionExtensions.MaxRemoteEndpointCacheSize; i++)
    +        {
    +            using var activity = ZipkinActivitySource.CreateTestActivity(
    +                additionalAttributes: new Dictionary<string, object>
    +                {
    +                    [SemanticConventions.AttributePeerService] = $"service-{i}",
    +                });
    +
    +            var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint);
    +            if (i == 0)
    +            {
    +                firstEndpoint = zipkinSpan.RemoteEndpoint;
    +            }
    +        }
    +
    +        using var overflowActivity = ZipkinActivitySource.CreateTestActivity(
    +            additionalAttributes: new Dictionary<string, object>
    +            {
    +                [SemanticConventions.AttributePeerService] = "service-overflow",
    +            });
    +        _ = overflowActivity.ToZipkinSpan(DefaultZipkinEndpoint);
    +
    +        using var evictedEntryActivity = ZipkinActivitySource.CreateTestActivity(
    +            additionalAttributes: new Dictionary<string, object>
    +            {
    +                [SemanticConventions.AttributePeerService] = "service-0",
    +            });
    +        var evictedEntrySpan = evictedEntryActivity.ToZipkinSpan(DefaultZipkinEndpoint);
    +
    +        Assert.NotNull(firstEndpoint);
    +        Assert.NotNull(evictedEntrySpan.RemoteEndpoint);
    +        Assert.NotSame(firstEndpoint, evictedEntrySpan.RemoteEndpoint);
    +        Assert.Equal(
    +            ZipkinActivityConversionExtensions.MaxRemoteEndpointCacheSize,
    +            ZipkinActivityConversionExtensions.GetRemoteEndpointCacheCount());
    +    }
     }
    
  • test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinEndpointLruCacheTests.cs+80 0 added
    @@ -0,0 +1,80 @@
    +// Copyright The OpenTelemetry Authors
    +// SPDX-License-Identifier: Apache-2.0
    +
    +using System.Collections.Concurrent;
    +using Xunit;
    +
    +namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests;
    +
    +public class ZipkinEndpointLruCacheTests
    +{
    +    [Fact]
    +    public void GetOrAdd_ReturnsSameValueForExistingKey()
    +    {
    +        var cache = new ZipkinEndpointLruCache(capacity: 2);
    +
    +        var first = cache.GetOrAdd("service-a");
    +        var second = cache.GetOrAdd("service-a");
    +
    +        Assert.Same(first, second);
    +        Assert.Equal(1, cache.Count);
    +    }
    +
    +    [Fact]
    +    public void GetOrAdd_EvictsLeastRecentlyUsedEntry()
    +    {
    +        var cache = new ZipkinEndpointLruCache(capacity: 2);
    +
    +        var first = cache.GetOrAdd("service-a");
    +        _ = cache.GetOrAdd("service-b");
    +        _ = cache.GetOrAdd("service-c");
    +
    +        var firstAfterEviction = cache.GetOrAdd("service-a");
    +
    +        Assert.NotSame(first, firstAfterEviction);
    +        Assert.Equal(2, cache.Count);
    +    }
    +
    +    [Fact]
    +    public void Clear_RemovesAllEntries()
    +    {
    +        var cache = new ZipkinEndpointLruCache(capacity: 3);
    +
    +        _ = cache.GetOrAdd("service-a");
    +        _ = cache.GetOrAdd("service-b");
    +
    +        cache.Clear();
    +
    +        Assert.Equal(0, cache.Count);
    +    }
    +
    +    [Fact]
    +    public void GetOrAdd_ForSameKeyCreatesSingleInstanceAcrossThreads()
    +    {
    +        var cache = new ZipkinEndpointLruCache(capacity: 8);
    +
    +        var createdEndpoints = new ConcurrentDictionary<ZipkinEndpoint, byte>();
    +
    +        Parallel.For(0, 500, _ =>
    +        {
    +            var endpoint = cache.GetOrAdd("shared-service");
    +            createdEndpoints.TryAdd(endpoint, 0);
    +        });
    +
    +        Assert.Single(createdEndpoints);
    +        Assert.Equal(1, cache.Count);
    +    }
    +
    +    [Fact]
    +    public void GetOrAdd_IsThreadSafeAndStaysBounded()
    +    {
    +        var cache = new ZipkinEndpointLruCache(capacity: 64);
    +
    +        Parallel.For(0, 10_000, i =>
    +        {
    +            _ = cache.GetOrAdd($"service-{i % 512}");
    +        });
    +
    +        Assert.True(cache.Count <= 64);
    +    }
    +}
    
  • test/OpenTelemetry.Tests/Internal/JsonStringArrayTagWriterTests.cs+29 0 modified
    @@ -120,6 +120,35 @@ public void DoubleArray(double[] data, string expectedValue)
         public void ObjectArray(object?[] data, string expectedValue)
             => VerifySerialization(data, expectedValue);
     
    +    [Fact]
    +    public void ThreadStaticStreamCapacityIsReducedAfterLargeWrite()
    +    {
    +        var streamField = typeof(JsonStringArrayTagWriter<TestTagWriter.Tag>.JsonArrayTagWriter).GetField("threadStream", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
    +        var writerField = typeof(JsonStringArrayTagWriter<TestTagWriter.Tag>.JsonArrayTagWriter).GetField("threadWriter", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
    +        Assert.NotNull(streamField);
    +        Assert.NotNull(writerField);
    +
    +        var largeData = new[] { new string('x', 128 * 1024) };
    +        VerifySerialization(largeData, $"""["{largeData[0]}"]""");
    +
    +        var largeStream = (MemoryStream?)streamField.GetValue(null);
    +        var largeWriter = writerField.GetValue(null);
    +        Assert.NotNull(largeStream);
    +        Assert.NotNull(largeWriter);
    +        Assert.True(largeStream.Capacity > 64 * 1024);
    +
    +        string[] smallData = ["small"];
    +        VerifySerialization(smallData, """["small"]""");
    +
    +        var reusedStream = (MemoryStream?)streamField.GetValue(null);
    +        var reusedWriter = writerField.GetValue(null);
    +        Assert.NotNull(reusedStream);
    +        Assert.NotNull(reusedWriter);
    +        Assert.Same(largeStream, reusedStream);
    +        Assert.Same(largeWriter, reusedWriter);
    +        Assert.True(reusedStream.Capacity <= 64 * 1024);
    +    }
    +
         private static void VerifySerialization(Array data, string expectedValue)
         {
             TestTagWriter.Tag tag = default;
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.