diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69223.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69223.patch
new file mode 100644
index 0000000000..e1fca9b8b9
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69223.patch
@@ -0,0 +1,848 @@
+From 7c9340cd2be5c8dd6d829a62220bb9129e7a9d8a Mon Sep 17 00:00:00 2001
+From: Sam Bull <git@sambull.org>
+Date: Sat, 3 Jan 2026 15:56:02 +0000
+Subject: [PATCH] Use decompressor max_length parameter (#11898) (#11918)
+
+---------
+
+CVE: CVE-2025-69223
+Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/2b920c39002cee0ec5b402581779bbaaf7c9138a]
+
+Backport Changes:
+- Adapted the decompression base class to aiohttp 3.9.5's
+  direct `zlib` implementation because the newer upstream
+  buffer and backend wrappers are absent.
+- Kept `BodyPartReader.decode()` and `_decode_content()`
+  synchronous. Applied the output limit through
+  `ZLibDecompressor.decompress_sync()` to preserve the
+  aiohttp 3.9.5 API while bounding gzip and deflate output.
+- Omitted the upstream `aiohttp/web_request.py` change from
+  `field.decode(chunk)` to `await field.decode(chunk)`.
+  That change is required only for upstream's asynchronous
+  `BodyPartReader.decode()` conversion. This backport keeps
+  synchronous decoding, so the existing call remains valid.
+- Did not carry the Brotli 1.2 dependency updates from
+  `pyproject.toml` and `requirements/runtime-deps.in`.
+  Scarthgap supplies python3-brotli 1.1.0, which lacks the
+  bounded-output API required by the upstream fix.
+- Disabled Brotli decoding by forcing `HAS_BROTLI` to
+  `False`. As a result, `Content-Encoding: br` is rejected
+  before a Brotli decoder is created. This closes the
+  decompression-bomb path at the cost of Brotli support.
+- Omitted upstream Zstandard implementation, dependency,
+  and test changes because aiohttp 3.9.5 does not support
+  Zstandard content decoding.
+- Adapted the client, parser, and multipart tests to the
+  aiohttp 3.9.5 fixtures and synchronous decoding API.
+
+(cherry picked from commit 92477c5a74c43dfe0474bd24f8de11875daa2298)
+Co-authored-by: J. Nick Koston <nick@koston.org>
+(cherry picked from commit 2b920c39002cee0ec5b402581779bbaaf7c9138a)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ CHANGES/11898.breaking.rst      |  3 ++
+ aiohttp/compression_utils.py    | 92 +++++++++++++++++++++------------
+ aiohttp/http_exceptions.py      |  4 ++
+ aiohttp/http_parser.py          | 29 +++++++++--
+ aiohttp/multipart.py            | 19 +++++--
+ docs/spelling_wordlist.txt      |  1 +
+ tests/test_client_functional.py | 80 +++++++++++++++++++++++++++-
+ tests/test_http_parser.py       | 44 +++++++++++++++-
+ tests/test_multipart.py         | 92 +++++++++++++++++++++++++--------
+ 9 files changed, 299 insertions(+), 65 deletions(-)
+ create mode 100644 CHANGES/11898.breaking.rst
+
+diff --git a/CHANGES/11898.breaking.rst b/CHANGES/11898.breaking.rst
+new file mode 100644
+index 000000000..228b69baa
+--- /dev/null
++++ b/CHANGES/11898.breaking.rst
+@@ -0,0 +1,3 @@
++``Brotli`` decoding is disabled in the Scarthgap backport because its
++``python3-brotli`` recipe provides version 1.1.0, not the required 1.2.
++Decompression now has a default maximum output size of 32MiB per decompress call -- by :user:`Dreamsorcerer`.
+diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py
+index 9631d377e..fe762c755 100644
+--- a/aiohttp/compression_utils.py
++++ b/aiohttp/compression_utils.py
+@@ -1,5 +1,6 @@
+ import asyncio
+ import zlib
++from abc import ABC, abstractmethod
+ from concurrent.futures import Executor
+ from typing import Optional, cast
+ 
+@@ -13,7 +14,17 @@ try:
+ except ImportError:  # pragma: no cover
+     HAS_BROTLI = False
+ 
+-MAX_SYNC_CHUNK_SIZE = 1024
++# Scarthgap provides python3-brotli 1.1.0, whose Decompressor API does not
++# support the max_length argument required by the bounded decoder below.
++# Do not advertise or instantiate Brotli decoding until the recipe provides
++# Brotli 1.2 or newer.
++HAS_BROTLI = False
++
++MAX_SYNC_CHUNK_SIZE = 4096
++DEFAULT_MAX_DECOMPRESS_SIZE = 2**25  # 32MiB
++
++# Unlimited decompression constant
++ZLIB_MAX_LENGTH_UNLIMITED = 0  # zlib uses 0 to mean unlimited
+ 
+ 
+ def encoding_to_mode(
+@@ -26,19 +37,37 @@ def encoding_to_mode(
+     return -zlib.MAX_WBITS if suppress_deflate_header else zlib.MAX_WBITS
+ 
+ 
+-class ZlibBaseHandler:
++class DecompressionBaseHandler(ABC):
+     def __init__(
+         self,
+-        mode: int,
+         executor: Optional[Executor] = None,
+         max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE,
+     ):
+-        self._mode = mode
++        """Base class for decompression handlers."""
+         self._executor = executor
+         self._max_sync_chunk_size = max_sync_chunk_size
+ 
++    @abstractmethod
++    def decompress_sync(
++        self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
++    ) -> bytes:
++        """Decompress the given data."""
++
++    async def decompress(
++        self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
++    ) -> bytes:
++        """Decompress the given data."""
++        if (
++            self._max_sync_chunk_size is not None
++            and len(data) > self._max_sync_chunk_size
++        ):
++            return await asyncio.get_event_loop().run_in_executor(
++                self._executor, self.decompress_sync, data, max_length
++            )
++        return self.decompress_sync(data, max_length)
++
+ 
+-class ZLibCompressor(ZlibBaseHandler):
++class ZLibCompressor:
+     def __init__(
+         self,
+         encoding: Optional[str] = None,
+@@ -49,12 +78,12 @@ class ZLibCompressor(ZlibBaseHandler):
+         executor: Optional[Executor] = None,
+         max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE,
+     ):
+-        super().__init__(
+-            mode=encoding_to_mode(encoding, suppress_deflate_header)
++        self._executor = executor
++        self._max_sync_chunk_size = max_sync_chunk_size
++        self._mode = (
++            encoding_to_mode(encoding, suppress_deflate_header)
+             if wbits is None
+-            else wbits,
+-            executor=executor,
+-            max_sync_chunk_size=max_sync_chunk_size,
++            else wbits
+         )
+         if level is None:
+             self._compressor = zlib.compressobj(wbits=self._mode, strategy=strategy)
+@@ -86,7 +115,7 @@ class ZLibCompressor(ZlibBaseHandler):
+         return self._compressor.flush(mode)
+ 
+ 
+-class ZLibDecompressor(ZlibBaseHandler):
++class ZLibDecompressor(DecompressionBaseHandler):
+     def __init__(
+         self,
+         encoding: Optional[str] = None,
+@@ -94,26 +123,15 @@ class ZLibDecompressor(ZlibBaseHandler):
+         executor: Optional[Executor] = None,
+         max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE,
+     ):
+-        super().__init__(
+-            mode=encoding_to_mode(encoding, suppress_deflate_header),
+-            executor=executor,
+-            max_sync_chunk_size=max_sync_chunk_size,
+-        )
++        super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size)
++        self._mode = encoding_to_mode(encoding, suppress_deflate_header)
+         self._decompressor = zlib.decompressobj(wbits=self._mode)
+ 
+-    def decompress_sync(self, data: bytes, max_length: int = 0) -> bytes:
++    def decompress_sync(
++        self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
++    ) -> bytes:
+         return self._decompressor.decompress(data, max_length)
+ 
+-    async def decompress(self, data: bytes, max_length: int = 0) -> bytes:
+-        if (
+-            self._max_sync_chunk_size is not None
+-            and len(data) > self._max_sync_chunk_size
+-        ):
+-            return await asyncio.get_event_loop().run_in_executor(
+-                self._executor, self.decompress_sync, data, max_length
+-            )
+-        return self.decompress_sync(data, max_length)
+-
+     def flush(self, length: int = 0) -> bytes:
+         return (
+             self._decompressor.flush(length)
+@@ -134,24 +152,34 @@ class ZLibDecompressor(ZlibBaseHandler):
+         return self._decompressor.unused_data
+ 
+ 
+-class BrotliDecompressor:
++class BrotliDecompressor(DecompressionBaseHandler):
+     # Supports both 'brotlipy' and 'Brotli' packages
+     # since they share an import name. The top branches
+     # are for 'brotlipy' and bottom branches for 'Brotli'
+-    def __init__(self) -> None:
++    def __init__(
++        self,
++        executor: Optional[Executor] = None,
++        max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE,
++    ) -> None:
++        """Decompress data using the Brotli library."""
+         if not HAS_BROTLI:
+             raise RuntimeError(
+                 "The brotli decompression is not available. "
+                 "Please install `Brotli` module"
+             )
+         self._obj = brotli.Decompressor()
++        super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size)
+ 
+-    def decompress_sync(self, data: bytes) -> bytes:
++    def decompress_sync(
++        self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
++    ) -> bytes:
++        """Decompress the given data."""
+         if hasattr(self._obj, "decompress"):
+-            return cast(bytes, self._obj.decompress(data))
+-        return cast(bytes, self._obj.process(data))
++            return cast(bytes, self._obj.decompress(data, max_length))
++        return cast(bytes, self._obj.process(data, max_length))
+ 
+     def flush(self) -> bytes:
++        """Flush the decompressor."""
+         if hasattr(self._obj, "flush"):
+             return cast(bytes, self._obj.flush())
+         return b""
+diff --git a/aiohttp/http_exceptions.py b/aiohttp/http_exceptions.py
+index 72eac3a3c..877b07d4c 100644
+--- a/aiohttp/http_exceptions.py
++++ b/aiohttp/http_exceptions.py
+@@ -75,6 +75,10 @@ class ContentLengthError(PayloadEncodingError):
+     """Not enough data for satisfy content length header."""
+ 
+ 
++class DecompressSizeError(PayloadEncodingError):
++    """Decompressed size exceeds the configured limit."""
++
++
+ class LineTooLong(BadHttpMessage):
+     def __init__(
+         self, line: str, limit: str = "Unknown", actual_size: str = "Unknown"
+diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py
+index 5768bd623..cdf3fc89a 100644
+--- a/aiohttp/http_parser.py
++++ b/aiohttp/http_parser.py
+@@ -26,7 +26,12 @@ from yarl import URL
+ 
+ from . import hdrs
+ from .base_protocol import BaseProtocol
+-from .compression_utils import HAS_BROTLI, BrotliDecompressor, ZLibDecompressor
++from .compression_utils import (
++    DEFAULT_MAX_DECOMPRESS_SIZE,
++    HAS_BROTLI,
++    BrotliDecompressor,
++    ZLibDecompressor,
++)
+ from .helpers import (
+     _EXC_SENTINEL,
+     DEBUG,
+@@ -41,6 +46,7 @@ from .http_exceptions import (
+     BadStatusLine,
+     ContentEncodingError,
+     ContentLengthError,
++    DecompressSizeError,
+     InvalidHeader,
+     InvalidURLError,
+     LineTooLong,
+@@ -959,7 +965,12 @@ class DeflateBuffer:
+ 
+     decompressor: Any
+ 
+-    def __init__(self, out: StreamReader, encoding: Optional[str]) -> None:
++    def __init__(
++        self,
++        out: StreamReader,
++        encoding: Optional[str],
++        max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
++    ) -> None:
+         self.out = out
+         self.size = 0
+         self.encoding = encoding
+@@ -976,6 +987,8 @@ class DeflateBuffer:
+         else:
+             self.decompressor = ZLibDecompressor(encoding=encoding)
+ 
++        self._max_decompress_size = max_decompress_size
++
+     def set_exception(
+         self,
+         exc: BaseException,
+@@ -1004,7 +1017,10 @@ class DeflateBuffer:
+             )
+ 
+         try:
+-            chunk = self.decompressor.decompress_sync(chunk)
++            # Decompress with limit + 1 so we can detect if output exceeds limit
++            chunk = self.decompressor.decompress_sync(
++                chunk, max_length=self._max_decompress_size + 1
++            )
+         except Exception:
+             raise ContentEncodingError(
+                 "Can not decode content-encoding: %s" % self.encoding
+@@ -1012,6 +1028,13 @@ class DeflateBuffer:
+ 
+         self._started_decoding = True
+ 
++        # Check if decompression limit was exceeded
++        if len(chunk) > self._max_decompress_size:
++            raise DecompressSizeError(
++                "Decompressed data exceeds the configured limit of %d bytes"
++                % self._max_decompress_size
++            )
++
+         if chunk:
+             self.out.feed_data(chunk, len(chunk))
+ 
+diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py
+index 9e5ff9b41..baf07fd16 100644
+--- a/aiohttp/multipart.py
++++ b/aiohttp/multipart.py
+@@ -27,7 +27,12 @@ from urllib.parse import parse_qsl, unquote, urlencode
+ 
+ from multidict import CIMultiDict, CIMultiDictProxy
+ 
+-from .compression_utils import ZLibCompressor, ZLibDecompressor
++from .abc import AbstractStreamWriter
++from .compression_utils import (
++    DEFAULT_MAX_DECOMPRESS_SIZE,
++    ZLibCompressor,
++    ZLibDecompressor,
++)
+ from .hdrs import (
+     CONTENT_DISPOSITION,
+     CONTENT_ENCODING,
+@@ -263,6 +268,7 @@ class BodyPartReader:
+         *,
+         subtype: str = "mixed",
+         default_charset: Optional[str] = None,
++        max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
+     ) -> None:
+         self.headers = headers
+         self._boundary = boundary
+@@ -278,6 +284,7 @@ class BodyPartReader:
+         self._prev_chunk: Optional[bytes] = None
+         self._content_eof = 0
+         self._cache: Dict[str, Any] = {}
++        self._max_decompress_size = max_decompress_size
+ 
+     def __aiter__(self) -> AsyncIterator["BodyPartReader"]:
+         return self  # type: ignore[return-value]
+@@ -471,7 +478,7 @@ class BodyPartReader:
+             return ZLibDecompressor(
+                 encoding=encoding,
+                 suppress_deflate_header=True,
+-            ).decompress_sync(data)
++            ).decompress_sync(data, max_length=self._max_decompress_size)
+ 
+         raise RuntimeError(f"unknown content encoding: {encoding}")
+ 
+@@ -528,7 +535,7 @@ class BodyPartReaderPayload(Payload):
+         if params:
+             self.set_content_disposition("attachment", True, **params)
+ 
+-    async def write(self, writer: Any) -> None:
++    async def write(self, writer: AbstractStreamWriter) -> None:
+         field = self._value
+         chunk = await field.read_chunk(size=2**16)
+         while chunk:
+@@ -927,7 +934,9 @@ class MultipartWriter(Payload):
+         total += 2 + len(self._boundary) + 4  # b'--'+self._boundary+b'--\r\n'
+         return total
+ 
+-    async def write(self, writer: Any, close_boundary: bool = True) -> None:
++    async def write(
++        self, writer: AbstractStreamWriter, close_boundary: bool = True
++    ) -> None:
+         """Write body."""
+         for part, encoding, te_encoding in self._parts:
+             if self._is_form_data:
+@@ -956,7 +965,7 @@ class MultipartWriter(Payload):
+ 
+ 
+ class MultipartPayloadWriter:
+-    def __init__(self, writer: Any) -> None:
++    def __init__(self, writer: AbstractStreamWriter) -> None:
+         self._writer = writer
+         self._encoding: Optional[str] = None
+         self._compress: Optional[ZLibCompressor] = None
+diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
+index 514477e8f..34399e6ba 100644
+--- a/docs/spelling_wordlist.txt
++++ b/docs/spelling_wordlist.txt
+@@ -182,6 +182,7 @@ lowercased
+ Mako
+ manylinux
+ metadata
++MiB
+ microservice
+ middleware
+ middlewares
+diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
+index dbb2dff5a..7d126d185 100644
+--- a/tests/test_client_functional.py
++++ b/tests/test_client_functional.py
+@@ -8,9 +8,18 @@ import json
+ import pathlib
+ import socket
+ import ssl
++import zlib
+ from typing import Any, AsyncIterator
+ from unittest import mock
+ 
++try:
++    try:
++        import brotlicffi as brotli
++    except ImportError:
++        import brotli
++except ImportError:
++    brotli = None  # pragma: no cover
++
+ import pytest
+ from multidict import MultiDict
+ from yarl import URL
+@@ -19,6 +28,8 @@ import aiohttp
+ from aiohttp import Fingerprint, ServerFingerprintMismatch, hdrs, web
+ from aiohttp.abc import AbstractResolver
+ from aiohttp.client_exceptions import TooManyRedirects
++from aiohttp.compression_utils import DEFAULT_MAX_DECOMPRESS_SIZE, HAS_BROTLI
++from aiohttp.http_exceptions import DecompressSizeError
+ from aiohttp.pytest_plugin import AiohttpClient, TestClient
+ from aiohttp.test_utils import unused_port
+ 
+@@ -1903,8 +1914,73 @@ async def test_bad_payload_compression(aiohttp_client) -> None:
+     resp.close()
+ 
+ 
+-async def test_bad_payload_chunked_encoding(aiohttp_client) -> None:
+-    async def handler(request):
++async def test_payload_decompress_size_limit(aiohttp_client: AiohttpClient) -> None:
++    """Test that decompression size limit triggers DecompressSizeError.
++
++    When a compressed payload expands beyond the configured limit,
++    we raise DecompressSizeError.
++    """
++    # Create a highly compressible payload that exceeds the decompression limit.
++    # 64MiB of repeated bytes compresses to ~32KB but expands beyond the
++    # 32MiB per-call limit.
++    original = b"A" * (64 * 2**20)
++    compressed = zlib.compress(original)
++    assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE
++
++    async def handler(request: web.Request) -> web.Response:
++        # Send compressed data with Content-Encoding header
++        resp = web.Response(body=compressed)
++        resp.headers["Content-Encoding"] = "deflate"
++        return resp
++
++    app = web.Application()
++    app.router.add_get("/", handler)
++    client = await aiohttp_client(app)
++
++    async with client.get("/") as resp:
++        assert resp.status == 200
++
++        with pytest.raises(aiohttp.ClientPayloadError) as exc_info:
++            await resp.read()
++
++        assert isinstance(exc_info.value.__cause__, DecompressSizeError)
++        assert "Decompressed data exceeds" in str(exc_info.value.__cause__)
++
++
++@pytest.mark.skipif(
++    brotli is None or not HAS_BROTLI, reason="brotli decoding is unavailable"
++)
++async def test_payload_decompress_size_limit_brotli(
++    aiohttp_client: AiohttpClient,
++) -> None:
++    """Test that brotli decompression size limit triggers DecompressSizeError."""
++    assert brotli is not None
++    # Create a highly compressible payload that exceeds the decompression limit.
++    original = b"A" * (64 * 2**20)
++    compressed = brotli.compress(original)
++    assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE
++
++    async def handler(request: web.Request) -> web.Response:
++        resp = web.Response(body=compressed)
++        resp.headers["Content-Encoding"] = "br"
++        return resp
++
++    app = web.Application()
++    app.router.add_get("/", handler)
++    client = await aiohttp_client(app)
++
++    async with client.get("/") as resp:
++        assert resp.status == 200
++
++        with pytest.raises(aiohttp.ClientPayloadError) as exc_info:
++            await resp.read()
++
++        assert isinstance(exc_info.value.__cause__, DecompressSizeError)
++        assert "Decompressed data exceeds" in str(exc_info.value.__cause__)
++
++
++async def test_bad_payload_chunked_encoding(aiohttp_client: AiohttpClient) -> None:
++    async def handler(request: web.Request) -> web.StreamResponse:
+         resp = web.StreamResponse()
+         resp.force_close()
+         resp._length_check = False
+diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
+index 0fcefdefd..9449c4061 100644
+--- a/tests/test_http_parser.py
++++ b/tests/test_http_parser.py
+@@ -2,6 +2,7 @@
+ 
+ import asyncio
+ import re
++import zlib
+ from contextlib import nullcontext
+ from typing import Any, Dict, List
+ from unittest import mock
+@@ -14,6 +15,7 @@ from yarl import URL
+ import aiohttp
+ from aiohttp import http_exceptions, streams
+ from aiohttp.base_protocol import BaseProtocol
++from aiohttp.compression_utils import HAS_BROTLI
+ from aiohttp.http_parser import (
+     NO_EXTENSIONS,
+     DeflateBuffer,
+@@ -561,7 +563,9 @@ def test_compression_gzip(parser) -> None:
+     assert msg.compression == "gzip"
+ 
+ 
+-@pytest.mark.skipif(brotli is None, reason="brotli is not installed")
++@pytest.mark.skipif(
++    brotli is None or not HAS_BROTLI, reason="brotli decoding is unavailable"
++)
+ def test_compression_brotli(parser) -> None:
+     text = b"GET /test HTTP/1.1\r\n" b"content-encoding: br\r\n\r\n"
+     messages, upgrade, tail = parser.feed_data(text)
+@@ -1736,7 +1740,9 @@ class TestParsePayload:
+         assert p.done
+         assert out.is_eof()
+ 
+-    @pytest.mark.skipif(brotli is None, reason="brotli is not installed")
++    @pytest.mark.skipif(
++        brotli is None or not HAS_BROTLI, reason="brotli decoding is unavailable"
++    )
+     async def test_http_payload_brotli(self, stream) -> None:
+         compressed = brotli.compress(b"brotli data")
+         out = aiohttp.FlowControlDataQueue(
+@@ -1816,6 +1822,7 @@ class TestDeflateBuffer:
+         dbuf.feed_eof()
+         assert [b"line"] == list(d for d, _ in buf._buffer)
+ 
++    @pytest.mark.skipif(not HAS_BROTLI, reason="brotli decoding is unavailable")
+     async def test_feed_eof_no_err_brotli(self, stream) -> None:
+         buf = aiohttp.FlowControlDataQueue(
+             stream, 2**16, loop=asyncio.get_event_loop()
+@@ -1837,3 +1844,36 @@ class TestDeflateBuffer:
+         dbuf.feed_eof()
+ 
+         assert buf.at_eof()
++
++    @pytest.mark.parametrize(
++        "chunk_size",
++        [1024, 2**14, 2**16],  # 1KB, 16KB, 64KB
++        ids=["1KB", "16KB", "64KB"],
++    )
++    async def test_streaming_decompress_large_payload(
++        self, protocol: BaseProtocol, chunk_size: int
++    ) -> None:
++        """Test that large payloads decompress correctly when streamed in chunks.
++
++        This simulates real HTTP streaming where compressed data arrives in
++        small network chunks. Each chunk's decompressed output should be within
++        the max_decompress_size limit, allowing full recovery of the original data.
++        """
++        # Create a large payload (3MiB) that compresses well
++        original = b"A" * (3 * 2**20)
++        compressed = zlib.compress(original)
++
++        buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
++        dbuf = DeflateBuffer(buf, "deflate")
++
++        # Feed compressed data in chunks (simulating network streaming)
++        for i in range(0, len(compressed), chunk_size):
++            chunk = compressed[i : i + chunk_size]
++            dbuf.feed_data(chunk, len(chunk))
++
++        dbuf.feed_eof()
++
++        # Read all decompressed data
++        result = b"".join(buf._buffer)
++        assert len(result) == len(original)
++        assert result == original
+diff --git a/tests/test_multipart.py b/tests/test_multipart.py
+index e4a2be1f3..553085ca5 100644
+--- a/tests/test_multipart.py
++++ b/tests/test_multipart.py
+@@ -9,6 +9,7 @@ import pytest
+ 
+ import aiohttp
+ from aiohttp import payload
++from aiohttp.abc import AbstractStreamWriter
+ from aiohttp.hdrs import (
+     CONTENT_DISPOSITION,
+     CONTENT_ENCODING,
+@@ -32,14 +33,14 @@ def buf():
+ 
+ 
+ @pytest.fixture
+-def stream(buf):
+-    writer = mock.Mock()
++def stream(buf: bytearray) -> AbstractStreamWriter:
++    writer = mock.create_autospec(AbstractStreamWriter, instance=True, spec_set=True)
+ 
+     async def write(chunk):
+         buf.extend(chunk)
+ 
+     writer.write.side_effect = write
+-    return writer
++    return writer  # type: ignore[no-any-return]
+ 
+ 
+ @pytest.fixture
+@@ -336,6 +337,17 @@ class TestPartReader:
+             result = await obj.read(decode=True)
+         assert b"Time to Relax!" == result
+ 
++    def test_decode_remains_synchronous(self) -> None:
++        data = b"\x0b\xc9\xccMU(\xc9W\x08J\xcdI\xacP\x04\x00"
++        with Stream(b"") as stream:
++            obj = aiohttp.BodyPartReader(
++                BOUNDARY,
++                {CONTENT_ENCODING: "deflate"},
++                stream,
++            )
++            result = obj.decode(data)
++        assert b"Time to Relax!" == result
++
+     async def test_read_with_content_encoding_identity(self) -> None:
+         thing = (
+             b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x0b\xc9\xccMU"
+@@ -1012,7 +1024,9 @@ async def test_writer(writer) -> None:
+     assert writer.boundary == ":"
+ 
+ 
+-async def test_writer_serialize_io_chunk(buf, stream, writer) -> None:
++async def test_writer_serialize_io_chunk(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     with io.BytesIO(b"foobarbaz") as file_handle:
+         writer.append(file_handle)
+         await writer.write(stream)
+@@ -1022,7 +1036,9 @@ async def test_writer_serialize_io_chunk(buf, stream, writer) -> None:
+     )
+ 
+ 
+-async def test_writer_serialize_json(buf, stream, writer) -> None:
++async def test_writer_serialize_json(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     writer.append_json({"привет": "мир"})
+     await writer.write(stream)
+     assert (
+@@ -1031,7 +1047,9 @@ async def test_writer_serialize_json(buf, stream, writer) -> None:
+     )
+ 
+ 
+-async def test_writer_serialize_form(buf, stream, writer) -> None:
++async def test_writer_serialize_form(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     data = [("foo", "bar"), ("foo", "baz"), ("boo", "zoo")]
+     writer.append_form(data)
+     await writer.write(stream)
+@@ -1039,7 +1057,9 @@ async def test_writer_serialize_form(buf, stream, writer) -> None:
+     assert b"foo=bar&foo=baz&boo=zoo" in buf
+ 
+ 
+-async def test_writer_serialize_form_dict(buf, stream, writer) -> None:
++async def test_writer_serialize_form_dict(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     data = {"hello": "мир"}
+     writer.append_form(data)
+     await writer.write(stream)
+@@ -1047,7 +1067,9 @@ async def test_writer_serialize_form_dict(buf, stream, writer) -> None:
+     assert b"hello=%D0%BC%D0%B8%D1%80" in buf
+ 
+ 
+-async def test_writer_write(buf, stream, writer) -> None:
++async def test_writer_write(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     writer.append("foo-bar-baz")
+     writer.append_json({"test": "passed"})
+     writer.append_form({"test": "passed"})
+@@ -1093,7 +1115,9 @@ async def test_writer_write(buf, stream, writer) -> None:
+     ) == bytes(buf)
+ 
+ 
+-async def test_writer_write_no_close_boundary(buf, stream) -> None:
++async def test_writer_write_no_close_boundary(
++    buf: bytearray, stream: AbstractStreamWriter
++) -> None:
+     writer = aiohttp.MultipartWriter(boundary=":")
+     writer.append("foo-bar-baz")
+     writer.append_json({"test": "passed"})
+@@ -1125,12 +1149,18 @@ async def test_writer_write_no_close_boundary(buf, stream) -> None:
+     ) == bytes(buf)
+ 
+ 
+-async def test_writer_write_no_parts(buf, stream, writer) -> None:
++async def test_writer_write_no_parts(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     await writer.write(stream)
+     assert b"--:--\r\n" == bytes(buf)
+ 
+ 
+-async def test_writer_serialize_with_content_encoding_gzip(buf, stream, writer):
++async def test_writer_serialize_with_content_encoding_gzip(
++    buf: bytearray,
++    stream: AbstractStreamWriter,
++    writer: aiohttp.MultipartWriter,
++) -> None:
+     writer.append("Time to Relax!", {CONTENT_ENCODING: "gzip"})
+     await writer.write(stream)
+     headers, message = bytes(buf).split(b"\r\n\r\n", 1)
+@@ -1146,7 +1176,9 @@ async def test_writer_serialize_with_content_encoding_gzip(buf, stream, writer):
+     assert b"Time to Relax!" == data
+ 
+ 
+-async def test_writer_serialize_with_content_encoding_deflate(buf, stream, writer):
++async def test_writer_serialize_with_content_encoding_deflate(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     writer.append("Time to Relax!", {CONTENT_ENCODING: "deflate"})
+     await writer.write(stream)
+     headers, message = bytes(buf).split(b"\r\n\r\n", 1)
+@@ -1160,7 +1192,9 @@ async def test_writer_serialize_with_content_encoding_deflate(buf, stream, write
+     assert thing == message
+ 
+ 
+-async def test_writer_serialize_with_content_encoding_identity(buf, stream, writer):
++async def test_writer_serialize_with_content_encoding_identity(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     thing = b"\x0b\xc9\xccMU(\xc9W\x08J\xcdI\xacP\x04\x00"
+     writer.append(thing, {CONTENT_ENCODING: "identity"})
+     await writer.write(stream)
+@@ -1175,12 +1209,16 @@ async def test_writer_serialize_with_content_encoding_identity(buf, stream, writ
+     assert thing == message.split(b"\r\n")[0]
+ 
+ 
+-def test_writer_serialize_with_content_encoding_unknown(buf, stream, writer):
++def test_writer_serialize_with_content_encoding_unknown(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     with pytest.raises(RuntimeError):
+         writer.append("Time to Relax!", {CONTENT_ENCODING: "snappy"})
+ 
+ 
+-async def test_writer_with_content_transfer_encoding_base64(buf, stream, writer):
++async def test_writer_with_content_transfer_encoding_base64(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     writer.append("Time to Relax!", {CONTENT_TRANSFER_ENCODING: "base64"})
+     await writer.write(stream)
+     headers, message = bytes(buf).split(b"\r\n\r\n", 1)
+@@ -1193,7 +1231,9 @@ async def test_writer_with_content_transfer_encoding_base64(buf, stream, writer)
+     assert b"VGltZSB0byBSZWxheCE=" == message.split(b"\r\n")[0]
+ 
+ 
+-async def test_writer_content_transfer_encoding_quote_printable(buf, stream, writer):
++async def test_writer_content_transfer_encoding_quote_printable(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     writer.append("Привет, мир!", {CONTENT_TRANSFER_ENCODING: "quoted-printable"})
+     await writer.write(stream)
+     headers, message = bytes(buf).split(b"\r\n\r\n", 1)
+@@ -1209,7 +1249,9 @@ async def test_writer_content_transfer_encoding_quote_printable(buf, stream, wri
+     )
+ 
+ 
+-def test_writer_content_transfer_encoding_unknown(buf, stream, writer) -> None:
++def test_writer_content_transfer_encoding_unknown(
++    buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter
++) -> None:
+     with pytest.raises(RuntimeError):
+         writer.append("Time to Relax!", {CONTENT_TRANSFER_ENCODING: "unknown"})
+ 
+@@ -1333,7 +1375,9 @@ class TestMultipartWriter:
+             with aiohttp.MultipartWriter(boundary=":") as writer:
+                 writer.append(None)
+ 
+-    async def test_write_preserves_content_disposition(self, buf, stream) -> None:
++    async def test_write_preserves_content_disposition(
++        self, buf: bytearray, stream: AbstractStreamWriter
++    ) -> None:
+         with aiohttp.MultipartWriter(boundary=":") as writer:
+             part = writer.append(b"foo", headers={CONTENT_TYPE: "test/passed"})
+             part.set_content_disposition("form-data", filename="bug")
+@@ -1350,7 +1394,9 @@ class TestMultipartWriter:
+         )
+         assert message == b"foo\r\n--:--\r\n"
+ 
+-    async def test_preserve_content_disposition_header(self, buf, stream):
++    async def test_preserve_content_disposition_header(
++        self, buf: bytearray, stream: AbstractStreamWriter
++    ) -> None:
+         # https://github.com/aio-libs/aiohttp/pull/3475#issuecomment-451072381
+         with pathlib.Path(__file__).open("rb") as fobj:
+             with aiohttp.MultipartWriter("form-data", boundary=":") as writer:
+@@ -1374,7 +1420,9 @@ class TestMultipartWriter:
+             b'Content-Disposition: attachments; filename="bug.py"'
+         )
+ 
+-    async def test_set_content_disposition_override(self, buf, stream):
++    async def test_set_content_disposition_override(
++        self, buf: bytearray, stream: AbstractStreamWriter
++    ) -> None:
+         # https://github.com/aio-libs/aiohttp/pull/3475#issuecomment-451072381
+         with pathlib.Path(__file__).open("rb") as fobj:
+             with aiohttp.MultipartWriter("form-data", boundary=":") as writer:
+@@ -1398,7 +1446,9 @@ class TestMultipartWriter:
+             b'Content-Disposition: attachments; filename="bug.py"'
+         )
+ 
+-    async def test_reset_content_disposition_header(self, buf, stream):
++    async def test_reset_content_disposition_header(
++        self, buf: bytearray, stream: AbstractStreamWriter
++    ) -> None:
+         # https://github.com/aio-libs/aiohttp/pull/3475#issuecomment-451072381
+         with pathlib.Path(__file__).open("rb") as fobj:
+             with aiohttp.MultipartWriter("form-data", boundary=":") as writer:
+-- 
+2.35.6
+
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb
index 69cad8cb44..feb9039ae1 100644
--- a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb
+++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb
@@ -15,6 +15,7 @@ SRC_URI += "file://CVE-2024-52304.patch \
            file://CVE-2025-69229_p1.patch \
            file://CVE-2025-69229_p2.patch \
            file://CVE-2025-69227.patch \
+           file://CVE-2025-69223.patch \
            "
 
 PYPI_PACKAGE = "aiohttp"
