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 6bec67cfb2
PiperOrigin-RevId: 914940764
This commit is contained in:
Paweł Łukasik 2026-05-13 10:36:58 -07:00 committed by Copybara-Service
parent 6da2cd928e
commit fe9848e7b3
2 changed files with 18 additions and 3 deletions

View file

@ -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<InvalidProtocolBufferException>(() => FileDescriptorProto.Parser.ParseFrom(data));
}
}
}

View file

@ -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);