Skip to content

Commit dc3f873

Browse files
authored
[DSM] Guard against future timestamps when extracting pathway context (#8672)
## Summary of changes Guards against future timestamps when extracting pathwayContext and calculating `pathwayLatencyNs` ## Reason for change We decode the `dd-pathway-ctx` from inbound headers, and use the decoded `PathwayStart` to compute `pathwayLatencyNs`. This is then used to calculate an `originTimestamp` and is added to `_originBuckets`. We then only flush `_originBuckets` for which the start time is <= the current bucket window. If a "future" timestamp ends up in `PathwayStart`, then we would end up with that future bucket never being flushed and accumulating. This PR guards against those future timestamps so we don't end up with unbounded growth. ## Implementation details Add two guards: - In `SetCheckpoint`, guard against generating negative `pathwayLatencyNs` or `edgeLatencyNs` - In `PathwayContextEncoder.Decode`, reject data points with a "future" `PathwayStart` time - Using a tolerance here of 60s to make sure clock skew issues don't bite us. I _think_ that number seems reasonable, but open to suggestions. - I was initially concerned about daylight savings etc, but given we're using UTC for all of this, that shouldn't be an issue ## Test coverage Added a bunch of additional unit tests around this.
1 parent 3125784 commit dc3f873

3 files changed

Lines changed: 96 additions & 4 deletions

File tree

tracer/src/Datadog.Trace/DataStreamsMonitoring/DataStreamsManager.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,15 +325,20 @@ public void InjectPathwayContextAsBase64String<TCarrier>(PathwayContext? context
325325
var parentHash = previousContext?.Hash ?? default;
326326
var pathwayHash = HashHelper.CalculatePathwayHash(nodeHash, parentHash);
327327

328+
// Prevent negative value latencies from a future-dated upstream timestamp
329+
// (clock skew or a buggy/malicious peer)
330+
var pathwayLatencyNs = Math.Max(0, nowNs - pathwayStartNs);
331+
var edgeLatencyNs = Math.Max(0, nowNs - (previousContext?.EdgeStart ?? edgeStartNs));
332+
328333
var writer = Volatile.Read(ref _writer);
329334
writer?.Add(
330335
new StatsPoint(
331336
edgeTags: edgeTags,
332337
hash: pathwayHash,
333338
parentHash: parentHash,
334339
timestampNs: nowNs,
335-
pathwayLatencyNs: nowNs - pathwayStartNs,
336-
edgeLatencyNs: nowNs - (previousContext?.EdgeStart ?? edgeStartNs),
340+
pathwayLatencyNs: pathwayLatencyNs,
341+
edgeLatencyNs: edgeLatencyNs,
337342
payloadSizeBytes));
338343

339344
var pathway = new PathwayContext(

tracer/src/Datadog.Trace/DataStreamsMonitoring/PathwayContextEncoder.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ internal static class PathwayContextEncoder
2121
/// <summary>Maximum byte length of the Base64 encoding of <see cref="MaxEncodedSize"/> bytes: ceil(26/3)*4 = 36.</summary>
2222
internal const int MaxBase64EncodedSize = 36;
2323

24+
/// <summary>
25+
/// Tolerance (in nanoseconds) applied when validating that decoded pathway/edge timestamps
26+
/// are not in the future. Allows for normal clock drift between hosts.
27+
/// </summary>
28+
internal const long MaxClockSkewNs = 60L * 1_000_000_000L;
29+
2430
/// <summary>
2531
/// Encodes a <see cref="PathwayContext"/> as a series of bytes
2632
/// NOTE: the encoding is lossy, in that we convert <see cref="PathwayContext.PathwayStart"/>
@@ -108,6 +114,15 @@ public static int EncodeInto(PathwayContext pathway, Span<byte> buffer)
108114
return null;
109115
}
110116

117+
var maxAllowedNs = DateTimeOffset.UtcNow.ToUnixTimeNanoseconds() + MaxClockSkewNs;
118+
if (pathwayStartNs > maxAllowedNs || edgeStartNs > maxAllowedNs)
119+
{
120+
Log.Warning(
121+
"Error decoding Data Stream PathwayContext from bytes {Base64EncodedBytes}: pathway or edge start is in the future",
122+
Convert.ToBase64String(bytes));
123+
return null;
124+
}
125+
111126
// Pathway context values are in ns
112127
return new PathwayContext(new PathwayHash(hash), pathwayStartNs, edgeStartNs);
113128
}
@@ -152,6 +167,13 @@ public static int EncodeInto(PathwayContext pathway, Span<byte> buffer)
152167
return null;
153168
}
154169

170+
var maxAllowedNs = DateTimeOffset.UtcNow.ToUnixTimeNanoseconds() + MaxClockSkewNs;
171+
if (pathwayStartNs > maxAllowedNs || edgeStartNs > maxAllowedNs)
172+
{
173+
Log.Warning("Error decoding Data Stream PathwayContext from bytes: pathway or edge start is in the future");
174+
return null;
175+
}
176+
155177
return new PathwayContext(new PathwayHash(hash), pathwayStartNs, edgeStartNs);
156178
}
157179
#endif

