new file mode 100644
@@ -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()
@@ -17,6 +17,7 @@ SRC_URI = "https://downloads.apache.org/${BPN}/${PV}/${BP}.tar.gz \
file://CVE-2026-58023.patch \
file://CVE-2026-48144.patch \
file://CVE-2026-58389.patch \
+ file://CVE-2026-41608.patch \
"
SRC_URI[sha256sum] = "794a0e455787960d9f27ab92c38e34da27e8deeda7a5db0e59dc64a00df8a1e5"
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> --- .../thrift/thrift/CVE-2026-41608.patch | 176 ++++++++++++++++++ .../thrift/thrift_0.22.0.bb | 1 + 2 files changed, 177 insertions(+) create mode 100644 meta-oe/recipes-connectivity/thrift/thrift/CVE-2026-41608.patch