From fe9848e7b3a06c09a3016d8bcb7aa384f5b7345d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20=C5=81ukasik?= Date: Wed, 13 May 2026 10:36:58 -0700 Subject: [PATCH] C#: fix IndexOutOfRangeException in ReadRawByte on truncated messages (#26914) **Summary** A near-int.MaxValue length varint overflows PushLimit, corrupting bufferSize to a negative value. ReadRawByte's == guard then never triggers RefillBuffer, causing an out-of-bounds read instead of InvalidProtocolBufferException.TruncatedMessage(). **Fix** change == to >= in ReadRawByte. Regression tests added for all four affected slow-path variants. **Tests** Added `TruncatedMessageWithLargeInnerLengthThrowsInvalidProtocolBufferException` with 4 test cases. Fixes #26856 Closes #26914 COPYBARA_INTEGRATE_REVIEW=https://github.com/protocolbuffers/protobuf/pull/26914 from pawlos:fix/csharp-readrawbyte-ioob-truncated-message 6bec67cfb24f7e4cceb757eaf3a40589ab22b22b PiperOrigin-RevId: 914940764 --- csharp/src/Google.Protobuf.Test/IssuesTest.cs | 13 +++++++++++++ csharp/src/Google.Protobuf/SegmentedBufferHelper.cs | 8 +++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/csharp/src/Google.Protobuf.Test/IssuesTest.cs b/csharp/src/Google.Protobuf.Test/IssuesTest.cs index fa31bea0de..707a90e00f 100644 --- a/csharp/src/Google.Protobuf.Test/IssuesTest.cs +++ b/csharp/src/Google.Protobuf.Test/IssuesTest.cs @@ -105,5 +105,18 @@ namespace Google.Protobuf Assert.AreEqual(message, parsed); Assert.AreEqual("test", parsed.None); } + + // Issue 26856: ReadRawByte throws IndexOutOfRangeException instead of + // InvalidProtocolBufferException on truncated messages with a near-int.MaxValue + // inner length varint, corrupting bufferSize via integer overflow in PushLimit. + [Test] + [TestCase(new byte[] { 0x2a, 0xff, 0xff, 0xff, 0xff, 0x67, 0x2a, 0xcc }, "ParseRawVarint32SlowPath")] + [TestCase(new byte[] { 0x42, 0xfc, 0xff, 0xff, 0xff, 0x57, 0xd8, 0x01 }, "ParseRawVarint64SlowPath")] + [TestCase(new byte[] { 0x3a, 0xff, 0xff, 0xff, 0xff, 0x67, 0xb5, 0x34 }, "ParseRawLittleEndian32SlowPath")] + [TestCase(new byte[] { 0x42, 0xff, 0xff, 0xff, 0xff, 0x67, 0x39, 0x34 }, "ParseRawLittleEndian64SlowPath")] + public void TruncatedMessageWithLargeInnerLengthThrowsInvalidProtocolBufferException(byte[] data, string _) + { + Assert.Throws(() => FileDescriptorProto.Parser.ParseFrom(data)); + } } } diff --git a/csharp/src/Google.Protobuf/SegmentedBufferHelper.cs b/csharp/src/Google.Protobuf/SegmentedBufferHelper.cs index 6ec422381c..97b01514e5 100644 --- a/csharp/src/Google.Protobuf/SegmentedBufferHelper.cs +++ b/csharp/src/Google.Protobuf/SegmentedBufferHelper.cs @@ -93,13 +93,15 @@ namespace Google.Protobuf { throw InvalidProtocolBufferException.NegativeSize(); } - byteLimit += state.totalBytesRetired + state.bufferPos; + // Compute in long to avoid int overflow when byteLimit is near int.MaxValue; + // the oldLimit guard below ensures the result fits back into int. + long absoluteLimit = (long)byteLimit + state.totalBytesRetired + state.bufferPos; int oldLimit = state.currentLimit; - if (byteLimit > oldLimit) + if (absoluteLimit > oldLimit) { throw InvalidProtocolBufferException.TruncatedMessage(); } - state.currentLimit = byteLimit; + state.currentLimit = (int)absoluteLimit; RecomputeBufferSizeAfterLimit(ref state);