Medium severity5.3NVD Advisory· Published May 5, 2026· Updated May 6, 2026
CVE-2026-43868
CVE-2026-43868
Description
Memory Allocation with Excessive Size Value vulnerability in Apache Thrift.
This issue affects Apache Thrift: before 0.23.0.
Users are recommended to upgrade to version 0.23.0, which fixes the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
thriftcrates.io | <= 0.22.0 | — |
Affected products
2Patches
1d5152211af61Add byte-count limit to TCompactProtocol varint reader
21 files changed · +416 −88
lib/dart/lib/src/protocol/t_compact_protocol.dart+7 −6 modified@@ -39,6 +39,7 @@ class TCompactProtocol extends TProtocol { static const int TYPE_MASK = 0xE0; static const int TYPE_BITS = 0x07; static const int TYPE_SHIFT_AMOUNT = 5; + static const int MAX_VARINT_BYTES = 10; // ceil(64/7); matches protobuf wire format static final TField TSTOP = TField("", TType.STOP, 0); static const int TYPE_BOOLEAN_TRUE = 0x01; @@ -427,25 +428,25 @@ static final List<int> _typeMap = List<int>.unmodifiable( Int32 _readVarInt32() { Int32 result = Int32.ZERO; int shift = 0; - while (true) { + for (int idx = 0; idx < MAX_VARINT_BYTES; idx++) { Int32 b = Int32(readByte()); result |= (b & 0x7f) << shift; - if ((b & 0x80) != 0x80) break; + if ((b & 0x80) != 0x80) return result; shift += 7; } - return result; + throw TProtocolError(TProtocolErrorType.INVALID_DATA, 'Variable-length int over 10 bytes.'); } Int64 _readVarInt64() { Int64 result = Int64.ZERO; int shift = 0; - while (true) { + for (int idx = 0; idx < MAX_VARINT_BYTES; idx++) { Int64 b = Int64(readByte()); result |= (b & 0x7f) << shift; - if ((b & 0x80) != 0x80) break; + if ((b & 0x80) != 0x80) return result; shift += 7; } - return result; + throw TProtocolError(TProtocolErrorType.INVALID_DATA, 'Variable-length int over 10 bytes.'); } Int32 _zigzagToInt32(Int32 n) {
lib/dart/test/protocol/t_protocol_test.dart+23 −0 modified@@ -395,6 +395,29 @@ void main() { }); group('shared tests', sharedTests); + + test('readI64 rejects overlong varint', () async { + final trans = TBufferedTransport(); + final proto = TCompactProtocol(trans); + final bytes = Uint8List(11)..fillRange(0, 11, 0x80); + trans.write(bytes, 0, 11); + await trans.flush(); + expect( + () => proto.readI64(), + throwsA(isA<TProtocolError>().having( + (e) => e.type, 'type', TProtocolErrorType.INVALID_DATA))); + }); + + test('readI64 accepts valid 10-byte varint', () async { + final trans = TBufferedTransport(); + final proto = TCompactProtocol(trans); + final bytes = Uint8List(10) + ..fillRange(0, 9, 0x80) + ..[9] = 0x01; + trans.write(bytes, 0, 10); + await trans.flush(); + expect(() => proto.readI64(), returnsNormally); + }); }); }
lib/delphi/src/Thrift.Protocol.Compact.pas+11 −7 modified@@ -60,7 +60,9 @@ TFactory = class( TInterfacedObject, IProtocolFactory) VERSION_MASK = Byte( $1F); // 0001 1111 TYPE_MASK = Byte( $E0); // 1110 0000 TYPE_BITS = Byte( $07); // 0000 0111 - TYPE_SHIFT_AMOUNT = Byte( 5); + TYPE_SHIFT_AMOUNT = Byte( 5); + MAX_VARINT32_BYTES = Integer( 5); // ceil(32/7); matches protobuf wire format + MAX_VARINT64_BYTES = Integer( 10); // ceil(64/7); matches protobuf wire format strict private type // All of the on-wire type codes. @@ -915,36 +917,38 @@ procedure TCompactProtocolImpl.ReadSetEnd; // Read an i32 from the wire as a varint. The MSB of each byte is set // if there is another byte to follow. This can Read up to 5 bytes. function TCompactProtocolImpl.ReadVarint32 : Cardinal; -var shift : Integer; +var shift, idx : Integer; b : Byte; begin result := 0; shift := 0; - while TRUE do begin + for idx := 0 to MAX_VARINT32_BYTES - 1 do begin b := Byte( ReadByte); result := result or (Cardinal(b and $7F) shl shift); if ((b and $80) <> $80) - then Break; + then Exit; Inc( shift, 7); end; + raise TProtocolExceptionInvalidData.Create('Variable-length int over 5 bytes.'); end; // Read an i64 from the wire as a proper varint. The MSB of each byte is set // if there is another byte to follow. This can Read up to 10 bytes. function TCompactProtocolImpl.ReadVarint64 : UInt64; -var shift : Integer; +var shift, idx : Integer; b : Byte; begin result := 0; shift := 0; - while TRUE do begin + for idx := 0 to MAX_VARINT64_BYTES - 1 do begin b := Byte( ReadByte); result := result or (UInt64(b and $7F) shl shift); if ((b and $80) <> $80) - then Break; + then Exit; Inc( shift, 7); end; + raise TProtocolExceptionInvalidData.Create('Variable-length int over 10 bytes.'); end;
lib/erl/src/thrift_compact_protocol.erl+17 −9 modified@@ -129,13 +129,15 @@ to_varint(Value, Acc) when (Value < 16#80) -> [Acc, Value]; to_varint(Value, Acc) -> to_varint(Value bsr 7, [Acc, ((Value band 16#7F) bor 16#80)]). -spec read_varint(t_compact(), non_neg_integer(), non_neg_integer()) -> - {t_compact(), {'ok', integer()}}. -read_varint(This0, Acc, Count) -> + {t_compact(), {'ok', integer()} | {error, term()}}. +read_varint(This0, Acc, Count) when Count < 10 -> {This1, {ok, Byte}} = read(This0, byte), case (Byte band 16#80) of 0 -> {This1, {ok, (Byte bsl (7 * Count)) + Acc}}; _ -> read_varint(This1, ((Byte band 16#7f) bsl (7 * Count)) + Acc, Count + 1) - end. + end; +read_varint(This0, _Acc, _Count) -> + {This0, {error, {invalid_data, "Variable-length int exceeds 10 bytes"}}}. write(This0, #protocol_message_begin{ name = Name, @@ -348,16 +350,22 @@ read(This0, byte) -> Else -> {This1, Else} end; read(This0, i16) -> - {This1, {ok, Zigzag}} = read_varint(This0, 0, 0), - {This1, {ok, from_zigzag(Zigzag)}}; + case read_varint(This0, 0, 0) of + {This1, {ok, Zigzag}} -> {This1, {ok, from_zigzag(Zigzag)}}; + {This1, {error, _} = Err} -> {This1, Err} + end; read(This0, ui32) -> read_varint(This0, 0, 0); read(This0, i32) -> - {This1, {ok, Zigzag}} = read_varint(This0, 0, 0), - {This1, {ok, from_zigzag(Zigzag)}}; + case read_varint(This0, 0, 0) of + {This1, {ok, Zigzag}} -> {This1, {ok, from_zigzag(Zigzag)}}; + {This1, {error, _} = Err} -> {This1, Err} + end; read(This0, i64) -> - {This1, {ok, Zigzag}} = read_varint(This0, 0, 0), - {This1, {ok, from_zigzag(Zigzag)}}; + case read_varint(This0, 0, 0) of + {This1, {ok, Zigzag}} -> {This1, {ok, from_zigzag(Zigzag)}}; + {This1, {error, _} = Err} -> {This1, Err} + end; read(This0, double) -> {This1, Bytes} = read_data(This0, 8), case Bytes of
lib/go/thrift/compact_protocol.go+7 −3 modified@@ -766,23 +766,27 @@ func (p *TCompactProtocol) readVarint32() (int32, error) { return int32(v), err } +// maxVarint64Bytes is the maximum wire size of a varint-encoded 64-bit integer: +// ceil(64/7) = 10 bytes, matching the protobuf wire-format specification. +const maxVarint64Bytes = 10 + // Read an i64 from the wire as a proper varint. The MSB of each byte is set // if there is another byte to follow. This can read up to 10 bytes. func (p *TCompactProtocol) readVarint64() (int64, error) { shift := uint(0) result := int64(0) - for { + for rsize := 0; rsize < maxVarint64Bytes; rsize++ { b, err := p.readByteDirect() if err != nil { return 0, err } result |= int64(b&0x7f) << shift if (b & 0x80) != 0x80 { - break + return result, nil } shift += 7 } - return result, nil + return 0, NewTProtocolExceptionWithType(INVALID_DATA, errors.New("variable-length int over 10 bytes")) } // Read a byte, unlike ReadByte that reads Thrift-byte that is i8.
lib/go/thrift/compact_protocol_test.go+24 −0 modified@@ -24,6 +24,30 @@ import ( "testing" ) +func TestCompactProtocolVarintRejectsOverlong(t *testing.T) { + // 11 continuation bytes (bit 7 set), no terminating byte + payload := bytes.Repeat([]byte{0x80}, 11) + trans := NewTMemoryBufferLen(len(payload)) + trans.Write(payload) + p := NewTCompactProtocol(trans) + _, err := p.readVarint64() + if err == nil { + t.Fatal("expected error for varint over 10 bytes, got nil") + } +} + +func TestCompactProtocolVarintAcceptsValid10Byte(t *testing.T) { + // 9 continuation bytes followed by a terminating byte + payload := append(bytes.Repeat([]byte{0x80}, 9), 0x01) + trans := NewTMemoryBufferLen(len(payload)) + trans.Write(payload) + p := NewTCompactProtocol(trans) + _, err := p.readVarint64() + if err != nil { + t.Fatalf("unexpected error for valid 10-byte varint: %v", err) + } +} + func TestReadWriteCompactProtocol(t *testing.T) { ReadWriteProtocolTest(t, NewTCompactProtocolFactory())
lib/haxe/src/org/apache/thrift/protocol/TCompactProtocol.hx+7 −7 modified@@ -50,6 +50,7 @@ class TCompactProtocol extends TProtocolImplBase implements TProtocol { private static inline var TYPE_MASK : Int = 0xE0; // 1110 0000 private static inline var TYPE_BITS : Int = 0x07; // 0000 0111 private static inline var TYPE_SHIFT_AMOUNT : Int = 5; + private static inline var MAX_VARINT_BYTES : Int = 10; // ceil(64/7); matches protobuf wire format private static var ttypeToCompactType = [ @@ -650,15 +651,15 @@ class TCompactProtocol extends TProtocolImplBase implements TProtocol { private function ReadVarint32() : UInt { var result : UInt = 0; var shift : Int = 0; - while (true) { + for (idx in 0 ... MAX_VARINT_BYTES) { var b : Int = readByte(); result |= cast((b & 0x7f) << shift, UInt); if ((b & 0x80) != 0x80) { - break; + return result; } shift += 7; } - return result; + throw new TProtocolException( TProtocolException.INVALID_DATA, "Variable-length int over 10 bytes."); } /** @@ -668,16 +669,15 @@ class TCompactProtocol extends TProtocolImplBase implements TProtocol { private function ReadVarint64() : Int64 { var shift : Int = 0; var result : Int64 = Int64.make(0,0); - while (true) { + for (idx in 0 ... MAX_VARINT_BYTES) { var b : Int = readByte(); result = Int64.or( result, Int64.shl( Int64.make(0,b & 0x7f), shift)); if ((b & 0x80) != 0x80) { - break; + return result; } shift += 7; } - - return result; + throw new TProtocolException( TProtocolException.INVALID_DATA, "Variable-length int over 10 bytes."); }
lib/java/src/main/java/org/apache/thrift/protocol/TCompactProtocol.java+22 −6 modified@@ -93,6 +93,10 @@ public TProtocol getProtocol(TTransport trans) { private static final byte TYPE_BITS = 0x07; // 0000 0111 private static final int TYPE_SHIFT_AMOUNT = 5; + // Maximum wire bytes for a varint-encoded integer: ceil(32/7)=5, ceil(64/7)=10. + private static final int MAX_VARINT32_BYTES = 5; + private static final int MAX_VARINT64_BYTES = 10; + /** All of the on-wire type codes. */ private static class Types { public static final byte BOOLEAN_TRUE = 0x01; @@ -750,7 +754,7 @@ public void readSetEnd() throws TException {} private int readVarint32() throws TException { int result = 0; int shift = 0; - if (trans_.getBytesRemainingInBuffer() >= 5) { + if (trans_.getBytesRemainingInBuffer() >= MAX_VARINT32_BYTES) { byte[] buf = trans_.getBuffer(); int pos = trans_.getBufferPosition(); int off = 0; @@ -760,15 +764,21 @@ private int readVarint32() throws TException { if ((b & 0x80) != 0x80) break; shift += 7; off++; + if (off >= MAX_VARINT32_BYTES) { + throw new TProtocolException( + TProtocolException.INVALID_DATA, "Variable-length int over 5 bytes."); + } } trans_.consumeBuffer(off + 1); } else { - while (true) { + for (int rsize = 0; rsize < MAX_VARINT32_BYTES; rsize++) { byte b = readByte(); result |= (b & 0x7f) << shift; - if ((b & 0x80) != 0x80) break; + if ((b & 0x80) != 0x80) return result; shift += 7; } + throw new TProtocolException( + TProtocolException.INVALID_DATA, "Variable-length int over 5 bytes."); } return result; } @@ -780,7 +790,7 @@ private int readVarint32() throws TException { private long readVarint64() throws TException { int shift = 0; long result = 0; - if (trans_.getBytesRemainingInBuffer() >= 10) { + if (trans_.getBytesRemainingInBuffer() >= MAX_VARINT64_BYTES) { byte[] buf = trans_.getBuffer(); int pos = trans_.getBufferPosition(); int off = 0; @@ -790,15 +800,21 @@ private long readVarint64() throws TException { if ((b & 0x80) != 0x80) break; shift += 7; off++; + if (off >= MAX_VARINT64_BYTES) { + throw new TProtocolException( + TProtocolException.INVALID_DATA, "Variable-length int over 10 bytes."); + } } trans_.consumeBuffer(off + 1); } else { - while (true) { + for (int rsize = 0; rsize < MAX_VARINT64_BYTES; rsize++) { byte b = readByte(); result |= (long) (b & 0x7f) << shift; - if ((b & 0x80) != 0x80) break; + if ((b & 0x80) != 0x80) return result; shift += 7; } + throw new TProtocolException( + TProtocolException.INVALID_DATA, "Variable-length int over 10 bytes."); } return result; }
lib/java/src/test/java/org/apache/thrift/protocol/TestTCompactProtocol.java+42 −0 modified@@ -19,8 +19,14 @@ package org.apache.thrift.protocol; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; import org.apache.thrift.TDeserializer; import org.apache.thrift.TException; +import org.apache.thrift.transport.TMemoryInputTransport; import org.junit.jupiter.api.Test; import thrift.test.Bonk; @@ -49,6 +55,42 @@ public void testOOMDenialOfService() throws Exception { } } + @Test + public void testReadVarint64FastPathRejectsOverlong() throws Exception { + byte[] buf = new byte[11]; + Arrays.fill(buf, (byte) 0x80); // 11 continuation bytes, no terminator + TCompactProtocol proto = new TCompactProtocol(new TMemoryInputTransport(buf)); + TProtocolException ex = assertThrows(TProtocolException.class, proto::readI64); + assertEquals(TProtocolException.INVALID_DATA, ex.getType()); + } + + @Test + public void testReadVarint64FastPathAcceptsValid10Byte() throws Exception { + byte[] buf = new byte[10]; + Arrays.fill(buf, (byte) 0x80); + buf[9] = 0x01; // terminating byte + TCompactProtocol proto = new TCompactProtocol(new TMemoryInputTransport(buf)); + assertDoesNotThrow(proto::readI64); + } + + @Test + public void testReadVarint32FastPathRejectsOverlong() throws Exception { + byte[] buf = new byte[6]; + Arrays.fill(buf, (byte) 0x80); // 6 continuation bytes, no terminator + TCompactProtocol proto = new TCompactProtocol(new TMemoryInputTransport(buf)); + TProtocolException ex = assertThrows(TProtocolException.class, proto::readI32); + assertEquals(TProtocolException.INVALID_DATA, ex.getType()); + } + + @Test + public void testReadVarint32FastPathAcceptsValid5Byte() throws Exception { + byte[] buf = new byte[5]; + Arrays.fill(buf, (byte) 0x80); + buf[4] = 0x01; // terminating byte + TCompactProtocol proto = new TCompactProtocol(new TMemoryInputTransport(buf)); + assertDoesNotThrow(proto::readI32); + } + public static void main(String args[]) throws Exception { new TestTCompactProtocol().benchmark(); }
lib/lua/TCompactProtocol.lua+10 −7 modified@@ -29,7 +29,8 @@ TCompactProtocol = __TObject.new(TProtocolBase, { COMPACT_VERSION_MASK = 0x1f, COMPACT_TYPE_MASK = 0xE0, COMPACT_TYPE_BITS = 0x07, - COMPACT_TYPE_SHIFT_AMOUNT = 5, + COMPACT_TYPE_SHIFT_AMOUNT = 5, + COMPACT_MAX_VARINT_BYTES = 10, -- ceil(64/7); matches protobuf wire format -- Used to keep track of the last field for the current and previous structs, -- so we can do the delta stuff. @@ -445,31 +446,33 @@ end function TCompactProtocol:readVarint32() local shiftl = 0 local result = 0 - while true do + for idx = 0, self.COMPACT_MAX_VARINT_BYTES - 1 do b = self:readByte() result = libluabitwise.bor(result, libluabitwise.shiftl(libluabitwise.band(b, 0x7f), shiftl)) if libluabitwise.band(b, 0x80) ~= 0x80 then - break + return result end shiftl = shiftl + 7 end - return result + terror(TProtocolException:new{errorCode = TProtocolException.INVALID_DATA, + message = 'Variable-length int over 10 bytes.'}) end function TCompactProtocol:readVarint64() local result = liblualongnumber.new local data = result(0) local shiftl = 0 - while true do + for idx = 0, self.COMPACT_MAX_VARINT_BYTES - 1 do b = self:readSignByte() endFlag, data = libluabpack.fromVarint64(b, shiftl, data) shiftl = shiftl + 7 if endFlag == 0 then - break + return data end end - return data + terror(TProtocolException:new{errorCode = TProtocolException.INVALID_DATA, + message = 'Variable-length int over 10 bytes.'}) end function TCompactProtocol:getTType(ctype)
lib/netstd/Thrift/Protocol/TCompactProtocol.cs+9 −6 modified@@ -42,6 +42,8 @@ public class TCompactProtocol : TProtocol private const byte TypeMask = 0xE0; // 1110 0000 private const byte TypeBits = 0x07; // 0000 0111 private const int TypeShiftAmount = 5; + private const int MaxVarint32Bytes = 5; // ceil(32/7); matches protobuf wire format + private const int MaxVarint64Bytes = 10; // ceil(64/7); matches protobuf wire format private const byte NoTypeOverride = 0xFF; @@ -744,18 +746,18 @@ Read an i32 from the wire as a varint. The MSB of each byte is set uint result = 0; var shift = 0; - while (true) + for (var idx = 0; idx < MaxVarint32Bytes; idx++) { var b = (byte) await ReadByteAsync(cancellationToken); result |= (uint) (b & 0x7f) << shift; if ((b & 0x80) != 0x80) { - break; + return result; } shift += 7; } - return result; + throw new TProtocolException(TProtocolException.INVALID_DATA, "Variable-length int over 5 bytes."); } private async ValueTask<ulong> ReadVarInt64Async(CancellationToken cancellationToken) @@ -769,18 +771,19 @@ Read an i64 from the wire as a proper varint. The MSB of each byte is set var shift = 0; ulong result = 0; - while (true) + + for (var idx = 0; idx < MaxVarint64Bytes; idx++) { var b = (byte) await ReadByteAsync(cancellationToken); result |= (ulong) (b & 0x7f) << shift; if ((b & 0x80) != 0x80) { - break; + return result; } shift += 7; } - return result; + throw new TProtocolException(TProtocolException.INVALID_DATA, "Variable-length int over 10 bytes."); } private static int ZigzagToInt(uint n)
lib/php/lib/Protocol/TCompactProtocol.php+4 −4 modified@@ -64,6 +64,8 @@ class TCompactProtocol extends TProtocol const TYPE_BITS = 0x07; const TYPE_SHIFT_AMOUNT = 5; + const MAX_VARINT_BYTES = 10; // ceil(64/7); matches protobuf wire format + protected static $ctypes = array( TType::STOP => TCompactProtocol::COMPACT_STOP, TType::BOOL => TCompactProtocol::COMPACT_TRUE, // used for collection @@ -145,7 +147,7 @@ public function readVarint(&$result) $idx = 0; $shift = 0; $result = 0; - while (true) { + while ($idx < self::MAX_VARINT_BYTES) { $x = $this->trans_->readAll(1); $arr = unpack('C', $x); $byte = $arr[1]; @@ -156,9 +158,7 @@ public function readVarint(&$result) } $shift += 7; } - - #unreachable statement - return $idx; + throw new TProtocolException('Variable-length int over 10 bytes.', TProtocolException::INVALID_DATA); } public function __construct($trans)
lib/php/test/Unit/Lib/Protocol/TCompactProtocolTest.php+32 −0 modified@@ -164,6 +164,38 @@ public function testReadVarint() $this->assertSame(1000, $result); } + public function testReadVarintRejectsOverlong() + { + $transport = $this->createMock(TTransport::class); + $protocol = new TCompactProtocol($transport); + + $transport->expects($this->exactly(10)) + ->method('readAll') + ->with(1) + ->willReturn("\x80"); // continuation byte — bit 7 always set + + $this->expectException(TProtocolException::class); + $this->expectExceptionCode(TProtocolException::INVALID_DATA); + $protocol->readVarint($result); + } + + public function testReadVarintAcceptsValid10ByteVarint() + { + $transport = $this->createMock(TTransport::class); + $protocol = new TCompactProtocol($transport); + + $continuations = array_fill(0, 9, "\x80"); + $continuations[] = "\x01"; // terminating byte + + $transport->expects($this->exactly(10)) + ->method('readAll') + ->with(1) + ->willReturnOnConsecutiveCalls(...$continuations); + + $byteCount = $protocol->readVarint($result); + $this->assertSame(10, $byteCount); + } + public function testWriteMessageBegin() { $name = 'testName';
lib/py/src/protocol/TCompactProtocol.py+6 −1 modified@@ -69,16 +69,21 @@ def writeVarint(trans, n): trans.write(bytes(out)) +_MAX_VARINT_BYTES = 10 # ceil(64/7); matches protobuf wire format + + def readVarint(trans): result = 0 shift = 0 - while True: + for _ in range(_MAX_VARINT_BYTES): x = trans.readAll(1) byte = ord(x) result |= (byte & 0x7f) << shift if byte >> 7 == 0: return result shift += 7 + raise TProtocolException(TProtocolException.INVALID_DATA, + "Variable-length int over 10 bytes.") # As per TCompactProtocol.tcc
lib/py/test/thrift_TCompactProtocol.py+15 −0 modified@@ -19,6 +19,7 @@ import _import_local_thrift # noqa from thrift.protocol import TCompactProtocol +from thrift.protocol.TProtocol import TProtocolException from thrift.transport import TTransport import unittest import uuid @@ -301,6 +302,20 @@ def test_TCompactProtocol_write_read(self): print("Assertion fail") raise e + def test_readVarint_rejects_overlong_input(self): + payload = bytes([0x80] * 11) # 11 continuation bytes, no terminator + trans = TTransport.TMemoryBuffer(payload) + with self.assertRaises(TProtocolException) as ctx: + TCompactProtocol.readVarint(trans) + self.assertEqual(ctx.exception.type, TProtocolException.INVALID_DATA) + + def test_readVarint_accepts_valid_10_byte_varint(self): + # max valid varint64: 10 bytes, last byte has bit 7 clear + payload = bytes([0x80] * 9 + [0x01]) + trans = TTransport.TMemoryBuffer(payload) + result = TCompactProtocol.readVarint(trans) + self.assertIsNotNone(result) + if __name__ == "__main__": unittest.main()
lib/rb/ext/compact_protocol.c+20 −5 modified@@ -436,22 +436,37 @@ static int32_t zig_zag_to_int(uint32_t n) { return (int32_t)((n >> 1) ^ (0U - (n & 1U))); } +#define MAX_VARINT32_BYTES 5 /* ceil(32/7); matches protobuf wire format */ +#define MAX_VARINT64_BYTES 10 /* ceil(64/7); matches protobuf wire format */ + static uint64_t read_varint64(VALUE self) { - int shift = 0; + int i, shift = 0; uint64_t result = 0; - while (true) { + for (i = 0; i < MAX_VARINT64_BYTES; i++) { int8_t b = read_byte_direct(self); result |= ((uint64_t)(b & 0x7f) << shift); if ((b & 0x80) != 0x80) { - break; + return result; } shift += 7; } - return result; + rb_exc_raise(get_protocol_exception(INT2FIX(PROTOERR_INVALID_DATA), rb_str_new2("Variable-length int over 10 bytes."))); + return 0; /* unreachable */ } static uint32_t read_varint32(VALUE self) { - return (uint32_t)read_varint64(self); + int i, shift = 0; + uint32_t result = 0; + for (i = 0; i < MAX_VARINT32_BYTES; i++) { + int8_t b = read_byte_direct(self); + result |= ((uint32_t)(b & 0x7f) << shift); + if ((b & 0x80) != 0x80) { + return result; + } + shift += 7; + } + rb_exc_raise(get_protocol_exception(INT2FIX(PROTOERR_INVALID_DATA), rb_str_new2("Variable-length int over 5 bytes."))); + return 0; /* unreachable */ } static int16_t read_i16(VALUE self) {
lib/rb/lib/thrift/protocol/compact_protocol.rb+14 −4 modified@@ -27,6 +27,8 @@ class CompactProtocol < BaseProtocol TYPE_MASK = 0xE0 TYPE_BITS = 0x07 TYPE_SHIFT_AMOUNT = 5 + MAX_VARINT32_BYTES = 5 # ceil(32/7); matches protobuf wire format + MAX_VARINT_BYTES = 10 # ceil(64/7); matches protobuf wire format TSTOP = [nil, Types::STOP, 0] @@ -411,19 +413,27 @@ def write_varint64(n) end def read_varint32() - read_varint64() + shift = 0 + result = 0 + MAX_VARINT32_BYTES.times do + b = read_byte() + result |= (b & 0x7f) << shift + return result if (b & 0x80) != 0x80 + shift += 7 + end + raise ProtocolException.new(ProtocolException::INVALID_DATA, 'Variable-length int over 5 bytes.') end def read_varint64() shift = 0 result = 0 - while true + MAX_VARINT_BYTES.times do b = read_byte() result |= (b & 0x7f) << shift - break if (b & 0x80) != 0x80 + return result if (b & 0x80) != 0x80 shift += 7 end - result + raise ProtocolException.new(ProtocolException::INVALID_DATA, 'Variable-length int over 10 bytes.') end def int_to_zig_zag(n)
lib/rb/spec/compact_protocol_spec.rb+28 −0 modified@@ -241,6 +241,34 @@ expect(trans.read(trans.available).unpack("C*")).to eq([0x07, 0x61, 0x62, 0x63, 0x20, 0xE2, 0x82, 0xAC]) end + it "should reject a varint with more than 10 continuation bytes" do + trans = Thrift::MemoryBufferTransport.new(([0x80] * 11).pack("C*")) + proto = Thrift::CompactProtocol.new(trans) + expect { proto.read_i64 }.to raise_error(Thrift::ProtocolException) do |e| + expect(e.type).to eq(Thrift::ProtocolException::INVALID_DATA) + end + end + + it "should accept a valid 10-byte varint" do + trans = Thrift::MemoryBufferTransport.new((([0x80] * 9) + [0x01]).pack("C*")) + proto = Thrift::CompactProtocol.new(trans) + expect { proto.read_i64 }.not_to raise_error + end + + it "should reject a 32-bit varint with more than 5 continuation bytes" do + trans = Thrift::MemoryBufferTransport.new(([0x80] * 6).pack("C*")) + proto = Thrift::CompactProtocol.new(trans) + expect { proto.read_i32 }.to raise_error(Thrift::ProtocolException) do |e| + expect(e.type).to eq(Thrift::ProtocolException::INVALID_DATA) + end + end + + it "should accept a valid 5-byte varint for i32" do + trans = Thrift::MemoryBufferTransport.new((([0x80] * 4) + [0x0f]).pack("C*")) + proto = Thrift::CompactProtocol.new(trans) + expect { proto.read_i32 }.not_to raise_error + end + class JankyHandler def Janky(i32arg) i32arg * 2
lib/rs/src/protocol/compact.rs+77 −8 modified@@ -16,7 +16,7 @@ // under the License. use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use integer_encoding::{VarIntReader, VarIntWriter}; +use integer_encoding::VarIntWriter; use std::convert::{From, TryFrom}; use std::io; @@ -31,6 +31,8 @@ use crate::{ProtocolError, ProtocolErrorKind, TConfiguration}; const COMPACT_PROTOCOL_ID: u8 = 0x82; const COMPACT_VERSION: u8 = 0x01; const COMPACT_VERSION_MASK: u8 = 0x1F; +const MAX_VARINT32_BYTES: usize = 5; // ceil(32/7); matches protobuf wire format +const MAX_VARINT64_BYTES: usize = 10; // ceil(64/7); matches protobuf wire format /// Read messages encoded in the Thrift compact protocol. /// @@ -101,7 +103,7 @@ where // high bits set high if count and type encoded separately possible_element_count as i32 } else { - self.transport.read_varint::<u32>()? as i32 + self.read_varint32()? as i32 }; let min_element_size = self.min_serialized_size(element_type); @@ -121,6 +123,40 @@ where } Ok(()) } + + fn read_varint32(&mut self) -> crate::Result<u32> { + let mut result = 0u32; + let mut shift = 0u32; + for _ in 0..MAX_VARINT32_BYTES { + let b = self.read_byte()?; + result |= ((b & 0x7F) as u32) << shift; + if b & 0x80 == 0 { + return Ok(result); + } + shift += 7; + } + Err(crate::Error::Protocol(ProtocolError::new( + ProtocolErrorKind::InvalidData, + "Variable-length int over 5 bytes.", + ))) + } + + fn read_varint64(&mut self) -> crate::Result<u64> { + let mut result = 0u64; + let mut shift = 0u32; + for _ in 0..MAX_VARINT64_BYTES { + let b = self.read_byte()?; + result |= ((b & 0x7F) as u64) << shift; + if b & 0x80 == 0 { + return Ok(result); + } + shift += 7; + } + Err(crate::Error::Protocol(ProtocolError::new( + ProtocolErrorKind::InvalidData, + "Variable-length int over 10 bytes.", + ))) + } } impl<T> TInputProtocol for TCompactInputProtocol<T> @@ -156,7 +192,7 @@ where // NOTE: unsigned right shift will pad with 0s let message_type: TMessageType = TMessageType::try_from(type_and_byte >> 5)?; // writing side wrote signed sequence number as u32 to avoid zigzag encoding - let sequence_number = self.transport.read_varint::<u32>()? as i32; + let sequence_number = self.read_varint32()? as i32; let service_call_name = self.read_string()?; self.last_read_field_id = 0; @@ -265,7 +301,7 @@ where } fn read_bytes(&mut self) -> crate::Result<Vec<u8>> { - let len = self.transport.read_varint::<u32>()?; + let len = self.read_varint32()?; if let Some(max_size) = self.config.max_string_size() { if len as usize > max_size { @@ -291,15 +327,15 @@ where } fn read_i16(&mut self) -> crate::Result<i16> { - self.transport.read_varint::<i16>().map_err(From::from) + Ok(zigzag_to_i32(self.read_varint32()?) as i16) } fn read_i32(&mut self) -> crate::Result<i32> { - self.transport.read_varint::<i32>().map_err(From::from) + Ok(zigzag_to_i32(self.read_varint32()?)) } fn read_i64(&mut self) -> crate::Result<i64> { - self.transport.read_varint::<i64>().map_err(From::from) + Ok(zigzag_to_i64(self.read_varint64()?)) } fn read_double(&mut self) -> crate::Result<f64> { @@ -340,7 +376,7 @@ where } fn read_map_begin(&mut self) -> crate::Result<TMapIdentifier> { - let element_count = self.transport.read_varint::<u32>()? as i32; + let element_count = self.read_varint32()? as i32; if element_count == 0 { Ok(TMapIdentifier::new(None, None, 0)) } else { @@ -397,6 +433,16 @@ pub(crate) fn compact_protocol_min_serialized_size(field_type: TType) -> usize { } } +#[inline] +fn zigzag_to_i32(n: u32) -> i32 { + ((n >> 1) as i32) ^ (0i32.wrapping_sub((n & 1) as i32)) +} + +#[inline] +fn zigzag_to_i64(n: u64) -> i64 { + ((n >> 1) as i64) ^ (0i64.wrapping_sub((n & 1) as i64)) +} + impl<T> io::Seek for TCompactInputProtocol<T> where T: io::Seek + TReadTransport, @@ -2907,6 +2953,29 @@ mod tests { assert!(i_prot.read_map_end().is_ok()); // will blow up if we try to read from empty buffer } + #[test] + fn must_reject_overlong_varint_i64() { + let (mut i_prot, _) = test_objects(); + i_prot.transport.set_readable_bytes(&[0x80u8; 11]); // 11 continuation bytes, no terminator + let res = i_prot.read_i64(); + assert!(res.is_err()); + match res { + Err(crate::Error::Protocol(e)) => { + assert_eq!(e.kind, ProtocolErrorKind::InvalidData); + } + _ => panic!("expected Protocol/InvalidData error"), + } + } + + #[test] + fn must_accept_valid_10_byte_varint_i64() { + let (mut i_prot, _) = test_objects(); + let mut bytes = [0x80u8; 10]; + bytes[9] = 0x01; // terminating byte + i_prot.transport.set_readable_bytes(&bytes); + assert!(i_prot.read_i64().is_ok()); + } + fn test_objects() -> ( TCompactInputProtocol<ReadHalf<TBufferChannel>>, TCompactOutputProtocol<WriteHalf<TBufferChannel>>,
lib/swift/Sources/TCompactProtocol.swift+13 −14 modified@@ -47,7 +47,8 @@ public class TCompactProtocol: TProtocol { public static let protocolID: UInt8 = 0x82 public static let version: UInt8 = 1 public static let versionMask: UInt8 = 0x1F // 0001 1111 - + static let maxVarintBytes = 10 // ceil(64/7); matches protobuf wire format + public var transport: TTransport var lastField: [UInt8] = [] @@ -139,35 +140,33 @@ public class TCompactProtocol: TProtocol { func readVarint32() throws -> UInt32 { var result: UInt32 = 0 var shift: UInt32 = 0 - while true { + for _ in 0 ..< TCompactProtocol.maxVarintBytes { let byte: UInt8 = try read() - + result |= UInt32(byte & 0x7F) << shift if (byte & 0x80) == 0 { - break + return result } - + shift += 7 } - - return result + throw TProtocolError(error: .invalidData, message: "Variable-length int over 10 bytes.") } - + func readVarint64() throws -> UInt64 { var result: UInt64 = 0 var shift: UInt64 = 0 - - while true { + for _ in 0 ..< TCompactProtocol.maxVarintBytes { let byte: UInt8 = try read() - + result |= UInt64(byte & 0x7F) << shift if (byte & 0x80) == 0 { - break + return result } - + shift += 7 } - return result + throw TProtocolError(error: .invalidData, message: "Variable-length int over 10 bytes.") }
lib/swift/Tests/ThriftTests/TCompactProtocolTests.swift+28 −1 modified@@ -211,6 +211,31 @@ class TCompactProtocolTests: XCTestCase { XCTAssertEqual(proto.zigZagToi64(UInt64.max), min, "Error 64bit zigzag on \(min)") } + func testVarintRejectsOverlong() { + let buf = Data(repeating: 0x80, count: 11) // 11 continuation bytes, no terminator + let t = TMemoryBufferTransport(readBuffer: buf) + let p = TCompactProtocol(on: t) + do { + let _: Int64 = try p.read() + XCTFail("Expected TProtocolError but no error was thrown") + } catch let protoError as TProtocolError { + guard case .invalidData = protoError.error else { + XCTFail("Expected .invalidData, got \(protoError.error)"); return + } + } catch { + XCTFail("Expected TProtocolError, got \(error)") + } + } + + func testVarintAcceptsValid10Byte() { + var bytes = [UInt8](repeating: 0x80, count: 9) + bytes.append(0x01) // terminating byte + let buf = Data(bytes) + let t = TMemoryBufferTransport(readBuffer: buf) + let p = TCompactProtocol(on: t) + XCTAssertNoThrow(try p.read() as Int64) + } + static var allTests : [(String, (TCompactProtocolTests) -> () throws -> Void)] { return [ ("testInt8WriteRead", testInt8WriteRead), @@ -223,7 +248,9 @@ class TCompactProtocolTests: XCTestCase { ("testDataWriteRead", testDataWriteRead), ("testStructWriteRead", testStructWriteRead), ("testInt32ZigZag", testInt32ZigZag), - ("testInt64ZigZag", testInt64ZigZag) + ("testInt64ZigZag", testInt64ZigZag), + ("testVarintRejectsOverlong", testVarintRejectsOverlong), + ("testVarintAcceptsValid10Byte", testVarintAcceptsValid10Byte) ] } }
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
4- github.com/advisories/GHSA-2f9f-gq7v-9h6mghsaADVISORY
- lists.apache.org/thread/zj76dtwnbbs1m7z3focf4wd51pqpsmn9nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-43868ghsaADVISORY
- github.com/apache/thrift/commit/d5152211af61f850ec393604316804096dd4632eghsaWEB
News mentions
1- Patch Tuesday - May 2026Rapid7 Blog · May 13, 2026