thrift: fix CVE-2026-41608

Backport patch to fix CVE-2026-41608.

References:
  https://nvd.nist.gov/vuln/detail/CVE-2026-41608

Upstream fix:
  https://github.com/apache/thrift/commit/8c605880ea4ef050a689c859de3b702ad8d4ce7b

Signed-off-by: Adarsh Jagadish Kamini <adarsh.jagadish.kamini@est.tech>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Adarsh Jagadish Kamini authored and Anuj Mittal committed 2026-09-15 07:52:14 +05:30
1 parent 194a8339ce
commit 50b2a64565
2 files changed
+177

No files matched your search

@@ -0,0 +1,176 @@
From d6ef4cd811666ce8c72dd5417d95debce52f490b Mon Sep 17 00:00:00 2001
From: Jens Geyer <jensg@apache.org>
Date: Sun, 19 Apr 2026 10:10:35 +0200
Subject: [PATCH] Add decompressed payload size limit to Python
THeaderTransport Client: py
Adds _max_decompressed_size (default: HARD_MAX_FRAME_SIZE) and
set_max_decompressed_size() to THeaderTransport. ZLIB decompression
now uses decompressobj with a size cap instead of unbounded
zlib.decompress(), consistent with the C++ implementation.
Refactor THeaderTransport read-transform dispatch into _apply_read_transform
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CVE: CVE-2026-41608
Upstream-Status: Backport [https://github.com/apache/thrift/commit/8c605880ea4ef050a689c859de3b702ad8d4ce7b]
Signed-off-by: Adarsh Jagadish Kamini <adarsh.jagadish.kamini@est.tech>
---
lib/py/src/transport/THeaderTransport.py | 26 +++++++---
lib/py/test/thrift_transport.py | 65 ++++++++++++++++++++++++
2 files changed, 85 insertions(+), 6 deletions(-)
diff --git a/lib/py/src/transport/THeaderTransport.py b/lib/py/src/transport/THeaderTransport.py
index 4fb203430..51bc43cf7 100644
--- a/lib/py/src/transport/THeaderTransport.py
+++ b/lib/py/src/transport/THeaderTransport.py
@@ -60,9 +60,7 @@ class THeaderTransformID(object):
ZLIB = 0x01
-READ_TRANSFORMS_BY_ID = {
- THeaderTransformID.ZLIB: zlib.decompress,
-}
+KNOWN_READ_TRANSFORM_IDS = frozenset({THeaderTransformID.ZLIB})
WRITE_TRANSFORMS_BY_ID = {
@@ -102,6 +100,7 @@ class THeaderTransport(TTransportBase, CReadableTransport):
self.sequence_id = 0
self._protocol_id = default_protocol
self._max_frame_size = HARD_MAX_FRAME_SIZE
+ self._max_decompressed_size = HARD_MAX_FRAME_SIZE
def isOpen(self):
return self._transport.isOpen()
@@ -135,6 +134,22 @@ class THeaderTransport(TTransportBase, CReadableTransport):
raise ValueError("maximum frame size should be < %d and > 0" % HARD_MAX_FRAME_SIZE)
self._max_frame_size = size
+ def set_max_decompressed_size(self, size):
+ if not 0 < size <= HARD_MAX_FRAME_SIZE:
+ raise ValueError("maximum decompressed size should be <= %d and > 0" % HARD_MAX_FRAME_SIZE)
+ self._max_decompressed_size = size
+
+ def _apply_read_transform(self, transform_id, payload):
+ if transform_id == THeaderTransformID.ZLIB:
+ decompressor = zlib.decompressobj()
+ payload = decompressor.decompress(payload, self._max_decompressed_size)
+ if decompressor.unconsumed_tail:
+ raise TTransportException(
+ TTransportException.SIZE_LIMIT,
+ "Decompressed payload exceeds maximum allowed size.",
+ )
+ return payload
+
@property
def protocol_id(self):
if self._client_type == THeaderClientType.HEADERS:
@@ -250,7 +265,7 @@ class THeaderTransport(TTransportBase, CReadableTransport):
transform_count = readVarint(buffer_transport)
for _ in range(transform_count):
transform_id = readVarint(buffer_transport)
- if transform_id not in READ_TRANSFORMS_BY_ID:
+ if transform_id not in KNOWN_READ_TRANSFORM_IDS:
raise TApplicationException(
TApplicationException.INVALID_TRANSFORM,
"Unknown transform: %d" % transform_id,
@@ -276,8 +291,7 @@ class THeaderTransport(TTransportBase, CReadableTransport):
payload = buffer.read()
for transform_id in transforms:
- transform_fn = READ_TRANSFORMS_BY_ID[transform_id]
- payload = transform_fn(payload)
+ payload = self._apply_read_transform(transform_id, payload)
return BytesIO(payload)
def write(self, buf):
diff --git a/lib/py/test/thrift_transport.py b/lib/py/test/thrift_transport.py
index cb1bb0ce7..cb0d815cb 100644
--- a/lib/py/test/thrift_transport.py
+++ b/lib/py/test/thrift_transport.py
@@ -17,11 +17,17 @@
# under the License.
#
+import struct
import unittest
+import zlib
import os
import _import_local_thrift # noqa
from thrift.transport import TTransport
+from thrift.transport.THeaderTransport import (
+ THeaderTransport, THeaderClientType, THeaderTransformID, HEADER_MAGIC,
+)
+from thrift.transport.TTransport import TTransportException
class TestTFileObjectTransport(unittest.TestCase):
@@ -66,5 +72,64 @@ class TestMemoryBuffer(unittest.TestCase):
buffer_r.close()
+def _craft_zlib_header_frame(plain_payload):
+ """Return a complete THeaderTransport frame with a ZLIB-compressed payload."""
+ compressed = zlib.compress(plain_payload)
+ # proto_id=0, 1 transform; values <128 encode as single varint bytes
+ hdr_content = bytes([0, 1, THeaderTransformID.ZLIB])
+ padding = (4 - len(hdr_content) % 4) % 4
+ hdr_bytes = hdr_content + b'\x00' * padding
+ header_len_u16 = len(hdr_bytes) // 4
+ # 10 fixed bytes: 2(magic)+2(flags)+4(seqid)+2(hdr_len_u16)
+ frame_size = 10 + len(hdr_bytes) + len(compressed)
+ frame = struct.pack('!i', frame_size)
+ frame += struct.pack('!H', HEADER_MAGIC)
+ frame += struct.pack('!H', 0) # flags
+ frame += struct.pack('!i', 0) # sequence_id
+ frame += struct.pack('!H', header_len_u16)
+ frame += hdr_bytes
+ frame += compressed
+ return frame
+
+
+class TestTHeaderTransportDecompressionLimit(unittest.TestCase):
+
+ def _make_transport(self, payload, max_decompressed_size=None):
+ frame = _craft_zlib_header_frame(payload)
+ inner = TTransport.TMemoryBuffer(frame)
+ transport = THeaderTransport(inner, [THeaderClientType.HEADERS])
+ if max_decompressed_size is not None:
+ transport.set_max_decompressed_size(max_decompressed_size)
+ return transport
+
+ def test_zlib_roundtrip(self):
+ payload = b'Hello Thrift! ' * 100
+ t = self._make_transport(payload)
+ t.readFrame(1)
+ self.assertEqual(t._read_buffer.read(), payload)
+
+ def test_decompression_bomb_blocked(self):
+ # 10,000 bytes decompressed; limit set to 1,000
+ payload = b'A' * 10000
+ t = self._make_transport(payload, max_decompressed_size=1000)
+ with self.assertRaises(TTransportException) as ctx:
+ t.readFrame(1)
+ self.assertEqual(ctx.exception.type, TTransportException.SIZE_LIMIT)
+
+ def test_payload_at_limit_passes(self):
+ payload = b'B' * 5000
+ t = self._make_transport(payload, max_decompressed_size=5000)
+ t.readFrame(1)
+ self.assertEqual(t._read_buffer.read(), payload)
+
+ def test_set_max_decompressed_size_rejects_invalid(self):
+ inner = TTransport.TMemoryBuffer(b'')
+ t = THeaderTransport(inner, [THeaderClientType.HEADERS])
+ with self.assertRaises(ValueError):
+ t.set_max_decompressed_size(0)
+ with self.assertRaises(ValueError):
+ t.set_max_decompressed_size(-1)
+
+
if __name__ == '__main__':
unittest.main()
@@ -18,6 +18,7 @@ SRC_URI = "https://downloads.apache.org/${BPN}/${PV}/${BP}.tar.gz \
file://CVE-2026-48144.patch \
file://CVE-2026-58389.patch \
file://CVE-2026-58662.patch \
file://CVE-2026-41608.patch \
"
SRC_URI[sha256sum] = "794a0e455787960d9f27ab92c38e34da27e8deeda7a5db0e59dc64a00df8a1e5"