]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13][3.14] gh-143988: Fix re-entrant mutation crashes in socket sendmsg/recvmsg_int...
authorVictor Stinner <vstinner@python.org>
Wed, 10 Jun 2026 17:04:04 +0000 (19:04 +0200)
committerGitHub <noreply@github.com>
Wed, 10 Jun 2026 17:04:04 +0000 (19:04 +0200)
[3.14] gh-143988: Fix re-entrant mutation crashes in socket sendmsg/recvmsg_into (#143987) (#151251)

gh-143988: Fix re-entrant mutation crashes in socket sendmsg/recvmsg_into (#143987)

Fix crashes in socket.sendmsg() and socket.recvmsg_into() that could
occur if buffer sequences are mutated re-entrantly during argument
parsing via __buffer__ protocol callbacks.

The bug occurs because:

1. PySequence_Fast() returns the original list object when the input
   is already a list (not a copy).
2. During iteration, PyObject_GetBuffer() triggers __buffer__
   callbacks which may clear the list.
3. Subsequent iterations access invalid memory (heap OOB read).

The fix replaces PySequence_Fast() with PySequence_Tuple() which
always creates a new tuple, ensuring the sequence cannot be mutated
during iteration.

(cherry picked from commit 896f7fdc7d0ba6d4ace06935b9d67c4da0f9ecbe)

(cherry picked from commit 632daaf5e9fb172e3206c4c1f700a4490a2d6df3)

Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com>
Co-authored-by: tonghuaroot <23011166+tonghuaroot@users.noreply.github.com>
Lib/test/test_socket.py
Misc/NEWS.d/next/Library/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst [new file with mode: 0644]
Modules/socketmodule.c

index c6f6985faefd5f59d10db27c3d85bad20b3e89ff..342e1e03732236067a1cf5973c879b753fa879c7 100644 (file)
@@ -7284,6 +7284,62 @@ class SendRecvFdsTests(unittest.TestCase):
             self.assertEqual(data,  str(index).encode())
 
 
+class ReentrantMutationTests(unittest.TestCase):
+    """Regression tests for re-entrant mutation in sendmsg/recvmsg_into.
+
+    These tests verify that mutating sequences during argument parsing
+    via __buffer__ protocol does not cause crashes.
+
+    See: https://github.com/python/cpython/issues/143988
+    """
+
+    @unittest.skipUnless(hasattr(socket.socket, "sendmsg"),
+                         "sendmsg not supported")
+    def test_sendmsg_reentrant_data_mutation(self):
+        seq = []
+
+        class MutBuffer:
+            def __init__(self):
+                self.tripped = False
+
+            def __buffer__(self, flags):
+                if not self.tripped:
+                    self.tripped = True
+                    seq.clear()
+                return memoryview(b'Hello')
+
+        seq = [MutBuffer(), b'World', b'Test']
+
+        left, right = socket.socketpair()
+        with left, right:
+            left.sendmsg(seq)
+            self.assertEqual(right.recv(1024), b'HelloWorldTest')
+
+    @unittest.skipUnless(hasattr(socket.socket, "recvmsg_into"),
+                         "recvmsg_into not supported")
+    def test_recvmsg_into_reentrant_buffer_mutation(self):
+        seq = []
+        buf1 = bytearray(100)
+
+        class MutBuffer:
+            def __init__(self):
+                self.tripped = False
+
+            def __buffer__(self, flags):
+                if not self.tripped:
+                    self.tripped = True
+                    seq.clear()
+                return memoryview(buf1)
+
+        seq = [MutBuffer(), bytearray(100), bytearray(100)]
+
+        left, right = socket.socketpair()
+        with left, right:
+            left.send(b'Hello World!')
+            right.recvmsg_into(seq)
+        self.assertEqual(buf1, b'Hello World!'.ljust(100, b'\x00'))
+
+
 def setUpModule():
     thread_info = threading_helper.threading_setup()
     unittest.addModuleCleanup(threading_helper.threading_cleanup, *thread_info)
diff --git a/Misc/NEWS.d/next/Library/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst b/Misc/NEWS.d/next/Library/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst
new file mode 100644 (file)
index 0000000..fcc0cb5
--- /dev/null
@@ -0,0 +1,2 @@
+Fixed crashes in :meth:`socket.socket.sendmsg` and :meth:`socket.socket.recvmsg_into`
+that could occur if buffer sequences are concurrently mutated.
index 9cdf207e7a3380de927ce87d77926ff5548e906a..9abd94b9b905ea51a41679c814bc13488ff5d058 100644 (file)
@@ -4296,17 +4296,19 @@ sock_recvmsg_into(PySocketSockObject *s, PyObject *args)
     struct iovec *iovs = NULL;
     Py_ssize_t i, nitems, nbufs = 0;
     Py_buffer *bufs = NULL;
-    PyObject *buffers_arg, *fast, *retval = NULL;
+    PyObject *buffers_arg, *buffers_tuple, *retval = NULL;
 
     if (!PyArg_ParseTuple(args, "O|ni:recvmsg_into",
                           &buffers_arg, &ancbufsize, &flags))
         return NULL;
 
-    if ((fast = PySequence_Fast(buffers_arg,
-                                "recvmsg_into() argument 1 must be an "
-                                "iterable")) == NULL)
+    buffers_tuple = PySequence_Tuple(buffers_arg);
+    if (buffers_tuple == NULL) {
+        PyErr_SetString(PyExc_TypeError,
+                        "recvmsg_into() argument 1 must be an iterable");
         return NULL;
-    nitems = PySequence_Fast_GET_SIZE(fast);
+    }
+    nitems = PyTuple_GET_SIZE(buffers_tuple);
     if (nitems > INT_MAX) {
         PyErr_SetString(PyExc_OSError, "recvmsg_into() argument 1 is too long");
         goto finally;
@@ -4320,7 +4322,7 @@ sock_recvmsg_into(PySocketSockObject *s, PyObject *args)
         goto finally;
     }
     for (; nbufs < nitems; nbufs++) {
-        if (!PyArg_Parse(PySequence_Fast_GET_ITEM(fast, nbufs),
+        if (!PyArg_Parse(PyTuple_GET_ITEM(buffers_tuple, nbufs),
                          "w*;recvmsg_into() argument 1 must be an iterable "
                          "of single-segment read-write buffers",
                          &bufs[nbufs]))
@@ -4336,7 +4338,7 @@ finally:
         PyBuffer_Release(&bufs[i]);
     PyMem_Free(bufs);
     PyMem_Free(iovs);
-    Py_DECREF(fast);
+    Py_DECREF(buffers_tuple);
     return retval;
 }
 
@@ -4629,14 +4631,14 @@ sock_sendmsg_iovec(PySocketSockObject *s, PyObject *data_arg,
 
     /* Fill in an iovec for each message part, and save the Py_buffer
        structs to release afterwards. */
-    data_fast = PySequence_Fast(data_arg,
-                                "sendmsg() argument 1 must be an "
-                                "iterable");
+    data_fast = PySequence_Tuple(data_arg);
     if (data_fast == NULL) {
+        PyErr_SetString(PyExc_TypeError,
+                        "sendmsg() argument 1 must be an iterable");
         goto finally;
     }
 
-    ndataparts = PySequence_Fast_GET_SIZE(data_fast);
+    ndataparts = PyTuple_GET_SIZE(data_fast);
     if (ndataparts > INT_MAX) {
         PyErr_SetString(PyExc_OSError, "sendmsg() argument 1 is too long");
         goto finally;
@@ -4658,7 +4660,7 @@ sock_sendmsg_iovec(PySocketSockObject *s, PyObject *data_arg,
         }
     }
     for (; ndatabufs < ndataparts; ndatabufs++) {
-        if (!PyArg_Parse(PySequence_Fast_GET_ITEM(data_fast, ndatabufs),
+        if (!PyArg_Parse(PyTuple_GET_ITEM(data_fast, ndatabufs),
                          "y*;sendmsg() argument 1 must be an iterable of "
                          "bytes-like objects",
                          &databufs[ndatabufs]))