diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 14e4620669491f..285bcac65ab57b 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -5598,6 +5598,18 @@ def msg_cb(conn, direction, version, content_type, msg_type, data): with self.assertRaises(TypeError): client_context._msg_callback = object() + # the attribute of the underlying C type accepts only a callable + # and cannot be deleted + descr = _ssl._SSLContext.__dict__['_msg_callback'] + with self.assertRaises(TypeError): + descr.__set__(client_context, object()) + # a failed assignment does not change the value + self.assertIs(client_context._msg_callback, msg_cb) + with self.assertRaisesRegex(AttributeError, 'cannot be deleted'): + descr.__delete__(client_context) + # a failed deletion does not change the value + self.assertIs(client_context._msg_callback, msg_cb) + def test_msg_callback_exception(self): client_context, server_context, hostname = testing_context() diff --git a/Misc/NEWS.d/next/Library/2026-08-21-13-30-00.gh-issue-156166.Xv8pQm.rst b/Misc/NEWS.d/next/Library/2026-08-21-13-30-00.gh-issue-156166.Xv8pQm.rst new file mode 100644 index 00000000000000..2417479d333e3e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-21-13-30-00.gh-issue-156166.Xv8pQm.rst @@ -0,0 +1,3 @@ +:mod:`ssl`: A failed assignment or deletion of the ``_msg_callback`` +attribute of :class:`ssl.SSLContext` no longer removes the current callback. +Deleting it now raises :exc:`AttributeError` instead of :exc:`TypeError`. diff --git a/Modules/_ssl/debughelpers.c b/Modules/_ssl/debughelpers.c index b2d552f97e5b0e..e8da76907971ed 100644 --- a/Modules/_ssl/debughelpers.c +++ b/Modules/_ssl/debughelpers.c @@ -102,20 +102,28 @@ _PySSLContext_set_msg_callback(PyObject *op, PyObject *arg, void *Py_UNUSED(closure)) { PySSLContext *self = PySSLContext_CAST(op); - Py_CLEAR(self->msg_cb); + if (arg == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_msg_callback' of '%.100s' objects " + "cannot be deleted", Py_TYPE(op)->tp_name); + return -1; + } + if (arg != Py_None && !PyCallable_Check(arg)) { + PyErr_SetString(PyExc_TypeError, + "not a callable object"); + return -1; + } + /* Releasing the old callback can run arbitrary code. */ + PyObject *old_cb = self->msg_cb; if (arg == Py_None) { + self->msg_cb = NULL; SSL_CTX_set_msg_callback(self->ctx, NULL); } else { - if (!PyCallable_Check(arg)) { - SSL_CTX_set_msg_callback(self->ctx, NULL); - PyErr_SetString(PyExc_TypeError, - "not a callable object"); - return -1; - } self->msg_cb = Py_NewRef(arg); SSL_CTX_set_msg_callback(self->ctx, _PySSL_msg_callback); } + Py_XDECREF(old_cb); return 0; }