Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions Lib/test/test_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -3573,6 +3573,41 @@ class SendmsgStreamTests(SendmsgTests):
# Tests for sendmsg() which require a stream socket and do not
# involve recvmsg() or recvmsg_into().

@unittest.skipUnless(hasattr(socket.socket, "sendmsg"),
"sendmsg not supported")
def test_sendmsg_reentrant_ancillary_mutation(self):
self._test_sendmsg_reentrant_ancillary_mutation()

def _test_sendmsg_reentrant_ancillary_mutation(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a private method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a helper method rather than a standalone test because it depends on the surrounding test class setup and socket lifecycle, and is not meant to be picked up directly by unittest discovery.
The skipUnless(sendmsg) decorator is placed here so the helper only runs on platforms that support sendmsg(),with execution still controlled by the enclosing test logic.
I can add a small public test_* wrapper calling this helper if that’s prefferable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But where is it tested then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the confusion, you are right, I have missed it- I have added a public test method that invokes _test_sendmsg_reentrant_ancillary_mutation in my updated PR is it correct?, Thanks for the clarification.

import socket

seq = []

class Mut:
def __init__(self):
self.tripped = False
def __index__(self):
if not self.tripped:
self.tripped = True
seq.clear()
return 0

seq[:] = [
(socket.SOL_SOCKET, Mut(), b'x'),
(socket.SOL_SOCKET, 0, b'x'),
]

left, right = socket.socketpair()
self.addCleanup(left.close)
self.addCleanup(right.close)

self.assertRaises(
(TypeError, OSError),
left.sendmsg,
[b'x'],
seq,
)

def testSendmsgExplicitNoneAddr(self):
# Check that peer address can be specified as None.
self.assertEqual(self.serv_sock.recv(len(MSG)), MSG)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed a crash in socket.sendmsg() that could occur if ancillary data is mutated re-entrantly during argument parsing.
31 changes: 17 additions & 14 deletions Modules/socketmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -4977,11 +4977,13 @@ _socket_socket_sendmsg_impl(PySocketSockObject *s, PyObject *data_arg,
if (cmsg_arg == NULL)
ncmsgs = 0;
else {
if ((cmsg_fast = PySequence_Fast(cmsg_arg,
"sendmsg() argument 2 must be an "
"iterable")) == NULL)
cmsg_fast = PySequence_Tuple(cmsg_arg);
if (cmsg_fast == NULL) {
PyErr_SetString(PyExc_TypeError,
"sendmsg() argument 2 must be an iterable");
goto finally;
ncmsgs = PySequence_Fast_GET_SIZE(cmsg_fast);
}
ncmsgs = PyTuple_GET_SIZE(cmsg_fast);
}

#ifndef CMSG_SPACE
Expand All @@ -5001,20 +5003,21 @@ _socket_socket_sendmsg_impl(PySocketSockObject *s, PyObject *data_arg,
controllen = controllen_last = 0;
while (ncmsgbufs < ncmsgs) {
size_t bufsize, space;
PyObject *item;

if (!PyArg_Parse(PySequence_Fast_GET_ITEM(cmsg_fast, ncmsgbufs),
"(iiy*):[sendmsg() ancillary data items]",
&cmsgs[ncmsgbufs].level,
&cmsgs[ncmsgbufs].type,
&cmsgs[ncmsgbufs].data))
item = PyTuple_GET_ITEM(cmsg_fast, ncmsgbufs);

if (!PyArg_Parse(item,
"(iiy*):[sendmsg() ancillary data items]",
&cmsgs[ncmsgbufs].level,
&cmsgs[ncmsgbufs].type,
&cmsgs[ncmsgbufs].data)){
goto finally;
}

bufsize = cmsgs[ncmsgbufs++].data.len;

#ifdef CMSG_SPACE
if (!get_CMSG_SPACE(bufsize, &space)) {
#else
if (!get_CMSG_LEN(bufsize, &space)) {
#endif
if(!get_CMSG_SPACE(bufsize, &space)){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value is only used to decide how much memory to allocate for msg.msg_control.
get_CMSG_LEN() can give a smaller size on some systems because it doesnt include extra padding that the OS needs which means the buffer can be too small.
get_CMSG_SPACE() includes this padding, so it correctly calculates how much space is needed. This function already uses get_CMSG_SPACE() elsewhere for the same reason, so using it here keeps the logic consistent and safe.

Copy link
Member

@picnixz picnixz Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is not necessarily present though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_CMSG_SPACE() here is not the platform macro. It’s a CPython helper defined in socketmodule.c itself, so it is always available when this code is compiled. Internally, that helper already handles platform differences and falls back appropriately when CMSG_SPACE is not provided by the system. So using get_CMSG_SPACE() unconditionally here doesn’t introduce a new dependency, it just reuses the same helper that this file already relies on in other places.

PyErr_SetString(PyExc_OSError, "ancillary data item too large");
goto finally;
}
Expand Down
Loading