Skip to content

Commit a2b6372

Browse files
[API] Add propagation fuzz tests and fix findings (#7061)
1 parent fc1a286 commit a2b6372

29 files changed

Lines changed: 1701 additions & 236 deletions

OpenTelemetry.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@
219219
<Project Path="src/OpenTelemetry/OpenTelemetry.csproj" />
220220
<Project Path="test/Benchmarks/Benchmarks.csproj" />
221221
<Project Path="test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj" />
222+
<Project Path="test/OpenTelemetry.Api.FuzzTests/OpenTelemetry.Api.FuzzTests.csproj" />
222223
<Project Path="test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/OpenTelemetry.Api.ProviderBuilderExtensions.Tests.csproj" />
223224
<Project Path="test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj" />
224225
<Project Path="test/OpenTelemetry.Exporter.Console.Tests/OpenTelemetry.Exporter.Console.Tests.csproj" />
@@ -228,6 +229,7 @@
228229
<Project Path="test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests.csproj" />
229230
<Project Path="test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj" />
230231
<Project Path="test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj" />
232+
<Project Path="test/OpenTelemetry.Extensions.Propagators.FuzzTests/OpenTelemetry.Extensions.Propagators.FuzzTests.csproj" />
231233
<Project Path="test/OpenTelemetry.Extensions.Propagators.Tests/OpenTelemetry.Extensions.Propagators.Tests.csproj" />
232234
<Project Path="test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj" />
233235
<Project Path="test/OpenTelemetry.Shims.OpenTracing.Tests/OpenTelemetry.Shims.OpenTracing.Tests.csproj" />

src/OpenTelemetry.Api/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ Notes](../../RELEASENOTES.md).
66

77
## Unreleased
88

9+
* Fix baggage and trace headers not respecting the maximum length in some cases.
10+
([#7061](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7061))
11+
12+
* Improve efficiency of parsing of baggage and B3 propagation headers.
13+
([#7061](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7061))
14+
915
## 1.15.2
1016

1117
Released 2026-Apr-08

src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs

Lines changed: 89 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -195,65 +195,111 @@ private static PropagationContext ExtractFromSingleHeader<T>(PropagationContext
195195
{
196196
try
197197
{
198-
var header = getter(carrier, XB3Combined)?.FirstOrDefault();
199-
if (string.IsNullOrWhiteSpace(header))
198+
var headers = getter(carrier, XB3Combined);
199+
if (headers == null)
200200
{
201201
return context;
202202
}
203203

204-
var parts =
205-
#if NET
206-
header.Split(XB3CombinedDelimiter);
207-
#else
208-
header!.Split(XB3CombinedDelimiter);
209-
#endif
204+
var header = headers.FirstOrDefault();
210205

211-
if (parts.Length is < 2 or > 4)
212-
{
213-
return context;
214-
}
206+
return string.IsNullOrWhiteSpace(header)
207+
? context
208+
: !TryExtractSingleHeaderContext(header, out var traceId, out var spanId, out var traceOptions)
209+
? context
210+
: new PropagationContext(
211+
new ActivityContext(traceId, spanId, traceOptions, isRemote: true),
212+
context.Baggage);
213+
}
214+
catch (Exception e)
215+
{
216+
OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e);
217+
return context;
218+
}
219+
}
215220

216-
var traceIdStr = parts[0];
217-
if (string.IsNullOrWhiteSpace(traceIdStr))
218-
{
219-
return context;
220-
}
221+
private static bool TryExtractSingleHeaderContext(
222+
string header,
223+
out ActivityTraceId traceId,
224+
out ActivitySpanId spanId,
225+
out ActivityTraceFlags traceOptions)
226+
{
227+
traceId = default;
228+
spanId = default;
229+
traceOptions = ActivityTraceFlags.None;
230+
231+
var headerValue = header.AsSpan();
232+
var position = 0;
233+
var traceIdStr = ReadNextPart(headerValue, position, out position);
234+
if (position >= headerValue.Length || traceIdStr.IsEmpty)
235+
{
236+
return false;
237+
}
238+
239+
var spanIdStr = ReadNextPart(headerValue, position, out position);
240+
if (spanIdStr.IsEmpty)
241+
{
242+
return false;
243+
}
221244

222-
if (traceIdStr.Length == 16)
245+
ReadOnlySpan<char> traceFlagsStr = default;
246+
if (position < headerValue.Length)
247+
{
248+
traceFlagsStr = ReadNextPart(headerValue, position, out position);
249+
if (position < headerValue.Length)
223250
{
224-
// This is an 8-byte traceID.
225-
traceIdStr = UpperTraceId + traceIdStr;
251+
_ = ReadNextPart(headerValue, position, out position);
252+
if (position < headerValue.Length)
253+
{
254+
return false;
255+
}
226256
}
257+
}
227258

228-
var traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan());
259+
traceId = CreateTraceId(traceIdStr);
260+
spanId = ActivitySpanId.CreateFromString(spanIdStr);
229261

230-
var spanIdStr = parts[1];
231-
if (string.IsNullOrWhiteSpace(spanIdStr))
232-
{
233-
return context;
234-
}
262+
if (IsSampledValue(traceFlagsStr) ||
263+
traceFlagsStr.Equals(FlagsValue.AsSpan(), StringComparison.Ordinal))
264+
{
265+
traceOptions |= ActivityTraceFlags.Recorded;
266+
}
235267

236-
var spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan());
268+
return true;
269+
}
237270

238-
var traceOptions = ActivityTraceFlags.None;
239-
if (parts.Length > 2)
240-
{
241-
var traceFlagsStr = parts[2];
242-
if (SampledValues.Contains(traceFlagsStr)
243-
|| FlagsValue.Equals(traceFlagsStr, StringComparison.Ordinal))
244-
{
245-
traceOptions |= ActivityTraceFlags.Recorded;
246-
}
247-
}
271+
private static bool IsSampledValue(ReadOnlySpan<char> value) =>
272+
value.Equals(SampledValue.AsSpan(), StringComparison.Ordinal) ||
273+
value.Equals(LegacySampledValue.AsSpan(), StringComparison.Ordinal);
248274

249-
return new PropagationContext(
250-
new ActivityContext(traceId, spanId, traceOptions, isRemote: true),
251-
context.Baggage);
275+
private static ActivityTraceId CreateTraceId(ReadOnlySpan<char> traceId)
276+
{
277+
if (traceId.Length == 16)
278+
{
279+
Span<char> fullTraceId = stackalloc char[UpperTraceId.Length + 16];
280+
281+
UpperTraceId.AsSpan().CopyTo(fullTraceId);
282+
traceId.CopyTo(fullTraceId.Slice(UpperTraceId.Length));
283+
284+
return ActivityTraceId.CreateFromString(fullTraceId);
252285
}
253-
catch (Exception e)
286+
287+
return ActivityTraceId.CreateFromString(traceId);
288+
}
289+
290+
private static ReadOnlySpan<char> ReadNextPart(ReadOnlySpan<char> header, int position, out int nextPosition)
291+
{
292+
var remaining = header.Slice(position);
293+
var separatorIndex = remaining.IndexOf(XB3CombinedDelimiter);
294+
if (separatorIndex < 0)
254295
{
255-
OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e);
256-
return context;
296+
nextPosition = header.Length;
297+
var part = remaining;
298+
return part;
257299
}
300+
301+
var result = remaining.Slice(0, separatorIndex);
302+
nextPosition = position + separatorIndex + 1;
303+
return result;
258304
}
259305
}

