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/4] 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 934b7137096bc0..16c3cbe4fad4f7 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 00000000000000..08a83c3e9ae0ff --- /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 ee78ad01598262..ad3bdff1e0e9e5 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/4] 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 08a83c3e9ae0ff..fcc0cb54934b90 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/4] 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 16c3cbe4fad4f7..16361a6527bee9 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/4] 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 16361a6527bee9..36fb5829b868b2 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():