diff --git a/meta-oe/recipes-connectivity/thrift/thrift/CVE-2026-41608.patch b/meta-oe/recipes-connectivity/thrift/thrift/CVE-2026-41608.patch new file mode 100644 index 0000000000..a65f0ac73b --- /dev/null +++ b/meta-oe/recipes-connectivity/thrift/thrift/CVE-2026-41608.patch @@ -0,0 +1,176 @@ +From d6ef4cd811666ce8c72dd5417d95debce52f490b Mon Sep 17 00:00:00 2001 +From: Jens Geyer +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 + +CVE: CVE-2026-41608 +Upstream-Status: Backport [https://github.com/apache/thrift/commit/8c605880ea4ef050a689c859de3b702ad8d4ce7b] + +Signed-off-by: Adarsh Jagadish Kamini +--- + 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() diff --git a/meta-oe/recipes-connectivity/thrift/thrift_0.22.0.bb b/meta-oe/recipes-connectivity/thrift/thrift_0.22.0.bb index 949ffc3e70..6d76771ab2 100644 --- a/meta-oe/recipes-connectivity/thrift/thrift_0.22.0.bb +++ b/meta-oe/recipes-connectivity/thrift/thrift_0.22.0.bb @@ -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"