diff --git a/Lib/test/_test_atexit.py b/Lib/test/_test_atexit.py index 490b0686a0c179..2e961d6a4854a0 100644 --- a/Lib/test/_test_atexit.py +++ b/Lib/test/_test_atexit.py @@ -148,6 +148,40 @@ def __eq__(self, other): 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 index 00000000000000..02f22d367bd831 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-17-20-18-17.gh-issue-142881.5IizIQ.rst @@ -0,0 +1 @@ +Fix concurrent and reentrant call of :func:`atexit.unregister`. diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index f81f0b5724799b..1c901d9124d9ca 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -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); } }