diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-69244.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-69244.patch new file mode 100644 index 0000000000..6fc982356a --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-69244.patch @@ -0,0 +1,159 @@ +From bfa3f83ad150bf6bc660dd0e3178375942b9528a Mon Sep 17 00:00:00 2001 +From: "J. Nick Koston" +Date: Wed, 22 Jul 2026 11:14:50 -1000 +Subject: [PATCH] [PR #13222/f4866933 backport][3.14] Build C parser error + message from bounded buffer slice (#13223) + +CVE: CVE-2026-69244 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/49f65d54150397892f7bcc4aae887767d51c322d] + +Backport Changes: +- Used mock protocols in three parser-error regression tests: + the 3.13.5 test setup differs from upstream, and these tests + need no transport behavior to check the bounded error snippet. +- Omitted generated _http_parser.c because the Wrynose recipe + regenerates it from _http_parser.pyx during do_configure. + +(cherry picked from commit 49f65d54150397892f7bcc4aae887767d51c322d) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13222.bugfix.rst | 1 + + aiohttp/_http_parser.pyx | 9 ++-- + tests/test_http_parser.py | 92 +++++++++++++++++++++++++++++++++++++++ + 3 files changed, 98 insertions(+), 4 deletions(-) + create mode 100644 CHANGES/13222.bugfix.rst + +diff --git a/CHANGES/13222.bugfix.rst b/CHANGES/13222.bugfix.rst +new file mode 100644 +index 000000000..dcdda1705 +--- /dev/null ++++ b/CHANGES/13222.bugfix.rst +@@ -0,0 +1 @@ ++Fixed error message construction in the C HTTP parser -- by :user:`bdraco`. +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index 478d82ecb..ffbd53fde 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -623,11 +623,12 @@ cdef class HttpParser: + ex = self._last_error + self._last_error = None + else: +- after = cparser.llhttp_get_error_pos(self._cparser) +- before = data[:after - base] +- after_b = after.split(b"\r\n", 1)[0] ++ error_pos = cparser.llhttp_get_error_pos(self._cparser) ++ error_off = error_pos - base ++ before = data[:error_off] ++ after = data[error_off:].split(b"\r\n", 1)[0] + before = before.rsplit(b"\r\n", 1)[-1] +- data = before + after_b ++ data = before + after + pointer = " " * (len(repr(before))-1) + "^" + ex = parser_error_from_errno(self._cparser, data, pointer) + self._payload = None +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 92e59af1c..4cdd94cb6 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -1607,6 +1607,98 @@ async def test_http_response_parser_bad_chunked_strict_c(loop, protocol) -> None + response.feed_data(text) + + ++# The C parser builds its error message from a snippet of the fed buffer. That ++# snippet must be sliced out of the bounded ``data`` bytes object; materializing the ++# raw llhttp error-position pointer instead runs strlen past the end of the buffer. ++# These tests pin the snippet to bytes that lie inside the fed input, and cover error ++# positions right at the buffer end where the over-read used to happen. ++ ++_CHUNKED_RESP_HDR = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" ++ ++# (malformed body appended to a chunked response header, expected error snippet, ++# llhttp error reason) ++_BAD_CHUNKED_RESPONSES = [ ++ (b"0\rX\r\n\r\n", b"0\rX", "Expected LF after chunk size"), ++ (b"5\r\nhello\rX\r\n0\r\n\r\n", b"hello\rX", "Expected LF after chunk data"), ++ (b"5\r\nhelloXY\r\n0\r\n\r\n", b"helloXY", "Expected LF after chunk data"), ++ (b"1\r\nA\rB\r\n0\r\n\r\n", b"A\rB", "Expected LF after chunk data"), ++ (b"0_2e\r\n\r\n", b"0_2e", "Invalid character in chunk size"), ++] ++ ++# Same, but the buffer ends exactly at the offending byte so the error position sits ++# at the end of the input with no trailing CRLF to bound the old strlen-based slice. ++_BAD_CHUNKED_RESPONSES_AT_END = [ ++ (b"0\rX", b"0\rX", "Expected LF after chunk size"), ++ (b"5\r\nhello\rX", b"hello\rX", "Expected LF after chunk data"), ++ (b"0_", b"0_", "Invalid character in chunk size"), ++] ++ ++_BAD_REQUESTS_AT_END = [ ++ (b"GET / HTTP/1.1\r\nHost: x\n", b"Host: x\n"), ++ (b"POST / HTTP/1.1\r\nHost: x\r\nA: b\x01", b"A: b\x01"), ++] ++ ++ ++@pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.") ++@pytest.mark.parametrize( ++ ("body", "snippet", "reason"), ++ _BAD_CHUNKED_RESPONSES + _BAD_CHUNKED_RESPONSES_AT_END, ++) ++async def test_c_parser_error_snippet_bounded_response( ++ body: bytes, snippet: bytes, reason: str ++) -> None: ++ loop = asyncio.get_running_loop() ++ protocol = mock.Mock() ++ response = HttpResponseParserC( ++ protocol, loop, 2**16, max_line_size=8190, max_field_size=8190 ++ ) ++ protocol._parser = response ++ text = _CHUNKED_RESP_HDR + body ++ assert snippet in text ++ match = f"{re.escape(reason)}[\\s\\S]*{re.escape(repr(snippet))}" ++ with pytest.raises(http_exceptions.BadHttpMessage, match=match): ++ response.feed_data(text) ++ ++ ++@pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.") ++@pytest.mark.parametrize(("text", "snippet"), _BAD_REQUESTS_AT_END) ++def test_c_parser_error_snippet_at_buffer_end_request( ++ loop: asyncio.AbstractEventLoop, ++ text: bytes, ++ snippet: bytes, ++) -> None: ++ protocol = mock.Mock() ++ parser = HttpRequestParserC( ++ protocol, loop, 2**16, max_line_size=8190, max_field_size=8190 ++ ) ++ protocol._parser = parser ++ assert snippet in text ++ with pytest.raises(http_exceptions.BadHttpMessage, match=re.escape(repr(snippet))): ++ parser.feed_data(text) ++ ++ ++@pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.") ++@pytest.mark.parametrize("split", [1, 2, 3, 5]) ++@pytest.mark.parametrize(("body", "snippet", "reason"), _BAD_CHUNKED_RESPONSES) ++async def test_c_parser_error_snippet_fragmented_response( ++ body: bytes, snippet: bytes, reason: str, split: int ++) -> None: ++ # Feed the malformed body in two pieces so the second feed runs through the ++ # tail-prepend path before the error is reported; the parser must report a clean ++ # error rather than reading past the buffer. The snippet offset shifts with the ++ # split, so pin the stable llhttp reason instead. ++ loop = asyncio.get_running_loop() ++ protocol = mock.Mock() ++ response = HttpResponseParserC( ++ protocol, loop, 2**16, max_line_size=8190, max_field_size=8190 ++ ) ++ protocol._parser = response ++ response.feed_data(_CHUNKED_RESP_HDR) ++ with pytest.raises(http_exceptions.BadHttpMessage, match=re.escape(reason)): ++ response.feed_data(body[:split]) ++ response.feed_data(body[split:]) ++ ++ + async def test_http_response_parser_notchunked(response) -> None: + text = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: notchunked\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n" + messages, upgrade, tail = response.feed_data(text) +-- +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 417760c9ca..bd5b5fe4d9 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 @@ -21,6 +21,7 @@ SRC_URI += " \ file://CVE-2026-54273_p2.patch \ file://CVE-2026-59881.patch \ file://CVE-2026-69243.patch \ + file://CVE-2026-69244.patch \ " CVE_PRODUCT = "aiohttp:aiohttp"