]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-142881: Fix concurrent and reentrant call of atexit.unregister() (GH-142901)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 12 Jan 2026 08:45:10 +0000 (10:45 +0200)
committerGitHub <noreply@github.com>
Mon, 12 Jan 2026 08:45:10 +0000 (10:45 +0200)
Lib/test/_test_atexit.py
Misc/NEWS.d/next/Library/2025-12-17-20-18-17.gh-issue-142881.5IizIQ.rst [new file with mode: 0644]
Modules/atexitmodule.c

index 490b0686a0c1793240da3627d9817874ccb8f91b..2e961d6a4854a0b4607d72858267c885b17bfa6e 100644 (file)
@@ -148,6 +148,40 @@ class GeneralTest(unittest.TestCase):
                 atexit.unregister(Evil())
                 atexit._clear()
 
+    def test_eq_unregister(self):
+        # Issue #112127: callback's __eq__ may call unregister
+        def f1():
+            log.append(1)
+        def f2():
+            log.append(2)
+        def f3():
+            log.append(3)
+
+        class Pred:
+            def __eq__(self, other):
+                nonlocal cnt
+                cnt += 1
+                if cnt == when:
+                    atexit.unregister(what)
+                if other is f2:
+                    return True
+                return False
+
+        for what, expected in (
+                (f1, [3]),
+                (f2, [3, 1]),
+                (f3, [1]),
+            ):
+            for when in range(1, 4):
+                with self.subTest(what=what.__name__, when=when):
+                    cnt = 0
+                    log = []
+                    for f in (f1, f2, f3):
+                        atexit.register(f)
+                    atexit.unregister(Pred())
+                    atexit._run_exitfuncs()
+                    self.assertEqual(log, expected)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2025-12-17-20-18-17.gh-issue-142881.5IizIQ.rst b/Misc/NEWS.d/next/Library/2025-12-17-20-18-17.gh-issue-142881.5IizIQ.rst
new file mode 100644 (file)
index 0000000..02f22d3
--- /dev/null
@@ -0,0 +1 @@
+Fix concurrent and reentrant call of :func:`atexit.unregister`.
index f81f0b5724799bf5ab379c458d5edfc4eb8df335..1c901d9124d9cac3e194d3f80414039990f75df6 100644 (file)
@@ -256,22 +256,36 @@ atexit_ncallbacks(PyObject *module, PyObject *Py_UNUSED(dummy))
 static int
 atexit_unregister_locked(PyObject *callbacks, PyObject *func)
 {
-    for (Py_ssize_t i = 0; i < PyList_GET_SIZE(callbacks); ++i) {
+    for (Py_ssize_t i = PyList_GET_SIZE(callbacks) - 1; i >= 0; --i) {
         PyObject *tuple = Py_NewRef(PyList_GET_ITEM(callbacks, i));
         assert(PyTuple_CheckExact(tuple));
         PyObject *to_compare = PyTuple_GET_ITEM(tuple, 0);
         int cmp = PyObject_RichCompareBool(func, to_compare, Py_EQ);
-        Py_DECREF(tuple);
-        if (cmp < 0)
-        {
+        if (cmp < 0) {
+            Py_DECREF(tuple);
             return -1;
         }
         if (cmp == 1) {
             // We found a callback!
-            if (PyList_SetSlice(callbacks, i, i + 1, NULL) < 0) {
-                return -1;
+            // But its index could have changed if it or other callbacks were
+            // unregistered during the comparison.
+            Py_ssize_t j = PyList_GET_SIZE(callbacks) - 1;
+            j = Py_MIN(j, i);
+            for (; j >= 0; --j) {
+                if (PyList_GET_ITEM(callbacks, j) == tuple) {
+                    // We found the callback index! For real!
+                    if (PyList_SetSlice(callbacks, j, j + 1, NULL) < 0) {
+                        Py_DECREF(tuple);
+                        return -1;
+                    }
+                    i = j;
+                    break;
+                }
             }
-            --i;
+        }
+        Py_DECREF(tuple);
+        if (i >= PyList_GET_SIZE(callbacks)) {
+            i = PyList_GET_SIZE(callbacks);
         }
     }