diff --git a/src/TraceEvent/EventPipe/EventCache.cs b/src/TraceEvent/EventPipe/EventCache.cs
index 6492a41e5..aa9c85bac 100644
--- a/src/TraceEvent/EventPipe/EventCache.cs
+++ b/src/TraceEvent/EventPipe/EventCache.cs
@@ -46,6 +46,12 @@ public void ProcessEventBlock(Block block)
while (reader.RemainingBytes.Length > 0)
{
_source.ReadEventHeader(ref reader, useHeaderCompression, ref eventMarker.Header);
+ // Validate the backing block before dispatching or retaining a payload pointer.
+ if ((uint)eventMarker.Header.PayloadSize > (uint)reader.RemainingBytes.Length)
+ {
+ throw new FormatException($"Invalid event payload size {eventMarker.Header.PayloadSize} at stream offset 0x{reader.StreamOffset:x}.");
+ }
+
bool isSortedEvent = eventMarker.Header.IsSorted;
thread.LastCachedEventTimestamp = timestamp = eventMarker.Header.TimeStamp;
maxTimestamp = Math.Max(maxTimestamp, timestamp);
diff --git a/src/TraceEvent/Parsers/KernelTraceEventParser.cs b/src/TraceEvent/Parsers/KernelTraceEventParser.cs
index f773f91d6..35d217f3d 100644
--- a/src/TraceEvent/Parsers/KernelTraceEventParser.cs
+++ b/src/TraceEvent/Parsers/KernelTraceEventParser.cs
@@ -4192,6 +4192,13 @@ public override object PayloadValue(int index)
internal override unsafe void FixupData()
{
+ // V1 Stop events contain only IDs; newer versions require the pointer-sized fixed prefix.
+ int minimumLength = Version < 2 ? 8 : HostOffset(40, 7);
+ if (EventDataLength < minimumLength)
+ {
+ throw new FormatException($"Invalid thread event payload: version {Version} requires at least {minimumLength} bytes, but received {EventDataLength}.");
+ }
+
if (Version < 2)
{
return;
diff --git a/src/TraceEvent/TraceEvent.Tests/Parsing/EventPipeParsing.cs b/src/TraceEvent/TraceEvent.Tests/Parsing/EventPipeParsing.cs
index a59967dbd..f3b275b6e 100644
--- a/src/TraceEvent/TraceEvent.Tests/Parsing/EventPipeParsing.cs
+++ b/src/TraceEvent/TraceEvent.Tests/Parsing/EventPipeParsing.cs
@@ -68,6 +68,43 @@ public EventPipeParsing(ITestOutputHelper output)
{
}
+ ///
+ /// Rejects a generic event whose claimed payload is missing from its block before any callback runs.
+ /// Covers compressed/uncompressed headers and immediate sorted dispatch versus queued unsorted dispatch.
+ ///
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public void NettraceTruncatedPayloadIsRejectedBeforeCallback(bool compressed, bool sorted)
+ {
+ // No native fixup or payload read: this remains safe on the unfixed implementation.
+ EventPipeWriterV6 writer = new EventPipeWriterV6();
+ writer.WriteHeaders();
+ writer.WriteMetadataBlock(new EventMetadata(1, "TestProvider", "TestEvent", 15));
+ writer.WriteThreadBlock(w => w.WriteThreadEntry(1, 202, 101));
+ // Claim 68 payload bytes but supply none; the length has no kernel-event layout significance.
+ writer.WriteEventBlock(compressed, w => w.WriteEventHeader(new WriteEventOptions
+ {
+ MetadataId = 1,
+ ThreadIndexOrId = 1,
+ CaptureThreadIndexOrId = 1,
+ SequenceNumber = 1,
+ Timestamp = 1,
+ IsSorted = sorted
+ }, 68));
+ writer.WriteEndBlock();
+
+ using (var source = new EventPipeEventSource(new MemoryStream(writer.ToArray())))
+ {
+ int callbacks = 0;
+ source.Dynamic.All += data => callbacks++;
+ Assert.Throws(() => source.Process());
+ Assert.Equal(0, callbacks);
+ }
+ }
+
#if NETCOREAPP3_0_OR_GREATER
[Theory(Skip = "Snapshot difs due to increased float accuracy on newer .NET versions.")]
#else
diff --git a/src/TraceEvent/TraceEvent.Tests/Parsing/KernelThreadPayloadTests.cs b/src/TraceEvent/TraceEvent.Tests/Parsing/KernelThreadPayloadTests.cs
new file mode 100644
index 000000000..2c8a55ca6
--- /dev/null
+++ b/src/TraceEvent/TraceEvent.Tests/Parsing/KernelThreadPayloadTests.cs
@@ -0,0 +1,197 @@
+using Microsoft.Diagnostics.Tracing;
+using Microsoft.Diagnostics.Tracing.Parsers;
+using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace TraceEventTests
+{
+ ///
+ /// Checks thread fixup boundaries directly with padded, test-owned classic ETW records.
+ /// Kernel thread events are ETW-only; these tests do not use EventPipe or require malformed ETL files.
+ /// Padding safely exposes writes beyond the advertised payload without exceeding the backing allocation.
+ ///
+ public unsafe class KernelThreadPayloadTests
+ {
+ ///
+ /// Covers legacy ID layouts (0/1), the pointer-sized prefix (2), optional tails (3), and a later version (4).
+ /// Pointer widths describe the trace, not the test process; opcodes 1-4 are Start, Stop, DCStart, and DCStop.
+ ///
+ public static IEnumerable