https://github.com/python/cpython/commit/cae853e3b44cd5cb033b904e163c490dd28bc30a
commit: cae853e3b44cd5cb033b904e163c490dd28bc30a
branch: main
author: Kumar Aditya <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2024-10-25T18:19:30+05:30
summary:

GH-125789: fix `fut._callbacks` to always return a copy of callbacks (#125922)

Fix `asyncio.Future._callbacks` to always return a copy of the internal list of 
callbacks to avoid mutation from user code affecting the internal state.

files:
A Misc/NEWS.d/next/Library/2024-10-24-14-08-10.gh-issue-125789.eaiAMw.rst
M Lib/test/test_asyncio/test_futures.py
M Modules/_asynciomodule.c

diff --git a/Lib/test/test_asyncio/test_futures.py 
b/Lib/test/test_asyncio/test_futures.py
index c566b28adb2408..b3517407e5394f 100644
--- a/Lib/test/test_asyncio/test_futures.py
+++ b/Lib/test/test_asyncio/test_futures.py
@@ -697,6 +697,24 @@ def test_future_del_segfault(self):
         with self.assertRaises(AttributeError):
             del fut._log_traceback
 
+    def test_callbacks_copy(self):
+        # See https://github.com/python/cpython/issues/125789
+        # In C implementation, the `_callbacks` attribute
+        # always returns a new list to avoid mutations of internal state
+
+        fut = self._new_future(loop=self.loop)
+        f1 = lambda _: 1
+        f2 = lambda _: 2
+        fut.add_done_callback(f1)
+        fut.add_done_callback(f2)
+        callbacks = fut._callbacks
+        self.assertIsNot(callbacks, fut._callbacks)
+        fut.remove_done_callback(f1)
+        callbacks = fut._callbacks
+        self.assertIsNot(callbacks, fut._callbacks)
+        fut.remove_done_callback(f2)
+        self.assertIsNone(fut._callbacks)
+
 
 @unittest.skipUnless(hasattr(futures, '_CFuture'),
                      'requires the C _asyncio module')
diff --git 
a/Misc/NEWS.d/next/Library/2024-10-24-14-08-10.gh-issue-125789.eaiAMw.rst 
b/Misc/NEWS.d/next/Library/2024-10-24-14-08-10.gh-issue-125789.eaiAMw.rst
new file mode 100644
index 00000000000000..964a006bb47b7b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-10-24-14-08-10.gh-issue-125789.eaiAMw.rst
@@ -0,0 +1 @@
+Fix possible crash when mutating list of callbacks returned by 
:attr:`!asyncio.Future._callbacks`. It now always returns a new copy in C 
implementation :mod:`!_asyncio`. Patch by Kumar Aditya.
diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c
index 0a769c46b87ac8..a7385b4c541df9 100644
--- a/Modules/_asynciomodule.c
+++ b/Modules/_asynciomodule.c
@@ -1265,52 +1265,49 @@ static PyObject *
 FutureObj_get_callbacks(FutureObj *fut, void *Py_UNUSED(ignored))
 {
     asyncio_state *state = get_asyncio_state_by_def((PyObject *)fut);
-    Py_ssize_t i;
-
     ENSURE_FUTURE_ALIVE(state, fut)
 
-    if (fut->fut_callback0 == NULL) {
-        if (fut->fut_callbacks == NULL) {
-            Py_RETURN_NONE;
-        }
-
-        return Py_NewRef(fut->fut_callbacks);
+    Py_ssize_t len = 0;
+    if (fut->fut_callback0 != NULL) {
+        len++;
     }
-
-    Py_ssize_t len = 1;
     if (fut->fut_callbacks != NULL) {
         len += PyList_GET_SIZE(fut->fut_callbacks);
     }
 
-
-    PyObject *new_list = PyList_New(len);
-    if (new_list == NULL) {
-        return NULL;
+    if (len == 0) {
+        Py_RETURN_NONE;
     }
 
-    PyObject *tup0 = PyTuple_New(2);
-    if (tup0 == NULL) {
-        Py_DECREF(new_list);
+    PyObject *callbacks = PyList_New(len);
+    if (callbacks == NULL) {
         return NULL;
     }
 
-    Py_INCREF(fut->fut_callback0);
-    PyTuple_SET_ITEM(tup0, 0, fut->fut_callback0);
-    assert(fut->fut_context0 != NULL);
-    Py_INCREF(fut->fut_context0);
-    PyTuple_SET_ITEM(tup0, 1, (PyObject *)fut->fut_context0);
-
-    PyList_SET_ITEM(new_list, 0, tup0);
+    Py_ssize_t i = 0;
+    if (fut->fut_callback0 != NULL) {
+        PyObject *tup0 = PyTuple_New(2);
+        if (tup0 == NULL) {
+            Py_DECREF(callbacks);
+            return NULL;
+        }
+        PyTuple_SET_ITEM(tup0, 0, Py_NewRef(fut->fut_callback0));
+        assert(fut->fut_context0 != NULL);
+        PyTuple_SET_ITEM(tup0, 1, Py_NewRef(fut->fut_context0));
+        PyList_SET_ITEM(callbacks, i, tup0);
+        i++;
+    }
 
     if (fut->fut_callbacks != NULL) {
-        for (i = 0; i < PyList_GET_SIZE(fut->fut_callbacks); i++) {
-            PyObject *cb = PyList_GET_ITEM(fut->fut_callbacks, i);
+        for (Py_ssize_t j = 0; j < PyList_GET_SIZE(fut->fut_callbacks); j++) {
+            PyObject *cb = PyList_GET_ITEM(fut->fut_callbacks, j);
             Py_INCREF(cb);
-            PyList_SET_ITEM(new_list, i + 1, cb);
+            PyList_SET_ITEM(callbacks, i, cb);
+            i++;
         }
     }
 
-    return new_list;
+    return callbacks;
 }
 
 static PyObject *

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to