new file mode 100644
@@ -0,0 +1,29 @@
+From 9016d7e8afc68185496ef07f3c3a4a743d04922e Mon Sep 17 00:00:00 2001
+From: Illia Volochii <illia.volochii@gmail.com>
+Date: Tue, 15 Sep 2026 22:04:41 +0300
+Subject: [PATCH] Skip `test_read_chunked_with_trailing_data_does_not_hang` for
+ brotlicffi (#5258)
+
+CVE: CVE-2026-97688
+Upstream-Status: Backport [https://github.com/urllib3/urllib3/commit/9016d7e8afc68185496ef07f3c3a4a743d04922e]
+
+Signed-off-by: Yogita Urade <yurade@cisco.com>
+---
+ test/test_response.py | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/test/test_response.py b/test/test_response.py
+index 5ecdbd9..24118f6 100644
+--- a/test/test_response.py
++++ b/test/test_response.py
+@@ -831,6 +831,8 @@ class TestResponse:
+ # while trailing data remains in the decoder's input buffer.
+ original_data = b"A" * 100
+ content_encoding, compress_func = data
++ if content_encoding == "br" and brotli.__name__ == "brotlicffi":
++ pytest.skip("This case is not supported by brotlicffi")
+ compressed_data = compress_func(original_data) + b"tail"
+ httplib_r = httplib.HTTPResponse(MockSock) # type: ignore[arg-type]
+ httplib_r.fp = MockChunkedEncodingResponse([compressed_data]) # type: ignore[assignment]
+--
+2.35.6
new file mode 100644
@@ -0,0 +1,107 @@
+From ea2ad7b21a80da3632f80016526a18864586077f Mon Sep 17 00:00:00 2001
+From: Illia Volochii <illia.volochii@gmail.com>
+Date: Tue, 15 Sep 2026 21:32:04 +0300
+Subject: [PATCH] Merge commit from fork
+
+* Prevent an infinite loop when decoding Deflate responses
+
+* Move the new test to a better place
+
+* Add a changelog entry
+
+CVE: CVE-2026-97688
+Upstream-Status: Backport [https://github.com/urllib3/urllib3/commit/ea2ad7b21a80da3632f80016526a18864586077f]
+
+Backport Changes:
+- Omitted changelog/GHSA-gh4c-6fx4-qh6g.bugfix.rst because the OE recipe
+ backport applies the runtime fix and tests; the package does not ship it.
+
+Signed-off-by: Yogita Urade <yurade@cisco.com>
+---
+ src/urllib3/response.py | 8 +++++++-
+ test/test_response.py | 38 +++++++++++++++++++++++++++++++++++++-
+ 2 files changed, 44 insertions(+), 2 deletions(-)
+
+diff --git a/src/urllib3/response.py b/src/urllib3/response.py
+index e9e0cee75e..dccdb12936 100644
+--- a/src/urllib3/response.py
++++ b/src/urllib3/response.py
+@@ -76,6 +76,10 @@ def __init__(self) -> None:
+ def decompress(self, data: bytes, max_length: int = -1) -> bytes:
+ data = self._unfed_data + data
+ self._unfed_data = b""
++ # Data received after EOF cannot produce more output from this
++ # Deflate stream.
++ if self._obj.eof:
++ return b""
+ if not data and not self._obj.unconsumed_tail:
+ return data
+ original_max_length = max_length
+@@ -118,7 +122,9 @@ def decompress(self, data: bytes, max_length: int = -1) -> bytes:
+ @property
+ def has_unconsumed_tail(self) -> bool:
+ return bool(self._unfed_data) or (
+- bool(self._obj.unconsumed_tail) and not self._first_try
++ bool(self._obj.unconsumed_tail)
++ and not self._first_try
++ and not self._obj.eof
+ )
+
+ def flush(self) -> bytes:
+
+diff --git a/test/test_response.py b/test/test_response.py
+index 799325b58c..4675646232 100644
+--- a/test/test_response.py
++++ b/test/test_response.py
+@@ -11,7 +11,7 @@
+ from base64 import b64decode
+ from http.client import IncompleteRead as httplib_IncompleteRead
+ from io import BufferedReader, BytesIO, TextIOWrapper
+-from test import onlyBrotli, onlyZstd
++from test import LONG_TIMEOUT, onlyBrotli, onlyZstd
+ from unittest import mock
+
+ import pytest
+@@ -813,6 +813,42 @@ def test_read_chunked_with_all_data_already_in_decompressor(
+ assert len(result[-1]) == len(original_data) % 10240
+ assert b"".join(result) == original_data
+
++ @pytest.mark.timeout(LONG_TIMEOUT)
++ @pytest.mark.parametrize(
++ "data",
++ [d[1] for d in _test_compressor_params],
++ ids=[d[0] for d in _test_compressor_params],
++ )
++ def test_read_chunked_with_trailing_data_does_not_hang(
++ self,
++ request: pytest.FixtureRequest,
++ data: tuple[str, typing.Callable[[bytes], bytes]] | None,
++ ) -> None:
++ if data is None:
++ pytest.skip(f"Proper {request.node.callspec.id} decoder is not available")
++ # The decoded body must fill multiple bounded reads so EOF is reached
++ # while trailing data remains in the decoder's input buffer.
++ original_data = b"A" * 100
++ content_encoding, compress_func = data
++ compressed_data = compress_func(original_data) + b"tail"
++ httplib_r = httplib.HTTPResponse(MockSock) # type: ignore[arg-type]
++ httplib_r.fp = MockChunkedEncodingResponse([compressed_data]) # type: ignore[assignment]
++ r = HTTPResponse(
++ httplib_r,
++ preload_content=False,
++ headers={
++ "transfer-encoding": "chunked",
++ "content-encoding": content_encoding,
++ },
++ )
++
++ stream = r.stream(len(original_data) // 2, decode_content=True)
++ if content_encoding in ("br", "zstd"):
++ with pytest.raises(DecodeError):
++ list(stream)
++ else:
++ assert b"".join(stream) == original_data
++
+ @pytest.mark.parametrize(
+ "delta",
+ (
@@ -8,6 +8,8 @@ SRC_URI += "file://CVE-2026-44431.patch \
file://CVE-2026-44432.patch \
file://CVE-2026-97687_p1.patch \
file://CVE-2026-97687_p2.patch \
+ file://CVE-2026-97688.patch \
+ file://CVE-2026-97688-regression.patch \
"
SRC_URI[sha256sum] = "1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"
This patch applies the upstream fix as referenced in [3], using the commit shown in [1] and regression commit from [2]. Regression test-only follow-up skips the Brotli case when urllib3 uses brotlicffi, because that trailing-data case is unsupported by that backend. The skip is limited to Brotli; the Deflate regression case remains covered. [1] https://github.com/urllib3/urllib3/commit/ea2ad7b21a80da3632f80016526a18864586077f [2] https://github.com/urllib3/urllib3/commit/9016d7e8afc68185496ef07f3c3a4a743d04922e [3] https://security-tracker.debian.org/tracker/CVE-2026-97688 [4] https://github.com/urllib3/urllib3/pull/5258 Signed-off-by: Yogita Urade <yurade@cisco.com> --- .../CVE-2026-97688-regression.patch | 29 +++++ .../python3-urllib3/CVE-2026-97688.patch | 107 ++++++++++++++++++ .../python/python3-urllib3_2.6.3.bb | 2 + 3 files changed, 138 insertions(+) create mode 100644 meta/recipes-devtools/python/python3-urllib3/CVE-2026-97688-regression.patch create mode 100644 meta/recipes-devtools/python/python3-urllib3/CVE-2026-97688.patch