From 2c8aad6e0182c11fadc1f8e39e3971ca63256c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Sun, 18 Jan 2026 14:57:14 +0800 Subject: [PATCH 1/8] gh-143988: Fix re-entrant mutation crashes in socket sendmsg/recvmsg_into via __buffer__ 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 vulnerability 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. This addresses two vulnerabilities related to gh-143637: - sendmsg() argument 1 (data buffers) - via __buffer__ - recvmsg_into() argument 1 (buffers) - via __buffer__ --- Lib/test/test_socket.py | 72 +++++++++++++++++++ ...-01-18-06-42-47.gh-issue-143988.MtLtCP.rst | 3 + Modules/socketmodule.c | 22 +++--- 3 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 934b7137096bc04..16c3cbe4fad4f72 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -7491,6 +7491,78 @@ def detach(): pass +class ReentrantMutationTests(unittest.TestCase): + """Regression tests for re-entrant mutation vulnerabilities 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): + # Test that sendmsg() handles re-entrant mutation of data buffers + # via __buffer__ protocol. + # See: https://github.com/python/cpython/issues/143988 + 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() + self.addCleanup(left.close) + self.addCleanup(right.close) + # Should not crash. With the fix, the call succeeds; + # without the fix, it would crash (SIGSEGV). + try: + left.sendmsg(seq) + except (TypeError, OSError): + pass # Also acceptable + + @unittest.skipUnless(hasattr(socket.socket, "recvmsg_into"), + "recvmsg_into not supported") + def test_recvmsg_into_reentrant_buffer_mutation(self): + # Test that recvmsg_into() handles re-entrant mutation of buffers + # via __buffer__ protocol. + # See: https://github.com/python/cpython/issues/143988 + seq = [] + + class MutBuffer: + def __init__(self, data): + self._data = bytearray(data) + self.tripped = False + + def __buffer__(self, flags): + if not self.tripped: + self.tripped = True + seq.clear() + return memoryview(self._data) + + seq = [MutBuffer(b'x' * 100), bytearray(100), bytearray(100)] + + left, right = socket.socketpair() + self.addCleanup(left.close) + self.addCleanup(right.close) + left.send(b'Hello World!') + # Should not crash. With the fix, the call succeeds; + # without the fix, it would crash (SIGSEGV). + try: + right.recvmsg_into(seq) + except (TypeError, OSError): + pass # Also acceptable + + def setUpModule(): thread_info = threading_helper.threading_setup() unittest.addModuleCleanup(threading_helper.threading_cleanup, *thread_info) diff --git a/Misc/NEWS.d/next/Security/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst b/Misc/NEWS.d/next/Security/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst new file mode 100644 index 000000000000000..08a83c3e9ae0ffd --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst @@ -0,0 +1,3 @@ +Fixed crashes in :meth:`socket.socket.sendmsg` and :meth:`socket.socket.recvmsg_into` +that could occur if buffer sequences are mutated re-entrantly during argument parsing +via ``__buffer__`` protocol callbacks. diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index ee78ad01598262b..ad3bdff1e0e9e52 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -4527,11 +4527,13 @@ sock_recvmsg_into(PyObject *self, PyObject *args) &buffers_arg, &ancbufsize, &flags)) return NULL; - if ((fast = PySequence_Fast(buffers_arg, - "recvmsg_into() argument 1 must be an " - "iterable")) == NULL) + fast = PySequence_Tuple(buffers_arg); + if (fast == 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(fast); if (nitems > INT_MAX) { PyErr_SetString(PyExc_OSError, "recvmsg_into() argument 1 is too long"); goto finally; @@ -4545,7 +4547,7 @@ sock_recvmsg_into(PyObject *self, PyObject *args) goto finally; } for (; nbufs < nitems; nbufs++) { - if (!PyArg_Parse(PySequence_Fast_GET_ITEM(fast, nbufs), + if (!PyArg_Parse(PyTuple_GET_ITEM(fast, nbufs), "w*;recvmsg_into() argument 1 must be an iterable " "of single-segment read-write buffers", &bufs[nbufs])) @@ -4854,14 +4856,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; @@ -4883,7 +4885,7 @@ sock_sendmsg_iovec(PySocketSockObject *s, PyObject *data_arg, } } for (; ndatabufs < ndataparts; ndatabufs++) { - if (PyObject_GetBuffer(PySequence_Fast_GET_ITEM(data_fast, ndatabufs), + if (PyObject_GetBuffer(PyTuple_GET_ITEM(data_fast, ndatabufs), &databufs[ndatabufs], PyBUF_SIMPLE) < 0) goto finally; iovs[ndatabufs].iov_base = databufs[ndatabufs].buf; From e0f92e6234ac507c619885669efb1e8254c77e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Mon, 19 Jan 2026 11:28:33 +0800 Subject: [PATCH 2/8] Simplify NEWS entry: use 'concurrently' instead of 're-entrantly' --- .../Security/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Security/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst b/Misc/NEWS.d/next/Security/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst index 08a83c3e9ae0ffd..fcc0cb54934b90e 100644 --- a/Misc/NEWS.d/next/Security/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst +++ b/Misc/NEWS.d/next/Security/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst @@ -1,3 +1,2 @@ Fixed crashes in :meth:`socket.socket.sendmsg` and :meth:`socket.socket.recvmsg_into` -that could occur if buffer sequences are mutated re-entrantly during argument parsing -via ``__buffer__`` protocol callbacks. +that could occur if buffer sequences are concurrently mutated. From 12c8272a0297210a509c0b44d069aabcc9138748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Mon, 19 Jan 2026 11:31:46 +0800 Subject: [PATCH 3/8] Simplify MutBuffer in recvmsg_into test: remove self._data --- Lib/test/test_socket.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 16c3cbe4fad4f72..16361a6527bee9a 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -7539,17 +7539,16 @@ def test_recvmsg_into_reentrant_buffer_mutation(self): seq = [] class MutBuffer: - def __init__(self, data): - self._data = bytearray(data) + def __init__(self): self.tripped = False def __buffer__(self, flags): if not self.tripped: self.tripped = True seq.clear() - return memoryview(self._data) + return memoryview(bytearray(100)) - seq = [MutBuffer(b'x' * 100), bytearray(100), bytearray(100)] + seq = [MutBuffer(), bytearray(100), bytearray(100)] left, right = socket.socketpair() self.addCleanup(left.close) From b578feb132c97da822612654e762379d2fc7a91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Mon, 19 Jan 2026 11:34:43 +0800 Subject: [PATCH 4/8] Remove unnecessary comments and try/except - calls succeed after fix --- Lib/test/test_socket.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 16361a6527bee9a..36fb5829b868b28 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -7523,12 +7523,7 @@ def __buffer__(self, flags): left, right = socket.socketpair() self.addCleanup(left.close) self.addCleanup(right.close) - # Should not crash. With the fix, the call succeeds; - # without the fix, it would crash (SIGSEGV). - try: - left.sendmsg(seq) - except (TypeError, OSError): - pass # Also acceptable + left.sendmsg(seq) @unittest.skipUnless(hasattr(socket.socket, "recvmsg_into"), "recvmsg_into not supported") @@ -7554,12 +7549,7 @@ def __buffer__(self, flags): self.addCleanup(left.close) self.addCleanup(right.close) left.send(b'Hello World!') - # Should not crash. With the fix, the call succeeds; - # without the fix, it would crash (SIGSEGV). - try: - right.recvmsg_into(seq) - except (TypeError, OSError): - pass # Also acceptable + right.recvmsg_into(seq) def setUpModule(): From 838183bc9e503d7130e47a431424a6f642d45b2c Mon Sep 17 00:00:00 2001 From: tonghuaroot <23011166+tonghuaroot@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:12:08 +0800 Subject: [PATCH 5/8] Address review: use context managers and assert transferred bytes --- Lib/test/test_socket.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 36fb5829b868b28..387165a90dafea0 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -7521,9 +7521,9 @@ def __buffer__(self, flags): seq = [MutBuffer(), b'World', b'Test'] left, right = socket.socketpair() - self.addCleanup(left.close) - self.addCleanup(right.close) - left.sendmsg(seq) + with left, right: + left.sendmsg(seq) + self.assertEqual(right.recv(1024), b'HelloWorldTest') @unittest.skipUnless(hasattr(socket.socket, "recvmsg_into"), "recvmsg_into not supported") @@ -7532,6 +7532,7 @@ def test_recvmsg_into_reentrant_buffer_mutation(self): # via __buffer__ protocol. # See: https://github.com/python/cpython/issues/143988 seq = [] + buf1 = bytearray(100) class MutBuffer: def __init__(self): @@ -7541,15 +7542,15 @@ def __buffer__(self, flags): if not self.tripped: self.tripped = True seq.clear() - return memoryview(bytearray(100)) + return memoryview(buf1) seq = [MutBuffer(), bytearray(100), bytearray(100)] left, right = socket.socketpair() - self.addCleanup(left.close) - self.addCleanup(right.close) - left.send(b'Hello World!') - right.recvmsg_into(seq) + with left, right: + left.send(b'Hello World!') + right.recvmsg_into(seq) + self.assertEqual(buf1, b'Hello World!'.ljust(100, b'\x00')) def setUpModule(): From 6e6ff76e0d8fa34ccfd9a9a2ea37bfc950ca9714 Mon Sep 17 00:00:00 2001 From: tonghuaroot <23011166+tonghuaroot@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:55:39 +0800 Subject: [PATCH 6/8] Address review: drop redundant test comments, rename data_fast to data_tuple The per-test comments duplicated the class docstring; the class-level description is enough. Rename data_fast to data_tuple since the value is now built by PySequence_Tuple(), so the old name was misleading. --- Lib/test/test_socket.py | 6 ------ Modules/socketmodule.c | 12 ++++++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 387165a90dafea0..a4797a67962929b 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -7503,9 +7503,6 @@ class ReentrantMutationTests(unittest.TestCase): @unittest.skipUnless(hasattr(socket.socket, "sendmsg"), "sendmsg not supported") def test_sendmsg_reentrant_data_mutation(self): - # Test that sendmsg() handles re-entrant mutation of data buffers - # via __buffer__ protocol. - # See: https://github.com/python/cpython/issues/143988 seq = [] class MutBuffer: @@ -7528,9 +7525,6 @@ def __buffer__(self, flags): @unittest.skipUnless(hasattr(socket.socket, "recvmsg_into"), "recvmsg_into not supported") def test_recvmsg_into_reentrant_buffer_mutation(self): - # Test that recvmsg_into() handles re-entrant mutation of buffers - # via __buffer__ protocol. - # See: https://github.com/python/cpython/issues/143988 seq = [] buf1 = bytearray(100) diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index ad3bdff1e0e9e52..dd4b172bc428718 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -4851,19 +4851,19 @@ sock_sendmsg_iovec(PySocketSockObject *s, PyObject *data_arg, Py_ssize_t ndataparts, ndatabufs = 0; int result = -1; struct iovec *iovs = NULL; - PyObject *data_fast = NULL; + PyObject *data_tuple = NULL; Py_buffer *databufs = NULL; /* Fill in an iovec for each message part, and save the Py_buffer structs to release afterwards. */ - data_fast = PySequence_Tuple(data_arg); - if (data_fast == NULL) { + data_tuple = PySequence_Tuple(data_arg); + if (data_tuple == NULL) { PyErr_SetString(PyExc_TypeError, "sendmsg() argument 1 must be an iterable"); goto finally; } - ndataparts = PyTuple_GET_SIZE(data_fast); + ndataparts = PyTuple_GET_SIZE(data_tuple); if (ndataparts > INT_MAX) { PyErr_SetString(PyExc_OSError, "sendmsg() argument 1 is too long"); goto finally; @@ -4885,7 +4885,7 @@ sock_sendmsg_iovec(PySocketSockObject *s, PyObject *data_arg, } } for (; ndatabufs < ndataparts; ndatabufs++) { - if (PyObject_GetBuffer(PyTuple_GET_ITEM(data_fast, ndatabufs), + if (PyObject_GetBuffer(PyTuple_GET_ITEM(data_tuple, ndatabufs), &databufs[ndatabufs], PyBUF_SIMPLE) < 0) goto finally; iovs[ndatabufs].iov_base = databufs[ndatabufs].buf; @@ -4895,7 +4895,7 @@ sock_sendmsg_iovec(PySocketSockObject *s, PyObject *data_arg, finally: *databufsout = databufs; *ndatabufsout = ndatabufs; - Py_XDECREF(data_fast); + Py_XDECREF(data_tuple); return result; } From 74b5139d7ca5c307feedf291599633216fa2e8d2 Mon Sep 17 00:00:00 2001 From: tonghuaroot <23011166+tonghuaroot@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:15:17 +0800 Subject: [PATCH 7/8] Keep variable name data_fast per review @vstinner prefers the original name; revert the rename and keep the diff minimal. --- Modules/socketmodule.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index dd4b172bc428718..ad3bdff1e0e9e52 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -4851,19 +4851,19 @@ sock_sendmsg_iovec(PySocketSockObject *s, PyObject *data_arg, Py_ssize_t ndataparts, ndatabufs = 0; int result = -1; struct iovec *iovs = NULL; - PyObject *data_tuple = NULL; + PyObject *data_fast = NULL; Py_buffer *databufs = NULL; /* Fill in an iovec for each message part, and save the Py_buffer structs to release afterwards. */ - data_tuple = PySequence_Tuple(data_arg); - if (data_tuple == NULL) { + data_fast = PySequence_Tuple(data_arg); + if (data_fast == NULL) { PyErr_SetString(PyExc_TypeError, "sendmsg() argument 1 must be an iterable"); goto finally; } - ndataparts = PyTuple_GET_SIZE(data_tuple); + ndataparts = PyTuple_GET_SIZE(data_fast); if (ndataparts > INT_MAX) { PyErr_SetString(PyExc_OSError, "sendmsg() argument 1 is too long"); goto finally; @@ -4885,7 +4885,7 @@ sock_sendmsg_iovec(PySocketSockObject *s, PyObject *data_arg, } } for (; ndatabufs < ndataparts; ndatabufs++) { - if (PyObject_GetBuffer(PyTuple_GET_ITEM(data_tuple, ndatabufs), + if (PyObject_GetBuffer(PyTuple_GET_ITEM(data_fast, ndatabufs), &databufs[ndatabufs], PyBUF_SIMPLE) < 0) goto finally; iovs[ndatabufs].iov_base = databufs[ndatabufs].buf; @@ -4895,7 +4895,7 @@ sock_sendmsg_iovec(PySocketSockObject *s, PyObject *data_arg, finally: *databufsout = databufs; *ndatabufsout = ndatabufs; - Py_XDECREF(data_tuple); + Py_XDECREF(data_fast); return result; } From 1056648ca886b5cc042ddb9da801c9ee3374c07a Mon Sep 17 00:00:00 2001 From: tonghuaroot <23011166+tonghuaroot@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:20:29 +0800 Subject: [PATCH 8/8] Address review: rename recvmsg buffers_tuple, trim docstring, move NEWS to Library - Rename sock_recvmsg_into's fast to buffers_tuple (per @vstinner). - Trim ReentrantMutationTests docstring to fit 80 columns. - This is a crash-robustness fix, not a security vulnerability; move the NEWS entry from Security/ to Library/. --- Lib/test/test_socket.py | 2 +- .../2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst | 0 Modules/socketmodule.c | 12 ++++++------ 3 files changed, 7 insertions(+), 7 deletions(-) rename Misc/NEWS.d/next/{Security => Library}/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst (100%) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index a4797a67962929b..585232476056ef5 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -7492,7 +7492,7 @@ def detach(): class ReentrantMutationTests(unittest.TestCase): - """Regression tests for re-entrant mutation vulnerabilities in sendmsg/recvmsg_into. + """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. diff --git a/Misc/NEWS.d/next/Security/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 similarity index 100% rename from Misc/NEWS.d/next/Security/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst rename to Misc/NEWS.d/next/Library/2026-01-18-06-42-47.gh-issue-143988.MtLtCP.rst diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index ad3bdff1e0e9e52..5081e79f12c9132 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -4521,19 +4521,19 @@ sock_recvmsg_into(PyObject *self, 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; - fast = PySequence_Tuple(buffers_arg); - if (fast == 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 = PyTuple_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; @@ -4547,7 +4547,7 @@ sock_recvmsg_into(PyObject *self, PyObject *args) goto finally; } for (; nbufs < nitems; nbufs++) { - if (!PyArg_Parse(PyTuple_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])) @@ -4563,7 +4563,7 @@ sock_recvmsg_into(PyObject *self, PyObject *args) PyBuffer_Release(&bufs[i]); PyMem_Free(bufs); PyMem_Free(iovs); - Py_DECREF(fast); + Py_DECREF(buffers_tuple); return retval; }