tracer/test/Datadog.Trace.Tests/DataStreamsMonitoring/PathwayContextEncoderTests.cs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
using System;
77
using Datadog.Trace.DataStreamsMonitoring;
88
using Datadog.Trace.DataStreamsMonitoring.Hashes;
9+
using Datadog.Trace.ExtensionMethods;
910
using FluentAssertions;
11+
using FluentAssertions.Extensions;
1012
using Xunit;
1113

1214
namespace Datadog.Trace.Tests.DataStreamsMonitoring;
@@ -18,18 +20,34 @@ public class PathwayContextEncoderTests
1820
[Fact]
1921
public void TestRandomValues()
2022
{
23+
var nowNs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000;
2124
for (var i = 0; i < 1000; i++)
2225
{
23-
EncodeTest(unchecked((ulong)GetLong()), Math.Abs(GetLong()), Math.Abs(GetLong()));
26+
// Mask off the sign bit to guarantee non-negative — Math.Abs(long.MinValue) returns long.MinValue
27+
// (overflow), which would feed a negative timestamp into EncodeTest and fail the overflow guard.
28+
EncodeTest(unchecked((ulong)GetLong()), GetNonNegativeLongInThePast(nowNs), GetNonNegativeLongInThePast(nowNs));
2429
}
30+
31+
static long GetNonNegativeLongInThePast(long nowNs) => (GetLong() & long.MaxValue) % nowNs;
2532
}
2633

2734
[Theory]
2835
[InlineData(0, 0, 0)]
29-
[InlineData(ulong.MaxValue, long.MaxValue, long.MaxValue)]
3036
public void TestEdgeCases(ulong hash, long pathwayStartNs, long edgeStartNs)
3137
=> EncodeTest(hash, pathwayStartNs, edgeStartNs);
3238

39+
[Theory]
40+
[InlineData(ulong.MaxValue, long.MaxValue, long.MaxValue)]
41+
public void DecoderFailure_OverflowTimestamps(ulong hash, long pathwayStartNs, long edgeStartNs)
42+
{
43+
var pathway = new PathwayContext(new PathwayHash(hash), pathwayStartNs, edgeStartNs);
44+
45+
var bytes = PathwayContextEncoder.Encode(pathway);
46+
var decoded = PathwayContextEncoder.Decode(bytes);
47+
48+
decoded.Should().BeNull();
49+
}
50+
3351
[Theory]
3452
[InlineData(0)]
3553
[InlineData(1)]
@@ -105,6 +123,37 @@ public void DecoderFailure_OutOfRangePathwayBytes()
105123
decoded.Should().BeNull();
106124
}
107125

126+
[Fact]
127+
public void DecoderFailure_FutureTimestamps()
128+
{
129+
var futureNs = DateTimeOffset.UtcNow
130+
.AddNanoseconds(PathwayContextEncoder.MaxClockSkewNs * 5)
131+
.ToUnixTimeNanoseconds();
132+
var pathway = new PathwayContext(new PathwayHash(123), pathwayStartNs: futureNs, edgeStartNs: futureNs);
133+
134+
var bytes = PathwayContextEncoder.Encode(pathway);
135+
var decoded = PathwayContextEncoder.Decode(bytes);
136+
137+
decoded.Should().BeNull();
138+
}
139+
140+
[Fact]
141+
public void DecodeSucceeds_TimestampExactlyNow()
142+
{
143+
// The check uses strict >, so a timestamp equal to nowNs (within tolerance) must round-trip.
144+
var nowNs = DateTimeOffset.UtcNow.ToUnixTimeNanoseconds();
145+
EncodeTest(hash: 123, pathwayStartNs: nowNs, edgeStartNs: nowNs);
146+
}
147+
148+
[Fact]
149+
public void DecodeSucceeds_TimestampWithinSkewTolerance()
150+
{
151+
var futureNs = DateTimeOffset.UtcNow
152+
.AddNanoseconds(PathwayContextEncoder.MaxClockSkewNs / 2)
153+
.ToUnixTimeNanoseconds();
154+
EncodeTest(hash: 123, pathwayStartNs: futureNs, edgeStartNs: futureNs);
155+
}
156+
108157
#if NETCOREAPP3_1_OR_GREATER
109158
[Theory]
110159
[InlineData(0, 0, 0)]
@@ -120,6 +169,22 @@ public void EncodeInto_ProducesSameBytesAsEncode(ulong hash, long pathwayStartNs
120169

121170
buffer.Slice(0, bytesWritten).ToArray().Should().Equal(expected);
122171
}
172+
173+
[Fact]
174+
public void DecoderFailure_FutureTimestamps_Span()
175+
{
176+
var futureNs = DateTimeOffset.UtcNow
177+
.AddNanoseconds(PathwayContextEncoder.MaxClockSkewNs * 5)
178+
.ToUnixTimeNanoseconds();
179+
var pathway = new PathwayContext(new PathwayHash(123), pathwayStartNs: futureNs, edgeStartNs: futureNs);
180+
181+
Span<byte> buffer = stackalloc byte[PathwayContextEncoder.MaxEncodedSize];
182+
var bytesWritten = PathwayContextEncoder.EncodeInto(pathway, buffer);
183+
184+
var decoded = PathwayContextEncoder.Decode(buffer.Slice(0, bytesWritten));
185+
186+
decoded.Should().BeNull();
187+
}
123188
#endif
124189

125190
private static void EncodeTest(ulong hash, long pathwayStartNs, long edgeStartNs)

0 commit comments

Comments
 (0)