From 96dc88c2a05705287856b2cd6b4b4034f9a6aaac Mon Sep 17 00:00:00 2001
From: Ben Darnell <ben@bendarnell.com>
Date: Tue, 26 May 2026 21:17:59 -0400
Subject: [PATCH] speedups: validate mask length

The lack of this check permitted a read of up to 3 bytes past the end
of the string in some cases.
---
 tornado/speedups.c             | 67 ++++++++++++++++++++--------------
 tornado/test/websocket_test.py |  7 ++++
 tornado/util.py                |  4 +-
 3 files changed, 50 insertions(+), 28 deletions(-)

Index: tornado-6.4/tornado/speedups.c
===================================================================
--- tornado-6.4.orig/tornado/speedups.c
+++ tornado-6.4/tornado/speedups.c
@@ -17,6 +17,11 @@ static PyObject* websocket_mask(PyObject
         return NULL;
     }
 
+    if (mask_len != 4) {
+        PyErr_SetString(PyExc_ValueError, "mask must be 4 bytes");
+        return NULL;
+    }
+
     uint32_mask = ((uint32_t*)mask)[0];
 
     result = PyBytes_FromStringAndSize(NULL, data_len);
Index: tornado-6.4/tornado/test/websocket_test.py
===================================================================
--- tornado-6.4.orig/tornado/test/websocket_test.py
+++ tornado-6.4/tornado/test/websocket_test.py
@@ -788,12 +788,18 @@ class MaskFunctionMixin(object):
             b"\xff\xfa\xff\xff\xfb\xfe",
         )
 
+    def test_length_validation(self: typing.Any):
+        # Test all lengths of mask that are not 4 bytes.
+        for mask in (b"", b"a", b"ab", b"abc", b"abcde", b"abcdef"):
+            with self.subTest(mask=mask):
+                with self.assertRaises(ValueError):
+                    self.mask(mask, b"data asdf")
+
 
 class PythonMaskFunctionTest(MaskFunctionMixin, unittest.TestCase):
     def mask(self, mask, data):
         return _websocket_mask_python(mask, data)
 
-
 @unittest.skipIf(speedups is None, "tornado.speedups module not present")
 class CythonMaskFunctionTest(MaskFunctionMixin, unittest.TestCase):
     def mask(self, mask, data):
Index: tornado-6.4/tornado/util.py
===================================================================
--- tornado-6.4.orig/tornado/util.py
+++ tornado-6.4/tornado/util.py
@@ -162,7 +162,7 @@ def exec_in(
 
 
 def raise_exc_info(
-    exc_info: Tuple[Optional[type], Optional[BaseException], Optional["TracebackType"]]
+    exc_info: Tuple[Optional[type], Optional[BaseException], Optional["TracebackType"]],
 ) -> typing.NoReturn:
     try:
         if exc_info[1] is not None:
@@ -435,6 +435,8 @@ def _websocket_mask_python(mask: bytes,
 
     This pure-python implementation may be replaced by an optimized version when available.
     """
+    if len(mask) != 4:
+        raise ValueError("mask must be 4 bytes")
     mask_arr = array.array("B", mask)
     unmasked_arr = array.array("B", data)
     for i in range(len(data)):