src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
#if NET
5+
#if NET9_0_OR_GREATER
6+
using System.Buffers;
7+
#endif
58
using System.Diagnostics.CodeAnalysis;
69
#endif
710
using System.Net;
@@ -20,8 +23,9 @@ public class BaggagePropagator : TextMapPropagator
2023
private const int MaxBaggageLength = 8192;
2124
private const int MaxBaggageItems = 180;
2225

23-
private static readonly char[] EqualSignSeparator = ['='];
24-
private static readonly char[] CommaSignSeparator = [','];
26+
#if NET9_0_OR_GREATER
27+
private static readonly SearchValues<char> DecodeHints = SearchValues.Create('%', '+');
28+
#endif
2529

2630
/// <inheritdoc/>
2731
public override ISet<string> Fields => new HashSet<string> { BaggageHeaderName };
@@ -50,9 +54,9 @@ public override PropagationContext Extract<T>(PropagationContext context, T carr
5054
try
5155
{
5256
var baggageCollection = getter(carrier, BaggageHeaderName);
53-
if (baggageCollection?.Any() ?? false)
57+
if (baggageCollection is not null)
5458
{
55-
if (TryExtractBaggage([.. baggageCollection], out var baggageItems))
59+
if (TryExtractBaggage(baggageCollection, out var baggageItems))
5660
{
5761
Baggage baggage =
5862
#if NET
@@ -104,16 +108,40 @@ public override void Inject<T>(PropagationContext context, T carrier, Action<T,
104108
continue;
105109
}
106110

107-
baggage.Append(WebUtility.UrlEncode(item.Key)).Append('=').Append(WebUtility.UrlEncode(item.Value)).Append(',');
111+
var encodedKey = WebUtility.UrlEncode(item.Key);
112+
var encodedValue = WebUtility.UrlEncode(item.Value);
113+
var baggageItemLength = encodedKey.Length + encodedValue.Length + 1;
114+
115+
if (baggage.Length > 0)
116+
{
117+
baggageItemLength++;
118+
}
119+
120+
if (baggage.Length + baggageItemLength > MaxBaggageLength)
121+
{
122+
break;
123+
}
124+
125+
if (baggage.Length > 0)
126+
{
127+
baggage.Append(',');
128+
}
129+
130+
baggage.Append(encodedKey)
131+
.Append('=')
132+
.Append(encodedValue);
133+
}
134+
while (e.MoveNext() && ++itemCount < MaxBaggageItems);
135+
136+
if (baggage.Length > 0)
137+
{
138+
setter(carrier, BaggageHeaderName, baggage.ToString());
108139
}
109-
while (e.MoveNext() && ++itemCount < MaxBaggageItems && baggage.Length < MaxBaggageLength);
110-
baggage.Remove(baggage.Length - 1, 1);
111-
setter(carrier, BaggageHeaderName, baggage.ToString());
112140
}
113141
}
114142

115143
internal static bool TryExtractBaggage(
116-
string[] baggageCollection,
144+
IEnumerable<string> baggageCollection,
117145
#if NET
118146
[NotNullWhen(true)]
119147
#endif
@@ -135,8 +163,10 @@ internal static bool TryExtractBaggage(
135163
continue;
136164
}
137165

138-
foreach (var pair in item.Split(CommaSignSeparator))
166+
var remaining = item.AsSpan();
167+
while (!remaining.IsEmpty)
139168
{
169+
var pair = ReadNextSegment(ref remaining, ',');
140170
baggageLength += pair.Length + 1; // pair and comma
141171

142172
if (baggageLength >= MaxBaggageLength || baggageDictionary?.Count >= MaxBaggageItems)
@@ -145,36 +175,48 @@ internal static bool TryExtractBaggage(
145175
break;
146176
}
147177

148-
#if NET
149-
if (pair.IndexOf('=', StringComparison.Ordinal) < 0)
150-
#else
151-
if (pair.IndexOf('=') < 0)
152-
#endif
153-
{
154-
continue;
155-
}
156-
157-
var parts = pair.Split(EqualSignSeparator, 2);
158-
if (parts.Length != 2)
178+
var separatorIndex = pair.IndexOf('=');
179+
if (separatorIndex < 0)
159180
{
160181
continue;
161182
}
162183

163-
var key = WebUtility.UrlDecode(parts[0]);
164-
var value = WebUtility.UrlDecode(parts[1]);
184+
var key = DecodeIfNeeded(pair.Slice(0, separatorIndex));
185+
var value = DecodeIfNeeded(pair.Slice(separatorIndex + 1));
165186

166187
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value))
167188
{
168189
continue;
169190
}
170191

171-
baggageDictionary ??= [];
172-
192+
baggageDictionary ??= new(StringComparer.Ordinal);
173193
baggageDictionary[key] = value;
174194
}
175195
}
176196

