Skip to content

Commit ef66839

Browse files
[OTLP] Fix OTLP/gRPC status parsing (#7064)
Co-authored-by: martincostello <martin@martincostello.com>
1 parent a2b6372 commit ef66839

4 files changed

Lines changed: 226 additions & 1 deletion

File tree

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md

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

88
## Unreleased
99

10+
* Fixed an issue in OTLP/gRPC retry handling where parsing gRPC status.
11+
([#7064](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7064))
12+
1013
## 1.15.2
1114

1215
Released 2026-Apr-08

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/GrpcStatusDeserializer.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ internal static class GrpcStatusDeserializer
125125
private static Duration DecodeDuration(Stream stream)
126126
{
127127
var length = DecodeVarint(stream);
128+
129+
CheckLength(length, stream);
130+
128131
var endPosition = stream.Position + length;
129132
long seconds = 0;
130133
int nanos = 0;
@@ -155,6 +158,9 @@ private static Duration DecodeDuration(Stream stream)
155158
private static Any DecodeAny(Stream stream)
156159
{
157160
var length = DecodeVarint(stream);
161+
162+
CheckLength(length, stream);
163+
158164
var endPosition = stream.Position + length;
159165

160166
string? typeUrl = null;
@@ -228,6 +234,9 @@ private static string DecodeString(Stream stream)
228234
private static byte[] DecodeBytes(Stream stream)
229235
{
230236
var length = (int)DecodeVarint(stream);
237+
238+
CheckLength(length, stream);
239+
231240
var buffer = new byte[length];
232241
int read = stream.Read(buffer, 0, length);
233242
if (read != length)
@@ -250,6 +259,7 @@ private static void SkipField(Stream stream, uint wireType)
250259
break;
251260
case WIRETYPE_LENGTH_DELIMITED:
252261
var length = DecodeVarint(stream);
262+
CheckLength(length, stream);
253263
stream.Position += length;
254264
break;
255265
case WIRETYPE_FIXED32:
@@ -260,6 +270,21 @@ private static void SkipField(Stream stream, uint wireType)
260270
}
261271
}
262272

273+
private static void CheckLength(long length, Stream stream)
274+
{
275+
if (length < 0 || length > int.MaxValue)
276+
{
277+
throw new InvalidDataException($"Invalid length: {length}.");
278+
}
279+
280+
long available = stream.Length - stream.Position;
281+
282+
if (length > available)
283+
{
284+
throw new EndOfStreamException();
285+
}
286+
}
287+
263288
internal readonly struct Duration
264289
{
265290
internal Duration(long seconds, int nanos)

test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/GrpcStatusDeserializerTests.cs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Google.Protobuf;
55
using Google.Protobuf.WellKnownTypes;
66
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc;
7+
using Type = System.Type;
78

89
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.Implementation.ExportClient;
910

@@ -314,7 +315,7 @@ public void TryGetGrpcRetryDelay_OnlyNanos_ReturnsExpected()
314315
}
315316

316317
[Fact]
317-
public void DeserializeStatus_TruncatedStream_ThrowsEndOfStreamException()
318+
public void DeserializeStatus_TruncatedStream_ThrowsException()
318319
{
319320
// Arrange: Create valid Base64 data and truncate it
320321
var status = new Google.Rpc.Status
@@ -332,4 +333,83 @@ public void DeserializeStatus_TruncatedStream_ThrowsEndOfStreamException()
332333
Assert.Throws<EndOfStreamException>(() =>
333334
GrpcStatusDeserializer.DeserializeStatus(grpcStatusDetailsBin));
334335
}
336+
337+
[Fact]
338+
public void DeserializeStatus_WithLargeLengthDelimitedField_ThrowsException()
339+
{
340+
// Arrange
341+
// This payload encodes a Status.details Any.value field with an extremely large
342+
// length value (0x7FFFFFF0) but without enough bytes in the payload.
343+
const string grpcStatusDetailsBin = "GgYS8P///wc=";
344+
345+
// Act & Assert
346+
Assert.Throws<EndOfStreamException>(() =>
347+
GrpcStatusDeserializer.DeserializeStatus(grpcStatusDetailsBin));
348+
}
349+
350+
[Theory]
351+
[InlineData("GgsS////////////AQ==")] // -1
352+
[InlineData("GgYSgICAgAg=")] // 0x80000000
353+
public void DeserializeStatus_WithInvalidLengthDelimitedField_ThrowsException(string grpcStatusDetailsBin)
354+
{
355+
// Act & Assert
356+
var exception = Assert.Throws<InvalidDataException>(() => GrpcStatusDeserializer.DeserializeStatus(grpcStatusDetailsBin));
357+
Assert.Contains("Invalid length", exception.Message, StringComparison.Ordinal);
358+
}
359+
360+
[Theory]
361+
[InlineData(int.MaxValue / 2, typeof(EndOfStreamException))]
362+
[InlineData(int.MaxValue - 1024, typeof(EndOfStreamException))]
363+
[InlineData(int.MaxValue - 1, typeof(EndOfStreamException))]
364+
[InlineData(int.MaxValue, typeof(EndOfStreamException))]
365+
[InlineData(uint.MaxValue, typeof(InvalidDataException))]
366+
public void DeserializeStatus_InvalidDetailValueLength_Throws(long value, Type expected)
367+
{
368+
var anyValueLength = EncodeVarint(value);
369+
var statusBytes = new byte[2 + 1 + anyValueLength.Length];
370+
371+
statusBytes[0] = 0x1A; // field 3 (details), wire type 2
372+
statusBytes[1] = (byte)(1 + anyValueLength.Length); // embedded Any payload length
373+
statusBytes[2] = 0x12; // field 2 (value), wire type 2
374+
anyValueLength.CopyTo(statusBytes, 3);
375+
376+
var grpcStatusDetailsBin = Convert.ToBase64String(statusBytes);
377+
378+
Assert.Throws(expected, () => GrpcStatusDeserializer.DeserializeStatus(grpcStatusDetailsBin));
379+
}
380+
381+
[Fact]
382+
public void DeserializeStatus_InvalidEmbeddedMessageLength_Throws()
383+
{
384+
var statusBytes = new byte[1 + EncodeVarint(long.MaxValue).Length];
385+
386+
statusBytes[0] = 0x1A; // field 3 (details), wire type 2
387+
EncodeVarint(long.MaxValue).CopyTo(statusBytes, 1);
388+
389+
var grpcStatusDetailsBin = Convert.ToBase64String(statusBytes);
390+
391+
Assert.Throws<InvalidDataException>(() => GrpcStatusDeserializer.DeserializeStatus(grpcStatusDetailsBin));
392+
}
393+
394+
private static byte[] EncodeVarint(long value)
395+
{
396+
var encoded = new List<byte>();
397+
var remaining = unchecked((ulong)value);
398+
399+
do
400+
{
401+
var current = (byte)(remaining & 0x7F);
402+
remaining >>= 7;
403+
404+
if (remaining != 0)
405+
{
406+
current |= 0x80;
407+
}
408+
409+
encoded.Add(current);
410+
}
411+
while (remaining != 0);
412+
413+
return [.. encoded];
414+
}
335415
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
5+
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc;
6+
7+
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission.Tests;
8+
9+
public class OtlpExporterRetryTransmissionHandlerTests
10+
{
11+
[Theory]
12+
[InlineData(int.MaxValue / 2)]
13+
[InlineData(int.MaxValue - 1024)]
14+
[InlineData(int.MaxValue - 1)]
15+
[InlineData(int.MaxValue)]
16+
[InlineData(uint.MaxValue)]
17+
public void TrySubmitRequest_FailedRequestWithOverflowingRetryHeader_LogsParsingFailureAndRetries(long value)
18+
{
19+
var maliciousHeader = CreateMalformedGrpcStatusDetailsHeader(value);
20+
var exportClient = new RetryingTestExportClient(maliciousHeader);
21+
22+
using var transmissionHandler = new OtlpExporterRetryTransmissionHandler(exportClient, timeoutMilliseconds: 1_000);
23+
24+
bool actual;
25+
26+
#if NET
27+
using (new AllocationAssertion())
28+
#endif
29+
{
30+
actual = transmissionHandler.TrySubmitRequest([1, 2, 3], 3);
31+
}
32+
33+
Assert.False(actual);
34+
Assert.True(exportClient.SendCount >= 2, $"Expected at least 2 send attempts, but got {exportClient.SendCount}.");
35+
}
36+
37+
private static string CreateMalformedGrpcStatusDetailsHeader(long value)
38+
{
39+
var anyValueLength = EncodeVarint(value);
40+
var statusBytes = new byte[2 + 1 + anyValueLength.Length];
41+
42+
statusBytes[0] = 0x1A; // field 3 (details), wire type 2
43+
statusBytes[1] = (byte)(1 + anyValueLength.Length); // embedded Any payload length
44+
statusBytes[2] = 0x12; // field 2 (value), wire type 2
45+
46+
anyValueLength.CopyTo(statusBytes, 3);
47+
48+
return Convert.ToBase64String(statusBytes);
49+
}
50+
51+
private static byte[] EncodeVarint(long value)
52+
{
53+
var encoded = new List<byte>();
54+
var remaining = unchecked((ulong)value);
55+
56+
do
57+
{
58+
var current = (byte)(remaining & 0x7F);
59+
remaining >>= 7;
60+
61+
if (remaining != 0)
62+
{
63+
current |= 0x80;
64+
}
65+
66+
encoded.Add(current);
67+
}
68+
while (remaining != 0);
69+
70+
return [.. encoded];
71+
}
72+
73+
private sealed class RetryingTestExportClient(string maliciousHeader) : IExportClient
74+
{
75+
public int SendCount { get; private set; }
76+
77+
public ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default)
78+
{
79+
this.SendCount++;
80+
81+
return new ExportClientGrpcResponse(
82+
success: false,
83+
deadlineUtc: deadlineUtc,
84+
exception: null,
85+
status: new Status(StatusCode.Unavailable, "retryable"),
86+
grpcStatusDetailsHeader: maliciousHeader);
87+
}
88+
89+
public bool Shutdown(int timeoutMilliseconds) => true;
90+
}
91+
92+
#if NET
93+
private sealed class AllocationAssertion : IDisposable
94+
{
95+
private readonly long before;
96+
97+
public AllocationAssertion()
98+
{
99+
GC.Collect();
100+
GC.WaitForPendingFinalizers();
101+
GC.Collect();
102+
103+
this.before = GC.GetTotalAllocatedBytes();
104+
}
105+
106+
public void Dispose()
107+
{
108+
var allocatedBytes = GC.GetTotalAllocatedBytes() - this.before;
109+
110+
const int Limit = 1_000_000;
111+
Assert.False(
112+
allocatedBytes > Limit,
113+
$"{allocatedBytes} bytes were allocated during the operation which is more than the limit of {Limit}.");
114+
}
115+
}
116+
#endif
117+
}

0 commit comments

Comments
 (0)