From patchwork Mon Sep 28 17:48:06 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 99505 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 6782DCA5FA1 for ; Mon, 28 Sep 2026 17:48:25 +0000 (UTC) Received: from rcdn-iport-9.cisco.com (rcdn-iport-9.cisco.com [173.37.86.80]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63697.1790617699509903270 for ; Mon, 28 Sep 2026 10:48:19 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=GB/r4sHt; spf=pass (domain: cisco.com, ip: 173.37.86.80, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=37093; q=dns/txt; s=iport01; t=1790617699; x=1791827299; h=from:to:cc:subject:date:message-id:mime-version: content-transfer-encoding; bh=Tr5Jp/Br11oWrlsrjcHHfA57BLuaOz3BY3dG4uydbEg=; b=GB/r4sHtQywNrV9AfjWnA9K/8pdWnKmqLjCFGu69EEiHNmn3udoOtLAT oGFhPMKil+1bhZvbHYcz8nnm4wkxHyguVp09Q9/OPuzRKUw0QXCSM1bR8 XWAeAra1OxC64jUek6zKlT0z+JmG/Y56Ix4YrGSVqRV56WeNZ6CEWeFOP 1UEMl6lRMbSLHCmguHxE0nf2BNJaJlW3Xamv5ta19MYoU5zL7wzlDBF6k 8yzDNPcFk8j2TQph/FIRv2X3veLDgfKgqKujgnIjgUcbVxbwqr9EnX1f9 Shlb6l8o2qPqdDqw7p2YDJghtU4MyY1tU1jX3Su/WF18/ZroxALHnzkLL Q==; X-CSE-ConnectionGUID: GXKe2OIVQzKzG1RJnZPgSw== X-CSE-MsgGUID: h7JxbuyTQmW+wZbCNLbcDg== X-IPAS-Result: A0B9AgD4p7pq/43/Ja1aglmCGD91YBopSZZKi2eFZYxRgX4PAQEBD0QNBAEBhD9GAo4JAiY0CQ4BAgQDAgMBAQEBAQEBAQEBAQEKAQEFAQEBAgEHBYEOE4ZPDZASAQIBHQEMCwEYAS0sAwECTwsjIYIqWAGCOgM3AxHCSYF5M4EBgykBgVTYSw2CWgELFAGBOIVAgn+FJF0YAYNrgREnGxuBcoEVgTuCLoEFgRpCAoEnERAJhlQEgiKBDIFaHmWSQkiBHgNZLAFVEw0KCwcFgWYDNRIqFW4yHYEjPheBBxsGBYEdgSCCGSMZNnqBCV6BKylgARAXgQeCBwKCVIIBAgFJQw4HRVMJJUEKEkcmIggSCQETGjALgSE4QQkoFBgNSBEsNxUZBD5uB5AvHoJTCwcWGzAtAQcMFwEEUikGGxMyCBc5BQYCMJJDJQEBQpF8oB5xCiiDdowijz6FfBozhASBV5JAklQLmH2CWYsxhAmRXwsNUIRpgWg8gVlwFTuCZwlKGQ+OKwMLC4NgzDknMgIJMgEBBwIHDgMLgWiEY4sdAQEmgVYBAQ IronPort-Data: A9a23:2whQzKKa64mNd5dgFE+RgZQlxSXFcZb7ZxGr2PjKsXjdYENS0zYAy DEfUTjUOP/cMWHzKox3YNuyphsF78OHmtAxHgMd+CA2RRqmiyZq6fd1j6vUF3nPRiEWZBs/t 63yUvGZcoZpCCea+Uf1WlTYhSEU/bmSQbbhA/LzNCl0RAt1IA8skhsLd9QR2uaEuvDnRVnS0 T/Oi5eHYgH9imQkajt8B5+r8XuDgtyj4Fv0gXRmDRx7lAe2v2UYCpsZOZawIxPQKqFIHvS3T vr017qw+GXU5X8FUrtJRZ6iLyXm6paLVeS/oiI+t5qK23CulQRuukoPD8fwXG8M49m/c3+d/ /0W3XC4YV9B0qQhA43xWTEAe811FfUuFLMqvRFTvOTLp3AqfUcAzN1BPGI7Z6o5xN9lInNhr MY6LgI1YSuc0rfeLLKTEoGAh+w5J8XteYdasXZ6wHSBUbAtQIvIROPB4towMDUY358VW62BI ZBENHw2ME2ojx5nYj/7DLoyn+qsj3juehVTqUmeouw85G27IAlZjeKyboaKIozTLSlTtkbHh HLM4nXFOxQld8OtwxGj1ninquCayEsXX6pXTtVU7MVCh0WewGEWAhAaWVa35PW0lEO6c9ZeM FAPvC02oK4/8UamQtXwU1u/unHsg/IHc8BbH+t/7ESGzbDZpl7FQGMFVTVGLtchsafaWAAX6 7NApPuxbRQHjVFfYSj1Gmu8xd9qBRUoEA== IronPort-HdrOrdr: A9a23:TUzE76iggC+ONt0L6eX3nMR153BQXuwji2hC6mlwRA09TyVXra +TdZMgpHrJYVkqOU3I9ersBEDiewK/yXcK2+ks1N6ZNWGM0ldAR7sN0WKN+VHd8lXFh41g/J YlVbRiA9vtClU/p8P77A6kV+sE+rC8gcSVbSO09QYKcemsAJsQiDtENg== X-Talos-CUID: 9a23:PCcIG2GSJHFB1jKDqmJIxG0ZQOcETETP72X/enWaD1RRR4aKHAo= X-Talos-MUID: 9a23:m2EYVAkeYTHRqAuuub/ddnpia+V46bm/NHkIrtYZicOvEwh6MBuS2WE= X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="527647081" Received: from rcdn-l-core-04.cisco.com ([173.37.255.141]) by rcdn-iport-9.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:48:18 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by rcdn-l-core-04.cisco.com (Postfix) with ESMTPS id 01770180004AC; Mon, 28 Sep 2026 17:48:18 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 9D9BECC1611; Mon, 28 Sep 2026 10:48:17 -0700 (PDT) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][wrynose][PATCH] python3-aiohttp: fix CVE-2026-54273 Date: Mon, 28 Sep 2026 10:48:06 -0700 Message-Id: <20260928174806.1818191-1-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: rcdn-l-core-04.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 28 Sep 2026 17:48:25 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130460 From: Darsh Kelaiya This patch applies the upstream 3.14.1 backport for CVE-2026-54273. The upstream fix commit is referenced in [1], its required follow-up commit is referenced in [2], and the public advisory is referenced in [3]. The generated aiohttp/_http_parser.c changes are omitted. The recipe-time Cython regeneration introduced in this patch regenerates that file from the patched _http_parser.pyx before the accelerated parser is compiled. [1] https://github.com/aio-libs/aiohttp/commit/dfdfa9d5aad5d21f91c79fb2ceeba0f8046cb6cf [2] https://github.com/aio-libs/aiohttp/commit/47babd8c23ca79e2bae6cc8dcb5752b61e369fc7 [3] https://github.com/advisories/GHSA-4fvr-rgm6-gqmc Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-54273_p1.patch | 773 ++++++++++++++++++ .../python3-aiohttp/CVE-2026-54273_p2.patch | 177 ++++ .../python/python3-aiohttp_3.13.5.bb | 11 +- 3 files changed, 960 insertions(+), 1 deletion(-) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p1.patch create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p2.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p1.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p1.patch new file mode 100644 index 0000000000..f5a8da015d --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p1.patch @@ -0,0 +1,773 @@ +From 4f6228cdad5a097880dbf1314e1c56773dbeb1eb Mon Sep 17 00:00:00 2001 +From: "J. Nick Koston" +Date: Sun, 7 Jun 2026 01:16:13 -0500 +Subject: [PATCH] [PR #12830/93a2b1c3 backport][3.14] Bound pipelined request + queue per connection (#12854) + +CVE: CVE-2026-54273 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/dfdfa9d5aad5d21f91c79fb2ceeba0f8046cb6cf] + +Backport Changes: +- Added llhttp pause resumption and tail buffering because the 3.13.5 + parser lacks the newer 3.14 pause state. +- Adapted protocol fields to the 3.13.5 names _request_parser and + _upgrade, and used the existing 2**16 read buffer. +- Coordinated the older BaseProtocol read-pause state with the + message-queue pause and adapted the related tests. +- Added the missing 3.13.5 test imports and fixtures. +- Omitted generated aiohttp/_http_parser.c because the Wrynose recipe + regenerates it from _http_parser.pyx during do_configure. +- Added llhttp_resume to _cparser.pxd for the generated parser. + +(cherry picked from commit dfdfa9d5aad5d21f91c79fb2ceeba0f8046cb6cf) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12830.bugfix.rst | 1 + + aiohttp/_cparser.pxd | 1 + + aiohttp/_http_parser.pyx | 45 +++++++++++++- + aiohttp/base_protocol.py | 25 ++++++-- + aiohttp/http_parser.py | 20 ++++++ + aiohttp/web_protocol.py | 66 +++++++++++++++++++++ + docs/spelling_wordlist.txt | 1 + + tests/test_http_parser.py | 72 +++++++++++++++++++++++ + tests/test_web_functional.py | 132 ++++++++++++++++++++++++++++++++++++++++++- + tests/test_web_protocol.py | 112 ++++++++++++++++++++++++++++++++++++ + 10 files changed, 464 insertions(+), 11 deletions(-) + create mode 100644 CHANGES/12830.bugfix.rst + create mode 100644 tests/test_web_protocol.py + +diff --git a/CHANGES/12830.bugfix.rst b/CHANGES/12830.bugfix.rst +new file mode 100644 +index 000000000..d44d76da4 +--- /dev/null ++++ b/CHANGES/12830.bugfix.rst +@@ -0,0 +1 @@ ++Bounded the number of parsed-but-unhandled pipelined HTTP/1 requests buffered per connection on the server; once the queue reaches an internal limit the parser stops emitting and the transport is paused, resuming as the request handler drains the queue, so a client keeping one handler busy can no longer accumulate an unbounded backlog of pipelined requests -- by :user:`bdraco`. +diff --git a/aiohttp/_cparser.pxd b/aiohttp/_cparser.pxd +index 1b3be6d4e..cc7ef58d6 100644 +--- a/aiohttp/_cparser.pxd ++++ b/aiohttp/_cparser.pxd +@@ -145,6 +145,7 @@ cdef extern from "llhttp.h": + + int llhttp_should_keep_alive(const llhttp_t* parser) + ++ void llhttp_resume(llhttp_t* parser) + void llhttp_resume_after_upgrade(llhttp_t* parser) + + llhttp_errno_t llhttp_get_errno(const llhttp_t* parser) +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index e1edee310..0d0762748 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -319,6 +319,9 @@ cdef class HttpParser: + list _raw_headers + bint _upgraded + list _messages ++ Py_ssize_t _msg_in_flight ++ Py_ssize_t _max_msg_queue_size ++ bytes _tail + object _payload + bint _payload_error + object _payload_exception +@@ -353,6 +356,7 @@ cdef class HttpParser: + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True, ++ Py_ssize_t max_msg_queue_size=0, + ): + cparser.llhttp_settings_init(self._csettings) + cparser.llhttp_init(self._cparser, mode, self._csettings) +@@ -364,6 +368,9 @@ cdef class HttpParser: + self._timer = timer + + self._buf = bytearray() ++ self._msg_in_flight = 0 ++ self._max_msg_queue_size = max_msg_queue_size ++ self._tail = b"" + self._payload = None + self._payload_error = 0 + self._payload_exception = payload_exception +@@ -535,6 +542,11 @@ cdef class HttpParser: + + ### Public API ### + ++ def message_consumed(self): ++ # Protocol drained a queued message; free a slot for parsing. ++ if self._msg_in_flight > 0: ++ self._msg_in_flight -= 1 ++ + def feed_eof(self): + cdef bytes desc + +@@ -555,12 +567,22 @@ cdef class HttpParser: + if self._messages: + return self._messages[-1][0] + +- def feed_data(self, data): ++ def feed_data(self, incoming_data): + cdef: + size_t data_len + size_t nb ++ size_t pos + char* base + cdef cparser.llhttp_errno_t errno ++ cdef bytes data ++ ++ if type(incoming_data) is not bytes: ++ data = bytes(incoming_data) ++ else: ++ data = incoming_data ++ ++ if self._tail: ++ data, self._tail = self._tail + data, b"" + + PyObject_GetBuffer(data, &self.py_buf, PyBUF_SIMPLE) + # Cache buffer pointer before PyBuffer_Release to avoid use-after-release. +@@ -574,12 +596,19 @@ cdef class HttpParser: + + if errno is cparser.HPE_PAUSED_UPGRADE: + cparser.llhttp_resume_after_upgrade(self._cparser) +- + nb = cparser.llhttp_get_error_pos(self._cparser) - base ++ elif errno is cparser.HPE_PAUSED: ++ cparser.llhttp_resume(self._cparser) ++ pos = cparser.llhttp_get_error_pos(self._cparser) - base ++ self._tail = data[pos:] + + PyBuffer_Release(&self.py_buf) + +- if errno not in (cparser.HPE_OK, cparser.HPE_PAUSED_UPGRADE): ++ if errno not in ( ++ cparser.HPE_OK, ++ cparser.HPE_PAUSED, ++ cparser.HPE_PAUSED_UPGRADE, ++ ): + if self._payload_error == 0: + if self._last_error is not None: + ex = self._last_error +@@ -617,12 +646,12 @@ cdef class HttpRequestParser(HttpParser): + size_t max_line_size=8190, size_t max_headers=128, + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, +- bint auto_decompress=True, ++ bint auto_decompress=True, Py_ssize_t max_msg_queue_size=0, + ): + self._init(cparser.HTTP_REQUEST, protocol, loop, limit, timer, + max_line_size, max_headers, max_field_size, + payload_exception, response_with_body, read_until_eof, +- auto_decompress) ++ auto_decompress, max_msg_queue_size) + + cdef object _on_status_complete(self): + cdef int idx1, idx2 +@@ -823,6 +852,12 @@ cdef int cb_on_message_complete(cparser.llhttp_t* parser) except -1: + pyparser._last_error = exc + return -1 + else: ++ if pyparser._max_msg_queue_size: ++ pyparser._msg_in_flight += 1 ++ if pyparser._msg_in_flight >= pyparser._max_msg_queue_size: ++ # Queue full: pause llhttp between messages. feed_data() buffers ++ # the remainder as tail; resumes once the queue drains. ++ return cparser.HPE_PAUSED + return 0 + + +diff --git a/aiohttp/base_protocol.py b/aiohttp/base_protocol.py +index b0a67ed6f..505b398e8 100644 +--- a/aiohttp/base_protocol.py ++++ b/aiohttp/base_protocol.py +@@ -5,6 +5,13 @@ from .client_exceptions import ClientConnectionResetError + from .helpers import set_exception + from .tcp_helpers import tcp_nodelay + ++# Raised by transport.pause_reading()/resume_reading() when the transport ++# does not support flow control; safe to ignore. ++# NOTE: Catch these with a plain try/except/pass, never contextlib.suppress(): ++# pause/resume run on the hot read path and suppress() is ~6x slower than ++# try/except here (it builds a context manager and unpacks this tuple per call). ++PAUSE_RESUME_READING_ERRORS = (AttributeError, NotImplementedError, RuntimeError) ++ + + class BaseProtocol(asyncio.Protocol): + __slots__ = ( +@@ -51,17 +58,27 @@ class BaseProtocol(asyncio.Protocol): + if not self._reading_paused and self.transport is not None: + try: + self.transport.pause_reading() +- except (AttributeError, NotImplementedError, RuntimeError): ++ except PAUSE_RESUME_READING_ERRORS: ++ # Transport lacks flow control; nothing to pause. Intentionally ++ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). + pass + self._reading_paused = True + ++ def _reading_paused_for_msg_queue(self) -> bool: ++ """Keep the transport paused for protocol-specific reasons (overridden).""" ++ return False ++ + def resume_reading(self) -> None: +- if self._reading_paused and self.transport is not None: ++ if not self._reading_paused: ++ return ++ self._reading_paused = False ++ if not self._reading_paused_for_msg_queue() and self.transport is not None: + try: + self.transport.resume_reading() +- except (AttributeError, NotImplementedError, RuntimeError): ++ except PAUSE_RESUME_READING_ERRORS: ++ # Transport lacks flow control; nothing to resume. Intentionally ++ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). + pass +- self._reading_paused = False + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + tr = cast(asyncio.Transport, transport) +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index 6a471cd79..e60fcc690 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -271,6 +271,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + response_with_body: bool = True, + read_until_eof: bool = False, + auto_decompress: bool = True, ++ max_msg_queue_size: int = 0, + ) -> None: + self.protocol = protocol + self.loop = loop +@@ -295,6 +296,9 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + self._headers_parser = HeadersParser( + max_line_size, max_headers, max_field_size, self.lax + ) ++ # Stop emitting messages once this many are queued unconsumed (0 = off). ++ self._max_msg_queue_size = max_msg_queue_size ++ self._msg_in_flight = 0 + + @abc.abstractmethod + def parse_message(self, lines: List[bytes]) -> _MsgT: ... +@@ -302,6 +306,11 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + @abc.abstractmethod + def _is_chunked_te(self, te: str) -> bool: ... + ++ def message_consumed(self) -> None: ++ """Protocol drained a queued message; free a slot for parsing.""" ++ if self._msg_in_flight > 0: ++ self._msg_in_flight -= 1 ++ + def feed_eof(self) -> Optional[_MsgT]: + if self._payload_parser is not None: + self._payload_parser.feed_eof() +@@ -344,6 +353,15 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + # read HTTP message (request/response line + headers), \r\n\r\n + # and split by lines + if self._payload_parser is None and not self._upgraded: ++ if ( ++ self._max_msg_queue_size ++ and self._msg_in_flight >= self._max_msg_queue_size ++ ): ++ # Queue full: buffer the rest and stop. Safe pause point; ++ # any preceding body is consumed before the next request ++ # line. Resumes via feed_data(b"") when the queue drains. ++ self._tail = data[start_pos:] ++ break + pos = data.find(SEP, start_pos) + # consume \r\n + if pos == start_pos and not self._lines: +@@ -485,6 +503,8 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + payload = EMPTY_PAYLOAD + + messages.append((msg, payload)) ++ if self._max_msg_queue_size: ++ self._msg_in_flight += 1 + should_close = msg.should_close + else: + self._tail = data[start_pos:] +diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py +index 84e70e590..7ef8f12ed 100644 +--- a/aiohttp/web_protocol.py ++++ b/aiohttp/web_protocol.py +@@ -27,7 +27,7 @@ import yarl + from propcache import under_cached_property + + from .abc import AbstractAccessLogger, AbstractStreamWriter +-from .base_protocol import BaseProtocol ++from .base_protocol import PAUSE_RESUME_READING_ERRORS, BaseProtocol + from .helpers import ceil_timeout + from .http import ( + HttpProcessingError, +@@ -47,6 +47,11 @@ from .web_response import Response, StreamResponse + + __all__ = ("RequestHandler", "RequestPayloadError", "PayloadAccessError") + ++# Max parsed-but-unhandled pipelined requests buffered per connection before ++# reading is paused. Bounds memory a client can pin by keeping one handler busy ++# and pipelining behind it; reading resumes as the queue drains. ++MAX_MSG_QUEUE_SIZE = 32 ++ + if TYPE_CHECKING: + import ssl + +@@ -156,6 +161,9 @@ class RequestHandler(BaseProtocol): + "_keepalive_timeout", + "_lingering_time", + "_messages", ++ "_max_msg_queue_size", ++ "_msg_queue_resume_size", ++ "_msg_queue_paused", + "_message_tail", + "_handler_waiter", + "_waiter", +@@ -198,6 +206,11 @@ class RequestHandler(BaseProtocol): + auto_decompress: bool = True, + timeout_ceil_threshold: float = 5, + ): ++ self._max_msg_queue_size = MAX_MSG_QUEUE_SIZE ++ # Low-water mark: resume reading once the queue drains to half the limit ++ # so we refill in batches instead of churning pause/resume per request. ++ self._msg_queue_resume_size = MAX_MSG_QUEUE_SIZE // 2 ++ self._msg_queue_paused = False + super().__init__(loop) + + # _request_count is the number of requests processed with the same connection. +@@ -237,6 +250,7 @@ class RequestHandler(BaseProtocol): + max_headers=max_headers, + payload_exception=RequestPayloadError, + auto_decompress=auto_decompress, ++ max_msg_queue_size=MAX_MSG_QUEUE_SIZE, + ) + + self._timeout_ceil_threshold: float = 5 +@@ -429,6 +443,14 @@ class RequestHandler(BaseProtocol): + # don't set result twice + waiter.set_result(None) + ++ # Queue full: pause the transport (the parser already stopped ++ # emitting). start() resumes as it drains the queue. ++ if ( ++ not self._msg_queue_paused ++ and len(self._messages) >= self._max_msg_queue_size ++ ): ++ self._pause_msg_queue_reading() ++ + self._upgrade = upgraded + if upgraded and tail: + self._message_tail = tail +@@ -443,6 +465,36 @@ class RequestHandler(BaseProtocol): + if eof: + self.close() + ++ def _reading_paused_for_msg_queue(self) -> bool: ++ return self._msg_queue_paused ++ ++ def _pause_msg_queue_reading(self) -> None: ++ self._msg_queue_paused = True ++ if self.transport is not None: ++ try: ++ self.transport.pause_reading() ++ except PAUSE_RESUME_READING_ERRORS: ++ # Transport lacks flow control; nothing to pause. Intentionally ++ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). ++ pass ++ ++ def _resume_msg_queue_reading(self) -> None: ++ if not self._upgrade: ++ # Reparse buffered pipelined requests while still marked paused so ++ # a refill past the limit does not re-pause an already-paused ++ # transport; only resume below once it stayed under the limit. ++ self.data_received(b"") ++ if len(self._messages) >= self._max_msg_queue_size: ++ return ++ self._msg_queue_paused = False ++ if not self._reading_paused and self.transport is not None: ++ try: ++ self.transport.resume_reading() ++ except PAUSE_RESUME_READING_ERRORS: ++ # Transport lacks flow control; nothing to resume. Intentionally ++ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). ++ pass ++ + def keep_alive(self, val: bool) -> None: + """Set keep-alive connection mode. + +@@ -575,6 +627,18 @@ class RequestHandler(BaseProtocol): + + message, payload = self._messages.popleft() + ++ # Free a parser slot; resume reading once drained to low water so ++ # pipelining keeps flowing while this request is handled. ++ # no branch: _request_parser is only None after connection_lost, whose path ++ # exits this loop, so the None case is not reachably exercisable. ++ if self._request_parser is not None: # pragma: no branch ++ self._request_parser.message_consumed() ++ if ( ++ self._msg_queue_paused ++ and len(self._messages) <= self._msg_queue_resume_size ++ ): ++ self._resume_msg_queue_reading() ++ + # time is only fetched if logging is enabled as otherwise + # its thrown away and never used. + start = loop.time() if self._logging_enabled else None +diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt +index 382ad414e..e429f8c62 100644 +--- a/docs/spelling_wordlist.txt ++++ b/docs/spelling_wordlist.txt +@@ -239,6 +239,7 @@ peername + performant + pickleable + ping ++pipelined + pipelining + pluggable + plugin +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 8cb591f20..f054e6b3c 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -120,6 +120,78 @@ def test_c_parser_loaded(): + assert "RawResponseMessageC" in dir(aiohttp.http_parser) + + ++_PIPELINED_GET = b"GET / HTTP/1.1\r\nHost: a\r\n\r\n" ++ ++ ++def _build_request_parser( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++ max_msg_queue_size: int, ++) -> HttpRequestParser: ++ return request_cls( ++ protocol, ++ loop, ++ 2**16, ++ max_line_size=8190, ++ max_headers=128, ++ max_field_size=8190, ++ max_msg_queue_size=max_msg_queue_size, ++ ) ++ ++ ++def test_max_msg_queue_size_caps_emitted_messages( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++) -> None: ++ parser = _build_request_parser(request_cls, protocol, loop, 4) ++ messages, upgraded, _tail = parser.feed_data(_PIPELINED_GET * 10) ++ assert len(messages) == 4 ++ assert not upgraded ++ ++ ++def test_max_msg_queue_size_resumes_after_consume( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++) -> None: ++ limit = 4 ++ total = 10 ++ parser = _build_request_parser(request_cls, protocol, loop, limit) ++ messages, _upgraded, _tail = parser.feed_data(_PIPELINED_GET * total) ++ seen = 0 ++ while messages: ++ assert len(messages) <= limit ++ seen += len(messages) ++ for _msg, _payload in messages: ++ parser.message_consumed() ++ messages, _upgraded, _tail = parser.feed_data(b"") ++ assert seen == total ++ ++ ++def test_max_msg_queue_size_zero_is_unbounded( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++) -> None: ++ parser = _build_request_parser(request_cls, protocol, loop, 0) ++ messages, _upgraded, _tail = parser.feed_data(_PIPELINED_GET * 50) ++ assert len(messages) == 50 ++ ++ ++def test_message_consumed_underflow_is_ignored( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++) -> None: ++ parser = _build_request_parser(request_cls, protocol, loop, 4) ++ # No message is in flight; consuming must not underflow the counter. ++ parser.message_consumed() ++ messages, _upgraded, _tail = parser.feed_data(_PIPELINED_GET * 4) ++ assert len(messages) == 4 ++ ++ + def test_parse_headers(parser: Any) -> None: + text = b"""GET /test HTTP/1.1\r + test: a line\r +diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py +index e2fd40432..e26c5ae57 100644 +--- a/tests/test_web_functional.py ++++ b/tests/test_web_functional.py +@@ -29,7 +29,7 @@ from aiohttp.helpers import DEFAULT_CHUNK_SIZE + from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer + from aiohttp.streams import StreamReader + from aiohttp.typedefs import Handler +-from aiohttp.web_protocol import RequestHandler ++from aiohttp.web_protocol import MAX_MSG_QUEUE_SIZE, RequestHandler + + try: + import brotlicffi as brotli +@@ -1687,6 +1687,136 @@ async def test_response_prepared_with_clone(aiohttp_client) -> None: + await resp.release() + + ++async def test_http1_pipelined_requests_are_count_limited( ++ aiohttp_server: AiohttpServer, ++ monkeypatch: pytest.MonkeyPatch, ++) -> None: ++ """Requests pipelined behind a busy handler must not grow unbounded. ++ ++ A client can keep one handler active and pipeline many complete requests ++ behind it; the per-connection queue stays bounded by MAX_MSG_QUEUE_SIZE. ++ """ ++ pipelined_requests = 500 ++ slow_handler_started = asyncio.Event() ++ queue_observed = asyncio.Event() ++ max_queued = 0 ++ data_received = RequestHandler.data_received ++ ++ def observe_data_received(self: RequestHandler, data: bytes) -> None: ++ nonlocal max_queued ++ data_received(self, data) ++ if self._request_in_progress and self._messages: ++ max_queued = max(max_queued, len(self._messages)) ++ queue_observed.set() ++ ++ monkeypatch.setattr(RequestHandler, "data_received", observe_data_received) ++ ++ async def slow_handler(request: web.Request) -> web.Response: ++ slow_handler_started.set() ++ await asyncio.sleep(0.5) ++ return web.Response(text="slow") ++ ++ async def fast_handler(request: web.Request) -> NoReturn: ++ # The pipelined requests are only counted, never handled: the test ++ # closes the connection while the slow handler still holds the loop. ++ assert False ++ ++ app = web.Application() ++ app.router.add_get("/slow", slow_handler) ++ app.router.add_get("/x", fast_handler) ++ server = await aiohttp_server(app) ++ ++ def raw_get(path: str) -> bytes: ++ return ( ++ f"GET {path} HTTP/1.1\r\nHost: localhost\r\n" ++ "Connection: keep-alive\r\n\r\n" ++ ).encode("ascii") ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ writer.write(raw_get("/slow")) ++ await writer.drain() ++ await asyncio.wait_for(slow_handler_started.wait(), 1) ++ ++ writer.write(raw_get("/x") * pipelined_requests) ++ await writer.drain() ++ await asyncio.wait_for(queue_observed.wait(), 1) ++ finally: ++ writer.close() ++ with suppress(ConnectionResetError, BrokenPipeError): ++ await writer.wait_closed() ++ ++ # Tight lower bound also catches over-aggressive pausing (e.g. clamping to 1). ++ assert MAX_MSG_QUEUE_SIZE // 2 < max_queued <= MAX_MSG_QUEUE_SIZE ++ ++ ++async def test_http1_pipelined_queue_resumes_after_drain( ++ aiohttp_server: AiohttpServer, ++ monkeypatch: pytest.MonkeyPatch, ++) -> None: ++ """A paused pipeline queue resumes reading once handlers drain it. ++ ++ Once enough requests are pipelined behind a busy handler to fill the queue, ++ reading is paused; as the handlers drain the queue past the low-water mark ++ reading must resume so the remaining buffered requests are still served. ++ """ ++ # Several times the limit so the queue refills and re-pauses while draining. ++ pipelined_requests = MAX_MSG_QUEUE_SIZE * 3 ++ first_started = asyncio.Event() ++ release_first = asyncio.Event() ++ resumed = asyncio.Event() ++ handled: list[str] = [] ++ all_handled = asyncio.Event() ++ ++ resume = RequestHandler._resume_msg_queue_reading ++ ++ def observe_resume(self: RequestHandler) -> None: ++ resume(self) ++ resumed.set() ++ ++ monkeypatch.setattr(RequestHandler, "_resume_msg_queue_reading", observe_resume) ++ ++ async def handler(request: web.Request) -> web.Response: ++ if request.path == "/first": ++ first_started.set() ++ await release_first.wait() ++ handled.append(request.path) ++ if len(handled) == pipelined_requests + 1: ++ all_handled.set() ++ return web.Response() ++ ++ app = web.Application() ++ app.router.add_get("/{tail:.*}", handler) ++ server = await aiohttp_server(app) ++ ++ def raw_get(path: str) -> bytes: ++ return ( ++ f"GET {path} HTTP/1.1\r\nHost: localhost\r\n" ++ "Connection: keep-alive\r\n\r\n" ++ ).encode("ascii") ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ writer.write(raw_get("/first")) ++ await writer.drain() ++ await asyncio.wait_for(first_started.wait(), 1) ++ ++ writer.write(b"".join(raw_get(f"/r{i}") for i in range(pipelined_requests))) ++ await writer.drain() ++ ++ # Let the busy handler finish so the queue drains and reading resumes. ++ release_first.set() ++ await asyncio.wait_for(resumed.wait(), 5) ++ # Every pipelined request is still served only if reading resumed. ++ await asyncio.wait_for(all_handled.wait(), 5) ++ finally: ++ writer.close() ++ with suppress(ConnectionResetError, BrokenPipeError): ++ await writer.wait_closed() ++ ++ assert len(handled) == pipelined_requests + 1 ++ ++ + @pytest.mark.parametrize("decompressed_size", [4 * 1024 * 1024, 32 * 1024 * 1024]) + async def test_unread_compressed_body_drain_is_bounded( + aiohttp_server: AiohttpServer, +diff --git a/tests/test_web_protocol.py b/tests/test_web_protocol.py +new file mode 100644 +index 000000000..879be2080 +--- /dev/null ++++ b/tests/test_web_protocol.py +@@ -0,0 +1,112 @@ ++import asyncio ++from unittest import mock ++ ++import pytest ++ ++from aiohttp.web_protocol import RequestHandler ++from aiohttp.web_server import Server ++ ++ ++@pytest.fixture ++def dummy_manager() -> Server: ++ return mock.create_autospec( ++ Server, ++ request_handler=mock.Mock(), ++ request_factory=mock.Mock(), ++ instance=True, ++ ) ++ ++ ++def test_pause_msg_queue_reading_without_transport( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """Pausing with no transport still records the paused state.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ handler.transport = None ++ ++ handler._pause_msg_queue_reading() ++ ++ assert handler._msg_queue_paused is True ++ ++ ++def test_resume_msg_queue_reading_after_upgrade_skips_reparse( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """Resume after an upgrade clears the pause and resumes without reparsing.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ transport = mock.Mock() ++ handler.transport = transport ++ handler._upgrade = True ++ handler._msg_queue_paused = True ++ handler._reading_paused = False ++ ++ with mock.patch.object(RequestHandler, "data_received") as data_received: ++ handler._resume_msg_queue_reading() ++ ++ data_received.assert_not_called() ++ assert handler._msg_queue_paused is False ++ transport.resume_reading.assert_called_once_with() ++ ++ ++def test_resume_msg_queue_reading_without_transport( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """Resume clears the pause but does not touch a missing transport.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ handler.transport = None ++ handler._upgrade = True # skip the reparse branch ++ handler._msg_queue_paused = True ++ ++ handler._resume_msg_queue_reading() ++ ++ assert handler._msg_queue_paused is False ++ ++ ++def test_resume_reading_stays_paused_for_msg_queue( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """Base resume_reading must not un-pause the transport while queue-paused.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ transport = mock.Mock() ++ handler.transport = transport ++ handler._reading_paused = True ++ handler._msg_queue_paused = True ++ ++ handler.resume_reading() ++ ++ transport.resume_reading.assert_not_called() ++ assert handler._reading_paused is False ++ ++ ++def test_pause_msg_queue_reading_ignores_unsupported_transport( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """A transport without flow control raising on pause is ignored.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ # Bare asyncio.Transport.pause_reading() raises NotImplementedError. ++ handler.transport = asyncio.Transport() ++ ++ handler._pause_msg_queue_reading() ++ ++ assert handler._msg_queue_paused is True ++ ++ ++def test_resume_msg_queue_reading_ignores_unsupported_transport( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """A transport without flow control raising on resume is ignored.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ # Bare asyncio.Transport.resume_reading() raises NotImplementedError. ++ handler.transport = asyncio.Transport() ++ handler._upgrade = True # skip the reparse branch ++ handler._msg_queue_paused = True ++ ++ handler._resume_msg_queue_reading() ++ ++ assert handler._msg_queue_paused is False diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p2.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p2.patch new file mode 100644 index 0000000000..c6a318798d --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p2.patch @@ -0,0 +1,177 @@ +From 47babd8c23ca79e2bae6cc8dcb5752b61e369fc7 Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Sun, 9 Aug 2026 17:17:34 +0100 +Subject: [PATCH] [PR #13356/72eaa429 backport][3.14] Stop handing back + pipelined requests the parser buffered (#13360) + +**This is a backport of PR #13356 as merged into master +(72eaa429cf89b1e20212590f3e704444239569fb).** + +--------- + +CVE: CVE-2026-54273 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/47babd8c23ca79e2bae6cc8dcb5752b61e369fc7] + +Backport Changes: +- Adapted the parser regression test to the 3.13.5 synchronous loop + fixture. +- Adapted finish_response() to the older 3.13.5 replay path by + retaining and queueing parser results before enforcing the limit. + +Co-authored-by: Rodrigo Nogueira +(cherry picked from commit 47babd8c23ca79e2bae6cc8dcb5752b61e369fc7) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13356.bugfix.rst | 4 ++++ + aiohttp/http_parser.py | 2 ++ + aiohttp/web_protocol.py | 16 ++++++++++++++-- + tests/test_http_parser.py | 18 ++++++++++++++++++ + tests/test_web_functional.py | 51 ++++++++++++++++++++++++++++++++++++++++++++ + 5 files changed, 91 insertions(+), 2 deletions(-) + create mode 100644 CHANGES/13356.bugfix.rst + +diff --git a/CHANGES/13356.bugfix.rst b/CHANGES/13356.bugfix.rst +new file mode 100644 +index 000000000..a9617231a +--- /dev/null ++++ b/CHANGES/13356.bugfix.rst +@@ -0,0 +1,4 @@ ++Fixed requests pipelined behind a request whose upgrade the handler declined ++going unanswered once there were more of them than the per-connection queue ++holds. With the pure-Python parser the same requests were also served more ++than once -- by :user:`rodrigobnogueira`. +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index e60fcc690..32375f83a 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -361,6 +361,8 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + # any preceding body is consumed before the next request + # line. Resumes via feed_data(b"") when the queue drains. + self._tail = data[start_pos:] ++ # The remainder now lives in self._tail only. Don't return it. ++ data = EMPTY + break + pos = data.find(SEP, start_pos) + # consume \r\n +diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py +index 7ef8f12ed..d9843f969 100644 +--- a/aiohttp/web_protocol.py ++++ b/aiohttp/web_protocol.py +@@ -763,8 +763,22 @@ class RequestHandler(BaseProtocol): + self._request_parser.set_upgraded(False) + self._upgrade = False + if self._message_tail: +- self._request_parser.feed_data(self._message_tail) +- self._message_tail = b"" ++ messages, _upgraded, tail = self._request_parser.feed_data( ++ self._message_tail ++ ) ++ self._message_tail = tail ++ for msg, payload in messages: ++ self._request_count += 1 ++ self._messages.append((msg, payload)) ++ # Pause the transport, like in data_received(). ++ if ( ++ not self._msg_queue_paused ++ and len(self._messages) >= self._max_msg_queue_size ++ ): ++ self._pause_msg_queue_reading() ++ # This should not be possible. If a future refactor results in ++ # this failing, update the code to set the waiter. ++ assert self._waiter is None + try: + prepare_meth = resp.prepare + except AttributeError: +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index f054e6b3c..a4e6bc056 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -151,6 +151,24 @@ def test_max_msg_queue_size_caps_emitted_messages( + assert not upgraded + + ++def test_max_msg_queue_size_keeps_tail_to_itself( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++) -> None: ++ """The remainder is buffered for the next feed, so it must not be returned. ++ ++ Handing it back as well gives the caller a second copy of bytes the parser ++ is already holding, and both copies get parsed. ++ """ ++ parser = _build_request_parser(request_cls, protocol, loop, 4) ++ ++ messages, _upgraded, tail = parser.feed_data(_PIPELINED_GET * 10) ++ ++ assert len(messages) == 4 ++ assert tail == b"" ++ ++ + def test_max_msg_queue_size_resumes_after_consume( + request_cls: type[HttpRequestParser], + protocol: BaseProtocol, +diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py +index e26c5ae57..b958bef9d 100644 +--- a/tests/test_web_functional.py ++++ b/tests/test_web_functional.py +@@ -1817,6 +1817,57 @@ async def test_http1_pipelined_queue_resumes_after_drain( + assert len(handled) == pipelined_requests + 1 + + ++async def test_http1_pipelined_behind_declined_upgrade_served_once( ++ aiohttp_server: AiohttpServer, ++) -> None: ++ """Requests pipelined behind a declined upgrade are each served once. ++ ++ The bytes following an upgrade request are buffered whole, then re-fed once ++ the handler answers it normally. More of them than the queue holds must ++ still be served, and none of them twice. ++ """ ++ pipelined_requests = MAX_MSG_QUEUE_SIZE + 8 ++ handled: list[str] = [] ++ all_handled = asyncio.Event() ++ ++ async def handler(request: web.Request) -> web.Response: ++ handled.append(request.path) ++ if len(handled) == pipelined_requests + 1: ++ all_handled.set() ++ return web.Response() ++ ++ app = web.Application() ++ app.router.add_get("/{tail:.*}", handler) ++ server = await aiohttp_server(app) ++ ++ def raw_get(path: str) -> bytes: ++ return ( ++ f"GET {path} HTTP/1.1\r\nHost: localhost\r\n" ++ "Connection: keep-alive\r\n\r\n" ++ ).encode("ascii") ++ ++ # An upgrade the handler answers normally, then the pipeline, in one write. ++ upgrade = ( ++ b"GET /upgrade HTTP/1.1\r\nHost: localhost\r\n" ++ b"Connection: Upgrade\r\nUpgrade: websocket\r\n\r\n" ++ ) ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ writer.write( ++ upgrade + b"".join(raw_get(f"/r{i}") for i in range(pipelined_requests)) ++ ) ++ await writer.drain() ++ await asyncio.wait_for(all_handled.wait(), 10) ++ finally: ++ writer.close() ++ with suppress(ConnectionResetError, BrokenPipeError): ++ await writer.wait_closed() ++ ++ assert len(handled) == pipelined_requests + 1 ++ assert len(set(handled)) == len(handled) ++ ++ + @pytest.mark.parametrize("decompressed_size", [4 * 1024 * 1024, 32 * 1024 * 1024]) + async def test_unread_compressed_body_drain_is_bounded( + aiohttp_server: AiohttpServer, +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb index b9d08ac97b..01162a589a 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb @@ -17,6 +17,8 @@ SRC_URI += " \ file://CVE-2026-54278.patch \ file://CVE-2026-54279.patch \ file://CVE-2026-54280.patch \ + file://CVE-2026-54273_p1.patch \ + file://CVE-2026-54273_p2.patch \ " CVE_PRODUCT = "aiohttp" @@ -28,7 +30,14 @@ CVE-2026-34520 CVE-2026-34525" inherit python_setuptools_build_meta pypi -DEPENDS = "python3-pkgconfig-native" +DEPENDS = "python3-pkgconfig-native python3-cython-native" + +do_configure:prepend() { + cython3 -3 -Werror \ + -I ${S}/aiohttp \ + -o ${S}/aiohttp/_http_parser.c \ + ${S}/aiohttp/_http_parser.pyx +} PACKAGECONFIG ??= "" PACKAGECONFIG[extras] = ",,,python3-aiodns python3-brotli"