177197
baggage = baggageDictionary;
178198
return baggageDictionary != null;
179199
}
200+
201+
private static ReadOnlySpan<char> ReadNextSegment(ref ReadOnlySpan<char> remaining, char separator)
202+
{
203+
var separatorIndex = remaining.IndexOf(separator);
204+
if (separatorIndex < 0)
205+
{
206+
var segment = remaining;
207+
remaining = [];
208+
return segment;
209+
}
210+
211+
var result = remaining.Slice(0, separatorIndex);
212+
remaining = remaining.Slice(separatorIndex + 1);
213+
return result;
214+
}
215+
216+
private static string DecodeIfNeeded(ReadOnlySpan<char> value) =>
217+
#if NET9_0_OR_GREATER
218+
value.ContainsAny(DecodeHints) ? WebUtility.UrlDecode(value.ToString()) : value.ToString();
219+
#else
220+
value.IndexOfAny('%', '+') < 0 ? value.ToString() : WebUtility.UrlDecode(value.ToString());
221+
#endif
180222
}

src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,15 @@ public override void Inject<T>(PropagationContext context, T carrier, Action<T,
125125
var tracestateStr = context.ActivityContext.TraceState;
126126
if (tracestateStr?.Length > 0)
127127
{
128-
setter(carrier, TraceState, tracestateStr);
128+
var tracestateEntries = new List<KeyValuePair<string, string>>();
129+
if (TraceStateUtils.AppendTraceState(tracestateStr, tracestateEntries))
130+
{
131+
var normalizedTraceState = TraceStateUtils.GetString(tracestateEntries);
132+
if (normalizedTraceState.Length > 0)
133+
{
134+
setter(carrier, TraceState, normalizedTraceState);
135+
}
136+
}
129137
}
130138
}
131139

0 commit comments

Comments
 (0)