new file mode 100644
@@ -0,0 +1,195 @@
+From 1b7a1dc9f340f52de929852e8125aa12f25297c9 Mon Sep 17 00:00:00 2001
+From: Ben Darnell <ben@bendarnell.com>
+Date: Tue, 6 Oct 2026 09:59:02 -0400
+Subject: [PATCH] httputil: Remove support for "obsolete line folding"
+
+This optional part of the HTTP specification is no longer used
+in practice, and continuing to support it adds complexity and risk
+of DoS via quadratic performance on malicious data.
+
+CVE: CVE-2026-116577
+Upstream-Status: Backport [https://github.com/tornadoweb/tornado/commit/1b7a1dc9f340f52de929852e8125aa12f25297c9]
+
+Backport Changes:
+- Kept unittest.TestCase for HTTPHeadersTest because 6.5.7's httputil_test.py
+ does not import tornado.test.util.TestCase.
+- Updated test_multi_line_headers to preserve folded-header acceptance for Curl
+ and expect a 599 response from simple_httpclient.
+- The httputil_test.py hunk removes two extra target-version lines because
+ 6.5.7 formats the removed multiline .replace() expression across three lines.
+
+Signed-off-by: Yogita Urade <yurade@cisco.com>
+---
+ tornado/httputil.py | 34 +++++++++---------------
+ tornado/test/httpclient_test.py | 17 +++++++++++--
+ tornado/test/httputil_test.py | 47 +++------------------------------
+ 3 files changed, 29 insertions(+), 69 deletions(-)
+
+diff --git a/tornado/httputil.py b/tornado/httputil.py
+index 6a5db233..45e1883d 100644
+--- a/tornado/httputil.py
++++ b/tornado/httputil.py
+@@ -195,7 +195,6 @@ class HTTPHeaders(StrMutableMapping):
+ # on demand (and cleared whenever the list is modified).
+ self._as_list: dict[str, list[str]] = {}
+ self._combined_cache: dict[str, str] = {}
+- self._last_key = None # type: Optional[str]
+ if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], HTTPHeaders):
+ # Copy constructor
+ for k, v in args[0].get_all():
+@@ -219,7 +218,6 @@ class HTTPHeaders(StrMutableMapping):
+ if _FORBIDDEN_HEADER_CHARS_RE.search(value):
+ raise HTTPInputError("Invalid header value %r" % value)
+ norm_name = _normalize_header(name)
+- self._last_key = norm_name
+ if norm_name in self:
+ self._combined_cache.pop(norm_name, None)
+ self._as_list[norm_name].append(value)
+@@ -260,6 +258,11 @@ class HTTPHeaders(StrMutableMapping):
+ In Tornado 7.0, certain deprecated features of HTTP will become errors.
+ Specifically, line folding and the use of LF (with CR) as a line separator
+ will be removed.
++
++ .. versionchanged:: 6.6
++ Support for line folding was removed due to security/DoS concerns. This removal
++ was ahead of the original removal schedule announced for Tornado 7.0.
++ Support for LF without CR remains until Tornado 7.0.
+ """
+ if m := re.search(r"\r?\n$", line):
+ # RFC 9112 section 2.2: a recipient MAY recognize a single LF as a line
+@@ -270,27 +273,14 @@ class HTTPHeaders(StrMutableMapping):
+ # Empty line, or the final CRLF of a header block.
+ return
+ if line[0] in HTTP_WHITESPACE:
+- # continuation of a multi-line header
+- # TODO(7.0): Remove support for line folding.
+- if self._last_key is None:
+- raise HTTPInputError("first header line cannot start with whitespace")
+- new_part = " " + line.strip(HTTP_WHITESPACE)
+- if _chars_are_bytes:
+- if not _ABNF.field_value.fullmatch(new_part[1:]):
+- raise HTTPInputError("Invalid header continuation %r" % new_part)
+- else:
+- if _FORBIDDEN_HEADER_CHARS_RE.search(new_part):
+- raise HTTPInputError("Invalid header value %r" % new_part)
+- self._as_list[self._last_key][-1] += new_part
+- self._combined_cache.pop(self._last_key, None)
+- else:
+- try:
+- name, value = line.split(":", 1)
+- except ValueError:
+- raise HTTPInputError("no colon in header line")
+- self.add(
+- name, value.strip(HTTP_WHITESPACE), _chars_are_bytes=_chars_are_bytes
++ raise HTTPInputError(
++ "header line cannot start with whitespace, folding not supported"
+ )
++ try:
++ name, value = line.split(":", 1)
++ except ValueError:
++ raise HTTPInputError("no colon in header line")
++ self.add(name, value.strip(HTTP_WHITESPACE), _chars_are_bytes=_chars_are_bytes)
+
+ @classmethod
+ def parse(cls, headers: str, *, _chars_are_bytes: bool = True) -> "HTTPHeaders":
+diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py
+index 46300200..4cb2e850 100644
+--- a/tornado/test/httpclient_test.py
++++ b/tornado/test/httpclient_test.py
+@@ -734,10 +734,14 @@ def test_multi_line_headers(self):
+ % (resp.body, value, container),
+ )
+
+ def test_multi_line_headers(self):
+- # Multi-line http headers are rare but rfc-allowed
+- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
++ # Line folding is an obsolete way of representing headers split across multiple lines.
++ # https://www.rfc-editor.org/info/rfc9112/#section-5.2
++ #
++ # They must either be folded into a single line or rejected.
++ # Tornado 6.6 removed support for line folding in simple_httpclient, but they are
++ # still supported in libcurl.
+ sock, port = bind_unused_port()
+ with closing(sock):
+
+ @gen.coroutine
+@@ -760,10 +764,15 @@ def test_multi_line_headers(self):
+
+ netutil.add_accept_handler(sock, accept_callback) # type: ignore
+ try:
+ resp = self.fetch("http://127.0.0.1:%d/" % port)
+- resp.rethrow()
+- self.assertEqual(resp.headers["X-XSS-Protection"], "1; mode=block")
++ if self.http_client.__class__.__module__ == "tornado.curl_httpclient":
++ resp.rethrow()
++ self.assertEqual(
++ resp.headers["X-XSS-Protection"], "1; mode=block"
++ )
++ else:
++ self.assertEqual(resp.code, 599)
+ finally:
+ self.io_loop.remove_handler(sock.fileno())
+
+ @gen_test
+diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py
+index 0e9d5c41..40108d9c 100644
+--- a/tornado/test/httputil_test.py
++++ b/tornado/test/httputil_test.py
+@@ -384,50 +384,11 @@ Content-Disposition: form-data; name="files"; filename="ab.txt"
+
+
+ class HTTPHeadersTest(unittest.TestCase):
+- def test_multi_line(self):
+- # Lines beginning with whitespace are appended to the previous line
+- # with any leading whitespace replaced by a single space.
+- # Note that while multi-line headers are a part of the HTTP spec,
+- # their use is strongly discouraged.
+- data = """\
+-Foo: bar
+- baz
+-Asdf: qwer
+-\tzxcv
+-Foo: even
+- more
+- lines
+-""".replace(
+- "\n", "\r\n"
+- )
+- headers = HTTPHeaders.parse(data)
+- self.assertEqual(headers["asdf"], "qwer zxcv")
+- self.assertEqual(headers.get_list("asdf"), ["qwer zxcv"])
+- self.assertEqual(headers["Foo"], "bar baz,even more lines")
+- self.assertEqual(headers.get_list("foo"), ["bar baz", "even more lines"])
+- self.assertEqual(
+- sorted(list(headers.get_all())),
+- [("Asdf", "qwer zxcv"), ("Foo", "bar baz"), ("Foo", "even more lines")],
+- )
+- # Verify case insensitivity in-operator
+- self.assertTrue("asdf" in headers)
+- self.assertTrue("Asdf" in headers)
+-
+- def test_continuation(self):
++ def test_folding_rejected(self):
+ data = "Foo: bar\r\n\tasdf"
+- headers = HTTPHeaders.parse(data)
+- self.assertEqual(headers["Foo"], "bar asdf")
+-
+- # If the first line starts with whitespace, it's a
+- # continuation line with nothing to continue, so reject it
+- # (with a proper error).
+- data = " Foo: bar"
+- self.assertRaises(HTTPInputError, HTTPHeaders.parse, data)
+-
+- # \f (formfeed) is whitespace according to str.isspace, but
+- # not according to the HTTP spec.
+- data = "Foo: bar\r\n\fasdf"
+- self.assertRaises(HTTPInputError, HTTPHeaders.parse, data)
++ with self.assertRaises(HTTPInputError):
++ # Folded continuation lines are not supported.
++ HTTPHeaders.parse(data)
+
+ def test_forbidden_ascii_characters(self):
+ # Control characters and ASCII whitespace other than space, tab, and CRLF are not allowed in
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,99 @@
+From 09591f65286a600c2196df9c33b792b889a6bccb Mon Sep 17 00:00:00 2001
+From: Claude <noreply@anthropic.com>
+Date: Tue, 6 Oct 2026 14:14:19 +0000
+Subject: [PATCH] http1connection: Notify client delegate of header parse
+ errors
+
+Previously, an HTTPInputError raised while parsing response headers
+happened before the delegate was marked as needing on_connection_close,
+and close() cleared the stream's close callback, so simple_httpclient
+never completed the request and it hung until the timeout. Now the
+delegate is told the connection closed, matching the behavior for
+errors raised after headers_received.
+
+Update test_multi_line_headers to expect an error from simple_httpclient
+now that line folding is no longer supported (curl still accepts it).
+
+CVE: CVE-2026-116577
+Upstream-Status: Backport [https://github.com/tornadoweb/tornado/commit/09591f65286a600c2196df9c33b792b889a6bccb]
+
+Backport Changes:
+- Added a supports_line_folding flag to the shared test; Curl keeps the
+ acceptance check while Simple expects HTTPError for folded response headers.
+
+Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
+Claude-Session: https://claude.ai/code/session_016cNR4NyeuzQ1qFdqCk1yum
+Signed-off-by: Yogita Urade <yurade@cisco.com>
+---
+ tornado/http1connection.py | 5 +++++
+ tornado/test/httpclient_test.py | 19 +++++++++++++------
+ tornado/test/curl_httpclient_test.py | 2 ++
+ 3 files changed, 20 insertions(+), 6 deletions(-)
+
+diff --git a/tornado/http1connection.py b/tornado/http1connection.py
+--- a/tornado/http1connection.py
++++ b/tornado/http1connection.py
+@@ -316,6 +316,11 @@ async def _read_message
+ gen_log.info("Malformed HTTP message from %s: %s", self.context, e)
+ if not self.is_client:
+ await self.stream.write(b"HTTP/1.1 400 Bad Request\r\n\r\n")
++ elif not need_delegate_close:
++ # The error came before headers_received, so the delegate
++ # doesn't know about this message yet. Tell it the
++ # connection is closed so a client request doesn't hang.
++ need_delegate_close = True
+ self.close()
+ return False
+ finally:
+
+diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py
+--- a/tornado/test/httpclient_test.py
++++ b/tornado/test/httpclient_test.py
+@@ -263,6 +263,9 @@ class HTTPClientCommonTestCase
+
+
+ class HTTPClientCommonTestCase(AsyncHTTPTestCase):
++ # Set by subclasses whose client accepts obsolete line folding in
++ # response headers (see test_multi_line_headers).
++ supports_line_folding = False
+ # Set by subclasses whose client decodes ``Content-Encoding: br``. No
+ # implementation asks for brotli, so this is only true where the client
+ # decodes an encoding it did not request (see BrotliBombHandler).
+@@ -764,14 +765,18 @@ class HTTPClientCommonTestCase
+
+ netutil.add_accept_handler(sock, accept_callback) # type: ignore
+ try:
+- resp = self.fetch("http://127.0.0.1:%d/" % port)
+- if self.http_client.__class__.__module__ == "tornado.curl_httpclient":
++ if self.supports_line_folding:
++ resp = self.fetch("http://127.0.0.1:%d/" % port)
+ resp.rethrow()
+- self.assertEqual(
+- resp.headers["X-XSS-Protection"], "1; mode=block"
+- )
++ self.assertEqual(resp.headers["X-XSS-Protection"], "1; mode=block")
+ else:
+- self.assertEqual(resp.code, 599)
++ with ExpectLog(
++ gen_log, "Malformed HTTP message.*folding", level=logging.INFO
++ ):
++ with self.assertRaises(HTTPError):
++ self.fetch(
++ "http://127.0.0.1:%d/" % port, raise_error=True
++ )
+ finally:
+ self.io_loop.remove_handler(sock.fileno())
+
+
+diff --git a/tornado/test/curl_httpclient_test.py b/tornado/test/curl_httpclient_test.py
+--- a/tornado/test/curl_httpclient_test.py
++++ b/tornado/test/curl_httpclient_test.py
+@@ -45,6 +45,8 @@ class CurlHTTPClientCommonTestCase
+ # not we listed it in Accept-Encoding, so brotli is reachable whenever
+ # libcurl was built with it (which is common).
+ decompresses_brotli = pycurl is not None and "brotli" in pycurl.version
++ # libcurl still accepts obsolete line folding in response headers.
++ supports_line_folding = True
+
+ def get_http_client(self):
+ client = CurlAsyncHTTPClient(defaults=dict(allow_ipv6=False))
new file mode 100644
@@ -0,0 +1,101 @@
+From 3ef5ec3fc7a158ecd0ca7bfc5fcee42fa2773094 Mon Sep 17 00:00:00 2001
+From: Claude <noreply@anthropic.com>
+Date: Tue, 6 Oct 2026 15:28:59 +0000
+Subject: [PATCH] test: Accept either outcome in test_multi_line_headers
+
+Rather than declaring per-implementation whether line folding is
+supported (which was wrong when --httpclient changes the default
+client), accept either a correctly unfolded header or a 599 error.
+
+CVE: CVE-2026-116577
+Upstream-Status: Backport [https://github.com/tornadoweb/tornado/commit/3ef5ec3fc7a158ecd0ca7bfc5fcee42fa2773094]
+
+Backport Changes:
+- Rebased the test update on p2's 6.5.7-specific test body. Its fetch call and
+ error assertion use a different line layout, accounting for two additional
+ deleted lines in httpclient_test.py compared with upstream.
+
+Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
+Claude-Session: https://claude.ai/code/session_016cNR4NyeuzQ1qFdqCk1yum
+Signed-off-by: Yogita Urade <yurade@cisco.com>
+---
+ tornado/test/httpclient_test.py | 37 +++++++++++++++++-----------------
+ tornado/test/curl_httpclient_test.py | 2 --
+ 2 files changed, 19 insertions(+), 20 deletions(-)
+
+diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py
+index bb7de5c4..449f35a4 100644
+--- a/tornado/test/httpclient_test.py
++++ b/tornado/test/httpclient_test.py
+@@ -263,9 +263,6 @@ class HTTPClientCommonTestCase(AsyncHTTPTestCase):
+
+
+ class HTTPClientCommonTestCase(AsyncHTTPTestCase):
+- # Set by subclasses whose client accepts obsolete line folding in
+- # response headers (see test_multi_line_headers).
+- supports_line_folding = False
+ # Set by subclasses whose client decodes ``Content-Encoding: br``. No
+ # implementation asks for brotli, so this is only true where the client
+ # decodes an encoding it did not request (see BrotliBombHandler).
+@@ -741,9 +738,10 @@ def test_multi_line_headers(self):
+ # Line folding is an obsolete way of representing headers split across multiple lines.
+ # https://www.rfc-editor.org/info/rfc9112/#section-5.2
+ #
+- # They must either be folded into a single line or rejected.
+- # Tornado 6.6 removed support for line folding in simple_httpclient, but they are
+- # still supported in libcurl.
++ # The spec requires user agents to unfold them, but simple_httpclient
++ # rejects them as of Tornado 6.6 while libcurl still accepts them. Either
++ # outcome is acceptable here; what matters is that the client doesn't hang
++ # and doesn't misparse the header.
+ sock, port = bind_unused_port()
+ with closing(sock):
+
+@@ -767,18 +765,21 @@ def test_multi_line_headers(self):
+
+ netutil.add_accept_handler(sock, accept_callback) # type: ignore
+ try:
+- if self.supports_line_folding:
+- resp = self.fetch("http://127.0.0.1:%d/" % port)
+- resp.rethrow()
+- self.assertEqual(resp.headers["X-XSS-Protection"], "1; mode=block")
+- else:
+- with ExpectLog(
+- gen_log, "Malformed HTTP message.*folding", level=logging.INFO
+- ):
+- with self.assertRaises(HTTPError):
+- self.fetch(
+- "http://127.0.0.1:%d/" % port, raise_error=True
+- )
++ with ExpectLog(
++ gen_log,
++ "Malformed HTTP message.*folding",
++ level=logging.INFO,
++ required=False,
++ ):
++ try:
++ resp = self.fetch("http://127.0.0.1:%d/" % port)
++ except HTTPError as e:
++ self.assertEqual(e.code, 599)
++ else:
++ resp.rethrow()
++ self.assertEqual(
++ resp.headers["X-XSS-Protection"], "1; mode=block"
++ )
+ finally:
+ self.io_loop.remove_handler(sock.fileno())
+
+
+diff --git a/tornado/test/curl_httpclient_test.py b/tornado/test/curl_httpclient_test.py
+index 7a53f206..d7eb1adf 100644
+--- a/tornado/test/curl_httpclient_test.py
++++ b/tornado/test/curl_httpclient_test.py
+@@ -45,8 +45,6 @@ class CurlHTTPClientCommonTestCase
+ # not we listed it in Accept-Encoding, so brotli is reachable whenever
+ # libcurl was built with it (which is common).
+ decompresses_brotli = pycurl is not None and "brotli" in pycurl.version
+- # libcurl still accepts obsolete line folding in response headers.
+- supports_line_folding = True
+
+ def get_http_client(self):
+ client = CurlAsyncHTTPClient(defaults=dict(allow_ipv6=False))
@@ -16,6 +16,9 @@ SRC_URI += "file://CVE-2026-91990.patch \
file://CVE-2026-103263.patch \
file://CVE-2026-103263-dependent.patch \
file://CVE-2026-115927.patch \
+ file://CVE-2026-116577_p1.patch \
+ file://CVE-2026-116577_p2.patch \
+ file://CVE-2026-116577_p3.patch \
"
inherit pypi python_setuptools_build_meta
This patch applies the upstream fix as referenced in [4], using the commit shown in [1], [2] and [3]. [1] https://github.com/tornadoweb/tornado/commit/1b7a1dc9f340f52de929852e8125aa12f25297c9 [2] https://github.com/tornadoweb/tornado/commit/09591f65286a600c2196df9c33b792b889a6bccb [3] https://github.com/tornadoweb/tornado/commit/3ef5ec3fc7a158ecd0ca7bfc5fcee42fa2773094 [4] https://github.com/tornadoweb/tornado/pull/3765 [5] https://nvd.nist.gov/vuln/detail/CVE-2026-116577 Signed-off-by: Yogita Urade <yurade@cisco.com> --- .../python3-tornado/CVE-2026-116577_p1.patch | 195 ++++++++++++++++++ .../python3-tornado/CVE-2026-116577_p2.patch | 99 +++++++++ .../python3-tornado/CVE-2026-116577_p3.patch | 101 +++++++++ .../python/python3-tornado_6.5.7.bb | 3 + 4 files changed, 398 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p1.patch create mode 100644 meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p2.patch create mode 100644 meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p3.patch