Skip to content

Commit 4bd9156

Browse files
authored
Fix GC dynamic ETLX replay (#2440)
PerfView 3.2.4 can throw `ArgumentOutOfRangeException` when GC Stats or Heap Analyzer replays `GC/CommittedUsage` events from ETLX. During conversion, `FixupData()` classifies the raw dynamic event and the ETLX persists the synthetic event ID, but the original dynamic payload envelope remains unchanged. On replay, the typed template therefore still needs to parse that envelope. This change refreshes the payload layout in `EventPayload` only when processing a `TraceLog`. Raw ETW and EventPipe dispatch continue using the payload prepared by `FixupData()`, avoiding redundant parsing. Fixed-offset `CommittedUsage` fields also return safe defaults for truncated payloads. Regression coverage includes valid ETLX replay without `FixupData()`, reused replay templates with distinct payloads, and malformed payload access through properties, `PayloadValues`, and `ToXml`. Fixes #2438
1 parent 36d9a6a commit 4bd9156

2 files changed

Lines changed: 159 additions & 20 deletions

File tree

src/TraceEvent/Parsers/GCDynamicTraceEventParser.cs

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Diagnostics;
44
using System.Text;
55

6+
using Microsoft.Diagnostics.Tracing.Etlx;
67
using Microsoft.Diagnostics.Tracing.Parsers.GCDynamic;
78

89
namespace Microsoft.Diagnostics.Tracing.Parsers
@@ -128,10 +129,8 @@ internal GCDynamicTraceEventImpl(Action<GCDynamicTraceEventImpl> action, int eve
128129

129130
/// <summary>
130131
/// These are the raw payload fields of the underlying event. They read through
131-
/// the cached <see cref="_payload"/> that <see cref="FixupData"/> populates per
132-
/// event; when the payload didn't pass the bounds check (i.e. <c>_payload</c>
133-
/// is <c>null</c>) the accessors return type-appropriate safe defaults rather
134-
/// than throwing, so the dispatch hot path stays exception-safe.
132+
/// the cached <see cref="_payload"/> populated before the typed event is accessed.
133+
/// Invalid layouts return type-appropriate safe defaults.
135134
/// </summary>
136135
internal string Name { get { return _payload?.Name ?? string.Empty; } }
137136
internal Int32 DataSize { get { return _payload?.DataSize ?? 0; } }
@@ -145,12 +144,10 @@ internal GCDynamicTraceEventImpl(Action<GCDynamicTraceEventImpl> action, int eve
145144
/// The single <see cref="GCDynamicTraceEventImpl"/> instance is reused for every
146145
/// GCDynamic event the source produces, so the per-event payload validation
147146
/// must run here (not once at construction) -- it refreshes <see cref="_payload"/>
148-
/// for the event that's about to dispatch. Centralising the bounds check here
149-
/// lets all downstream payload accessors (Name / DataSize / Data / ClrInstanceID,
150-
/// the typed CommittedUsage fixed-offset reads, PayloadValue / PayloadValues,
151-
/// ToXml) trust the cached layout without re-validating, and prevents an
152-
/// attacker-controlled DataSize from triggering an OOB read or an unhandled
153-
/// exception on the dispatch hot path.
147+
/// for the raw event that's about to be converted and prevents an
148+
/// attacker-controlled DataSize from selecting an incompatible template.
149+
/// Payload accessors validate independently because TraceLog replay skips this
150+
/// method after the converted event type has been persisted.
154151
/// </summary>
155152
internal override void FixupData()
156153
{
@@ -186,6 +183,13 @@ public GCDynamicEventBase EventPayload
186183
{
187184
get
188185
{
186+
// TraceLog replay reuses this template and does not call FixupData, so
187+
// refresh the layout before exposing the typed payload for this event.
188+
if (traceEventSource is TraceLog)
189+
{
190+
_payload = ReadPayloadLayout();
191+
}
192+
189193
if (eventID == GCDynamicEventBase.CommittedUsageTemplate.ID)
190194
{
191195
return _committedUsageTemplate.Bind(this);
@@ -222,10 +226,8 @@ public override StringBuilder ToXml(StringBuilder sb)
222226

223227
private event Action<GCDynamicTraceEventImpl> Action;
224228

225-
// Per-event scratch state populated by FixupData. null means the bound event's
226-
// payload failed the bounds check; accessors above return safe defaults in
227-
// that case. The cache lifetime is exactly one dispatch -- FixupData
228-
// overwrites it before each event is delivered.
229+
// Per-event scratch state refreshed by FixupData during raw conversion and by
230+
// EventPayload during TraceLog replay.
229231
private PayloadLayout? _payload;
230232

231233
/// <summary>
@@ -438,12 +440,12 @@ public sealed class CommittedUsageTraceEvent : GCDynamicEventBase
438440
/// </summary>
439441
internal const int MinimumDataSize = 42;
440442

441-
public short Version { get { return BitConverter.ToInt16(DataField, 0); } }
442-
public long TotalCommittedInUse { get { return BitConverter.ToInt64(DataField, 2); } }
443-
public long TotalCommittedInGlobalDecommit { get { return BitConverter.ToInt64(DataField, 10); } }
444-
public long TotalCommittedInFree { get { return BitConverter.ToInt64(DataField, 18); } }
445-
public long TotalCommittedInGlobalFree { get { return BitConverter.ToInt64(DataField, 26); } }
446-
public long TotalBookkeepingCommitted { get { return BitConverter.ToInt64(DataField, 34); } }
443+
public short Version { get { return GetInt16(0); } }
444+
public long TotalCommittedInUse { get { return GetInt64(2); } }
445+
public long TotalCommittedInGlobalDecommit { get { return GetInt64(10); } }
446+
public long TotalCommittedInFree { get { return GetInt64(18); } }
447+
public long TotalCommittedInGlobalFree { get { return GetInt64(26); } }
448+
public long TotalBookkeepingCommitted { get { return GetInt64(34); } }
447449

448450
internal override TraceEventID ID => TraceEventID.Illegal - 11;
449451
internal override string TaskName => "GC";
@@ -499,6 +501,18 @@ internal override IEnumerable<KeyValuePair<string, object>> PayloadValues
499501
yield return new KeyValuePair<string, object>("TotalBookkeepingCommitted", TotalBookkeepingCommitted);
500502
}
501503
}
504+
505+
private short GetInt16(int offset)
506+
{
507+
byte[] data = DataField;
508+
return data.Length >= offset + sizeof(short) ? BitConverter.ToInt16(data, offset) : (short)0;
509+
}
510+
511+
private long GetInt64(int offset)
512+
{
513+
byte[] data = DataField;
514+
return data.Length >= offset + sizeof(long) ? BitConverter.ToInt64(data, offset) : 0;
515+
}
502516
}
503517

504518
public sealed class CommittedUsage

src/TraceEvent/TraceEvent.Tests/Regression/GCDynamicTraceEventParserTests.cs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Diagnostics.Tracing;
2+
using Microsoft.Diagnostics.Tracing.Etlx;
23
using Microsoft.Diagnostics.Tracing.Parsers;
34
using Microsoft.Diagnostics.Tracing.Parsers.GCDynamic;
45
using System;
@@ -143,6 +144,79 @@ public void GCDynamicCommittedUsageWithValidDataIsDispatched()
143144
});
144145
}
145146

147+
[Fact]
148+
public void GCDynamicCommittedUsageReplayDecodesWithoutFixupData()
149+
{
150+
byte[] data = CreateCommittedUsageData(1, 100, 200, 300, 400, 500);
151+
byte[] payload = CreatePayload("CommittedUsage", data.Length, data, 7);
152+
153+
WithGCDynamicReplayEvent(payload, delegate (GCDynamicTraceEventImpl traceEvent)
154+
{
155+
CommittedUsageTraceEvent committedUsage = (CommittedUsageTraceEvent)traceEvent.EventPayload;
156+
157+
Assert.Equal(1, committedUsage.Version);
158+
Assert.Equal(100, committedUsage.TotalCommittedInUse);
159+
Assert.Equal(500, committedUsage.TotalBookkeepingCommitted);
160+
});
161+
}
162+
163+
[Fact]
164+
public void GCDynamicCommittedUsageReplayDoesNotReusePreviousPayload()
165+
{
166+
byte[] firstData = CreateCommittedUsageData(1, 100, 200, 300, 400, 500);
167+
byte[] secondData = CreateCommittedUsageData(2, 600, 700, 800, 900, 1000);
168+
byte[] firstPayload = CreatePayload("CommittedUsage", firstData.Length, firstData, 7);
169+
byte[] secondPayload = CreatePayload("CommittedUsage", secondData.Length, secondData, 8);
170+
List<short> versions = new List<short>();
171+
List<long> committedInUse = new List<long>();
172+
173+
WithReusedGCDynamicReplayEvent(firstPayload, secondPayload, delegate (GCDynamicTraceEventImpl traceEvent)
174+
{
175+
CommittedUsageTraceEvent committedUsage = (CommittedUsageTraceEvent)traceEvent.EventPayload;
176+
versions.Add(committedUsage.Version);
177+
committedInUse.Add(committedUsage.TotalCommittedInUse);
178+
});
179+
180+
Assert.Equal(new short[] { 1, 2 }, versions);
181+
Assert.Equal(new long[] { 100, 600 }, committedInUse);
182+
}
183+
184+
[Fact]
185+
public void GCDynamicCommittedUsageReplayWithTruncatedDataIsSafe()
186+
{
187+
byte[] payload = CreatePayload("CommittedUsage", 1, new byte[] { 1 }, 7);
188+
189+
WithGCDynamicReplayEvent(payload, delegate (GCDynamicTraceEventImpl traceEvent)
190+
{
191+
CommittedUsageTraceEvent committedUsage = (CommittedUsageTraceEvent)traceEvent.EventPayload;
192+
Assert.Equal(0, committedUsage.Version);
193+
Assert.Equal(0, committedUsage.TotalCommittedInUse);
194+
Assert.Equal(0, committedUsage.TotalCommittedInGlobalDecommit);
195+
Assert.Equal(0, committedUsage.TotalCommittedInFree);
196+
Assert.Equal(0, committedUsage.TotalCommittedInGlobalFree);
197+
Assert.Equal(0, committedUsage.TotalBookkeepingCommitted);
198+
199+
List<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>();
200+
Exception payloadValuesException = Record.Exception(delegate
201+
{
202+
foreach (KeyValuePair<string, object> value in committedUsage.PayloadValues)
203+
{
204+
values.Add(value);
205+
}
206+
});
207+
Assert.Null(payloadValuesException);
208+
Assert.Equal(6, values.Count);
209+
Assert.Equal((short)0, values[0].Value);
210+
for (int i = 1; i < values.Count; i++)
211+
{
212+
Assert.Equal((long)0, values[i].Value);
213+
}
214+
215+
Exception toXmlException = Record.Exception(delegate { traceEvent.ToXml(new StringBuilder()); });
216+
Assert.Null(toXmlException);
217+
});
218+
}
219+
146220
/// <summary>
147221
/// Regression test for the propagated-exception path through
148222
/// PayloadValues on a malformed payload. ToXml iterates
@@ -243,6 +317,18 @@ private static byte[] CreatePayload(string name, int dataSize, byte[] data, shor
243317
return payload.ToArray();
244318
}
245319

320+
private static byte[] CreateCommittedUsageData(short version, long committedInUse, long committedInGlobalDecommit, long committedInFree, long committedInGlobalFree, long bookkeepingCommitted)
321+
{
322+
byte[] data = new byte[CommittedUsageTraceEvent.MinimumDataSize];
323+
BitConverter.GetBytes(version).CopyTo(data, 0);
324+
BitConverter.GetBytes(committedInUse).CopyTo(data, 2);
325+
BitConverter.GetBytes(committedInGlobalDecommit).CopyTo(data, 10);
326+
BitConverter.GetBytes(committedInFree).CopyTo(data, 18);
327+
BitConverter.GetBytes(committedInGlobalFree).CopyTo(data, 26);
328+
BitConverter.GetBytes(bookkeepingCommitted).CopyTo(data, 34);
329+
return data;
330+
}
331+
246332
private static unsafe void WithGCDynamicEvent(byte[] payload, Action<GCDynamicTraceEventImpl> action)
247333
{
248334
fixed (byte* payloadBytes = payload)
@@ -260,5 +346,44 @@ private static unsafe void WithGCDynamicEvent(byte[] payload, Action<GCDynamicTr
260346
action(traceEvent);
261347
}
262348
}
349+
350+
private static unsafe void WithGCDynamicReplayEvent(byte[] payload, Action<GCDynamicTraceEventImpl> action)
351+
{
352+
WithReusedGCDynamicReplayEvent(payload, null, action);
353+
}
354+
355+
private static unsafe void WithReusedGCDynamicReplayEvent(byte[] firstPayload, byte[] secondPayload, Action<GCDynamicTraceEventImpl> action)
356+
{
357+
fixed (byte* firstPayloadBytes = firstPayload)
358+
fixed (byte* secondPayloadBytes = secondPayload)
359+
{
360+
TraceLog source = (TraceLog)Activator.CreateInstance(typeof(TraceLog), true);
361+
source._QPCFreq = 1;
362+
source._syncTimeQPC = 1;
363+
source._syncTimeUTC = DateTime.UtcNow;
364+
source.sessionStartTimeQPC = 1;
365+
366+
TraceEventNativeMethods.EVENT_RECORD eventRecord = new TraceEventNativeMethods.EVENT_RECORD();
367+
eventRecord.EventHeader.ProviderId = GCDynamicTraceEventParser.ProviderGuid;
368+
eventRecord.EventHeader.Id = (ushort)GCDynamicEventBase.CommittedUsageTemplate.ID;
369+
370+
GCDynamicTraceEventImpl traceEvent = new GCDynamicTraceEventImpl(null, (int)GCDynamicEventBase.CommittedUsageTemplate.ID, 1, "GC", Guid.Empty, 41, "CommittedUsage", GCDynamicTraceEventParser.ProviderGuid, "Microsoft-Windows-DotNETRuntime");
371+
traceEvent.eventRecord = &eventRecord;
372+
traceEvent.traceEventSource = source;
373+
374+
eventRecord.UserDataLength = (ushort)firstPayload.Length;
375+
eventRecord.UserData = (IntPtr)firstPayloadBytes;
376+
traceEvent.userData = eventRecord.UserData;
377+
action(traceEvent);
378+
379+
if (secondPayload != null)
380+
{
381+
eventRecord.UserDataLength = (ushort)secondPayload.Length;
382+
eventRecord.UserData = (IntPtr)secondPayloadBytes;
383+
traceEvent.userData = eventRecord.UserData;
384+
action(traceEvent);
385+
}
386+
}
387+
}
263388
}
264389
}

0 commit comments

Comments
 (0)